Repository navigation
feat(web): redesign the Assistant as a chat with a saved-chats rail - #255
kinqsradio merged 6 commits into
Conversation
- Conversation layout: your messages as bubbles, answers beside an avatar, with Copy, Reply and Details as icon actions. - A hideable saved-chats rail; searching and managing chats stays in the existing dialog. Upcoming and a More menu move into the header. - Scope is chosen before the first message (Personal or a project), drives the suggestions, and is then locked for the chat. - Suggestions send immediately. - A quiet work trace: live "Working" time and progress steps while a run is attached, then "Worked for Ns" from the recorded trace, expandable. - New answers are revealed a few words at a time (skipped for reduced motion and for history); Stop replaces Send while a run is attached. - Drops the per-message status line, execution trace block, scope field, always-on character counter and provider section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Goals, routines and chats waiting on the person are grouped as Need you now (overdue goals, routines whose last run recorded an error, chats needing an answer), Today, This week and Later, with paused and done items folded away. Counts jump to each group and filters narrow by type. Each row has one quick action (Plan next step, Pause/Resume, Check history, Answer) and a menu for the rest. Permissions and admission controls move behind a settings view. The panel opens beside the chat and covers it on narrow screens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps the priority-first content but returns to the shared Dialog, so it opens centred over the chat and closes with Close or Escape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each change kind now has its own choice, Ask each time or Always allow, instead of fixed text with Revoke only. Both directions confirm first. An existing goal- or routine-scoped grant still shows as its own option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kinqsradio
left a comment
There was a problem hiding this comment.
Reviewed #255 against feat/web-rebuild (cadf0db6). The chat-first redesign, the saved-chats rail and the Upcoming priority model are coherent, and the scope picker/locked-scope behavior is well thought through. I would request changes before merge: the inline comments cover the failed-send replay, the reveal timing, the rail refetch, the live-region and radiogroup accessibility issues, the filtered Upcoming empty state, and the routine priority edge case.
Two smaller regressions that do not have a single changed line to attach to:
- The old
deliveryAvailable === falsenotice was removed. When conversation notifications are unsupported, the mute menu item simply disappears; a one-line “Conversation notifications are unavailable on this server” status would preserve the previous explanation. docs/design.md:42says to center the empty Chat/Work composer and dock it only once conversation begins, anddocs/design.md:117says Assistant starter prompts wrap in two columns. This PR docks the composer always and uses a single flex row (horizontal scrolling on mobile) for the prompts. If the redesign is intentional, please update the guide; otherwise align the UI. Several new clickable rows are also 30–32px tall (assistant.css:420/682/791) instead of the 36px desktop action token.
Verification gap: the PR body says no live run, no Stop/reveal exercise, no narrow-screen rail close, and no keyboard/screen-reader pass. docs/verification.md asks for a scoped acceptance receipt for substantial UI changes covering failure/recovery, narrow layout and both themes. The failed-send replay below is in that unexercised path; please test a failed send and a successful run before merging.
Local checks: tsc --noEmit -p web/tsconfig.json, Prettier on the changed files and npm run build -w web pass. No CI checks are reported for this PR, and I did not run a browser or a live AI run.
| !last.turn.proposal && | ||
| last.turn.text | ||
| ) | ||
| setFresh(last.turn.text); |
There was a problem hiding this comment.
This infers “a new answer arrived” from assistant.sending going true → false, then takes the last display item. On a failed send, send() removes the optimistic user turn and sets sending false; the last item is then the previous assistant answer, so this calls setFresh(oldText) and re-runs the reveal on an old answer. The same can happen for a stopped run that adds no new turn. The PR's claim that only an answer arriving while the page watches is revealed is therefore not true on the failure path. Track the last assistant turn id before sending (or mark a fresh turn id in accept()) and only reveal when that id changes.
| return () => clearInterval(timer); | ||
| }, []); | ||
| return ( | ||
| <div className="assistant-trace" role="status"> |
There was a problem hiding this comment.
role="status" is an implicit polite live region, and it wraps the elapsed timer that updates every second. The step list is aria-live="off", so screen-reader users hear repeated “Working · Ns” announcements but miss the progress labels. Move the timer outside the live region (or make it aria-live="off") and announce step changes in a polite region instead.
| const [shown, setShown] = useState(0); | ||
| const words = text?.match(/\S+\s*/g) ?? []; | ||
| const total = words.length; | ||
| useEffect(() => { |
There was a problem hiding this comment.
This reset runs in useEffect, after paint. When fresh changes, the first render still has the previous shown value — often Number.MAX_SAFE_INTEGER after a null text — so done is true and the full answer paints once before the effect resets it to 0. Use a state keyed by the text, a useLayoutEffect, or derive the initial shown value synchronously to avoid the flash.
| const owned = useOwned(); | ||
| const read = useRead( | ||
| () => client.aiChats({ search: "", kind: "all", limit: 50 }), | ||
| [active], |
There was a problem hiding this comment.
useRead clears its data and sets loading whenever a dependency changes. Passing [active] means every chat switch blanks the rail and refetches up to 50 chats even though only the highlight changed. Remove active from the dependencies, or add a retaining reload mode so the rail stays visible while it refreshes.
| </p> | ||
| <div | ||
| className="assistant-scopes" | ||
| role="radiogroup" |
There was a problem hiding this comment.
This declares a radiogroup, but the buttons have no arrow-key navigation or roving tabindex; every option is a tab stop. The checked task option also has no click handler and is not disabled, so it looks interactive but does nothing. Either implement the radio keyboard model (one tab stop plus arrow keys) or use aria-pressed buttons in a plain group; render the task scope as a non-interactive tag.
| ))} | ||
| </div> | ||
| {loading && !items.length && <Loading label="Loading upcoming" />} | ||
| {!loading && items.every((item) => item.bucket === "quiet") && ( |
There was a problem hiding this comment.
items.every(...) is true for an empty array, so filtering to a category with no rows (for example “Waiting chats” when there are none) shows “Nothing is coming up. You're caught up.” even when other goals or routines still need attention. Gate the global caught-up message on filter === "all" (or on all.length === 0) and use a category-specific empty message otherwise.
| minute: "2-digit", | ||
| timeZone: zone, | ||
| }); | ||
| if (gap <= 0) |
There was a problem hiding this comment.
gap <= 0 means any next_run_at in the past is labelled “Today ”, even if the schedule is days stale and last_result.error is not set. That can show a long-past time as today's work. Consider treating gap < 0 as overdue/now, or only treating it as Today within a small scheduling-delay window.
|
Review fixes pushed in
The new Assistant menus also depend on #254 for WebKit anchoring. Checks: Prettier and |
See CONTRIBUTING before starting engineering work.
Change
The web Assistant becomes a conversation-first chat screen. The old page stacked a header, a scope field, an "Activity and preferences" block, a transcript with a status line and an execution-trace section on every message, and a composer with a counter and a provider section. This replaces that with one chat column, a saved-chats rail and a quieter record of what each run did.
All changes are in
web/src/screens/assistant/:ChatRail.tsx) sits beside the conversation, grouped as Pinned, by project and Recent. A header button hides and shows it, and the choice is remembered inlocalStorage. On narrow screens (≤760px) the rail starts hidden, opens over the chat and has its own close button. Search, rename, pin, save-as-note and delete stay in the existingChatListdialog, now reached from "Search and manage".?fresh=/?project=routes. Once the chat has a message, the scope shows as a locked tag and New chat is the way to choose again. Task-scoped chats opened from a task keep that scope.assistant.send(text), instead of copying the text into the composer.Activity.tsx). While a run is attached, a live row shows "Working · Ns" (or "Waiting for you") with each distinctprogress.labelas it arrives. A finished turn shows "Worked for Ns", using the span of its recordedtrace[].attimestamps, which expands to thetraceWordslines. Non-defaultreplyWordsstates such as "Run stopped" or "Changes waiting for review" join that line. The trace uses smaller muted text so it never reads as the answer.prefers-reduced-motionskips the effect.Upcoming, most urgent first (
a0d013bb,26e9d863)Upcoming.tsxand the newpriority.ts. Upcoming was a dialog with separate Goals and Routines lists. It is still a pop-up window built on the sharedDialog, so it closes with Close or Escape, but its content is reorganised. (a0d013bbtried it as a side panel;26e9d863returns it to the pop-up window at the requester's choice.)upcomingItems):active: "needs_you", active goals whosetarget_datehas passed in the planner timezone, and routines whoselast_result.erroris set. The worker and run code write that field when a scheduled run can't start, can't complete or is interrupted.next_run_atfalls today in the routine's timezone.GoalWork.9d6449fd). Each change kind has a select, Ask each time or Always allow, saved through the existingsetAssistantApprovalScopes. Previously the only control was Revoke on an existing grant. Both directions confirm first, and an existing goal- or routine-scoped grant shows as its own option. Checked in the browser: all seven kinds load as Ask each time, and choosing Always allow on Tasks opens "Allow tasks changes without asking?". Cancelling leaves it on Ask each time. No grant was actually saved.Waiting questions and approvals, failed-answer restore, reply targets, selection replies, Draft project, Upcoming, saved history and identity keep their existing owners and behavior.
Checks and limits
Run on
feat/assistant-chat-redesignat2f46f409:npx tsc --noEmitinweb/: exit 0.npx prettier --check src/screens/assistant/inweb/: passes.Follow-up
ad684440(3 Nov 2026): Prettier andvite buildpass;tsc --noEmitpasses with the ES2023 array lib. The stocknpm run buildstill stops at the pre-existingArray.findLast/ES2022 error inlibrary/page/Source.tsx. No browser, live AI run, or screen-reader pass was run for the follow-up. The follow-up covers the failed-send replay, paint-before-reveal flash, step-only live announcements, retaining rail refresh, pressed-button scope group, filter-specific empty states, overdue routine bucket,--controltarget sizes, restored delivery notice, design-guide sync, andaria-haspopupon the new menus.Rendered checks in the built-in Chromium browser against the local Vite dev server, with seeded demo data. These ran before the commit was rebased onto
feat/web-rebuild, on a tree that also had #254's two fixes. The rebase touched only these files, and the typecheck was rerun after it.?project=…, marks it selected and changes the suggestions and placeholder. Opening a saved chat shows the locked scope tag, the bubbles and "Worked for 4s · Outcome not verified", which expands to its steps.Upcoming, at
a0d013bb:tscandprettierpass again.upcomingItemswas run withtsxon sample data (fixed "now" 2026-11-03 14:00 Melbourne) and grouped a waiting chat, a failed routine and an overdue goal under Need you now; a goal due today and a routine at 18:00 under Today; a Friday goal under This week; a December routine and an undated goal under Later; and paused and done items as quiet. In the browser at 1440×900 the side-panel version showed the demo data (2 goals under Later, 2 folded items), and the Routines filter worked. At 375×812 it filled the width with no horizontal overflow. After26e9d863, at 1440×900: the header button opens the pop-up with the same content; the permissions view and Back work; and Close and a real Escape key both close it. The pop-up was not rechecked at phone width. The demo data has nothing urgent, so the Need you now and Today groups were not seen rendered, and the row actions and menus were not clicked in the browser.Not verified:
Workspace
repo/, and read the relevant project guides.This contributor confirmation is not an automated claim that a host loaded policies.
Agent disclosure
git,gh,tsc,prettier.claude-opus-5-5); effort as set by the session.🤖 Generated with Claude Code
Follow-up fixes (3 Nov 2026)
git,gh,npm,tsc, Prettier, Vite.