Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ function MainApp() {
activeItems,
approvals,
threadsByWorkspace,
threadParentById,
threadStatusById,
threadListLoadingByWorkspace,
threadListPagingByWorkspace,
Expand Down Expand Up @@ -889,6 +890,7 @@ function MainApp() {
groupedWorkspaces,
hasWorkspaceGroups: workspaceGroups.length > 0,
threadsByWorkspace,
threadParentById,
threadStatusById,
threadListLoadingByWorkspace,
threadListPagingByWorkspace,
Expand Down
322 changes: 203 additions & 119 deletions src/features/app/components/Sidebar.tsx

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/features/layout/hooks/useLayoutNodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type LayoutNodesOptions = {
}>;
hasWorkspaceGroups: boolean;
threadsByWorkspace: Record<string, ThreadSummary[]>;
threadParentById: Record<string, string>;
threadStatusById: Record<string, ThreadActivityStatus>;
threadListLoadingByWorkspace: Record<string, boolean>;
threadListPagingByWorkspace: Record<string, boolean>;
Expand Down Expand Up @@ -276,6 +277,7 @@ export function useLayoutNodes(options: LayoutNodesOptions): LayoutNodesResult {
groupedWorkspaces={options.groupedWorkspaces}
hasWorkspaceGroups={options.hasWorkspaceGroups}
threadsByWorkspace={options.threadsByWorkspace}
threadParentById={options.threadParentById}
threadStatusById={options.threadStatusById}
threadListLoadingByWorkspace={options.threadListLoadingByWorkspace}
threadListPagingByWorkspace={options.threadListPagingByWorkspace}
Expand Down
103 changes: 99 additions & 4 deletions src/features/threads/hooks/useThreads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ function asString(value: unknown) {
return typeof value === "string" ? value : value ? String(value) : "";
}

function normalizeStringList(value: unknown) {
if (Array.isArray(value)) {
return value.map((entry) => asString(entry)).filter(Boolean);
}
const single = asString(value);
return single ? [single] : [];
}

function extractRpcErrorMessage(response: unknown) {
if (!response || typeof response !== "object") {
return null;
Expand Down Expand Up @@ -448,6 +456,84 @@ export function useThreads({
}
}, [onMessageActivity]);

const wouldCreateThreadCycle = useCallback(
(parentId: string, childId: string) => {
const visited = new Set([childId]);
let current: string | undefined = parentId;
while (current) {
if (visited.has(current)) {
return true;
}
visited.add(current);
current = state.threadParentById[current];
}
return false;
},
[state.threadParentById],
);

const updateThreadParent = useCallback(
(parentId: string, childIds: string[]) => {
if (!parentId || childIds.length === 0) {
return;
}
childIds.forEach((childId) => {
if (!childId || childId === parentId) {
return;
}
const existingParent = state.threadParentById[childId];
if (existingParent === parentId) {
return;
}
if (existingParent) {
return;
}
if (wouldCreateThreadCycle(parentId, childId)) {
return;
}
dispatch({ type: "setThreadParent", threadId: childId, parentId });
});
},
[state.threadParentById, wouldCreateThreadCycle],
);

const applyCollabThreadLinks = useCallback(
(fallbackThreadId: string, item: Record<string, unknown>) => {
const itemType = asString(item?.type ?? "");
if (itemType !== "collabToolCall" && itemType !== "collabAgentToolCall") {
return;
}
const sender = asString(item.senderThreadId ?? item.sender_thread_id ?? "");
const parentId = sender || fallbackThreadId;
if (!parentId) {
return;
}
const receivers = [
...normalizeStringList(item.receiverThreadId ?? item.receiver_thread_id),
...normalizeStringList(item.receiverThreadIds ?? item.receiver_thread_ids),
...normalizeStringList(item.newThreadId ?? item.new_thread_id),
];
updateThreadParent(parentId, receivers);
},
[updateThreadParent],
);

const applyCollabThreadLinksFromThread = useCallback(
(fallbackThreadId: string, thread: Record<string, unknown>) => {
const turns = Array.isArray(thread.turns) ? thread.turns : [];
turns.forEach((turn) => {
const turnRecord = turn as Record<string, unknown>;
const turnItems = Array.isArray(turnRecord.items)
? (turnRecord.items as Record<string, unknown>[])
: [];
turnItems.forEach((item) => {
applyCollabThreadLinks(fallbackThreadId, item);
});
});
},
[applyCollabThreadLinks],
);

const handleItemUpdate = useCallback(
(
workspaceId: string,
Expand All @@ -459,6 +545,7 @@ export function useThreads({
if (shouldMarkProcessing) {
markProcessing(threadId, true);
}
applyCollabThreadLinks(threadId, item);
const itemType = asString(item?.type ?? "");
if (itemType === "enteredReviewMode") {
dispatch({ type: "markReviewing", threadId, isReviewing: true });
Expand All @@ -472,7 +559,7 @@ export function useThreads({
}
safeMessageActivity();
},
[markProcessing, safeMessageActivity],
[applyCollabThreadLinks, markProcessing, safeMessageActivity],
);

const handleToolOutputDelta = useCallback(
Expand Down Expand Up @@ -523,7 +610,7 @@ export function useThreads({
}) => {
dispatch({ type: "ensureThread", workspaceId, threadId });
markProcessing(threadId, true);
dispatch({ type: "appendAgentDelta", threadId, itemId, delta });
dispatch({ type: "appendAgentDelta", workspaceId, threadId, itemId, delta });
},
onAgentMessageCompleted: ({
workspaceId,
Expand All @@ -538,7 +625,13 @@ export function useThreads({
}) => {
const timestamp = Date.now();
dispatch({ type: "ensureThread", workspaceId, threadId });
dispatch({ type: "completeAgentMessage", threadId, itemId, text });
dispatch({
type: "completeAgentMessage",
workspaceId,
threadId,
itemId,
text,
});
dispatch({
type: "setLastAgentMessage",
threadId,
Expand Down Expand Up @@ -771,6 +864,7 @@ export function useThreads({
| Record<string, unknown>
| null;
if (thread) {
applyCollabThreadLinksFromThread(threadId, thread);
const items = buildItemsFromThread(thread);
const localItems = state.itemsByThread[threadId] ?? [];
const mergedItems =
Expand Down Expand Up @@ -823,7 +917,7 @@ export function useThreads({
return null;
}
},
[onDebug, state.itemsByThread],
[applyCollabThreadLinksFromThread, onDebug, state.itemsByThread],
);

const listThreadsForWorkspace = useCallback(
Expand Down Expand Up @@ -1424,6 +1518,7 @@ export function useThreads({
activeItems,
approvals: state.approvals,
threadsByWorkspace: state.threadsByWorkspace,
threadParentById: state.threadParentById,
threadStatusById: state.threadStatusById,
threadListLoadingByWorkspace: state.threadListLoadingByWorkspace,
threadListPagingByWorkspace: state.threadListPagingByWorkspace,
Expand Down
Loading