feat: detect prompt-too-long errors and offer compact action

Closes #158

- Detect "Prompt is too long" in assistant text blocks in wsl_bridge
- Emit error line type instead of assistant for better visibility
- Emit compact-prompt event after the error line
- Render a compact-prompt line as an action button in the terminal
- Clicking the button sends /compact to the active session
- Add getLineClass/getLinePrefix tests for compact-prompt type
This commit is contained in:
2026-02-24 17:18:44 -08:00
committed by Naomi Carrigan
parent 1c02ca1bb5
commit 108a1b16b2
5 changed files with 96 additions and 6 deletions
+22 -2
View File
@@ -1082,17 +1082,37 @@ fn process_json_line(
stats.write().increment_code_blocks();
}
let is_prompt_too_long = text.starts_with("Prompt is too long");
let _ = app.emit(
"claude:output",
OutputEvent {
line_type: "assistant".to_string(),
line_type: if is_prompt_too_long {
"error".to_string()
} else {
"assistant".to_string()
},
content: text.clone(),
tool_name: None,
conversation_id: conversation_id.clone(),
cost: message_cost.clone(), // Include cost with assistant text
cost: message_cost.clone(),
parent_tool_use_id: parent_tool_use_id.clone(),
},
);
if is_prompt_too_long {
let _ = app.emit(
"claude:output",
OutputEvent {
line_type: "compact-prompt".to_string(),
content: String::new(),
tool_name: None,
conversation_id: conversation_id.clone(),
cost: None,
parent_tool_use_id: None,
},
);
}
}
ContentBlock::Thinking { thinking } => {
state = CharacterState::Thinking;
+37 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { claudeStore, type TerminalLine } from "$lib/stores/claude";
import { afterUpdate, tick, onMount, onDestroy } from "svelte";
import { invoke } from "@tauri-apps/api/core";
import ConversationTabs from "./ConversationTabs.svelte";
import Markdown from "./Markdown.svelte";
@@ -95,6 +96,8 @@
return "terminal-thinking";
case "rate-limit":
return "terminal-rate-limit";
case "compact-prompt":
return "terminal-compact-prompt";
default:
return "terminal-default";
}
@@ -193,6 +196,11 @@
}, 2000);
}
async function handleCompact() {
if (!currentConversationId) return;
await invoke("send_prompt", { conversationId: currentConversationId, message: "/compact" });
}
// Collapsible tool lines
const TOOL_COLLAPSE_THRESHOLD = 60;
let expandedToolLines: Record<string, boolean> = {};
@@ -283,7 +291,11 @@
{#if line.toolName}
<span class="terminal-tool-name mr-2">[{line.toolName}]</span>
{/if}
{#if line.type === "assistant" || line.type === "user"}
{#if line.type === "compact-prompt"}
<button class="compact-action-btn" onclick={handleCompact}>
⚡ Compact Conversation
</button>
{:else if line.type === "assistant" || line.type === "user"}
<div class="message-content-wrapper">
<Markdown
content={maskPaths(line.content, hidePaths)}
@@ -370,6 +382,30 @@
color: var(--terminal-rate-limit, #fb923c);
}
.terminal-compact-prompt {
color: var(--text-secondary);
}
.compact-action-btn {
display: inline-flex;
align-items: center;
gap: 0.4em;
background: var(--bg-secondary);
border: 1px solid var(--terminal-error, #f87171);
color: var(--terminal-error, #f87171);
padding: 0.3em 0.8em;
cursor: pointer;
border-radius: 4px;
font-size: 0.9em;
font-family: inherit;
transition: all 0.15s ease;
}
.compact-action-btn:hover {
background: color-mix(in srgb, var(--terminal-error, #f87171) 15%, transparent);
color: var(--terminal-error, #f87171);
}
.terminal-default {
color: var(--text-primary);
}
+10
View File
@@ -38,6 +38,8 @@ function getLineClass(type: string): string {
return "terminal-thinking";
case "rate-limit":
return "terminal-rate-limit";
case "compact-prompt":
return "terminal-compact-prompt";
default:
return "terminal-default";
}
@@ -110,6 +112,10 @@ describe("getLineClass", () => {
expect(getLineClass("rate-limit")).toBe("terminal-rate-limit");
});
it("returns terminal-compact-prompt for compact-prompt lines", () => {
expect(getLineClass("compact-prompt")).toBe("terminal-compact-prompt");
});
it("returns terminal-default for unknown line types", () => {
expect(getLineClass("unknown")).toBe("terminal-default");
expect(getLineClass("")).toBe("terminal-default");
@@ -142,6 +148,10 @@ describe("getLinePrefix", () => {
expect(getLinePrefix("rate-limit")).toBe("[rate-limit]");
});
it("returns empty string for compact-prompt lines (button renders instead)", () => {
expect(getLinePrefix("compact-prompt")).toBe("");
});
it("returns empty string for thinking lines (no prefix)", () => {
expect(getLinePrefix("thinking")).toBe("");
});
+18 -2
View File
@@ -323,7 +323,15 @@ export async function initializeTauriListeners() {
if (conversation_id) {
claudeStore.addLineToConversation(
conversation_id,
line_type as "user" | "assistant" | "system" | "tool" | "error" | "thinking" | "rate-limit",
line_type as
| "user"
| "assistant"
| "system"
| "tool"
| "error"
| "thinking"
| "rate-limit"
| "compact-prompt",
content,
tool_name || undefined,
costData,
@@ -332,7 +340,15 @@ export async function initializeTauriListeners() {
} else {
// Fallback to active conversation if no conversation_id provided
claudeStore.addLine(
line_type as "user" | "assistant" | "system" | "tool" | "error" | "thinking" | "rate-limit",
line_type as
| "user"
| "assistant"
| "system"
| "tool"
| "error"
| "thinking"
| "rate-limit"
| "compact-prompt",
content,
tool_name || undefined,
costData,
+9 -1
View File
@@ -1,6 +1,14 @@
export interface TerminalLine {
id: string;
type: "user" | "assistant" | "system" | "tool" | "error" | "thinking" | "rate-limit";
type:
| "user"
| "assistant"
| "system"
| "tool"
| "error"
| "thinking"
| "rate-limit"
| "compact-prompt";
content: string;
timestamp: Date;
toolName?: string;