fix(web): Keep chip shows its own icon when the composer is narrow#30045

open2
main
fix/issue-29974
opened4d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 4d ago

Problem

When the composer is narrower than 480px (for example with a file open in a wide right panel), the Compact chip hides its Compact/Keep label and leaves only the icon and token count. The icon was Minimize2 in both states, so clicking the chip to switch to Keep left it looking the same. The only remaining cue was the faint pressed background, and the hover background covers it while the pointer is still on the chip.

Reproduced on main at e2bb9e63e1 in the web dev server (1512×945 window, a stale Claude thread at 549k tokens). With the right panel closed, the composer was 736px and the chip read "Compact 549k". With Files open and a file shown, and the panel dragged to 896px, the composer was 264px and the chip showed only "549k". Clicking the chip changed aria-pressed from true to false and the send button label from "Compact and send" to "Submit message", but the chip's icon and text stayed the same. Closing the panel showed "Keep 549k".

Change

The chip's icon is now a MorphIcon that shows Minimize2 for Compact and Maximize2 for Keep. That is the same pair, and the same component, the right panel's maximize toggle uses. The label, colours, layout and toggle behaviour are unchanged. The icon is still decorative (aria-hidden), and the button keeps its aria-label and aria-pressed.

Scope and approval

Fixes pingdotgg/t3code#29974

Triaged via the automated issue flow; GitCafe issue #29974.

Verification

  • vp run check:changed passes on the final head: format, lint, knip, typecheck of 2 packages, react compiler, and the tests beside the change.
  • No new test. This is a purely visual bug, so the proof is the before/after captures below.

Evidence. Before is e2bb9e6 and After is a87b809, the fix commit. The later merge of main touches nothing the chip renders. Each side ran as the real web app from its own checkout, on a fresh database seeded by the same driver: one real Claude Haiku turn, then the server stopped and that turn's usage raised to 549k tokens with an expired cache. Every capture used a fresh browser context.

Rendering environment for every capture: Linux, Playwright 1.60, headless Chromium 148, no device emulation, DPR 1, 100% zoom and font scaling, clock frozen at 2026-11-03T07:00Z. Composer widths: 736px with the label shown, 386px narrow, 366px at phone width.

Sheet (Before | After | Diff0 | DiffT, SHA under each column)RowsViewport, theme
light-narrow-F3-F4.pngF3 narrow Compact, F4 narrow Keep1512×945, light
dark-narrow-F3-F4.pngF3 narrow Compact, F4 narrow Keep1512×945, dark
light-labelled-F1-F2.pngF1 Compact with label, F2 Keep with label1512×945, light
dark-labelled-F1-F2.pngF1 Compact with label, F2 Keep with label1512×945, dark
light-phone-F7-F8.pngF7 Compact, F8 Keep, web composer expanded at phone width390×844, light

Diff0 marks any nonzero channel delta, amplified ×255. DiffT marks a channel delta above 16/255. Both compare native PNGs at full viewport size. Changed pixels per pair (Diff0 / DiffT), with 0 outside the 14×14 icon area in every pair:

ThemeF1F2F3F4
light50 / 2586 / 7650 / 2586 / 76
dark52 / 1188 / 7552 / 1188 / 75

Phone width, light: F7 50 / 25, F8 86 / 76.

Recordings (Before on the left, After on the right, step captions burned in):

  • P1-light-labelled-narrow.gif, 1512×945 light:
    • 0.2s: Compact with label. 1.3s: hover tooltip. 2.4s: click to Keep, pointer stays on the chip. 3.6s: pointer away. 4.6s: keyboard focus. 5.4s: Space toggles back to Compact.
    • 6.3s: Files and math.ts opened and the panel widened below 480px (a captioned cut removes the editor loading frames). 6.5s: narrow Compact. 8.7s: click to Keep with the pointer on the chip; Before is unchanged, After shows outward arrows. 11.8s: toggle back. 12.8s: rapid double toggle.
    • 13.9s to 17.4s: Keep survives closing and reopening the panel.
  • P2-dark-labelled-narrow.gif, 1512×945 dark: the same forward and reverse sequence, wide and narrow, with the same captioned cut.
  • P3-light-reduced-motion-narrow.gif, 1512×945 light, prefers-reduced-motion: reduce, narrow: Compact to Keep and back. After switches straight to the end icon, with no animation and no blank frame.
  • P4-light-phone-web-expanded.gif, 390×844 light, web composer expanded at phone width: Compact to Keep and back. The editor keeps focus and the composer stays expanded.

What else changed, every visible difference:

  • Keep shows outward arrows in the wide layout too, where the label is visible, not only in the narrow one.
  • On After, toggling morphs between the two icons. Reduced motion switches straight to the end icon.
  • Compact still points inward, but the new renderer draws its strokes slightly differently: 50 changed pixels in light and 52 in dark, all inside the icon.
  • Nothing outside the 14×14 icon area changes in any still pair.
