Add debug log panel, centered modals, and fix multiple UX issues
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
This commit is contained in:
20
package-lock.json
generated
20
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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<string, CommandHandler> = 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<string, CommandHandler> = 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;
|
||||
|
||||
@@ -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<void> => {
|
||||
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);
|
||||
|
||||
@@ -103,4 +103,5 @@ export type CommandName =
|
||||
| "p"
|
||||
| "status"
|
||||
| "remember"
|
||||
| "learnings";
|
||||
| "learnings"
|
||||
| "logs";
|
||||
|
||||
182
src/constants/help-content.ts
Normal file
182
src/constants/help-content.ts
Normal file
@@ -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);
|
||||
@@ -47,6 +47,8 @@ export const MODE_DISPLAY_CONFIG: Record<string, ModeDisplayConfig> = {
|
||||
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
|
||||
{
|
||||
|
||||
14
src/index.ts
14
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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<void> => {
|
||||
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;
|
||||
|
||||
@@ -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<void> => {
|
||||
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,
|
||||
|
||||
@@ -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> | 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<CommandName, CommandHandler> = {
|
||||
help: showHelp,
|
||||
h: showHelp,
|
||||
@@ -137,6 +142,7 @@ const COMMAND_HANDLERS: Record<CommandName, CommandHandler> = {
|
||||
status: showStatus,
|
||||
remember: handleRememberCommand,
|
||||
learnings: (_, callbacks) => handleLearningsCommand(callbacks),
|
||||
logs: toggleDebugLogs,
|
||||
};
|
||||
|
||||
export const executeCommand = async (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
29
src/tui-solid/components/centered-modal.tsx
Normal file
29
src/tui-solid/components/centered-modal.tsx
Normal file
@@ -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 (
|
||||
<box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
backgroundColor={theme.colors.background}
|
||||
>
|
||||
{props.children}
|
||||
</box>
|
||||
);
|
||||
}
|
||||
200
src/tui-solid/components/debug-log-panel.tsx
Normal file
200
src/tui-solid/components/debug-log-panel.tsx
Normal file
@@ -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<DebugEntry[]>([...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<DebugEntry["type"], string> = {
|
||||
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<DebugEntry["type"], string> = {
|
||||
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 (
|
||||
<box
|
||||
flexDirection="column"
|
||||
width="20%"
|
||||
borderColor={theme.colors.border}
|
||||
border={["top", "bottom", "left", "right"]}
|
||||
backgroundColor={theme.colors.background}
|
||||
>
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
borderColor={theme.colors.border}
|
||||
border={["bottom"]}
|
||||
>
|
||||
<text fg={theme.colors.accent} attributes={TextAttributes.BOLD}>
|
||||
Debug Logs
|
||||
</text>
|
||||
<text fg={theme.colors.textDim}> ({entries().length})</text>
|
||||
</box>
|
||||
|
||||
<scrollbox
|
||||
ref={scrollboxRef}
|
||||
stickyScroll={stickyEnabled()}
|
||||
stickyStart="bottom"
|
||||
flexGrow={1}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
>
|
||||
<box flexDirection="column">
|
||||
<For each={entries()}>
|
||||
{(entry) => (
|
||||
<box flexDirection="row">
|
||||
<text fg={theme.colors.textDim}>
|
||||
{formatTime(entry.timestamp)}{" "}
|
||||
</text>
|
||||
<text fg={getTypeColor(entry.type)}>
|
||||
[{getTypeLabel(entry.type)}]{" "}
|
||||
</text>
|
||||
<text fg={theme.colors.text} wrapMode="word">
|
||||
{truncateMessage(entry.message, 50)}
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</scrollbox>
|
||||
|
||||
<box
|
||||
paddingLeft={1}
|
||||
borderColor={theme.colors.border}
|
||||
border={["top"]}
|
||||
>
|
||||
<text fg={theme.colors.textDim}>Shift+PgUp/PgDn scroll</text>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
115
src/tui-solid/components/help-detail.tsx
Normal file
115
src/tui-solid/components/help-detail.tsx
Normal file
@@ -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 (
|
||||
<box
|
||||
flexDirection="column"
|
||||
borderColor={theme.colors.error}
|
||||
border={["top", "bottom", "left", "right"]}
|
||||
backgroundColor={theme.colors.background}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
>
|
||||
<text fg={theme.colors.error}>Topic not found</text>
|
||||
<text fg={theme.colors.textDim}>Press Esc to go back</text>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<box
|
||||
flexDirection="column"
|
||||
borderColor={theme.colors.info}
|
||||
border={["top", "bottom", "left", "right"]}
|
||||
backgroundColor={theme.colors.background}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
>
|
||||
<text fg={theme.colors.info} attributes={TextAttributes.BOLD}>
|
||||
{currentTopic.name}
|
||||
</text>
|
||||
|
||||
<box height={1} />
|
||||
|
||||
<text fg={theme.colors.text}>{currentTopic.fullDescription}</text>
|
||||
|
||||
<Show when={currentTopic.usage}>
|
||||
<box height={1} />
|
||||
<text fg={theme.colors.warning} attributes={TextAttributes.BOLD}>
|
||||
Usage
|
||||
</text>
|
||||
<text fg={theme.colors.success}> {currentTopic.usage}</text>
|
||||
</Show>
|
||||
|
||||
<Show when={currentTopic.examples && currentTopic.examples.length > 0}>
|
||||
<box height={1} />
|
||||
<text fg={theme.colors.warning} attributes={TextAttributes.BOLD}>
|
||||
Examples
|
||||
</text>
|
||||
<For each={currentTopic.examples}>
|
||||
{(example) => (
|
||||
<text fg={theme.colors.text}> • {example}</text>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
<Show when={currentTopic.shortcuts && currentTopic.shortcuts.length > 0}>
|
||||
<box height={1} />
|
||||
<text fg={theme.colors.warning} attributes={TextAttributes.BOLD}>
|
||||
Shortcuts
|
||||
</text>
|
||||
<For each={currentTopic.shortcuts}>
|
||||
{(shortcut) => (
|
||||
<text fg={theme.colors.primary}> {shortcut}</text>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
<box height={1} />
|
||||
|
||||
<text fg={theme.colors.textDim}>
|
||||
Esc/Backspace back | Enter close
|
||||
</text>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
161
src/tui-solid/components/help-menu.tsx
Normal file
161
src/tui-solid/components/help-menu.tsx
Normal file
@@ -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 (
|
||||
<box
|
||||
flexDirection="column"
|
||||
borderColor={theme.colors.info}
|
||||
border={["top", "bottom", "left", "right"]}
|
||||
backgroundColor={theme.colors.background}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
>
|
||||
<box marginBottom={1}>
|
||||
<text fg={theme.colors.info} attributes={TextAttributes.BOLD}>
|
||||
Help - Select a topic
|
||||
</text>
|
||||
</box>
|
||||
|
||||
<For each={groupedTopics()}>
|
||||
{(group) => (
|
||||
<box flexDirection="column" marginBottom={1}>
|
||||
<box>
|
||||
<text fg={theme.colors.warning} attributes={TextAttributes.BOLD}>
|
||||
{group.categoryName}
|
||||
</text>
|
||||
</box>
|
||||
<For each={group.topics}>
|
||||
{(topic) => {
|
||||
const selected = () => isTopicSelected(topic.id);
|
||||
return (
|
||||
<box flexDirection="row">
|
||||
<text
|
||||
fg={selected() ? theme.colors.primary : theme.colors.text}
|
||||
attributes={
|
||||
selected() ? TextAttributes.BOLD : TextAttributes.NONE
|
||||
}
|
||||
>
|
||||
{selected() ? "> " : " "}
|
||||
</text>
|
||||
<text
|
||||
fg={
|
||||
selected() ? theme.colors.primary : theme.colors.success
|
||||
}
|
||||
>
|
||||
{topic.name.padEnd(14).substring(0, 14)}
|
||||
</text>
|
||||
<text fg={theme.colors.textDim}>
|
||||
{" "}
|
||||
{topic.description.substring(0, 40)}
|
||||
</text>
|
||||
</box>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<box marginTop={1}>
|
||||
<text fg={theme.colors.textDim}>
|
||||
↑↓ navigate | Enter details | Esc close
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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 = () =>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
</box>
|
||||
|
||||
<box flexDirection="column" marginTop={1}>
|
||||
<For each={SCOPE_OPTIONS}>
|
||||
<For each={PERMISSION_OPTIONS}>
|
||||
{(option, index) => {
|
||||
const isSelected = () => index() === selectedIndex();
|
||||
const keyColor = () =>
|
||||
option.allowed ? theme.colors.success : theme.colors.error;
|
||||
return (
|
||||
<box flexDirection="row">
|
||||
<text
|
||||
@@ -127,7 +147,7 @@ export function PermissionModal(props: PermissionModalProps) {
|
||||
>
|
||||
{isSelected() ? "> " : " "}
|
||||
</text>
|
||||
<text fg={theme.colors.success}>[{option.key}] </text>
|
||||
<text fg={keyColor()}>[{option.key}] </text>
|
||||
<text fg={isSelected() ? theme.colors.primary : undefined}>
|
||||
{option.label}
|
||||
</text>
|
||||
@@ -135,32 +155,6 @@ export function PermissionModal(props: PermissionModalProps) {
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
<box flexDirection="row">
|
||||
<text
|
||||
fg={
|
||||
selectedIndex() === SCOPE_OPTIONS.length
|
||||
? theme.colors.primary
|
||||
: undefined
|
||||
}
|
||||
attributes={
|
||||
selectedIndex() === SCOPE_OPTIONS.length
|
||||
? TextAttributes.BOLD
|
||||
: TextAttributes.NONE
|
||||
}
|
||||
>
|
||||
{selectedIndex() === SCOPE_OPTIONS.length ? "> " : " "}
|
||||
</text>
|
||||
<text fg={theme.colors.error}>[n] </text>
|
||||
<text
|
||||
fg={
|
||||
selectedIndex() === SCOPE_OPTIONS.length
|
||||
? theme.colors.primary
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
No, deny this request
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
<box marginTop={1}>
|
||||
|
||||
@@ -14,7 +14,7 @@ interface ProviderOption {
|
||||
}
|
||||
|
||||
interface ProviderSelectProps {
|
||||
onSelect: (providerId: string) => void;
|
||||
onSelect: (providerId: string) => Promise<void> | 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;
|
||||
|
||||
@@ -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<ProviderModel[]>;
|
||||
sessionStats: Accessor<SessionStats>;
|
||||
todosVisible: Accessor<boolean>;
|
||||
debugLogVisible: Accessor<boolean>;
|
||||
interruptPending: Accessor<boolean>;
|
||||
exitPending: Accessor<boolean>;
|
||||
isCompacting: Accessor<boolean>;
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
<Switch>
|
||||
<Match when={app.mode() === "command_menu"}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<CommandMenu
|
||||
onSelect={(command) => {
|
||||
const lowerCommand = command.toLowerCase();
|
||||
@@ -95,38 +96,38 @@ export function Home(props: HomeProps) {
|
||||
onCancel={() => app.closeCommandMenu()}
|
||||
isActive={app.mode() === "command_menu"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "model_select"}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<ModelSelect
|
||||
onSelect={(model) => props.onModelSelect?.(model)}
|
||||
onClose={handleModelClose}
|
||||
isActive={app.mode() === "model_select"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "theme_select"}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<ThemeSelect
|
||||
onSelect={(themeName) => props.onThemeSelect?.(themeName)}
|
||||
onClose={handleThemeClose}
|
||||
isActive={app.mode() === "theme_select"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "file_picker"}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<FilePicker
|
||||
files={props.files ?? []}
|
||||
onSelect={(file) => props.onFileSelect?.(file)}
|
||||
onClose={handleFilePickerClose}
|
||||
isActive={app.mode() === "file_picker"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
|
||||
@@ -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<string | null>(
|
||||
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<void> => {
|
||||
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 (
|
||||
<box
|
||||
flexDirection="column"
|
||||
@@ -175,6 +208,10 @@ export function Session(props: SessionProps) {
|
||||
<Show when={app.todosVisible() && props.plan}>
|
||||
<TodoPanel plan={props.plan ?? null} visible={app.todosVisible()} />
|
||||
</Show>
|
||||
|
||||
<Show when={app.debugLogVisible()}>
|
||||
<DebugLogPanel />
|
||||
</Show>
|
||||
</box>
|
||||
|
||||
<StatusBar />
|
||||
@@ -182,37 +219,37 @@ export function Session(props: SessionProps) {
|
||||
|
||||
<Switch>
|
||||
<Match when={app.mode() === "command_menu"}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<CommandMenu
|
||||
onSelect={handleCommandSelect}
|
||||
onCancel={handleCommandCancel}
|
||||
isActive={app.mode() === "command_menu"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "model_select"}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<ModelSelect
|
||||
onSelect={props.onModelSelect}
|
||||
onClose={handleModelClose}
|
||||
isActive={app.mode() === "model_select"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "theme_select"}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<ThemeSelect
|
||||
onSelect={props.onThemeSelect}
|
||||
onClose={handleThemeClose}
|
||||
isActive={app.mode() === "theme_select"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "agent_select" && props.agents}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<AgentSelect
|
||||
agents={props.agents ?? []}
|
||||
currentAgent={app.currentAgent()}
|
||||
@@ -223,11 +260,11 @@ export function Session(props: SessionProps) {
|
||||
onClose={handleAgentClose}
|
||||
isActive={app.mode() === "agent_select"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "mcp_select"}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<MCPSelect
|
||||
servers={props.mcpServers ?? []}
|
||||
onSelect={props.onMCPSelect}
|
||||
@@ -235,31 +272,31 @@ export function Session(props: SessionProps) {
|
||||
onClose={handleMCPClose}
|
||||
isActive={app.mode() === "mcp_select"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "mcp_add"}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<MCPAddForm
|
||||
onSubmit={props.onMCPAdd}
|
||||
onClose={handleMCPAddClose}
|
||||
isActive={app.mode() === "mcp_add"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "mode_select"}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<ModeSelect
|
||||
onSelect={handleModeSelect}
|
||||
onClose={handleModeClose}
|
||||
isActive={app.mode() === "mode_select"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "provider_select"}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<ProviderSelect
|
||||
onSelect={handleProviderSelect}
|
||||
onClose={handleProviderClose}
|
||||
@@ -269,40 +306,61 @@ export function Session(props: SessionProps) {
|
||||
providerStatuses={props.providerStatuses}
|
||||
providerScores={props.providerScores}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "file_picker"}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<FilePicker
|
||||
files={props.files ?? []}
|
||||
onSelect={props.onFileSelect}
|
||||
onClose={handleFilePickerClose}
|
||||
isActive={app.mode() === "file_picker"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match
|
||||
when={app.mode() === "permission_prompt" && app.permissionRequest()}
|
||||
>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<PermissionModal
|
||||
request={app.permissionRequest()!}
|
||||
onRespond={props.onPermissionResponse}
|
||||
isActive={app.mode() === "permission_prompt"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "learning_prompt" && app.learningPrompt()}>
|
||||
<box position="absolute" top={3} left={2}>
|
||||
<CenteredModal>
|
||||
<LearningModal
|
||||
prompt={app.learningPrompt()!}
|
||||
onRespond={props.onLearningResponse}
|
||||
isActive={app.mode() === "learning_prompt"}
|
||||
/>
|
||||
</box>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "help_menu"}>
|
||||
<CenteredModal>
|
||||
<HelpMenu
|
||||
onSelectTopic={handleHelpTopicSelect}
|
||||
onClose={handleHelpMenuClose}
|
||||
isActive={app.mode() === "help_menu"}
|
||||
/>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
|
||||
<Match when={app.mode() === "help_detail" && selectedHelpTopic()}>
|
||||
<CenteredModal>
|
||||
<HelpDetail
|
||||
topicId={selectedHelpTopic()!}
|
||||
onBack={handleHelpDetailBack}
|
||||
onClose={handleHelpDetailClose}
|
||||
isActive={app.mode() === "help_detail"}
|
||||
/>
|
||||
</CenteredModal>
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
|
||||
@@ -35,6 +35,8 @@ export type AppMode =
|
||||
| "theme_select"
|
||||
| "mcp_select"
|
||||
| "file_picker"
|
||||
| "help_menu"
|
||||
| "help_detail"
|
||||
| "error";
|
||||
|
||||
export type ScreenMode = "home" | "session";
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user