Frame Claude Code's errors into the compact pane
The compact pane is prompt-only because the feed above shows the context. An error is the exception: the feed cannot show it. A tool_result only reaches us with the next request, which a turn that died never sends, and an `API Error` row is Claude Code's own text, never in the stream at all. So the frame walks further up to take the report in, and the scheduled ctrl-l wipe is cancelled while one is framed — Ink redraws only the live frame, so wiping would delete the error 400ms after it appeared. Detection is text shape, not colour: the tool error renders palette 211 and the API error 220, both theme values. ERROR_SCAN is the only bound, which is what makes it self-limiting — the error drifts out of the window as the next turn prints output and the pane shrinks back on its own.
This commit is contained in:
@@ -8,7 +8,7 @@ ExitPlanMode, TodoWrite/Task*) on demand, so the pane's frame detector in
|
||||
`src/term.rs` can be developed against what Ink actually draws.
|
||||
|
||||
Scenario is picked per turn from `CT_FAKE_SCENARIO`
|
||||
(ask | plan | todo | taskupdate | agent | websearch | ansi | text).
|
||||
(ask | plan | todo | taskupdate | agent | websearch | ansi | toolerror | apierror | text).
|
||||
|
||||
`websearch` also answers the *nested* request Claude Code makes to run WebSearch:
|
||||
that call declares Anthropic's server-side `web_search` tool, so it is replied to
|
||||
@@ -162,6 +162,12 @@ ANSI_INPUT = {
|
||||
}
|
||||
|
||||
|
||||
# A tool call that *fails* on the child's side, so Claude Code prints its own
|
||||
# red error row (`⎿ Error: …`) above the input box. `Read` is auto-approved, so
|
||||
# this needs no permission prompt — the failure comes from the missing file.
|
||||
TOOL_ERROR_INPUT = {"file_path": "/nonexistent/claude-cloak/does-not-exist.txt"}
|
||||
|
||||
|
||||
ASK_INPUT = {"questions": [{
|
||||
"question": "The compact pane currently crops the top of this prompt. Which framing "
|
||||
"should the pane use when an interactive question is on screen, given that "
|
||||
@@ -269,6 +275,21 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return
|
||||
|
||||
scenario = read_scenario()
|
||||
# `apierror`: a non-retryable upstream failure, so Claude Code prints
|
||||
# its own red `API Error: 400 …` row where the turn would have been.
|
||||
# Only the *turn* request fails — the side/title calls still answer, so
|
||||
# the pane behaves normally around the error.
|
||||
if scenario == "apierror" and tools and not hosted and not has_result:
|
||||
out = json.dumps({"type": "error", "error": {
|
||||
"type": "invalid_request_error",
|
||||
"message": "fake upstream rejected this turn on purpose"}}).encode()
|
||||
self.send_response(400)
|
||||
self.send_header("content-type", "application/json")
|
||||
self.send_header("content-length", str(len(out)))
|
||||
self.end_headers()
|
||||
self.wfile.write(out)
|
||||
return
|
||||
|
||||
# The nested hosted-tool request answers itself, whatever the scenario.
|
||||
if hosted:
|
||||
gen = stream_server_websearch()
|
||||
@@ -300,6 +321,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
"Let me search for that.")
|
||||
elif scenario == "ansi":
|
||||
gen = stream_tool("Bash", ANSI_INPUT, "Printing coloured output.")
|
||||
elif scenario == "basherror":
|
||||
gen = stream_tool("Bash", {"command": "echo boom >&2; exit 3", "description": "Fail"}, "Running that.")
|
||||
elif scenario == "toolerror":
|
||||
gen = stream_tool("Read", TOOL_ERROR_INPUT, "Reading that file.")
|
||||
elif scenario == "taskupdate":
|
||||
gen = stream_tool("TaskUpdate", {"taskId": "1", "status": "in_progress"},
|
||||
"Starting the first task.")
|
||||
|
||||
Reference in New Issue
Block a user