From 3f29da10d112f1859ed3e5cfdf947fa0cfd6a5c0 Mon Sep 17 00:00:00 2001 From: Bertan Ari Date: Thu, 6 Aug 2026 21:29:48 -0700 Subject: [PATCH 1/3] feat(delegation): link subtasks to their parent task in chat history Adds parent/child task identifiers to the message payload so the chat UI can render navigable links between a delegated subtask and the task that spawned it, including after a history resume. --- packages/types/src/message.ts | 7 + .../history-resume-delegation.spec.ts | 53 +++++ src/__tests__/provider-delegation.spec.ts | 84 ++++++++ src/core/webview/ClineProvider.ts | 54 +++++- webview-ui/src/components/chat/ChatRow.tsx | 84 ++++++-- .../__tests__/ChatRow.subtask-links.spec.tsx | 183 ++++++++++++++++++ 6 files changed, 449 insertions(+), 16 deletions(-) diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index e518972a1c2..1ad0f6eee93 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -268,6 +268,13 @@ export const clineMessageSchema = z.object({ * Present when `say: "sliding_window_truncation"`. */ contextTruncation: contextTruncationSchema.optional(), + /** + * Id of the child task this delegation row refers to, stamped when the child is + * created (`ask: "tool"` / `newTask`) or when its result is injected + * (`say: "subtask_result"`). Absent on history written before this field existed, + * which falls back to positional matching against `HistoryItem.childIds`. + */ + childTaskId: z.string().optional(), isProtected: z.boolean().optional(), apiProtocol: z.union([z.literal("openai"), z.literal("anthropic")]).optional(), isAnswered: z.boolean().optional(), diff --git a/src/__tests__/history-resume-delegation.spec.ts b/src/__tests__/history-resume-delegation.spec.ts index a78c41b7c06..b2565bfd92f 100644 --- a/src/__tests__/history-resume-delegation.spec.ts +++ b/src/__tests__/history-resume-delegation.spec.ts @@ -784,4 +784,57 @@ describe("History resume delegation - parent metadata transitions", () => { }), ) }) + + it("stamps each injected subtask_result with its own child id across sequential delegations", async () => { + const makeProvider = () => + ({ + contextProxy: { globalStorageUri: { fsPath: "/storage" } }, + getTaskWithId: vi.fn().mockResolvedValue({ + historyItem: { + id: "p1", + status: "delegated", + childIds: ["c1", "c2"], + ts: 100, + task: "Parent", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + }, + }), + emit: vi.fn(), + getCurrentTask: vi.fn(() => ({ taskId: "c1" })), + removeClineFromStack: vi.fn().mockResolvedValue(undefined), + createTaskWithHistoryItem: vi.fn().mockResolvedValue({ + taskId: "p1", + resumeAfterDelegation: vi.fn().mockResolvedValue(undefined), + }), + updateTaskHistory: vi.fn().mockResolvedValue([]), + }) as unknown as ClineProvider + + vi.mocked(readApiMessages).mockResolvedValue([]) + + vi.mocked(readTaskMessages).mockResolvedValue([]) + await (ClineProvider.prototype as any).reopenParentFromDelegation.call(makeProvider(), { + parentTaskId: "p1", + childTaskId: "c1", + completionResultSummary: "First result", + }) + + const firstSavedMessages = vi.mocked(saveTaskMessages).mock.calls.at(-1)![0].messages + expect(firstSavedMessages.at(-1)).toEqual(expect.objectContaining({ say: "subtask_result", childTaskId: "c1" })) + + vi.mocked(readTaskMessages).mockResolvedValue(firstSavedMessages) + await (ClineProvider.prototype as any).reopenParentFromDelegation.call(makeProvider(), { + parentTaskId: "p1", + childTaskId: "c2", + completionResultSummary: "Second result", + }) + + const secondSavedMessages = vi.mocked(saveTaskMessages).mock.calls.at(-1)![0].messages + const stampedChildIds = secondSavedMessages + .filter((message) => message.say === "subtask_result") + .map((message) => message.childTaskId) + + expect(stampedChildIds).toEqual(["c1", "c2"]) + }) }) diff --git a/src/__tests__/provider-delegation.spec.ts b/src/__tests__/provider-delegation.spec.ts index 4b04fb5bbb9..47ee37ca739 100644 --- a/src/__tests__/provider-delegation.spec.ts +++ b/src/__tests__/provider-delegation.spec.ts @@ -2,7 +2,18 @@ import { describe, it, expect, vi } from "vitest" import { RooCodeEventName } from "@roo-code/types" + +vi.mock("../core/task-persistence/taskMessages", () => ({ + readTaskMessages: vi.fn().mockResolvedValue([]), +})) +vi.mock("../core/task-persistence", async (importOriginal) => ({ + ...(await importOriginal()), + saveTaskMessages: vi.fn().mockResolvedValue(undefined), +})) + import { ClineProvider } from "../core/webview/ClineProvider" +import { readTaskMessages } from "../core/task-persistence/taskMessages" +import { saveTaskMessages } from "../core/task-persistence" describe("ClineProvider.delegateParentAndOpenChild()", () => { it("persists parent delegation metadata and emits TaskDelegated", async () => { @@ -142,4 +153,77 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { // Verify ordering: createTask → updateTaskHistory → child.start expect(callOrder).toEqual(["createTask", "updateTaskHistory", "child.start"]) }) + + it("stamps the parent's pending newTask message with the created child id", async () => { + const pendingNewTaskMessage = { + ts: 1000, + type: "ask", + ask: "tool", + text: JSON.stringify({ tool: "newTask", mode: "code", content: "Do something" }), + } + vi.mocked(readTaskMessages).mockResolvedValue([pendingNewTaskMessage] as any) + + const provider = { + contextProxy: { globalStorageUri: { fsPath: "/storage" } }, + emit: vi.fn(), + getCurrentTask: vi.fn(() => ({ taskId: "parent-1", emit: vi.fn() })), + removeClineFromStack: vi.fn().mockResolvedValue(undefined), + createTask: vi.fn().mockResolvedValue({ taskId: "child-1", start: vi.fn() }), + getTaskWithId: vi.fn().mockResolvedValue({ + historyItem: { id: "parent-1", task: "Parent", tokensIn: 0, tokensOut: 0, totalCost: 0, childIds: [] }, + }), + updateTaskHistory: vi.fn(), + handleModeSwitch: vi.fn().mockResolvedValue(undefined), + log: vi.fn(), + } as unknown as ClineProvider + + await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { + parentTaskId: "parent-1", + message: "Do something", + initialTodos: [], + mode: "code", + }) + + expect(saveTaskMessages).toHaveBeenCalledWith( + expect.objectContaining({ + taskId: "parent-1", + messages: [expect.objectContaining({ ask: "tool", childTaskId: "child-1" })], + }), + ) + }) + + it("does not re-stamp an earlier newTask message when the newest one is already stamped", async () => { + const newTaskMessage = (ts: number, childTaskId?: string) => ({ + ts, + type: "ask", + ask: "tool", + text: JSON.stringify({ tool: "newTask", mode: "code", content: `Subtask ${ts}` }), + ...(childTaskId ? { childTaskId } : {}), + }) + vi.mocked(saveTaskMessages).mockClear() + vi.mocked(readTaskMessages).mockResolvedValue([newTaskMessage(1000), newTaskMessage(2000, "child-2")] as any) + + const provider = { + contextProxy: { globalStorageUri: { fsPath: "/storage" } }, + emit: vi.fn(), + getCurrentTask: vi.fn(() => ({ taskId: "parent-1", emit: vi.fn() })), + removeClineFromStack: vi.fn().mockResolvedValue(undefined), + createTask: vi.fn().mockResolvedValue({ taskId: "child-3", start: vi.fn() }), + getTaskWithId: vi.fn().mockResolvedValue({ + historyItem: { id: "parent-1", task: "Parent", tokensIn: 0, tokensOut: 0, totalCost: 0, childIds: [] }, + }), + updateTaskHistory: vi.fn(), + handleModeSwitch: vi.fn().mockResolvedValue(undefined), + log: vi.fn(), + } as unknown as ClineProvider + + await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { + parentTaskId: "parent-1", + message: "Do something", + initialTodos: [], + mode: "code", + }) + + expect(saveTaskMessages).not.toHaveBeenCalled() + }) }) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 61a54f8ead7..36431f08e12 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2879,7 +2879,58 @@ export class ClineProvider startTask: false, }) - // 5) Persist parent delegation metadata BEFORE the child starts writing. + // 5) Stamp the child id onto the parent's pending `newTask` row so the UI can resolve + // the link from the message itself. Positional matching against `childIds` drifts + // whenever a delegation is rejected, aborted, or re-issued. + try { + const delegationStoragePath = this.contextProxy.globalStorageUri.fsPath + const parentMessages = await readTaskMessages({ + taskId: parentTaskId, + globalStoragePath: delegationStoragePath, + }) + + if (Array.isArray(parentMessages)) { + for (let messageIndex = parentMessages.length - 1; messageIndex >= 0; messageIndex--) { + const candidate = parentMessages[messageIndex] + + if (candidate.type !== "ask" || candidate.ask !== "tool") { + continue + } + + let parsedTool: { tool?: string } | undefined + try { + parsedTool = candidate.text ? JSON.parse(candidate.text) : undefined + } catch { + parsedTool = undefined + } + + if (parsedTool?.tool !== "newTask") { + continue + } + + // Stop at the newest `newTask` row: it is the one being delegated now. Walking + // further back would stamp an earlier delegation with this child's id. + if (!candidate.childTaskId) { + candidate.childTaskId = child.taskId + await saveTaskMessages({ + messages: parentMessages, + taskId: parentTaskId, + globalStoragePath: delegationStoragePath, + }) + } + + break + } + } + } catch (err) { + this.log( + `[delegateParentAndOpenChild] Failed to stamp childTaskId on parent ${parentTaskId}: ${ + (err as Error)?.message ?? String(err) + }`, + ) + } + + // 6) Persist parent delegation metadata BEFORE the child starts writing. try { const { historyItem } = await this.getTaskWithId(parentTaskId) const childIds = Array.from(new Set([...(historyItem.childIds ?? []), child.taskId])) @@ -2958,6 +3009,7 @@ export class ClineProvider say: "subtask_result", text: completionResultSummary, ts, + childTaskId, } parentClineMessages.push(subtaskUiMessage) await saveTaskMessages({ messages: parentClineMessages, taskId: parentTaskId, globalStoragePath }) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 2fd3a274f6a..7059676d03f 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -109,6 +109,67 @@ function getPreviousTodos(messages: ClineMessage[], currentMessageTs: number): a return [] } +function isNewTaskAsk(message: ClineMessage): boolean { + if (message.type !== "ask" || message.ask !== "tool") { + return false + } + + return safeJsonParse(message.text)?.tool === "newTask" +} + +// Resolves which child task a delegation row refers to. +// +// The message's own `childTaskId` is authoritative: it is stamped when the child is +// created. Positional matching against `childIds` is only a fallback for history +// written before that stamp existed, and drifts when a delegation was rejected or +// re-issued because `childIds` then holds fewer entries than there are `newTask` rows. +// +// In the fallback path, `childIds` is appended in delegation order, so the Nth +// `newTask` row maps to `childIds[N]`. A `subtask_result` row reports the outcome of +// the most recent preceding `newTask` row, so it resolves to that same id. Counting +// `newTask` rows up to (and including) the current row handles both cases, and stays +// correct when a `subtask_result` is separated from its `newTask` row by other +// messages such as `resume_task` or `user_feedback`. +function getDelegatedChildTaskId( + messages: ClineMessage[], + currentMessageTs: number, + childIds: string[], +): string | undefined { + const currentMessageIndex = messages.findIndex((msg) => msg.ts === currentMessageTs) + + if (currentMessageIndex === -1) { + return undefined + } + + const stampedChildTaskId = messages[currentMessageIndex].childTaskId + + if (stampedChildTaskId) { + return stampedChildTaskId + } + + // A `subtask_result` predating the stamp still resolves via its originating + // `newTask` row, which may itself carry the stamp. + let newTaskCount = 0 + let precedingNewTaskChildId: string | undefined + + for (let messageIndex = 0; messageIndex <= currentMessageIndex; messageIndex++) { + if (isNewTaskAsk(messages[messageIndex])) { + newTaskCount++ + // Deliberately not retained across rows: an older row's stamp says nothing + // about this one, so an unstamped `newTask` row must fall through to `childIds`. + precedingNewTaskChildId = messages[messageIndex].childTaskId + } + } + + if (newTaskCount === 0) { + return undefined + } + + return messages[currentMessageIndex].say === "subtask_result" && precedingNewTaskChildId + ? precedingNewTaskChildId + : childIds[newTaskCount - 1] +} + interface ChatRowProps { message: ClineMessage lastModifiedMessage?: ClineMessage @@ -831,23 +892,11 @@ export const ChatRowContent = ({ ) case "newTask": - // Find all newTask messages to determine which child task ID corresponds to this message - const newTaskMessages = clineMessages.filter((msg) => { - if (msg.type === "ask" && msg.ask === "tool") { - const t = safeJsonParse(msg.text) - return t?.tool === "newTask" - } - return false - }) - const thisNewTaskIndex = newTaskMessages.findIndex((msg) => msg.ts === message.ts) - const childIds = currentTaskItem?.childIds || [] - // Only get the child task ID if this newTask has been approved (has a corresponding entry in childIds) // This prevents showing a link to a previous task when the current newTask is still awaiting approval // Note: We don't use delegatedToId here because it persists after child tasks complete and would // incorrectly point to the previous task when a new newTask is awaiting approval - const childTaskId = - thisNewTaskIndex >= 0 && thisNewTaskIndex < childIds.length ? childIds[thisNewTaskIndex] : undefined + const childTaskId = getDelegatedChildTaskId(clineMessages, message.ts, currentTaskItem?.childIds || []) // Check if the next message is a subtask_result - if so, don't show the button // since the result is displayed right after this message @@ -1021,8 +1070,13 @@ export const ChatRowContent = ({ /> ) case "subtask_result": - // Get the child task ID that produced this result - const completedChildTaskId = currentTaskItem?.completedByChildId + // Get the child task ID that produced this result. + // `completedByChildId` only ever holds the most recently completed child, so it is + // used as a fallback for legacy history items that predate `childIds`. Relying on it + // alone made every subtask result in a task link to the last child task. + const completedChildTaskId = + getDelegatedChildTaskId(clineMessages, message.ts, currentTaskItem?.childIds || []) ?? + currentTaskItem?.completedByChildId return (
diff --git a/webview-ui/src/components/chat/__tests__/ChatRow.subtask-links.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatRow.subtask-links.spec.tsx index 3a1971ec68f..2a616770eba 100644 --- a/webview-ui/src/components/chat/__tests__/ChatRow.subtask-links.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatRow.subtask-links.spec.tsx @@ -223,5 +223,188 @@ describe("ChatRow - subtask links", () => { const goToSubtaskButton = screen.queryByText("Go to subtask") expect(goToSubtaskButton).toBeNull() }) + + it("should link to the matching child task, not the last one, when several subtasks completed", () => { + const newTask = (ts: number) => ({ + ts, + type: "ask" as const, + ask: "tool" as const, + text: JSON.stringify({ tool: "newTask", mode: "code", content: `Subtask ${ts}` }), + }) + const subtaskResult = (ts: number) => ({ + ts, + type: "say" as const, + say: "subtask_result" as const, + text: `Result ${ts}`, + }) + + const firstResult = subtaskResult(1001) + const clineMessages = [ + newTask(1000), + firstResult, + newTask(1002), + subtaskResult(1003), + newTask(1004), + subtaskResult(1005), + ] as ClineMessage[] + + renderChatRow( + firstResult, + { + childIds: ["first-child", "second-child", "third-child"], + // Only ever holds the most recently completed child. + completedByChildId: "third-child", + }, + clineMessages, + ) + + fireEvent.click(screen.getByText("Go to subtask")) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "showTaskWithId", + text: "first-child", + }) + }) + + it("should link to the matching child task when the result is not adjacent to its newTask row", () => { + const newTaskMessage = { + ts: 1000, + type: "ask" as const, + ask: "tool" as const, + text: JSON.stringify({ tool: "newTask", mode: "code", content: "Implement feature X" }), + } + const resumeTaskMessage = { + ts: 1001, + type: "ask" as const, + ask: "resume_task" as const, + } + const resultMessage = { + ts: 1002, + type: "say" as const, + say: "subtask_result" as const, + text: "The subtask has been completed successfully.", + } + + renderChatRow(resultMessage, { childIds: ["only-child"], completedByChildId: "stale-child" }, [ + newTaskMessage, + resumeTaskMessage, + resultMessage, + ] as ClineMessage[]) + + fireEvent.click(screen.getByText("Go to subtask")) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "showTaskWithId", + text: "only-child", + }) + }) + + it("should prefer the message's own childTaskId over positional childIds", () => { + const message = { + ts: 1002, + type: "say" as const, + say: "subtask_result" as const, + text: "Result", + childTaskId: "stamped-child", + } + + renderChatRow(message, { childIds: ["wrong-child"], completedByChildId: "last-child" }, [ + message, + ] as ClineMessage[]) + + fireEvent.click(screen.getByText("Go to subtask")) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "showTaskWithId", + text: "stamped-child", + }) + }) + + it("should link each of several stamped results to its own child", () => { + const firstResult = { + ts: 1001, + type: "say" as const, + say: "subtask_result" as const, + text: "Result 1", + childTaskId: "child-alpha", + } + const secondResult = { + ts: 1003, + type: "say" as const, + say: "subtask_result" as const, + text: "Result 2", + childTaskId: "child-beta", + } + const clineMessages = [firstResult, secondResult] as ClineMessage[] + + renderChatRow(secondResult, { completedByChildId: "child-beta" }, clineMessages) + + fireEvent.click(screen.getByText("Go to subtask")) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "showTaskWithId", + text: "child-beta", + }) + }) + + it("should resolve an unstamped result via its stamped newTask row", () => { + const newTaskMessage = { + ts: 1000, + type: "ask" as const, + ask: "tool" as const, + text: JSON.stringify({ tool: "newTask", mode: "code", content: "Do work" }), + childTaskId: "stamped-child", + } + const resultMessage = { + ts: 1001, + type: "say" as const, + say: "subtask_result" as const, + text: "Result", + } + + renderChatRow(resultMessage, { childIds: [], completedByChildId: "stale-child" }, [ + newTaskMessage, + resultMessage, + ] as ClineMessage[]) + + fireEvent.click(screen.getByText("Go to subtask")) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "showTaskWithId", + text: "stamped-child", + }) + }) + }) + + describe("rejected delegations", () => { + it("should link a stamped newTask row correctly when an earlier ask was rejected", () => { + const rejectedNewTask = { + ts: 1000, + type: "ask" as const, + ask: "tool" as const, + text: JSON.stringify({ tool: "newTask", mode: "code", content: "Rejected work" }), + } + const approvedNewTask = { + ts: 1001, + type: "ask" as const, + ask: "tool" as const, + text: JSON.stringify({ tool: "newTask", mode: "code", content: "Approved work" }), + childTaskId: "real-child", + } + + // `childIds` holds one entry while two `newTask` rows exist, so index matching + // would resolve the approved row to nothing. + renderChatRow(approvedNewTask, { childIds: ["real-child"] }, [ + rejectedNewTask, + approvedNewTask, + ] as ClineMessage[]) + + fireEvent.click(screen.getByText("Go to subtask")) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "showTaskWithId", + text: "real-child", + }) + }) }) }) From 79ddc199316e28f5981d4f7380c9bdc5f4d89962 Mon Sep 17 00:00:00 2001 From: Bertan Ari Date: Thu, 6 Aug 2026 21:31:25 -0700 Subject: [PATCH 2/3] chore(release): bump version to 3.53.5 and require VS Code ^1.120.0 --- src/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/package.json b/src/package.json index 271979e701c..bc0eb924317 100644 --- a/src/package.json +++ b/src/package.json @@ -3,14 +3,14 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "RooVeterinaryInc", - "version": "3.53.3", + "version": "3.53.5", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", "theme": "dark" }, "engines": { - "vscode": "^1.106.0", + "vscode": "^1.120.0", "node": "20.19.2" }, "author": { From b4ed3a5347ff6587383b89e38f94237d78a8e68f Mon Sep 17 00:00:00 2001 From: Bertan Ari Date: Thu, 6 Aug 2026 21:31:40 -0700 Subject: [PATCH 3/3] docs(analysis): record the Zoo Code selective-import decision Documents that three of the four candidate imports are already present locally, corrects the assumed v3.54.0 baseline, and settles on adding Zoo Code as a read-only remote for targeted cherry-picks rather than a wholesale realignment. --- docs/analysis/zoo-code-import-decision.md | 206 ++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 docs/analysis/zoo-code-import-decision.md diff --git a/docs/analysis/zoo-code-import-decision.md b/docs/analysis/zoo-code-import-decision.md new file mode 100644 index 00000000000..2e737dfeda7 --- /dev/null +++ b/docs/analysis/zoo-code-import-decision.md @@ -0,0 +1,206 @@ +# Zoo Code import decision + +Decision document. No code changes are proposed here; this records what to import, what to +defer, and what to leave alone. + +## Situation + +- Local `src/package.json` is 3.53.5. `origin/main` (RooCodeInc/Roo-Code) is 3.53.0. +- `git rev-list --left-right --count origin/main...HEAD` = 0 / 45. Local is 45 commits ahead + and 0 behind, so nothing of substance is left to take from `origin`. +- RooCodeInc/Roo-Code is archived read-only since 2026-05-15. Its final release, v3.54.0, was + almost entirely removals. +- Active development moved to Zoo Code (`Zoo-Code-Org/Zoo-Code`), continuing the same version + line up to 3.76.0. + +## Verification of "likely already local" items + +Checked directly in the working tree: + +| Item | Status | Evidence | +| ------------------------------------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Per-mode MCP server allowlist | Present | `allowedMcpServers` in [`packages/types/src/mode.ts`](../../packages/types/src/mode.ts), enforced in [`src/core/tools/mcpServerRestriction.ts`](../../src/core/tools/mcpServerRestriction.ts) and [`src/core/prompts/tools/filter-tools-for-mode.ts`](../../src/core/prompts/tools/filter-tools-for-mode.ts) | +| Workspace `rootResolution` setting | Present | `roo-cline.workspace.rootResolution` in [`src/utils/path.ts`](../../src/utils/path.ts), tests in [`src/utils/__tests__/path.spec.ts`](../../src/utils/__tests__/path.spec.ts) | +| VS Code LM auto-condensing | Present, and locally extended | `getCondenseContextWindow()` in [`src/api/providers/vscode-lm.ts`](../../src/api/providers/vscode-lm.ts); `maxTokens: -1` and available-input denominator handling in [`src/core/context-management/index.ts`](../../src/core/context-management/index.ts) | +| `WorkspacePathResolver` symlink canonicalization | Not present as a named module | No `WorkspacePathResolver` anywhere. Symlink realpath handling exists only ad hoc in [`src/core/ignore/RooIgnoreController.ts`](../../src/core/ignore/RooIgnoreController.ts) and [`src/services/skills/SkillsManager.ts`](../../src/services/skills/SkillsManager.ts) | + +So three of the four are already local and must not be re-imported. Only the symlink +canonicalization work is genuinely missing, and the local ad hoc handling already covers the +two places that mattered. + +## Correction to the assumed baseline + +The briefing assumed the local tree sits on the de-Roo'd v3.54.0 removals. It does not. +`packages/cloud`, `packages/evals`, `packages/telemetry` and `apps/web-evals` are all still +present. Only `src/services/marketplace` and the webview marketplace UI are gone. + +This changes the structural-divergence picture in our favour: Zoo commits that assume Cloud, +telemetry or evals exist will mostly apply, because those subsystems are still here. Only +marketplace-touching commits hit missing ground. + +## Strategy + +**Add Zoo Code as a read-only remote; import selectively; never realign wholesale.** + +Add `zoo` as a fetch-only remote so Zoo history is available for `git log`, `git show`, and +targeted cherry-picks. Do not make it a merge target and do not set it as the upstream of any +local branch. + +Why not realign wholesale: + +- Sixteen releases of drift across four HIGH-conflict files that are exactly where this fork's + value lives ([`src/core/webview/ClineProvider.ts`](../../src/core/webview/ClineProvider.ts), + [`packages/types/src/message.ts`](../../packages/types/src/message.ts), + [`webview-ui/src/components/chat/ChatRow.tsx`](../../webview-ui/src/components/chat/ChatRow.tsx), + and the local delegation tests). +- A wholesale merge would force resolving the delegation rewrite, the `TaskRegistry` refactor, + the provider-identifier migration, and the Roo-to-Zoo rebrand all at once, with no way to + test any one of them in isolation. +- Selective import lets each area be judged on its own value against its own conflict cost. + +Why a remote at all rather than copying patches by hand: cherry-picks keep authorship and +commit messages, and `git log zoo/main -- ` is the cheapest way to see whether a later +Zoo commit already fixed something we are about to import. + +### Structural divergence and branding + +- **Telemetry, Cloud, evals**: subsystems are present locally, so Zoo changes to them apply. + But this fork has no use for them. Treat them as SKIP by policy, not by mechanics. +- **Marketplace**: removed locally. Any Zoo commit touching `src/services/marketplace` or the + marketplace webview is SKIP. Do not reintroduce the subsystem to make a cherry-pick apply. +- **MDM / org enforcement**: SKIP. No org-policy requirement in a personal fork. +- **Zoo branding**: SKIP the rebrand commit. It is mechanical, touches locales, README and UI + strings repo-wide, and would poison every future diff against Roo-era files for no + functional gain. Accept that later Zoo commits carrying incidental Zoo strings will need + those strings dropped during the cherry-pick. + +## Classification + +### IMPORT NOW + +| Area | Rationale | Value vs conflict cost | +| ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| Terminal cold-start output loss fix (3.70.0); "next step before command finishes" fix (3.76.0) | Both are correctness bugs that silently corrupt tool results. Localized to the terminal layer. | High value, low cost | +| Multi-line quoted command parsing, `list_files` directory validation (3.60.0) | Small, self-contained tool-correctness fixes. | Moderate value, low cost | +| Chat memory exhaustion on large transcripts (3.60.0) | Affects long sessions, which this fork produces constantly. Touches `ChatRow.tsx`, so conflict is real but bounded. | High value, medium cost | +| Settings `cachedState` synchronization fixes (3.72.0, 3.74.0) | Directly supports the `cachedState` rule in [`AGENTS.md`](../../AGENTS.md); these are race-condition fixes we would otherwise rediscover. | High value, low cost | +| Security dependency bumps: shell-quote, esbuild, vite, undici | Pure dependency version changes with no code conflict. | High value, near-zero cost | +| Ollama tool-result handling and premature condensing fix (3.68.0) | Condensing correctness; adjacent to local VS Code LM condensing work but not overlapping. | Moderate value, low cost | + +### IMPORT LATER + +| Area | Rationale | Ordering | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| Model and provider additions (Claude Sonnet 5 / Opus 5, GPT-5.5 / 5.6, Gemini 3.5 / 3.6 Flash, GLM-5.2, Grok 4.5, Kimi K3, MiniMax-M3, `xhigh` reasoning effort) | Genuinely useful but individually low urgency, and each is a small isolated table edit. Batch them. | Must come **before** the canonical provider-identifier migration, or the migration has to be re-applied to each new entry | +| Canonical provider-identifier migration (3.72.0–3.74.0, ~8 PRs) | Cross-cutting rename across `packages/types`, `src/api` and webview. Worth doing once so later provider commits apply cleanly, but it is a large mechanical churn that should not be interleaved with anything else. | After the model batch; before any further provider imports | +| `TaskSemaphore` / `TaskRegistry` replacing the task stack (3.64.0, 3.74.0) | The structural prerequisite for every later delegation fix. Also the single largest conflict with the local delegation rewrite. | **Blocks** all delegation-area imports. Nothing from the delegation list should be attempted first | +| Delegation and subtask fixes (3.60.0, 3.64.0, 3.66.0, 3.68.0, 3.72.0) | See the delegation section below. | After `TaskRegistry` | +| Destructive Command Guard and grouped tool approval (3.76.0) | Real safety value; opt-in so it can land dark. Touches the approval path, which the local fork also edits. | After the terminal fixes | +| Configurable relaxed diff thresholds and `apply_diff` prompt improvements (3.64.0) | Quality-of-life for editing accuracy; no urgency. | Independent | +| Context-compaction button and context-window progress bar (3.70.0); completion review actions (3.64.0) | Useful UI, but all land in `ChatRow.tsx` / task header where local subtask links live. Take them together in one pass to pay the conflict cost once. | After the chat memory fix | +| Router-provider model metadata fetched before context decisions (3.74.0) | Correctness improvement for context sizing. | After the provider-identifier migration | +| Node 22.23.1, Vitest 4, `@vscode/ripgrep` 1.18+ | Toolchain upgrades; do them in a quiet window since they can break the whole test suite at once. | Independent, but isolate | +| Rules Management UI (3.64.0); relative-symlink realpath in rules files (3.60.0) | The symlink fix is the one verified gap. The Rules UI is nice but interacts with local `.roo/shared-rules/` layout. | Symlink fix first, UI later | + +### SKIP + +| Area | Rationale | +| ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | +| Zoo Gateway provider, auth callback, multi-profile token sync | Ties the fork to a hosted service we do not use. | +| Kenari, Friendli, Semble embedding, OpenCode-Go, Moonshot / Kimi Code OAuth device flow | New provider integrations with no local demand; each adds surface area and future merge weight. | +| Telemetry circuit breaking and delta aggregation (3.76.0) | Expands a subsystem this fork has no use for. | +| Cloud, evals, MDM / org-membership enforcement changes | Same reasoning; not used here. | +| MCP marketplace changes and `tool-writer` marketplace mode (3.62.0) | The marketplace subsystem is absent locally; importing would mean reintroducing it. | +| Roo-to-Zoo branding (3.74.0) | Mechanical repo-wide string churn, zero functional gain, permanently noisy diffs. | +| Playwright visual-regression harness | Heavy CI infrastructure for a single-maintainer fork. | +| Per-mode MCP allowlist (3.60.0), `rootResolution` (3.60.0), VS Code LM condensing (3.66.0) | Already present locally, and the local versions are further along. Verified above. | +| Architect-mode plans kept workspace-relative (3.74.0) | Conflicts with this fork's own plan-location rules. | +| `ask_followup_question` non-array `follow_up` as a type error (3.64.0) | Behaviour change with modest payoff; local modes already pass arrays. | +| GitHub-style alerts, configurable chat font size (3.58.0) | Cosmetic. | +| Roo Code history import on About page (3.64.0) | Migration aid for users moving off Roo; irrelevant to a fork that never left. | +| Dart and plain-text indexing fixes (3.72.0) | No Dart in this workspace. Revisit only if that changes. | +| Ripgrep diagnostic command, terminal profile settings redesign (3.60.0) | Diagnostic and preference surface; not worth the settings-schema conflict. | + +## The delegation and subtask area + +This is where the fork's main value-add lives, so it gets an explicit call. + +**Recommendation: port local behaviour on top of upstream. Adopt Zoo's model; retire the local +implementation as the structural base; keep only the local behaviour that Zoo does not +provide.** + +Reasoning: + +- Zoo has spent five releases (3.60.0 through 3.72.0) hardening exactly this area: return to + the active parent, `atomicReadAndUpdate` serialization of `delegateParentAndOpenChild`, a + status transition guard, startup delegation reconciliation, preserved parent-child links on + interrupt, safe abandonment, and a task-history lock. That is a body of concurrency and + crash-recovery work that a fork will not independently reproduce. +- The 3.74.0 `TaskRegistry` refactor removes the task stack that the local rewrite is built + on. Staying divergent means every future delegation fix from Zoo becomes unusable, and the + divergence compounds with each release. +- The local fork's distinctive value in this area is mostly _user-visible_: subtask links in + [`webview-ui/src/components/chat/ChatRow.tsx`](../../webview-ui/src/components/chat/ChatRow.tsx) + and its delegation UX. That is a thin layer and it can sit on top of Zoo's lifecycle model. + Zoo's own 3.72.0 delegation-status surfacing may already cover part of it. + +Sequenced approach: + +1. Write down the local delegation behaviour as observable requirements, using + [`src/__tests__/provider-delegation.spec.ts`](../../src/__tests__/provider-delegation.spec.ts) + and [`src/__tests__/history-resume-delegation.spec.ts`](../../src/__tests__/history-resume-delegation.spec.ts) + as the source of truth. Do this before touching anything. +2. Import `TaskSemaphore` / `TaskRegistry` (3.64.0, 3.74.0). Accept a large one-time conflict + in [`src/core/webview/ClineProvider.ts`](../../src/core/webview/ClineProvider.ts). +3. Import the delegation fixes in release order: 3.60.0, 3.64.0, 3.66.0, 3.68.0, 3.72.0. Order + matters; the later fixes assume the earlier state machine. +4. Replay the step-1 requirements against the result. Keep local tests only where they cover + behaviour Zoo's tests do not. Delete local tests that merely duplicate upstream coverage, + since keeping both means two competing definitions of correct. +5. Reapply the local subtask-link UI on top, dropping any part Zoo's delegation-status UI + already provides. + +This is the largest item in the whole plan and should be its own effort, not mixed with the +provider or UI batches. + +## Assumptions + +- The maintainer intends to keep tracking upstream rather than freeze the fork. If the fork is + headed for a hard freeze or a move to a different base, most of IMPORT LATER becomes SKIP. +- Zoo Code is a legitimate continuation of the same codebase and the same license, so + cherry-picking is appropriate. This has not been independently verified. +- The categorized inventory of Zoo changes supplied in the task brief is accurate. It was not + re-derived from Zoo's own history. +- Cloud, telemetry and evals remain present locally but unused. If they are later removed to + match Roo v3.54.0, several IMPORT LATER items would need re-checking for hidden dependencies. + +## Open questions + +These would change the recommendation: + +- **Is Zoo Code stable and maintained?** If it forks again or stalls, investing in the + `TaskRegistry` realignment buys nothing. Worth watching commit cadence for a period before + committing to step 2 of the delegation plan. +- **Does Zoo's 3.72.0 delegation-status UI already deliver the local subtask-link experience?** + If yes, the local UI layer can be dropped entirely rather than reapplied, which shrinks the + delegation effort considerably. +- **Is the local delegation rewrite ahead of Zoo's in any respect?** If it solves something Zoo + still gets wrong, that specific behaviour should be contributed upstream rather than merely + preserved locally. +- **Will the fork ever remove Cloud, telemetry and evals?** Deciding this first would avoid + importing changes into subsystems that are about to be deleted. +- **Does the provider-identifier migration have a mechanical codemod in Zoo's history?** If so, + that migration drops from large to routine and could move up to IMPORT NOW. + +## Related local documents + +- [`docs/design/per-mode-mcp-settings.md`](../design/per-mode-mcp-settings.md) +- [`docs/design/workspace-root-resolution.md`](../design/workspace-root-resolution.md) +- [`docs/investigation/roo-to-copilot/README.md`](../investigation/roo-to-copilot/README.md) — + no Zoo counterpart, no conflict risk. + +## References + +- Zoo Code releases: https://github.com/Zoo-Code-Org/Zoo-Code/releases +- Zoo Code changelog: https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/CHANGELOG.md +- Roo Code releases (archived): https://github.com/RooCodeInc/Roo-Code/releases