Seed driver (synthetic data only)
sh
1#!/usr/bin/env bash
2# Synthetic real-app capture seed. Server running for run; stopped for fixture.
3set -euo pipefail
4checkout="$1"
5project="$2"
6phase="${3:-run}"
7cd "$checkout"
8if [ "$phase" = fixture ]; then
9 python3 /tmp/issue-lead-29974-r2b/capture/scripts/normalize-fixture.py "$checkout"
10 exit
11fi
12mkdir -p "$project"
13git -C "$project" init -q
14printf '# Demo\n\nA small sample project.\n' > "$project/README.md"
15printf 'export const add = (a: number, b: number) => a + b;\n' > "$project/math.ts"
16git -C "$project" add README.md math.ts
17git -C "$project" -c user.email=demo@example.com -c user.name=demo commit -qm init
18node apps/server/src/bin.ts project add --base-dir .t3 "$project"
19node apps/server/src/bin.ts thread start --base-dir .t3 --provider claudeAgent --model claude-haiku-5-5 --title 'Refactor math helpers' --wait --json "$project" 'Reply with one short sentence describing math.ts. Do not edit files.'
python
1#!/usr/bin/env python3
2"""Stopped-server, synthetic capture fixtures only. Run after lead's seed driver."""
3import sqlite3,json,sys,pathlib
4checkout=pathlib.Path(sys.argv[1]); db=checkout/'.t3/userdata/statev2.sqlite'
5c=sqlite3.connect(db)
6project=c.execute("SELECT project_id FROM projection_projects WHERE workspace_root LIKE '%/capture/demo-%'").fetchone()[0]
7# Hide only the automatic, empty checkout project; the demo is the sole capture project.
8c.execute("UPDATE projection_projects SET deleted_at='2026-11-03T05:00:00.000Z' WHERE project_id!=?",(project,))
9c.execute("UPDATE projection_projects SET title='Compact proof',created_at='2026-11-03T05:00:00.000Z',updated_at='2026-11-03T05:00:08.000Z' WHERE project_id=?",(project,))
10def normalized(v):
11 if isinstance(v,list): return [normalized(x) for x in v]
12 if not isinstance(v,dict): return v
13 out={}
14 for k,x in v.items():
15 if k in ['createdAt','startedAt','lastVisitedAt'] and isinstance(x,str): out[k]='2026-11-03T05:00:00.000Z'
16 elif k in ['updatedAt','completedAt','settledAt'] and isinstance(x,str): out[k]='2026-11-03T05:00:08.000Z'
17 else: out[k]=normalized(x)
18 return out
19for (t,) in c.execute("SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE 'orchestration_v2_projection_%'").fetchall():
20 cols=[r[1] for r in c.execute(f'PRAGMA table_info({t})')]
21 if 'payload_json' not in cols:continue
22 for rowid,raw in c.execute(f'SELECT rowid,payload_json FROM {t}').fetchall():
23 p=normalized(json.loads(raw))
24 if t=='orchestration_v2_projection_provider_turns' and p.get('tokenUsage'):
25 p['tokenUsage'].update(usedTokens=549000,inputTokens=549000,updatedAt='2026-11-03T05:00:00.000Z',promptCacheTtlMs=3600000,promptCacheExpiresAt='2026-11-03T06:00:00.000Z')
26 if t=='orchestration_v2_projection_threads' and p.get('projectId')!=project:p['deletedAt']='2026-11-03T05:00:00.000Z'
27 updates={'payload_json':json.dumps(p,separators=(',',':'))}
28 for col in ['created_at','started_at','last_visited_at']:
29 if col in cols:updates[col]='2026-11-03T05:00:00.000Z'
30 for col in ['updated_at','completed_at']:
31 if col in cols:updates[col]='2026-11-03T05:00:08.000Z'
32 c.execute(f"UPDATE {t} SET "+','.join(k+'=?' for k in updates)+' WHERE rowid=?',(*updates.values(),rowid))
33c.commit();c.close()
34print('Normalized synthetic data:',checkout.name)

Not verified separately: the Electron desktop app. It renders this same web component. The mobile app has no Compact chip. Optional matrix rows (other token counts, neighbouring controls, the chip being absent, the cache-expiry boundary) were not captured; the diff does not change those.

Model and harness: Claude Opus 5.5 (lead, implementation, review B) and GPT-6.1-sol (review A, proof audits, capture) via Claude Code and Codex in T3 Code.

🤖 Generated with Claude Code

Checks

24 successful · 9 skipped
All checks passed
611ef71

