Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
f3c3df8
init
skoob13 Dec 9, 2025
11f844c
fix: electron deps
skoob13 Dec 9, 2025
b4de0da
modules
skoob13 Dec 9, 2025
c68999e
deps again
skoob13 Dec 9, 2025
578efd2
auth
skoob13 Dec 9, 2025
0733cfd
Bativewind init
a-lider Dec 9, 2025
8465b1a
fix: nativewind
skoob13 Dec 9, 2025
6449458
Bump reanimated version
a-lider Dec 9, 2025
1fcaadb
wip fonts
skoob13 Dec 9, 2025
80bc59f
Merge branch 'auth' into mobile-app
skoob13 Dec 9, 2025
0be1980
feat: fonts
skoob13 Dec 9, 2025
b58912d
biome
skoob13 Dec 9, 2025
1ed0016
Restructure project
a-lider Dec 9, 2025
173dd77
Fix classname sorting and OAuth callback
a-lider Dec 9, 2025
d7de818
fix OAuth token exchange
a-lider Dec 9, 2025
6db5a5a
streaming wip
skoob13 Dec 9, 2025
8dda38d
Merge branch 'streaming' into mobile-app
skoob13 Dec 9, 2025
ecae512
Update app schema to posthog
a-lider Dec 9, 2025
54f4b2a
streaming
skoob13 Dec 9, 2025
3bc5d58
Merge branch 'mobile-app' of github.com:PostHog/Array into mobile-app
skoob13 Dec 9, 2025
2eef20d
feat: agent chat
jonathanlab Dec 9, 2025
c4156e3
undo mode switch changes
jonathanlab Dec 9, 2025
a4d313c
undo package bumps for array
jonathanlab Dec 9, 2025
25ae735
Add tabs navigation - first draft
a-lider Dec 9, 2025
b25cdf6
Use streaming
a-lider Dec 9, 2025
28f9b8b
Tabs navigation
a-lider Dec 9, 2025
0b9a4c6
Tabs with action button
a-lider Dec 9, 2025
a2f4e56
Create task button on the main screen instead of tabs
a-lider Dec 9, 2025
2e1315c
bump back
jonathanlab Dec 9, 2025
ab6f439
Merge branch 'mobile-app' of github.com:PostHog/Array into mobile-app
jonathanlab Dec 9, 2025
169d400
Chat UI - first draft with components and keyboard-controller
a-lider Dec 9, 2025
992c15a
query
skoob13 Dec 9, 2025
390c309
Merge branch 'mobile-app' of github.com:PostHog/Array into mobile-app
skoob13 Dec 9, 2025
3e78e75
Make both PostHog AI chat and Array tasks
a-lider Dec 9, 2025
e428698
feat: tasks list
skoob13 Dec 10, 2025
0183b9f
feat: list of tasks and convos
skoob13 Dec 10, 2025
d9b3787
feat: render tool calls
skoob13 Dec 10, 2025
fcd82ed
try to fix infinite rerender in virutalizedlist
jonathanlab Dec 10, 2025
2f243d5
Merge branch 'mobile-app' of github.com:PostHog/Array into mobile-app
jonathanlab Dec 10, 2025
43f5aa8
prevent picking up electron react
jonathanlab Dec 10, 2025
007c7f8
Update the styles to match Array app
a-lider Dec 10, 2025
5bcf116
Update loading and streaming styles
a-lider Dec 10, 2025
aea34e5
tasks component
skoob13 Dec 10, 2025
5b491d5
Merge branch 'mobile-app' of github.com:PostHog/Array into mobile-app
skoob13 Dec 10, 2025
5454e3b
new task
skoob13 Dec 10, 2025
f12ae66
cleanup
skoob13 Dec 10, 2025
388a9f8
Liquid glass chat input
a-lider Dec 10, 2025
b0684b3
Liquid glass mic and send button
a-lider Dec 10, 2025
f215bf4
better sync/persistence for sessions
jonathanlab Dec 10, 2025
d39747d
Merge branch 'mobile-app' of github.com:PostHog/Array into mobile-app
jonathanlab Dec 10, 2025
88e2355
voice mode
skoob13 Dec 10, 2025
825a6e6
Merge branch 'mobile-app' of github.com:PostHog/Array into mobile-app
skoob13 Dec 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/bin/bash

