From 09ddc758b043ce02b4ab17a5823996583f3ed147 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 5 Aug 2026 21:01:00 -0700 Subject: [PATCH] fix(native-chat): identify option-less models on narrow panes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live capture at 60 columns: a Haiku session prints a bare `Haiku 4.5` row with its billing wrapped to the next line. No middot, no effort suffix — nothing marks it as the model, so it reported nothing. Claude always closes the frame with the working directory and prints at most the descriptor plus a wrapped billing line above it, so fall back to walking up from there when no row carries descriptor metadata. The height bound is what keeps the walk from climbing into the welcome art. --- .../claude-terminal-session-options.test.ts | 42 +++++++++++++++++++ .../claude-terminal-session-options.ts | 35 +++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/native-chat/claude-terminal-session-options.test.ts b/src/renderer/src/components/native-chat/claude-terminal-session-options.test.ts index 175e0c01ec1..d1879011f16 100644 --- a/src/renderer/src/components/native-chat/claude-terminal-session-options.test.ts +++ b/src/renderer/src/components/native-chat/claude-terminal-session-options.test.ts @@ -73,6 +73,39 @@ const FRESH_SESSION_60_COLS = [ '\u001b[2C\u001b[38;2;255;107;128m⏵⏵ bypass permissions on\u001b[38;2;153;153;153m (shift+tab to cycle)\u001b[4A\u001b[45D\u001b[0m\u001b[?2004h\u001b[?1004h\u001b[?1003h' ].join('\n') +const OPTION_LESS_MODEL_60_COLS = [ + '\u001b[?1049h\u001b[H\r', + '\u001b[38;2;215;119;87m╭─ Claude Code ────────────────────────────────────────────╮\r', + '│\u001b[58C│\r', + '│\u001b[22C\u001b[39;1mWelcome back!\u001b[23C\u001b[38;2;215;119;87;22m│\r', + '│\u001b[58C│\r', + '│\u001b[25C ▐\u001b[48;2;0;0;0m▛███▜\u001b[49m▌\u001b[25C│\r', + '│\u001b[25C▝▜\u001b[48;2;0;0;0m█████\u001b[49m▛▘\u001b[24C│\r', + '│\u001b[25C ▘▘ ▝▝ \u001b[24C│\r', + '│\u001b[58C│\r', + '│\u001b[24C\u001b[38;2;153;153;153mHaiku 4.5\u001b[25C\u001b[38;2;215;119;87m│\r', + '│\u001b[20C\u001b[38;2;153;153;153mAPI Usage Billing\u001b[21C\u001b[38;2;215;119;87m│\r', + '│\u001b[23C\u001b[38;2;153;153;153m/private/tmp\u001b[23C\u001b[38;2;215;119;87m│\r', + '│\u001b[58C│\r', + '╰──────────────────────────────────────────────────────────╯\r', + '\r', + '\r', + '\r', + '\r', + '\r', + '\r', + '\r', + '\r', + '\r', + '\u001b[2C\u001b[38;2;255;193;7m⚠ claude.ai connectors are disabled because ANTHROPIC_AP…\r', + '\u001b[38;2;136;136;136m────────────────────────────────────────────────────────────\r', + '\u001b[0m❯ \u001b[2mTry "how does work?"\r', + '\u001b[38;2;136;136;136;22m────────────────────────────────────────────────────────────\r', + '\u001b[2C\u001b[38;2;255;193;7m⚠ Transcript saving is off — inherited CLAUDE_CODE_CHILD_…\r', + '\u001b[2C\u001b[96m🤖\u001b[38;2;153;153;153m \u001b[96mHaiku 4.5\u001b[37m | \u001b[93m📁\u001b[38;2;153;153;153m \u001b[92mtmp\u001b[37m | \u001b[2C\u001b[95m⚡️ \u001b[38;2;153;153;153m \u001b[95m11.9% · 23.8k tokens\r', + '\u001b[2C\u001b[38;2;255;107;128m⏵⏵ bypass permissions on\u001b[38;2;153;153;153m (shift+tab to cycle)\u001b[4A\u001b[45D\u001b[0m\u001b[?2004h\u001b[?1004h\u001b[?1003h' +].join('\n') + const EFFORT = { id: 'effort', label: 'Effort', @@ -104,6 +137,15 @@ describe('Claude terminal session option detection', () => { }) }) + it('reads a real narrow frame whose option-less model carries no metadata', () => { + // Why: `Haiku 4.5` has no effort control and its billing wrapped to the next + // row, so nothing marks the row as a model — only its position above the + // working directory does. + expect(readClaudeSessionOptionsFromTerminalScreen(OPTION_LESS_MODEL_60_COLS)).toEqual({ + model: 'haiku' + }) + }) + it('reports nothing rather than release-notes prose when the model row is bare', () => { // Why: a custom model with its billing wrapped away carries no descriptor // metadata, so nothing in the frame identifies it. The panel shares rows diff --git a/src/renderer/src/components/native-chat/claude-terminal-session-options.ts b/src/renderer/src/components/native-chat/claude-terminal-session-options.ts index 0ee39ad9319..6939c57796d 100644 --- a/src/renderer/src/components/native-chat/claude-terminal-session-options.ts +++ b/src/renderer/src/components/native-chat/claude-terminal-session-options.ts @@ -85,7 +85,40 @@ function claudeModelDescriptorCell(lines: string[], headerIndex: number): string } // xterm serialization can join the descriptor onto the version row itself. const joined = frameCell((lines[headerIndex] ?? '').replace(/^.*?\bClaude Code\s*v?[\d.]*/i, '')) - return isModelDescriptorCell(joined) ? joined : null + if (isModelDescriptorCell(joined)) { + return joined + } + return frameBottom > 0 ? modelRowAboveWorkingDirectory(lines, headerIndex, frameBottom) : null +} + +/** + * A model with no effort control whose billing wrapped away — `Haiku 4.5` on a + * narrow pane — leaves a bare name that no metadata identifies. Claude always + * closes the frame with the working directory, and prints at most the + * descriptor plus a wrapped billing line above it, so walk up from there. + */ +function modelRowAboveWorkingDirectory( + lines: string[], + headerIndex: number, + frameBottom: number +): string | null { + let workingDirectory = -1 + for (let index = frameBottom - 1; index > headerIndex; index -= 1) { + const cell = frameCell(lines[index] ?? '') + if (cell.startsWith('/') || cell.startsWith('~')) { + workingDirectory = index + break + } + } + if (workingDirectory < 0) { + return null + } + let top = workingDirectory + while (top - 1 > headerIndex && frameCell(lines[top - 1] ?? '')) { + top -= 1 + } + // Anything taller than descriptor + billing is the welcome art, not a model. + return top < workingDirectory && workingDirectory - top <= 2 ? frameCell(lines[top] ?? '') : null } /** The displayed model name, without the effort suffix or billing tail. */