polish: refine structured composer

This commit is contained in:
Miguel Palhas
2026-09-06 17:06:40 +01:00
parent a1f3690aaf
commit 4fda393378
3 changed files with 100 additions and 19 deletions
+2
View File
@@ -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();
});
+8 -2
View File
@@ -104,7 +104,9 @@ export function createStructuredConnection(
</button>
<div class="structured-config-menu" role="group" aria-label="Session settings" hidden></div>
</div>
<button type="submit" class="btn btn-primary structured-send">Send</button>
<button type="submit" class="btn btn-primary structured-send"><span>Send</span>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m7 5 5 5-5 5M12 10H3"/></svg>
</button>
<button type="button" class="btn structured-cancel" hidden>Cancel turn</button>
</form>
</div>
@@ -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", () => {
+90 -17
View File
@@ -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);