pnpm lint-staged
# pnpm lint-staged
3 changes: 2 additions & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node-linker=hoisted
node-linker=hoisted
shamefully-hoist=true
10 changes: 5 additions & 5 deletions apps/array/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^20.19.21",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/ui": "^4.0.10",
Expand All @@ -57,7 +57,7 @@
"knip": "^5.66.3",
"lint-staged": "^15.5.2",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"tailwindcss": "^3.4.18",
"tsx": "^4.20.6",
"typed-openapi": "^2.2.2",
"typescript": "^5.9.3",
Expand Down Expand Up @@ -134,8 +134,8 @@
"posthog-js": "^1.283.0",
"posthog-node": "^4.18.0",
"radix-themes-tw": "0.2.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.64.0",
"react-hotkeys-hook": "^4.4.4",
"react-markdown": "^10.1.0",
Expand Down
40 changes: 38 additions & 2 deletions apps/array/src/main/services/folders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { RegisteredFolder } from "../../shared/types";
import { generateId } from "../../shared/utils/id";
import { createIpcHandler } from "../lib/ipcHandler";
import { logger } from "../lib/logger";
import { isGitRepository } from "./git";
import { getRemoteUrl, isGitRepository, parseGitHubUrl } from "./git";
import { getWorktreeLocation } from "./settingsStore";
import { clearAllStoreData, foldersStore } from "./store";
import { deleteWorktreeIfExists } from "./worktreeUtils";
Expand All @@ -25,8 +25,38 @@ function extractFolderName(folderPath: string): string {
return path.basename(folderPath);
}

async function getRepositoryString(folderPath: string): Promise<string | undefined> {
try {
const remoteUrl = await getRemoteUrl(folderPath);
if (!remoteUrl) return undefined;

const parsed = parseGitHubUrl(remoteUrl);
if (!parsed) return undefined;

return `${parsed.organization}/${parsed.repository}`;
} catch {
return undefined;
}
}

async function getFolders(): Promise<RegisteredFolder[]> {
return foldersStore.get("folders", []);
const folders = foldersStore.get("folders", []);

let needsUpdate = false;
for (const folder of folders) {
if (!folder.repository) {
folder.repository = await getRepositoryString(folder.path);
if (folder.repository) {
needsUpdate = true;
}
}
}

if (needsUpdate) {
foldersStore.set("folders", folders);
}

return folders;
}

async function addFolder(folderPath: string): Promise<RegisteredFolder> {
Expand All @@ -35,16 +65,22 @@ async function addFolder(folderPath: string): Promise<RegisteredFolder> {
const existing = folders.find((f) => f.path === folderPath);
if (existing) {
existing.lastAccessed = new Date().toISOString();
if (!existing.repository) {
existing.repository = await getRepositoryString(folderPath);
}
foldersStore.set("folders", folders);
return existing;
}

const repository = await getRepositoryString(folderPath);

const newFolder: RegisteredFolder = {
id: generateFolderId(),
path: folderPath,
name: extractFolderName(folderPath),
lastAccessed: new Date().toISOString(),
createdAt: new Date().toISOString(),
repository,
};

folders.push(newFolder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
} from "react";

Expand Down Expand Up @@ -34,15 +35,32 @@ export function VirtualizedList<T>({
const isAtBottomRef = useRef(true);
const isInitialMountRef = useRef(true);

const itemsRef = useRef(items);
const getItemKeyRef = useRef(getItemKey);
itemsRef.current = items;
getItemKeyRef.current = getItemKey;

const getScrollElement = useCallback(() => scrollRef.current, []);
const getEstimateSize = useCallback(() => estimateSize, [estimateSize]);

const hasGetItemKey = getItemKey !== undefined;
const stableGetItemKey = useMemo(() => {
if (!hasGetItemKey) return undefined;
return (index: number) => {
const currentItems = itemsRef.current;
const currentGetKey = getItemKeyRef.current;
if (!currentGetKey || !currentItems[index]) return index;
return currentGetKey(currentItems[index], index);
};
}, [hasGetItemKey]);

const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => scrollRef.current,
estimateSize: () => estimateSize,
getScrollElement,
estimateSize: getEstimateSize,
overscan,
gap,
getItemKey: getItemKey
? (index) => getItemKey(items[index], index)
: undefined,
getItemKey: stableGetItemKey,
});

const handleScroll = useCallback(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
events: [...state.sessions[session.taskRunId].events, userEvent],
processedLineCount:
(state.sessions[session.taskRunId].processedLineCount ?? 0) + 1,
isPromptPending: true,
},
},
}));
Expand Down Expand Up @@ -482,6 +483,41 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
const session = get().getSessionForTask(taskId);
if (!session) return false;

