* 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.
* 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.
* 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.
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.
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.
* 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.
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.
* feat(telemetry): add system metrics collection
Add periodic CPU and memory telemetry sampling with warmup capture, lifecycle cleanup, and tests.
* fix(telemetry): attach prompt session to system metrics
* 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
* 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>
* 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.
Keep the command preview in the body after the result lands so a multi-line command with short output no longer collapses the card. Render the command in textDim with a shellMode $ and the result one shade dimmer in textMuted, and simplify the header to "Running a command" / "Ran a command".
* 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.
* fix(tui): keep input anchored after slash command menu closes
Force a full re-render when the slash command menu closes, but only when the session content already overflows one screen; skipped under tmux. Detect the close edge from a render frame so asynchronous closes (Backspace deleting the leading slash) are covered too. Apply the same overflow/tmux gating when restoring the editor from selector panels.
* fix(tui): measure overflow against restored editor tree
Address Codex review: the overflow probe ran before the editor container was swapped back, so it counted the tall replacement panel and forced a full clear/home even when the restored content fit on one screen, yanking the editor to the top. Measure after the editor is mounted instead.
* fix(tui): redraw when content exactly fills one screen
Address Codex review: the guard skipped the forced redraw when the post-close layout is exactly one screen, leaving the editor shifted up because the differential renderer keeps the old viewport offset after a shrink. An exact fill is safe to clear (no blank tail), so redraw when content fills or overflows the viewport, and cover it with a test.
* fix(tui): complete @ file mentions across additional workspace roots with fd
When additional workspace directories are added via /add-dir, @ file completion fell back to a readdir-based scanner capped at 2000 entries, so deeply nested files in large projects never appeared. Route @ completion through fd across every root instead, keeping the query pushed down to fd and deduplicating by absolute path. The readdir fallback remains for when fd is unavailable.
* fix(tui): preserve per-root full-path fallback for @ mentions
Address review feedback: decide the scoped-versus-full-path fallback per root instead of globally. When one root has the scoped directory but another does not, the latter still runs a whole-tree --full-path search with the original query, so a match that only exists under that root is not hidden just because a sibling root happens to contain the prefix directory.
* fix(tui): fall back to filesystem when fd binary is not executable
Address review: when fdPath is non-null but the binary cannot be spawned (managed fd removed or lost execute permission), @ completion returned null because pi-tui swallows the spawn error into an empty result, so the catch never ran. Probe fd with accessSync(X_OK) before delegating and use the filesystem fallback when it is not executable, while still returning null for genuine no-match results.
* fix(tui): trust bare fd command names when probing executability
Address review: when fd is discovered on the system PATH, detectSystemFdPath returns the bare name (fd/fdfind). accessSync checked that literal string relative to cwd and never searched PATH, so a valid system fd was treated as unavailable and @ completion fell back to the capped scanner. Trust bare names (spawn resolves them via PATH) and only probe absolute/relative paths, which is how the managed fd is referenced and which can go stale.
* chore: remove accidentally committed plan files
* test(pi-tui): stabilize paste-burst test by freezing the clock
The paste-burst heuristic uses an 8ms inter-character interval that a slow or busy CI runner can exceed between synchronous handleInput calls, which resets the burst and lets Enter submit. Freeze Date so the synchronous keystrokes always register as one burst, making the assertion deterministic.
* chore: ignore top-level plan directory
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.
* 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).
- 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
* 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.
* feat: hold print-mode turn until background subagents drain
In `kimi -p` (print mode), when the main agent ends a turn while background
subagents (`kind === 'agent'`) are still running, hold the turn open and
idle-wait until they finish, flushing their completions into the turn so the
model can react before the run exits.
Previously, the main agent could end its turn after launching background
subagents; the print flow then drained them with their completion
notifications suppressed, so the main agent never saw the results and the run
exited with the work abandoned (e.g. no nomination). This was the root cause
of the swarm-alpha-mining eval failures.
The hold is gated on a new `drainAgentTasksOnStop` session option (set by the
print flow), only affects `kind === 'agent'` background tasks, and is bounded
by `background.printWaitCeilingS`. Backfill / fan-out is handled by
re-enumerating active tasks. Other background task kinds and non-print modes
are unaffected.
* 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
* 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.
* 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>
* feat(agent-core): guide the model away from repeating denied or failed tool calls
- system.md: add a diagnose-before-retrying paragraph next to the existing
permission-denial guidance, covering failed tool calls
- permission: when the user rejects an approval on the main agent, tell the
model not to re-attempt the exact same call (sub agents already had an
equivalent hint)
* fix(agent-core): close abandoned tool exchanges and dedupe duplicate tool_use ids
A turn that dies between a recorded tool.call and its paired tool.result
(e.g. a transcript write failure mid-batch) used to leave
pendingToolResultIds open forever: every later message was stranded in
deferredMessages and user input was silently swallowed.
- runOneTurn now defensively closes any dangling tool calls when a turn
ends (completed, cancelled, or failed), synthesizing an error result
that names the cause, with a warn log and a tool_exchange_abandoned
telemetry event
- the projector drops assistant tool calls whose id already appeared
earlier (first occurrence wins): a duplicate id is wire-invalid on
strict providers and not repairable by the strict resend; reported via
the existing projection-repair log and telemetry
- resume-side closePendingToolResults now logs what it closes (warn for
a mid-history gap, info for the routine trailing interruption)
* chore: add changesets for tool exchange fixes
* fix(agent-core): scope duplicate tool_use id dedup to the strict resend
Unconditional dedup regressed providers that emit per-response counter
ids (e.g. call_0 in every step) and accept their own duplicates: later
tool exchanges silently vanished from the projected history, and a
duplicate call's own recorded result was left dangling.
- the dedupe pass is now opt-in via dedupeDuplicateToolCalls and enabled
only in strictMessages, so the normal projection keeps the history the
provider produced
- the pass also drops every tool result after the first for an id, so no
dangling tool message survives; when the kept call has no result of
its own, the surviving one is reattached by the adjacency repair
- kosong now classifies the Anthropic "tool_use ids must be unique" 400
as a recoverable request-structure error so it triggers the strict
resend
* feat(cli): wait for background subagents before exiting kimi -p
When `background.keep_alive_on_exit` is enabled, `kimi -p` now waits for
all background subagents to reach a terminal state before exiting, bounded
by `background.print_wait_ceiling_s` (default 3600s). This lets concurrent
background subagents run to completion in single-turn runs instead of being
torn down when the main agent's turn ends.
Stable height across running, done, failed and backgrounded states: all share the same header + one-line tool summary + two-row content window, so the card no longer shrinks when a run finishes. Add a braille spinner in the header while active, collapse sub-tool calls into a one-line summary, and have the two-row window follow the live stream (tool output, text, or thinking) instead of showing thinking and text side by side. Mute the window tones so a brief text or tool-output segment no longer flashes white against dim thinking.
* 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
A detached Windows child gets its own console window. With the shell: true introduced for the CVE-2024-27980 fix, a passive background auto-update could flash a command window even though stdio is ignored. Set windowsHide on the detached background child so the silent updater stays silent. The foreground `kimi upgrade` path is interactive (stdio: inherit, non-detached) and reuses the parent console, so it is left unchanged.
On Windows, npm/pnpm/yarn are .cmd shims. Since Node's CVE-2024-27980 fix, spawning a .cmd/.bat without a shell throws EINVAL, which broke `kimi upgrade` and background auto-install on Windows. Pass shell: true on win32 so the install runs through the shell.
* 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.
* 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.
* fix(pi-tui): repaint viewport in place when content collapses above it
When content shrinks past the viewport top while a line above the
viewport also changes, the clamped differential path left the render
loop empty, cleared deleted lines past the screen bottom, and desynced
the cursor anchor — leaving the viewport blank with the input box gone
until a full redraw.
Repaint the visible viewport in place for that case (no ESC[3J, so the
scrollback and the user's scroll position are preserved), and clamp
deleted-line clearing to the screen bottom so it can never scroll
untracked.
* fix(kimi-code): clear the screen fully on session reset
The collapse repaint in pi-tui intentionally preserves scrollback, so
/new, /clear, and session switches no longer got a clean screen as a
side effect of the destructive full redraw — the previous session's
text stayed above the welcome banner.
Session resets want a pristine screen, so force a destructive full
render explicitly instead of relying on the renderer's shrink
behavior.
* fix(pi-tui): delete kitty images straddling the viewport top on collapse repaint
A multi-row kitty image can start above prevViewportTop while its
reserved rows are still visible. The collapse repaint's image-delete
range started at prevViewportTop and missed the image line carrying the
id, leaving a stale overlay that also dropped out of
previousKittyImageIds tracking. Widen the range to include such a
straddling block.
* chore: shorten session reset changeset wording
* fix(pi-tui): re-anchor the viewport whenever content shrinks below the screen bottom
previousViewportTop only ever grows during normal rendering, so after a
shrink the content bottom could hover above the screen bottom, leaving
dead rows that nothing repaints. Upstream masked this by frequently
doing destructive full redraws, which re-anchored as a side effect; the
fork removed those redraws without replacing the re-anchoring.
Generalize the collapse repaint into a re-anchor check at the top of
the differential path: whenever prevViewportTop exceeds
max(0, newLines - height), repaint the visible viewport in place with
the tail of the new content. The input area snaps back to the screen
bottom, scrollback and the user's scroll position stay intact (no
ESC[3J), and the previous collapse branch becomes a defensive
destructive fallback.
Update the three renderer tests that encoded the old behavior
(destructive redraw on shrink, viewport hover after clamped shrink) to
assert the re-anchored behavior instead.
* fix(kimi-code): full repaint on ctrl+o expansion toggle
Expanding tool output shifts content above the viewport; the clamped
differential render paints the shifted content through the screen,
stacking a duplicate copy below the stale one in scrollback on every
toggle. The toggle is a deliberate user action (like /clear), so do a
destructive full render instead: scrollback holds exactly one copy and
the expanded output stays readable by scrolling up.
* chore: simplify user-facing changeset wording
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.
* 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.
The prompt editor no longer needs custom newline interception: the underlying editor handles Shift+Enter and Ctrl+J natively. Drop the interception along with the shortcut_newline telemetry hook.
* feat(agent-core): keep head and tail of user messages during compaction
Compaction used to keep only the most recent 20k tokens of real user
input, so the original task statement was the first thing to vanish in
long sessions. Now, when the user-message pool fits the 20k budget it is
still kept whole; when it overflows, the oldest 2k tokens and the most
recent 18k are kept instead, with an elision marker between the two
segments telling the model what was omitted and that the summary covers
it. The summary prefix and the default system prompt describe the new
shape as well.
The new `keptHeadUserMessageCount` record field keeps restore and the
wire-transcript folded length consistent: records without it (written by
older versions) restore with the original tail-only selection that
produced them, and the vis model-mode projection mirrors the same
head/marker/tail rebuild.
* style(agent-core): drop redundant spread over slice in head selection
* 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.