From 8adf48abd328c6e8fa2a6d55df8288fd5a7b92d5 Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Fri, 6 Feb 2026 09:09:21 -0500 Subject: [PATCH] feat: inline permission prompt and improve TUI layout - Render permission modal inline below log panel instead of floating CenteredModal overlay - Hide input area when permission prompt is active - Add dev:debug and dev:debug-brk scripts for Bun inspector debugging - Add background color to header to prevent content bleeding through - Add margin spacing between header and log panel - Change permission modal border to top-only for cleaner inline appearance --- .gitignore | 1 + package.json | 2 + .../components/callbacks/on-tool-call.ts | 1 + src/services/chat-tui/streaming.ts | 3 +- src/tui-solid/app.tsx | 24 +++- src/tui-solid/components/layout/header.tsx | 2 + .../components/modals/permission-modal.tsx | 5 +- src/tui-solid/routes/session.tsx | 26 ++--- src/utils/ansi.ts | 108 ++++++++++++++++++ 9 files changed, 152 insertions(+), 20 deletions(-) create mode 100644 src/utils/ansi.ts diff --git a/.gitignore b/.gitignore index 8af226e..2bcd8ed 100644 --- a/.gitignore +++ b/.gitignore @@ -261,3 +261,4 @@ npm-debug.log* # Other .vscode-test/ coverage-final.json +.codetyper-backup diff --git a/package.json b/package.json index e74e401..dba7edb 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "dev": "bun src/index.ts", "dev:nobump": "bun scripts/build.ts && npm link", "dev:watch": "bun scripts/dev-watch.ts", + "dev:debug": "bun --inspect=localhost:6499 src/index.ts", + "dev:debug-brk": "bun --inspect-brk=localhost:6499 src/index.ts", "build": "bun scripts/build.ts", "sync-version": "bun scripts/sync-version.ts", "start": "bun src/index.ts", diff --git a/src/commands/components/callbacks/on-tool-call.ts b/src/commands/components/callbacks/on-tool-call.ts index 92ab794..126af28 100644 --- a/src/commands/components/callbacks/on-tool-call.ts +++ b/src/commands/components/callbacks/on-tool-call.ts @@ -19,6 +19,7 @@ export const onToolCall = (call: ToolCallParams): void => { toolName: call.name, toolStatus: "running", toolDescription: call.description, + toolArgs: call.args, quiet: isQuiet, }, }); diff --git a/src/services/chat-tui/streaming.ts b/src/services/chat-tui/streaming.ts index be6ba9d..6c725bd 100644 --- a/src/services/chat-tui/streaming.ts +++ b/src/services/chat-tui/streaming.ts @@ -98,7 +98,8 @@ const createAgentOptionsWithTUI = ( metadata: { toolName: toolCall.name, toolStatus: "running", - toolDescription: JSON.stringify(toolCall.arguments), + toolDescription: `Executing ${toolCall.name}`, + toolArgs: toolCall.arguments, }, }); diff --git a/src/tui-solid/app.tsx b/src/tui-solid/app.tsx index 5db9e97..d53f6a5 100644 --- a/src/tui-solid/app.tsx +++ b/src/tui-solid/app.tsx @@ -16,6 +16,7 @@ import { advanceStep, getExecutionState, } from "@services/chat-tui-service"; +import { DISABLE_MOUSE_TRACKING } from "@constants/terminal"; import versionData from "@/version.json"; import { ExitProvider, useExit } from "@tui-solid/context/exit"; import { RouteProvider, useRoute } from "@tui-solid/context/route"; @@ -179,7 +180,11 @@ function AppContent(props: AppProps) { const toggled = togglePauseResume(); if (toggled) { const state = getExecutionState(); - toast.info(state.state === "paused" ? "⏸ Execution paused" : "▶ Execution resumed"); + toast.info( + state.state === "paused" + ? "⏸ Execution paused" + : "▶ Execution resumed", + ); evt.preventDefault(); return; } @@ -191,7 +196,9 @@ function AppContent(props: AppProps) { if (state.state !== "idle") { abortCurrentOperation(true).then((aborted) => { if (aborted) { - toast.info(`Aborted with rollback of ${state.rollbackCount} action(s)`); + toast.info( + `Aborted with rollback of ${state.rollbackCount} action(s)`, + ); } }); evt.preventDefault(); @@ -205,7 +212,9 @@ function AppContent(props: AppProps) { if (state.state !== "idle") { const isStepMode = state.state === "stepping"; setStepMode(!isStepMode); - toast.info(isStepMode ? "🏃 Step mode disabled" : "🚶 Step mode enabled"); + toast.info( + isStepMode ? "🏃 Step mode disabled" : "🚶 Step mode enabled", + ); evt.preventDefault(); return; } @@ -511,11 +520,16 @@ export interface TuiRenderOptions extends TuiInput { export function tui(options: TuiRenderOptions): Promise { return new Promise((resolve) => { - render(() => , { + const handleExit = (output: TuiOutput): void => { + process.stdout.write(DISABLE_MOUSE_TRACKING); + resolve(output); + }; + + render(() => , { targetFps: 60, exitOnCtrlC: false, useKittyKeyboard: {}, - useMouse: false, + useMouse: true, }); }); } diff --git a/src/tui-solid/components/layout/header.tsx b/src/tui-solid/components/layout/header.tsx index 0c7fe1a..39dc85e 100644 --- a/src/tui-solid/components/layout/header.tsx +++ b/src/tui-solid/components/layout/header.tsx @@ -139,8 +139,10 @@ export function Header(props: HeaderProps) { justifyContent="space-between" paddingLeft={1} paddingRight={1} + marginBottom={1} borderColor={theme.colors.border} border={["bottom"]} + backgroundColor={theme.colors.background} > diff --git a/src/tui-solid/components/modals/permission-modal.tsx b/src/tui-solid/components/modals/permission-modal.tsx index 31735e8..482ae40 100644 --- a/src/tui-solid/components/modals/permission-modal.tsx +++ b/src/tui-solid/components/modals/permission-modal.tsx @@ -100,12 +100,15 @@ export function PermissionModal(props: PermissionModalProps) { diff --git a/src/tui-solid/routes/session.tsx b/src/tui-solid/routes/session.tsx index 561595c..3f8c712 100644 --- a/src/tui-solid/routes/session.tsx +++ b/src/tui-solid/routes/session.tsx @@ -287,8 +287,20 @@ export function Session(props: SessionProps) { + + + + - + + + @@ -393,18 +405,6 @@ export function Session(props: SessionProps) { - - - - - - { + return text.replace(ANSI_REGEX, ""); +}; + +/** + * Check if a string contains ANSI escape codes + */ +export const hasAnsi = (text: string): boolean => { + return ANSI_REGEX.test(text); +}; + +/** + * Truncate text to a maximum number of lines + */ +export const truncateLines = ( + text: string, + maxLines: number, + showCount = true, +): { text: string; truncated: boolean; hiddenCount: number } => { + const lines = text.split("\n"); + + if (lines.length <= maxLines) { + return { text, truncated: false, hiddenCount: 0 }; + } + + const visibleLines = lines.slice(0, maxLines); + const hiddenCount = lines.length - maxLines; + + const suffix = showCount ? `\n... (${hiddenCount} more lines)` : ""; + + return { + text: visibleLines.join("\n") + suffix, + truncated: true, + hiddenCount, + }; +}; + +/** + * Format a tool name with optional truncated args for display + * Like: Bash(git add -A && git commit...) + */ +export const formatToolCall = ( + toolName: string, + args?: Record, + maxLength = 60, +): string => { + if (!args) { + return toolName; + } + + // Get the main argument (command, file_path, filePath, query, etc.) + const mainArg = + args.command ?? + args.file_path ?? + args.filePath ?? + args.query ?? + args.pattern ?? + args.url; + + if (!mainArg || typeof mainArg !== "string") { + return toolName; + } + + // Clean and truncate the argument + const cleanArg = stripAnsi(mainArg).replace(/\n/g, " ").trim(); + const truncatedArg = + cleanArg.length > maxLength + ? cleanArg.substring(0, maxLength) + "..." + : cleanArg; + + return `${toolName}(${truncatedArg})`; +}; + +/** + * Format a file operation tool name + * Like: Write(src/utils/ansi.ts), Edit(package.json), Read(1 file) + */ +export const formatFileToolCall = ( + toolName: string, + filePath?: string, + fileCount?: number, +): string => { + if (fileCount && fileCount > 1) { + return `${toolName}(${fileCount} files)`; + } + + if (filePath) { + // Just show the filename or last path segment + const filename = filePath.split("/").pop() || filePath; + return `${toolName}(${filename})`; + } + + return toolName; +};