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
11 changes: 8 additions & 3 deletions apps/desktop/src/main/todo-daemon/pty-turn-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const CLAUDE_BIN =
const CLAUDE_PROJECTS_ROOT = path.join(os.homedir(), ".claude", "projects");

/** How long we wait after spawn for the JSONL file to appear. */
const JSONL_DISCOVERY_TIMEOUT_MS = 15_000;
const JSONL_DISCOVERY_TIMEOUT_MS = 30_000;

/** How often we poll the JSONL file for appended lines. */
const JSONL_POLL_INTERVAL_MS = 250;
Expand Down Expand Up @@ -330,14 +330,19 @@ export async function runClaudeTurnPty(

if (!state.jsonlPath) {
const runtimeSid = hookSink.readRuntimeSessionId();
const foundJsonls = listJsonl(projectDir);
const foundStr =
foundJsonls.length > 0
? `projectDir に存在するファイル: ${foundJsonls.slice(0, 5).join(", ")}${foundJsonls.length > 5 ? ` 他${foundJsonls.length - 5}件` : ""}`
: "projectDir に .jsonl ファイルが見つかりません";
return {
result: null,
sessionId: state.claudeSessionId,
costUsd: null,
numTurns: null,
error: runtimeSid
? `Claude Code のセッション JSONL (${runtimeSid}.jsonl) が発見できませんでした`
: "SessionStart hook が発火しなかったため JSONL を同定できませんでした (PTY 起動は成功)",
? `Claude Code のセッション JSONL (${runtimeSid}.jsonl) が発見できませんでした — ${foundStr}`
: `SessionStart hook が発火しなかったため JSONL を同定できませんでした (PTY 起動は成功) — ${foundStr}`,
Comment thread
MocA-Love marked this conversation as resolved.
interrupted: false,
scheduledWakeup: null,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { setPaneWorkspaceRunState } from "renderer/stores/tabs/workspace-run";
import { DEBUG_TERMINAL } from "../config";
import { logTerminalWrite, terminalRendererDebug } from "../debug";
import type { TerminalExitReason, TerminalStreamEvent } from "../types";
import { flushWrite, scheduleWrite } from "../v1-terminal-cache";

export interface UseTerminalStreamOptions {
paneId: string;
Expand Down Expand Up @@ -192,15 +193,18 @@ export function useTerminalStream({

updateModesRef.current(event.data);
logTerminalWrite("stream-data", event.data.length, { paneId });
xterm.write(event.data);
scheduleWrite(paneId, event.data);
updateCwdRef.current(event.data);
} else if (event.type === "exit") {
flushWrite(paneId);
handleTerminalExit(event.exitCode, xterm, event.reason);
Comment thread
MocA-Love marked this conversation as resolved.
} else if (event.type === "disconnect") {
flushWrite(paneId);
setConnectionError(
event.reason || "Connection to terminal daemon lost",
);
} else if (event.type === "error") {
flushWrite(paneId);
handleStreamError(event, xterm);
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export interface CachedTerminal {
subscriptionErrorHandler: ((error: unknown) => void) | null;
/** ResizeObserver for the attached container. Managed by attach/detach. */
resizeObserver: ResizeObserver | null;
/** rAF-batched write buffer: data accumulates here until the next frame. */
rafWriteBuffer: string;
rafWriteId: ReturnType<typeof requestAnimationFrame> | null;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

const cache = new Map<string, CachedTerminal>();
Expand Down Expand Up @@ -95,6 +98,8 @@ export function getOrCreate(
resizeObserver: null,
lastCols: xterm.cols,
lastRows: xterm.rows,
rafWriteBuffer: "",
rafWriteId: null,
};

cache.set(paneId, entry);
Expand Down Expand Up @@ -212,6 +217,48 @@ export function updateAppearance(
};
}

// --- rAF write buffer ---

/**
* Batch xterm.write calls into one per animation frame to reduce the number
* of parser/render cycles. Callers accumulate data here; the actual write
* fires in the next rAF, coalescing all chunks that arrived within ~16 ms.
*/
export function scheduleWrite(paneId: string, data: string): void {
const entry = cache.get(paneId);
if (!entry) return;
entry.rafWriteBuffer += data;
if (entry.rafWriteId === null) {
entry.rafWriteId = requestAnimationFrame(() => {
const e = cache.get(paneId);
Comment thread
MocA-Love marked this conversation as resolved.
if (!e) return;
if (e.rafWriteBuffer) {
e.xterm.write(e.rafWriteBuffer);
e.rafWriteBuffer = "";
}
e.rafWriteId = null;
});
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Immediately flush any buffered data to xterm, cancelling the pending rAF.
* Must be called before processing exit/error/disconnect events so that
* trailing output is rendered before the exit banner or pane disposal.
*/
export function flushWrite(paneId: string): void {
const entry = cache.get(paneId);
if (!entry) return;
if (entry.rafWriteId !== null) {
cancelAnimationFrame(entry.rafWriteId);
entry.rafWriteId = null;
}
if (entry.rafWriteBuffer) {
entry.xterm.write(entry.rafWriteBuffer);
entry.rafWriteBuffer = "";
}
}

// --- Stream subscription ---

function routeEvent(
Expand Down Expand Up @@ -243,8 +290,9 @@ function routeEvent(
data: { paneId },
});
logTerminalWrite("hidden-stream-data", event.data.length, { paneId });
entry.xterm.write(event.data);
scheduleWrite(paneId, event.data);
} else {
flushWrite(paneId);
entry.pendingLifecycleEvents.push(event);
}
}
Expand Down Expand Up @@ -402,6 +450,9 @@ export function dispose(paneId: string): void {

entry.resizeObserver?.disconnect();
entry.subscription?.unsubscribe();
if (entry.rafWriteId !== null) {
cancelAnimationFrame(entry.rafWriteId);
}
entry.cleanupCreation();
entry.xterm.dispose();
cache.delete(paneId);
Expand All @@ -415,6 +466,8 @@ if (hot) {
| undefined;
if (existing) {
for (const [k, v] of existing) {
v.rafWriteBuffer ??= "";
v.rafWriteId ??= null;
cache.set(k, v);
}
}
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/renderer/stores/tabs/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ import {
resolveActiveTabIdForWorkspace,
resolveFileViewerMode,
} from "./utils";
import { killTerminalForPane } from "./utils/terminal-cleanup";
import {
killTerminalForPane,
releaseTerminalCache,
} from "./utils/terminal-cleanup";

const DEFAULT_FILE_VIEWER_SPLIT_PERCENTAGE = 50;

Expand Down Expand Up @@ -492,6 +495,7 @@ export const useTabsStore = create<TabsStore>()(
const newPanes = { ...state.panes };
for (const paneId of paneIds) {
delete newPanes[paneId];
releaseTerminalCache(paneId);
}

const newTabs = state.tabs.filter((t) => t.id !== tabId);
Expand Down Expand Up @@ -670,6 +674,7 @@ export const useTabsStore = create<TabsStore>()(
killTerminalForPane(paneId);
}
delete newPanes[paneId];
releaseTerminalCache(paneId);
}
}

Expand Down Expand Up @@ -1256,6 +1261,7 @@ export const useTabsStore = create<TabsStore>()(
const newPanes = { ...state.panes };
for (const id of paneIdsToRemove) {
delete newPanes[id];
releaseTerminalCache(id);
}

let newFocusedPaneIds = state.focusedPaneIds;
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { rejectTerminalSessionReady } from "../../../lib/terminal/session-readiness";
import { electronTrpcClient } from "../../../lib/trpc-client";
import * as v1TerminalCache from "../../../screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/v1-terminal-cache";

/**
* Uses standalone tRPC client to avoid React hook dependencies
Expand All @@ -13,3 +14,14 @@ export const killTerminalForPane = (paneId: string): void => {
console.warn(`Failed to kill terminal for pane ${paneId}:`, error);
});
};

/**
* Release xterm/WebGL resources for a terminal pane.
* Call this AFTER the pane has been removed from the store so that the React
* component is already unmounted (or will unmount in the same batch) before
* xterm is disposed. dispose() is idempotent — safe to call even if the
* useTerminalLifecycle cleanup already ran.
*/
export const releaseTerminalCache = (paneId: string): void => {
v1TerminalCache.dispose(paneId);
};
2 changes: 1 addition & 1 deletion apps/desktop/src/shared/debug-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ interface AggregateState {
timer: ReturnType<typeof setTimeout> | null;
}

const DEFAULT_AGGREGATE_INTERVAL_MS = 1_000;
const DEFAULT_AGGREGATE_INTERVAL_MS = 30_000;
const DEFAULT_MAX_STRING_LENGTH = 500;

function truncateString(value: string, maxLength: number): string {
Expand Down
Loading