Skip to content
Closed
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
5 changes: 3 additions & 2 deletions client/electron/embedded-terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
import {
DEFAULT_PENDING_TERMINAL_OUTPUT_MAX_CHARS,
appendBoundedTerminalOutput,
terminalReplayBufferTail,
} from "./terminal-buffer.js";
import { OutputAckGate } from "./terminal-output-ack.js";
import { agentConfig, terminalLaunch } from "./terminal-launch.js";
Expand Down Expand Up @@ -250,8 +251,8 @@ export function getEmbeddedTerminalBuffer(id: string): string {
return outputBuffers.get(id) ?? "";
}

export function attachEmbeddedTerminalBuffer(id: string): string {
const buffer = getEmbeddedTerminalBuffer(id);
export function attachEmbeddedTerminalBuffer(id: string, maxChars?: number | null): string {
const buffer = terminalReplayBufferTail(getEmbeddedTerminalBuffer(id), maxChars);
pendingOutput.delete(id);
outputAckGate.clear(id);
if (pendingOutput.size === 0) clearOutputFlushTimer();
Expand Down
2 changes: 1 addition & 1 deletion client/electron/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export function registerIpcHandlers(appRoot: string): void {
handle("embeddedTerminal:restore", (_event, allowedWorkspaces?: string[]): Promise<EmbeddedTerminalSession[]> =>
restoreEmbeddedTerminals(allowedWorkspaces),
);
handle("embeddedTerminal:attachBuffer", (_event, id: string): string => attachEmbeddedTerminalBuffer(id));
handle("embeddedTerminal:attachBuffer", (_event, id: string, maxChars?: number): string => attachEmbeddedTerminalBuffer(id, maxChars));
handle("embeddedTerminal:buffer", (_event, id: string): string => getEmbeddedTerminalBuffer(id));
handle("agentMessages:list", (_event, workspace?: string, limit?: number): AgentMessage[] => listEmbeddedAgentMessages(workspace, limit));
handle("agentMessages:send", (_event, request: SendAgentMessageRequest): Promise<SendAgentMessageResult> => sendAgentMessage({ ...request, source: "ui" }));
Expand Down
4 changes: 2 additions & 2 deletions client/electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export type WorkspaceApi = {
writeEmbeddedTerminal: (id: string, data: string) => Promise<EmbeddedTerminalSession>;
renameEmbeddedTerminal: (id: string, title: string) => Promise<EmbeddedTerminalSession>;
resizeEmbeddedTerminal: (id: string, cols: number, rows: number) => Promise<EmbeddedTerminalSession>;
attachEmbeddedTerminalBuffer: (id: string) => Promise<string>;
attachEmbeddedTerminalBuffer: (id: string, maxChars?: number) => Promise<string>;
getEmbeddedTerminalBuffer: (id: string) => Promise<string>;
listAgentMessages: (workspace?: string, limit?: number) => Promise<AgentMessage[]>;
sendAgentMessage: (request: SendAgentMessageRequest) => Promise<SendAgentMessageResult>;
Expand Down Expand Up @@ -257,7 +257,7 @@ const api: WorkspaceApi = {
writeEmbeddedTerminal: (id, data) => ipcRenderer.invoke("embeddedTerminal:write", id, data),
renameEmbeddedTerminal: (id, title) => ipcRenderer.invoke("embeddedTerminal:rename", id, title),
resizeEmbeddedTerminal: (id, cols, rows) => ipcRenderer.invoke("embeddedTerminal:resize", id, cols, rows),
attachEmbeddedTerminalBuffer: (id) => ipcRenderer.invoke("embeddedTerminal:attachBuffer", id),
attachEmbeddedTerminalBuffer: (id, maxChars) => ipcRenderer.invoke("embeddedTerminal:attachBuffer", id, maxChars),
getEmbeddedTerminalBuffer: (id) => ipcRenderer.invoke("embeddedTerminal:buffer", id),
listAgentMessages: (workspace, limit) => ipcRenderer.invoke("agentMessages:list", workspace, limit),
sendAgentMessage: (request) => ipcRenderer.invoke("agentMessages:send", request),
Expand Down
10 changes: 9 additions & 1 deletion client/electron/terminal-buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
export const DEFAULT_TERMINAL_BUFFER_MAX_CHARS = 40_000;
export const MIN_TERMINAL_BUFFER_MAX_CHARS = 1_000;
export const MAX_TERMINAL_BUFFER_MAX_CHARS = 200_000;
export const DEFAULT_TERMINAL_REPLAY_MAX_CHARS = DEFAULT_TERMINAL_BUFFER_MAX_CHARS;
export const DEFAULT_PENDING_TERMINAL_OUTPUT_MAX_CHARS = 64_000;
export const TERMINAL_OUTPUT_TRUNCATED_NOTICE = "\r\n\x1b[33m[Athena truncated terminal output backlog]\x1b[0m\r\n";

Expand All @@ -13,7 +14,7 @@ export type TerminalBufferResult = {
max_chars: number;
};

export function boundedTerminalBufferMaxChars(value: string | null): number {
export function boundedTerminalBufferMaxChars(value: string | number | null | undefined): number {
const parsed = Number(value ?? DEFAULT_TERMINAL_BUFFER_MAX_CHARS);
if (!Number.isFinite(parsed)) return DEFAULT_TERMINAL_BUFFER_MAX_CHARS;
return Math.max(
Expand All @@ -26,6 +27,13 @@ export function terminalBufferTail(value: string, maxChars: number): string {
return value.length > maxChars ? value.slice(-maxChars) : value;
}

export function terminalReplayBufferTail(
value: string,
maxChars: string | number | null | undefined = DEFAULT_TERMINAL_REPLAY_MAX_CHARS,
): string {
return terminalBufferTail(value, boundedTerminalBufferMaxChars(maxChars));
}

export function formatTerminalBuffer(value: string, maxChars: number): TerminalBufferResult {
const buffer = terminalBufferTail(value, maxChars);
return {
Expand Down
2 changes: 1 addition & 1 deletion client/src/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ type WorkspaceApi = {
writeEmbeddedTerminal: (id: string, data: string) => Promise<EmbeddedTerminalSession>;
renameEmbeddedTerminal: (id: string, title: string) => Promise<EmbeddedTerminalSession>;
resizeEmbeddedTerminal: (id: string, cols: number, rows: number) => Promise<EmbeddedTerminalSession>;
attachEmbeddedTerminalBuffer: (id: string) => Promise<string>;
attachEmbeddedTerminalBuffer: (id: string, maxChars?: number) => Promise<string>;
getEmbeddedTerminalBuffer: (id: string) => Promise<string>;
listAgentMessages: (workspace?: string, limit?: number) => Promise<AgentMessage[]>;
sendAgentMessage: (request: SendAgentMessageRequest) => Promise<SendAgentMessageResult>;
Expand Down
16 changes: 16 additions & 0 deletions client/tests/terminal-buffer.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import test from "node:test";

import {
DEFAULT_PENDING_TERMINAL_OUTPUT_MAX_CHARS,
DEFAULT_TERMINAL_REPLAY_MAX_CHARS,
TERMINAL_OUTPUT_TRUNCATED_NOTICE,
appendBoundedTerminalOutput,
boundedTerminalBufferMaxChars,
formatTerminalBuffer,
terminalBufferTail,
terminalReplayBufferTail,
} from "../dist-electron/terminal-buffer.js";

test("terminal buffer max chars uses default and clamps bounds", () => {
Expand All @@ -31,6 +33,20 @@ test("format terminal buffer reports returned char count and limit", () => {
});
});

test("terminal replay buffer defaults to the smaller renderer replay cap", () => {
const output = "x".repeat(DEFAULT_TERMINAL_REPLAY_MAX_CHARS + 5);

assert.equal(terminalReplayBufferTail(output).length, DEFAULT_TERMINAL_REPLAY_MAX_CHARS);
assert.equal(terminalReplayBufferTail(output), output.slice(-DEFAULT_TERMINAL_REPLAY_MAX_CHARS));
});

test("terminal replay buffer accepts explicit bounded caps", () => {
const output = "abcdef".repeat(1000);

assert.equal(terminalReplayBufferTail(output, 200_000), output);
assert.equal(terminalReplayBufferTail(output, 10), output.slice(-1_000));
});

test("pending terminal output is capped to the newest text", () => {
const maxChars = TERMINAL_OUTPUT_TRUNCATED_NOTICE.length + 4;
const output = "abcdefghij".repeat(10);
Expand Down