diff --git a/web/src/structured.test.ts b/web/src/structured.test.ts index 4ebbbdc..e675f57 100644 --- a/web/src/structured.test.ts +++ b/web/src/structured.test.ts @@ -583,9 +583,11 @@ describe("structured connection: turn/composer gating", () => { ws.emit({ type: "reduced_state", state: { turn: { responding: true, thinking: false, active_tool_calls: [] } } }); expect(input.disabled).toBe(true); expect(cancel.hidden).toBe(false); + expect((conn.el.querySelector(".structured-send") as HTMLButtonElement).hidden).toBe(true); ws.emit({ type: "reduced_state", state: idleState }); expect(input.disabled).toBe(false); expect(cancel.hidden).toBe(true); + expect((conn.el.querySelector(".structured-send") as HTMLButtonElement).hidden).toBe(false); conn.dispose(); }); diff --git a/web/src/structured.ts b/web/src/structured.ts index d4e7bc0..1ef8961 100644 --- a/web/src/structured.ts +++ b/web/src/structured.ts @@ -104,7 +104,9 @@ export function createStructuredConnection( - + @@ -478,9 +480,11 @@ export function createStructuredConnection( function renderComposerState(): void { const active = state.turn.responding || state.turn.thinking || awaitingBeginGeneration !== null; + const projectedActive = state.turn.responding || state.turn.thinking; input.disabled = active; sendBtn.disabled = active; - cancelBtn.hidden = !state.turn.responding && !state.turn.thinking; + sendBtn.hidden = projectedActive; + cancelBtn.hidden = !projectedActive; if (active) closeCommandMenu(); renderActivity(); } @@ -696,7 +700,9 @@ export function createStructuredConnection( function autoResize(): void { updateTranscriptLayout(() => { input.style.height = "auto"; + input.style.overflowY = "hidden"; input.style.height = `${Math.min(input.scrollHeight, 160)}px`; + if (input.scrollHeight > 160) input.style.overflowY = "auto"; }); } input.addEventListener("input", () => { diff --git a/web/src/styles.css b/web/src/styles.css index e0ee2fe..28377db 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -2175,19 +2175,19 @@ input.leaf-rename:focus { .structured-config { position: relative; - flex: 0 0 var(--control-h); + flex: 0 0 40px; } .structured-config-trigger { - width: var(--control-h); - height: var(--control-h); + width: 40px; + height: 40px; display: inline-flex; justify-content: center; align-items: center; padding: 0; - border: 1px solid var(--border); + border: 0; border-radius: var(--r-2); - background: var(--surface-2); + background: transparent; color: var(--muted); cursor: pointer; } @@ -2204,7 +2204,6 @@ input.leaf-rename:focus { .structured-config-trigger:hover, .structured-config-trigger[aria-expanded="true"] { color: var(--ink); - border-color: var(--border-strong); background: var(--surface-3); } @@ -2829,16 +2828,28 @@ input.leaf-rename:focus { stays reachable at the pane edge without changing transcript scroll math. */ .structured-composer { flex: 0 0 auto; - padding: var(--sp-3); - border-top: 1px solid var(--border); + margin: var(--sp-2) var(--sp-3) max(var(--sp-2), env(safe-area-inset-bottom)); + padding: var(--sp-1); + border: 1px solid var(--border-strong); + border-radius: var(--r-3); background: var(--surface); display: flex; align-items: flex-end; - gap: var(--sp-2); + gap: var(--sp-1); + box-shadow: + inset 0 1px 0 color-mix(in oklab, var(--ink) 6%, transparent), + var(--shadow-1); + transition: + border-color var(--dur-fast) var(--ease-out), + box-shadow var(--dur-fast) var(--ease-out); } -.structured-composer { - padding-bottom: max(var(--sp-3), env(safe-area-inset-bottom)); +.structured-composer:focus-within { + border-color: var(--accent); + box-shadow: + inset 0 1px 0 color-mix(in oklab, var(--ink) 8%, transparent), + 0 0 0 3px var(--accent-tint), + var(--shadow-1); } .structured-input-wrap { @@ -2852,13 +2863,72 @@ input.leaf-rename:focus { width: 100%; min-width: 0; max-height: 160px; + min-height: 40px; resize: none; - padding: var(--sp-2) var(--sp-3); - border: 1px solid var(--border-strong); + overflow-y: hidden; + padding: 10px var(--sp-2); + border: 0; border-radius: var(--r-2); - background: var(--bg); + background: transparent; color: var(--ink); font-size: var(--fs-13); + line-height: 20px; +} + +.structured-composer .structured-send, +.structured-composer .structured-cancel { + height: 40px; + min-height: 40px; + border-radius: var(--r-2); + font-family: var(--font-sans); +} + +.structured-composer .structured-send { + gap: 6px; + padding: 0 var(--sp-3); + border-color: var(--accent); + background: var(--accent); + color: var(--accent-ink); + font-weight: 650; + box-shadow: inset 0 1px 0 color-mix(in oklab, white 24%, transparent); +} + +.structured-composer .structured-send::before, +.structured-composer .structured-send::after, +.structured-composer .structured-cancel::before, +.structured-composer .structured-cancel::after { + content: none; +} + +.structured-composer .structured-send svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 1.7; + stroke-linecap: round; + stroke-linejoin: round; + transition: transform var(--dur-fast) var(--ease-out); +} + +.structured-composer .structured-send:hover { + background: var(--accent-strong); + border-color: var(--accent-strong); + color: var(--accent-ink); +} + +.structured-composer .structured-send:hover svg { + transform: translateX(2px); +} + +.structured-composer .structured-cancel { + padding: 0 var(--sp-3); + border-color: color-mix(in oklab, var(--danger) 35%, var(--border)); + color: var(--danger); +} + +.structured-composer .structured-cancel:hover { + background: var(--danger-tint); } @media (max-width: 520px) { @@ -2933,13 +3003,12 @@ input.leaf-rename:focus { .structured-input:focus { outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-tint); + box-shadow: none; } .structured-input:disabled { color: var(--faint); - background: var(--surface-2); + background: transparent; } /* `.btn` sets `display: inline-flex`, which outranks the UA sheet's @@ -2948,6 +3017,10 @@ input.leaf-rename:focus { display: none; } +.structured-send[hidden] { + display: none; +} + .structured-error { margin: 0 var(--sp-3); padding: var(--sp-2) var(--sp-3);