feat(web): redesign web UI and add design system - #1258
Conversation
Animate toast enter/leave, dialog open, and workspace-list and tool-row expand/collapse instead of snapping, and add the changesets covering the web redesign.
🦋 Changeset detectedLatest commit: a11c490 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c946ab9fb5
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (tool.media && tool.status === 'ok') return MediaTool; | ||
| const name = normalizeToolName(tool.name); | ||
| if (name === 'edit' || name === 'write' || name === 'multi_edit') return EditTool; | ||
| if (name === 'agent') return AgentTool; |
There was a problem hiding this comment.
Route Agent calls to the Agent renderer
When a foreground subagent is spawned by the Agent tool, this branch is unreachable because normalizeToolName() maps both agent and subagent to task in lib/toolMeta.ts. Those calls therefore render with GenericTool instead of AgentTool, losing the inline Open button; since foreground subagents are also filtered out of the dock, users cannot open the live subagent detail panel for them.
Useful? React with 👍 / 👎.
| }, | ||
| ); |
There was a problem hiding this comment.
Make Dialog focus run on initial open
This watcher is not immediate, but most callers mount Dialog with open already true (for example Login/AddWorkspace set open = ref(true) and rely on Dialog to own focus). In that common path the focus/restoration branch never runs, so keyboard focus can remain behind the overlay and the initial Tab is not trapped inside the modal until open changes; add an immediate run or mount-time focus handling.
Useful? React with 👍 / 👎.
- toolRegistry matched the raw 'agent' name, but normalizeToolName folds agent/subagent into 'task', so agent calls fell through to GenericTool and lost the inline Open button for the subagent detail panel. - Dialog's focus watcher only fired on change; callers that mount with open already true (Login, Settings, ...) never moved focus into the modal. Run it immediately so initial focus and restore-on-close work.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 79baf3adad
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| function thinkingEnabled(): boolean { | ||
| const thinking = props.config?.thinking; | ||
| if (!thinking || typeof thinking !== 'object') return false; | ||
| return (thinking as { enabled?: boolean }).enabled === true; |
There was a problem hiding this comment.
Treat implicit thinking config as enabled
When config.thinking exists with an effort but no enabled field, the core resolver still starts new sessions with thinking on because only enabled === false disables it. This helper returns false unless enabled is literally true, so the Settings switch shows off for an effectively-on config and the first click writes { enabled: true } instead of disabling thinking; derive the displayed value from the effective config semantics rather than strict equality.
Useful? React with 👍 / 👎.
| <span class="tg-title">{{ count }} 个工具调用</span> | ||
| <span class="tg-meta">· {{ statusLabel }}</span> |
There was a problem hiding this comment.
Use locale strings for tool group header
In any non-Chinese locale, assistant turns with multiple consecutive tool calls still render this header as e.g. 3 个工具调用 (and the adjacent statusLabel is also hard-coded Chinese), because these literals bypass vue-i18n. This makes the redesigned tool stack partially Chinese regardless of the selected language; route the count/status text through the existing locale files.
Useful? React with 👍 / 👎.
| <button type="button" class="at-open" @click.stop="emit('openAgent', tool.id)"> | ||
| {{ t('tasks.openDetail') }} | ||
| </button> |
There was a problem hiding this comment.
Hide the subagent detail button when no task exists
After a refresh, foreground subagents are reconstructed only from the persisted Agent tool call, while openAgentPanel() resolves this emitted tool-call id against activeAppTasks and silently returns when no live/background task matches. In that common completed-session scenario this button is still shown but does nothing, so either hide/disable it unless a matching task exists or provide a transcript-based fallback detail view.
Useful? React with 👍 / 👎.
Hold the sidebar logo for 3 seconds to open a dialog showing the design system page. Also trim and rebalance the redesign changesets.
Nest the design-system Teleport inside the sidebar <aside> so the component has a single element root. App applies v-show to Sidebar, which needs an element root to attach to; the fragment root logged a "non-element root node" warning on every reactive update and the collapse did not take effect.
- Show the default-thinking switch as on whenever thinking is effectively enabled (enabled !== false), matching the core resolver. - Route the grouped tool-call header and status through vue-i18n. - Hide the subagent "Open detail" button when no matching task exists (e.g. a completed foreground subagent after a refresh).
oxlint eqeqeq flagged the loose != null check; resolveAgentTaskId returns string | undefined, so compare with !== undefined.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7e4c245362
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| loadInitialSessionsForWorkspace(w.id).catch(() => ({ | ||
| workspaceId: w.id, | ||
| page: { items: [] as AppSession[], hasMore: false }, |
There was a problem hiding this comment.
Keep fetched sessions when a continuation page fails
When a workspace has more than the initial 5 recent sessions, loadInitialSessionsForWorkspace now issues continuation requests; if any later request rejects after the first page was already fetched, this catch replaces the whole workspace with an empty page and hasMore: false. A transient failure on page 2 therefore hides even the sessions that were successfully loaded on page 1, whereas the previous one-page load would still show them; catch per continuation or return the accumulated items on partial failure.
Useful? React with 👍 / 👎.
| function thinkingEnabled(): boolean { | ||
| const thinking = props.config?.thinking; | ||
| if (!thinking || typeof thinking !== 'object') return false; | ||
| return (thinking as { enabled?: boolean }).enabled !== false; |
There was a problem hiding this comment.
Treat an absent thinking config as enabled
For a fresh config with no thinking section, the core resolver does not treat thinking as disabled (enabled === false is the disabling case, otherwise it falls back to the model/default effort). Returning false here makes the Settings toggle show “off” even when new sessions will think by default for thinking-capable models, and the user's first click writes enabled: true instead of disabling it; return true for an absent object (or derive the effective default) so the switch matches behavior.
Useful? React with 👍 / 👎.
Remove the design exploration mockups, screenshots, prompts, and notes that were accidentally committed under apps/kimi-web/design. Keep design-system.html, which the sidebar logo easter egg still references.
…g as on - Keep sessions already loaded from earlier pages when a continuation page fetch fails, instead of replacing the workspace with an empty page. - Treat an absent thinking config as enabled in the settings toggle, matching the core resolver (thinking is on unless explicitly disabled).
Replace the 3-second long-press trigger with 10 consecutive clicks on the Kimi mark; the count resets after a short idle. The long-press was unreliable because pointerleave cancelled the timer on any drift.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cdf41d3e1e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (swarmMembers.length > 1) { | ||
| const live = swarmMembers.some((t) => { | ||
| const phase = phaseForTask(t); | ||
| return phase !== 'completed' && phase !== 'failed'; |
There was a problem hiding this comment.
Treat cancelled swarm members as finished
When a multi-member swarm is stopped/cancelled, taskCompleted records status: 'cancelled' but does not update the task's existing subagentPhase; if that phase is still working, phaseForTask(t) here is neither completed nor failed, so live stays true and the continue suppresses the normal AgentSwarm tool card/result even after the session is done. In that cancellation path the swarm remains treated as live instead of falling through to the final result, so this check should also treat cancelled terminal tasks as finished (or map them to a terminal phase).
Useful? React with 👍 / 👎.
phaseForTask now lets a terminal status (completed/failed/cancelled) override a stale subagentPhase, so a cancelled swarm member no longer stays live and suppresses the finished AgentSwarm card.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7caa6bbf17
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| <template> | ||
| <div class="activity-notice" role="status"> | ||
| <span class="an-moon" aria-hidden="true">{{ MOON_FRAMES[moonFrame] }}</span> | ||
| <span aria-hidden="true"><MoonSpinner size="sm" /></span> |
There was a problem hiding this comment.
Use the plain spinner for activity notices
ActivityNotice is used for non-chat long-running states such as compaction, but this now renders MoonSpinner. The local apps/kimi-web/AGENTS.md rule reserves the moon spinner solely for the chat “waiting for the agent's first response” state and says every other loading state must use Spinner, so this introduces a documented design-system violation whenever compaction/activity notices render.
Useful? React with 👍 / 👎.
| <SearchSessionsDialog | ||
| v-if="showSearch" | ||
| :sessions="sessions" | ||
| :active-id="activeId" | ||
| @select="onSelectSession" |
There was a problem hiding this comment.
Let Escape close search before background panels
Because the new search modal is owned inside Sidebar, App.vue's capture-phase Escape handler does not include it in anyOverlayOpen; when a right-side detail panel is open, pressing Escape in the search dialog closes the background panel and stops propagation before Dialog can close the search. This affects the common “open search while inspecting a detail panel, then press Escape” flow, leaving the modal open and unexpectedly discarding the panel state instead.
Useful? React with 👍 / 👎.
Switch the logo easter egg back to a long-press, shortened to 1 second, and make it reliable this time: use pointer capture plus touch-action:none so a slight drift no longer cancels the hold.
ActivityNotice renders for non-chat loading states (e.g. compaction), so it must use Spinner per the design-system rule that reserves MoonSpinner for the chat first-response state.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 804cd17027
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| <div class="search-wrap"> | ||
| <input | ||
| ref="searchRef" | ||
| <Input |
There was a problem hiding this comment.
Restore focus to the model search input
When the model picker opens, this replacement <Input> is no longer connected to the searchRef passed to useDialogFocus, so the shared Dialog focuses the first focusable control in its header (the close button) instead of the search field. In that state, typing does not filter models until the user clicks the field, and pressing Enter can trigger the document-level model selection handler while focus is still on the close button.
Useful? React with 👍 / 👎.
| } | ||
| .ui-panel-header__title { | ||
| flex: none; | ||
| font: var(--weight-bold) var(--text-xs) var(--font-mono); |
There was a problem hiding this comment.
Use a defined weight token for panel headers
--weight-bold is not declared in src/style.css (the defined weight tokens are regular/medium/semibold), so this font shorthand is invalid and panel header titles fall back to inherited typography across the file, diff, thinking, agent, and tool-diff panels. Use an existing token such as --weight-semibold or add the token before referencing it.
Useful? React with 👍 / 👎.
- Bind the model picker's search Input to searchRef so useDialogFocus moves focus into it on open instead of the dialog's close button. - Use the defined --weight-semibold token for panel header titles (--weight-bold is not declared, so the shorthand was invalid).
Track open design-system Dialog instances in a shared count and include it in App.vue's anyOverlayOpen, so a dialog whose open state lives outside App.vue (such as the sidebar session search) captures Escape before the background side panel closes.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 82b26d1a72
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const subagentTasks = computed(() => | ||
| props.tasks.filter((t) => t.kind === 'subagent' && t.runInBackground), |
There was a problem hiding this comment.
Exclude foreground subagents from the dock trigger too
With this filter, foreground Agent subagents (runInBackground is false by default) no longer render as dock pills, but hasDockWork still becomes true from props.tasks.length. In a session whose only task is a normal foreground subagent, ChatDock receives hasDockWork=true while bashTasks, subagentTasks, todos, and queue are all empty, so it renders an empty workbar above the composer. Base the dock-work flag on the filtered dock task lists instead.
Useful? React with 👍 / 👎.
Foreground subagents are excluded from the dock task lists, so a session whose only task is a foreground subagent no longer renders an empty workbar above the composer.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bed63830f6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| return [ | ||
| { | ||
| type: 'taskProgress', | ||
| sessionId, | ||
| taskId: subagentId, |
There was a problem hiding this comment.
Create a subagent task before forwarding text deltas
When the client subscribes from a snapshot after subagent.spawned has already passed, this branch emits only taskProgress. The reducer only updates existing tasks, and foreground subagents are not returned by /tasks, so these assistant deltas are dropped and the live subagent detail stays missing until a later non-text progress/lifecycle frame creates the task. Mirror the tool-progress path by patching/emitting taskCreated before the text progress.
Useful? React with 👍 / 👎.
A client that subscribes from a snapshot after subagent.spawned already fired never received the lifecycle taskCreated; the reducer only applies taskProgress to existing tasks, so assistant text deltas were dropped and the live subagent detail stayed blank. Emit taskCreated (via patchSubagent) before the text progress, mirroring the tool-progress path.
Plan, swarm, and goal modes were stored as global scalars on the web client and a single localStorage key each, so they leaked across sessions. Bind them to the active session via per-session maps, persist per session, and apply server status/events to the originating session so background sessions keep independent state.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 46f177d26a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const task = patchSubagent(state, sessionId, subagentId, { | ||
| status: 'running', | ||
| subagentPhase: 'working', | ||
| startedAt: previous?.startedAt ?? new Date().toISOString(), | ||
| }); |
There was a problem hiding this comment.
Preserve parent tool-call metadata for synthesized subagent tasks
In the snapshot-after-subagent.spawned path described above, this synthesizes an AppTask from the first subagent assistant.delta without the parentToolCallId that the missed spawn event would have provided. AgentTool only exposes its Open-detail action when the spawning tool id matches either task.id or task.parentToolCallId, so with normal ids like agent-0 vs call_agent, a foreground subagent that was already spawned before the web client subscribed accumulates streamed text but has no visible entry point to the detail panel. Preserve/recover that parent tool-call mapping for synthesized tasks, or avoid hiding the entry point in this case.
Useful? React with 👍 / 👎.
When the web client subscribes after a subagent already spawned, the synthesized subagent task has no parentToolCallId, so the Agent tool's Open-detail button was hidden and the panel would not open. Fall back to the single unmapped subagent task when resolving the detail target in both the button visibility and the panel open paths.
Teleport the SessionRow kebab menu to body and anchor it with fixed positioning so the collapsing group-sessions list's overflow:hidden no longer clips the dropdown.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2188a5a677
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (draftModes.planMode) setPlanMode(true); | ||
| if (draftModes.swarmMode) setSwarmMode(true); | ||
| if (draftModes.goalMode) setGoalMode(true); |
There was a problem hiding this comment.
Apply staged modes to the created session
When a user starts a first prompt with draft plan/swarm/goal enabled and switches to another session while selectSession(session.id) is awaiting the snapshot, these setters read the then-current rawState.activeSessionId instead of the just-created session.id. The prompt is still submitted to session.id, and submitPromptInternal reads modes from that session's per-session maps, so the first prompt can be sent without the staged modes while the other session is polluted with them.
Useful? React with 👍 / 👎.
When starting the first prompt, apply the staged plan/swarm/goal modes to the just-created session's per-session maps by id instead of via the activeSessionId-based setters, so a session switch during the selectSession await can't drop the modes for this session or pollute another.
|
❌ Nix build failed |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 23ed23428f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| import { getKimiWebApi } from '../../api'; | ||
| import { i18n } from '../../i18n'; | ||
| import { useConfirmDialog } from '../useConfirmDialog'; |
There was a problem hiding this comment.
Add the missing confirm dialog module
This new import targets ../useConfirmDialog, but there is no matching module anywhere under apps/kimi-web/src (repo-wide file search only finds this import). Because useWorkspaceState is part of the main client bundle, the web app cannot resolve this module during build/typecheck, so the redesigned UI will fail to start until the composable is added or the code falls back to an existing confirmation path.
Useful? React with 👍 / 👎.
Replace the inconsistent confirmation patterns (native confirm(), two-step menu arming, hand-rolled inline strips, bare buttons) with one modal ConfirmDialog driven by a global useConfirmDialog() composable, and consolidate the duplicated confirm/cancel i18n keys.
- Send button always sends/enqueues; interrupt moves to a separate red Stop button shown only while running, so the two can no longer be confused. - Queued prompts now render inline at the tail of the transcript (after the running turn) instead of behind the dock panel: click to edit, remove, drag the grip to reorder, with image thumbnails and a "next up" marker. - Remove the dock queue panel and the QueuePane component; Steer stays on Ctrl/Cmd+S.
…lish - Fix spurious errors when question/approval/task actions were already complete - Add loading feedback to question and approval prompts; block double-clicks - Make the question "Other" option selectable by row click and let Enter advance/submit - Consolidate workspace section actions into an overflow menu - Tighten markdown prose line-height and block spacing - Recall input history only when the caret is at text start
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
Related Issue
No linked issue — see Problem below.
Problem
The web UI had grown without a unified design system, so visuals were inconsistent across the sidebar, chat, dialogs, and settings, and a few common workflows were missing or rough: no session search, no workspace ordering, basic tool-call rendering, and no motion on state changes.
What changed
Redesigns the web UI on a new token-based design system and fills in the missing workflows:
Cmd/Ctrl+Ksession search dialog with highlighted matches.Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.