* 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.
* feat(agent-core): announce image compression and keep originals readable
Every image ingestion point (ReadMediaFile, MCP tool results, clipboard
paste, REST upload/inline base64, ACP) now places a <system> caption next
to a compressed image stating the original vs. delivered dimensions, byte
size, and format, so downsampling is never silent to the model.
Originals stay readable: file uploads point at the stored blob, and
in-memory images are persisted into the session's media-originals dir
(content-addressed, size-capped, removed with the session; temp-dir
fallback when no session is known).
ReadMediaFile gains region (crop in original-image pixel coordinates,
delivered at full fidelity) and full_resolution (skip downscaling, with
an explicit error over the per-image byte limit), so the model can zoom
into fine detail instead of silently degrading on large images.
* fix(agent-core): exempt compression captions from the MCP text budget
The caption announcing an image's compression was inserted before the
shared 100K text budget was applied, so a chatty MCP result (page text +
screenshot) consumed the budget first and the caption was evicted — or
sliced mid-string into an unclosed <system> fragment — while the
downsampled image survived, silently reintroducing the exact degradation
the caption exists to report, and orphaning the persisted original.
Split the size-limit pass in two and reorder the pipeline: the text
budget now runs on the tool's own text BEFORE compression inserts
captions (exempt by construction), and the per-part binary cap still
runs after compression so compressible screenshots are kept.
* fix(agent-core): harden crop error reporting and document readback semantics
- cropImageForModel rejects non-finite region coordinates with a clean
message instead of surfacing the codec's internal validation dump
- the full_resolution and cropped-region over-budget errors now include
exact byte counts alongside the rounded sizes, so a file a hair over
budget no longer reads "is 3.8 MB, over the 3.8 MB limit"
- read-media.md notes that re-reading a file without region or
full_resolution reproduces the same downsampled image
* feat(tui): include shell commands in input history
Shell commands entered through the `!` prompt are now saved to input history. Recalling one restores bash mode, and in bash mode Up only cycles through previous shell commands while a normal prompt browses all history.
* docs(interaction): document shell command recall in input history
Note that shell commands are now saved to input history and can be recalled in Shell mode, in both the English and Chinese interaction guides.
* feat(pi-tui): add setHistoryFilter and onRecall to editor history
Add two first-class hooks to the editor's history navigation: setHistoryFilter to limit which entries Up/Down visit, and onRecall to decorate a recalled entry before it is shown. Draft restore, direction-aware cursor placement, and undo behavior are unchanged.
* refactor(tui): use pi-tui history filter for shell command recall
Replace the CustomEditor navigateHistory shadow with pi-tui's setHistoryFilter + onRecall hooks, wired in the editor-keyboard controller. This keeps pi-tui's draft-restore and direction-aware cursor behavior intact (the shadow dropped both) and moves the shell/prompt filtering and mode-restore logic into the business layer.
* feat(pi-tui): save and restore host state with the history draft
Add onHistoryDraftSave/onHistoryDraftRestore hooks so hosts can stash their own state when entering history browsing and restore it when the user navigates back to the draft. The saved host state is discarded when browsing ends any other way (typing, submit), mirroring the editor draft lifecycle.
* fix(tui): restore input mode when returning to the history draft
Wire pi-tui's history draft save/restore hooks to the editor input mode. Without this, recalling a shell entry and then pressing Down back to an empty draft left the editor in bash mode, so the next typed message was submitted as a shell command.
* fix(pi-tui): capture host draft state before running the history filter
Fire onHistoryDraftSave before the history filter runs when entering browse, so the host's filter can read the browse-entry mode rather than a mode that changes as entries are recalled. The captured state is still only committed once a matching entry is found.
* fix(tui): lock history filter to the browse-entry mode
Lock the history filter to the input mode captured when entering browse. Previously the filter read inputMode live, so after recalling a shell entry (which flips to bash mode) a second Up would only show shell commands.
* 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
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.
* 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.
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.
* 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.
* fix(tui): restore terminal state on crash and release leaked resources
Restore raw mode, cursor, and flow control on uncaughtException, unhandledRejection, and SIGTERM cleanup failure; reclaim pasted image bytes when transcript entries are trimmed; and stop feedback, activity, transcript, footer, and editor timers during shutdown.
* fix(tui): keep crash handlers installed and attach stty to the tty
Keep uncaughtException / unhandledRejection handlers installed for the whole interactive session; removing them right after start() resolved left runtime crashes uncaught. Run stty with stdin inherited from the TTY, since stty fails when stdin is /dev/null.
* 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
* chore(kimi-code): upgrade pi-tui to 0.78.1 and adapt native helpers
Bump @earendil-works/pi-tui from ^0.74.0 to ^0.78.1. pi-tui 0.75.5 replaced its koffi-based Windows VT input with a bundled native helper, and 0.76.0 added a darwin native helper for Terminal.app Shift+Enter.
- SEA build: teach native-deps to collect pi-tui's per-target .node files, drop the koffi registry, and add a native-file-only collect mode so only package.json + the target .node ship (28 -> 2 files).
- Redirect pi-tui's absolute-path native require() into the native-asset cache through the Module._load hook, and extend the native smoke test to actually load the helper.
- npm package: ship pi-tui's native/ directory so macOS Terminal.app Shift+Enter and Windows Shift+Tab keep working for npm installs.
* chore: add changeset for pi-tui upgrade
* chore: vendor @earendil-works/pi-tui 0.80.2
Fork the upstream pi-tui source into packages/pi-tui for local modification. Pristine snapshot of @earendil-works/pi-tui@0.80.2; the apps/kimi-code dependency on ^0.78.1 from npm is left unchanged.
* feat(kimi-code): integrate vendored @moonshot-ai/pi-tui
Replace the npm @earendil-works/pi-tui dependency with the vendored @moonshot-ai/pi-tui workspace package so the fork can be modified locally.
- Point apps/kimi-code imports and native-deps at @moonshot-ai/pi-tui.
- Make pi-tui source-first (exports -> src, publishConfig.exports -> dist, mirroring node-sdk) and strict-clean: bracket access for process.env / named capture groups, an override modifier, and non-null assertions for noUncheckedIndexedAccess.
- Bump the root tsconfig target to ES2024 and enable allowImportingTsExtensions (needed for pi-tui's /v regex and .ts imports, which node --test requires).
- Add packages/pi-tui to flake.nix workspaces and exclude the vendored source from oxlint.
* fix(pi-tui): export package.json for native asset resolution
The SEA native-asset collector resolves the package root via
require.resolve('@moonshot-ai/pi-tui/package.json'). The vendored
package's exports field only exposed ".", which blocked the
"./package.json" subpath and broke build:native:sea with
ERR_PACKAGE_PATH_NOT_EXPORTED.
* chore(kimi-code): sync pi-tui native prebuilds at build time
Copy packages/pi-tui/native prebuilds into apps/kimi-code/native
during build instead of tracking a manual copy in git. Only the
.node prebuilds are copied (not the C sources); the directory is
now a build artifact covered by .gitignore.
* fix(pi-tui): avoid destructive full redraw during streaming
When the first changed line is above the viewport, the differential
renderer fell back to fullRender(true), which clears scrollback and
yanks the user's viewport. On Windows Terminal this jumps to the
absolute top (microsoft/Terminal#20370).
Clamp the diff to the visible viewport when content length is
unchanged (spinner tick / markdown reflow above the viewport), so
streaming no longer triggers a full redraw in those cases. Length
changes still fall back to fullRender to reset the viewport.
* fix(kimi-code): update pi-tui imports in files merged from main
Two files added on main (effort-selector, plugin-command) still
imported the old @earendil-works/pi-tui package name; point them at
the vendored @moonshot-ai/pi-tui.
* chore(nix): update pnpmDeps hash after lockfile regen
* fix(pi-tui): clamp above-viewport diff even when content shrinks
Previously, when the first changed line was above the viewport and
content length changed (e.g. spinner removed at end of streaming), the
renderer fell back to fullRender(true), which clears scrollback and
yanks the viewport to the absolute top on Windows Terminal.
Always clamp the diff to the visible viewport instead, preserving the
user's scroll position. Stale bytes remain in scrollback but are not
visible.
* fix(kimi-code): keep activity placeholder to avoid streaming shrink
When streaming ends, removing the activity spinner shrank the content
by two rows, which (combined with transient→final code highlighting
above the viewport) triggered a destructive full redraw. Keep a one-row
placeholder in the activity pane when idle so the content does not
fully shrink.
* chore: refine streaming scroll changeset wording
* chore: remove obsolete pi-tui native helpers changeset
* chore: add pi-tui changesets and document the pi-tui changelog rule
Add changesets for the fork integration, package manifest export, and
viewport clamp fix so the vendored pi-tui keeps its own changelog.
Update the gen-changesets skill to treat @moonshot-ai/pi-tui as a
special internal package that lists itself instead of the CLI, with a
separate CLI changeset only when the change is user-visible there.
* chore(nix): update pnpmDeps hash after merging main
* fix(changeset): drop private kimi-code-docs from google-genai changeset
* chore(telemetry): track conversation undo, shell mode, and effort changes
- conversation_undo: fires when an undo reverts history (double-Esc selector, /undo, /undo N)
- shell_command: fires when a ! shell command runs in the TUI
- thinking_toggle: now fires on any effort change (not just on/off flips) and carries { enabled, effort, from }; align the no-session TUI path to the same payload
* chore(telemetry): track conversation_undo in core undoHistory
Tracking it in the TUI only covered the TUI; web/acp/REST undos reach
core.rpc.undoHistory directly and were missed. Emit the event from the
agent undoHistory RPC after the undo succeeds so every host is covered,
and drop the now-redundant TUI track.
Addresses review feedback on #1271.
* feat: add model alias overrides
Preserve user model overrides across provider catalog refreshes and resolve effective model metadata for runtime, TUI, protocol, and ACP consumers.
* fix: apply model display name overrides
Show overridden model display names in the footer, welcome panel, status output, and model switch confirmations.
* fix: pass through kimi effort when undeclared
Keep support_efforts authoritative when declared, but pass requested Kimi thinking effort through when the model does not declare support_efforts.
* fix: honor model overrides in effort commands
Use effective model metadata for /effort choices and for always_thinking clamping when resolving thinking effort.
* chore(kimi-desktop): rename installers to kcd-beta-alpha-crazy-internal
New artifact name: kcd-beta-alpha-crazy-internal-v50-<arch>-<MMDD>.<ext>,
where MMDD is the build date in UTC+8. This makes leaked or forwarded
installers harder to mistake for an official release, and the date makes
each build easy to identify.
* chore(kimi-desktop): uppercase KCD in installer name
* feat(agent-core): compress oversized images before sending to the model
Downsample images to a 2000px longest-edge and per-image byte budget at the
single prompt-ingestion chokepoint (the prompt/steer RPC) and on tool results
(ReadMediaFile, MCP), so every client transport — CLI, web, desktop, ACP, SDK —
is covered uniformly inside the core. PNG screenshots stay lossless and only
degrade to JPEG when the byte budget cannot otherwise be met. Best-effort: the
original image is sent unchanged if compression fails.
* fix(agent-core): serialize prompt/steer RPCs to avoid a turn-claim race
The prompt/steer RPC handlers await image compression before turn.launch()
synchronously claims the active turn, so two overlapping calls could both
compress first — letting the faster-to-compress one win the turn and strand the
other on agent_busy. Run these two RPCs through a per-agent serialization chain
so they claim in submit order; cancel and the other RPCs stay immediate.
* fix: update flake.nix pnpmDeps hash for the jimp dependency
Adding jimp to the workspace changed pnpm-lock.yaml, so the pnpmDeps
fixed-output hash was stale and the nix build failed. Update it to the value
the CI nix build reported.
* fix(agent-core): guard image compression against decompression bombs
A tiny-byte, huge-dimension image (e.g. a solid 30000x30000 PNG) would be fully
decoded into a multi-gigabyte bitmap by Jimp before any resize — an OOM vector
the byte budget never catches. Skip compression when the sniffed pixel count
exceeds MAX_DECODE_PIXELS (~100 MP), before the decode; oversized images pass
through uncompressed as they did before compression existed.
* fix(agent-core): cap decode byte size before compressing images
Compression runs before downstream size caps (e.g. the 10MB MCP per-part
limit), so a huge or invalid base64 image from an MCP tool was Buffer.from-
decoded — and handed to Jimp — just to be dropped afterward. Add a
MAX_DECODE_BYTES ceiling (64MB, overridable) checked before the base64 decode
and before Jimp, the byte-side complement to the pixel-count guard; oversized
payloads pass through uncompressed.
* refactor(agent-core): compress images at ingestion, not on the turn RPC
Move image compression off the prompt/steer RPC path and back to each ingestion
site (CLI paste, server upload resolution, ACP conversion; ReadMediaFile and MCP
already compressed at their producers). Compressing on the RPC control path put
an async step before the synchronous turn-claim, which spawned a series of
races: prompt/steer interleaving, and — with a cancel arriving mid-compression —
an ineffective abort that let a cancelled prompt launch anyway.
Treating compression as a pure input-stage transform (done while the content
part is built, before it ever enters the agent loop) removes those races
structurally: rpc.prompt/steer are plain synchronous handlers again, and the
serialization/cancel-window machinery is gone. Records stay compressed, resume
stays consistent, and coverage degrades gracefully (a new client that skips
compression just sends a larger image, as before this feature).
* fix: compress inline base64 prompts and honor ACP cancels mid-compression
Two contained ingestion-site follow-ups:
- server: resolvePromptMediaFiles now also compresses images submitted as an
inline `{ kind: 'base64' }` source, not just uploaded files, so the REST
inline-base64 path gets the same downsampling.
- acp-adapter: AcpSession tracks a pending-abort flag while prompt() awaits
image compression (before any turn exists). A session/cancel in that window
flips it, so the prompt returns `cancelled` instead of launching a turn the
client already stopped.
* fix(acp-adapter): cover all concurrent pre-turn prompts on cancel
The pending-abort marker was a single session field, so with two
`session/prompt` requests compressing large inline images at once the later
one overwrote it and a `session/cancel` could mark only one — the other
launched after the client had cancelled. Track a token per in-flight prompt in
a set and flip them all on cancel so every pre-turn prompt is covered.
* chore(node-sdk): declare jimp as a devDependency
The SDK re-exports the image compressor, whose lazy `import('jimp')` (inside
the bundled agent-core code) is inlined into the published dist. jimp was
resolved only transitively via agent-core, so declare it as an explicit build
input here — matching the CLI — to make the bundling reliable rather than
phantom. It stays a devDependency: jimp is bundled, not a runtime dependency.
Lower KIMI_CODE_TUI_MAX_TURNS default from 50 to 15 and KIMI_CODE_TUI_HYSTERESIS from 10 to 5 so the TUI keeps fewer transcript turns in long sessions.
Always-on models that expose multiple effort levels already render only the effort segments in the /model switcher, so the trailing "Off (Unsupported)" label was non-selectable clutter. Drop it for those models while keeping it for legacy boolean always-on models.
Empty (encrypted/redacted) thinking deltas no longer switch out of waiting mode, which previously stopped the moon spinner while no thinking component was ever created, leaving a blank spinner-less gap until the first real text/tool token.
* feat(vis): support importing debug zips via drag and drop
Add a window-level drop target so a /export-debug-zip bundle can be
imported by dragging it anywhere into the vis UI, alongside the existing
file-picker button. A full-screen overlay gives feedback during the drag
and while the upload is in flight, and non-zip files are rejected with a
hint.
* fix(vis): gate drop handler to file drags
Match the other drag handlers by checking dataTransfer.types before
calling preventDefault, so non-file drops (selected text or a URL into
the search input) keep their native behavior instead of being swallowed
by the window-level listener.
* 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>
* fix(web): persist workspace rename via the daemon update API
* fix(web): hide the unshipped provider manager
* fix(web): fall back to a local override for derived workspace renames
* fix(web): preserve workspace name override across registration
* feat(agent-core): rework compaction to keep only user prompts and summary
* refactor(agent-core): rewrite compaction summary as first-person handoff
Rework the full-compaction summary to read as the agent's own continuing
notes instead of a third-party report:
- compaction-instruction.md: free-form first-person continuation that
preserves exact commands, paths and outcomes, states the precise next
action, and flags claimed-but-unverified work rather than trusting it.
- compaction-summary-prefix.md: skeptical "your own working notes"
framing; drop the collaborative third-party prefix.
- system.md: add compaction-awareness guidance so the model continues
naturally from a summary and re-checks any reported "done".
- Rename the compaction helpers module to handoff.ts.
Update tests and regenerate snapshots for the new prompt text, and fill
in contextSummary in the restored-compaction replay expectations.
* fix(agent-core): count image/audio/video parts in token estimation
estimateTokensForContentPart returned 0 for image_url/audio_url/video_url,
so auto-compaction triggers, the overflow-shrink budget, the kept-user
budget, and the reported context size all went blind to media — a
media-heavy session could overflow the model window while the estimate
reported a near-empty context. Media parts now carry a fixed estimate
(MEDIA_TOKEN_ESTIMATE), and the content-part switch is exhaustive so a new
ContentPart kind must declare its estimate rather than silently count as
zero.
* feat(agent-core): re-surface active background tasks after compaction
Folding the live context to [recent user prompts, summary] drops the
messages that started background tasks and their status updates, so the
model could forget a task is still running and spawn a duplicate.
injectAfterCompaction now appends a system-reminder listing active
background tasks (with guidance to use TaskOutput/TaskList/TaskStop
instead of re-spawning). It runs only post-compaction and carries an
injection origin, so the next compaction drops and rebuilds it rather
than stacking copies; the all-user-role post-compaction shape is
preserved (no tool-pairing reintroduced).
* test(agent-core): add compaction scenario guards and risk probes
Adds compaction-scenarios.test.ts driving the real Agent/ContextMemory/
FullCompaction machinery:
- A guard test locking in that repeated compaction folds the prior summary
into the new one instead of stacking two summaries.
- Seven `it.fails` probes that executably reproduce known, currently-accepted
edge-case defects so the suite stays green while documenting each one
precisely; any of them will flip red (forcing removal of `.fails`) the day
the behavior is fixed. They cover: assistant/tool appended during an
in-flight summarizer call being dropped; unbounded shrink on empty
summaries; the fixed 20k kept-user budget overflowing a small model window;
a tool result orphaned when compaction starts mid-exchange; legacy
compaction records dropping their verbatim tail on replay; micro-compaction
clearing recent tool results in an overflow-shrunk suffix; and media being
discarded when the oldest kept user message is truncated.
* fix(agent-core): repair tool_use/tool_result adjacency in projected context
A tool call and its result can end up non-adjacent in history — a
background-task notification or flushed steer lands between them, or an
interrupted/nested step delays the result — which strict providers reject
with HTTP 400. The projector now moves each tool_use's result up to
immediately follow it (projection-time only; the stored history is
untouched), and full compaction projects its summarizer input with a
synthetic result for any still-open call so the summary request stays
well-formed. Micro-compaction only surfaced this latent ordering by busting
the prompt cache, so it now defaults off.
Includes projector adjacency regression tests, a context-level integration
test, and a compaction synthesize-missing guard; the prior "keeps an
unresolved tool exchange out of the compaction prompt" test is updated to
the now-well-formed (synthetic-result) behavior.
* fix(agent-core): preserve the verbatim tail when restoring legacy compactions
A pre-rework `context.apply_compaction` record used
`[summary, ...history.slice(compactedCount)]` semantics and kept a verbatim
recent tail, but it has no `keptUserMessageCount`. The reworked applyCompaction
re-folded such records into the all-user shape, dropping the recent
assistant/tool tail — so resuming a session compacted by an older version
silently lost its most recent context.
On restore of such a record (gated on records.restoring, no keptUserMessageCount,
and compactedCount < history length) reproduce the old shape instead. The
forward/live path is unchanged; the projector's tool-adjacency repair keeps the
restored tail well-formed, and compaction only runs at clean step boundaries so
the tail has no open exchange. The legacy-tail probe now passes as a regression
guard via the real restore path.
* fix(agent-core): align legacy compaction foldedLength with live restore
The transcript reducer re-derived foldedLength for pre-rework
context.apply_compaction records (no keptUserMessageCount) using the new
kept-user+summary rule, but ContextMemory's restore now reproduces the legacy
[summary, ...history.slice(compactedCount)] shape for those records. The two
diverged for legacy sessions, so MessageService's foldedLength-vs-live-history
comparison could mis-handle GET /messages (miss or misorder recent output).
The reducer now mirrors the live legacy fold: when compactedCount is below the
pre-compaction length it computes 1 + (length - compactedCount); otherwise it
falls back to the kept-user derivation. The MessageService transcript test's
fixture is corrected to a new-format record, matching its all-user live mock.
* fix(kosong): merge a follow-up user turn into the preceding tool_results
The Anthropic message merge keyed on isToolResultOnly(last) ===
isToolResultOnly(converted), which left a tool_result-only user turn
followed by a plain-text user turn unmerged. After tool-exchange repair
this shape (assistant tool_use -> tool_result -> injected notification)
produces two adjacent user messages, which strict Anthropic-compatible
backends reject with HTTP 400.
Switch to the asymmetric predicate isToolResultOnly(last) ||
!isToolResultOnly(converted): a tool-result-only running message absorbs
whatever user turn follows (parallel tool_results or a trailing text),
yielding a valid [tool_result, ..., text] message; a plain-text running
message still only absorbs plain text. [tool_result, text] is valid for
both native Anthropic (which concatenates anyway) and strict backends.
* test(agent-core): pin micro-compaction flag in the shrunk-suffix probe
The 'does not clear recent tool results when projecting a shrunk suffix'
probe is an it.fails that only documents a real defect while
micro-compaction is active. It inherited the ambient
KIMI_CODE_EXPERIMENTAL master switch, so its pass/fail flipped with the
runner: green locally (master switch on) but a hard failure in CI, where
the flag defaults off and MicroCompaction.compact() is a no-op that
leaves the tool result intact.
Enable KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION explicitly for this probe
so it deterministically exercises the micro-compaction path regardless of
the environment.
* fix(agent-core): harden full compaction against in-flight races, unbounded shrink, and media loss
Three compaction-path fixes surfaced by review, each flipping its
documenting it.fails probe to a passing it:
- Append race (CMP-02): after the summarizer returns, the post-summary
history check only compared the compacted prefix. A live step appending
to the tail while a manual/SDK compaction was in flight slipped through —
an appended assistant/tool turn is neither summarized (the summary covers
only the snapshot) nor kept (the rebuild keeps user input), so it
vanished. Now cancel when the appended tail contains a non-user message;
an appended user message is still kept (rebuild picks it up), preserving
the existing 'keeps messages appended while compacting an unchanged
prefix' behavior.
- Unbounded empty/truncated shrink: an empty or truncated summary dropped
the oldest message and reset retryCount, so a model that kept returning
empty could issue ~one request per history entry. Bound the shrink
attempts by MAX_COMPACTION_RETRY_ATTEMPTS, mirroring the overflow-shrink
counter.
- Media dropped on truncation (CMP-07): truncating the oldest kept user
message replaced its whole content with one text block, discarding any
image/audio/video. Keep the non-text parts and spend the remaining budget
(maxTokens minus their cost) on truncated text.
* fix(vis): mirror legacy compaction tail in the model-mode projector
For a pre-rework context.apply_compaction record (no keptUserMessageCount),
agent-core's ContextMemory restore and the transcript reducer keep the old
[summary, ...history.slice(compactedCount)] tail — a verbatim recent tail
including assistant/tool. The vis model-mode projector always applied the
new kept-user selection, so opening an older compacted session in model
mode hid the assistant/tool tail the resumed agent still holds (and
surfaced a pre-compaction user message the agent dropped).
Branch on a missing keptUserMessageCount with compactedCount < history
length and reproduce the legacy shape, matching the agent-core restore.
* fix(agent-core): cancel compaction on any droppable user-role tail
The in-flight append guard cancelled only when the tail grew with a
non-user role. A user-role message that compaction would still drop — a
background-task notification, hook/cron reminder, or shell-command output —
slipped through: appended after the summary snapshot (so absent from the
summary) and dropped by the all-user rebuild (which keeps only real user
input), vanishing silently.
Key the guard on the same predicate applyCompaction uses (!isRealUserInput)
so it cancels whenever the appended tail holds anything compaction would
drop. A real user message is still kept, so a live user turn racing a
manual/SDK compaction continues to complete.
* fix(agent-core): exclude pre-clear prompts from legacy folded length
The transcript reducer's legacy fallback (records predating
keptUserMessageCount, compacted with no verbatim tail) re-derived the
kept-user count from the whole transcript, including messages before the
last context.clear. Live ContextMemory rebuilds _history from post-clear
messages only, so counting pre-clear prompts overstated foldedLength;
MessageService then saw context.history.length <= foldedLength and skipped
appending unflushed live tail messages, dropping recent output from the
messages endpoint for old sessions compacted after a clear.
Derive only from entries at or after clearFloor to match the live context.
* fix(agent-core): drop media when truncating the oldest kept prompt
Revert the media-preserving truncation: keeping non-text parts on the
truncated boundary message overshot the kept-user budget when the media
alone exceeded it, and reordered interleaved text/media parts. Both codex
(no media-aware truncation) and Claude Code (strips media at compaction)
decline to preserve media on a truncated message, since media cannot be
partially truncated and keeping it whole breaks the budget.
truncateUserMessage now keeps only the truncated text. Recent messages
that fit the budget are still kept verbatim with their media; only the
oldest, partially-overflowing boundary message loses its attachments.
* fix(agent-core): make manual compaction and turns mutually exclusive
A manual/SDK compaction could start while a turn was streaming, or a new
turn could launch while a compaction was in flight. Either way the turn
mutates the shared context (streaming content into an existing assistant
message, or appending new messages) during the summarizer await, and that
output is neither summarized nor preserved by the all-user rebuild —
silent loss that object-identity checks can't detect (the streamed message
is mutated in place).
Guard both directions so the agent does one of {turn, compaction} at a
time: begin() refuses a manual compaction while a turn is active, and
launch() refuses a new turn while a compaction is in progress. Auto
compaction is exempt — it runs from within the turn at a step boundary,
which blocks the turn for its duration.
* chore(changeset): consolidate compaction changesets into one
* chore(agent-core): drop external-product references from compaction comments
* test(agent-core): add Anthropic wire-compliance smoke tests for compaction
Drive real compaction output and the compaction summarizer projection
through the real Anthropic provider conversion and assert the wire request
is well-formed: strict user/assistant alternation and every tool_use
answered by an adjacent tool_result. Locks in the cross-layer guarantee
(projector merge + Anthropic consecutive-user merge + adjacency repair +
synthesizeMissing) that compacted sessions stay valid for strict
Anthropic-compatible backends.
* fix(agent-core): defer and replay inputs during manual compaction instead of rejecting
Manual/SDK compaction runs outside a turn, so the earlier guard rejected
prompts/steers that arrived while it held the context. That broke three
things: a REST/web prompt got stuck 'running' (no terminal turn event), a
background-task/cron steer was silently lost (null was read as 'buffered'
but nothing was), and a follow-up prompt could land in the window after
isCompacting cleared but before reminders were reinjected.
Reuse the existing defer-and-replay model instead of rejecting:
- steer() and launch() buffer into steerBuffer while a compaction is in
progress (returning null = buffered), mirroring how an active turn defers
input.
- FullCompaction.compactionWorker keeps isCompacting true through
refreshSystemPrompt + injectAfterCompaction (moving markCompleted and the
completed event after reinjection), then replays the buffer via
TurnFlow.onCompactionFinished — on success, on an A1 prefix/tail cancel,
and on failure/abort.
- onCompactionFinished flushes into an active turn if one exists, else
launches a fresh turn from the deferred input.
No PromptService change: a deferred prompt's eventual turn.started lets it
associate the pending prompt and clear it on turn.ended.
* fix(kosong): merge consecutive user turns for strict providers
Gemini/Vertex require strictly alternating user/model turns and reject
consecutive user turns with HTTP 400. They arise after compaction (kept
prompts + user-role summary + injected reminders) and when a turn is
steered in right after a tool result. Anthropic already merged them
inline; the Google converter did not, so post-compaction requests failed.
Extract the asymmetric merge into a shared mergeConsecutiveUserMessages
helper applied at each strict provider's conversion boundary: refactor
Anthropic to use it (behavior unchanged) and apply it at the Google
converter's exit. A conformance suite drives every strict provider with
the post-compaction shape and a steer-after-tool-result shape, asserting
no consecutive same-role turns reach the wire, so a new strict provider
cannot silently omit the merge.
The provider-agnostic projector stays structure-preserving: lenient
providers (OpenAI/Kimi) keep distinct turns for clearer message
boundaries; only strict providers normalize, where the requirement lives.
- Increase the sidebar header's left padding (100px) on macOS so the floating
traffic lights no longer overlap the Kimi Code brand.
- Make the conversation header a window-drag region on macOS (interactive
controls opt out with no-drag), so the whole top of the window can be dragged
without adding a separate titlebar strip.
* fix(web): surface error when adding a workspace by path fails
Previously, addWorkspaceByPath swallowed any error from the daemon and created a local-only workspace that could not host sessions and vanished on reload, so an invalid absolute path appeared to succeed but never took effect. Now the error is reported to the user and no fake workspace is added.
* fix(web): preserve pending prompt when adding workspace fails
addWorkspaceByPath now returns a boolean success signal. handleAddWorkspace only clears pendingWorkspaceSubmit and sends the prompt on success, so a daemon-rejected path no longer drops the user's pending submission; they can retry with a valid path.
* fix(web): keep workspace picker open when add fails
Move closing the add-workspace dialog until after a successful add. On a daemon-rejected path the picker now stays open with the user's input intact, so they can correct the path and retry; the pending submission is consumed by a successful retry or dropped only when the user explicitly closes the picker.
* fix(web): show add-workspace errors inline in the picker
When the daemon rejects a path, the global warning toast rendered behind the picker's backdrop (z-index 60 vs 200) and could auto-dismiss unseen. Surface the failure as an inline error inside the dialog instead, so it is visible and persists until the user retries or closes. addWorkspaceByPath now returns a boolean and leaves surfacing to the caller; the picker shows a localized inline message.
* feat: support multi-level thinking effort switching
- kimi provider: emit thinking.effort in the new wire format; keep reasoning_effort mirrored during the transition
- model catalog: thread support_efforts / default_effort from oauth through to /models
- config schema: add supportEfforts / defaultEffort on model aliases
- TUI: multi-segment thinking control in /model, new /effort command, footer effort display
- switch status uses displayName and distinguishes model vs effort-only changes
* docs: add thinking effort design plans
- thinking-effort-switching.md: implemented multi-level effort switching
- thinking-model-overhaul.md: follow-up refactor plan for the thinking state model
* docs: collapse thinking overhaul plan into a single PR
* refactor!: overhaul thinking config and effort resolution
Replace default_thinking and thinking.mode with a single [thinking] enabled/effort table. ThinkingEffort is now an open string ('off' | 'on' | model-declared effort); effort levels come from each model's support_efforts instead of a fixed enum.
Centralize default and always_thinking clamp logic in resolveThinkingEffort/defaultThinkingEffortFor, and honor an explicitly configured effort when an always_thinking model is forced back on.
TUI keeps a single thinkingEffort field instead of the boolean + level pair; 'on' is normalized to the model default at the UI boundary.
BREAKING CHANGE: default_thinking and thinking.mode are removed from config; migrate to [thinking] enabled/effort.
* refactor: rename residual thinking level wording to effort
Rename comments, error messages, parameter names, the SetThinkingPayload wire field (level -> effort), and TUI local variables so the thinking effort naming is consistent throughout. No behavior change.
* refactor: rename remaining camelCase thinking level identifiers to effort
Rename liveLevel/prevLevel/levelChanged/commitLevel/effectiveLevel to liveEffort/prevEffort/effortChanged/commitEffort/effectiveEffort in the TUI model picker and config commands.
* refactor: eliminate remaining thinking level wording in comments and tests
Rename levelLabel -> effortLabel, EffortSelectorOptions.levels -> efforts, and 'effort level(s)' / 'default level' / 'requested level' wording in comments, error messages, slash-command description, and test titles to effort. Also restore the withThinking(effort) parameter rename in the Kimi provider that was accidentally reverted.
* fix: address codex review feedback on thinking effort handling
- OpenAI thinkingEffortToReasoningEffort and Anthropic clampEffort now normalize 'on' / unrecognized efforts instead of throwing, so boolean non-Kimi models no longer crash on session start.
- ACP resolveCurrentThinkingEnabled treats a non-empty thinking.effort as enabled, matching agent-core's resolveThinkingEffort.
- REST promptThinkingSchema accepts any non-empty effort string so model-declared efforts are not rejected at the API boundary.
* test: align kimi e2e expectations with supportEfforts-gated reasoning_effort
The kimi provider now sends reasoning_effort only when the model declares support_efforts; boolean models (no support_efforts) send only thinking.type. Update the kimi e2e tests to drop the stale reasoning_effort expectation for the boolean test model.
* test: cover [thinking] effort parsing in config.test
Add effort = "high" to the documented [thinking] table in the config parse test and assert config.thinking.effort is resolved, so the new [thinking] effort field has direct parse coverage.
* docs: add thinking test coverage gap analysis
Capture the explore agent's test coverage review for the thinking overhaul PR, including P1/P2 gaps and the two open design questions, for follow-up test additions.
* feat(oauth): parse nested think_efforts from /models response
The /models endpoint now returns effort levels under a nested think_efforts object ({ support, valid_efforts, default_effort }). Parse it preferentially in both managed-kimi-code and open-platform model parsing, falling back to the legacy flat support_efforts / default_effort fields for older servers.
* refactor(oauth): only read nested think_efforts; gate on support=true
Drop the legacy flat support_efforts / default_effort fallback. The think_efforts object is now the single source, and its support flag gates the whole object — when support is not true, valid_efforts and default_effort are ignored entirely.
* chore: remove unused parseStringArray import in open-platform
* docs: finalize thinking effort release notes
Downgrade the changeset to minor with an English summary, drop the version-specific 'added in 1.0.0' info block, and present the deprecated config fields as a table (field / deprecated in 0.21.0 / description).
* refactor: drop temporary refresh toggles and kimi reasoning_effort mirror
Remove the always-true REFRESH_MODELS_ON_PICKER_OPEN / REFRESH_PROVIDER_MODELS_ON_STARTUP toggles and their stale re-enable TODOs, and stop sending reasoning_effort from the kimi provider (thinking.effort is the only wire field now).
* fix(tui): avoid persisting "on" as thinking effort
* fix: preserve persisted thinking effort across login and provider setup
* fix(tui): show actual thinking effort in /status and footer
* test(tui): align message-flow expectations with effort persistence and /status display
* fix(vis): rename thinkingLevel to thinkingEffort in config.update analysis
* ci(kimi-desktop): strip Developer ID prefix from CSC_NAME
electron-builder rejects the 'Developer ID Application: ' prefix in CSC_NAME;
the keychain setup exports the full certificate name, so strip the prefix
before passing it to electron-builder.
* ci(kimi-desktop): add homepage and maintainer for linux .deb
electron-builder's .deb target requires a project homepage and a package
maintainer; set both so the Linux build succeeds.
* fix(cli): force-exit headless runs so a lingering handle can't wedge kimi -p
Print mode (`kimi -p`) never calls process.exit(); it relies on the Node
event loop draining once the run completes. A stray ref'd handle left over
from the run — a lingering socket, an un-cleared timer, or a child whose
pipes stay open — keeps the loop alive, so a finished run hangs until an
external timeout kills it instead of exiting.
- Arm an unref'd force-exit fallback after a headless run completes. A
healthy run drains and exits before it fires (behaviour unchanged); it
only force-exits a run whose loop is already wedged.
- Bound prompt cleanup with a timeout so a wedged shutdown step (a
SessionEnd hook, MCP shutdown, or a connection blackholed by a
restrictive firewall) can't keep a completed run alive forever.
Add unit tests for the force-exit guard and for cleanup staying bounded
when harness.close() hangs.
* refactor(cli): move headless force-exit to the entrypoint, keep handleMainCommand pure
handleMainCommand is a reusable, exported, unit-tested handler — scheduling a
deferred process.exit inside it could terminate a test runner or an embedding
host once the spied exit was restored. Make it pure: it now returns a
MainCommandOutcome, and the process entrypoint (the program action, which
already owns the error-path process.exit) arms the unref'd force-exit fallback
only for a completed headless run.
* fix(cli): drain stdio before the headless force-exit to avoid truncating output
The unref'd force-exit fallback fired after a fixed grace even when the event
loop was alive only because buffered stdout/stderr had not finished flushing to
a slow or piped consumer (the prompt writers ignore write() backpressure). That
could truncate the assistant output or resume hint of an otherwise-successful
`kimi -p` run — a regression versus the previous natural-drain exit.
finalizeHeadlessRun now flushes stdio first (bounded by HEADLESS_STDIO_DRAIN_TIMEOUT_MS)
and only then arms the force-exit. Draining first also means a leaked handle is
the only thing that can still hold the loop, so the backstop stays precise and
bounded.
* fix(cli): only swallow cleanup rejections abandoned by the timeout
The cleanup timeout guard caught every rejection, so a cleanup step that failed
fast (e.g. restoring a resumed session's permission, or harness.close hitting a
persistence error) was silently swallowed: runPrompt resolved as success and the
session could be left in `auto` with no error surfaced.
A timeout should bound how long we wait, not change the outcome. raceWithTimeout
now propagates a rejection that settles before the timeout, and only swallows the
abandoned promise's late rejection once the timeout has won the race.
* feat(kimi-desktop): add Electron desktop client wrapping kimi-web
New apps/kimi-desktop — a thin Electron shell + process manager around
the existing web UI. It reuses kimi-code's shared daemon: it runs the
bundled SEA's `server run` (the same ensureDaemon reuse-or-spawn flow as
`kimi web`), reads ~/.kimi-code/server/lock for the real origin, and
loads the SEA-served kimi-web same-origin. The daemon is left running on
quit so the CLI / browser / TUI keep sharing it.
- main process: ensure-server (run SEA, read lock, confirm healthz),
sea-path (dev vs packaged), window + native menu + window-state +
loading/error screens
- packaging: electron-builder config; before-pack stages the
matching-platform SEA into <resources>/bin/<target>
- CI: desktop-build workflow builds unsigned mac/win/linux installers,
each runner building its own SEA
- workspace wiring: register in flake.nix, allow electron postinstall
(onlyBuiltDependencies), root dev:desktop + typecheck entries
v1 is unsigned, default icon, no auto-update.
* feat(kimi-desktop): sign + notarize macOS builds
Unsigned macOS builds are blocked by Gatekeeper ("app is damaged") once
transferred to another Mac. Add Developer ID signing + Apple notarization,
mirroring the TUI native build:
- build/entitlements.mac.plist: hardened-runtime entitlements (allow-jit,
disable-library-validation for koffi/clipboard, etc.) applied to the app
and — via entitlementsInherit — the nested SEA backend
- electron-builder.config.cjs (replaces .yml): hardenedRuntime + entitlements;
signing and notarization are env-driven (CSC_* + KIMI_DESKTOP_NOTARIZE +
APPLE_API_* ), so the same config builds unsigned locally or signed+notarized
- desktop-build CI: sign-macos input reuses the existing macos-keychain-setup
action + APPLE_* secrets, notarizes via the notary API key
- README: document signing, the Developer-ID requirement, and the
"don't rename the .app" gotcha
Verified locally that electron-builder signs both the app and the nested SEA
with hardened runtime + the entitlements, and the signed app still launches and
serves the web UI. Notarization itself needs a Developer ID cert (CI / a machine
that has one).
* feat(kimi-desktop): rename product to Kimi Code Desktop
productName / window title / menu label / error-screen text all use
"Kimi Code Desktop" so the bundle name matches its executable (a
mismatch from manual renaming is itself reported as "damaged").
* ci(kimi-desktop): build and attach desktop installers in the release pipeline
Make desktop-build.yml reusable (workflow_call) and invoke it from the
release workflow, mirroring the native-build pipeline, so each release
also attaches signed+notarized macOS, Windows and Linux desktop
installers to the GitHub Release.
* feat(kimi-desktop): brand the desktop as an internal testing build
- Add an inline 'internal testing build' tag next to the Kimi Code brand
in the sidebar header, shown only inside the desktop app.
- Use a hidden native title bar on macOS with the traffic lights folded
into the sidebar header, and pin the window title to the product name.
- Ship the Kimi app icon for macOS and Linux builds.
Desktop detection is runtime (a query hint from the Electron shell,
persisted in sessionStorage) so the branding appears even when the
window is served by an already-running shared daemon.
* docs(kimi-desktop): update v1 scope now that the app icon ships
* feat(kimi-desktop): add the Kimi app icon for Windows builds
* chore(nix): update pnpmDeps hash after lockfile refresh
* ci(kimi-desktop): build desktop on release but do not attach to GitHub Release
The desktop build is an internal-testing artifact (branded as such), so
keep it as a CI artifact for internal download instead of publishing it
to the public GitHub Release.
* chore(kimi-desktop): mark installers as internal pre-release builds
Rename the packaged artifacts to KCD-Internal-<version>-<arch>.<ext> and
bump the version to the 0.1.1-internal.0 pre-release, so a leaked or
forwarded installer file is not mistaken for an official public release.
* feat(kimi-desktop): strengthen the internal-build tag wording
Change the sidebar tag to 'Internal testing · do not distribute' /
'内部测试 · 禁止外传' so the no-distribution intent is explicit.
* feat(kimi-desktop): tweak internal-build tag to '仅供内部测试'
* fix(kimi-desktop): pass the server token to the web UI on launch
Read the daemon's persistent bearer token from <KIMI_CODE_HOME>/server.token
and carry it in the URL fragment (#token=), matching how 'kimi web' opens
the Web UI. Without this, a fresh launch (no saved credential) boots the
web UI without a token, hits 401, and falls into the manual token dialog
even though the desktop started the daemon itself.
Addresses review feedback on the desktop URL.
The telemetry toggle was hidden unless the config explicitly set a value, and its on/off mapping treated the default (unset) state as off even though telemetry is enabled by default. Show it always, treat unset/true as on, and rename it with a description and a restart note.
* feat(timing): split TTFT into api-server and client portions
Time-to-first-token previously lumped in-process request building
(message serialization, param assembly) together with network + server
latency, making it impossible to tell whether a slow turn was the client
or the API server.
Add an `onRequestSent` hook to kosong's GenerateOptions, fired by every
provider immediately before it dispatches the network call. The window
from request start to dispatch is attributed to the client; the window
from dispatch to the first streamed token is attributed to the API
server. The split flows through the step.end / turn.step.completed
events (and therefore wire.jsonl) and is surfaced in three places:
- KIMI_CODE_DEBUG=1: `TTFT: 2.5s (api 2.4s + client 100ms)`
- session log: new `llm response` line with the timing breakdown
- vis: firstToken/api + firstToken/client rows and timeline label
The split is omitted (total only) when a provider does not report the
boundary, preserving backward compatibility.
* feat(timing): split the decode window into server vs client time
Time-to-first-token now reports a client/server split, but the slow part
of a long turn is the decode window (inter-token streaming), which was
still a single opaque number. Profiling long sessions showed decode
throughput halving over a session's lifetime independent of context
size, which the synchronous per-chunk stream pipeline can cause: kosong
awaits the host callback for every streamed part, so a loaded main
thread throttles how fast tokens are pulled off the wire.
Account for this directly in the stream loop: the time awaiting the next
part (server + network) versus the time spent processing each part
in-process (deep copy, host callback, part merge). The split is reported
through onStreamEnd and flows through the step.end / turn.step.completed
events (and wire.jsonl) into the same three surfaces as the TTFT split:
- KIMI_CODE_DEBUG=1: `TPS: 40.0 tok/s (200 tokens in 5.0s; server 4.6s + client 400ms)`
- session log: serverDecodeMs / clientConsumeMs on the `llm response` line
- vis: streamDuration/server + streamDuration/client rows and timeline label
A large, growing client share confirms host-side throttling; a dominant
server share points at the server/connection. The per-chunk accounting is
wrapped in try/finally so it stays correct across `continue` and aborts,
and is omitted when the stream reports nothing.
Typing `/\' in shell mode (`!\') now triggers file path completion instead of the slash command menu, for both a bare leading `/\' and inline paths like `ls /\'. Hidden entries are skipped to match `/add-dir\', and accepting a completion no longer produces a double leading slash.
Collapse registered workspaces that share a root in the daemon registry (preferring the canonical id) and in the web sidebar merge, so the same folder no longer renders as two identical, synchronously-selected entries.
* feat(tui): open undo selector on double-Esc
Pressing Esc twice while idle now opens the undo selector, equivalent to running /undo with no arguments. Esc during streaming, compaction, or with a popup open keeps its cancel/close behavior and does not arm the double-press.
* fix(tui): disarm double-Esc undo on any intervening key
A pending double-Esc was only cleared by text changes, so a sequence like Esc, Ctrl-C, Esc within the window still opened the undo selector. Fire an onNonEscapeInput hook for every non-Escape key and clear the pending state there, so the shortcut only triggers for two consecutive Escape presses.
Typing @ in the middle of a slash command argument (for example `/goal Fix the @checkout docs`) was swallowed by the slash-argument completion guard before the @ mention branch ran, so the file list never opened. Run the @ mention branch ahead of the slash guards so file mentions take priority; plain slash-argument editing is still suppressed as before.
* feat(vis): surface background tasks and cron jobs
The visualizer read every wire/state/blob artifact a session persists but
ignored the two on-demand families agent-core also writes under the session
directory: background tasks (tasks/<id>.json + output.log) and cron jobs
(cron/<id>.json). Neither is reconstructable from the wire, so there was no
way to inspect what a session spawned in the background or scheduled.
Server:
- task-store / cron-store read-only readers mirroring agent-core's on-disk
layout, id-validation guard, and legacy snake_case task normalization
- GET /:id/tasks, /:id/tasks/:taskId/output (byte-window paged via an exact
nextOffset cursor), and /:id/cron routes
- re-export the public background-task types from agent-core; mirror the
non-exported CronTask shape with a fixture-backed drift test
Web:
- Tasks tab: process/agent/question kinds with status, timing, kind-specific
fields, raw JSON, and a progressively paged output.log viewer
- Cron tab: expression, prompt, recurring/one-shot, created/last-fired
- count badges on both tabs
Tests: +20 (lib + route), all 113 vis-server tests green; web typecheck and
build clean.
* feat(agent-core): persist step retries and tool progress summary
Two transient signals were only ever emitted as live-only loop events, so
nothing survived in the agent record for post-hoc analysis:
- step retries: chatWithRetry gains an onRetry callback; turn-step collects
the recovered attempts and attaches them to step.end as an optional
`retries` array (previously only the live `step.retrying` event).
- tool progress: tool-call distills a tool's sparse status/percent updates
into a bounded `progress` summary (updateCount / lastStatus / maxPercent)
on tool.result. Streamed stdout/stderr is excluded — it would bloat the
wire and is already reflected in the result output.
Both are additive optional fields, so the wire protocol version is unchanged
and existing records keep loading. New public types: LoopStepRetryRecord,
LoopToolProgressSummary.
* feat(vis): add execution-analysis timeline and surface retries/progress
Turn the debugger from a flat record viewer into an analysis tool.
New Timeline tab: folds the wire into turns → steps → tool calls (client-side,
no extra round-trip) and derives the metrics the raw list hides — per-turn /
per-step / per-tool duration, per-turn token cost, a context-window fill
sparkline with cache-hit rate, a tool usage table, idle-gap detection, and a
config-change timeline.
Inline elsewhere:
- Wire rows show tool.call → tool.result elapsed time; tool.result detail
shows truncation, output size, retries, and the progress summary.
- Issues drawer gains tool-error, truncation, filtered, max_tokens, and
retried categories.
- Tasks tab links agent-kind tasks to the subagent's wire.
Wires up vitest for the web package and adds analysis/issues unit tests.
* feat(vis): import debug zips with a logs view and imported-session filtering
A `/export-debug-zip` bundle is just `manifest.json` plus a flattened session
directory, which vis already knows how to read. Importing one therefore lights
up every existing tab for a session that lives on someone else's machine.
Server:
- zip-import: yauzl extraction with zip-slip path guards and entry-count /
uncompressed-size caps for untrusted uploads.
- import-store: extract a bundle into <home>/imported/<imp_…>/, validate it
has a main wire, and record an import-meta.json sidecar.
- session-store resolves imp_-prefixed ids against imported/, so wire /
context / tasks / cron / blobs / logs all work on imported sessions; agent
homedirs are re-derived locally (the bundle holds foreign absolute paths).
- POST /api/imports (raw zip body) and GET /api/sessions/:id/logs (structured
log lines — also available for local sessions).
Web:
- session rail: import button + all/local/imported filter + imported badge.
- new Logs tab: virtualized, level filter, search, session/global toggle.
- manifest card atop the State tab for imported sessions.
SessionSummary/SessionDetail gain `imported` + `importMeta`. Tests cover
extraction, the zip-slip guard, list merge, reading an imported wire through
the existing route, and log parsing.
* fix(vis): read tasks/cron from agent homedirs and stop persisting tool status text
Addresses review feedback on the debug-tooling changes:
- Background tasks and cron jobs are persisted under each agent's homedir
(<session>/agents/<id>/tasks and /cron), not the session root. The Tasks and
Cron tabs read the session root, so they showed nothing for normal sessions.
Both routes now aggregate across detail.agents homedirs; task entries carry
the owning agentId. The route-test fixtures were writing to the wrong
(session-root) location too — corrected to the real agents/main layout so
they actually exercise the path.
- tool.result progress no longer keeps free-form status text, only updateCount
and maxPercent. A tool's status string can contain sensitive data (e.g. an
MCP OAuth authorization URL) that must not leak into persisted wire files or
exported debug bundles.
* fix(vis): stop the main content area from overflowing horizontally
The <main> flex child lacked min-w-0, so it defaulted to min-width:auto and
refused to shrink below its content's intrinsic width. Tabs that lay out in
normal flow with flex-wrap rows (the Timeline tab) then got unbounded width,
never wrapped, and blew the layout out to thousands of pixels wide. Adding
min-w-0 lets the column shrink to the available width so its content wraps,
truncates, or scrolls within its own container.
* fix(vis): resolve local global log path and imported-state agent fallback
- Logs tab: for non-imported sessions the shared global log lives at
<KIMI_CODE_HOME>/logs/kimi-code.log, not under the session dir (that path is
only used inside exported bundles). The route now reads the home path for
local sessions, so the global-log toggle works for them.
- Imported detail: a bundle's state.json is best-effort and may omit the
agents map. When the inventory is empty, fall back to discovering agents
from disk so routes that require an agent (wire/context) still resolve main.
* fix(vis): harden imported manifest and task parsing against corrupt input
An imported debug zip is untrusted, so a syntactically valid but type-corrupt
file could crash whole views:
- manifest.json: a non-string field (e.g. workspaceDir: 123) flowed into
SessionSummary.workDir, where the session rail calls .split('/') and crashed
the entire list. readManifest/readImportMeta now sanitize declared string
fields, keeping only strings.
- task JSON: a record that passed the shape guard but held a non-string legacy
field (e.g. stop_reason: 5) threw in normalization, failing GET /tasks with a
500 and hiding all of a session's tasks. optionalNonEmptyString now tolerates
non-strings, and listBackgroundTasks skips any record that still fails to
normalize — honouring the reader's documented silently-skips contract.
* fix(vis): discover rotated logs; keep tool progress on thrown failures
- Logs tab: the diagnostic log can rotate (kimi-code.log.1, .2, …) and an
exported bundle may contain only the archives. The route now discovers the
active file plus its rotated siblings and concatenates them oldest-first, so
a rotated-away log still surfaces (covered by node-sdk's rotated-export case).
- agent-core: a tool that reported sparse progress and then threw lost its
progress summary, because the catch path built the error tool.result without
it. Thread progressSummary through that path too, matching the success and
malformed-return paths.
* fix(vis): skip type-corrupt agent entries in imported state
readImportedDetail's empty-inventory fallback never ran when a bundle's
state.json had a non-empty but type-corrupt agents map (e.g.
`{ "agents": { "main": null } }`): inventoryAgents dereferenced the null entry
and threw, so readSessionDetail returned 500 instead of recovering main from
the on-disk agents/main/wire.jsonl. inventoryAgents now skips non-object
entries, letting the disk-discovery fallback take over.
* fix(vis): reset timeline agent on session change; preserve context on zero-usage steps
- Timeline tab kept the previously-selected agent id across session navigation,
so a subagent selection would 404 against the next session. Reset it to main
on sessionId change, mirroring WireTab/ContextTab.
- A zero-usage step.end (e.g. a content-filtered response) reset the
context-window fill to 0, pushing a false drop into the Timeline chart and the
Context tab. agent-core's ContextMemory keeps the prior count in that case;
the analysis lib and the context projector now do the same.
* revert: drop agent-core retries/tool-progress persistence
These were the only changes in this branch that touched agent-core. They
persisted two previously live-only signals (step retries, tool progress) to
the wire purely so the visualizer could display them — marginal features that
did not justify modifying the core loop or extending the wire surface.
Reverts the agent-core loop/type/export changes (restored to main, keeping
#1209) and its changeset, and removes the vis-side rendering and types that
consumed step.end.retries / tool.result.progress. The rest of vis is unchanged
and reads only data agent-core already persists.