Added 2 commits

  • Merge remote-tracking branch 'origin/main' into fix/issue-29974611ef71
  • fix(web): Keep chip shows its own icon when the composer is narrowa87b809
Synced from GitHub

Approvability

Verdict: Approved at 611ef71

Macroscope's review found this PR approvable — The history chip now shows distinct Keep and Compact icons, including when narrow layouts hide its text. This is a presentation-only change; history handling and product defaults are unchanged.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

The compact-history toggle now uses MorphIcon to render Maximize2 when keeping full history and Minimize2 otherwise. The icons are imported from lucide, and the toggle no longer sets aria-hidden.

Changes: Compact-history toggle

Layer / File(s)Summary
State-dependent toggle icons
apps/web/src/components/chat/ComposerPrimaryActions.tsx
The toggle selects Maximize2 for keep-full-history and Minimize2 otherwise. It renders the selected icon through MorphIcon and removes the explicit aria-hidden prop.

Estimated code review effort: 1 (Trivial) | ~3 minutes

Change: Bug fix · Severity of issue fixed: Low

Possibly related PRs

  • pingdotgg/t3code#24865: Changes the labels and styling of the same compact-history toggle.
  • pingdotgg/t3code#27033: Hides the toggle label at narrow composer widths, where the icon remains visible.
  • pingdotgg/t3code#24837: Changes the styling and state-dependent presentation of the same toggle.

Suggested labels: bugfix

Suggested reviewers: flamboh

Merge Risk: External image Minimal · up to 611ef

The focused icon change is mergeable with no remaining material risk identified.

Pre-merge checks: Passed 4
Check nameStatusExplanation
Title checkPassedThe title clearly identifies the web fix: the Keep state now shows its own icon when the composer is narrow. It is concise and uses the repository's conventional commit format.
Description checkPassedThe description includes the required Problem, Change, Scope and approval, and Verification sections. It explains the reproduction, implementation, linked issue, validation results, visual evidence, a…
Linked Issues checkPassedCoding requirement from #29974: the Compact chip must show a visible state difference between Compact and Keep when the label is hidden (composer under 480px). The head implements this in ComposerPrim…
Out of Scope Changes checkPassedThe whole-PR diff against the merge base (cefb544) changes one file, apps/web/src/components/chat/ComposerPrimaryActions.tsx, with +3/-3 lines. The changes are: the lucide-react import drops Minimize2…
Full details: Description check

Explanation

The description includes the required Problem, Change, Scope and approval, and Verification sections. It explains the reproduction, implementation, linked issue, validation results, visual evidence, and known verification limits.

Full details: Linked Issues check

Explanation

Coding requirement from #29974: the Compact chip must show a visible state difference between Compact and Keep when the label is hidden (composer under 480px). The head implements this in ComposerPrimaryActions.tsx: the chip icon is now <MorphIcon icon={keepFullHistory ? Maximize2 : Minimize2} />. Compact shows Minimize2 (inward arrows). Keep shows Maximize2 (outward arrows), the icon pair suggested in the issue. The label span, aria-pressed, aria-label and onClick are unchanged in the diff. The issue's suggested behaviour is therefore implemented in code. The PR's before/after captures were not inspected; this verdict relies on the source diff. The issue's documentation item (docs/user/providers-claude.md, 'Full' versus 'Keep') is not a coding requirement of this PR and is not assessed here. Unresolved point: the diff removes the explicit aria-hidden="true" from the chip icon. The PR says the icon stays decorative. That depends on MorphIcon's default behaviour in the morphicons/react package, which could not be read from the repository. The issue does not state an accessibility requirement, and the chip button keeps its aria-label, so this does not change the verdict.

Full details: Out of Scope Changes check

Explanation

The whole-PR diff against the merge base (cefb544) changes one file, apps/web/src/components/chat/ComposerPrimaryActions.tsx, with +3/-3 lines. The changes are: the lucide-react import drops Minimize2Icon; the lucide import adds Maximize2 and Minimize2; the chip icon switches from Minimize2Icon to MorphIcon with the state-dependent icon. Each change serves the chip icon requirement of #29974. No unrelated code, styling, label, or behaviour changes appear in the diff. The removed aria-hidden prop is part of the same icon change and is noted as an unresolved point in the linked-issue assessment. The PR-described extra effects (Keep shows outward arrows in the wide layout; morph animation) follow from the same icon change and do not add unrelated scope.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
External image Recent review info
External image Run configuration
External image Commits

Reviewing files that changed from the base of the PR and between cefb544af3c98224874419198c0359b652ca3c94 and 611ef71770dfb25e447183c27ae91a757ce23358.

External image Files selected for processing: 1
  • apps/web/src/components/chat/ComposerPrimaryActions.tsx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.