mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* fix(web): ellipsize the fork divider note instead of hard-cutting it The fork divider footnote lost its ellipsis when #7863 merged the two-block layout into one line. `.fork-note` became `display: flex` so the branch icon and the label could sit side by side, but the three truncation declarations (`overflow: hidden`, `text-overflow: ellipsis`, `white-space: nowrap`) stayed on `.fork-sep span` — the very same element. Bare text inside a flex container lives in an anonymous flex item, and `text-overflow` is not inherited, so the anonymous box falls back to `clip`: the 62% cap still clipped, just without the three dots. Move the label into its own `.fork-note-label` span and hang the truncation on that layer, with `min-width: 0` to release the flex item's default `min-width: auto`. `.fork-sep span` becomes `.fork-sep > span` so the inner label is not pinned to `flex: none` — a non-shrinkable flex item is always exactly as wide as its content and can never overflow. Also finishes OPEND-2714's copy: the 16 locales other than en / zh-CN / zh-TW still carried translations of the retired English sentence. They now say what `Continued from chat` says, each within 1.5x the English length so they stay inside the 62% cap. * test(web): witness the fork note ellipsis in a browser, not in chat.css apps/web/src/components/chat/AGENTS.md §5 forbids asserting CSS class names or declarations in chat tests, and the spec added in the previous commit did exactly that: it located the label by className, parsed chat.css, and pinned the resolved values of text-overflow, overflow, white-space, min-width and flex. Delete it. Replace it with a narrow browser witness. e2e/ui/fork-note-ellipsis.test.ts forks a conversation through the real daemon endpoint, renders it in the longest shipped locale (German), squeezes the chat panel until the label overflows, then checks two observable things: the label really is overflowing its box, and its paint differs from the same element forced to `text-overflow: clip`. Comparing the element against itself keeps the oracle baseline-free, and a build that still hard-cuts paints identically under both, so the check cannot pass vacuously. Asserting the declarations was the wrong instrument anyway: the broken build declared `text-overflow: ellipsis` too. What was wrong was the box it sat on, and only a laid-out browser can see that. The one structural fact worth keeping - the label is carried by its own element rather than as bare text beside the icon - moves into the existing fork-divider spec and keys off a data-testid instead of a class name. Enrolls the new file in the project-workspace UI P0 group. An unenrolled P1 would run only in the prerelease full pool, which means shipping a witness that nothing on the merge path ever executes. * test(e2e): wait for the fork divider before squeezing the chat panel The width squeeze is a raw custom-property write onto the split, which React's own width effect would overwrite if a late fetch re-rendered between the write and the measurement. Wait for the divider to paint at the default width first, then squeeze with nothing awaited in between. Also corrects a stale doc comment that named a helper this file never had. * fix(e2e): squeeze the chat panel without racing its width transition CI caught the guard doing exactly what it was written to catch: it failed with "content 161px vs box 161px", meaning the note never overflowed and the ellipsis check would have been measuring nothing. The cause was the squeeze, not the copy. `--project-chat-panel-width` is a registered `@property` with a 200ms transition, so writing it tweens from the default 460px and the measurement taken right after read the panel mid-flight. Suspend the transition the way the product's own resize handle does (`.is-resizing-chat`), and do the write and the read inside a single evaluate so no re-render can land between them. Also measures the label before and after the squeeze, so the next failure names its own cause: a panel that never narrowed is a fixture problem, a narrowed panel that still fits the text is a copy problem. The previous message could not tell those apart. * chore(web): drop the now-unused splitList export from the cascade helper It was exported only to feed the CSS-parsing spec that the previous commit deleted. Nothing outside the helper calls it any more, so put it back to a module-private function and leave the shared test infrastructure exactly as it was on main. * fix(web): collapse the queued row's leading action into one 「引导对话」 button The queued-send row's leading action had two faces: a 74px labelled 「引导对话」 while `canSteerCurrentTurn` held, and an unlabelled 22px "send now" icon otherwise. Both were wired to the same `sendQueuedChatSendNow` — the gate only changed the name, the tooltip and the analytics `element` value. Product ruled on 2026-09-08: 「引导对话就是原本的立即发送啊,只不过我们 换了个名字跟 codex 客户端对齐了下」. So the gate goes, the fallback face goes, and the row keeps one button that is always 「引导对话」. The delivered spec agrees. `729fa43ce7:docs/design/chat-panel-next.html` component 17 "Queue" draws exactly one action there — `aria-label="引导对话" data-tip="引导对话"` with `<svg/><span>引导对话</span>` — and never the icon-only send-now key. `data-tip` is the plain name, so `title` / `data-tooltip` / `aria-label` collapse from `chat.queuedSteerInterrupts` back to `chat.queuedSteer`; visible text and accessible name are now identical (WCAG 2.5.3 Label in Name). Order is untouched: steer → edit → delete (OPEND-2715). Analytics: this surface no longer emits `ui_click` with `element: 'send_now'`. The surviving button reports `'steer'`, matching the name it now carries. The `send_now` member stays in the contract union because PostHog still holds the historical events. Props: `onSteerQueuedSend` existed only to carry the gate, so it is removed from ChatPane, SideChatTab and both ProjectView call sites, along with `canSteerCurrentTurn`. `onSendQueuedNow` survives as the single handler — it has the second pass-through face in `SideChatTab`'s `controlledChat`, and it names what the handler actually does. `steerBlockedReason` is left alone: it had no producer before this change either, and retiring a deliberately parked placeholder is its own call. * fix(prompts): stop asking the model for a per-question help string OPEND-2707. The clarification card stopped rendering the per-question subtitle (`q.help`) in #7863, but the prompt layer kept asking the model to write one — so the model spent tokens on copy nobody would ever see, and believed it had already told the user something it had not. The user ruled on 2026-09-08 to go all the way and retire the prompt half too. Three prompt surfaces carried it: - `core-slim.ts` routed leftover context into "the individual question labels/help"; context now has one destination, the question label. - The UI locale override (daemon + contracts mirror) listed "helper text" among the question-form controls that must be localized — naming the field without naming it. The other four entries in that list all still render. - The ElevenLabs voice picker is the only form the repo itself authors, and it shipped the only `help` string in the tree. Its content is real information (the visible labels are voice descriptions; the submitted value is the voice_id), so it moves into the question label rather than being dropped. `FormQuestion` has no per-question `description`, and `placeholder` is already taken by "Choose a voice". That form is `JSON.stringify`d whole into the system prompt, so it doubles as the model's worked example of what a question may look like — leaving a `help` key there would have quietly undone the rest of the change. `e2e/tests/question-form-help-retired.test.ts` guards all seven authorized prompt paths, mirroring `question-form-visual-style-retired.test.ts`. It reads source bytes, so it also asserts the parser keeps tolerating `help` — the dormant field must not be removed along with the prompt guidance. Snapshot: only `totalChars` moves, on the three scenarios that compose the touched text; no section is added or removed. * docs(web): record FormQuestion.help as dormant, not dead OPEND-2707 follow-up. With the renderer, the CSS rule, the prompt guidance and the last host-authored producer all gone, `help` now has no producer and no consumer anywhere in the tree. It still stays. Removing `help?: string` would break `QuestionForm.no-question-subtitle.test.tsx` and `QuestionForm.test.tsx` — and the first of those is the only positive evidence that the subtitle is not rendered. Deleting a zero-cost field at the price of the test that guards the invariant is a bad trade. Cached prompts, older clients and the model's own memory of the old format can also still send a question carrying the key, and the parser should keep tolerating it rather than change parse shape as a side effect of this retirement. Same reasoning as the six `qf.visual*` i18n keys in `specs/current/chat-panel-decisions-sheet.md`. The field now documents when and why it went dormant, and how to bring the rendering half back (the pre-#7863 revision of both files). Also corrects `DesignDirection.mood` in both prompt mirrors: its docblock said the text is "shown to the user as `help`", which was already inaccurate — it is the `mood` blurb on a `direction-cards` option, and that renderer has had no call site since the visual-style question was retired. * fix(web): keep the disconnect failure card when no reconnect row is there A turn that failed with `DAEMON_STREAM_DISCONNECTED` left nothing at all on screen once the user stepped out of the project and came back: no failure card, no reconnect line, no way to retry. `anotherSurfaceOwnsFailure` in `ChatPane` handed the story to the reconnect line unconditionally, on the premise (stated in the comment right above it) that the reconnect line "is always there". It is not: `ProjectView` has a dedicated unmount effect that clears `reconnectView` on project change / screen exit, so on re-entry the card yields to a surface that has already left. The neighbouring balance rung already encodes the right invariant — `balanceCardCannotTakeTheHandoff` refuses the handoff when the upgrade card cannot be drawn. The same invariant was only written on one of the two rungs. Both rungs now name their handoff target first, then ask one shared question: is that surface actually present. Red spec: `ChatPane.error-card-ladder.test.tsx` — a disconnected run with no reconnect row must render the card, its own `chat.connectionDropped` copy, and a retry action. Reverse anchor (row present -> no card) is green on both sides of the fix. The two older cases in that file asserted "no card" while rendering *without* a reconnect row: they pinned this defect rather than the invariant. Both now put the handoff target on screen, so they pin what they were written for — that either signal (structured code, or a legacy detail-only row) is recognized as the disconnect failure. * fix(web): shorten the Cloud switch CTA to match the delivery spec The error card's primary button read 「切换到 OpenDesign Cloud 并重试」 while the delivery spec (docs/design/chat-panel-next.html, cell 79 "特殊错误 · 运行环境为 CLI / BYOK") writes 「切换到 Cloud」. Product ruled in person on 2026-09-08: "切换到 cloud 就行了,你怎么写那么 长的文案『切换到 Cloud 并重试』" and "具体的报错文案不一定跟设计稿对齐, 按钮文案对齐先". So only `chat.amrCard.switchCta` is aligned here — the card's title and body keep their own product copy and are deliberately left untouched, including the separate `chat.amrCard.switchBody` key. All 19 locales are shortened to the same "Switch to Cloud" register with the trailing "and retry" clause dropped. Comments in ChatPane.tsx and amr-guidance.ts that quoted the old label are updated so they stop describing copy that no longer exists; the reasoning around them is unchanged. Playwright assertions in e2e/ui that pinned the old label follow the new string. * docs(specs): record the three rulings taken on 2026-09-08 * fix(web): keep the failure card when a failed turn has no words for it A turn that ends in a terminal failure loses its entire recovery surface whenever the card cannot compute a description. The whole card — title, message, and all four actions (contact support, export logs, retry, switch to Cloud) — hangs off one `displayError` ternary, so a null description takes the explanation and every way out with it. What is left on screen is the execution shell's bare "run failed" line, written on the assumption that the card below it says why. The three description sources all go empty together more often than the shape suggests: the mapping table's own last-resort row carries no `messageKey`, the pane-level `error` state is reset when ProjectView unmounts on navigation, and the run's raw detail never reached the message for the `AGENT_UNAVAILABLE` / `BYOK_PROVIDER_REQUIRED` / `AGENT_RUNTIME_DEF_INVALID` family (daemon `fail()` emits without going through `persistRunEventToAssistantMessage`) or for the `runStatus:'failed'` rows ProjectView writes itself when self-healing a phantom running turn. `resolveRunErrorCardDescription` now takes `turnEndedInTerminalFailure` and falls back to `RUN_FAILURE_FALLBACK_MESSAGE_KEY` instead of rendering nothing. It is the last branch, so every handoff and every more precise source still wins ahead of it. The signal is `retryableAssistantMessage`, which already gates the recovery entries and already treats a delivery failure (`no_result` / `delivery_failed`) as terminal, minus the empty API response — that one has a surface of its own ("No output") and must not get a second one. Also stops an empty pane-error string from shadowing the run's own detail. That branch carried a comment saying the silence was a separate question; this is the same question, answered the same way.