Skip to content

Commit 01b44fb

Browse files
Refactor terminal API to support incremental output streaming (#162)
* Initial plan * Update terminal API to support async/incremental output processing Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> * Address code review: Use crypto.randomUUID() and properly lookup commands by ID Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> * Add fallback for crypto.randomUUID() for better compatibility Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> * Use simple sequential counter for commandId per terminalId Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com>
1 parent 595151c commit 01b44fb

File tree

3 files changed

+74
-22
lines changed

3 files changed

+74
-22
lines changed

app/terminal/embedContext.tsx

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,16 @@ interface IEmbedContext {
3232
) => Promise<Readonly<Record<Filename, string>>>;
3333

3434
replOutputs: Readonly<Record<TerminalId, ReplCommand[]>>;
35+
addReplCommand: (terminalId: TerminalId, command: string) => string;
3536
addReplOutput: (
3637
terminalId: TerminalId,
37-
command: string,
38-
output: ReplOutput[]
38+
commandId: string,
39+
output: ReplOutput
3940
) => void;
4041

4142
execResults: Readonly<Record<Filename, ReplOutput[]>>;
42-
setExecResult: (filename: Filename, output: ReplOutput[]) => void;
43+
clearExecResult: (filename: Filename) => void;
44+
addExecOutput: (filename: Filename, output: ReplOutput) => void;
4345
}
4446
const EmbedContext = createContext<IEmbedContext>(null!);
4547

@@ -63,6 +65,9 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) {
6365
const [replOutputs, setReplOutputs] = useState<
6466
Record<TerminalId, ReplCommand[]>
6567
>({});
68+
const [commandIdCounters, setCommandIdCounters] = useState<
69+
Record<TerminalId, number>
70+
>({});
6671
const [execResults, setExecResults] = useState<
6772
Record<Filename, ReplOutput[]>
6873
>({});
@@ -71,6 +76,7 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) {
7176
if (pathname && pathname !== currentPathname) {
7277
setCurrentPathname(pathname);
7378
setReplOutputs({});
79+
setCommandIdCounters({});
7480
setExecResults({});
7581
}
7682
}, [pathname, currentPathname]);
@@ -100,26 +106,70 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) {
100106
},
101107
[pathname]
102108
);
103-
const addReplOutput = useCallback(
104-
(terminalId: TerminalId, command: string, output: ReplOutput[]) =>
109+
const addReplCommand = useCallback(
110+
(terminalId: TerminalId, command: string): string => {
111+
let commandId = "";
112+
setCommandIdCounters((counters) => {
113+
const newCounters = { ...counters };
114+
const currentCount = newCounters[terminalId] ?? 0;
115+
commandId = String(currentCount);
116+
newCounters[terminalId] = currentCount + 1;
117+
return newCounters;
118+
});
105119
setReplOutputs((outs) => {
106120
outs = { ...outs };
107121
if (!(terminalId in outs)) {
108122
outs[terminalId] = [];
109123
}
110124
outs[terminalId] = [
111125
...outs[terminalId],
112-
{ command: command, output: output },
126+
{ command: command, output: [], commandId },
113127
];
114128
return outs;
129+
});
130+
return commandId;
131+
},
132+
[]
133+
);
134+
const addReplOutput = useCallback(
135+
(terminalId: TerminalId, commandId: string, output: ReplOutput) =>
136+
setReplOutputs((outs) => {
137+
outs = { ...outs };
138+
if (terminalId in outs) {
139+
outs[terminalId] = [...outs[terminalId]];
140+
// Find the command by commandId
141+
const commandIndex = outs[terminalId].findIndex(
142+
(cmd) => cmd.commandId === commandId
143+
);
144+
if (commandIndex >= 0) {
145+
const command = outs[terminalId][commandIndex];
146+
outs[terminalId][commandIndex] = {
147+
...command,
148+
output: [...command.output, output],
149+
};
150+
}
151+
}
152+
return outs;
153+
}),
154+
[]
155+
);
156+
const clearExecResult = useCallback(
157+
(filename: Filename) =>
158+
setExecResults((results) => {
159+
results = { ...results };
160+
results[filename] = [];
161+
return results;
115162
}),
116163
[]
117164
);
118-
const setExecResult = useCallback(
119-
(filename: Filename, output: ReplOutput[]) =>
165+
const addExecOutput = useCallback(
166+
(filename: Filename, output: ReplOutput) =>
120167
setExecResults((results) => {
121168
results = { ...results };
122-
results[filename] = output;
169+
if (!(filename in results)) {
170+
results[filename] = [];
171+
}
172+
results[filename] = [...results[filename], output];
123173
return results;
124174
}),
125175
[]
@@ -131,9 +181,11 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) {
131181
files: files[pathname] || {},
132182
writeFile,
133183
replOutputs,
184+
addReplCommand,
134185
addReplOutput,
135186
execResults,
136-
setExecResult,
187+
clearExecResult,
188+
addExecOutput,
137189
}}
138190
>
139191
{children}

app/terminal/exec.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function ExecFile(props: ExecProps) {
3232
}
3333
},
3434
});
35-
const { files, setExecResult } = useEmbedContext();
35+
const { files, clearExecResult, addExecOutput } = useEmbedContext();
3636

