From 18bf81819e4b5c7c0dd649fea75b8a3c5e56872d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Wed, 29 Jul 2026 19:07:58 +0100 Subject: [PATCH] fix(web): show only model in agent tab title (#2043) --- .../components/task/session-tab-title.test.ts | 14 +++- apps/web/components/task/session-tab-title.ts | 6 +- .../tests/chat/mobile-model-selector.spec.ts | 4 + .../tests/chat/model-selector-error.spec.ts | 8 +- docs/plans/agent-tab-model-only/plan.md | 61 +++++++++++++++ .../task-01-render-model-only-title.md | 74 +++++++++++++++++++ .../ui/acp-model-configuration-summary.md | 4 +- 7 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 docs/plans/agent-tab-model-only/plan.md create mode 100644 docs/plans/agent-tab-model-only/task-01-render-model-only-title.md diff --git a/apps/web/components/task/session-tab-title.test.ts b/apps/web/components/task/session-tab-title.test.ts index bfca1d8b4d..912922715d 100644 --- a/apps/web/components/task/session-tab-title.test.ts +++ b/apps/web/components/task/session-tab-title.test.ts @@ -96,13 +96,23 @@ describe("resolveSessionTabTitle", () => { }); describe("resolveSessionTabTitle fallbacks", () => { - it("includes non-model config selections in the title", () => { + it("excludes non-model config selections from the title", () => { expect( resolveSessionTabTitle({ ...baseArgs, agentLabel: null, activeModelId: SPARK_MODEL_ID, configOptions: [ + { + type: "select", + id: "mode", + name: "Mode", + currentValue: "default", + options: [ + { value: "default", name: "Default" }, + { value: "plan", name: "Plan" }, + ], + }, { type: "select", id: "model", @@ -125,7 +135,7 @@ describe("resolveSessionTabTitle fallbacks", () => { }, ], }), - ).toBe(`${SPARK_MODEL_NAME} / High`); + ).toBe(SPARK_MODEL_NAME); }); it("falls back to the agent label when live model state is unavailable", () => { diff --git a/apps/web/components/task/session-tab-title.ts b/apps/web/components/task/session-tab-title.ts index 20719b9b0d..9f38d68fe7 100644 --- a/apps/web/components/task/session-tab-title.ts +++ b/apps/web/components/task/session-tab-title.ts @@ -33,11 +33,7 @@ function resolveModelTitle( // active/current model switches are reflected immediately in the tab title. modelLabel = optionName(modelConfig, modelId); } - const extras = args.configOptions - .filter((option) => !isModelConfigOption(option)) - .map((option) => optionName(option, option.currentValue)) - .filter(Boolean); - return [modelLabel, ...extras].join(" / "); + return modelLabel; } export function resolveSessionTabTitle(args: ResolveSessionTabTitleArgs): string | null { diff --git a/apps/web/e2e/tests/chat/mobile-model-selector.spec.ts b/apps/web/e2e/tests/chat/mobile-model-selector.spec.ts index b8518a918f..a61ba9f971 100644 --- a/apps/web/e2e/tests/chat/mobile-model-selector.spec.ts +++ b/apps/web/e2e/tests/chat/mobile-model-selector.spec.ts @@ -79,6 +79,7 @@ test.describe("Mobile chat model selector", () => { testPage, apiClient, seedData, + prCapture, }, testInfo) => { const { task } = await seedAndOpenTask(testPage, apiClient, seedData); expect(task.session_id).toBeTruthy(); @@ -164,6 +165,9 @@ test.describe("Mobile chat model selector", () => { body: await testPage.screenshot(), contentType: "image/png", }); + await prCapture.screenshot("mobile-model-selector", { + caption: "Mobile model selector remains reachable by touch", + }); const viewport = testPage.viewportSize(); expect(viewport).not.toBeNull(); diff --git a/apps/web/e2e/tests/chat/model-selector-error.spec.ts b/apps/web/e2e/tests/chat/model-selector-error.spec.ts index 3a77fe387f..cc58963c7e 100644 --- a/apps/web/e2e/tests/chat/model-selector-error.spec.ts +++ b/apps/web/e2e/tests/chat/model-selector-error.spec.ts @@ -172,6 +172,7 @@ test.describe("Chat model selector — persistence", () => { testPage, apiClient, seedData, + prCapture, }) => { const task = await apiClient.createTaskWithAgent( seedData.workspaceId, @@ -197,7 +198,10 @@ test.describe("Chat model selector — persistence", () => { await expect(trigger).toContainText("Mock Smart", { timeout: 5_000 }); const agentTab = session.sessionTabBySessionId(task.session_id); - await expect(agentTab).toContainText("Mock Smart", { timeout: 5_000 }); + await expect(agentTab).toHaveText("Mock Smart", { timeout: 5_000 }); + await prCapture.screenshot("desktop-agent-tab-model-only", { + caption: "Desktop agent tab shows only the selected model", + }); await expect .poll(async () => { const { sessions } = await apiClient.listTaskSessions(task.id); @@ -219,7 +223,7 @@ test.describe("Chat model selector — persistence", () => { await session.waitForLoad(); await session.waitForChatIdle({ timeout: 30_000 }); await expect(trigger).toContainText("Mock Smart", { timeout: 15_000 }); - await expect(agentTab).toContainText("Mock Smart", { timeout: 15_000 }); + await expect(agentTab).toHaveText("Mock Smart", { timeout: 15_000 }); }); test("completed turn metadata keeps changed options after a page reload", async ({ diff --git a/docs/plans/agent-tab-model-only/plan.md b/docs/plans/agent-tab-model-only/plan.md new file mode 100644 index 0000000000..0480776c8c --- /dev/null +++ b/docs/plans/agent-tab-model-only/plan.md @@ -0,0 +1,61 @@ +--- +spec: docs/specs/ui/acp-model-configuration-summary.md +created: 2026-07-29 +status: complete +--- + +# Implementation Plan: Model-only Agent Tab Title + +## Overview + +PR [#2021](https://github.com/kdlbs/kandev/pull/2021) made unnamed agent tabs follow the +authoritative live model across model switches and task-detail reloads. Its title resolver also +appends every non-model ACP config value, producing labels such as +`GPT-5.6-Terra / Default / High / Off`. Narrow the derived tab title to the model display name while +preserving the PR's live-state precedence, reload behavior, and custom-name override. + +## Frontend + +### Agent tab title resolver + +- Update `apps/web/components/task/session-tab-title.ts` so `resolveModelTitle` resolves only the + provider-supplied display name for the chosen model. +- Continue using the live model config option to identify and display the authoritative current + model before the active model, agent-profile fallback, and start-time snapshot fallbacks. +- Keep user-supplied session names as the highest-precedence title. +- Do not change `ModelConfigSelector`, the task-chat compact changed-value summary, tooltips, or + agent-profile selector labels. + +### Mobile design contract + +`SessionTab` is mounted only by `DockviewDesktopLayout`; `TaskLayout` routes phone and +coarse-pointer tablet viewports to `SessionMobileLayout` and `SessionTabletLayout`. This copy-only +change therefore has no mobile tab surface, navigation, touch, scroll, or geometry impact. The +nearest mobile exemplar is the existing task mobile model selector, whose option access and compact +summary remain unchanged. No new mobile Playwright case is required; the focused desktop rendered +check proves the only affected surface. + +## Tests + +- **What:** A derived tab title contains the model display name and excludes non-model ACP config + values while preserving custom-name and fallback precedence. + **File:** `apps/web/components/task/session-tab-title.test.ts`. + **How:** Vitest table/fixture coverage around `resolveSessionTabTitle`, including live model, + mode, and effort options. + +## E2E Tests + +- **Scenario:** GIVEN an unnamed agent tab with provider mode and effort options, WHEN the user + switches to `Mock Smart`, THEN the tab text is exactly `Mock Smart`; WHEN the page reloads, THEN + the tab still contains only `Mock Smart`. + **File:** `apps/web/e2e/tests/chat/model-selector-error.spec.ts`. + **What to verify:** The stable session-ID tab locator has the exact model-only label before and + after reload, while the session model selector continues to show the selected model. + +## Implementation + +- [x] [Task 01: Render model-only agent tab titles](task-01-render-model-only-title.md) + +## Open Questions + +None. diff --git a/docs/plans/agent-tab-model-only/task-01-render-model-only-title.md b/docs/plans/agent-tab-model-only/task-01-render-model-only-title.md new file mode 100644 index 0000000000..79aa1889e9 --- /dev/null +++ b/docs/plans/agent-tab-model-only/task-01-render-model-only-title.md @@ -0,0 +1,74 @@ +--- +id: "01-render-model-only-title" +title: "Render model-only agent tab titles" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +spec: "../../specs/ui/acp-model-configuration-summary.md" +--- + +# Task 01: Render Model-only Agent Tab Titles + +## Acceptance + +- An unnamed agent tab resolves to the authoritative current model display name without appending + session mode or non-model ACP config values. +- A live model switch and task-detail reload retain the model-only title. +- A user-supplied session name still overrides every derived title, and task-chat/profile model + selector summaries remain unchanged. + +## Verification + +```bash +cd apps && rtk pnpm --filter @kandev/web test -- --run components/task/session-tab-title.test.ts +cd apps/web && rtk pnpm e2e:run tests/chat/model-selector-error.spec.ts -- --grep "agent tab keeps the selected model title after reload" +``` + +## Files Likely Touched + +- `apps/web/components/task/session-tab-title.ts` +- `apps/web/components/task/session-tab-title.test.ts` +- `apps/web/e2e/tests/chat/model-selector-error.spec.ts` +- `apps/web/e2e/tests/chat/mobile-model-selector.spec.ts` (PR evidence capture only) +- `docs/specs/ui/acp-model-configuration-summary.md` +- `docs/plans/agent-tab-model-only/plan.md` +- `docs/plans/agent-tab-model-only/task-01-render-model-only-title.md` + +## Dependencies + +None. + +## Parallelism + +`sequential`. The resolver, its unit contract, and the existing persistence E2E form one focused +behavioral change. + +## Inputs + +- Spec **What** and unnamed-agent-tab **Scenario**. +- PR #2021 and its `resolveSessionTabTitle` precedence and reload regression. +- Existing `SessionPage.sessionTabBySessionId` stable E2E locator. +- `TaskLayout` mobile/tablet branching, which confirms this Dockview tab has no mobile surface. + +## TDD Sequence + +1. Change the non-model-config unit case to require the exact model-only label and run the focused + Vitest command to confirm RED. +2. Remove non-model config composition from the derived model title without changing title + precedence or selector code, then rerun Vitest to GREEN. +3. Tighten the existing model-switch persistence E2E to require the exact tab label before and + after reload, then run the managed focused E2E against freshly built assets. +4. Mark this task `done`, mark the plan checkbox complete, and return the spec to `shipped`. + +## Risks + +- Model display names can come from either the model list or the live model config option; removing + the extra values must not bypass the provider-supplied model label. +- Dockview restoration can reapply stale panel titles; the existing session-tab component sync and + reload E2E must continue proving the rendered tab wins. + +## Output Contract + +Report the expected RED assertion, resolver change, exact unit and E2E command results, files +changed, mobile no-surface confirmation, remaining risks, and task/plan/spec status updates. diff --git a/docs/specs/ui/acp-model-configuration-summary.md b/docs/specs/ui/acp-model-configuration-summary.md index 469c5c415d..32487055d7 100644 --- a/docs/specs/ui/acp-model-configuration-summary.md +++ b/docs/specs/ui/acp-model-configuration-summary.md @@ -20,7 +20,7 @@ ACP agents can advertise an arbitrary ordered set of model-adjacent session conf - Hovering or focusing the closed task selector shows a compact tooltip containing every currently rendered selector option as provider-supplied `Name: Value` rows, including baseline-matching values. The tooltip contains no descriptions or inferred provider knowledge. Opening the selector shows compact option names and selected values; entering an option submenu shows that option's provider description and the provider descriptions of its selectable values when supplied. - Kandev preserves optional ACP descriptions for both top-level config options and selectable values throughout the adapter, backend event, WebSocket, store, and selector pipeline. Missing descriptions produce no invented or hard-coded explanatory text. - Task-detail boot data includes the last persisted model list, live config options, and provider-default baseline so the compact label is complete on the first render instead of repainting after WebSocket reconnection. -- An unnamed agent tab derives its title from the task session's authoritative current model and live config values. The title updates when the model changes and remains correct after a task-detail refresh; a user-supplied session name continues to override the derived title. +- An unnamed agent tab derives its title from the task session's authoritative current model. Live config values may identify the current model, but non-model config values and the session mode are never appended to the tab title. The title updates when the model changes and remains correct after a task-detail refresh; a user-supplied session name continues to override the derived title. - Each turn stores an immutable configuration snapshot when the turn is created. The snapshot contains the effective model, mode, ordered selected config values with their provider-supplied display names, and the task-session provider-default baseline used for comparison. Agent-message metadata renders from this turn snapshot instead of the session's latest mutable runtime configuration. - Agent-message metadata always shows the attributed model and shows only non-model config values that differ from the turn's captured baseline. It keeps option names in the compact row because message attribution is read outside the selector context. Baseline-matching values and the session mode are omitted from the compact row; the mode remains available in turn metadata. - Prompt-usage metadata may refine the turn's attributed model when the provider reports the actual model used, but it does not replace the turn's captured config values or baseline. @@ -40,7 +40,7 @@ ACP agents can advertise an arbitrary ordered set of model-adjacent session conf - **GIVEN** an ACP option or value supplies a description, **WHEN** the user enters that option's submenu, **THEN** Kandev shows the provider text. The closed trigger and top-level option list remain compact, and missing descriptions leave the description region absent. - **GIVEN** a task selector has current model and config values, **WHEN** the user hovers or focuses its closed trigger, **THEN** a compact tooltip lists every selector option as `Name: Value` without provider descriptions, including values omitted from the changed-only trigger label. - **GIVEN** a task session has persisted dynamic model configuration, **WHEN** the task-detail page is refreshed, **THEN** the first rendered selector label includes all changed values without waiting for a WebSocket event. -- **GIVEN** an unnamed agent tab whose selected model changes during the session, **WHEN** the model update converges or the task-detail page is refreshed, **THEN** the existing tab title shows the current selected model instead of the agent profile's original model label. +- **GIVEN** an unnamed agent tab whose selected model changes during the session, **WHEN** the model update converges or the task-detail page is refreshed, **THEN** the existing tab title shows only the current selected model instead of the agent profile's original model label or any session mode and non-model config values. - **GIVEN** turn A runs with reasoning `High`, **WHEN** reasoning changes to `Low` before turn B, **THEN** turn A continues to show `Reasoning effort: High` and turn B shows `Reasoning effort: Low`. - **GIVEN** a turn uses only provider-default options, **WHEN** its agent-message metadata renders, **THEN** the compact row shows only the attributed model. - **GIVEN** a legacy turn has no configuration snapshot, **WHEN** the session's runtime options later change, **THEN** the legacy turn does not display those current options.