Commit graph

124 commits

Author SHA1 Message Date
qer
ec8dc3456c
fix(web): stop sending prompts into a busy turn on the web UI (#1522)
* fix(web): prevent duplicate first prompts and keep goal drives from looking idle

- Guard startSessionAndSendPrompt with a per-workspace reentry lock so a
  double-click / repeated Enter during draft-session creation cannot fire
  two concurrent first prompts into the same new session.
- Track goal.active in the agent event projector so turn.ended between
  goal-driven continuation turns keeps the session 'running' instead of
  projecting a false 'idle' that drains the local queue into a still-busy
  core (turn.agent_busy).
- Show a 'starting conversation…' loading state on the empty-session
  landing while the first prompt is being created and submitted.
- Persist the resolved model in startSessionAndActivateSkill so the first
  skill turn on a fresh session does not fail with 'Model not set'.

* chore: add changeset for web first-prompt fixes

* fix(web): close remaining first-prompt and goal-settle gaps

- Pass the starting guard through the dock composer: draft-session
  creation selects the new session before submit, which swaps the empty
  composer for the dock; disabling both composers closes the last path
  to a concurrent first POST. Also take the workspace lock in
  startSessionAndActivateSkill / startSessionAndOpenSideChat.
- Emit the owed idle when a goal settles (blocked/paused/completed) in
  the inter-turn gap after a turn.ended was projected as 'running', so
  sending state, in-flight flags and queued prompts flush instead of
  the session staying 'running' forever.

* style(web): fix eqeqeq lint error in first-prompt guard

* fix(web): clear owed idle when a new goal turn starts

The idle debt from a 'running' projection survived turn.started, so an
UpdateGoal('complete'|'blocked') landing mid-turn in the NEXT goal turn
synthesized an early idle. onSessionIdle could then drain queued prompts
into a core that was mid-turn again, re-opening the turn.agent_busy race
for multi-turn goals. Clear the debt on turn.started: from that point the
turn's own turn.ended carries the idle with goalActive already false.

* fix(web): make first-prompt starting state workspace-id-agnostic

isStartingFirstPrompt now reads from the lock set directly (size > 0)
instead of the current activeWorkspaceId. createDraftSession can swap
activeWorkspaceId to a registered id mid-flight; a workspace-keyed read
would then return false while the first prompt is still in the create/
select/submit window, re-enabling the composer and reopening the
duplicate first-submit race.

* revert(web): drop goal-aware idle projection from agentEventProjector

The goalActive / idleOwed shadow state machine grew through multiple
review rounds and still leaves edge cases (snapshot-seeded turns, mid-
turn goal updates). Roll it back to the simple 'turn.ended projects idle'
behavior. Goal-driven sessions can once again race a queued prompt into a
busy core; this is accepted as a known limitation to be resolved properly
in a follow-up that has the core emit an authoritative idle signal.

* chore: align changeset with actual fix scope

* test(web): update profile-patch expectation for model field
2026-07-09 23:37:10 +08:00
liruifengv
170ae44205
style(web): polish the session sidebar (#1519)
* feat(web): use sidebar fold/unfold icons for sidebar toggle

* feat(web): move settings entry to a sidebar footer row

* feat(web): fully collapse sidebar with animated width transition

* feat(web): redesign sidebar colors, spacing and macos desktop chrome

* feat(desktop): center traffic lights on the 48px header row

* fix(web): restore webkit thin scrollbars and unify sidebar icon sizes

* feat(web): add Kbd keycap component and justify sidebar search shortcut

* style(web): rework sidebar palette and pin a resident sidebar toggle

* fix(desktop): sync window appearance with web UI theme so dimmed traffic lights stay visible

* feat(web): adopt Kimi design icons in the sidebar via a local icon collection

* style(web): mute workspace group title color in the sidebar

* style(web): refine sidebar typography, unify shortcut keycaps, float workspace row actions

* style(web): cap sidebar draggable width at 480px

* style(web): derive sidebar row height from type and padding, float the kebab

* chore: add changeset for sidebar UI polish

* fix(nix): update pnpmDeps hash

* style(web): put the sidebar collapse button inside the header on non-mac

* fix(nix): update pnpmDeps hash
2026-07-09 19:39:20 +08:00
Luyu Cheng
ad30a1c632
style(web): polish web UI typography and controls (#1502)
* style(web): polish sidebar and tool typography

- Use UI font and medium weight for sidebar and composer controls

- Add reusable shortcut and tool output blocks

- Cap long tool output at 50 lines with a scrollbar

- Update sidebar show-more copy and muted styling

* style(web): refine workspace picker sizing

* style(web): align composer mode menus

* style(web): tune list and question typography

* style(web): reuse shortcut keys in approvals

* style(web): size workspace picker from content

* feat(web): localize chat status labels

* style(web): refine composer toolbar controls

* style(web): use complete Inter variable font

* style(web): tune sidebar workspace typography

* style(web): polish composer and workspace picker

* style(web): refine markdown and thinking typography

* chore: add web UI polish changeset

* fix(web): pin Inter package for Nix build

* style(web): polish goal tool calls

* style: polish goal mode display

* fix: layer latest message pill below menus

* style: align goal strip content
2026-07-09 14:55:58 +08:00
liruifengv
735922c291
feat(kimi-web): add status-aware browser notifications (#1479)
* feat(kimi-web): add approval notification storage key and i18n copy

* feat(kimi-web): add approval notification helpers and tests

* feat(kimi-web): wire approval notifications and guard completion alerts

* fix(kimi-web): extract shouldNotifyCompletion helper and add tests

* feat(kimi-web): add approval notification settings toggle

* chore(kimi-web): add changeset and tidy notification module comment

- Align approval notification tag with spec (kimi-approval-${approvalId})

- Update module header to describe all three notification kinds

* fix(kimi-web): make notifications fire reliably

- Key completion notification tags by turn (sid + promptId) and question
  tags by request id, so a stale notification left in the notification
  center no longer swallows every follow-up alert in the same session
- Suppress notifications only while the window is actually focused, not
  merely visible (document.hasFocus() on top of visibilityState)
- Play the attention sound when a tool needs approval, matching the
  completion and question sounds

* chore(kimi-web): simplify changeset
2026-07-09 12:30:15 +08:00
qer
b0809ddac8
feat(web): prefix skill slash commands with skill: to distinguish them from built-in commands (#1492) 2026-07-08 15:03:30 +08:00
qer
0cc9831a2f
fix(web): composer model switch also updates global default model (#1491)
* fix(web): composer model switch also updates global default model

The composer model switcher still switches the active session's model via
POST /sessions/{id}/profile (awaited, so the model pill reflects the result),
and additionally fires POST /api/v1/config with { default_model } as a
fire-and-forget side effect so new sessions inherit the chosen default. The
config request is skipped when the model already matches the current default.

* fix(web): route ModelPicker overlay selection through the default-model update

The overlay opened from the composer's "More models" row (and /model) is a
continuation of the same switch flow, so its selection now also bumps the
global default model instead of only switching the active session.

* fix(web): only persist the default model after a confirmed session switch

setModel now returns whether the switch was accepted (true for the draft
path), so the composer flow no longer writes a stale or invalid model alias
into the global config when the session-level switch failed and rolled back.
2026-07-08 14:53:29 +08:00
liruifengv
2ad0120c2a
feat(web): redesign cron reminder as a message bubble (#1480)
* feat(web): redesign cron reminder as a message bubble

Restyle the cron trigger notice as a right-aligned user-style message bubble that shows the scheduled prompt in full (wrapping across lines), with a small meta row beneath it for the schedule, status, job id and run time. Extract a shared MessageTime component used by both user messages and the cron reminder so the timestamp format and click-to-expand behavior stay consistent, and give the CronCreate/CronList/CronDelete tools distinct calendar icons.

* refactor(web): render cron reminders only as standalone turns

Remove the embedded cron block path from the web transcript projector so cron reminder fires always render through the standalone right-aligned bubble path.

* chore(web): simplify cron redesign changeset
2026-07-08 13:42:22 +08:00
qer
b30a45efec
feat(web): support Enter key to confirm archive and other dialogs (#1490) 2026-07-08 13:24:44 +08:00
qer
11c6a37ce0
fix(web): dismiss stale connection error toast after ws reconnect (#1474) 2026-07-07 20:47:56 +08:00
liruifengv
ee385456d0
refactor(web): migrate icons to unplugin-icons (#1467)
* refactor(web): migrate icons to unplugin-icons

Replace the hand-written gen-icon-data.mjs + @iconify/utils runtime
rendering with unplugin-icons build-time imports. The public API
(<Icon name>, iconSvg, IconName, SIZE_PX, NAME_TO_REMIX) is unchanged;
127+ call sites are untouched.

- add unplugin-icons@^23.0.0 (devDep) + Vite Icons() plugin (compiler: vue3)
- rewrite src/lib/icons.ts: static ~icons/ri/* imports (component + ?raw)
  for 56 distinct Remix icons across 59 IconName entries
- Icon.vue renders <component :is> with unknown-name fallback
- append ICON_GROUPS export for DesignSystemView catalog
- DesignSystemView: v-for catalog, remove legacy-script references
- delete gen-icon-data.mjs, gen-icon-catalog.mjs, icon-data.ts,
  gen:icons script
- remove @iconify/vue and @iconify/utils from dependencies; move
  @iconify-json/ri to devDependencies
- drop Icon.vue from check-style ICON_EXEMPT (no hand-written <svg>)

* refactor(web): drop unused NAME_TO_REMIX icon mapping

NAME_TO_REMIX was a Record<IconName, string> table introduced to map
internal icon names to their ri: ids. After the unplugin-icons migration
it has no production consumers — only icons.test.ts imported it (for two
drift tests) and DesignSystemView mentioned it in descriptive copy. The
ICONS table already conveys the same ri: id via each entry's paired
component + ?raw imports (e.g. RiFolderOpenLine / RawFolderOpenLine).

- remove NAME_TO_REMIX const from src/lib/icons.ts (-63 lines)
- remove NAME_TO_REMIX import + describe block from icons.test.ts
- update DesignSystemView §02 copy: describe the import-pair idiom and
  stop claiming ICON_GROUPS is sourced from NAME_TO_REMIX

* chore: add changeset for web icon migration

* chore(nix): bump pnpmDeps hash for unplugin-icons

Adding unplugin-icons changed pnpm-lock.yaml, so the fixed-output
pnpmDeps derivation hash is stale. Update to the hash reported by the
Nix Build CI run.
2026-07-07 16:44:56 +08:00
qer
16dc940834
fix(web): recover from stale background websocket (#1451) 2026-07-07 14:48:22 +08:00
qer
809a88cb34
fix(web): run new-session slash actions (skills, /goal, /btw) from the empty composer (#1445)
* fix(web): activate slash skills on the new-session screen

The composer now lists workspace skills before a session exists, but
activating one from the empty-session composer silently did nothing:
activateSkill() short-circuits when there is no active session id, so
the command was cleared with no turn started.

Mirror the first-prompt path: when a slash skill is activated with no
active session (but a workspace is active), create the session first and
activate on the new session id. Extract the shared session-creation
block out of startSessionAndSendPrompt into createDraftSession and add a
startSessionAndActivateSkill counterpart; activateSkill now also accepts
an explicit session id so a concurrent session switch can't redirect it.

Unrecognized '/cmd' text still falls through to a plain-text message as
before.

* fix(web): persist draft modes when a new session is opened via skill

The skill-activate request carries only `args`, so for a skill launched
from the new-session composer createDraftSession's local plan/swarm maps
never reached the daemon, and the first skill turn ran at default modes
while the UI showed them enabled.

Persist the draft plan/swarm modes to the new session's profile inside
createDraftSession (by the new session id), and teach persistSessionProfile
to take an explicit session id so a concurrent session switch during the
snapshot load can't write the patch to the wrong session. Plain prompts
already send planMode/swarmMode on the prompt request itself, so this is
redundant but harmless on the first-prompt path. Goal mode is a one-shot
flag consumed per send, not a profile field, so there is nothing to
persist for it.

Add coverage that the profile is persisted before activation when draft
plan/swarm modes are enabled.

* fix(web): await draft-mode profile POST before activating a skill

The previous fix wrote the draft plan/swarm modes to the new session's
profile but fire-and-forget: persistSessionProfile returned immediately
after starting POST /profile, so startSessionAndActivateSkill sent
:activate without waiting. Since skill activation carries only args, the
daemon could process :activate (and start the turn) before
applyAgentState from /profile finished, running the first skill turn at
default modes while the UI showed them enabled.

Make persistSessionProfile return the update promise and await it inside
createDraftSession, so any origin that follows (the skill activation, or
a plain prompt) only starts after the profile is applied. Existing
callers still fire-and-forget via `void persistSessionProfile(...)`.

Test coverage now blocks activation behind a deferred profile POST and
asserts it is issued only after the profile resolves.

* refactor(web): persist draft modes on the skill path only

Move the awaited plan/swarm profile write out of createDraftSession and
into startSessionAndActivateSkill. The create path now only builds the
session and mirrors draft modes into the per-session maps, matching the
old first-prompt behavior: plain prompts already send planMode/swarmMode
on the prompt request itself, so a /profile write there was redundant.

The profile write stays on the one path that needs it: skill activation
carries only args, so the draft modes must be stored on the new session
and applied before :activate is sent.

* fix(web): persist draft permission and thinking for new-session skill

/auto, /yolo, and /thinking on the new-session composer only update
rawState (there is no session to persist to yet). A plain first prompt
still honors them because submitPromptInternal sends permissionMode and
thinking on the request, but skill activation carries only args, so the
first skill turn was running at daemon defaults.

Include permissionMode and thinking in the awaited profile patch alongside
planMode/swarmMode before activating, so the skill turn matches the
controls the UI shows.

* fix(web): start /goal from the new-session composer

createGoal() short-circuited when there was no active session, so
`/goal <objective>` from the empty-session composer silently cleared and
ran nothing — the same bug class as the slash-skill activation fixed
earlier in this PR.

Mirror startSessionAndSendPrompt: when no session exists but a workspace
is active, create one first then target it with the goal profile update
and the objective prompt. Send via submitPromptInternal with the explicit
sid so a concurrent session switch during creation can't redirect it.
Plain prompts already carry their own permissionMode / thinking / plan /
swarm, so no profile fallback is needed here.

* fix(web): use active-workspace fallback for empty-composer /goal

On a fresh-booted empty workspace, load() never writes
rawState.activeWorkspaceId (no most-recent session to anchor it). The
UI still has a usable workspace via the client-wide activeWorkspaceId
computed, which falls back to the first sidebar-visible workspace — but
createGoal read the raw value directly and silently no-op'd when it was
null.

Normal first prompts and skill activations didn't hit this because
App.vue passes the computed activeWorkspaceId in. Make createGoal use
the same fallback so a first-session `/goal <objective>` works in empty
workspaces too.

* fix(web): start /btw from the new-session composer

openSideChat() reads rawState.activeSessionId directly, so `/btw
[<question>]` from the empty-session composer silently no-oped — it
still set detailTarget to 'btw', leaving the side chat panel open but
empty.

Add startSessionAndOpenSideChat(workspaceId, prompt?) that creates the
parent session first, then calls a new sideChat.openSideChatOn(sid,
prompt) which targets the explicit parent session id (race-safe against
a concurrent session switch, like the skill activation case). Route
through it from the empty-composer branch in openSideChatTab.

Side-chat prompts now also carry model / thinking / permissionMode /
plan / swarm (via a shared sendSideChatPromptOn), so a BTW first turn
matches the UI even when the parent /profile is still in flight. Unlike
skill activation, this means the BTW path needs no profile fallback.

Tests: startSessionAndOpenSideChat creates a session then opens BTW on
the new id, works without an initial question, and is a no-op for an
unknown workspace; sendSideChatPromptOn carries the runtime controls
on the submitted prompt.

* fix(web): preserve send queue when creating goals

Switching createGoal from sendPrompt to submitPromptInternal avoided the
activeSessionId race during the empty-composer create window, but it also
bypassed sendPrompt's queue guard: when a goal is created against an
already-active session that is running another turn, the prompt posted
immediately instead of being locally queued.

Restore the guard for the overwhelmingly common case (the goal still
targets the active session): route through sendPrompt when activeSessionId
still matches the resolved sid, which enqueues when the session is
running or a prompt is already in flight. Only fall back to the explicit-
session submitPromptInternal(sid) when activeSessionId moved during the
create window, so a concurrent session switch can't redirect the goal
prompt. The newly-created session is idle+not-in-flight in that branch,
so the explicit submit does not race another turn.

Add a regression test: createGoal against a running existing session
enqueues instead of submits.

* fix(web): coerce thinking for skill/BTW and handle goal creation failures

Three follow-ups from review:

- createGoal now wraps createDraftSession in a try/catch: App.vue invokes
  it fire-and-forget, so a rejection from session creation previously
  leaked as an unhandled rejection with no operation failure surfaced.
  Mirrors the skill / BTW / first-prompt paths that already wrap it.

- startSessionAndActivateSkill coerces the draft thinking level against
  the new session's model before persisting the profile (via
  coercePromptThinking), matching what the first-prompt path submits.
  A value carried over from another/default model (e.g. 'max' from an
  effort model) would otherwise be persisted verbatim and the first
  skill turn would run at a level the UI wouldn't send for this model.

- sendSideChatPromptOn coerces thinking against the parent session's
  model the same way normal prompts do (coerceThinkingForModel against
  the model catalog). Model catalog threaded in via a new 'models' dep
  on useSideChat. Same reasoning: stale rawState.thinking must not be
  submitted raw into a BTW first turn.

* fix(web): clear staged goal mode when submitting explicit /goal

When the empty composer has goal mode staged (e.g. the user runs bare
`/goal`, then `/goal <objective>`) and an explicit objective is then
submitted, createDraftSession copies draftModes.goalMode into
goalModeBySession[sid]. createGoal then updateSession(goalObjective) for
the explicit goal, and sendPrompt(trimmed) re-enters submitPromptInternal
which sees goalModeBySession[sid] still set and POSTs a second
goalObjective. The daemon rejects that as an existing goal, the catch in
submitPromptInternal rolls back the optimistic message, and the user's
objective prompt never lands.

Clear the staged goalModeBySession[sid] flag right after the explicit
update, since `/goal <objective>` has exactly the same effect as the
flag's consumption.
2026-07-07 14:18:40 +08:00
qer
9b18dc46cf
fix(kimi-web): shrink collapsed thinking block on mobile (#1461) 2026-07-07 14:04:00 +08:00
qer
903e8ed93a
fix(web): clarify archive session confirmation copy (#1428)
* fix(web): clarify archive session confirmation copy

* fix(web): drop delete mention from archive confirmation copy

* feat(web): add archived-sessions restore entry to mobile settings

Mobile used a separate settings bottom-sheet with no archived-sessions
restore surface, so the archive confirmation pointing users to Settings
to restore was untrue on mobile. Add an Archived sessions sub-view to
the mobile settings sheet with search, sort and restore, mirroring the
desktop Settings tab.

* fix(web): refresh mobile archived list and use Input primitive

Refresh the archived-sessions list each time the mobile sub-view opens so
sessions archived from the mobile switcher show up without remounting the
sheet, and replace the hand-rolled search input plus inline svg with the
shared Input primitive.

* fix(web): use Button primitive for mobile archived restore

Render the mobile Archived sessions restore action with the shared Button
primitive instead of a hand-rolled button and scoped CSS, matching the
desktop archived list and the app's design-system rule.
2026-07-06 22:48:01 +08:00
liruifengv
2374bc41c3
feat(kimi-web): render cron fire notices in the web chat (#1426)
* feat(kimi-web): render cron fire notices as in-transcript cards

Show scheduled-reminder fires as distinct notice cards in the web chat,
both live and after a reload, instead of hiding them. Each card carries
a humanized schedule, a dimmed job id, and a collapsible prompt body.

* fix(kimi-web): isolate cron notice prompt id and route prefixed events

- Give synthesized cron messages a fresh promptId so a fire mid-turn
  cannot be reconciled into the optimistic user echo and hide it.
- Add cron.fired to KNOWN_AGENT_CORE_TYPES so event.-prefixed frames
  reach the projector and render live too.

* fix(kimi-web): truncate long one-line cron prompts when collapsed

Slice the first line to the collapse limit with an ellipsis when a
single-line prompt exceeds it, so the collapsed state actually truncates
and the expand toggle is not a no-op for common one-line reminders.

* fix(kimi-web): keep cron notices from reconciling into optimistic user echoes

Skip optimistic-echo reconciliation for user messages whose origin is
cron_job or cron_missed: their prompt text can coincide with a still-
optimistic user message, and the loose content match would otherwise
replace the user's turn with the cron notice instead of appending.

* fix(kimi-web): keep in-turn cron injections from breaking tool results

A cron injection steered into an active turn lands inside that turn's
message sequence, between a tool use and its result. Treating it as a
hard user-turn boundary flushed the pending assistant group, so the next
tool result had no group to fold into and the tool rendered without
output. Embed such in-turn cron notices as a block inside the assistant
group instead, and only render a cron at a turn boundary as its own turn.

* fix(kimi-web): only embed cron notices while a tool is in flight

Embedding whenever a group was pending was too broad: on REST snapshots
without prompt ids the whole transcript shares one group, so an idle cron
fire merged into the previous assistant answer and its own reply kept
going in that group. Embed only while the group has a running tool (a
cron sandwiched between a tool use and its result); flush to its own
turn otherwise.

* fix(kimi-web): omit synthetic prompt id on cron notices

The synthesized cron message carried a cron_pr_ promptId that the web
client caches into promptIdBySession for Stop/abort. Because it is not a
real daemon prompt id, it clobbered the active promptId, so Stop first
aborted a nonexistent prompt and only recovered via the error fallback.
Omit the promptId; the reducer already skips optimistic-echo
reconciliation for cron-origin messages, so it is not needed for de-dup.
2026-07-06 22:25:19 +08:00
liruifengv
ac5b5e4cbf
fix(kimi-web): keep tool components from jumping on expand or collapse (#1433)
* fix(kimi-web): keep tool components from jumping on expand or collapse

Also show the scroll-to-bottom button whenever scrolled up, and render a
fallback icon for tools without a dedicated glyph.

* fix(kimi-web): preserve bottom follow during content-only resizes

Late-loading media can grow after scrollKey has run; keep chasing the
bottom on content growth, and only suppress follow during the pinned
expand/collapse window.
2026-07-06 22:19:02 +08:00
qer
d86fa38e11
fix(kimi-web): disable text hyphenation and code ligatures (#1438)
Set hyphens: none (with the -webkit- prefix) on body so chat and markdown text never gain a hyphen glyph at a line break; components still control where lines wrap via word-break/overflow-wrap.

Disable the ligatures that coding fonts enable by default (liga/calt/ss01) on native code elements so code renders literally, e.g. != stays as two characters.
2026-07-06 22:12:43 +08:00
qer
0824e7b668
chore: ignore and remove throwaway scratch files (#1439)
Agent working notes (HANDOVER/handoff) and one-off UI prototype HTML files were committed by mistake. Remove the already-tracked ones, add .gitignore patterns for these classes of files and a .tmp/ scratch dir, and document the rule plus a pre-commit self-check in AGENTS.md so future mistakes are caught mechanically.

No publishable package is affected, so this PR needs no changeset.
2026-07-06 22:12:14 +08:00
qer
c5e3e80041
feat(web): render AgentSwarm as an inline tool card (#1425)
* feat(web): render AgentSwarm as an inline tool card

Replace the bottom SwarmCard footer and the messagesToTurns live-skip
with one dedicated inline tool card for AgentSwarm. The card shows a
phase overview plus a per-subagent accordion: live progress while it
runs, parsed aggregated result once it completes (and after a refresh
that has already dropped the live tasks).

Refresh and resync keep member identity metadata (swarmIndex,
parentToolCallId, subagentType, runInBackground) stable across skeleton
task replacement in the reducer, and the .content-wrap flex layout is
hardened against the overflow compression that previously displaced the
footer.

* fix(web): handle swarm review feedback

- SwarmTool: when AgentSwarm fails before producing a structured
  agent_swarm_result (e.g. argument validation), render the raw tool
  output instead of the "waiting for subagents" placeholder so the
  failure cause is visible.
- resolveSwarmMembers: source live members from the AppTask store keyed
  by parentToolCallId rather than buildSwarmGroups, which filters out
  single-member groups. A resume-only AgentSwarm now streams its live
  progress before the final result arrives. The badge counter still
  relies on buildSwarmGroups's filter.

* fix(web): carry streamed subagent text into swarm rows

Swarm subagents that stream normal assistant output accumulate it on
AppTask.text (text-kind taskProgress), not outputLines. The new live
member map was dropping `text`, so a still-composing subagent rendered
an empty / stale row until the structured result arrived.

- Add `text` to SwarmMember and thread it through buildSwarmGroups and
  swarmMembersByToolCall.
- SwarmTool: prefer member.text for both the row activity preview and
  the expanded body; fall back to outputLines / summary.
- Tests cover text propagation through both helpers.

* fix(web): merge swarm result rows and fall back to raw output

Address the two latest swarm review comments:

- Rows: when a parsed agent_swarm_result coexists with live AppTasks
  (which the detail panel also depends on), the inline card previously
  only rendered the live members. Interrupted swarms can carry
  state="not_started" / outcome="aborted" result entries for items that
  never spawned a task; those rows were dropped until a refresh cleared
  the live tasks. Extract the row model into buildSwarmCardRows and
  merge result-only aborted/not-started rows with the live member rows.
- Fallback: when the tool is no longer running but produced no
  structured result (argument validation, parser miss, or legacy
  legacy transcript), render the raw tool output instead of
  "Waiting for subagents…" so the final text / failure cause is
  visible to the user.

* fix(web): parse swarm result subagent bodies defensively

Producer writes subagent body unescaped, so a subagent that analyzes or
emits an AgentSwarm snippet can include a literal "</subagent>" inside
its body. The non-greedy regex treated that as the row close and truncated
the body in the result-only path (post-refresh where the AppTask store is
gone).

Rewrite the parser to scan opening tags, then resolve each row's body as
everything up to the last "</subagent>" before the next row's opening tag
(or document end), preserving embedded close-tag strings. Add tests for a
literal "</subagent>" within a single body and across sibling rows.

* fix(web): only count top-level subagent result tags

A subagent body that contains a literal `<subagent ...>` tag — for
example emitting an AgentSwarm/XML snippet — was being pre-collected as
another result row, splitting the real body and producing duplicate /
bogus subagents after refresh where the AppTask store is gone.

Rewrite parseSubagents with a depth-tracking tokenizer: scan every
`<subagent ...>` / `</subagent>` token in order, push a real row frame
only at the outermost level, and treat openings / closings while nested
inside another body as body text. Drop the now-inaccurate "literal
</subagent> without matching open" regression tests; replace with tests
that verify a balanced nested snippet stays inside the parent body and
does not register as a separate row.
2026-07-06 22:05:05 +08:00
qer
a5fbcb75b4
fix(kimi-web): keep composer toolbar usable on narrow windows (#1436)
The desktop composer toolbar renders every control on one row and relies on
overflow:hidden to fit, so between the mobile breakpoint and a wide window it
clipped its own content. Shed secondary ink below 980px (the context readout
moves into the ring tooltip, the model name truncates earlier, permission is
capped) and keep the context ring visible on phones too, instead of sending it
to the settings sheet.
2026-07-06 21:54:38 +08:00
qer
578f7d334c
fix(web): reconcile session from snapshot on reopen (#1409)
* fix(web): reconcile session from snapshot on reopen

* fix(web): discard stale snapshot when a newer prompt races reopen

* fix(web): harden reopen snapshot against first-open and optimistic-send races

* fix(web): keep evicted reopens subscribed when a snapshot races

* fix(web): let resync snapshots bypass the reopen staleness guard

* fix(web): force-apply the snapshot after an undo

* fix(web): gate session reopen on durable seq instead of updatedAt

* refactor(web): always rebuild reopened sessions from a snapshot

* fix(web): sharpen reopen snapshot discard and skip rebuilds mid-stream

* refactor(web): unconditionally apply session snapshots, drop the staleness guard

* fix(web): preserve loaded older messages when reopen snapshots apply
2026-07-06 21:30:25 +08:00
qer
4aacddc432
fix(kimi-web): clarify desktop notification title and icon (#1434) 2026-07-06 21:18:45 +08:00
Haozhe
6c0ce09414
feat(server): support restoring and listing archived sessions (#1073)
* feat(server): support restoring and listing archived sessions

- add a `:restore` session action that clears the archived flag in state.json and returns the restored session
- add an `archived_only` list query param, mutually exclusive with `include_archive`, that post-filters to archived sessions
- keep the implementation in the server layer as a temporary measure until agent-core exposes restore natively

* fix(server): paginate archived-only sessions before response

* feat(web): add archived sessions page in Settings

Browse, search, filter by workspace, sort, and restore archived sessions
from a new Archived tab in Settings, backed by the server archived_only
list and :restore action.

* fix(web): keep archived Load more visible when a page filters to empty

When a search or workspace filter empties the loaded archived page, the
Load more button was hidden inside the non-empty branch, so users could
not fetch older pages to find a match. Move the button out so it stays
available whenever more archived pages exist.

* fix(server): preserve after_id bound while draining archived pages

Draining an archived_only request that starts from after_id would switch
to before_id and cross the pivot, reintroducing the pivot and older
sessions. Take a single filtered page for after_id instead of draining
past the lower bound.

* fix(server): drain archived_only within the after_id bound

An archived_only request starting from after_id now keeps paging toward
older sessions until it reaches the pivot, instead of treating the first
page as exhaustive. The loop stops as soon as it encounters the pivot
session itself, so it never reintroduces the pivot or anything older.

* feat(web): drain all archived pages for global search and sort

When the user searches, sorts, or changes the workspace filter in the
Archived settings page, fetch every remaining archived page first so the
client-side filter and sort run over the full set rather than only the
pages loaded so far.

* refactor(web): load all archived sessions upfront instead of paginating

Fetch every archived session once when the Archived settings tab opens and
drop frontend pagination entirely. Search, sort and workspace filter now run
over the full set, removing the empty-page and cursor bookkeeping that
previously caused bugs.

---------

Co-authored-by: qer <wbxl2000@outlook.com>
2026-07-06 20:09:45 +08:00
liruifengv
fa6d198b01
fix(kimi-web): fix input caret and todo strikethrough (#1423)
* fix(kimi-web): keep composer caret visible when input is empty

The composer textarea coloured its empty state with `--faint`, and with no caret-color set the caret inherited that faint colour and nearly vanished until the first character was typed. Pin the caret to --color-text so it stays readable regardless of the placeholder state.

* fix(kimi-web): use currentColor for done todo strikethrough

TodoCard pinned the done-state strikethrough to --color-line-strong, which is lighter than the faint text in light theme and darker in dark theme, so the line looked washed-out and broken against the text. Drop the override so the line inherits the text colour, matching TasksPane and the design-system examples.

* fix(kimi-web): do not reserve width for hidden workspace row actions

The workspace header's more/add buttons were hidden with opacity: 0, which kept them in the flex layout and permanently reserved ~60px on the right, so the workspace name (flex:1) truncated long before the row was full. Hide them with display: none and restore display: inline-flex on hover/focus/open, so the name fills the row and only truncates once the buttons actually appear.

The same actions remain reachable via the right-click menu and the section kebab, so removing them from the tab order when hidden is acceptable.

* fix(kimi-web): keep workspace header row height stable on hover

Lock .gh-top to the sm IconButton height (26px) so revealing the hover actions no longer grows the row and nudges the path line and the groups below it. Follow-up to the display: none change: that freed the horizontal space but let the row height collapse when the buttons were hidden.

* chore: add changeset for kimi-web UI fixes

* fix(kimi-web): restore keyboard access to workspace actions

Revert the display: none change (and the min-height that accompanied it) for the workspace row's hover actions, going back to opacity-hidden buttons.

The display: none approach removed the buttons from the tab order, and the 'create in workspace' add button has no keyboard-accessible alternative in the right-click or section menus, so keyboard users lost that action. Restoring opacity keeps the buttons focusable again, at the cost of the workspace name truncating a little earlier to reserve their space.

Addresses the P2 review on PR #1423.
2026-07-06 18:34:06 +08:00
迷渡
e95fc83cc2
fix: honor web font size setting (#1394)
* fix: honor web font size setting

* fix: scale web font-size dependents

---------

Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-07-06 17:29:23 +08:00
Kai
c12f30951f
feat(agent-core): feed AskUserQuestion answers back as question text and option labels (#1414)
* feat(agent-core): feed AskUserQuestion answers back as question text and option labels

The flattened answers record the model receives was keyed by synthesized
ids (q_0 / opt_0_1), forcing a cross-message positional lookup against the
original tool call to understand what the user picked — both unreadable in
transcripts and a real model-misreads-the-choice badcase.

- toAgentCoreResponse now takes the original broker request and translates
  wire ids back to question text (keys) and option labels (values);
  unknown ids are kept verbatim, missing request falls back to raw ids
- wire protocol unchanged: clients still answer with option ids; the
  resolve route reads the pending request before settling it
- question texts must be unique per call and option labels unique per
  question, enforced in the tool execution path (AJV cannot express the
  zod refine) and mirrored on the exported schemas
- web transcript card resolves both the new label form and legacy id
  transcripts; TUI and ACP paths already produced the text form

* fix(agent-core): align multi-select answer join across clients and harden question schema

- Join multi-select labels with ', ' in the server translator, matching
  what the TUI reverse-RPC path already emits, so the model sees one
  format regardless of which client answered
- Trim segments in the web transcript resolver before label matching:
  TUI-answered multi-select transcripts (', '-joined) previously lost
  their highlight to a spurious leading-space Other row
- Move the question-text/legacy-q_<i> answer lookup out of the SFC into
  askUserToolParse as answerFor(), per that module's testability intent
- Require non-empty question text and option labels (.min(1)) so empty
  strings are rejected by AJV at the tool boundary instead of failing
  deeper in the protocol layer

* fix(agent-core): resolve option ids only within the answered question

The translator's option-id lookup was a single flat map across all
questions, so a stale or malformed response pairing one question with
another question's option id (q_1 + opt_0_0) was silently translated
into a label that was never offered for that question. Scope the lookup
to the answered question's own options; cross-question and unknown ids
now both pass through verbatim, staying diagnosable.
2026-07-06 16:37:54 +08:00
qer
e6e6dd53ce
fix(web): stop replaying the bubble entrance animation on session open (#1411) 2026-07-06 13:58:44 +08:00
qer
ce41f4b58d
fix(web): keep Sidebar single-root so collapse hides it (#1406)
A top-level Teleport in the sidebar template made the component multi-root, so v-show could not apply display:none and the collapsed sidebar stayed mounted at the rail width, squeezing the conversation. Move the teleport inside the aside so the sidebar is single-root again.
2026-07-06 12:39:58 +08:00
迷渡
4c43935e31
fix: show Windows session search shortcut (#1393) 2026-07-06 12:08:08 +08:00
qer
c5c6282f44
feat(web): render AskUserQuestion result as an option list (#1391)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(web): render AskUserQuestion result as an option list

Parse the AskUserQuestion tool result and echo the full option list, highlighting the chosen option(s) and dimming the rest, instead of dumping raw JSON. Handles single/multi select, free-text Other answers, and the dismissed state. Answers are zipped back to the input questions by index since the input carries no ids.

* fix(web): drop the stray indent in the tool-call card body

The expanded body of every tool-call card carried a hard-coded 36px left indent (a magic number meant to align with the header name). Drop it so expanded content aligns with the header's 11px padding instead. The design-system preview page mirrored the same rule and is updated to match.

* fix(web): align markdown diff block with the design system

Restyle the local ```diff renderer in chat Markdown to match the ~/diff panel: code text keeps the normal ink colour, the +/- sign carries the add/del colour, and rows use a soft background with an inset accent bar instead of dyeing the text green/red. The header and copy button now match the standard code-block chrome (height, hover, focus ring).

* fix(web): match the markdown diff block chrome to code blocks

Make the local markdown diff renderer's shell identical to a regular code block: swap the text copy button for the same icon button, inherit the header's text-xs sizing, and keep the container / header / code-area padding, background, radius and shadow in lockstep with .code-block-container and .code-block-header. The diff-specific row tinting (soft background, inset accent bar, coloured sign) is kept since that is what makes it a diff.

* fix(web): fall back to raw output for non-answer AskUserQuestion results

Background launches and error cases return plain-text tool output (task_id/status lines, or a failure reason), not the { answers } JSON the card expects. The card used to render an empty, fully-unselected option list in those cases, hiding the task id or error. Now the card only renders the option list when the output parses as the answer payload; otherwise it shows the raw output. Addresses review feedback (P2).

* fix(web): localize AskUserQuestion result labels

The card hard-coded user-visible strings (Dismissed, answer/answers, Answered, the (+N more) summary), so Chinese-locale transcripts mixed in English. Move them into the en/zh tools locale files and read them via t(). Addresses review feedback (P2).
2026-07-06 02:36:31 +08:00
Haozhe
4963c9016f
feat(skills): list workspace skills without a session (#1392)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
- add GET /api/v1/workspaces/{workspace_id}/skills backed by a listWorkspaceSkills core RPC and ISkillService.listForWorkDir, reusing the session skill-loading path so results match a new session
- web: populate the composer slash menu from workspace skills before a session exists, then fall back to session skills once one is active
2026-07-05 18:05:15 +08:00
qer
be7c9916b0
fix(web): refill attachments when editing a queued or undone message (#1357)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* fix(web): refill attachments when editing a queued or undone message

Queued prompts that carry images/video are no longer remove-only: clicking one loads its text and attachments back into the composer. Undo ("edit & resend") now restores the message's attachments too, not just its text. useAttachmentUpload gains loadAttachments, which reuses the existing fileIds (no re-upload) and fetches authenticated blob URLs for protected getFileUrl previews so the refilled thumbnails don't 401.

* fix(web): forward attachment refills through ChatDock

When the normal chat dock is mounted (non-empty conversation), bindChatDock receives the ChatDock instance, but ChatDock only exposed loadForEdit/focus — so the new loadAttachmentsForEdit fell back to a no-op and editing a queued media prompt or undoing a media message still dropped the attachments. ChatDock now forwards loadAttachmentsForEdit to the underlying Composer.

* fix(web): replace composer attachments when refilling edits

loadForEdit(text) overwrites the composer text, but loadAttachments appended to any unsent draft attachments, so a later submit sent the stale draft files together with the edited message's files. Make loadAttachments replace the current session's attachments (revoking their object URLs) so an edit/undo replaces the whole composer, mirroring loadForEdit.

* fix(web): clear stale attachments on text-only edits

When the composer already has attachments loaded (for example after editing a queued media prompt) and the user then edits a text-only queued prompt or undoes a text-only message, loadComposerForEdit skipped loadAttachmentsForEdit (the only path that clears the strip) because the new attachment list was empty/undefined, so the next submit would send the stale media with the new text. Always call loadAttachmentsForEdit(attachments ?? []) so text-only edits replace the strip with an empty set.

* fix(web): make fileId-less refilled media resendable

When editing a user turn whose media was base64-inlined by the server (no fileId), loadAttachments used to add a non-uploading chip with no fileId, which handleSubmit silently drops on resend (and an image-only edit would not submit at all). Re-upload the data URL to obtain a fileId so the attachment is actually resendable; when re-upload is unavailable, skip the chip instead of showing a misleading ready attachment. Also factor a patchAttachment helper.

* chore: prefix web changeset entry

The gen-changesets rules require web-app changelog entries to start with 'web: ' so the synced release notes classify web UI changes correctly.

* fix(web): don't dequeue a prompt when the composer is hidden

When a queued media item is clicked while the dock is showing a pending question or approval, ChatDock has no nested Composer (only rendered in its v-else), so loadComposerForEdit no-ops — but handleEditQueued still dequeued the item, losing it. Make ChatDock.loadForEdit report whether the nested composer is present, have loadComposerForEdit return success, and only dequeue when the load actually succeeds.

* fix(web): preserve URL-backed media when refilling composer

When an undone turn contains media with source.kind === 'url' (a URL but no fileId), loadAttachments used to fall through and drop it. Re-upload the URL (data: or http(s):) to obtain a fileId so URL-backed media is preserved on edit/resend; if the URL can't be fetched (CORS / non-2xx) the chip is dropped instead of shown as a misleading ready attachment.
2026-07-04 23:32:32 +08:00
Haozhe
7db88b6f0c
feat(server): add --dangerous-bypass-auth and --keep-alive flags (#1368)
* feat(server): add --dangerous-bypass-auth and --keep-alive flags

- --dangerous-bypass-auth disables bearer-token auth on every REST and
  WebSocket route and advertises it via /api/v1/meta so the web UI skips
  the token prompt; the startup banner drops the token and shows a red
  danger notice
- --keep-alive keeps the daemon running instead of idle-killing after 60s;
  implied by --host / --allowed-host and always on in --foreground mode

* fix(server): address review feedback on bypass-auth

- keep the token and skip the bypass notice when a daemon is reused, since
  the requested --dangerous-bypass-auth flag is not applied to the
  already-running server
- clear the cached dangerous_bypass_auth web state on HTTP 401 so a stale
  bypass value cannot hide the token prompt after the server restarts
  without the flag
2026-07-04 18:03:50 +08:00
qer
26b90225d2
feat(web): support multi-level thinking effort selection (#1344)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(web): support multi-level thinking effort selection

Surface each model's declared reasoning efforts (support_efforts /
default_effort) in the web model picker as a segmented control,
replacing the on/off toggle. ThinkingLevel is now an open string, the
model catalog carries the effort metadata to the web app, and the
mobile settings sheet and /thinking command cycle through the
available levels.

* fix(web): preserve persisted thinking level before models load

coerceThinkingForModel returned 'on' for any non-'off' level when the
active model was still undefined (catalog not loaded yet), which
rewrote a persisted/default effort like 'high' to 'on' and silently
dropped the model's declared effort on later prompts. Keep the
requested level as-is until loadModels() re-runs coercion with the
real model.

Addresses Codex P1 review on modelThinking.ts.

* fix(web): coerce stale thinking level against the active model

When selecting another session, the persisted thinking level can be a
boolean 'on'/'off' carried over from a previous model. Deriving the
pill suffix and active segment from that raw value could show
"thinking: off" on an always-on model or hide the concrete effort
behind a bare "thinking" tag. Coerce the level against the current
model before deriving display state.

Addresses Codex P2 review on Composer.vue.

* fix(web): submit coerced thinking level for the prompt's target model

The send paths (submitPromptInternal / steerPrompt) previously submitted
rawState.thinking verbatim, so a value carried over from another session
(e.g. 'max' from an effort model) was sent to a model that doesn't
declare it, even though the composer already showed the coerced default.
Coerce the level against the target model before submitting so the first
turn runs with the level the UI displays.

Addresses Codex P2 review on Composer.vue.

* fix(web): coerce stale thinking in /thinking and mobile settings

Two more surfaces derived their active state from the raw persisted
level, which is stale when the saved level came from a different model:

- /thinking slash command: indexing the raw value (e.g. 'on' from a
  boolean model) into an effort model's segments returned -1 and jumped
  to 'off' instead of advancing from the model's default effort.
- Mobile settings sheet: clamping the raw prop fell back to the first
  segment, showing/selecting 'off' or the first effort while the
  composer and prompt submission coerce to the model default.

Coerce the level against the active model in both places, matching the
composer and send-path behavior.

Addresses Codex P2 reviews on App.vue and MobileSettingsSheet.vue.
2026-07-04 00:57:35 +08:00
qer
ec758c747a
fix(web): make uploaded videos play in the chat (#1343)
* feat(media): materialize video uploads to cache and reference by path

- copy TUI video placeholders into the shared cache instead of
  inlining the original source path
- emit <video path="..."> tags so ReadMediaFile / the provider's
  VideoUploader owns upload behavior
- apply the same cache materialization to server prompt video
  submissions, matching the TUI flow
- update TUI unit tests and server e2e test to assert cache-path
  behavior

* fix(web): make uploaded videos play in the chat

Render the server's <video path> tag as a real video and reconcile the echoed user message so the bubble no longer shows raw markup or a duplicate. Serve file downloads with byte-range support and fetch video bytes with the bearer credential into a blob URL, since browsers cannot authorize a <video> src on their own. Also let users click an uploaded image to open it in the preview panel.

* fix(web): use authenticated source for uploaded image previews

openMediaPreview stored the raw getFileUrl as sourceUrl, and FilePreview renders it with a native <img> that sends no Authorization header, so the enlarge action 401'd for uploaded images. When the media carries a fileId, fetch the bytes through the authenticated API client and preview a blob URL instead, revoking it when the preview is replaced or closed.

* fix(web): ignore stale authenticated media fetches

AuthMedia fetches the file bytes asynchronously; when the component is reused with a new fileId before a prior fetch resolves (e.g. queued thumbnails keyed by index), the older response could still create a blob URL and show the previous file. Add a per-request sequence guard (and an unmount guard) so a stale response is discarded and its blob URL revoked instead of being applied.

* fix(web): gate media path tags on file-store id shape

Treating any standalone <video path="..."> text as an uploaded daemon file and stripping the basename into getFileUrl is only valid for server cache files named after the file-store id (f_…). TUI/ReadMediaFile tags use arbitrary cache names like <uuid>-<label>, and older transcripts may point at paths like /tmp/foo.mp4; those produced a broken /files/<basename> request. Only extract a fileId when the basename matches the file-store id shape, otherwise leave the raw tag as text.

* fix(web): invalidate pending media preview on close

Closing an uploaded-image preview before getFileBlob() resolved left previewRequestSeq untouched, so the fetch callback still passed its seq check, created a blob URL, then skipped attaching it because previewFile was already null — leaking up to the file size until another preview opened. Bump previewRequestSeq on close so the in-flight callback bails before creating the blob URL.

* fix(web): defer authenticated media fetch until near viewport

AuthMedia fetched the full image/video into a Blob on mount whenever a fileId was present, bypassing native loading="lazy" and preload="metadata". Opening a session with several historical large video uploads started many full downloads and held all blobs in memory even if the user never scrolled to or played them. Use an IntersectionObserver to defer the fetch until the element nears the viewport.

* fix(web): revoke preview blob when leaving the file panel

Switching to another detail panel only flips detailTarget and never calls closeFilePreview, so an in-flight getFileBlob could still create a blob URL after the file panel hid, and an already-shown blob URL was held until the next file preview. Check detailTarget before creating the blob URL, and reset/revoke the preview when detailTarget leaves 'file'.

---------

Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
2026-07-04 00:36:00 +08:00
qer
01b65bdddc
feat(web): open design-system easter egg at /design-system route (#1328)
* feat(web): open design-system easter egg at /design-system route

Replace the long-press-logo iframe overlay, which loaded a separately maintained static design-system.html, with a real /design-system route. The new view aliases to the product design tokens, so the design system is maintained in one place. Adds vue-router for the route, removes the duplicate static HTML copies, and exempts the showcase view from the style scanner.

* fix(web): lazy-load the design-system view

Address Codex review: load the 2.4k-line showcase via defineAsyncComponent so it is code-split and fetched only when /design-system is visited, instead of bloating the initial bundle for every load.

* chore(nix): update pnpmDeps hash for vue-router

Adding vue-router changed pnpm-lock.yaml, which invalidated the fetchPnpmDeps hash. Set it to the value reported by the nix build.

* fix(web): return to app root when closing the design system

In-page nav anchors push hash history entries, so router.back() only stepped through them and required multiple clicks to leave. Navigate to / directly; the client lives above the route so session state is preserved.

* feat(web): let /design-system bypass the auth gate

Render the design-system route ahead of the auth/server gates and skip the /login rewrite for it, so a direct deep link shows the showcase even before the app is OAuth-ready. The view is read-only and holds no user data, matching the old static page behavior.

* fix(web): make the design-system root scrollable

The route renders inside .app-shell (height:100dvh; overflow:hidden), so a position:fixed root could be clipped. Make .ds-page a flex item that fills the shell and scrolls internally, so later sections and hash navigation remain reachable.

* fix(web): preserve /design-system during initial session load

On load the app auto-selects the first session and rewrites the URL to /sessions/<id>, which clobbered a deep-linked /design-system. Skip the session URL write while on the design-system route so refreshing keeps the route.

* fix(web): restore the prior session URL when leaving the design system

Record the URL on entry to /design-system and navigate back to it on close, so a /sessions/<id> URL is preserved instead of falling back to /. This also keeps the earlier fix that sidesteps in-page hash anchors.

* fix(web): capture the real browser URL before opening the design system

Session URLs are rewritten via the native history API, so vue-router's from.fullPath can be stale ('/' after a session is selected). Read window.location on entry instead, falling back to / for a direct deep link.

* fix(web): sync the active session URL when leaving the design system

After a direct deep link to /design-system the app auto-selects a session but the address stays '/'. On close, fall back to the active session's canonical URL so the address bar matches the displayed session.

* fix(web): capture the design-system return path at logo entry

Capture window.location in the logo long-press handler instead of a navigation guard. The guard fired on browser Back/Forward too and overwrote the return path with the design-system URL itself; capturing only at the explicit entry action avoids that.

* fix(web): replace the design-system route when closing

Use router.replace instead of push for the captured return URL, so closing does not append a second app URL after /design-system and the browser Back button returns to the page before the easter egg.

* revert(web): drop the design-system auth-gate bypass

Keep /design-system behind the auth gate so it has a single in-app entry (logo long-press). This removes the deep-link machinery (auth exemption, active-session fallback) that drove most of the URL/session edge cases, while keeping the lazy-loaded route, the flex scroll fix, the logo-entry return-path capture, and replace-on-close.

* refactor(web): rebuild the design-system easter egg as an in-app overlay

The easter egg is a hidden, read-only spec viewer opened by long-pressing the logo; it does not need to be a URL route. Replace the vue-router approach with an overlay: Sidebar opens a lazy-loaded DesignSystemView in a body-teleported full-screen overlay, dismissed by the Back button or Escape. This removes vue-router, the route, the auth-gate exemption, the session-URL guards, and the return-path machinery — none of which the feature needs.

* chore(nix): restore pnpmDeps hash after removing vue-router
2026-07-03 13:42:27 +08:00
qer
5441ad1838
feat(web): collapse loaded sessions back to the first page (#1322)
* feat(web): collapse loaded sessions back to the first page

The workspace session list's load-more control was one-way: once expanded, the only way to hide the extra sessions was to collapse the whole group. Add a Show less / Show all toggle so an expanded list can be collapsed back to its first page and re-expanded without losing the loaded data.

Restyle the control as a session-row-shaped pill whose label aligns with the session titles, per design-system section 07, and mirror the behavior in the mobile switcher.

* fix(web): preserve first-page capacity for sparse workspaces

The collapse target was seeded with the exact number of sessions loaded on first paint, which is 0 for an empty workspace and below a full page for a sparse one. Newly created sessions are prepended without bumping that count, so a workspace that was empty on load would hide its first new session behind a Show all control, and a sparse one would hide an older row on each new session even when it had never paged.

Floor the collapse target at one full page so the first-page capacity is preserved.

* fix(web): keep the active session visible in a collapsed group

A collapsed workspace only rendered its first page, so an older session selected from outside the pagination flow — Cmd/Ctrl-K search (loadAllSessions) or a URL deep link (fetchSessionIntoList) — was marked active but had no visible row in the sidebar until the user manually clicked Show all.

Include the active session in the collapsed view (appended in newest-first order) on both the desktop sidebar and the mobile switcher, so selection and search never navigate to a hidden row.
2026-07-02 21:51:28 +08:00
qer
444e6b15f0
fix(web): cap live session subscriptions to reduce lag with many sessions (#1320)
* fix(web): cap live session subscriptions to reduce lag with many sessions

Every opened session stayed subscribed to its WebSocket event stream across reconnects, so opening hundreds of sessions turned background events into a constant reducer and sidebar recompute storm. Keep only the four most-recently-opened sessions subscribed; evicted sessions resume from their tracked cursor on re-open.

* fix(web): reset cursor for sessions evicted from the subscription cap

Some session events (status_changed, meta_updated, ...) are broadcast to every connection and still advance lastSeqBySession for an unsubscribed session. If an evicted session emits per-session durable events and then a global event, the cursor jumps past the missed events, so resuming from it later would skip them and leave the reopened session stale. Track evicted sessions and reset their cursor on the next re-subscribe so the daemon replays or snapshots what was missed.

* fix(web): rebuild evicted sessions from a snapshot on re-open

Two fixes for the subscription cap:

- Re-opening a session that was evicted now rebuilds it from a snapshot instead of resuming from seq 0. Replaying from zero made the projector regenerate assistant/tool message ids, which duplicated the already-loaded transcript; resuming from the kept cursor could skip per-session events that arrived while unsubscribed.

- Eviction now skips the active session wherever it sits in the list, instead of breaking when it lands at the tail. First-time opens retain only after an awaited snapshot, so rapid clicks can complete out of order and leave the active session at the tail, which previously let the list grow past the cap.

* fix(web): keep stale cursor marker until snapshot succeeds

Re-opening an evicted session deleted the stale-cursor marker before the snapshot ran. If the snapshot failed transiently, the marker was gone and a later re-open would fall back to subscribeToSessionEvents, resuming from a cursor that may have skipped per-session events while evicted. Read the marker instead of deleting it, and let syncSessionFromSnapshot clear it once the snapshot succeeds.
2026-07-02 21:48:47 +08:00
qer
e8ab7ca786
fix(web): stop session row from shifting on hover (#1319)
The session row swapped the relative time for the kebab on hover using display:none, which dropped the time from layout and appended the kebab at the end. Because the time is variable-width (2h / 5m / just now) and the kebab is a fixed 26px, the right region reflowed: status badges shifted and the title's truncation changed, causing visible jitter.

Place the time and kebab in one inline-grid cell (grid-area:1/1) and swap them via visibility instead, so the slot width stays max(time width, 26px) across hover. The badges and title no longer reflow. Documented in the design system (section 07 Session row) as a trailing action slot rule.
2026-07-02 20:40:47 +08:00
qer
5322c63889
fix(web): trim redundant and incorrect tooltips (#1316)
* fix(web): trim redundant and incorrect tooltips

Drop hover tooltips that only restated a button's accessible label, and remove the permission-pill tooltip that described cycling modes while the control actually opens a dropdown. Keep tooltips that reveal truncated text or explain status.

* fix(web): remove remaining ChatHeader tooltips

Drop the session-title, git-branch, and open-PR tooltips in ChatHeader so the header has no hover tooltips. Broaden the changeset wording to cover the wider trim.

* fix(web): keep tooltips from getting stuck on unmount

Tooltip attached its mouseleave listener to the slotted trigger element once, so if that element was removed (e.g. a v-if toggled while hovering a tool-call path) the open bubble never received mouseleave and stranded on screen. Re-sync the listener to the live slotted element via a MutationObserver and close the tooltip whenever the trigger changes.

* chore: add changeset for stuck-tooltip fix

* fix(web): restore session title tooltip in ChatHeader

The session title is truncated with an ellipsis when it exceeds the header width, so the tooltip was the only way to read the full name from the header. Restoring it keeps the truncation-revealing hint while the redundant git/branch/open-PR tooltips stay removed.
2026-07-02 19:53:01 +08:00
qer
d1275557f9
docs(web): translate design system to English and add anti-slop guidance (#1302)
* docs(web): add anti-slop design guidance inspired by taste-skill

Codify one icon family (Remix) with no hand-rolled SVG in §02. Expand the banned AI-tell list in §01 (AI-purple/blue glow, infinite-loop micro-animations). Add button and form contrast requirements to §08. Add a 'declare design intent (Design Read) first' callout.

* docs(web): resolve merge conflict with main

Reset design-system.html to latest main (which includes the merged #1300 and #1301) and re-apply the taste-skill design guidance on top, so the branch merges cleanly.

* docs(web): translate design system to English

Translate the entire design-system.html from Chinese to English, preserving all HTML structure, CSS, code blocks, SVGs, the scroll-spy script, and token values. The design system is now a single English document.
2026-07-02 19:16:04 +08:00
qer
c3653a1c50
refactor(web): show an up arrow on the composer send button (#1301)
* refactor(web): show an up arrow on the composer send button

* docs(web): regenerate design-system catalog with the send up-arrow
2026-07-02 15:56:48 +08:00
qer
7fa18c9e45
docs(web): sync design system with the Remix icon switch (#1300)
* docs(web): sync design system with the Remix icon switch

Update the §02 icon guidance to describe Remix Icon (fill, 24x24, registry-sourced) and drop the stale 'line-icon' wording. Convert the §03 component-gallery demo icons from hand-drawn stroke SVGs to Remix fill icons. Clarify that the workspace-group add button reveals on hover or keyboard focus for accessibility. Sync public/design-system.html with design/.

* docs(web): show composer send button as an up arrow
2026-07-02 15:34:33 +08:00
qer
0b4c05e394
fix(kimi-desktop): make the sidebar header buttons clickable (#1289)
On macOS the sidebar header has a hidden title bar, so the whole header doubles as the window-drag region (matching the chat header). The collapse / settings buttons sit inside it and were being captured by the drag, so they would not click. Mark the buttons and the logo as no-drag inside the drag-region header — the same no-drag-inside-drag pattern ChatHeader.vue already uses — so they receive clicks normally.
2026-07-02 14:39:52 +08:00
qer
6a469b3e07
refactor(web): replace hand-written icons with Remix Icon (#1293)
* refactor(web): replace hand-written icons with Remix Icon

Generate a tree-shaken Remix Icon subset at build time via @iconify/utils + @iconify-json/ri, keeping the <Icon>/iconSvg() API.

Add a chat-new icon for the new-chat buttons and reveal the workspace 'new chat in group' button on hover. Unify the message copy and undo buttons (matching hover style and tooltip, drop the undo hover label, align sizes). Switch the mobile switcher kebab to the horizontal dots icon and tweak sidebar search colors. Regenerate the design-system icon catalog.

* fix(web): address PR review feedback

Restore accessible names (aria-label) on the message copy and undo buttons. Keep the workspace add button reachable for keyboard users by revealing it on header focus-within. Update the nix pnpmDeps hash for the newly added icon dependencies.

* fix(web): address follow-up review feedback

Keep the workspace add/more buttons focusable without hover by revealing them via opacity instead of display:none, so keyboard and non-hover users can reach the control.

Drop explicit .ts extensions in icon imports to satisfy oxlint, and read the design-system icon catalog directly from the generated icon data.
2026-07-02 14:20:17 +08:00
qer
3ea84a56e4
fix(web): prevent horizontal scrollbar in session search dialog (#1290)
The search sessions dialog used a column flex layout where the title and snippet had white-space: nowrap but no min-width: 0, so the flex items refused to shrink below their text width and overflowed, surfacing a horizontal scrollbar on the dialog body. Add min-width: 0 so they shrink and ellipsize instead.
2026-07-02 12:24:46 +08:00
qer
bbda90af84
fix(web): hide conversation outline when it cannot expand (#1278)
Some checks are pending
Release / Publish native release assets (push) Blocked by required conditions
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
* fix(web): hide conversation outline when it cannot expand

* fix(web): re-measure conversation outline when it becomes visible

When ConversationToc mounts while hidden (sessionLoading, mobile, or before a second user turn), navRef is null and the ResizeObserver is never set up, so fits stays true and the outline shows even in narrow layouts. Re-initialize the measurement whenever the nav is rendered.
2026-07-02 00:44:11 +08:00
qer
b905dd4910
feat(web): redesign web UI and add design system (#1258)
* feat: redesign web ui & add design system

* feat(web): add motion to redesigned UI and add changesets

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.

* fix(web): remove undo message exit animation

* fix(web): route agent tools to AgentTool and focus dialog on open

- 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.

* feat(web): add logo long-press design-system easter egg

Hold the sidebar logo for 3 seconds to open a dialog showing the design system page. Also trim and rebalance the redesign changesets.

* fix(web): silence Sidebar v-show warning by making it single-root

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.

* fix(web): thinking toggle, tool-group i18n, agent detail button

- 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).

* fix(web): use strict equality in agent detail button guard

oxlint eqeqeq flagged the loose != null check; resolveAgentTaskId returns string | undefined, so compare with !== undefined.

* chore(web): remove stray design mockups and screenshots

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.

* fix(web): keep sessions on continuation failure, treat absent thinking 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).

* feat(web): open design-system easter egg on 10 logo clicks

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.

* fix(web): treat cancelled swarm members as finished

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.

* feat(web): use 1s long-press for design-system easter egg

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.

* fix(web): use plain Spinner for activity notices

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.

* docs(web): add a11y guidance to design system

* docs(web): drop stale design README link

* fix(web): restore model search focus and define panel header weight

- 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).

* fix(web): let any open dialog own Escape over the side panel

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.

* fix(web): base dock-work flag on filtered dock task lists

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.

* fix(web): create subagent task before forwarding text deltas

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.

* fix(web): keep plan, swarm, and goal mode toggles per session

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.

* fix(web): keep subagent detail reachable for synthesized tasks

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.

* fix(web): keep session kebab menu from being clipped

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.

* fix(web): apply staged modes to the created session by id

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.

* refactor(web): unify confirmation dialogs into a single modal

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.

* feat(web): inline message queue with separate stop button

- 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.

* chore: add changeset for web queue UX

* feat(web): prompt reliability, sidebar menu, and composer/markdown polish

- 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
2026-07-01 20:47:12 +08:00
qer
4d936d98b8
fix(kimi-desktop): revert sidebar header padding to 80px (#1242)
Some checks are pending
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
100px left too much empty space next to the traffic lights; 80px clears
them without the extra gap.
2026-07-01 02:25:41 +08:00
Simon He
7f05f589e7
feat(web): add Mermaid diagram rendering and off-thread KaTeX/Mermaid workers (#1226)
* feat(web): add Mermaid diagram rendering and off-thread KaTeX/Mermaid workers

Enable Mermaid diagram support in the web chat via markstream-vue's enableMermaid(). Set up Web Workers for both KaTeX rendering and Mermaid parsing using markstream-vue's pre-built workers (katexRenderer.worker, mermaidParser.worker), keeping heavy computation off the main thread during live streaming.

* fix(web): skip Mermaid SVG subtrees in file link and markdown link rewriters

processFileLinks() uses a TreeWalker that scans all text nodes in mdRef.
Mermaid diagrams render as inline SVG, and diagram labels containing
file-path-like strings (e.g. src/App.vue) would be replaced with HTML
<button> elements inside SVG <text> nodes, corrupting the rendered diagram.

processMarkdownLinks() similarly queries a[href] inside SVGs; while
isLocalLink() mostly filters these out, explicitly skipping SVG subtrees
is safer.

Add svg to the closest() exclusion in processFileLinks(), and skip
links inside svg in processMarkdownLinks().

* fix(web): satisfy worker import lint

* chore: align mermaid dependency versions

* chore: change mermaid workers changeset to patch

---------

Co-authored-by: qer <wbxl2000@outlook.com>
2026-07-01 02:17:34 +08:00