diff --git a/docs/cli/settings.md b/docs/cli/settings.md index 0f01558d2e..d1b4e0dced 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -74,6 +74,8 @@ they appear in the UI. | Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` | | Show User Identity | `ui.showUserIdentity` | Show the signed-in user's identity (e.g. email) in the UI. | `true` | | Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` | +| Render Process | `ui.renderProcess` | Enable Ink render process for the UI. | `true` | +| Terminal Buffer | `ui.terminalBuffer` | Use the new terminal buffer architecture for rendering. | `true` | | Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` | | Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` | | Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` | diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 87433ef4f1..cb073b2cf6 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -337,6 +337,16 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `false` - **Requires restart:** Yes +- **`ui.renderProcess`** (boolean): + - **Description:** Enable Ink render process for the UI. + - **Default:** `true` + - **Requires restart:** Yes + +- **`ui.terminalBuffer`** (boolean): + - **Description:** Use the new terminal buffer architecture for rendering. + - **Default:** `true` + - **Requires restart:** Yes + - **`ui.useBackgroundColor`** (boolean): - **Description:** Whether to use background colors in the UI. - **Default:** `true` diff --git a/docs/reference/keyboard-shortcuts.md b/docs/reference/keyboard-shortcuts.md index e87c8682df..68b3d884fe 100644 --- a/docs/reference/keyboard-shortcuts.md +++ b/docs/reference/keyboard-shortcuts.md @@ -102,7 +102,8 @@ available combinations. | `app.showFullTodos` | Toggle the full TODO list. | `Ctrl+T` | | `app.showIdeContextDetail` | Show IDE context details. | `Ctrl+G` | | `app.toggleMarkdown` | Toggle Markdown rendering. | `Alt+M` | -| `app.toggleCopyMode` | Toggle copy mode when in alternate buffer mode. | `Ctrl+S` | +| `app.toggleCopyMode` | Toggle copy mode when in alternate buffer mode. | `F9` | +| `app.toggleMouseMode` | Toggle mouse mode (scrolling and clicking). | `Ctrl+S` | | `app.toggleYolo` | Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl+Y` | | `app.cycleApprovalMode` | Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy. | `Shift+Tab` | | `app.showMoreLines` | Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl+O` | @@ -126,6 +127,9 @@ available combinations. | `background.unfocus` | Move focus from background shell to Gemini. | `Shift+Tab` | | `background.unfocusList` | Move focus from background shell list to Gemini. | `Tab` | | `background.unfocusWarning` | Show warning when trying to move focus away from background shell. | `Tab` | +| `app.dumpFrame` | Dump the current frame as a snapshot. | `F8` | +| `app.startRecording` | Start recording the session. | `F6` | +| `app.stopRecording` | Stop recording the session. | `F7` | #### Extension Controls diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap index f4b3a35884..0763decd4e 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap @@ -6,7 +6,7 @@ exports[`ToolResultDisplay > does not fall back to plain text if availableHeight `; exports[`ToolResultDisplay > keeps markdown if in alternate buffer even with availableHeight 1`] = ` -"Some result +"Some result " `; diff --git a/packages/cli/src/ui/hooks/useExecutionLifecycle.ts b/packages/cli/src/ui/hooks/useExecutionLifecycle.ts index e0b5c3ffaa..06df7e8d3f 100644 --- a/packages/cli/src/ui/hooks/useExecutionLifecycle.ts +++ b/packages/cli/src/ui/hooks/useExecutionLifecycle.ts @@ -525,47 +525,81 @@ export const useExecutionLifecycle = ( dispatch({ type: 'SET_ACTIVE_PTY', pid: null }); } - let mainContent: string; + let finalDisplayOutput: string | AnsiOutput; + let finalTextOutput: string; + if (isBinaryStream || isBinary(result.rawOutput)) { - mainContent = + finalTextOutput = '[Command produced binary output, which is not shown.]'; + finalDisplayOutput = finalTextOutput; } else { - mainContent = + finalTextOutput = result.output.trim() || '(Command produced no output)'; + finalDisplayOutput = Array.isArray(cumulativeStdout) + ? cumulativeStdout + : finalTextOutput; } - let finalOutput = mainContent; let finalStatus = CoreToolCallStatus.Success; + const prefixWarnings: string[] = []; if (result.error) { finalStatus = CoreToolCallStatus.Error; - finalOutput = `${result.error.message}\n${finalOutput}`; + prefixWarnings.push(result.error.message); } else if (result.aborted) { finalStatus = CoreToolCallStatus.Cancelled; - finalOutput = `Command was cancelled.\n${finalOutput}`; + prefixWarnings.push('Command was cancelled.'); } else if (result.backgrounded) { finalStatus = CoreToolCallStatus.Success; - finalOutput = `Command moved to background (PID: ${result.pid}). Output hidden. Press Ctrl+B to view.`; + finalDisplayOutput = `Command moved to background (PID: ${result.pid}). Output hidden. Press Ctrl+B to view.`; + finalTextOutput = finalDisplayOutput; + prefixWarnings.length = 0; } else if (result.signal) { finalStatus = CoreToolCallStatus.Error; - finalOutput = `Command terminated by signal: ${result.signal}.\n${finalOutput}`; - } else if (result.exitCode !== 0) { + prefixWarnings.push( + `Command terminated by signal: ${result.signal}.`, + ); + } else if (result.exitCode !== null && result.exitCode !== 0) { finalStatus = CoreToolCallStatus.Error; - finalOutput = `Command exited with code ${result.exitCode}.\n${finalOutput}`; + prefixWarnings.push(`Command exited with code ${result.exitCode}.`); } if (pwdFilePath && fs.existsSync(pwdFilePath)) { const finalPwd = fs.readFileSync(pwdFilePath, 'utf8').trim(); if (finalPwd && finalPwd !== targetDir) { - const warning = `WARNING: shell mode is stateless; the directory change to '${finalPwd}' will not persist.`; - finalOutput = `${warning}\n\n${finalOutput}`; + prefixWarnings.push( + `WARNING: shell mode is stateless; the directory change to '${finalPwd}' will not persist.\n`, + ); + } + } + + if (prefixWarnings.length > 0) { + const prefixString = prefixWarnings.join('\n') + '\n'; + finalTextOutput = prefixString + finalTextOutput; + + if (typeof finalDisplayOutput === 'string') { + finalDisplayOutput = prefixString + finalDisplayOutput; + } else { + const ansiPrefix = prefixString.split('\n').map((line) => [ + { + text: line, + bold: false, + italic: false, + underline: false, + dim: false, + inverse: false, + fg: '', + bg: '', + }, + ]); + finalDisplayOutput = [...ansiPrefix, ...finalDisplayOutput]; } } const finalToolDisplay: IndividualToolCallDisplay = { ...initialToolDisplay, status: finalStatus, - resultDisplay: finalOutput, + resultDisplay: finalDisplayOutput, }; if (finalStatus !== CoreToolCallStatus.Cancelled) { @@ -578,7 +612,11 @@ export const useExecutionLifecycle = ( ); } - addShellCommandToGeminiHistory(geminiClient, rawQuery, finalOutput); + addShellCommandToGeminiHistory( + geminiClient, + rawQuery, + finalTextOutput, + ); } catch (err) { setPendingHistoryItem(null); const errorMessage = err instanceof Error ? err.message : String(err); diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 63a9b1dc83..1b5289a552 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -473,7 +473,7 @@ export class ShellToolInvocation extends BaseToolInvocation< llmContent = llmContentParts.join('\n'); } - let returnDisplayMessage = ''; + let returnDisplayMessage: string | AnsiOutput = ''; if (this.context.config.getDebugMode()) { returnDisplayMessage = llmContent; } else { @@ -486,6 +486,8 @@ export class ShellToolInvocation extends BaseToolInvocation< } else { returnDisplayMessage = cancelMsg; } + } else if (Array.isArray(cumulativeOutput)) { + returnDisplayMessage = cumulativeOutput; } else if (result.output.trim()) { returnDisplayMessage = result.output; } else { diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 7d78a2e323..367f4c7347 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -455,6 +455,20 @@ "default": false, "type": "boolean" }, + "renderProcess": { + "title": "Render Process", + "description": "Enable Ink render process for the UI.", + "markdownDescription": "Enable Ink render process for the UI.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `true`", + "default": true, + "type": "boolean" + }, + "terminalBuffer": { + "title": "Terminal Buffer", + "description": "Use the new terminal buffer architecture for rendering.", + "markdownDescription": "Use the new terminal buffer architecture for rendering.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `true`", + "default": true, + "type": "boolean" + }, "useBackgroundColor": { "title": "Use Background Color", "description": "Whether to use background colors in the UI.",