diff --git a/.changeset/large-worktree-diffs.md b/.changeset/large-worktree-diffs.md new file mode 100644 index 00000000000..72bdbbcdde1 --- /dev/null +++ b/.changeset/large-worktree-diffs.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Reduce Agent Manager memory usage when viewing large worktree diffs. diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 41388c8622a..0bd5c666168 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -1005,7 +1005,7 @@ function ToolFileAccordion(props: { path: string; actions?: JSX.Element; childre
- {`\u202A${getDirectory(props.path)}\u202C`} + {`\u2066${getDirectory(props.path)}\u2069`} {getFilename(props.path)}
@@ -1517,7 +1517,7 @@ function ToolMetaLine(props: { > {props.filename} - {props.path} + {`\u2066${props.path}\u2069`} {(changes) => } @@ -2196,7 +2196,7 @@ ToolRegistry.register({
- {`\u202A${getDirectory(file.relativePath)}\u202C`} + {`\u2066${getDirectory(file.relativePath)}\u2069`} () + for (const sec of state.getSections()) { + if (!sec.collapsed) continue + for (const id of state.getWorktreesInSection(sec.id)) skipped.add(id) + } + this.statsPoller.syncSkips(skipped) + } + private pushState(): void { const state = this.state if (!state) return @@ -1272,6 +1285,9 @@ export class AgentManagerProvider implements Disposable { ...run, }) + // Sync skip set before enabling the poller so the first poll cycle + // already excludes worktrees in collapsed sections. + this.syncPollerSkips() this.statsPoller.setEnabled(worktrees.length > 0 || this.panel !== undefined) this.prBridge.poller.setEnabled(worktrees.length > 0) } diff --git a/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts b/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts index 8b283946f71..b815658168d 100644 --- a/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts @@ -84,12 +84,14 @@ export class GitStatsPoller { } } - skipWorktree(id: string): void { - this.skipWorktreeIds.add(id) + /** Replace the entire skip set with the given IDs. */ + syncSkips(ids: Set): void { + this.skipWorktreeIds = ids } - unskipWorktree(id: string): void { - this.skipWorktreeIds.delete(id) + /** Pre-emptively exclude a single worktree (e.g. before deletion). */ + skipWorktree(id: string): void { + this.skipWorktreeIds.add(id) } setEnabled(enabled: boolean): void { diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts index eacf044ae56..e31affefe71 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts @@ -352,6 +352,15 @@ export class WorktreeStateManager { return this.sections.get(id) } + /** Return IDs of worktrees assigned to the given section. */ + getWorktreesInSection(id: string): string[] { + const result: string[] = [] + for (const wt of this.worktrees.values()) { + if (wt.sectionId === id) result.push(wt.id) + } + return result + } + addSection(name: string, color: string | null, worktreeIds?: string[]): Section { this.setNormalizedWorktreeOrder(this.worktreeOrder) const id = generateId("sec") diff --git a/packages/kilo-vscode/src/kilo-provider-utils.ts b/packages/kilo-vscode/src/kilo-provider-utils.ts index 223f1b86d1e..2bc1b2fe085 100644 --- a/packages/kilo-vscode/src/kilo-provider-utils.ts +++ b/packages/kilo-vscode/src/kilo-provider-utils.ts @@ -195,10 +195,12 @@ export async function loadSessions(ctx: SessionRefreshContext): Promise() const extra = await Promise.all( [...worktreeDirs].map((dir) => list(dir).catch((err: unknown) => { console.error(`[Kilo] Failed to list sessions for ${dir}:`, err) + failed.add(dir) return [] as Session[] }), ), @@ -212,9 +214,19 @@ export async function loadSessions(ctx: SessionRefreshContext): Promise sessionToWebview(s)), + ...(preserve.length ? { preserveSessionIds: preserve } : {}), }) return projectID diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-session-refresh.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-session-refresh.test.ts index 7747f980adb..2dd31cf7b9b 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-session-refresh.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-session-refresh.test.ts @@ -157,6 +157,86 @@ describe("KiloProvider pending session refresh", () => { expect((sent[0] as { sessions: { id: string }[] }).sessions.map((s) => s.id)).toEqual(["ses_worktree"]) }) + it("preserves session ids when worktree directory listing fails", async () => { + const sent: unknown[] = [] + const ctx = createContext({ + connectionState: "connected", + sessionDirectories: new Map([ + ["ses_wt1", "/worktree1"], + ["ses_wt2", "/worktree2"], + ]), + listSessions: async (dir) => { + if (dir === "/repo") { + return [ + { + id: "ses_root", + projectID: "project", + title: "root", + directory: "/repo", + time: { created: 1, updated: 1 }, + }, + ] as never + } + if (dir === "/worktree1") throw new Error("backend not ready") + return [ + { + id: "ses_wt2", + projectID: "project", + title: "wt2", + directory: "/worktree2", + time: { created: 2, updated: 2 }, + }, + ] as never + }, + postMessage: (msg) => sent.push(msg), + }) + + await loadSessions(ctx) + + expect(sent).toHaveLength(1) + const msg = sent[0] as { sessions: { id: string }[]; preserveSessionIds?: string[] } + expect(msg.sessions.map((s) => s.id)).toEqual(["ses_root", "ses_wt2"]) + expect(msg.preserveSessionIds).toEqual(["ses_wt1"]) + }) + + it("omits preserveSessionIds when all directories succeed", async () => { + const sent: unknown[] = [] + const ctx = createContext({ + connectionState: "connected", + sessionDirectories: new Map([["ses_wt", "/worktree"]]), + listSessions: async (dir) => { + if (dir === "/repo") { + return [ + { + id: "ses_root", + projectID: "project", + title: "root", + directory: "/repo", + time: { created: 1, updated: 1 }, + }, + ] as never + } + return [ + { + id: "ses_wt", + projectID: "project", + title: "wt", + directory: "/worktree", + time: { created: 2, updated: 2 }, + }, + ] as never + }, + postMessage: (msg) => sent.push(msg), + }) + + await loadSessions(ctx) + + expect(sent).toHaveLength(1) + const msg = sent[0] as { sessions: { id: string }[]; preserveSessionIds?: string[] } + expect(msg.sessions.map((s) => s.id)).toEqual(["ses_root", "ses_wt"]) + expect(msg.preserveSessionIds).toBeUndefined() + }) + it("flushes deferred refresh via flushPendingSessionRefresh", async () => { const { calls, fn } = createListSessions() const ctx = createContext() diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/agentmanager/tab-bar-multiple-tabs-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/agentmanager/tab-bar-multiple-tabs-chromium-linux.png new file mode 100644 index 00000000000..bc4bc545585 --- /dev/null +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/agentmanager/tab-bar-multiple-tabs-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee8d8e04b09b58973a1ff66d5a3d8648ba590f88312cf1f37412c39ccdac0480 +size 3973 diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/agentmanager/tab-bar-single-tab-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/agentmanager/tab-bar-single-tab-chromium-linux.png new file mode 100644 index 00000000000..999c6b2f9e9 --- /dev/null +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/agentmanager/tab-bar-single-tab-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0e9ea1be8fbb44f05bed7960d1ba13f0d4c5e904e4025e4ab4743c4991e93c8 +size 4696 diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/agentmanager/tab-bar-with-review-tab-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/agentmanager/tab-bar-with-review-tab-chromium-linux.png new file mode 100644 index 00000000000..0760abd4b86 --- /dev/null +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/agentmanager/tab-bar-with-review-tab-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66a7ecedac685e17c442bb036d7392320e0642ef43da77ed8dc23be29bc7988e +size 2935 diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index 9588831f47a..31ac21b70e0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -431,7 +431,7 @@ export const DiffPanel: Component = (props) => {
- {getDirectory(diff.file)} + {`\u2066${getDirectory(diff.file)}\u2069`} {getFilename(diff.file)} 0}> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx index d43797d2ef9..fa8a5389c24 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx @@ -507,7 +507,7 @@ export const FullScreenDiffView: Component = (props) =>
- {`\u202A${getDirectory(diff.file)}\u202C`} + {`\u2066${getDirectory(diff.file)}\u2069`} {getFilename(diff.file)} 0}> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index a13c96c5edc..6f3b8f791ce 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -1143,6 +1143,11 @@ button.am-section-toggle:hover .am-section-label { touch-action: none; } +.am-tab-sortable > [data-component="tooltip-trigger"], +.am-tab-sortable > [data-slot="context-menu-trigger"] > [data-component="tooltip-trigger"] { + height: 100%; +} + .am-tab-dragging { opacity: 0.25; } diff --git a/packages/kilo-vscode/webview-ui/diff-virtual/DiffVirtualApp.tsx b/packages/kilo-vscode/webview-ui/diff-virtual/DiffVirtualApp.tsx index 50fd704c536..96841f7b7f6 100644 --- a/packages/kilo-vscode/webview-ui/diff-virtual/DiffVirtualApp.tsx +++ b/packages/kilo-vscode/webview-ui/diff-virtual/DiffVirtualApp.tsx @@ -85,7 +85,7 @@ const DiffVirtualContent: Component = () => { - {`\u202A${directory()}/\u202C`} + {`\u2066${directory()}/\u2069`} {filename()} +{d().additions} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDiff.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDiff.tsx index 110fe1b6fb0..1e2ce56c5cd 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDiff.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PermissionDiff.tsx @@ -64,7 +64,7 @@ export const PermissionDiff: Component = (props) => {
- {directory() && {directory()}/} + {directory() && {`\u2066${directory()}/\u2069`}} {filename()}
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx index ce36fe84673..49fdda93a4a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx @@ -246,7 +246,7 @@ export const VscodeSessionTurn: Component = (props) => { - {`\u202A${getDirectory(diff.file)}\u202C`} + {`\u2066${getDirectory(diff.file)}\u2069`} {getFilename(diff.file)} diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index c05364d5ac2..ea21f0730a4 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -703,7 +703,7 @@ export const SessionProvider: ParentComponent = (props) => { break case "sessionsLoaded": - handleSessionsLoaded(message.sessions) + handleSessionsLoaded(message.sessions, message.preserveSessionIds) break case "sessionUpdated": @@ -1196,16 +1196,20 @@ export const SessionProvider: ParentComponent = (props) => { setStore("todos", sessionID, items) } - function handleSessionsLoaded(loaded: SessionInfo[]) { + function handleSessionsLoaded(loaded: SessionInfo[], preserve?: string[]) { + const kept = preserve?.length ? new Set(preserve) : undefined batch(() => { // Reconcile: remove sessions not in the loaded list to prevent stale // entries from other projects accumulating in the store. + // Sessions whose worktree directories failed to list are preserved — + // their absence is transient, not a real deletion. const ids = new Set(loaded.map((s) => s.id)) setStore( "sessions", produce((sessions) => { for (const id of Object.keys(sessions)) { if (id.startsWith("cloud:")) continue + if (kept?.has(id)) continue if (!ids.has(id)) delete sessions[id] } }), diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index 4134b90beeb..ce7d852dc8a 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -1,7 +1,7 @@ /** @jsxImportSource solid-js */ /** * Stories for Agent Manager components: - * FileTree, DiffPanel, FullScreenDiffView, WorktreeItem + * FileTree, DiffPanel, FullScreenDiffView, WorktreeItem, TabBar */ import type { Meta, StoryObj } from "storybook-solidjs-vite" @@ -10,6 +10,10 @@ import { FileTree } from "../../agent-manager/FileTree" import { DiffPanel } from "../../agent-manager/DiffPanel" import { FullScreenDiffView } from "../../agent-manager/FullScreenDiffView" import { WorktreeItem } from "../../agent-manager/WorktreeItem" +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { Icon } from "@kilocode/kilo-ui/icon" +import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip" +import { ContextMenu } from "@kilocode/kilo-ui/context-menu" import type { WorktreeFileDiff, WorktreeState, WorktreeGitStats, PRStatus } from "../types/messages" import "../../agent-manager/agent-manager.css" import "../../agent-manager/agent-manager-review.css" @@ -444,3 +448,127 @@ export const WorktreeItemGrouped: Story = { ) }, } + +// --------------------------------------------------------------------------- +// TabBar — renders tab bar structure matching SortableTab / SortableReviewTab +// DOM to verify the tooltip-trigger height chain is correct. +// --------------------------------------------------------------------------- + +/** + * Mock tab matching the real SortableTab DOM: + * .am-tab-sortable > [context-menu-trigger] > [tooltip-trigger] > .am-tab + */ +const MockTab = (props: { title: string; active?: boolean }) => ( +
+ + + +
+ {props.title} + + + +
+
+
+
+
+) + +/** Mock review tab matching SortableReviewTab DOM (no ContextMenu wrapper). */ +const MockReviewTab = (props: { active?: boolean }) => ( +
+ +
+ + Review + + + +
+
+
+) + +export const TabBarMultipleTabs: Story = { + name: "TabBar — multiple tabs with active", + render: () => ( + +
+
+
+ + + +
+
+ + + +
+ + +
+
+
+ ), +} + +export const TabBarWithReviewTab: Story = { + name: "TabBar — with review tab", + render: () => ( + +
+
+
+ + +
+
+ + + +
+ + +
+
+
+ ), +} + +export const TabBarSingleTab: Story = { + name: "TabBar — single active tab", + render: () => ( + +
+
+
+ +
+
+ + + +
+ + +
+
+
+ ), +} diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index 83e4e18fcf5..32071d057fc 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -599,6 +599,7 @@ export interface MessageCreatedMessage { export interface SessionsLoadedMessage { type: "sessionsLoaded" sessions: SessionInfo[] + preserveSessionIds?: string[] } export interface CloudSessionsLoadedMessage { diff --git a/packages/opencode/src/bus/global.ts b/packages/opencode/src/bus/global.ts index 43386dd6b20..dc23020d27c 100644 --- a/packages/opencode/src/bus/global.ts +++ b/packages/opencode/src/bus/global.ts @@ -8,3 +8,4 @@ export const GlobalBus = new EventEmitter<{ }, ] }>() +GlobalBus.setMaxListeners(50) // kilocode_change — surface warning if SSE listeners accumulate diff --git a/packages/opencode/src/kilocode/review/worktree-diff.ts b/packages/opencode/src/kilocode/review/worktree-diff.ts index 3bccf81c79c..2d85677c9a9 100644 --- a/packages/opencode/src/kilocode/review/worktree-diff.ts +++ b/packages/opencode/src/kilocode/review/worktree-diff.ts @@ -1,6 +1,5 @@ // kilocode_change - new file import { $ } from "bun" -import { createTwoFilesPatch } from "diff" import fs from "node:fs/promises" import path from "node:path" import z from "zod" @@ -245,7 +244,7 @@ export namespace WorktreeDiff { const additions = meta.status === "added" && meta.additions === 0 && !meta.tracked ? lines(after) : meta.additions return { file: meta.file, - patch: createTwoFilesPatch(meta.file, meta.file, before, after), + patch: "", before, after, additions, diff --git a/packages/opencode/src/server/routes/event.ts b/packages/opencode/src/server/routes/event.ts index 989b857710d..565486d3c3a 100644 --- a/packages/opencode/src/server/routes/event.ts +++ b/packages/opencode/src/server/routes/event.ts @@ -70,14 +70,27 @@ export const EventRoutes = () => stream.onAbort(stop) + // kilocode_change start + // On Windows, stream.onAbort() may never fire after a client disconnects + // (delayed TCP RST detection via IOCP). Without this try/catch, the + // GlobalBus listener, heartbeat interval, and AsyncQueue stay alive + // indefinitely for each dead connection — leaking memory on every + // SSE reconnect. Catching write errors lets us clean up eagerly. try { for await (const data of q) { if (data === null) return - await stream.writeSSE({ data }) + try { + await stream.writeSSE({ data }) + } catch { + log.info("event write failed, cleaning up dead stream") + stop() + return + } } } finally { stop() } + // kilocode_change end }) }, ) diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts index 733a0ef6b27..ddfa4c9acad 100644 --- a/packages/opencode/src/server/routes/global.ts +++ b/packages/opencode/src/server/routes/global.ts @@ -56,14 +56,27 @@ async function streamEvents(c: Context, subscribe: (q: AsyncQueue stream.onAbort(stop) + // kilocode_change start + // On Windows, stream.onAbort() may never fire after a client disconnects + // (delayed TCP RST detection via IOCP). Without this try/catch, the + // GlobalBus listener, heartbeat interval, and AsyncQueue stay alive + // indefinitely for each dead connection — leaking memory on every + // SSE reconnect. Catching write errors lets us clean up eagerly. try { for await (const data of q) { if (data === null) return - await stream.writeSSE({ data }) + try { + await stream.writeSSE({ data }) + } catch { + log.info("global event write failed, cleaning up dead stream") + stop() + return + } } } finally { stop() } + // kilocode_change end }) } diff --git a/packages/opencode/test/kilocode/worktree-diff.test.ts b/packages/opencode/test/kilocode/worktree-diff.test.ts index 9a284a3add5..cb6d08794ae 100644 --- a/packages/opencode/test/kilocode/worktree-diff.test.ts +++ b/packages/opencode/test/kilocode/worktree-diff.test.ts @@ -1,6 +1,7 @@ import { test, expect, describe } from "bun:test" import { $ } from "bun" import { tmpdir } from "../fixture/fixture" +import { WorktreeDiff } from "../../src/kilocode/review/worktree-diff" import path from "path" /** @@ -145,6 +146,19 @@ describe("worktree diff git commands", () => { expect(allFiles).toContain("new-file.py") }) + test("worktree detail does not include unused patch content", async () => { + await using tmp = await setupRepo() + const dir = tmp.path + + await Bun.write(path.join(dir, "existing.txt"), "hello\nmodified\n") + + const detail = await WorktreeDiff.detail({ dir, base: "HEAD", file: "existing.txt" }) + + expect(detail?.patch).toBe("") + expect(detail?.before).toBe("hello\n") + expect(detail?.after).toBe("hello\nmodified\n") + }) + test("worktree scenario: branch with no new commits, only untracked files", async () => { // This is the exact scenario from the screenshot: // - Worktree created from main diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 63542d48e5b..527fcc92931 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -1301,7 +1301,7 @@ function ToolFileAccordion(props: { path: string; actions?: JSX.Element; childre
- {`\u202A${getDirectory(props.path)}\u202C`} + {`\u2066${getDirectory(props.path)}\u2069`} {getFilename(props.path)}
@@ -2112,7 +2112,7 @@ ToolRegistry.register({
- {`\u202A${getDirectory(file.relativePath)}\u202C`} + {`\u2066${getDirectory(file.relativePath)}\u2069`} {getFilename(file.relativePath)}
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 398755d01f9..d874697c6c8 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -509,7 +509,7 @@ export const SessionReview = (props: SessionReviewProps) => {
- {`\u202A${getDirectory(file)}\u202C`} + {`\u2066${getDirectory(file)}\u2069`} {getFilename(file)} diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 29301462ec3..92e5eab8e14 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -499,7 +499,7 @@ export function SessionTurn( - {`\u202A${getDirectory(diff.file)}\u202C`} + {`\u2066${getDirectory(diff.file)}\u2069`} {getFilename(diff.file)}