design phase 9 w8 sender - sinrimin/harness-nexus GitHub Wiki
Status: SHIPPED 2026-09-10 (see ยงPost-ship notes). Predecessor:
design-phase-9-portal-ui.mdยงW6 (the session page the composer lives in). User intent: ไผๅ Sender ๆก,ๅ่ฝๆ ทๅผๅ่ๆบ่ฐฑๆธ ่จ ้ฃๆ ผ็ composer(ๅก็ๅผไธค่กๅธๅฑ:ไธๆนๅค่ก่พๅ ฅๅบ,ไธๆนๅทฆไพง"+"้ไปถไธ ๆ้ๆจกๅผ chip,ๅณไพงๆจกๅ้ๆฉใๆจ็ๅๅบฆไธๆไธๅๅฝขๅ้้ฎ),้ ่ฒไธ้ฃๆ ผ ้ตๅพชๆไปฌ่ชๅทฑ็ Signal ไฝ็ณป(ไธ็ งๆๅ่ๅพ็่่ฒ็ณป)ใ Reference code index:~/acp-ref/README.md(the claude-acp-bridge snapshot
- adoption notes; the composer screenshot itself is not committed โ the feature list above is the spec).
The current composer (apps/web/src/pages/AgentSession.tsx, the
border-t strip at the bottom) is a bare Textarea + one wide Button โ
functionally complete (Enter send / Shift+Enter newline / stop-during-turn)
but visually a control strip, not an input surface. Everything a chat
composer should surface per-turn (context pressure, mode, what happens on
send) is invisible until it goes wrong.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๆๅบๅ็ปญไฟฎๆน่ฆๆฑ โ โ โ textarea, autogrow 1โ~10 rows
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ (context meter ยท 12.3k/64k) (model badge) (โฒ) โ โ toolbar row
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
One CARD, not a strip: the whole composer is a rounded bordered card floating on the page background with the stream scrolling behind a hard edge above it. Two rows inside the card: the input row and a compact toolbar row. The toolbar carries, right-aligned, the context meter and the send control; left-aligned slots exist for the follow-up controls (below).
The reference image has four controls we do NOT have wire for. Splitting honestly keeps W8 shippable with zero server/daemon changes:
-
Card container โ
rounded-xl border bg-background shadow-sm, focus-within ring in--ring(the standard shadcn/radix focus token). The strip'sborder-tgoes away; the card sits in the samep-3footer slot,max-w-3xl mx-autokept. -
Auto-growing textarea โ 1 row resting, grows with content to ~10
rows, then scrolls internally (
field-sizingis not baseline enough โ use the scrollHeight clamp pattern). KeepsEntersend /Shift+Enternewline,autoComplete="off",spellCheck={false}(form-hygiene rules). -
Icon send/stop toggle โ
a(superseded 2026-09-25: the height issize-9round icon button,rounded-full--control-h, the shape the skin's radius, and a phone takes--touchโ see the post-ship note at the end of this file). Idle+draft-empty โ disabled (muted); idle+draft โbg-primary text-primary-foreground(the page's single PRIMARY action โ NOT--signal, which stays reserved for the live-turn indicator per the Signal system); turn active โ swaps to Stop (CircleStop/Squareicon,outlinestyling, destructive-on-hover is enough โ no confirm, same semantics as today's Stop button). Text labels go away (title/aria-label keeps them). -
Context meter โ reads
conversation.usagefrom the fold state (contextUsed/contextSizeโ dsh reports it live today viaused/size; claude-code/codex show nothing yet). Render: tiny mononumstext12.3k / 64k+ a 64pxh-1bar. Bar color is NEUTRAL (bg-mutedโbg-primaryfill); turnstext-warn/--warnfill past 80%,text-danger/--dangerpast 95% โ state-encoded colors only, per Signal. Absent data โ the slot renders nothing (honesty over decoration). -
Phase-aware states โ connecting: card disabled with the existing
chat.connectinghint inline; closed: disabled withchat.closed; draft preserved across phases (it already survives in page state).
| Reference control | Why deferred | What it needs |
|---|---|---|
| "+" attach (images/files) |
PromptBlock is text-only by design (v1); the wire would drop attachments silently |
PromptBlock image variant end-to-end (shared schema โ chatPromptEventSchema โ daemon โ ACP content blocks). The new @agentclientprotocol wrapper already advertises promptCapabilities.image: true, so the adapter side is ready; ours is not. |
| Permission-mode chip ("ๅฎๅ จ่ฎฟ้ฎ" ็ญ) | The daemon always establishes sessions in ACP default permission mode; there is no mode arm on the wire | A mode field on chat:session.open + daemonโwrapper permissionMode mapping (default/acceptEdits/bypassPermissions) + a confirm-first chip UI (weakening the gate is a destructive-adjacent action). Natural home: alongside the C6 permission-policy work โ the reference's policy-engine shape (see below) fits the same feature. |
| Model selector | Model is machine-level today (W3 RuntimeConfig), and dsh PINS model per native session โ a composer switcher would desync the rail |
Per-channel model override on the wire (or a "change model = new session" UX). Interim candidate for W8.5: a read-only mono badge showing the machine's runtime-config model (one GET we already have). |
| Reasoning-effort selector | No wire: CC reads CLAUDE_CODE_EFFORT_LEVEL env at boot; dsh has per-provider config only |
Same arm as the model selector, plus per-harness effort semantics. Lowest priority. |
- New
apps/web/src/components/chat/composer.tsxexporting<Composer>โ all sub-parts (Toolbar, ContextMeter, SendButton) at MODULE scope (React perf rule). Props:value/onChange,phase,turnActive,usage,onSend,onCancel. The page keeps owningdraft/send()/cancelTurn()โ the composer is presentational. - Strings: extend
strings/chat.ts(en/zh side by side). New keys:chat.sendAria/chat.stopAria(icon-only buttons must carry labels),chat.contextUsed(meter titleUsed {used} of {size} tokens), possiblychat.placeholderBusy. Wire values stay English-in-both (numbers). - The old inline Textarea/Button block in
AgentSession.tsxis REPLACED by<Composer โฆ>;chat.send/chat.stoptext keys retire with it.
-
composer.tsx(card + textarea + send/stop + context meter) +AgentSession.tsxswap; strings en/zh. - Visual pass against the Signal checklist: accent spend (primary =
send, signal = live turn only), mono/nums for the meter, focus ring,
dark theme,
text-wrapnothing needed here. - Gates:
pnpm --filter @harness-nexus/web run build(+ typecheck), manual checklist โ Enter/Shift+Enter, autogrow clamp, stop swap, meter states (force values), disabled states, zh/en, dark mode. (No web unit-test infra exists; the fold'susageshape is already exercised by construction.) - Docs: this file โ SHIPPED + notes; AGENTS.md W8 pointer.
Already shipped from this snapshot (2026-09-10): the C5 default wrapper
switch to @agentclientprotocol/claude-agent-acp (thinking streams by
default; branch fix/cc-acp-wrapper-thinking). Remaining candidates:
-
permission/policy-engine.jsโ HIGH value, future feature. Glob tool-name rules รallow_always/allow_session/reject+ timeout auto-deny + optionId-must-be-in-request validation. The natural shape for our permission-policy work (C6 or a per-machine setting); pairs with the deferred permission-mode chip above. -
sandbox-template/patch-acp-agent.jsโ candidate. Push ausage_updateaftersession/loadso a resumed session's context meter refills. Worth a spike once the composer meter lands (W8 gives the surface; today a resume shows no usage until the next turn). Mind its own lesson: fetch usage fire-and-forget โ a synchronous IPC fetch before the first turn stalls the wrapper ~15s per call. -
host-rewrite.cjs+connect-proxy.cjsโ reference only. For machines in DNS-locked networks (monkey-patcheddns.lookupfor the Node side, an HTTP CONNECT proxy for native binaries viaHTTPS_PROXY). Revisit if/when a sandboxed machine target lands. -
client/acp-client.jsstop()(closeSession before kill) โ already safe. The hazard it documents (the wrapper keeps one native claude child per session; a bare SIGKILL orphans them) is why our daemon SIGTERMs first with a 3s SIGKILL grace โ rig leak scans (post-run/procsweeps for adapter processes) keep verifying this; keep that discipline in every rig chat verification.
- Any wire/schema/daemon change (the four deferred controls above).
- Mobile layout changes beyond what the card inherits for free.
- Streaming the meter for codex (adapter-side
used/sizereporting โ a wrapper-side question, not ours). The claude-code half of this item resolved itself: see ยงPost-ship notes.
-
Landed as designed in
apps/web/src/components/chat/composer.tsx(module-scopeContextMetersub-component; presentational propsvalue/onChange/phase/turnActive/usage/onSend/onCancelโ the page keeps owning the draft and actions). Strings:chat.sendAria/chat.stopAria/chat.contextUsedadded;chat.send/chat.stopretired. -
One small parity fix beyond the plan: Enter no longer sends while an
IME composition is active (
e.nativeEvent.isComposingguard) โ the old strip had this bug and the zh locale makes it user-visible. - Verified E2E on the rig (web image rebuilt from the tree, real dsh session through the live channel): card + 3px focus-within ring, autogrow 1 row (39px) โ 4 lines (107px) โ clamps at 224px with internal scroll, Enter sends / Shift+Enter breaks, send disabled on empty draft, live stop swap during a real turn (outline + ๅๆญข็ๆ), meter rendered from live usage, en/zh strings, dark mode, zero leaked adapters after disconnect.
-
Finding โ the claude-code meter is NOT empty after all. The design
assumed only dsh reports occupancy; in fact the
@agentclientprotocolclaude wrapper (the W7.1-era swap) reportscontextUsed/contextSizetoo โ a resumed session showed9.4k / 262.1klive. The meter's render-nothing-when-absent rule stays (it is what keeps the slot honest for codex); no code change needed, the wrapper just feeds it. - Deferred items unchanged (attach, permission-mode chip, model selector, effort selector) โ the table above still enumerates the wire arms each needs.
The card and its two-row split stand; two of the target-shape numbers above do not. The
send/stop control is not a size-9 circle: its height is the density contract's
--control-h (Signal 36 / BAY 30), its shape the skin's radius โ a rounded square in both
shipped skins, which is what the comps draw โ and on a phone it takes --touch (40px) as
the panel's primary action. The toolbar aligns items-end, so the config controls sit on
the same baseline as the control beside them instead of floating above the card's edge.
The config selects are variant="bare": text/icon only, no fill and no border of their own
in either theme โ a skin's window rule is unlayered, so that exemption has to be a variant
the skin can read.
Contract in one line: <Composer> stays presentational; the send control is the panel's
bg-primary action (never --signal, which is the live-turn indicator); every height the
toolbar uses comes from a token, so all skins agree. Reasoning + measurements:
design-skin-system.md ยงThe control scale and ยงDevices.