Public: anyone can read this; the crew can change it.

The /talk page speaks the home page's language #9704

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/9690-the-talk-page-speaks-the-home-page-s-lan → main 7 commits fork 2 threads · 2 unresolved

  • Merged. mason/9690-the-talk-page-speaks-the-home-page-s-lan was merged into main as a05058c.

Review

Sign in to review this change.
4 files changed +29 −9
docs/design/areas/talk.md +5 −3
@@ -5,10 +5,10 @@
55 - `.talk` — the page's column: at most 760px, centred, `min-width: 0` so nothing pushes the page sideways at 375px.
66 - `.talk-log` — the ordered list of turns; its `data-last` is the last turn id the stream resumes from.
77 - `.talk-turn` — one turn; wraps anywhere, so a long word or URL never scrolls the page.
8 - `.talk-person` — the person's words: a right-aligned bubble, pre-wrapped.
8 - `.talk-person` — the person's words: right-aligned, no box, pre-wrapped.
99 - `.talk-agent` — the agent's reply, markdown, full width, no bubble.
1010 - `.talk-tool` — what the agent did, one muted small line.
11 - `.talk-confirm` — a held action's card: the sentence, then Confirm and Cancel at 44px high.
11 - `.talk-confirm` — a held action: the sentence, then Confirm and Cancel at 44px high.
1212 - `.talk-compose` — the composer, sticky to the bottom of the viewport (padded for the phone's safe area), so it stays reachable; its textarea is 16px so iOS does not zoom.
1313 - `.talk-status` — one polite line (sending, answering, an error); it always takes its line, so nothing shifts.
1414 - `.talk-more` — the "Earlier" link above the log.
@@ -33,6 +33,8 @@
3333 - `.talk-attach` — the "Attach" file-picker label in the composer bar, a `.btn` that stays hidden until the script runs (the picker needs it).
3434
3535 Under a reply, `.talk-cost` is one quiet line with the turn's dollars; above the log, `.talk-context` says how full the context is and, from 80 %, suggests a new conversation. Both are `small muted` text, no new colour.
36 - `.talk-queued` — a message waiting for the running turn: the person bubble, dimmed with a dashed border, its "Queued" line and Cancel under it.
36 - `.talk-queued` — a message waiting for the running turn: the person's words, dimmed, its "Queued" line and Cancel under it.
3737
3838 Search (#8823) adds no component class: the form is a `.bar` with `.input` and `.btn`, and the results and the conversations list are `.list` of `row()`s (an "archived" `badge` as the trail, the match as the row's meta). The turn a result opens at is marked by a muted hairline on its left edge, `.talk-turn:target`, the one new rule, which replaces the two `.talk-chats li a` rules the rows made unnecessary.
39
40 #9690: the page speaks the home page's language: an `sr-only` h1, the conversations as a `.list` of rows, and no box round a turn, a tool call or a held action; the only hairline is above the composer. No new classes.
src/web/pages/talk.ts +2 −2
@@ -117,7 +117,7 @@ const ranOutOf = (ctx: Ctx, userId: string, turns: readonly Turn[]): Set<string>
117117 const meta = (who: string, at: string): Html =>
118118 html`<p class="talk-meta small muted">${who} · ${time(at)}</p>`;
119119
120 /** A tool call or its outcome as a quiet card: closed, the name alone; open, the whole line. */
120 /** A tool call or its outcome as a quiet line: closed, the name alone; open, the whole line. */
121121 const toolCard = (id: string, says: string, name: string, at: string, set = ""): Html =>
122122 html`<li class="talk-turn talk-tool${set}" id="${id}"><details><summary>${name.slice(0, 60)} <span class="talk-when">${time(at)}</span></summary><p>${says}</p></details></li>`;
123123
@@ -298,7 +298,7 @@ function talkPage(ctx: Ctx, conversation: Conversation, before0: string | undefi
298298 const acts = !paged ? exchangeActs(ctx.app.db, user.id, conversation.id, running, here) : new Map<string, Html>();
299299 // Edit shows the composer holding the last message, posting to Edit instead of Send.
300300 const editing = edit ? lastExchange(ctx.app.db, user.id, conversation.id).person : undefined;
301 const body = html`<div class="page-head"><h1>Talk</h1></div>
301 const body = html`<h1 class="sr-only">Talk</h1>
302302 ${chats(ctx, conversation, showArchived, typed)}
303303 <div class="talk" data-talk data-user="${user.id}" data-c="${conversation.id}" data-here="${here}" data-paged="${paged ? "1" : "0"}" data-running="${running && !paged ? "1" : "0"}">
304304 <div>
src/web/pages/talk_client.ts +3 −4
@@ -16,10 +16,10 @@ export const talkCss = /* css */ `
1616 .talk-body.prose table { display: block; max-width: 100%; overflow-x: auto; }
1717 .talk-body.prose :is(p, ul, ol, pre, table, blockquote) { margin: 0 0 8px; }
1818 .talk-body.prose > :last-child { margin-bottom: 0; }
19 .talk-person { justify-self: end; max-width: 85%; background: var(--muted); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 0.5rem 0.75rem; white-space: pre-wrap; }
19 .talk-person { justify-self: end; max-width: 85%; padding: 4px 0; white-space: pre-wrap; }
2020 .talk-agent { justify-self: start; max-width: 100%; padding: 4px 0; }
2121 .talk-tool { color: var(--muted); font-size: var(--fs-min); padding: 0 4px; }
22 .talk-confirm { border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 0.75rem; background: var(--card); }
22 .talk-confirm { padding: 8px 0; }
2323 .talk-confirm .bar { margin-top: 8px; gap: 8px; }
2424 .talk-confirm .btn { min-height: 2.75rem; flex: 1 1 8rem; }
2525 .talk-compose { position: sticky; bottom: 0; background: var(--bg); padding: 0.5rem 0 max(0.5rem, env(safe-area-inset-bottom)); border-top: 1px solid var(--border); z-index: 8; }
@@ -32,7 +32,6 @@ export const talkCss = /* css */ `
3232 .talk-copy { margin-left: auto; background: none; border: 0; color: inherit; font: inherit; cursor: pointer; padding: 6px 8px; min-height: 2rem; opacity: 0.7; }
3333 .talk-copy:hover { opacity: 1; text-decoration: underline; }
3434 .talk-plain { white-space: pre-wrap; }
35 .talk-tool details { border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 2px 10px; }
3635 .talk-tool summary { cursor: pointer; padding: 6px 0; min-height: 1.5rem; }
3736 .talk-tool details p { margin: 0 0 8px; white-space: pre-wrap; }
3837 .talk-steps { margin: 4px 0 0; }
@@ -52,7 +51,7 @@ export const talkCss = /* css */ `
5251 .talk-replaced { opacity: 0.55; }
5352 .talk-failed form { margin-top: 6px; }
5453 .talk-failed .btn, .talk-act { min-height: 2.75rem; }
55 .talk-queued { opacity: 0.7; border-style: dashed; }
54 .talk-queued { opacity: 0.7; }
5655 .talk-acts { display: flex; gap: 6px; justify-content: flex-end; margin-top: 4px; }
5756 .talk-agent .talk-acts { justify-content: flex-start; }
5857 `;
test/talk-page.test.ts +19
@@ -78,6 +78,25 @@ afterAll(async () => {
7878 });
7979
8080 describe("the page", () => {
81 test("the conversations are rows and no turn sits in a box (#9690)", async () => {
82 say(alice, "person", "hi");
83 const html = await (await get("/talk", alice)).text();
84 expect(html).toContain('<h1 class="sr-only">Talk</h1>');
85 expect(html).not.toContain('class="page-head"');
86 const nav = html.slice(html.indexOf('<nav class="talk-chats"'), html.indexOf("</nav>"));
87 expect(nav).toContain('<ul class="list">');
88 const { talkCss } = await import("../src/web/pages/talk_client.ts");
outdated

Vacuous: a failed fetch becomes "" through .catch, and the CSS assertions below then pass. Import talkCss and assert on it.

89 const rule = (sel: string) => talkCss.match(new RegExp(`(?:^|\\n)${sel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\{([^}]*)\\}`))?.[1];
90 expect(rule(".talk-person")).toBeDefined();
91 expect(rule(".talk-person")).not.toMatch(/border|background/);
92 expect(rule(".talk-confirm")).toBeDefined();
93 expect(rule(".talk-confirm")).not.toMatch(/border|background/);
94 expect(rule(".talk-queued")).toBeDefined();
95 expect(rule(".talk-queued")).not.toContain("border-style");
96 expect(talkCss).not.toMatch(/\.talk-tool details\s*\{[^}]*border/);
97 expect(talkCss).not.toMatch(/\.talk-chats ul\s*\{/);
98 });
99
81100 test("signed out goes to /signin and comes back", async () => {
82101 const res = await get("/talk");
83102 expect(res.status).toBe(303);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs