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:
2026-01-29 04:53:39 -05:00
parent dfbbab2ecb
commit adfebda501
27 changed files with 1031 additions and 167 deletions

View File

@@ -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 (

View File

@@ -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);