From adfebda501c09d76166615317bb13d7385576151 Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Thu, 29 Jan 2026 04:53:39 -0500 Subject: [PATCH] Add debug log panel, centered modals, and fix multiple UX issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: - Add /logs command to toggle debug log panel (20% width on right) - Debug panel shows API calls, streaming events, tool calls, state changes - Add /help submenus with detailed command explanations - Center all modal dialogs in the terminal window Bug Fixes: - Fix streaming content not displaying (add fallback when streaming fails) - Fix permission modal shortcut key mismatch ('a' → 'l' for local scope) - Fix agent prompt accumulation when switching agents multiple times - Fix permission modal using brittle index for "No" option Improvements: - Restrict git commands (add, commit, push, etc.) unless user explicitly requests - Unify permission options across all UI components - Add Ollama model selection when switching to Ollama provider - Store base system prompt to prevent agent prompt stacking New files: - src/tui-solid/components/debug-log-panel.tsx - src/tui-solid/components/centered-modal.tsx - src/tui-solid/components/help-menu.tsx - src/tui-solid/components/help-detail.tsx - src/constants/help-content.ts --- package-lock.json | 20 +- .../chat/commands/commandsRegistry.ts | 17 +- src/commands/components/execute/index.ts | 37 +++- src/constants/chat-service.ts | 3 +- src/constants/help-content.ts | 182 ++++++++++++++++ src/constants/tui-components.ts | 3 + src/index.ts | 14 +- src/prompts/system/agent.ts | 20 +- src/providers/copilot/chat.ts | 3 + src/providers/ollama/stream.ts | 2 + src/services/chat-tui/commands.ts | 20 +- src/services/chat-tui/message-handler.ts | 104 +++++---- src/tui-solid/app.tsx | 5 +- src/tui-solid/components/centered-modal.tsx | 29 +++ src/tui-solid/components/debug-log-panel.tsx | 200 ++++++++++++++++++ src/tui-solid/components/help-detail.tsx | 115 ++++++++++ src/tui-solid/components/help-menu.tsx | 161 ++++++++++++++ src/tui-solid/components/index.ts | 2 + src/tui-solid/components/input-area.tsx | 4 +- src/tui-solid/components/learning-modal.tsx | 6 +- src/tui-solid/components/permission-modal.tsx | 86 ++++---- src/tui-solid/components/provider-select.tsx | 17 +- src/tui-solid/context/app.tsx | 17 ++ src/tui-solid/routes/home.tsx | 17 +- src/tui-solid/routes/session.tsx | 108 +++++++--- src/tui-solid/types/index.ts | 2 + src/types/tui.ts | 4 +- 27 files changed, 1031 insertions(+), 167 deletions(-) create mode 100644 src/constants/help-content.ts create mode 100644 src/tui-solid/components/centered-modal.tsx create mode 100644 src/tui-solid/components/debug-log-panel.tsx create mode 100644 src/tui-solid/components/help-detail.tsx create mode 100644 src/tui-solid/components/help-menu.tsx diff --git a/package-lock.json b/package-lock.json index 234c8e1..fb98fc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "vitest": "^4.0.17" }, "engines": { - "node": ">=18.0.0" + "bun": ">=1.0.0" } }, "node_modules/@ampproject/remapping": { @@ -2678,9 +2678,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", - "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.1.0.tgz", + "integrity": "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==", "dev": true, "license": "MIT", "dependencies": { @@ -3468,9 +3468,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.18", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", - "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -5938,9 +5938,9 @@ } }, "node_modules/inquirer": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-13.2.1.tgz", - "integrity": "sha512-kjIN+joqgbSncQJ6GfN7gV9AbDQlMA+hJ96xcwkQUwP9KN/ZIusoJ2mAfdt0LPrZJQsEyk5i/YrgJQTxSgzlPw==", + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-13.2.2.tgz", + "integrity": "sha512-+hlN8I88JE9T3zjWHGnMhryniRDbSgFNJHJTyD2iKO5YNpMRyfghQ6wVoe+gV4ygMM4r4GzlsBxNa1g/UUZixA==", "license": "MIT", "dependencies": { "@inquirer/ansi": "^2.0.3", diff --git a/src/commands/components/chat/commands/commandsRegistry.ts b/src/commands/components/chat/commands/commandsRegistry.ts index 36c9b81..286ab52 100644 --- a/src/commands/components/chat/commands/commandsRegistry.ts +++ b/src/commands/components/chat/commands/commandsRegistry.ts @@ -1,6 +1,6 @@ import { saveSession } from "@services/session"; -import { showHelp } from "@commands/components/chat/commands/show-help"; import { clearConversation } from "@commands/components/chat/history/clear-conversation"; +import { appStore } from "@tui/index"; import { showContextFiles } from "@commands/components/chat/context/show-context-files"; import { removeFile } from "@commands/components/chat/context/remove-file"; import { showContext } from "@commands/components/chat/history/show-context"; @@ -24,8 +24,8 @@ const COMMAND_REGISTRY: Map = new Map< string, CommandHandler >([ - ["help", () => showHelp()], - ["h", () => showHelp()], + ["help", () => appStore.setMode("help_menu")], + ["h", () => appStore.setMode("help_menu")], ["clear", (ctx: CommandContext) => clearConversation(ctx.state)], ["c", (ctx: CommandContext) => clearConversation(ctx.state)], ["files", (ctx: CommandContext) => showContextFiles(ctx.state.contextFiles)], @@ -106,6 +106,17 @@ const COMMAND_REGISTRY: Map = new Map< }, ], ["mcp", async (ctx: CommandContext) => handleMCP(ctx.args)], + [ + "logs", + () => { + appStore.toggleDebugLog(); + const { debugLogVisible } = appStore.getState(); + appStore.addLog({ + type: "system", + content: `Debug logs panel ${debugLogVisible ? "enabled" : "disabled"}`, + }); + }, + ], ]); export default COMMAND_REGISTRY; diff --git a/src/commands/components/execute/index.ts b/src/commands/components/execute/index.ts index 574ec03..983dfef 100644 --- a/src/commands/components/execute/index.ts +++ b/src/commands/components/execute/index.ts @@ -26,6 +26,7 @@ import { agentLoader } from "@services/agent-loader"; interface ExecuteContext { state: ChatServiceState | null; + baseSystemPrompt: string | null; } const createHandleExit = (): (() => void) => (): void => { @@ -51,16 +52,22 @@ const createHandleAgentSelect = (ctx.state as ChatServiceState & { currentAgent?: string }).currentAgent = agentId; - if (agent.prompt) { - const basePrompt = ctx.state.systemPrompt; - ctx.state.systemPrompt = `${agent.prompt}\n\n${basePrompt}`; + // Use the stored base prompt to avoid accumulation when switching agents + const basePrompt = ctx.baseSystemPrompt ?? ctx.state.systemPrompt; - if ( - ctx.state.messages.length > 0 && - ctx.state.messages[0].role === "system" - ) { - ctx.state.messages[0].content = ctx.state.systemPrompt; - } + if (agent.prompt) { + ctx.state.systemPrompt = `${agent.prompt}\n\n${basePrompt}`; + } else { + // Reset to base prompt if agent has no custom prompt + ctx.state.systemPrompt = basePrompt; + } + + // Update the system message in the conversation + if ( + ctx.state.messages.length > 0 && + ctx.state.messages[0].role === "system" + ) { + ctx.state.messages[0].content = ctx.state.systemPrompt; } }; @@ -81,6 +88,15 @@ const createHandleProviderSelect = const config = await getConfig(); config.set("provider", providerId as "copilot" | "ollama"); await config.save(); + + // Load models for the new provider and update the store + const models = await loadModels(providerId as "copilot" | "ollama"); + appStore.setAvailableModels(models); + + // If Ollama is selected and has models, open model selector + if (providerId === "ollama" && models.length > 0) { + appStore.setMode("model_select"); + } }; const createHandleCascadeToggle = @@ -131,10 +147,13 @@ const createHandleSubmit = const execute = async (options: ChatTUIOptions): Promise => { const ctx: ExecuteContext = { state: null, + baseSystemPrompt: null, }; const { state, session } = await initializeChatService(options); ctx.state = state; + // Store the original system prompt before any agent modifications + ctx.baseSystemPrompt = state.systemPrompt; if (options.printMode && options.initialPrompt) { await executePrintMode(state, options.initialPrompt); diff --git a/src/constants/chat-service.ts b/src/constants/chat-service.ts index 2ef3047..e65a720 100644 --- a/src/constants/chat-service.ts +++ b/src/constants/chat-service.ts @@ -103,4 +103,5 @@ export type CommandName = | "p" | "status" | "remember" - | "learnings"; + | "learnings" + | "logs"; diff --git a/src/constants/help-content.ts b/src/constants/help-content.ts new file mode 100644 index 0000000..3154893 --- /dev/null +++ b/src/constants/help-content.ts @@ -0,0 +1,182 @@ +/** + * Help Content Constants + * + * Detailed help information for commands and features + */ + +export interface HelpTopic { + id: string; + name: string; + shortDescription: string; + fullDescription: string; + usage?: string; + examples?: string[]; + shortcuts?: string[]; + category: HelpCategory; +} + +export type HelpCategory = "commands" | "files" | "shortcuts"; + +export const HELP_CATEGORIES: Array<{ + id: HelpCategory; + name: string; + description: string; +}> = [ + { + id: "commands", + name: "Commands", + description: "Slash commands for controlling the assistant", + }, + { + id: "files", + name: "File References", + description: "How to reference and work with files", + }, + { + id: "shortcuts", + name: "Shortcuts", + description: "Keyboard shortcuts", + }, +]; + +export const HELP_TOPICS: HelpTopic[] = [ + // Commands + { + id: "help", + name: "/help", + shortDescription: "Show this help menu", + fullDescription: + "Opens the help menu where you can browse commands and features.", + usage: "/help", + category: "commands", + }, + { + id: "clear", + name: "/clear", + shortDescription: "Clear conversation", + fullDescription: "Clears the conversation history from the screen.", + usage: "/clear", + category: "commands", + }, + { + id: "save", + name: "/save", + shortDescription: "Save session", + fullDescription: "Saves the current conversation session.", + usage: "/save", + category: "commands", + }, + { + id: "model", + name: "/model", + shortDescription: "Select AI model", + fullDescription: "Opens menu to select which AI model to use.", + usage: "/model", + category: "commands", + }, + { + id: "provider", + name: "/provider", + shortDescription: "Switch provider", + fullDescription: "Switch between LLM providers (Copilot, Ollama).", + usage: "/provider", + category: "commands", + }, + { + id: "mode", + name: "/mode", + shortDescription: "Switch mode", + fullDescription: + "Switch between Agent (full access), Ask (read-only), and Code Review modes.", + usage: "/mode", + shortcuts: ["Ctrl+Tab"], + category: "commands", + }, + { + id: "theme", + name: "/theme", + shortDescription: "Change theme", + fullDescription: "Opens menu to select a color theme.", + usage: "/theme", + category: "commands", + }, + { + id: "exit", + name: "/exit", + shortDescription: "Exit application", + fullDescription: "Exits CodeTyper. You can also use Ctrl+C twice.", + usage: "/exit", + shortcuts: ["Ctrl+C twice"], + category: "commands", + }, + { + id: "logs", + name: "/logs", + shortDescription: "Toggle debug logs", + fullDescription: + "Toggles the debug log panel on the right side of the screen. Shows API calls, streaming events, tool calls, and internal state changes for debugging.", + usage: "/logs", + category: "commands", + }, + + // Files + { + id: "file-ref", + name: "@file", + shortDescription: "Reference a file", + fullDescription: + "Type @ to open file picker and include file content in context.", + usage: "@filename", + examples: ["@src/index.ts", "@package.json"], + category: "files", + }, + { + id: "file-glob", + name: "@pattern", + shortDescription: "Reference multiple files", + fullDescription: "Use glob patterns to reference multiple files.", + usage: "@pattern", + examples: ["@src/**/*.ts", "@*.json"], + category: "files", + }, + + // Shortcuts + { + id: "shortcut-slash", + name: "/", + shortDescription: "Open command menu", + fullDescription: "Press / when input is empty to open command menu.", + shortcuts: ["/"], + category: "shortcuts", + }, + { + id: "shortcut-at", + name: "@", + shortDescription: "Open file picker", + fullDescription: "Press @ to open the file picker.", + shortcuts: ["@"], + category: "shortcuts", + }, + { + id: "shortcut-ctrlc", + name: "Ctrl+C", + shortDescription: "Cancel/Exit", + fullDescription: "Press once to cancel, twice to exit.", + shortcuts: ["Ctrl+C"], + category: "shortcuts", + }, + { + id: "shortcut-ctrltab", + name: "Ctrl+Tab", + shortDescription: "Cycle modes", + fullDescription: "Cycle through interaction modes.", + shortcuts: ["Ctrl+Tab"], + category: "shortcuts", + }, +]; + +export const getTopicsByCategory = (category: HelpCategory): HelpTopic[] => + HELP_TOPICS.filter((topic) => topic.category === category); + +export const getTopicById = (id: string): HelpTopic | undefined => + HELP_TOPICS.find((topic) => topic.id === id); diff --git a/src/constants/tui-components.ts b/src/constants/tui-components.ts index 22ba098..836a599 100644 --- a/src/constants/tui-components.ts +++ b/src/constants/tui-components.ts @@ -47,6 +47,8 @@ export const MODE_DISPLAY_CONFIG: Record = { mode_select: { text: "Select Mode", color: "magenta" }, provider_select: { text: "Select Provider", color: "magenta" }, learning_prompt: { text: "Save Learning?", color: "cyan" }, + help_menu: { text: "Help", color: "cyan" }, + help_detail: { text: "Help Detail", color: "cyan" }, } as const; export const DEFAULT_MODE_DISPLAY: ModeDisplayConfig = { @@ -199,6 +201,7 @@ export const SLASH_COMMANDS: SlashCommand[] = [ }, { name: "theme", description: "Change color theme", category: "settings" }, { name: "mcp", description: "Manage MCP servers", category: "settings" }, + { name: "logs", description: "Toggle debug log panel", category: "settings" }, // Account commands { diff --git a/src/index.ts b/src/index.ts index a947b57..1cb337d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,9 @@ #!/usr/bin/env node -import { fileURLToPath } from "url"; -import { dirname, join } from "path"; import { Command } from "commander"; import { handleCommand } from "@commands/handlers"; -import { readFile } from "fs/promises"; import { execute } from "@commands/chat-tui"; +import versionData from "@/version.json"; import { initializeProviders, loginProvider, @@ -37,14 +35,8 @@ import { createPlan, displayPlan, approvePlan } from "@services/planner"; import { ensureXdgDirectories } from "@utils/ensure-directories"; import chalk from "chalk"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Read version from package.json -const packageJson = JSON.parse( - await readFile(join(__dirname, "../package.json"), "utf-8"), -); -const { version } = packageJson; +// Read version from version.json +const { version } = versionData; // Ensure XDG directories exist await ensureXdgDirectories(); diff --git a/src/prompts/system/agent.ts b/src/prompts/system/agent.ts index 9dc561e..869ed5c 100644 --- a/src/prompts/system/agent.ts +++ b/src/prompts/system/agent.ts @@ -150,7 +150,25 @@ assistant: Errors are handled in src/services/error-handler.ts:42. # Git Operations -Only commit when requested. When creating commits: +CRITICAL: Git commands that modify the repository are FORBIDDEN unless the user EXPLICITLY requests them. + +## Forbidden by Default (require explicit user request): +- \`git add\` - NEVER run git add (including \`git add .\` or \`git add -A\`) +- \`git commit\` - NEVER run git commit +- \`git push\` - NEVER run git push +- \`git merge\` - NEVER run git merge +- \`git rebase\` - NEVER run git rebase +- \`git reset\` - NEVER run git reset +- \`git checkout -- .\` or \`git restore\` - NEVER discard changes + +## Allowed without asking: +- \`git status\` - checking current state +- \`git diff\` - viewing changes +- \`git log\` - viewing history +- \`git branch\` - listing branches +- \`git show\` - viewing commits + +## When user requests a commit: - NEVER use destructive commands (push --force, reset --hard) unless explicitly asked - NEVER skip hooks unless explicitly asked - Use clear, concise commit messages focusing on "why" not "what" diff --git a/src/providers/copilot/chat.ts b/src/providers/copilot/chat.ts index b4edb92..7407325 100644 --- a/src/providers/copilot/chat.ts +++ b/src/providers/copilot/chat.ts @@ -23,6 +23,7 @@ import type { ChatCompletionResponse, StreamChunk, } from "@/types/providers"; +import { addDebugLog } from "@tui-solid/components/debug-log-panel"; interface FormattedMessage { role: string; @@ -267,6 +268,7 @@ export const chatStream = async ( options: ChatCompletionOptions | undefined, onChunk: (chunk: StreamChunk) => void, ): Promise => { + addDebugLog("api", `Copilot stream request: ${messages.length} messages`); const token = await refreshToken(); const endpoint = getEndpoint(token); const originalModel = @@ -274,6 +276,7 @@ export const chatStream = async ( ? options.model : getDefaultModel(); const body = buildRequestBody(messages, options, true); + addDebugLog("api", `Copilot model: ${body.model}`); let lastError: unknown; let switchedToUnlimited = false; diff --git a/src/providers/ollama/stream.ts b/src/providers/ollama/stream.ts index 23be2cf..bf2490d 100644 --- a/src/providers/ollama/stream.ts +++ b/src/providers/ollama/stream.ts @@ -13,6 +13,7 @@ import type { StreamChunk, } from "@/types/providers"; import type { OllamaChatResponse } from "@/types/ollama"; +import { addDebugLog } from "@tui-solid/components/debug-log-panel"; const parseStreamLine = ( line: string, @@ -67,6 +68,7 @@ export const ollamaChatStream = async ( ): Promise => { const baseUrl = getOllamaBaseUrl(); const body = buildChatRequest(messages, options, true); + addDebugLog("api", `Ollama stream request: ${messages.length} msgs, model=${body.model}`); const stream = got.stream.post(`${baseUrl}${OLLAMA_ENDPOINTS.CHAT}`, { json: body, diff --git a/src/services/chat-tui/commands.ts b/src/services/chat-tui/commands.ts index 54a532d..c35b858 100644 --- a/src/services/chat-tui/commands.ts +++ b/src/services/chat-tui/commands.ts @@ -4,11 +4,7 @@ import { saveSession as saveSessionSession } from "@services/session"; import { appStore } from "@tui/index"; -import { - CHAT_MESSAGES, - HELP_TEXT, - type CommandName, -} from "@constants/chat-service"; +import { CHAT_MESSAGES, type CommandName } from "@constants/chat-service"; import { handleLogin, handleLogout, showWhoami } from "@services/chat-tui/auth"; import { handleRememberCommand, @@ -31,8 +27,8 @@ type CommandHandler = ( callbacks: ChatServiceCallbacks, ) => Promise | void; -const showHelp: CommandHandler = (_, callbacks) => { - callbacks.onLog("system", HELP_TEXT); +const showHelp: CommandHandler = () => { + appStore.setMode("help_menu"); }; const clearConversation: CommandHandler = (state, callbacks) => { @@ -112,6 +108,15 @@ const selectMode: CommandHandler = () => { appStore.setMode("mode_select"); }; +const toggleDebugLogs: CommandHandler = (_, callbacks) => { + appStore.toggleDebugLog(); + const { debugLogVisible } = appStore.getState(); + callbacks.onLog( + "system", + `Debug logs panel ${debugLogVisible ? "enabled" : "disabled"}`, + ); +}; + const COMMAND_HANDLERS: Record = { help: showHelp, h: showHelp, @@ -137,6 +142,7 @@ const COMMAND_HANDLERS: Record = { status: showStatus, remember: handleRememberCommand, learnings: (_, callbacks) => handleLearningsCommand(callbacks), + logs: toggleDebugLogs, }; export const executeCommand = async ( diff --git a/src/services/chat-tui/message-handler.ts b/src/services/chat-tui/message-handler.ts index 018a26b..85639cf 100644 --- a/src/services/chat-tui/message-handler.ts +++ b/src/services/chat-tui/message-handler.ts @@ -54,6 +54,7 @@ import type { ChatServiceCallbacks, ToolCallInfo, } from "@/types/chat-service"; +import { addDebugLog } from "@tui-solid/components/debug-log-panel"; // Track last response for feedback learning let lastResponseContext: { @@ -116,47 +117,58 @@ const createToolResultHandler = /** * Create streaming callbacks for TUI integration */ -const createStreamCallbacks = (): StreamCallbacks => ({ - onContentChunk: (content: string) => { - appStore.appendStreamContent(content); - }, +const createStreamCallbacks = (): StreamCallbacks => { + let chunkCount = 0; - onToolCallStart: (toolCall) => { - appStore.setCurrentToolCall({ - id: toolCall.id, - name: toolCall.name, - description: `Calling ${toolCall.name}...`, - status: "pending", - }); - }, + return { + onContentChunk: (content: string) => { + chunkCount++; + addDebugLog("stream", `Chunk #${chunkCount}: "${content.substring(0, 30)}${content.length > 30 ? "..." : ""}"`); + appStore.appendStreamContent(content); + }, - onToolCallComplete: (toolCall) => { - appStore.updateToolCall({ - id: toolCall.id, - name: toolCall.name, - status: "running", - }); - }, + onToolCallStart: (toolCall) => { + addDebugLog("tool", `Tool start: ${toolCall.name} (${toolCall.id})`); + appStore.setCurrentToolCall({ + id: toolCall.id, + name: toolCall.name, + description: `Calling ${toolCall.name}...`, + status: "pending", + }); + }, - onModelSwitch: (info) => { - appStore.addLog({ - type: "system", - content: `Model switched: ${info.from} → ${info.to} (${info.reason})`, - }); - }, + onToolCallComplete: (toolCall) => { + addDebugLog("tool", `Tool complete: ${toolCall.name}`); + appStore.updateToolCall({ + id: toolCall.id, + name: toolCall.name, + status: "running", + }); + }, - onComplete: () => { - appStore.completeStreaming(); - }, + onModelSwitch: (info) => { + addDebugLog("api", `Model switch: ${info.from} → ${info.to}`); + appStore.addLog({ + type: "system", + content: `Model switched: ${info.from} → ${info.to} (${info.reason})`, + }); + }, - onError: (error: string) => { - appStore.cancelStreaming(); - appStore.addLog({ - type: "error", - content: error, - }); - }, -}); + onComplete: () => { + addDebugLog("stream", `Stream complete (${chunkCount} chunks)`); + appStore.completeStreaming(); + }, + + onError: (error: string) => { + addDebugLog("error", `Stream error: ${error}`); + appStore.cancelStreaming(); + appStore.addLog({ + type: "error", + content: error, + }); + }, + }; +}; /** * Run audit with Copilot on Ollama's response @@ -386,9 +398,12 @@ export const handleMessage = async ( } // Start streaming UI + addDebugLog("state", `Starting request: provider=${effectiveProvider}, model=${state.model}`); + addDebugLog("state", `Mode: ${appStore.getState().interactionMode}, Cascade: ${cascadeEnabled}`); appStore.setMode("thinking"); appStore.startThinking(); appStore.startStreaming(); + addDebugLog("state", "Streaming started"); const streamCallbacks = createStreamCallbacks(); const agent = createStreamingAgent( @@ -412,12 +427,15 @@ export const handleMessage = async ( ); try { + addDebugLog("api", `Agent.run() started with ${state.messages.length} messages`); const result = await agent.run(state.messages); + addDebugLog("api", `Agent.run() completed: success=${result.success}, iterations=${result.iterations}`); // Stop thinking timer appStore.stopThinking(); if (result.finalResponse) { + addDebugLog("info", `Final response length: ${result.finalResponse.length} chars`); let finalResponse = result.finalResponse; // Run audit if cascade mode with Ollama @@ -450,8 +468,18 @@ export const handleMessage = async ( role: "assistant", content: finalResponse, }); - // Note: Don't call callbacks.onLog here - streaming already added the log entry - // via appendStreamContent/completeStreaming + + // Check if streaming content was received - if not, add the response as a log + // This handles cases where streaming didn't work or content was all in final response + const streamingState = appStore.getState().streamingLog; + if (!streamingState.content && finalResponse) { + // Streaming didn't receive content, manually add the response + appStore.cancelStreaming(); // Remove empty streaming log + appStore.addLog({ + type: "assistant", + content: finalResponse, + }); + } addMessage("user", message); addMessage("assistant", finalResponse); diff --git a/src/tui-solid/app.tsx b/src/tui-solid/app.tsx index eeea8fd..be216a9 100644 --- a/src/tui-solid/app.tsx +++ b/src/tui-solid/app.tsx @@ -256,7 +256,8 @@ function AppContent(props: AppProps) { allowed: boolean, scope?: PermissionScope, ): void => { - app.setMode("idle"); + // Don't set mode here - the resolve callback in permissions.ts + // handles the mode transition to "tool_execution" props.onPermissionResponse(allowed, scope); }; @@ -265,7 +266,7 @@ function AppContent(props: AppProps) { scope?: LearningScope, editedContent?: string, ): void => { - app.setMode("idle"); + // Don't set mode here - the resolve callback handles the mode transition props.onLearningResponse(save, scope, editedContent); }; diff --git a/src/tui-solid/components/centered-modal.tsx b/src/tui-solid/components/centered-modal.tsx new file mode 100644 index 0000000..742f7e7 --- /dev/null +++ b/src/tui-solid/components/centered-modal.tsx @@ -0,0 +1,29 @@ +import { JSXElement } from "solid-js"; +import { useTheme } from "@tui-solid/context/theme"; + +interface CenteredModalProps { + children: JSXElement; +} + +/** + * A container that centers its children in the terminal window. + * Uses absolute positioning with flexbox centering. + */ +export function CenteredModal(props: CenteredModalProps) { + const theme = useTheme(); + + return ( + + {props.children} + + ); +} diff --git a/src/tui-solid/components/debug-log-panel.tsx b/src/tui-solid/components/debug-log-panel.tsx new file mode 100644 index 0000000..9a5a4ca --- /dev/null +++ b/src/tui-solid/components/debug-log-panel.tsx @@ -0,0 +1,200 @@ +import { createMemo, For, createSignal, onMount, onCleanup } from "solid-js"; +import { useKeyboard } from "@opentui/solid"; +import { TextAttributes } from "@opentui/core"; +import type { ScrollBoxRenderable } from "@opentui/core"; +import { useTheme } from "@tui-solid/context/theme"; +import { useAppStore } from "@tui-solid/context/app"; + +const SCROLL_LINES = 2; + +interface DebugEntry { + id: string; + timestamp: number; + type: "api" | "stream" | "tool" | "state" | "error" | "info"; + message: string; +} + +// Global debug log store +let debugEntries: DebugEntry[] = []; +let debugIdCounter = 0; +let listeners: Array<() => void> = []; + +const notifyListeners = (): void => { + for (const listener of listeners) { + listener(); + } +}; + +export const addDebugLog = ( + type: DebugEntry["type"], + message: string, +): void => { + const entry: DebugEntry = { + id: `debug-${++debugIdCounter}`, + timestamp: Date.now(), + type, + message, + }; + debugEntries.push(entry); + // Keep only last 500 entries + if (debugEntries.length > 500) { + debugEntries = debugEntries.slice(-500); + } + notifyListeners(); +}; + +export const clearDebugLogs = (): void => { + debugEntries = []; + debugIdCounter = 0; + notifyListeners(); +}; + +export function DebugLogPanel() { + const theme = useTheme(); + const app = useAppStore(); + let scrollboxRef: ScrollBoxRenderable | undefined; + const [entries, setEntries] = createSignal([...debugEntries]); + const [stickyEnabled, setStickyEnabled] = createSignal(true); + + const isActive = () => app.debugLogVisible(); + + onMount(() => { + const updateEntries = (): void => { + setEntries([...debugEntries]); + if (stickyEnabled() && scrollboxRef) { + scrollboxRef.scrollTo(Infinity); + } + }; + listeners.push(updateEntries); + + onCleanup(() => { + listeners = listeners.filter((l) => l !== updateEntries); + }); + }); + + const getTypeColor = (type: DebugEntry["type"]): string => { + const colorMap: Record = { + api: theme.colors.info, + stream: theme.colors.success, + tool: theme.colors.warning, + state: theme.colors.accent, + error: theme.colors.error, + info: theme.colors.textDim, + }; + return colorMap[type]; + }; + + const getTypeLabel = (type: DebugEntry["type"]): string => { + const labelMap: Record = { + api: "API", + stream: "STR", + tool: "TUL", + state: "STA", + error: "ERR", + info: "INF", + }; + return labelMap[type]; + }; + + const formatTime = (timestamp: number): string => { + const date = new Date(timestamp); + return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`; + }; + + const scrollUp = (): void => { + if (!scrollboxRef) return; + setStickyEnabled(false); + scrollboxRef.scrollBy(-SCROLL_LINES); + }; + + const scrollDown = (): void => { + if (!scrollboxRef) return; + scrollboxRef.scrollBy(SCROLL_LINES); + + const isAtBottom = + scrollboxRef.scrollTop >= + scrollboxRef.content.height - scrollboxRef.viewport.height - 1; + if (isAtBottom) { + setStickyEnabled(true); + } + }; + + useKeyboard((evt) => { + if (!isActive()) return; + + if (evt.shift && evt.name === "pageup") { + scrollUp(); + evt.preventDefault(); + evt.stopPropagation(); + return; + } + + if (evt.shift && evt.name === "pagedown") { + scrollDown(); + evt.preventDefault(); + evt.stopPropagation(); + } + }); + + const truncateMessage = (msg: string, maxLen: number): string => { + if (msg.length <= maxLen) return msg; + return msg.substring(0, maxLen - 3) + "..."; + }; + + return ( + + + + Debug Logs + + ({entries().length}) + + + + + + {(entry) => ( + + + {formatTime(entry.timestamp)}{" "} + + + [{getTypeLabel(entry.type)}]{" "} + + + {truncateMessage(entry.message, 50)} + + + )} + + + + + + Shift+PgUp/PgDn scroll + + + ); +} diff --git a/src/tui-solid/components/help-detail.tsx b/src/tui-solid/components/help-detail.tsx new file mode 100644 index 0000000..17f17cd --- /dev/null +++ b/src/tui-solid/components/help-detail.tsx @@ -0,0 +1,115 @@ +import { Show, For } from "solid-js"; +import { useKeyboard } from "@opentui/solid"; +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "@tui-solid/context/theme"; +import { getTopicById } from "@/constants/help-content"; + +interface HelpDetailProps { + topicId: string; + onBack: () => void; + onClose: () => void; + isActive?: boolean; +} + +export function HelpDetail(props: HelpDetailProps) { + const theme = useTheme(); + const isActive = () => props.isActive ?? true; + + const topic = () => getTopicById(props.topicId); + + useKeyboard((evt) => { + if (!isActive()) return; + + if (evt.name === "escape" || evt.name === "backspace" || evt.name === "q") { + props.onBack(); + evt.preventDefault(); + evt.stopPropagation(); + return; + } + + if (evt.name === "return") { + props.onClose(); + evt.preventDefault(); + evt.stopPropagation(); + } + }); + + const currentTopic = topic(); + + if (!currentTopic) { + return ( + + Topic not found + Press Esc to go back + + ); + } + + return ( + + + {currentTopic.name} + + + + + {currentTopic.fullDescription} + + + + + Usage + + {currentTopic.usage} + + + 0}> + + + Examples + + + {(example) => ( + • {example} + )} + + + + 0}> + + + Shortcuts + + + {(shortcut) => ( + {shortcut} + )} + + + + + + + Esc/Backspace back | Enter close + + + ); +} diff --git a/src/tui-solid/components/help-menu.tsx b/src/tui-solid/components/help-menu.tsx new file mode 100644 index 0000000..2604632 --- /dev/null +++ b/src/tui-solid/components/help-menu.tsx @@ -0,0 +1,161 @@ +import { createSignal, createMemo, For } from "solid-js"; +import { useKeyboard } from "@opentui/solid"; +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "@tui-solid/context/theme"; +import { + HELP_CATEGORIES, + getTopicsByCategory, + type HelpCategory, +} from "@/constants/help-content"; + +interface HelpMenuProps { + onSelectTopic: (topicId: string) => void; + onClose: () => void; + isActive?: boolean; +} + +interface TopicItem { + id: string; + name: string; + description: string; +} + +interface CategoryGroup { + category: HelpCategory; + categoryName: string; + topics: TopicItem[]; +} + +export function HelpMenu(props: HelpMenuProps) { + const theme = useTheme(); + const isActive = () => props.isActive ?? true; + + const [selectedIndex, setSelectedIndex] = createSignal(0); + + const groupedTopics = createMemo((): CategoryGroup[] => { + return HELP_CATEGORIES.map((cat) => ({ + category: cat.id, + categoryName: cat.name, + topics: getTopicsByCategory(cat.id).map((t) => ({ + id: t.id, + name: t.name, + description: t.shortDescription, + })), + })).filter((g) => g.topics.length > 0); + }); + + const allTopics = createMemo((): TopicItem[] => { + return groupedTopics().flatMap((g) => g.topics); + }); + + const selectedTopic = createMemo(() => { + const topics = allTopics(); + const idx = Math.min(selectedIndex(), topics.length - 1); + return topics[idx]; + }); + + useKeyboard((evt) => { + if (!isActive()) return; + + if (evt.name === "escape") { + props.onClose(); + evt.preventDefault(); + evt.stopPropagation(); + return; + } + + if (evt.name === "return") { + const topic = selectedTopic(); + if (topic) { + props.onSelectTopic(topic.id); + } + evt.preventDefault(); + evt.stopPropagation(); + return; + } + + if (evt.name === "up") { + const total = allTopics().length; + setSelectedIndex((prev) => (prev > 0 ? prev - 1 : total - 1)); + evt.preventDefault(); + evt.stopPropagation(); + return; + } + + if (evt.name === "down") { + const total = allTopics().length; + setSelectedIndex((prev) => (prev < total - 1 ? prev + 1 : 0)); + evt.preventDefault(); + evt.stopPropagation(); + } + }); + + const isTopicSelected = (topicId: string): boolean => { + return selectedTopic()?.id === topicId; + }; + + return ( + + + + Help - Select a topic + + + + + {(group) => ( + + + + {group.categoryName} + + + + {(topic) => { + const selected = () => isTopicSelected(topic.id); + return ( + + + {selected() ? "> " : " "} + + + {topic.name.padEnd(14).substring(0, 14)} + + + {" "} + {topic.description.substring(0, 40)} + + + ); + }} + + + )} + + + + + ↑↓ navigate | Enter details | Esc close + + + + ); +} diff --git a/src/tui-solid/components/index.ts b/src/tui-solid/components/index.ts index f25baa7..6188478 100644 --- a/src/tui-solid/components/index.ts +++ b/src/tui-solid/components/index.ts @@ -20,6 +20,8 @@ export { SelectMenu } from "./select-menu"; export type { SelectOption } from "./select-menu"; export { PermissionModal } from "./permission-modal"; export { LearningModal } from "./learning-modal"; +export { HelpMenu } from "./help-menu"; +export { HelpDetail } from "./help-detail"; export { TodoPanel } from "./todo-panel"; export type { TodoItem, Plan } from "./todo-panel"; export { DiffView, parseDiffOutput, isDiffContent } from "./diff-view"; diff --git a/src/tui-solid/components/input-area.tsx b/src/tui-solid/components/input-area.tsx index 70848bd..eb84e57 100644 --- a/src/tui-solid/components/input-area.tsx +++ b/src/tui-solid/components/input-area.tsx @@ -92,7 +92,9 @@ export function InputArea(props: InputAreaProps) { mode === "mcp_add" || mode === "file_picker" || mode === "permission_prompt" || - mode === "learning_prompt" + mode === "learning_prompt" || + mode === "help_menu" || + mode === "help_detail" ); }); const placeholder = () => diff --git a/src/tui-solid/components/learning-modal.tsx b/src/tui-solid/components/learning-modal.tsx index b73c92f..68cdf6c 100644 --- a/src/tui-solid/components/learning-modal.tsx +++ b/src/tui-solid/components/learning-modal.tsx @@ -46,6 +46,9 @@ export function LearningModal(props: LearningModalProps) { useKeyboard((evt) => { if (!isActive()) return; + // Stop propagation for all events when modal is active + evt.stopPropagation(); + if (isEditing()) { if (evt.name === "escape") { setIsEditing(false); @@ -95,7 +98,8 @@ export function LearningModal(props: LearningModalProps) { return; } - if (evt.name.length === 1) { + // Only handle known shortcut keys to avoid accidental triggers + if (evt.name.length === 1 && !evt.ctrl && !evt.meta) { const charLower = evt.name.toLowerCase(); const optionIndex = SCOPE_OPTIONS.findIndex((o) => o.key === charLower); if (optionIndex !== -1) { diff --git a/src/tui-solid/components/permission-modal.tsx b/src/tui-solid/components/permission-modal.tsx index 936693b..31735e8 100644 --- a/src/tui-solid/components/permission-modal.tsx +++ b/src/tui-solid/components/permission-modal.tsx @@ -10,15 +10,19 @@ interface PermissionModalProps { isActive?: boolean; } -const SCOPE_OPTIONS: Array<{ +interface PermissionOption { key: string; label: string; - scope: PermissionScope; -}> = [ - { key: "y", label: "Yes, this once", scope: "once" }, - { key: "s", label: "Yes, for this session", scope: "session" }, - { key: "a", label: "Always allow for this project", scope: "local" }, - { key: "g", label: "Always allow globally", scope: "global" }, + scope: PermissionScope | "deny"; + allowed: boolean; +} + +const PERMISSION_OPTIONS: PermissionOption[] = [ + { key: "y", label: "Yes, this once", scope: "once", allowed: true }, + { key: "s", label: "Yes, for this session", scope: "session", allowed: true }, + { key: "l", label: "Yes, for this project", scope: "local", allowed: true }, + { key: "g", label: "Yes, globally", scope: "global", allowed: true }, + { key: "n", label: "No, deny this request", scope: "deny", allowed: false }, ]; export function PermissionModal(props: PermissionModalProps) { @@ -38,41 +42,55 @@ export function PermissionModal(props: PermissionModalProps) { useKeyboard((evt) => { if (!isActive()) return; + // Stop propagation for all events when modal is active + evt.stopPropagation(); + if (evt.name === "up") { - setSelectedIndex((prev) => (prev > 0 ? prev - 1 : SCOPE_OPTIONS.length)); + setSelectedIndex((prev) => + prev > 0 ? prev - 1 : PERMISSION_OPTIONS.length - 1, + ); evt.preventDefault(); return; } if (evt.name === "down") { - setSelectedIndex((prev) => (prev < SCOPE_OPTIONS.length ? prev + 1 : 0)); + setSelectedIndex((prev) => + prev < PERMISSION_OPTIONS.length - 1 ? prev + 1 : 0, + ); evt.preventDefault(); return; } if (evt.name === "return") { - if (selectedIndex() === SCOPE_OPTIONS.length) { - handleResponse(false); + const option = PERMISSION_OPTIONS[selectedIndex()]; + if (option.allowed && option.scope !== "deny") { + handleResponse(true, option.scope as PermissionScope); } else { - const option = SCOPE_OPTIONS[selectedIndex()]; - handleResponse(true, option.scope); + handleResponse(false); } evt.preventDefault(); return; } - if (evt.name === "escape" || evt.name === "n") { + if (evt.name === "escape") { handleResponse(false); evt.preventDefault(); return; } - if (evt.name.length === 1) { + // Handle shortcut keys + if (evt.name.length === 1 && !evt.ctrl && !evt.meta) { const charLower = evt.name.toLowerCase(); - const optionIndex = SCOPE_OPTIONS.findIndex((o) => o.key === charLower); + const optionIndex = PERMISSION_OPTIONS.findIndex( + (o) => o.key === charLower, + ); if (optionIndex !== -1) { - const option = SCOPE_OPTIONS[optionIndex]; - handleResponse(true, option.scope); + const option = PERMISSION_OPTIONS[optionIndex]; + if (option.allowed && option.scope !== "deny") { + handleResponse(true, option.scope as PermissionScope); + } else { + handleResponse(false); + } evt.preventDefault(); } } @@ -114,9 +132,11 @@ export function PermissionModal(props: PermissionModalProps) { - + {(option, index) => { const isSelected = () => index() === selectedIndex(); + const keyColor = () => + option.allowed ? theme.colors.success : theme.colors.error; return ( {isSelected() ? "> " : " "} - [{option.key}] + [{option.key}] {option.label} @@ -135,32 +155,6 @@ export function PermissionModal(props: PermissionModalProps) { ); }} - - - {selectedIndex() === SCOPE_OPTIONS.length ? "> " : " "} - - [n] - - No, deny this request - - diff --git a/src/tui-solid/components/provider-select.tsx b/src/tui-solid/components/provider-select.tsx index 19620ae..9d424b2 100644 --- a/src/tui-solid/components/provider-select.tsx +++ b/src/tui-solid/components/provider-select.tsx @@ -14,7 +14,7 @@ interface ProviderOption { } interface ProviderSelectProps { - onSelect: (providerId: string) => void; + onSelect: (providerId: string) => Promise | void; onClose: () => void; onToggleCascade?: () => void; isActive?: boolean; @@ -97,8 +97,19 @@ export function ProviderSelect(props: ProviderSelectProps) { if (evt.name === "return") { const selected = providers()[selectedIndex()]; if (selected && selected.status.available) { - props.onSelect(selected.id); - props.onClose(); + // For Ollama, let the handler manage the mode transition to model_select + // For other providers, close after selection + const result = props.onSelect(selected.id); + if (result instanceof Promise) { + result.then(() => { + // Only close if not ollama (ollama opens model_select) + if (selected.id !== "ollama") { + props.onClose(); + } + }); + } else if (selected.id !== "ollama") { + props.onClose(); + } } evt.preventDefault(); return; diff --git a/src/tui-solid/context/app.tsx b/src/tui-solid/context/app.tsx index d1c505b..2505d48 100644 --- a/src/tui-solid/context/app.tsx +++ b/src/tui-solid/context/app.tsx @@ -37,6 +37,7 @@ interface AppStore { availableModels: ProviderModel[]; sessionStats: SessionStats; todosVisible: boolean; + debugLogVisible: boolean; interruptPending: boolean; exitPending: boolean; isCompacting: boolean; @@ -70,6 +71,7 @@ interface AppContextValue { availableModels: Accessor; sessionStats: Accessor; todosVisible: Accessor; + debugLogVisible: Accessor; interruptPending: Accessor; exitPending: Accessor; isCompacting: Accessor; @@ -136,6 +138,7 @@ interface AppContextValue { // UI state actions toggleTodos: () => void; + toggleDebugLog: () => void; setInterruptPending: (pending: boolean) => void; setExitPending: (pending: boolean) => void; setIsCompacting: (compacting: boolean) => void; @@ -212,6 +215,7 @@ export const { provider: AppStoreProvider, use: useAppStore } = availableModels: [], sessionStats: createInitialSessionStats(), todosVisible: true, + debugLogVisible: false, interruptPending: false, exitPending: false, isCompacting: false, @@ -254,6 +258,7 @@ export const { provider: AppStoreProvider, use: useAppStore } = const availableModels = (): ProviderModel[] => store.availableModels; const sessionStats = (): SessionStats => store.sessionStats; const todosVisible = (): boolean => store.todosVisible; + const debugLogVisible = (): boolean => store.debugLogVisible; const interruptPending = (): boolean => store.interruptPending; const exitPending = (): boolean => store.exitPending; const isCompacting = (): boolean => store.isCompacting; @@ -495,6 +500,10 @@ export const { provider: AppStoreProvider, use: useAppStore } = setStore("todosVisible", !store.todosVisible); }; + const toggleDebugLog = (): void => { + setStore("debugLogVisible", !store.debugLogVisible); + }; + const setInterruptPending = (pending: boolean): void => { setStore("interruptPending", pending); }; @@ -678,6 +687,7 @@ export const { provider: AppStoreProvider, use: useAppStore } = availableModels, sessionStats, todosVisible, + debugLogVisible, interruptPending, exitPending, isCompacting, @@ -746,6 +756,7 @@ export const { provider: AppStoreProvider, use: useAppStore } = // UI state actions toggleTodos, + toggleDebugLog, setInterruptPending, setExitPending, setIsCompacting, @@ -799,6 +810,7 @@ export const appStore = { sessionStats: storeRef.sessionStats(), cascadeEnabled: storeRef.cascadeEnabled(), todosVisible: storeRef.todosVisible(), + debugLogVisible: storeRef.debugLogVisible(), interruptPending: storeRef.interruptPending(), exitPending: storeRef.exitPending(), isCompacting: storeRef.isCompacting(), @@ -917,6 +929,11 @@ export const appStore = { storeRef.toggleTodos(); }, + toggleDebugLog: (): void => { + if (!storeRef) throw new Error("AppStore not initialized"); + storeRef.toggleDebugLog(); + }, + setInterruptPending: (pending: boolean): void => { if (!storeRef) throw new Error("AppStore not initialized"); storeRef.setInterruptPending(pending); diff --git a/src/tui-solid/routes/home.tsx b/src/tui-solid/routes/home.tsx index 13b06c0..b07764c 100644 --- a/src/tui-solid/routes/home.tsx +++ b/src/tui-solid/routes/home.tsx @@ -7,6 +7,7 @@ import { CommandMenu } from "@tui-solid/components/command-menu"; import { ModelSelect } from "@tui-solid/components/model-select"; import { ThemeSelect } from "@tui-solid/components/theme-select"; import { FilePicker } from "@tui-solid/components/file-picker"; +import { CenteredModal } from "@tui-solid/components/centered-modal"; import { HOME_VARS } from "@constants/home"; interface HomeProps { @@ -67,7 +68,7 @@ export function Home(props: HomeProps) { - + { const lowerCommand = command.toLowerCase(); @@ -95,38 +96,38 @@ export function Home(props: HomeProps) { onCancel={() => app.closeCommandMenu()} isActive={app.mode() === "command_menu"} /> - + - + props.onModelSelect?.(model)} onClose={handleModelClose} isActive={app.mode() === "model_select"} /> - + - + props.onThemeSelect?.(themeName)} onClose={handleThemeClose} isActive={app.mode() === "theme_select"} /> - + - + props.onFileSelect?.(file)} onClose={handleFilePickerClose} isActive={app.mode() === "file_picker"} /> - + diff --git a/src/tui-solid/routes/session.tsx b/src/tui-solid/routes/session.tsx index 9209d1d..a3cd3ee 100644 --- a/src/tui-solid/routes/session.tsx +++ b/src/tui-solid/routes/session.tsx @@ -1,4 +1,4 @@ -import { Show, Switch, Match } from "solid-js"; +import { Show, Switch, Match, createSignal } from "solid-js"; import { useTheme } from "@tui-solid/context/theme"; import { useAppStore } from "@tui-solid/context/app"; import { Header } from "@tui-solid/components/header"; @@ -16,7 +16,11 @@ import { ProviderSelect } from "@tui-solid/components/provider-select"; import { FilePicker } from "@tui-solid/components/file-picker"; import { PermissionModal } from "@tui-solid/components/permission-modal"; import { LearningModal } from "@tui-solid/components/learning-modal"; +import { HelpMenu } from "@tui-solid/components/help-menu"; +import { HelpDetail } from "@tui-solid/components/help-detail"; import { TodoPanel } from "@tui-solid/components/todo-panel"; +import { CenteredModal } from "@tui-solid/components/centered-modal"; +import { DebugLogPanel } from "@tui-solid/components/debug-log-panel"; import type { PermissionScope, LearningScope, InteractionMode } from "@/types/tui"; import type { MCPAddFormData } from "@/types/mcp"; @@ -73,6 +77,11 @@ export function Session(props: SessionProps) { const theme = useTheme(); const app = useAppStore(); + // Local state for help menu + const [selectedHelpTopic, setSelectedHelpTopic] = createSignal( + null + ); + const handleCommandSelect = (command: string): void => { const lowerCommand = command.toLowerCase(); // Handle menu-opening commands directly to avoid async timing issues @@ -100,6 +109,10 @@ export function Session(props: SessionProps) { app.transitionFromCommandMenu("provider_select"); return; } + if (lowerCommand === "help" || lowerCommand === "h" || lowerCommand === "?") { + app.transitionFromCommandMenu("help_menu"); + return; + } // For other commands, close menu and process through handler app.closeCommandMenu(); props.onCommand(command); @@ -141,9 +154,9 @@ export function Session(props: SessionProps) { app.setMode("idle"); }; - const handleProviderSelect = (providerId: string): void => { + const handleProviderSelect = async (providerId: string): Promise => { app.setProvider(providerId); - props.onProviderSelect?.(providerId); + await props.onProviderSelect?.(providerId); }; const handleProviderClose = (): void => { @@ -159,6 +172,26 @@ export function Session(props: SessionProps) { app.setMode("idle"); }; + const handleHelpTopicSelect = (topicId: string): void => { + setSelectedHelpTopic(topicId); + app.setMode("help_detail"); + }; + + const handleHelpMenuClose = (): void => { + setSelectedHelpTopic(null); + app.setMode("idle"); + }; + + const handleHelpDetailBack = (): void => { + setSelectedHelpTopic(null); + app.setMode("help_menu"); + }; + + const handleHelpDetailClose = (): void => { + setSelectedHelpTopic(null); + app.setMode("idle"); + }; + return ( + + + + @@ -182,37 +219,37 @@ export function Session(props: SessionProps) { - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + diff --git a/src/tui-solid/types/index.ts b/src/tui-solid/types/index.ts index f753bdd..e4d7740 100644 --- a/src/tui-solid/types/index.ts +++ b/src/tui-solid/types/index.ts @@ -35,6 +35,8 @@ export type AppMode = | "theme_select" | "mcp_select" | "file_picker" + | "help_menu" + | "help_detail" | "error"; export type ScreenMode = "home" | "session"; diff --git a/src/types/tui.ts b/src/types/tui.ts index 77ad9e4..5eb8587 100644 --- a/src/types/tui.ts +++ b/src/types/tui.ts @@ -25,7 +25,9 @@ export type AppMode = | "mcp_add" | "file_picker" | "provider_select" - | "learning_prompt"; + | "learning_prompt" + | "help_menu" + | "help_detail"; /** Screen mode for determining which view to show */ export type ScreenMode = "home" | "session";