if (session.isCloud) {
const authState = useAuthStore.getState();
const { client } = authState;
if (!client) {
log.error("API client not available for cloud cancel");
return false;
}

const cancelNotification: StoredLogEntry = {
type: "notification" as const,
timestamp: new Date().toISOString(),
direction: "client" as const,
notification: {
method: "session/cancel",
params: {
sessionId: session.taskRunId,
},
},
};

try {
await client.appendTaskRunLog(taskId, session.taskRunId, [
cancelNotification,
]);
log.info("Sent cloud cancel request via S3", {
taskId,
runId: session.taskRunId,
});
return true;
} catch (error) {
log.error("Failed to send cloud cancel request", error);
return false;
}
}

try {
return await window.electronAPI.agentCancelPrompt(session.taskRunId);
} catch (error) {
Expand Down Expand Up @@ -552,6 +588,8 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
const processedCount = session.processedLineCount ?? 0;
if (lines.length > processedCount) {
const newLines = lines.slice(processedCount);
let receivedAgentMessage = false;

for (const line of newLines) {
try {
const entry = JSON.parse(line);
Expand Down Expand Up @@ -581,19 +619,32 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
.params as SessionNotification,
};
get()._handleEvent(taskRunId, sessionUpdateEvent);

// Check if this is an agent message - means agent is responding
const sessionUpdate =
entry.notification?.params?.update?.sessionUpdate;
if (
sessionUpdate === "agent_message_chunk" ||
sessionUpdate === "agent_thought_chunk"
) {
receivedAgentMessage = true;
}
}
} catch {
// Skip invalid JSON
}
}

// Update processed line count
// Update processed line count and clear pending state if agent responded
set((state) => ({
sessions: {
...state.sessions,
[taskRunId]: {
...state.sessions[taskRunId],
processedLineCount: lines.length,
isPromptPending: receivedAgentMessage
? false
: state.sessions[taskRunId]?.isPromptPending ?? false,
},
},
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useTasks } from "@features/tasks/hooks/useTasks";
import { useTaskStore } from "@features/tasks/stores/taskStore";
import { useMeQuery } from "@hooks/useMeQuery";
import { useTaskContextMenu } from "@hooks/useTaskContextMenu";
import { FolderIcon, FolderOpenIcon } from "@phosphor-icons/react";
import { CloudIcon, FolderIcon, FolderOpenIcon } from "@phosphor-icons/react";
import { Box, Flex } from "@radix-ui/themes";
import { useRegisteredFoldersStore } from "@renderer/stores/registeredFoldersStore";
import type { Task } from "@shared/types";
Expand All @@ -19,6 +19,7 @@ import { useTaskViewedStore } from "../stores/taskViewedStore";
import { HomeItem } from "./items/HomeItem";
import { NewTaskItem } from "./items/NewTaskItem";
import { TaskItem } from "./items/TaskItem";
import { SidebarSection } from "./SidebarSection";
import { SortableFolderSection } from "./SortableFolderSection";

function SidebarMenuComponent() {
Expand Down Expand Up @@ -226,6 +227,31 @@ function SidebarMenuComponent() {
}
</DragOverlay>
</DragDropProvider>

{sidebarData.cloudRepos.map((cloudRepo) => (
<SidebarSection
key={cloudRepo.repository}
id={`cloud-${cloudRepo.repository}`}
label={cloudRepo.repoName}
icon={<CloudIcon size={14} weight="regular" />}
isExpanded={!collapsedSections.has(`cloud-${cloudRepo.repository}`)}
onToggle={() => toggleSection(`cloud-${cloudRepo.repository}`)}
>
{cloudRepo.tasks.map((task) => (
<TaskItem
key={task.id}
id={task.id}
label={task.title}
isActive={sidebarData.activeTaskId === task.id}
lastActivityAt={task.lastActivityAt}
isGenerating={task.isGenerating}
isUnread={task.isUnread}
onClick={() => handleTaskClick(task.id)}
onContextMenu={(e) => handleTaskContextMenu(task.id, e)}
/>
))}
</SidebarSection>
))}
</Flex>
</Box>
</>
Expand Down
Loading
Loading