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:
@@ -9,6 +9,7 @@ import {
|
||||
} from "solid-js";
|
||||
import { batch } from "solid-js";
|
||||
import { getFiles } from "@services/file-picker/files";
|
||||
import { abortCurrentOperation } from "@services/chat-tui-service";
|
||||
import versionData from "@/version.json";
|
||||
import {
|
||||
ExitProvider,
|
||||
@@ -90,7 +91,7 @@ function ErrorFallback(props: { error: Error }) {
|
||||
{props.error.message}
|
||||
</text>
|
||||
<text fg={theme.colors.textDim} marginTop={2}>
|
||||
Press Ctrl+C to exit
|
||||
Press Ctrl+C twice to exit
|
||||
</text>
|
||||
</box>
|
||||
);
|
||||
@@ -157,16 +158,29 @@ function AppContent(props: AppProps) {
|
||||
}
|
||||
|
||||
useKeyboard((evt) => {
|
||||
// ESC aborts current operation
|
||||
if (evt.name === "escape") {
|
||||
const aborted = abortCurrentOperation();
|
||||
if (aborted) {
|
||||
toast.info("Operation cancelled");
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl+C exits the application
|
||||
if (evt.ctrl && evt.name === "c") {
|
||||
if (app.interruptPending()) {
|
||||
exit.exit(0);
|
||||
} else {
|
||||
app.setInterruptPending(true);
|
||||
toast.warning("Press Ctrl+C again to exit");
|
||||
setTimeout(() => {
|
||||
app.setInterruptPending(false);
|
||||
}, 2000);
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
app.setInterruptPending(true);
|
||||
toast.warning("Press Ctrl+C again to exit");
|
||||
setTimeout(() => {
|
||||
app.setInterruptPending(false);
|
||||
}, 2000);
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createMemo, For, createSignal, onMount, onCleanup } from "solid-js";
|
||||
import { For, createSignal, onMount, onCleanup } from "solid-js";
|
||||
import { useKeyboard } from "@opentui/solid";
|
||||
import { TextAttributes } from "@opentui/core";
|
||||
import type { ScrollBoxRenderable } from "@opentui/core";
|
||||
@@ -10,7 +10,7 @@ const SCROLL_LINES = 2;
|
||||
interface DebugEntry {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
type: "api" | "stream" | "tool" | "state" | "error" | "info";
|
||||
type: "api" | "stream" | "tool" | "state" | "error" | "info" | "render";
|
||||
message: string;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ export function DebugLogPanel() {
|
||||
state: theme.colors.accent,
|
||||
error: theme.colors.error,
|
||||
info: theme.colors.textDim,
|
||||
render: theme.colors.primary,
|
||||
};
|
||||
return colorMap[type];
|
||||
};
|
||||
@@ -92,6 +93,7 @@ export function DebugLogPanel() {
|
||||
state: "STA",
|
||||
error: "ERR",
|
||||
info: "INF",
|
||||
render: "RND",
|
||||
};
|
||||
return labelMap[type];
|
||||
};
|
||||
|
||||
@@ -56,9 +56,9 @@ function DiffLine(props: DiffLineProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
const lineColor = (): string => {
|
||||
// Use white text for add/remove lines since they have colored backgrounds
|
||||
// Use light text for add/remove lines since they have dark colored backgrounds
|
||||
if (props.line.type === "add" || props.line.type === "remove") {
|
||||
return theme.colors.text;
|
||||
return theme.colors.diffLineText;
|
||||
}
|
||||
const colorMap: Record<string, string> = {
|
||||
context: theme.colors.diffContext,
|
||||
@@ -82,8 +82,8 @@ function DiffLine(props: DiffLineProps) {
|
||||
};
|
||||
|
||||
const bgColor = (): string | undefined => {
|
||||
if (props.line.type === "add") return theme.colors.bgAdded;
|
||||
if (props.line.type === "remove") return theme.colors.bgRemoved;
|
||||
if (props.line.type === "add") return theme.colors.diffLineBgAdded;
|
||||
if (props.line.type === "remove") return theme.colors.diffLineBgRemoved;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Show } from "solid-js";
|
||||
import { Show, createSignal, createEffect, onMount } from "solid-js";
|
||||
import { TextAttributes } from "@opentui/core";
|
||||
import { useTheme } from "@tui-solid/context/theme";
|
||||
import { useAppStore } from "@tui-solid/context/app";
|
||||
import type { LogEntry } from "@/types/tui";
|
||||
import { Spinner } from "@tui-solid/ui/spinner";
|
||||
import { addDebugLog } from "@tui-solid/components/debug-log-panel";
|
||||
|
||||
interface StreamingMessageProps {
|
||||
entry: LogEntry;
|
||||
@@ -10,8 +12,50 @@ interface StreamingMessageProps {
|
||||
|
||||
export function StreamingMessage(props: StreamingMessageProps) {
|
||||
const theme = useTheme();
|
||||
const isStreaming = () => props.entry.metadata?.isStreaming ?? false;
|
||||
const hasContent = () => Boolean(props.entry.content);
|
||||
const app = useAppStore();
|
||||
|
||||
// Use local signals that are updated via createEffect
|
||||
// This ensures proper reactivity with the store
|
||||
const [displayContent, setDisplayContent] = createSignal(props.entry.content);
|
||||
const [isActiveStreaming, setIsActiveStreaming] = createSignal(
|
||||
props.entry.metadata?.isStreaming ?? false
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
addDebugLog("render", `StreamingMessage mounted for entry: ${props.entry.id}`);
|
||||
});
|
||||
|
||||
// Effect to sync content from store's streamingLog
|
||||
// Use individual property accessors for fine-grained reactivity
|
||||
createEffect(() => {
|
||||
// Use dedicated property accessors that directly access store properties
|
||||
const logId = app.streamingLogId();
|
||||
const isActive = app.streamingLogIsActive();
|
||||
const storeContent = app.streamingLogContent();
|
||||
|
||||
// Check if this entry is the currently streaming log
|
||||
const isCurrentLog = logId === props.entry.id;
|
||||
|
||||
addDebugLog("render", `Effect: logId=${logId}, entryId=${props.entry.id}, isActive=${isActive}, contentLen=${storeContent?.length ?? 0}`);
|
||||
|
||||
if (isCurrentLog && isActive) {
|
||||
setDisplayContent(storeContent);
|
||||
setIsActiveStreaming(true);
|
||||
} else if (isCurrentLog && !isActive) {
|
||||
// Streaming just completed for this log
|
||||
setIsActiveStreaming(false);
|
||||
// Keep the content we have
|
||||
} else {
|
||||
// Not the current streaming log, use entry content
|
||||
setDisplayContent(props.entry.content);
|
||||
setIsActiveStreaming(props.entry.metadata?.isStreaming ?? false);
|
||||
}
|
||||
});
|
||||
|
||||
const hasContent = () => {
|
||||
const c = displayContent();
|
||||
return Boolean(c && c.length > 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<box flexDirection="column" marginBottom={1}>
|
||||
@@ -19,7 +63,7 @@ export function StreamingMessage(props: StreamingMessageProps) {
|
||||
<text fg={theme.colors.roleAssistant} attributes={TextAttributes.BOLD}>
|
||||
CodeTyper
|
||||
</text>
|
||||
<Show when={isStreaming()}>
|
||||
<Show when={isActiveStreaming()}>
|
||||
<box marginLeft={1}>
|
||||
<Spinner />
|
||||
</box>
|
||||
@@ -27,7 +71,7 @@ export function StreamingMessage(props: StreamingMessageProps) {
|
||||
</box>
|
||||
<Show when={hasContent()}>
|
||||
<box marginLeft={2}>
|
||||
<text wrapMode="word">{props.entry.content}</text>
|
||||
<text wrapMode="word">{displayContent()}</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
|
||||
@@ -76,6 +76,9 @@ interface AppContextValue {
|
||||
exitPending: Accessor<boolean>;
|
||||
isCompacting: Accessor<boolean>;
|
||||
streamingLog: Accessor<StreamingLogState>;
|
||||
streamingLogId: Accessor<string | null>;
|
||||
streamingLogContent: Accessor<string>;
|
||||
streamingLogIsActive: Accessor<boolean>;
|
||||
suggestions: Accessor<SuggestionState>;
|
||||
cascadeEnabled: Accessor<boolean>;
|
||||
|
||||
@@ -263,6 +266,10 @@ export const { provider: AppStoreProvider, use: useAppStore } =
|
||||
const exitPending = (): boolean => store.exitPending;
|
||||
const isCompacting = (): boolean => store.isCompacting;
|
||||
const streamingLog = (): StreamingLogState => store.streamingLog;
|
||||
// Individual property accessors for fine-grained reactivity
|
||||
const streamingLogId = (): string | null => store.streamingLog.logId;
|
||||
const streamingLogContent = (): string => store.streamingLog.content;
|
||||
const streamingLogIsActive = (): boolean => store.streamingLog.isStreaming;
|
||||
const suggestions = (): SuggestionState => store.suggestions;
|
||||
const cascadeEnabled = (): boolean => store.cascadeEnabled;
|
||||
|
||||
@@ -532,34 +539,30 @@ export const { provider: AppStoreProvider, use: useAppStore } =
|
||||
s.logs.push(entry);
|
||||
}),
|
||||
);
|
||||
setStore("streamingLog", {
|
||||
logId,
|
||||
content: "",
|
||||
isStreaming: true,
|
||||
});
|
||||
// Use path-based updates to ensure proper proxy reactivity
|
||||
setStore("streamingLog", "logId", logId);
|
||||
setStore("streamingLog", "content", "");
|
||||
setStore("streamingLog", "isStreaming", true);
|
||||
});
|
||||
return logId;
|
||||
};
|
||||
|
||||
const appendStreamContent = (content: string): void => {
|
||||
if (!store.streamingLog.logId || !store.streamingLog.isStreaming) {
|
||||
const logId = store.streamingLog.logId;
|
||||
const isCurrentlyStreaming = store.streamingLog.isStreaming;
|
||||
if (!logId || !isCurrentlyStreaming) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newContent = store.streamingLog.content + content;
|
||||
const logIndex = store.logs.findIndex((l) => l.id === logId);
|
||||
|
||||
batch(() => {
|
||||
setStore("streamingLog", {
|
||||
...store.streamingLog,
|
||||
content: newContent,
|
||||
});
|
||||
setStore(
|
||||
produce((s) => {
|
||||
const log = s.logs.find((l) => l.id === store.streamingLog.logId);
|
||||
if (log) {
|
||||
log.content = newContent;
|
||||
}
|
||||
}),
|
||||
);
|
||||
// Use path-based updates for proper reactivity tracking
|
||||
setStore("streamingLog", "content", newContent);
|
||||
if (logIndex !== -1) {
|
||||
setStore("logs", logIndex, "content", newContent);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -569,21 +572,19 @@ export const { provider: AppStoreProvider, use: useAppStore } =
|
||||
}
|
||||
|
||||
const logId = store.streamingLog.logId;
|
||||
const logIndex = store.logs.findIndex((l) => l.id === logId);
|
||||
|
||||
batch(() => {
|
||||
setStore("streamingLog", createInitialStreamingState());
|
||||
setStore(
|
||||
produce((s) => {
|
||||
const log = s.logs.find((l) => l.id === logId);
|
||||
if (log) {
|
||||
log.type = "assistant";
|
||||
log.metadata = {
|
||||
...log.metadata,
|
||||
isStreaming: false,
|
||||
streamComplete: true,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (logIndex !== -1) {
|
||||
const currentMetadata = store.logs[logIndex].metadata ?? {};
|
||||
setStore("logs", logIndex, "type", "assistant");
|
||||
setStore("logs", logIndex, "metadata", {
|
||||
...currentMetadata,
|
||||
isStreaming: false,
|
||||
streamComplete: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -692,6 +693,9 @@ export const { provider: AppStoreProvider, use: useAppStore } =
|
||||
exitPending,
|
||||
isCompacting,
|
||||
streamingLog,
|
||||
streamingLogId,
|
||||
streamingLogContent,
|
||||
streamingLogIsActive,
|
||||
suggestions,
|
||||
cascadeEnabled,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user