Improve agent autonomy and diff view readability
Agent behavior improvements: - Add project context detection (tsconfig.json, pom.xml, etc.) - Enforce validation after changes (tsc --noEmit, mvn compile, etc.) - Run tests automatically - never ask "do you want me to run tests" - Complete full loop: create → type-check → test → confirm - Add command detection for direct execution (run tree, run ls) Diff view improvements: - Use darker backgrounds for added/removed lines - Add diffLineBgAdded, diffLineBgRemoved, diffLineText theme colors - Improve text visibility with white text on dark backgrounds - Update both React/Ink and SolidJS diff components Streaming fixes: - Fix tool call argument accumulation using OpenAI index field - Fix streaming content display after tool calls - Add consecutive error tracking to prevent token waste Other changes: - ESC to abort operations, Ctrl+C to exit - Fix model selection when provider changes in cascade mode - Add debug logging for troubleshooting - Move tests to root tests/ folder - Fix banner test GRADIENT_COLORS reference
This commit is contained in:
@@ -43,7 +43,7 @@ import {
|
||||
checkOllamaAvailability,
|
||||
checkCopilotAvailability,
|
||||
} from "@services/cascading-provider";
|
||||
import { chat } from "@providers/chat";
|
||||
import { chat, getDefaultModel } from "@providers/chat";
|
||||
import { AUDIT_SYSTEM_PROMPT, createAuditPrompt, parseAuditResponse } from "@prompts/audit-prompt";
|
||||
import { PROVIDER_IDS } from "@constants/provider-quality";
|
||||
import { appStore } from "@tui/index";
|
||||
@@ -55,6 +55,12 @@ import type {
|
||||
ToolCallInfo,
|
||||
} from "@/types/chat-service";
|
||||
import { addDebugLog } from "@tui-solid/components/debug-log-panel";
|
||||
import { FILE_MODIFYING_TOOLS } from "@constants/tools";
|
||||
import type { StreamCallbacksWithState } from "@interfaces/StreamCallbacksWithState";
|
||||
import {
|
||||
detectCommand,
|
||||
executeDetectedCommand,
|
||||
} from "@services/command-detection";
|
||||
|
||||
// Track last response for feedback learning
|
||||
let lastResponseContext: {
|
||||
@@ -63,7 +69,25 @@ let lastResponseContext: {
|
||||
response: string;
|
||||
} | null = null;
|
||||
|
||||
const FILE_MODIFYING_TOOLS = ["write", "edit"];
|
||||
// Track current running agent for abort capability
|
||||
let currentAgent: { stop: () => void } | null = null;
|
||||
|
||||
/**
|
||||
* Abort the currently running agent operation
|
||||
* @returns true if an operation was aborted, false if nothing was running
|
||||
*/
|
||||
export const abortCurrentOperation = (): boolean => {
|
||||
if (currentAgent) {
|
||||
currentAgent.stop();
|
||||
currentAgent = null;
|
||||
appStore.cancelStreaming();
|
||||
appStore.stopThinking();
|
||||
appStore.setMode("idle");
|
||||
addDebugLog("state", "Operation aborted by user");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const createToolCallHandler =
|
||||
(
|
||||
@@ -72,7 +96,7 @@ const createToolCallHandler =
|
||||
) =>
|
||||
(call: { id: string; name: string; arguments?: Record<string, unknown> }) => {
|
||||
const args = call.arguments;
|
||||
if (FILE_MODIFYING_TOOLS.includes(call.name) && args?.path) {
|
||||
if ((FILE_MODIFYING_TOOLS as readonly string[]).includes(call.name) && args?.path) {
|
||||
toolCallRef.current = { name: call.name, path: String(args.path) };
|
||||
} else {
|
||||
toolCallRef.current = { name: call.name };
|
||||
@@ -117,10 +141,10 @@ const createToolResultHandler =
|
||||
/**
|
||||
* Create streaming callbacks for TUI integration
|
||||
*/
|
||||
const createStreamCallbacks = (): StreamCallbacks => {
|
||||
const createStreamCallbacks = (): StreamCallbacksWithState => {
|
||||
let chunkCount = 0;
|
||||
|
||||
return {
|
||||
const callbacks: StreamCallbacks = {
|
||||
onContentChunk: (content: string) => {
|
||||
chunkCount++;
|
||||
addDebugLog("stream", `Chunk #${chunkCount}: "${content.substring(0, 30)}${content.length > 30 ? "..." : ""}"`);
|
||||
@@ -155,8 +179,10 @@ const createStreamCallbacks = (): StreamCallbacks => {
|
||||
},
|
||||
|
||||
onComplete: () => {
|
||||
addDebugLog("stream", `Stream complete (${chunkCount} chunks)`);
|
||||
appStore.completeStreaming();
|
||||
// Note: Don't call completeStreaming() here!
|
||||
// The agent loop may have multiple iterations (tool calls + final response)
|
||||
// Streaming will be completed manually after the entire agent finishes
|
||||
addDebugLog("stream", `Stream iteration done (${chunkCount} chunks total)`);
|
||||
},
|
||||
|
||||
onError: (error: string) => {
|
||||
@@ -168,6 +194,11 @@ const createStreamCallbacks = (): StreamCallbacks => {
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
callbacks,
|
||||
hasReceivedContent: () => chunkCount > 0,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -245,6 +276,50 @@ export const handleMessage = async (
|
||||
// Check for feedback on previous response
|
||||
await checkUserFeedback(message, callbacks);
|
||||
|
||||
// Detect explicit command requests and execute directly
|
||||
const detected = detectCommand(message);
|
||||
if (detected.detected && detected.command) {
|
||||
addDebugLog("info", `Detected command: ${detected.command}`);
|
||||
|
||||
// Show the user's request
|
||||
appStore.addLog({
|
||||
type: "user",
|
||||
content: message,
|
||||
});
|
||||
|
||||
// Show what we're running
|
||||
appStore.addLog({
|
||||
type: "tool",
|
||||
content: detected.command,
|
||||
metadata: {
|
||||
toolName: "bash",
|
||||
toolStatus: "running",
|
||||
toolDescription: `Running: ${detected.command}`,
|
||||
},
|
||||
});
|
||||
|
||||
appStore.setMode("tool_execution");
|
||||
const result = await executeDetectedCommand(detected.command, process.cwd());
|
||||
appStore.setMode("idle");
|
||||
|
||||
// Show result
|
||||
if (result.success && result.output) {
|
||||
appStore.addLog({
|
||||
type: "assistant",
|
||||
content: result.output,
|
||||
});
|
||||
} else if (!result.success) {
|
||||
appStore.addLog({
|
||||
type: "error",
|
||||
content: result.error || "Command failed",
|
||||
});
|
||||
}
|
||||
|
||||
// Save to session (for persistence only, not UI)
|
||||
await saveSession();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get interaction mode and cascade setting from app store
|
||||
const { interactionMode, cascadeEnabled } = appStore.getState();
|
||||
const isReadOnlyMode = interactionMode === "ask" || interactionMode === "code-review";
|
||||
@@ -397,23 +472,34 @@ export const handleMessage = async (
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the correct model for the provider
|
||||
// If provider changed, use the provider's default model instead of state.model
|
||||
const effectiveModel =
|
||||
effectiveProvider === state.provider
|
||||
? state.model
|
||||
: getDefaultModel(effectiveProvider);
|
||||
|
||||
// Start streaming UI
|
||||
addDebugLog("state", `Starting request: provider=${effectiveProvider}, model=${state.model}`);
|
||||
addDebugLog("state", `Starting request: provider=${effectiveProvider}, model=${effectiveModel}`);
|
||||
addDebugLog("state", `Mode: ${appStore.getState().interactionMode}, Cascade: ${cascadeEnabled}`);
|
||||
appStore.setMode("thinking");
|
||||
appStore.startThinking();
|
||||
appStore.startStreaming();
|
||||
addDebugLog("state", "Streaming started");
|
||||
|
||||
const streamCallbacks = createStreamCallbacks();
|
||||
const streamState = createStreamCallbacks();
|
||||
const agent = createStreamingAgent(
|
||||
process.cwd(),
|
||||
{
|
||||
provider: effectiveProvider,
|
||||
model: state.model,
|
||||
model: effectiveModel,
|
||||
verbose: state.verbose,
|
||||
autoApprove: state.autoApprove,
|
||||
chatMode: isReadOnlyMode,
|
||||
onText: (text: string) => {
|
||||
addDebugLog("info", `onText callback: "${text.substring(0, 50)}..."`);
|
||||
appStore.appendStreamContent(text);
|
||||
},
|
||||
onToolCall: createToolCallHandler(callbacks, toolCallRef),
|
||||
onToolResult: createToolResultHandler(callbacks, toolCallRef),
|
||||
onError: (error) => {
|
||||
@@ -423,9 +509,12 @@ export const handleMessage = async (
|
||||
callbacks.onLog("system", warning);
|
||||
},
|
||||
},
|
||||
streamCallbacks,
|
||||
streamState.callbacks,
|
||||
);
|
||||
|
||||
// Store agent reference for abort capability
|
||||
currentAgent = agent;
|
||||
|
||||
try {
|
||||
addDebugLog("api", `Agent.run() started with ${state.messages.length} messages`);
|
||||
const result = await agent.run(state.messages);
|
||||
@@ -471,14 +560,18 @@ export const handleMessage = async (
|
||||
|
||||
// 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) {
|
||||
if (!streamState.hasReceivedContent() && finalResponse) {
|
||||
addDebugLog("info", "No streaming content received, adding fallback log");
|
||||
// Streaming didn't receive content, manually add the response
|
||||
appStore.cancelStreaming(); // Remove empty streaming log
|
||||
appStore.addLog({
|
||||
type: "assistant",
|
||||
content: finalResponse,
|
||||
});
|
||||
} else {
|
||||
// Streaming received content - finalize the streaming log
|
||||
addDebugLog("info", "Completing streaming with received content");
|
||||
appStore.completeStreaming();
|
||||
}
|
||||
|
||||
addMessage("user", message);
|
||||
@@ -501,5 +594,8 @@ export const handleMessage = async (
|
||||
appStore.cancelStreaming();
|
||||
appStore.stopThinking();
|
||||
callbacks.onLog("error", String(error));
|
||||
} finally {
|
||||
// Clear agent reference when done
|
||||
currentAgent = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type { Message } from "@/types/providers";
|
||||
import type { AgentOptions } from "@interfaces/AgentOptions";
|
||||
import type { AgentResult } from "@interfaces/AgentResult";
|
||||
import type { StreamingChatOptions } from "@interfaces/StreamingChatOptions";
|
||||
import type {
|
||||
StreamCallbacks,
|
||||
PartialToolCall,
|
||||
@@ -16,13 +17,8 @@ import type { ToolCall, ToolResult } from "@/types/tools";
|
||||
import { createStreamingAgent } from "@services/agent-stream";
|
||||
import { appStore } from "@tui/index";
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
export interface StreamingChatOptions extends AgentOptions {
|
||||
onModelSwitch?: (info: ModelSwitchInfo) => void;
|
||||
}
|
||||
// Re-export for convenience
|
||||
export type { StreamingChatOptions } from "@interfaces/StreamingChatOptions";
|
||||
|
||||
// =============================================================================
|
||||
// TUI Streaming Callbacks
|
||||
|
||||
@@ -5,16 +5,13 @@
|
||||
import { usageStore } from "@stores/usage-store";
|
||||
import { getUserInfo } from "@providers/copilot/credentials";
|
||||
import { getCopilotUsage } from "@providers/copilot/usage";
|
||||
import { PROGRESS_BAR } from "@constants/ui";
|
||||
import type {
|
||||
ChatServiceState,
|
||||
ChatServiceCallbacks,
|
||||
} from "@/types/chat-service";
|
||||
import type { CopilotQuotaDetail } from "@/types/copilot-usage";
|
||||
|
||||
const BAR_WIDTH = 40;
|
||||
const FILLED_CHAR = "█";
|
||||
const EMPTY_CHAR = "░";
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
return num.toLocaleString();
|
||||
};
|
||||
@@ -35,9 +32,12 @@ const formatDuration = (ms: number): string => {
|
||||
|
||||
const renderBar = (percent: number): string => {
|
||||
const clampedPercent = Math.max(0, Math.min(100, percent));
|
||||
const filledWidth = Math.round((clampedPercent / 100) * BAR_WIDTH);
|
||||
const emptyWidth = BAR_WIDTH - filledWidth;
|
||||
return FILLED_CHAR.repeat(filledWidth) + EMPTY_CHAR.repeat(emptyWidth);
|
||||
const filledWidth = Math.round((clampedPercent / 100) * PROGRESS_BAR.WIDTH);
|
||||
const emptyWidth = PROGRESS_BAR.WIDTH - filledWidth;
|
||||
return (
|
||||
PROGRESS_BAR.FILLED_CHAR.repeat(filledWidth) +
|
||||
PROGRESS_BAR.EMPTY_CHAR.repeat(emptyWidth)
|
||||
);
|
||||
};
|
||||
|
||||
const formatQuotaBar = (
|
||||
@@ -55,7 +55,7 @@ const formatQuotaBar = (
|
||||
|
||||
if (quota.unlimited) {
|
||||
lines.push(name);
|
||||
lines.push(FILLED_CHAR.repeat(BAR_WIDTH) + " Unlimited");
|
||||
lines.push(PROGRESS_BAR.FILLED_CHAR.repeat(PROGRESS_BAR.WIDTH) + " Unlimited");
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user