collin/browser-terminal-extension · 878ba185
Show the Claude model in the omnibar's pane rows
Collin Richards · 2026-08-18 06:44 UTC · 878ba18584ec9e968a19bb1f746cec27371e9d66 · parent 81bf09bf · browse files
modifieddaemon/src/agents.rs+25 −0
| ⋯ 62 unchanged lines | |||
| 63 | 63 | /// Session title, when Claude has one. | |
| 64 | 64 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 65 | 65 | pub name: Option<String>, | |
| 66 | + | /// The model id, e.g. `claude-opus-4-1-20250805`. Only `SessionStart` | |
| 67 | + | /// carries it, and even there it is not guaranteed, so this is carried | |
| 68 | + | /// forward across every later event the way `mode` and `name` are; it goes | |
| 69 | + | /// stale if the session switches models mid-conversation, but a stale | |
| 70 | + | /// answer beats none. | |
| 71 | + | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 72 | + | pub model: Option<String>, | |
| 66 | 73 | /// One line of the last thing the user asked, from `UserPromptSubmit`. It | |
| 67 | 74 | /// is what [`crate::pane_title`] writes to the pane title, and what the | |
| 68 | 75 | /// sidebar falls back to when a session has no name yet — which is most of | |
| ⋯ 119 unchanged lines | |||
| 188 | 195 | _ => None, | |
| 189 | 196 | }, | |
| 190 | 197 | name: str_field("session_name").or_else(|| carry(|r| r.name.clone())), | |
| 198 | + | model: str_field("model").or_else(|| carry(|r| r.model.clone())), | |
| 191 | 199 | // Only `UserPromptSubmit` carries one; every other event keeps the one | |
| 192 | 200 | // already on disk, so the title stays put for the whole turn instead of | |
| 193 | 201 | // blinking out on the first tool call. | |
| ⋯ 200 unchanged lines | |||
| 394 | 402 | }); | |
| 395 | 403 | } | |
| 396 | 404 | ||
| 405 | + | /// `model` only ever arrives on `SessionStart`; every later event has to | |
| 406 | + | /// keep reporting it anyway. | |
| 407 | + | #[test] | |
| 408 | + | fn model_survives_events_that_omit_it() { | |
| 409 | + | with_temp_dir(|| { | |
| 410 | + | event(serde_json::json!({ | |
| 411 | + | "session_id": "a", "hook_event_name": "SessionStart", | |
| 412 | + | "cwd": "/tmp/p", "model": "claude-opus-4-1-20250805", | |
| 413 | + | })); | |
| 414 | + | let r = event(serde_json::json!({ | |
| 415 | + | "session_id": "a", "hook_event_name": "PreToolUse", "tool_name": "Bash", | |
| 416 | + | })) | |
| 417 | + | .unwrap(); | |
| 418 | + | assert_eq!(r.model.as_deref(), Some("claude-opus-4-1-20250805")); | |
| 419 | + | }); | |
| 420 | + | } | |
| 421 | + | ||
| 397 | 422 | #[test] | |
| 398 | 423 | fn session_end_removes_the_record() { | |
| 399 | 424 | with_temp_dir(|| { | |
| ⋯ 84 unchanged lines | |||
modifieddaemon/src/status.rs+5 −0
| ⋯ 62 unchanged lines | |||
| 63 | 63 | /// asked it. The same string [`crate::pane_title`] gives to tmux, so a | |
| 64 | 64 | /// session reads the same in the sidebar and in the status line. | |
| 65 | 65 | pub title: Option<String>, | |
| 66 | + | /// The model id, when `SessionStart` reported one. See | |
| 67 | + | /// [`crate::agents::Record::model`] for why it can be stale or absent. | |
| 68 | + | pub model: Option<String>, | |
| 66 | 69 | } | |
| 67 | 70 | ||
| 68 | 71 | /// The tmux user option the sidebar keeps a session's colour in. | |
| ⋯ 367 unchanged lines | |||
| 436 | 439 | tool: record.and_then(|r| r.tool.clone()), | |
| 437 | 440 | message: record.and_then(|r| r.message.clone()), | |
| 438 | 441 | title: record.and_then(|r| r.name.clone().or_else(|| r.prompt.clone())), | |
| 442 | + | model: record.and_then(|r| r.model.clone()), | |
| 439 | 443 | }) | |
| 440 | 444 | }) | |
| 441 | 445 | .collect(); | |
| ⋯ 21 unchanged lines | |||
| 463 | 467 | tool: None, | |
| 464 | 468 | message: None, | |
| 465 | 469 | name: None, | |
| 470 | + | model: None, | |
| 466 | 471 | prompt: None, | |
| 467 | 472 | frame: 0, | |
| 468 | 473 | updated, | |
| ⋯ 49 unchanged lines | |||
modifieddaemon/src/window_status.rs+1 −0
| ⋯ 173 unchanged lines | |||
| 174 | 174 | tool: None, | |
| 175 | 175 | message: None, | |
| 176 | 176 | name: None, | |
| 177 | + | model: None, | |
| 177 | 178 | prompt: None, | |
| 178 | 179 | frame, | |
| 179 | 180 | updated: 0, | |
| ⋯ 24 unchanged lines | |||
modifiedextension/sidebar.js+17 −2
| ⋯ 3473 unchanged lines | |||
| 3474 | 3474 | // knows an id for, and they are the ones worth addressing individually — the | |
| 3475 | 3475 | // rest of a window's panes are reached by going to the window. | |
| 3476 | 3476 | for (const a of lastAgents) { | |
| 3477 | + | const family = modelFamily(a.model); | |
| 3477 | 3478 | // What you would search for is what it is doing, not "pane %12". | |
| 3478 | - | const text = [a.title, a.message, a.tool, a.name].filter(Boolean).join(" "); | |
| 3479 | + | const text = [a.title, a.message, a.tool, a.name, family].filter(Boolean).join(" "); | |
| 3479 | 3480 | const score = omniScore(text, q); | |
| 3480 | 3481 | if (score < 0 || !q) continue; | |
| 3481 | 3482 | out.push({ | |
| 3482 | 3483 | kind: "pane", | |
| 3483 | 3484 | label: a.title || agentLabel(a), | |
| 3484 | - | meta: `${a.session} · ${a.window} · claude ${a.state}`, | |
| 3485 | + | meta: `${a.session} · ${a.window} · claude ${a.state}${family ? ` · ${family}` : ""}`, | |
| 3485 | 3486 | state: a.state, | |
| 3486 | 3487 | score: score + 50, | |
| 3487 | 3488 | run: () => tmuxCommand({ cmd: "focus", pane: a.pane }), | |
| ⋯ 1453 unchanged lines | |||
| 4941 | 4942 | return MODE_SHORT[mode] ?? mode; | |
| 4942 | 4943 | } | |
| 4943 | 4944 | ||
| 4945 | + | // A model id is `claude-opus-4-1-20250805` or similar — a version and a date | |
| 4946 | + | // the omnibar has no room for and the user did not ask about. The family name | |
| 4947 | + | // is the one part of it that answers "which model", so that is all this pulls | |
| 4948 | + | // out. | |
| 4949 | + | /** @param {string | null | undefined} model */ | |
| 4950 | + | function modelFamily(model) { | |
| 4951 | + | if (!model) return null; | |
| 4952 | + | const m = model.toLowerCase(); | |
| 4953 | + | if (m.includes("opus")) return "opus"; | |
| 4954 | + | if (m.includes("sonnet")) return "sonnet"; | |
| 4955 | + | if (m.includes("haiku")) return "haiku"; | |
| 4956 | + | return null; | |
| 4957 | + | } | |
| 4958 | + | ||
| 4944 | 4959 | /** @param {TbOkFrame} msg */ | |
| 4945 | 4960 | function renderSessions(msg) { | |
| 4946 | 4961 | if (!msg.tmux) return; | |
| ⋯ 704 unchanged lines | |||
modifiedextension/types/globals.d.ts+6 −0
| ⋯ 327 unchanged lines | |||
| 328 | 328 | tool?: string | null; | |
| 329 | 329 | message?: string | null; | |
| 330 | 330 | title?: string | null; | |
| 331 | + | /** | |
| 332 | + | * The model id, e.g. `claude-opus-4-1-20250805`. Only ever reported at | |
| 333 | + | * `SessionStart`, so it can be stale after a mid-session model switch, and | |
| 334 | + | * absent entirely for a session that started before this field existed. | |
| 335 | + | */ | |
| 336 | + | model?: string | null; | |
| 331 | 337 | } | |
| 332 | 338 | ||
| 333 | 339 | /** | |
| ⋯ 170 unchanged lines | |||