3737
const { ready, runFiles, getCommandlineStr } = useRuntime(props.language);
3838

@@ -46,10 +46,12 @@ export function ExecFile(props: ExecProps) {
4646
(async () => {
4747
clearTerminal(terminalInstanceRef.current!);
4848
terminalInstanceRef.current!.write(systemMessageColor("実行中です..."));
49-
const outputs: ReplOutput[] = [];
49+
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
50+
const filenameKey = props.filenames.join(",");
51+
clearExecResult(filenameKey);
5052
let isFirstOutput = true;
5153
await runFiles(props.filenames, files, (output) => {
52-
outputs.push(output);
54+
addExecOutput(filenameKey, output);
5355
if (isFirstOutput) {
5456
// Clear "実行中です..." message only on first output
5557
clearTerminal(terminalInstanceRef.current!);
@@ -63,10 +65,7 @@ export function ExecFile(props: ExecProps) {
6365
null, // ファイル実行で"return"メッセージが返ってくることはないはずなので、Prismを渡す必要はない
6466
props.language
6567
);
66-
// TODO: 実行が完了したあとに出力された場合、embedContextのsetExecResultにも出力を追加する必要があるが、それに対応したAPIになっていない
6768
});
68-
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
69-
setExecResult(props.filenames.join(","), outputs);
7069
setExecutionState("idle");
7170
})();
7271
}
@@ -75,7 +74,8 @@ export function ExecFile(props: ExecProps) {
7574
ready,
7675
props.filenames,
7776
runFiles,
78-
setExecResult,
77+
clearExecResult,
78+
addExecOutput,
7979
terminalInstanceRef,
8080
props.language,
8181
files,

app/terminal/repl.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface ReplOutput {
3131
export interface ReplCommand {
3232
command: string;
3333
output: ReplOutput[];
34+
commandId?: string; // Optional for backward compatibility
3435
}
3536
export type SyntaxStatus = "complete" | "incomplete" | "invalid"; // 構文チェックの結果
3637

@@ -80,7 +81,7 @@ export function ReplTerminal({
8081
language,
8182
initContent,
8283
}: ReplComponentProps) {
83-
const { addReplOutput } = useEmbedContext();
84+
const { addReplCommand, addReplOutput } = useEmbedContext();
8485

8586
const [Prism, setPrism] = useState<typeof import("prismjs") | null>(null);
8687
useEffect(() => {
@@ -217,7 +218,7 @@ export function ReplTerminal({
217218
terminalInstanceRef.current.writeln("");
218219
const command = inputBuffer.current.join("\n").trim();
219220
inputBuffer.current = [];
220-
const collectedOutputs: ReplOutput[] = [];
221+
const commandId = addReplCommand(terminalId, command);
221222
let executionDone = false;
222223
await runtimeMutex.runExclusive(async () => {
223224
await runCommand(command, (output) => {
@@ -226,16 +227,14 @@ export function ReplTerminal({
226227
updateBuffer(null, () => {
227228
handleOutput(output);
228229
});
229-
// TODO: embedContextのaddReplOutputにも出力を追加する必要があるが、それに対応したAPIになっていない
230230
} else {
231-
collectedOutputs.push(output);
232231
handleOutput(output);
233232
}
233+
addReplOutput(terminalId, commandId, output);
234234
});
235235
});
236236
executionDone = true;
237237
updateBuffer(() => [""]);
238-
addReplOutput?.(terminalId, command, collectedOutputs);
239238
}
240239
} else if (code === 127) {
241240
// Backspace
@@ -279,6 +278,7 @@ export function ReplTerminal({
279278
runCommand,
280279
handleOutput,
281280
tabSize,
281+
addReplCommand,
282282
addReplOutput,
283283
terminalId,
284284
terminalInstanceRef,

0 commit comments

Comments
 (0)