Compare commits

...

162 commits

Author SHA1 Message Date
qer
0ad568436a
docs(changelog): sync 0.23.4 from apps/kimi-code/CHANGELOG.md (#1528)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-07-10 00:48:47 +08:00
qer
5a90d9d7f1
chore(agents): place Polish before Bug Fixes in changelog sections (#1527) 2026-07-10 00:08:25 +08:00
qer
7ce3c2dc31
fix(ci): pin npm to 11.x in release workflow (#1526) 2026-07-10 00:08:20 +08:00
github-actions[bot]
9f9324cdab
ci: release packages (#1513)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-09 23:45:47 +08:00
qer
ec8dc3456c
fix(web): stop sending prompts into a busy turn on the web UI (#1522)
* fix(web): prevent duplicate first prompts and keep goal drives from looking idle

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

* chore: add changeset for web first-prompt fixes

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

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

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

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

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

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

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

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

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

* chore: align changeset with actual fix scope

* test(web): update profile-patch expectation for model field
2026-07-09 23:37:10 +08:00
Kai
046b6c4175
fix(agent-core): scope [image] config limits to the owning core (#1521)
* fix(agent-core): scope [image] config limits to the owning core

* fix(agent-core): thread harness [image] max_edge_px to TUI paste and ACP ingestion

* chore(changeset): simplify entry to user-facing wording
2026-07-09 20:55:52 +08:00
qer
a3548035a8
feat(tui): add Kimi WebBridge install entry to /plugins panel (#1494)
* feat(tui): add Kimi WebBridge install entry to /plugins panel

Surface a hardcoded Kimi WebBridge entry at the top of the Official tab in the /plugins panel. Selecting it opens the WebBridge install page in the user's browser instead of going through the plugin install flow, since WebBridge is a browser extension plus local daemon rather than an installable plugin package.

* fix(tui): restrict WebBridge open-url shortcut to the pinned row

Match the hardcoded pinned WebBridge entry by object reference instead of by id. A curated or custom marketplace entry on the Third-party tab can legitimately reuse the kimi-webbridge id; routing by id hijacked Enter on those rows and opened the WebBridge page instead of installing. The Official tab still dedupes a same-id official catalog entry so the pinned row is not duplicated.

* fix(tui): label WebBridge plugins row as "open in browser"

The previous "webpage" status did not make it clear that selecting this row opens an external page rather than installing in-app. "open in browser" states the action directly and contrasts with the install label on regular plugin rows.

* test(tui): navigate past pinned WebBridge row in marketplace install tests

Two message-flow tests pressed Enter on the Official tab assuming index 0 was the Kimi Datasource entry. The hardcoded Kimi WebBridge row now leads that tab, so move down one row before installing.
2026-07-09 19:51:53 +08:00
liruifengv
170ae44205
style(web): polish the session sidebar (#1519)
* feat(web): use sidebar fold/unfold icons for sidebar toggle

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

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

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

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

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

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

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

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

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

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

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

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

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

* chore: add changeset for sidebar UI polish

* fix(nix): update pnpmDeps hash

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

* fix(nix): update pnpmDeps hash
2026-07-09 19:39:20 +08:00
Kai
1bf2c9afee
feat: keep image-heavy sessions within provider request-size limits (#1508)
* feat(kosong): classify HTTP 413 request-body-too-large as a dedicated error type

* feat(agent-core): lower default image downscale cap to 2000px and make it configurable

* feat(agent-core): strip media to text markers and retry when the compaction request is too large

* feat(agent-core): cap model-initiated image reads with a configurable byte budget

* feat(agent-core): resend with degraded media when the provider rejects the request body as too large

* test(agent-core): add explicit timeouts to encode-heavy image budget tests

* feat: add WebP decoding support with wasm integration

- Introduced a new WebP decoding module using @jsquash/webp's wasm decoder.
- Implemented functions to decode WebP images and check for animated WebP formats.
- Updated image compression tests to include scenarios for WebP handling, including encoding and decoding.
- Enhanced error handling for API request size limits to accommodate various error messages.
- Updated pnpm lockfile to include new dependencies for WebP encoding and decoding.

* chore(changeset): consolidate this PR's entries into one

* fix(nix): update pnpmDeps hash for merged lockfile

* feat(agent-core): refuse HEIC/HEIF reads with platform-matched conversion guidance
2026-07-09 18:05:14 +08:00
liruifengv
b91099ed7a
feat: display Extra Usage fuel pack balance in /usage and /status (#1501)
* feat(oauth): parse boosterWallet extra usage from /usages

* feat(oauth): expose extraUsage on AuthManagedUsageResult

* feat(kimi-code): render Extra Usage section in /usage panel

* fix(kimi-code): address Task 3 review feedback for extra usage section

* feat(kimi-code): render Extra Usage section in /status panel

* feat(kimi-code): wire extraUsage into /usage and /status commands

* chore(extra-usage): address final review findings for fuel pack feature

- Update changeset to cover both kimi-code and kimi-code-sdk packages

- Add parser clamp tests and toolkit null-case test

- Replace 'as never' casts in usage-panel tests

- Wrap long import line in status-panel

* chore: temporarily log /usages raw response for debugging

* fix(oauth): accept BOOSTER balance type and drop debug log

* fix(oauth): drop reset hint from Extra Usage and revert periodEnd parsing

* fix(oauth): treat missing amountLeft as zero extra usage and drop debug log

* revert: keep missing amountLeft defaulting to 0 (fully used)

* feat(extra-usage): show monthly cap usage bar and balance in /usage and /status

* fix(extra-usage): show balance and unlimited marker when no monthly cap

* fix(extra-usage): show monthly used with unlimited marker and balance

* fix(extra-usage): label Used and use English Unlimited

* feat(extra-usage): render balance, monthly used and monthly limit as labeled rows

* fix(extra-usage): move Balance row to the bottom

* fix(extra-usage): format currency values with two decimals for column alignment

* fix(extra-usage): right-align currency values so numbers line up

* fix(extra-usage): align currency symbol and decimal point in usage rows
2026-07-09 17:44:59 +08:00
Kai
fe9479d89a
fix: rewrite repeated tool call reminders to redirect instead of prohibit (#1518)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
The r1/r2/r3 reminders injected into repeated tool results led with
prohibition verdicts and, in r2, echoed the repeated tool name and full
arguments back into the context, reinforcing the very pattern they were
meant to break. Rewrite them to state the situation factually and hand
the model a concrete next action: an expectation-setting sentence for
the next call (r1), a forced decision menu of falsify / ask-user /
conclude (r2), and a final hand-off summary without further tool calls
(r3). Detection, thresholds (3/5/8/12), force-stop, and telemetry are
unchanged.
2026-07-09 17:06:19 +08:00
Luyu Cheng
173bdfdab1
fix: resume sessions with missing workdir (#1517) 2026-07-09 16:46:57 +08:00
Luyu Cheng
9fb19154ac
fix: keep prompt goals running until terminal (#1516)
* fix: keep prompt goals running until terminal

* fix: reject invalid prompt goal commands

* fix: ignore stale prompt goal status checks
2026-07-09 15:54:50 +08:00
Luyu Cheng
ad30a1c632
style(web): polish web UI typography and controls (#1502)
* style(web): polish sidebar and tool typography

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

- Add reusable shortcut and tool output blocks

- Cap long tool output at 50 lines with a scrollbar

- Update sidebar show-more copy and muted styling

* style(web): refine workspace picker sizing

* style(web): align composer mode menus

* style(web): tune list and question typography

* style(web): reuse shortcut keys in approvals

* style(web): size workspace picker from content

* feat(web): localize chat status labels

* style(web): refine composer toolbar controls

* style(web): use complete Inter variable font

* style(web): tune sidebar workspace typography

* style(web): polish composer and workspace picker

* style(web): refine markdown and thinking typography

* chore: add web UI polish changeset

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

* style(web): polish goal tool calls

* style: polish goal mode display

* fix: layer latest message pill below menus

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

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

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

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

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

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

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

- Update module header to describe all three notification kinds

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

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

* chore(kimi-web): simplify changeset
2026-07-09 12:30:15 +08:00
liruifengv
b89fc1a4fb
docs(changelog): sync 0.23.3 and shorten OAuth error entry (#1509)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-07-08 23:33:29 +08:00
github-actions[bot]
93c0b7bb78
ci: release packages (#1507)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-08 23:18:00 +08:00
7Sageer
e83511a711
fix: surface provider auth error for unavailable models (#1506)
* fix: surface provider auth error for unavailable models

When an OAuth-managed model returns 401 after a forced token refresh, the token is valid but the provider rejected it for that model (the account lacks access). Emit provider.auth_error carrying the provider's message instead of auth.login_required with a misleading "OAuth login expired. Send /login" prompt.

* fix(agent-core): preserve provider auth errors through compaction

Treat provider.auth_error like auth.login_required in the compaction path so an auth rejection during compaction surfaces the provider's message instead of being wrapped as a generic compaction failure.
2026-07-08 21:16:44 +08:00
qer
2394d013bc
docs(changelog): sync 0.23.2 from apps/kimi-code/CHANGELOG.md (#1496) 2026-07-08 17:02:00 +08:00
github-actions[bot]
67b2147d8e
ci: release packages (#1468)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-08 15:37:29 +08:00
qer
b0809ddac8
feat(web): prefix skill slash commands with skill: to distinguish them from built-in commands (#1492) 2026-07-08 15:03:30 +08:00
qer
0cc9831a2f
fix(web): composer model switch also updates global default model (#1491)
* fix(web): composer model switch also updates global default model

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

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

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

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

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

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

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

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

* chore(web): simplify cron redesign changeset
2026-07-08 13:42:22 +08:00
qer
b30a45efec
feat(web): support Enter key to confirm archive and other dialogs (#1490) 2026-07-08 13:24:44 +08:00
qer
2206d21327
feat(plugins): add Vercel plugin to marketplace (#1489) 2026-07-08 12:22:28 +08:00
qer
f30781bb27
fix(kimi-code): exit 1 when a headless (-p) turn fails (#1483)
Some checks are pending
CI / typecheck (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Headless (`kimi -p`) failures could exit with code 0 when the event loop
drained during the shutdown cleanup (e.g. telemetry's unref'd retry backoff
when the network is blocked), because the rejection never reached the
process.exit(1) call. Set the failure exit code before any await in both
the run-prompt catch and the main catch, and keep the cleanup timeout ref'd
so the loop stays alive long enough for the rejection to propagate.
2026-07-07 23:38:20 +08:00
STAR-QUAKE
9b76e5bff6
feat(agent-core): discard loaded tool schemas on compaction (#1471)
Align progressive tool disclosure with the discard-on-compaction model:
compaction no longer rebuilds loaded dynamic tool schemas. The boundary
announcement re-lists every loadable name, the model re-selects what it
still needs, and a from-memory call to a no-longer-loaded tool is
rejected by preflight with select guidance.

This removes the keep-all rebuild and its half-trigger budget heuristics
entirely: the post-compaction floor is back to users + summary, which is
structurally outside the auto-compaction trigger band, and the guard
baseline degenerates to summary + reinjected reminders. Every downstream
mechanism already treated the empty loaded set as its consistent base
state (ledger scan, pending clear at the compaction boundary, deferred
extras, preflight wording), so this is a strict simplification.

Co-authored-by: fengchenchen <fengchenchen@moonshot.ai>
2026-07-07 23:06:29 +08:00
Luyu Cheng
131700097a
fix: clarify goal blocked audit guidance (#1481) 2026-07-07 22:29:40 +08:00
_Kerman
6c9abe8cf7
feat(kosong): support structured response formats (#1397) 2026-07-07 22:02:29 +08:00
Luyu Cheng
150206a6f7
fix: count goal creation turn (#1477) 2026-07-07 21:51:29 +08:00
Kai
474ce289dd
fix(agent-core): report EXIF-rotated image dimensions and raise edge cap to 3000px (#1460)
* fix(agent-core): report EXIF-rotated image dimensions and raise edge cap to 3000px

Image compression now reports original dimensions in the decoded
(EXIF-rotated) space, matching the coordinate system of the sent image
and of ReadMediaFile region readback; previously portrait JPEGs
(orientation 5-8) got swapped width/height in captions. The longest-edge
downscale cap rises from 2000px to 3000px, and the default jimp resize
path is documented as the anti-aliased area-average one so it is not
accidentally switched to a point-sampled interpolation mode.

* test: shrink oversized image fixtures to fit CI timeouts

The 3600x3600 fixtures introduced for the 3000px edge cap nearly doubled
the pixel area jimp has to decode and deflate, pushing the slowest
compression tests past the 5s vitest timeout on CI runners. 3600x1800
keeps every fixture over the cap while restoring roughly the workload of
the old 2600x2600 fixtures that CI handled comfortably.

* test: pin anti-aliased downscale quality with executable guards

A 1px checkerboard probe pins the compressor to full-coverage averaging
at integer and fractional ratios, with jimp's point-sampled BILINEAR
mode kept as the executable aliasing counter-example (it collapses the
50%-gray pattern to solid black at 4:1). Also guards the other classic
downscale bugs: transparent-pixel color bleed, mean-brightness drift,
iterative recompression degradation, and zero-size collapse on extreme
aspect ratios.

* fix(agent-core): report decoded EXIF-rotated dimensions in ReadMediaFile notes

The media note derived its original-dimensions line from the header
sniff, which reports pre-rotation values for EXIF orientation 5-8
JPEGs. The sent image and region readback both live in the decoded
(rotated) space, so portrait photos got axis-swapped coordinate
guidance. Once a decode has happened — compression or crop — its
dimensions now overwrite the sniffed ones.

* fix(agent-core): improve handling of EXIF orientation in image dimensions and metadata

* fix(agent-core): sniff EXIF orientation and step budget fallback through 2000px

Two follow-ups to the EXIF and 3000px-cap changes:

sniffImageDimensions now reads the JPEG EXIF Orientation tag (pure
header parse, both byte orders) and reports display-space dimensions
for orientations 5-8. Passthrough images — never decoded — previously
kept the pre-rotation header size in compression results and media
read notes, disagreeing with the decoded space that region readback
uses.

encodeWithinBudget steps the over-budget fallback through 2000px
before the 1000px last resort. Raising the cap to 3000px had left a
regression window: an image whose 2000px encode fits the byte budget
was sent at 1000px where the old 2000px cap used to send it at
2000px.

* fix(kimi-code): record pasted image dimensions in display space

The TUI paste path recorded attachment and original dimensions from its
raw header parser, which ignores EXIF orientation. For a portrait JPEG
the submit-time caption then contradicted the sent image's aspect and
region readback coordinates were axis-swapped. Dimensions now come from
the compression result, which reports display space on both the
compressed and passthrough paths; parseImageMeta remains only the
format/mime gate.

* feat(agent-core): add image compression and crop telemetry

Every image ingestion path now reports an image_compress event —
outcome (compressed / passthrough fast, guard, unsupported, unhelpful,
error), input/output formats, byte and pixel sizes, EXIF transposition,
and duration — and region readback reports an image_crop event with a
failure classification and the region's share of the original area.

Wiring is per call site via a new CompressImageOptions.telemetry
option, so the outcome split and timing are measured inside the
compressor while each caller only names its source: ReadMediaFile
(tool construction, like GrepTool), MCP tool results (McpOutputOptions),
server prompt ingestion (ICoreProcessService now exposes the host
telemetry client), ACP prompts (session track adapter), and TUI paste
(host.track adapter). Properties are numeric/enum only — never paths
or content — and a throwing client can never affect the compression
result.

* fix(agent-core): run the full JPEG quality ladder at fallback sizes

The fallback rescales encoded only at quality 20, so a JPEG whose
ladder failed at the fitted size collapsed straight to the lowest
quality even when the smaller size left budget headroom for a higher
rung (the realistic window is the 1000px step, where the 4x pixel
drop pays for q80/q60). Each fallback edge now walks the same
q80-to-q20 ladder as the fitted size.

* test: shrink heavy JPEG fixtures and add explicit timeouts

The fallback-ladder test runs ~11 pure-JS JPEG encodes and the EXIF
paste test decodes, rotates, and re-encodes a 6.5MP frame; both sat at
the edge of the 5s vitest timeout on CI runners. Narrower fixtures cut
the pixel area (the ladder test keeps its width above 2000px so the
full fallback chain still runs) and explicit 15s timeouts absorb runner
variance.

* fix(server): scope prompt image compression telemetry to the session

The prompt-ingestion image_compress events were emitted with the bare
host telemetry client, while every agent-side source inherits a
session-scoped client — so prompt_inline/prompt_file events could not
be correlated with their session. The route now wraps the client with
withTelemetryContext({ sessionId }) like rpc/core-impl does for
session telemetry.

* chore(changeset): consolidate image compression changesets

One entry covering the cap raise and the EXIF dimension fix, listed
for both the CLI and the SDK so the SDK changelog's compression
description (previously pinned at 2000px) stays accurate.
2026-07-07 21:38:43 +08:00
qer
11c6a37ce0
fix(web): dismiss stale connection error toast after ws reconnect (#1474) 2026-07-07 20:47:56 +08:00
7Sageer
2065ae4615
fix(telemetry): gate unsafe numeric telemetry values (#1478)
- Drop numeric telemetry properties whose absolute value exceeds Number.MAX_SAFE_INTEGER before enqueue, and reject them again during payload assembly as a backstop.

- Omit constrained_memory_bytes when process.constrainedMemory() is not a safe non-negative integer so Linux no-cgroup sentinels do not overflow int64 parsing downstream.

- Add telemetry tests for unsafe numeric properties and constrained_memory_bytes filtering.
2026-07-07 20:32:02 +08:00
Luyu Cheng
d1a964fba9
fix: forbid model-driven goal pauses (#1476) 2026-07-07 20:02:43 +08:00
7Sageer
ebd25a4a55
chore(telemetry): sample system metrics every 5 minutes (#1472)
Raise SystemMetricsCollector's default sampling interval from 30s to 5min to reduce the frequency of system_metrics telemetry events.
2026-07-07 18:33:24 +08:00
liruifengv
ee385456d0
refactor(web): migrate icons to unplugin-icons (#1467)
* refactor(web): migrate icons to unplugin-icons

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

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

* refactor(web): drop unused NAME_TO_REMIX icon mapping

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

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

* chore: add changeset for web icon migration

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

Adding unplugin-icons changed pnpm-lock.yaml, so the fixed-output
pnpmDeps derivation hash is stale. Update to the hash reported by the
Nix Build CI run.
2026-07-07 16:44:56 +08:00
qer
2a44ae075a
chore: update 0.23.1 changelog (#1469)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-07-07 16:22:40 +08:00
liruifengv
063bce2a2f
fix(agent-core): hide console window when running hooks on Windows (#1466)
Pass windowsHide:true when spawning the hook process so a visible console
no longer flashes and steals focus on Windows. The Bash-tool path was
already hardened (KAOS buildLocalSpawnOptions); the hook runner missed the
flag even though its own taskkill helper already set it.

Extract the spawn options into a pure builder and add a regression test
asserting windowsHide, mirroring the existing KAOS spawn-options test.

Relates to #1298.
2026-07-07 15:55:47 +08:00
github-actions[bot]
08b3c3fc53
ci: release packages (#1444)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-07 15:36:15 +08:00
Luyu Cheng
e9ef9399d0
fix(agent-core): harden goal-mode budget and outcome flow (#1456) 2026-07-07 15:23:57 +08:00
liruifengv
bfdbce593f
feat(kosong): honor explicit anthropic max output override (#1465)
* feat(kosong): honor explicit anthropic max output override

Add claude-opus-4-8 output ceiling and treat explicit max_output_size/KIMI_MODEL_MAX_OUTPUT_SIZE as the final Anthropic max_tokens value. Sync configuration and environment variable docs.

* feat(kosong): drop claude-opus-4-8 ceiling

Revert the newly added claude-opus-4-8 default output ceiling while keeping explicit max_output_size overrides for Anthropic.
2026-07-07 15:23:42 +08:00
Luyu Cheng
03e78ae190
fix(kosong): fall back to nearest lower catalogued minor for max tokens (#1463)
* fix(kosong): fall back to nearest lower catalogued minor for Claude max tokens

An uncatalogued Claude minor version (e.g. claude-opus-4-8) previously
dropped straight to the family/major baseline ceiling, so Opus 4.8
resolved to 32k max output tokens instead of the 128k it supports.
Walk the minor version down to the nearest catalogued entry before
using the family baseline, so a newer minor inherits its predecessor's
documented ceiling.

* fix(kosong): catalogue Opus 4.8's documented 128k output ceiling

Known models should resolve from explicit table entries; the
nearest-lower-minor fallback now only covers minors that are not yet
catalogued. Fable 5 already has an explicit entry.
2026-07-07 15:11:12 +08:00
qer
928726ace1
chore: correct wire-request-trace changeset (#1464)
PR #1448 records a per-request trace in wire.jsonl for debugging, with
no user-facing behavior change. Downgrade the CLI bump from minor to
patch, drop the unpublished private internal packages (agent-core,
kosong), and trim the entry to a single honest sentence.
2026-07-07 14:59:19 +08:00
qer
16dc940834
fix(web): recover from stale background websocket (#1451) 2026-07-07 14:48:22 +08:00
qer
809a88cb34
fix(web): run new-session slash actions (skills, /goal, /btw) from the empty composer (#1445)
* fix(web): activate slash skills on the new-session screen

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Three follow-ups from review:

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

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

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

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

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

Clear the staged goalModeBySession[sid] flag right after the explicit
update, since `/goal <objective>` has exactly the same effect as the
flag's consumption.
2026-07-07 14:18:40 +08:00
Kai
65d30177ad
feat(agent-core): record llm request trace in wire.jsonl (#1448)
* feat(agent-core): record llm request trace in wire.jsonl

Add three observability record types so every request sent to the model
can be reconstructed from the wire log at the logical-request level:

- llm.tools_snapshot: content-addressed snapshot of the top-level tools
  table as sent (post deferred-strip), written once per unique table
- llm.request: one record per outbound request (retries, strict resends,
  and compaction rounds included) carrying the effective request params
  and hash links to the system prompt and tools snapshot
- mcp.tools_discovered: the server's verbatim tools/list result plus the
  agent's gating (allow-list, collisions), deduplicated by content hash

Observability records never feed state rebuild; replay only restores the
write-dedup cursors. The records/types.ts contract now documents the two
record classes explicitly (persisted is not the same as replayed).

Recording happens at the single Agent.generate choke point. The
LLMRequestLogFields side channel gains kind/projection/maxTokens/
droppedCount, chatWithRetry preserves caller-set fields, and compaction
tags its requests. The vis wire view renders the new record kinds.

* fix(agent-core): record the provider-clamped completion cap in the request trace

The llm.request trace recorded the client-requested budget cap, but
chat-completions providers tighten the actual wire value inside
withMaxCompletionTokens (remaining-context sizing, transport ceilings,
model-default resolution) — with the default budget the clamp is active
on nearly every non-empty-context request, so the recorded value did not
match what was sent.

Providers now expose the effective cap they computed as a readonly
maxCompletionTokens field on the clone, and the recorder reads it from
the effective provider at the Agent.generate choke point. This replaces
the side-channel recomputation, which is removed along with the
appliedCompletionBudgetCap helper.

* fix(agent-core): park pre-replay MCP discovery records and hash the collision outcome

Two wire-hygiene fixes for the mcp.tools_discovered trace:

Parking: the real Session ordering connects MCP servers concurrently with
agent construction, so ToolManager can observe a connected server before
agent.resume() has replayed the wire. Recording at that point bypassed
the restored dedup cursor (duplicating a 1-50KB record on every resume)
and appended a stray metadata record ahead of replay. AgentRecords now
exposes a one-shot opened latch — set when replay completes (after the
migration rewrite flushes) or when the first live record is logged — and
ToolManager parks discoveries until then, re-running the dedup check at
drain time. A frozen range-limited replay never opens; those agents are
transient previews.

Collision hashing: the dedup hash now covers the collision outcome, not
just the raw list and allow-list. Collisions depend on which other
servers hold a sanitized qualified name at registration time, so a
server can re-register with identical tools but a flipped outcome; that
gating change must produce a new record instead of being suppressed.

* fix(agent-core): skip the request trace for pre-flight-aborted calls

Mirror kosong generate()'s pre-flight abort check at the Agent.generate
choke point: a call whose signal is already aborted never reaches the
wire (generate throws before dispatching), so it must not leave an
llm.request/llm.tools_snapshot trace or a diagnostic log line claiming a
request was sent. Recording stays before dispatch for every call that
passes the gate, preserving the crash-safety of the trace.

* chore(agent-core): remove a leftover adaptive-thinking override hook

The adaptiveThinkingOverride option was a temporary local hook explicitly
marked for removal before commit. Nothing passes it, so resolution falls
back to the alias-level adaptiveThinking value in all cases; drop the
option and the dead indirection.

* fix(kosong): derive the exposed completion cap from generation kwargs

maxCompletionTokens was a field stored only by withMaxCompletionTokens,
so caps that reach the wire through other paths were invisible to the
request trace: with completion budgeting disabled via env, Anthropic
still sends the constructor-resolved max_tokens (required by the
Messages API), and constructor-level kwargs like OpenAILegacyOptions
maxTokens were likewise unreported.

Replace the stored field with a getter derived from each provider's
generation kwargs — the single source the request body reads — covering
constructor defaults, direct withGenerationKwargs configuration, and
budget application in one place. Kimi mirrors its request-time legacy
max_tokens alias normalization; openai-legacy reuses the same
normalizeGenerationKwargs the request path uses.

* feat(agent-core): add thinkingKeep passthrough for Kimi providers and update tests
2026-07-07 14:09:19 +08:00
qer
9b18dc46cf
fix(kimi-web): shrink collapsed thinking block on mobile (#1461) 2026-07-07 14:04:00 +08:00
liruifengv
8d58d6ad4c
chore(dev): open monorepo root when running make dev (#1458)
* chore(dev): open monorepo root when running make dev

The dev runner spawned the tsx child process with cwd pinned to
apps/kimi-code, so the TUI always opened apps/kimi-code as its working
directory. Switch the child cwd to the monorepo root and pass the
tsconfig, raw-text loader, and entry point as absolute paths so their
resolution does not depend on cwd.

This only affects the local dev runner; the published CLI is unchanged.

* fix(dev): pass --import preload as a file URL

Node resolves `--import` preloads as ESM specifiers. On Windows, the
absolute path produced by path.resolve() looks like a `c:` URL scheme
and crashes the dev CLI with ERR_UNSUPPORTED_ESM_URL_SCHEME before
src/main.ts loads. Convert the preload to a file:// URL via
pathToFileURL so it resolves on every platform.

Addresses codex review on PR #1458.
2026-07-07 13:37:40 +08:00
qer
166e404a92
chore: downgrade #1432 changeset to patch (#1453) 2026-07-07 13:03:11 +08:00
liruifengv
260a80793a
fix(cli): respect --skills-dir in interactive mode (#1457)
The interactive shell entry point dropped opts.skillsDirs when building the harness, so --skills-dir only took effect in prompt mode. Forward it so the flag works in the TUI as documented.
2026-07-07 13:02:57 +08:00
liruifengv
6916240ae3
docs(changelog): remove contributor thanks credit from docs and update skills (#1454) 2026-07-07 12:49:27 +08:00
Haozhe
244ec077f9
fix(agent-core): remove print-mode subagent drain deadline (#1452)
- drop the session-wide absolute drain deadline that gated print-mode turn holds
- hold the turn until background subagents reach a terminal state, bounded by each subagent's own timeout
- fixes late or long-running subagents being abandoned (results suppressed) in long `kimi -p` runs
2026-07-07 12:37:14 +08:00
liruifengv
7a65e0d1c0
feat(tui): update permission mode copy and reorder /auto /yolo (#1450) 2026-07-07 12:26:25 +08:00
Kai
743f66e547
refactor: move tool-result metadata into a structured note side channel (#1437)
* fix: stop rendering <system> notes from tool results in the terminal and web UIs

Tool results carry <system> blocks as side-channel notes for the model (ReadMediaFile summaries, Read status, MCP image captions, error/empty sentinels). Keep them in history for the model, but strip them at every core-to-UI boundary so they no longer render as plain text. vis is intentionally left untouched to preserve the model's-eye view for debugging.

* fix: keep error/empty status text visible when stripping tool-result <system> tags

Unwrap the tool error/empty sentinels (<system>ERROR: ...</system>, <system>Tool output is empty.</system>) instead of deleting them: keep the human-readable text and drop only the tags. Otherwise a failed or empty tool result rendered as a blank output, indistinguishable from a rendering bug. The model still reads the wrapped form in history.

* refactor: move tool-result metadata into a structured note side channel

Tool-produced model-facing metadata (ReadMediaFile summaries, Read status
lines, MCP image-compression captions) was baked into tool output as
<system> text, so every UI had to strip it back out and three copies of
the model-view normalization had silently drifted apart.

- ExecutableToolResult gains `note`: content rendered to the model but
  never to UIs; records and history now store the raw output plus the
  structured isError/note fields
- the model view is rendered exactly once at the LLM projection boundary
  by renderToolResultForModel; the transcript and vis hand-copies are
  deleted (vis now calls the same function for its model view, fixing
  their drifted empty-output checks)
- ReadMediaFile / Read / MCP captions write `note`; tool outputs stay
  pure data, and text-only results keep a single text part (note joined
  with a newline) so provider tool content stays a plain string
- all UI-side <system> stripping is removed; failed tools show their own
  error text with the structured isError flag
- wire protocol 1.4 -> 1.5 migrates existing records' tool-produced
  <system> blocks into `note` on resume

* fix: provider-neutral wording, no wire migration, direct optional fields

- "The attached image was downsampled" replaces directional wording that
  depended on provider serialization order (inline media vs flatten-and-
  re-attach)
- drop the 1.4 -> 1.5 wire migration: legacy records replay verbatim, so
  the model view of old sessions stays byte-identical to what the model
  originally saw and UIs show the legacy <system> text as-is; this also
  removes the risk of the migration misclassifying user data that quotes
  tool metadata, and the additive note field needs no version bump
- pass optional result fields as undefined instead of conditional spreads
  (repo convention)

* fix: enforce the note contract at the trust boundary; narrow the TUI system-tag guard

- normalizeToolResult now keeps a note only when it is a non-empty string:
  tools and finalize hooks are arbitrary JS, and a malformed note (null,
  number, object) would previously persist into the record and crash every
  subsequent LLM projection of the session. Everything downstream now
  trusts note to be string | undefined.
- the TUI tool body suppression matches the full <system-reminder> tag
  instead of any <system prefix: reminder piggy-backing stays hidden,
  while real output that merely starts with a literal <system> tag (file
  contents, MCP text) stays visible, covered through the real
  ToolCallComponent path.

* fix: return MCP compression captions as data instead of extracting them from text

compressImageContentParts now returns { parts, captions } — captions come
back from the compressor as structured data and are never inserted into
the parts, so the MCP pipeline no longer pattern-matches text to move
them into the note side channel. Tool output that merely quotes a
caption (a doc, a log, a test fixture) stays verbatim in the output.
Also corrects the stale claim that prompt ingestion uses this helper
(it compresses per image while constructing the part).

* docs: correct the image-compression re-export comment; export CompressedContentParts

The package-root comment still described compressImageContentParts as the
input-stage helper every ingestion site calls; prompt ingestion compresses
per image with compressBase64ForModel / compressImageForModel, and the MCP
pipeline is the walker's only caller. Also export the walker's
CompressedContentParts return type so public-API consumers can name it.

* feat: wrap tool status sentinels in <system> so the model can tell harness verdicts from tool output

The error/empty status text is model-only after the note refactor (UIs
render the raw output and style failures via the structured isError
flag), so the earlier plain-text wording served no remaining audience.
Wrapping the statuses in <system> gives every piece of system-generated
text inside a tool result the same marker:

- failed calls get '<system>ERROR: Tool execution failed.</system>'
  unconditionally — the ERROR:-prefix guard is removed, so the harness
  verdict can no longer be confused with tool output that happens to
  start with error-like text
- empty outputs render as '<system>Tool output is empty.</system>'; the
  plain placeholder the loop layer bakes into records is still
  recognized and upgraded at projection time

* style: collapse an internal helper docstring per the services subtree convention
2026-07-07 11:40:27 +08:00
qer
4aeb33637f
docs(changelog): sync 0.23.0 from apps/kimi-code/CHANGELOG.md (#1446)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-07-06 23:59:08 +08:00
Kai
25a655cf88
feat(agent-core): enable Preserved Thinking by default on the Anthropic provider (#1432)
* feat(agent-core): enable Preserved Thinking by default on the Anthropic provider

Default thinking.keep to "all" for the Anthropic provider (Claude and Kimi in Anthropic-compatible mode) while Thinking is on, via a context_management clear_thinking_20251015 edit, mirroring the Kimi default. Reuses [thinking] keep and KIMI_MODEL_THINKING_KEEP (env > config > default "all"); off-values disable it.

* feat(kosong): route Anthropic Preserved Thinking through the beta Messages API

Force the beta endpoint (client.beta.messages.create) when thinking.keep is enabled, since clear_thinking_20251015 is only honored there. Also prepend clear_thinking to any existing context-management edits (for example clear_tool_uses) instead of replacing them, keeping it first as Anthropic requires when combining edits.

* docs: clarify Anthropic beta endpoint and compaction keep behavior

Note in code comments and bilingual docs that enabling Anthropic Preserved Thinking routes requests to the beta Messages API (client.beta.messages.create), with keep=off as the escape hatch back to the standard endpoint. Correct the resolveThinkingKeep comment to reflect that compaction shares ConfigState.provider and intentionally carries the same keep.

* test(kosong): cover Anthropic beta endpoint (streaming and forced betaApi)

Add a streaming beta-endpoint capture and a test that withThinkingKeep forces the beta endpoint even when constructed with betaApi: false, pinning down the documented behavior.
2026-07-06 23:45:17 +08:00
github-actions[bot]
379bc57ef0
ci: release packages (#1378)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-06 23:18:32 +08:00
qer
c4c2fef162
chore(skills): merge similar changelog entries and credit external contributors (#1442) 2026-07-06 23:18:09 +08:00
qer
903e8ed93a
fix(web): clarify archive session confirmation copy (#1428)
* fix(web): clarify archive session confirmation copy

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix(web): handle swarm review feedback

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

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

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

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

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

Address the two latest swarm review comments:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix(web): preserve loaded older messages when reopen snapshots apply
2026-07-06 21:30:25 +08:00
qer
4aacddc432
fix(kimi-web): clarify desktop notification title and icon (#1434) 2026-07-06 21:18:45 +08:00
7Sageer
dd9077595d
chore(agent-core): classify turn_interrupted telemetry cause (#1431)
Add an `interrupt_reason` field to the `turn_interrupted` telemetry event so the data can tell a deliberate user cancel (`user_cancelled`) apart from a programmatic abort (`aborted`), max-steps exhaustion (`max_steps`), an error (`error`), or a hook-filtered turn (`filtered`).

The user-cancel signal comes from the existing UserCancellationError carried as the abort signal's reason, reused here without changing any loop control or external protocol semantics.
2026-07-06 20:52:40 +08:00
Haozhe
6c0ce09414
feat(server): support restoring and listing archived sessions (#1073)
* feat(server): support restoring and listing archived sessions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

* chore: add changeset for kimi-web UI fixes

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

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

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

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

* fix: scale web font-size dependents

---------

Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-07-06 17:29:23 +08:00
liruifengv
1de028612c
fix(tui): keep Edit preview height stable as results land (#1421) 2026-07-06 16:57:52 +08:00
liruifengv
5ea3ec489e
fix(tui): stabilize Bash card height and distinguish command from output (#1419)
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".
2026-07-06 16:50:07 +08:00
Kai
c12f30951f
feat(agent-core): feed AskUserQuestion answers back as question text and option labels (#1414)
* feat(agent-core): feed AskUserQuestion answers back as question text and option labels

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

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

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

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

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

The translator's option-id lookup was a single flat map across all
questions, so a stale or malformed response pairing one question with
another question's option id (q_1 + opt_0_0) was silently translated
into a label that was never offered for that question. Scope the lookup
to the answered question's own options; cross-question and unknown ids
now both pass through verbatim, staying diagnosable.
2026-07-06 16:37:54 +08:00
Kai
f7b557732b
chore: symlink CLAUDE.md to AGENTS.md for compatibility (#1420)
Some agent tooling looks for a CLAUDE.md at the repo root. Add a
symlink to the canonical AGENTS.md so both filename conventions
resolve to the same instructions, avoiding any duplicated content.
2026-07-06 16:21:52 +08:00
STAR-QUAKE
f0896a53b0
feat(agent-core): progressive tool disclosure via select_tools (#1369)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(agent-core): progressive tool disclosure via select_tools

Keep MCP tool schemas out of the immutable top-level tools[] and let the
model load them on demand, preserving the provider prompt cache:

- kosong: Message.tools (append-only load primitive, serialized as Kimi
  messages[].tools with type:function wrapping and no content),
  Tool.deferred (stripped once in generate() so loaded tools stay
  executable without re-entering the top level), select_tools capability
  bit (UNKNOWN/catalog default false).
- select_tools builtin: load-by-exact-name, three-branch semantics
  settled per name (Loaded / Already available / Unknown), schemas read
  from the live registry, injection-origin schema messages survive undo.
- ToolsDiffInjector: <tools_added>/<tools_removed> announcements at turn
  boundaries and post-compaction, folded from history (undo/compaction/
  resume self-heal), appended only when the loadable set changes.
- Loaded-tools ledger = history scan + defer-window pending set (cleared
  on /clear); loop re-reads the executable table per step so a selected
  tool dispatches on the next step of the same turn; preflight
  distinguishes not-loaded from loaded-but-disconnected.
- Cross-cuts: projection strips protocol context for non-select_tools
  models (lossless mid-session model switch both ways), compaction
  filters it from the summarizer input and rebuilds loaded schemas
  keep-all after folding, token estimation counts message.tools, request
  logging reflects the post-strip wire tools.
- Three-condition gate: capability.select_tools x capability.tool_use x
  tool-select experimental flag (KIMI_CODE_EXPERIMENTAL_TOOL_SELECT).
  Any gate closed reproduces the inline request byte-for-byte; all
  current models keep the capability off, so behavior is unchanged
  until a supporting model is catalogued. The SDK catalog-to-alias
  mapping forwards the capability so catalog-driven setups can enable it.

* feat(kosong): skip tool-declaration-only messages in non-Kimi providers

Message-level tool declarations (messages[].tools) are a Kimi wire
feature. The other providers' explicit field construction already keeps
the tools field off the wire, but the content-free leftover message
would be rejected (OpenAI: system message without content) or serialize
as a garbage <system></system> turn (Anthropic/Google system-to-user
wrapping). Skip such messages entirely via a shared predicate; a message
that also carries content only loses the tools field, as before.

Unreachable in kimi-code (the projection gate strips dynamic-tool
context for models without the select_tools capability before any
provider sees it) — defense-in-depth for direct kosong consumers.

* fix(agent-core): survive runtime flag flips and align tool table with post-compaction state

Two fixes from PR review:

- Register select_tools unconditionally and gate only its exposure in
  loopTools. The tool-select flag can flip at runtime (config reload
  calls setConfigOverrides on the live resolver) without
  initializeBuiltinTools re-running; previously the disclosure shape
  activated while the tool itself was unregistered, cutting the session
  off from MCP entirely until a model/cwd change rebuilt the builtins.
  A profile listing the name explicitly still never surfaces it in
  inline mode, and execution guards the flip race defensively.

- Resolve the per-step tool table AFTER beforeStep, next to
  buildMessages. beforeStep can run full compaction, which trims loaded
  schemas and rewrites the ledger; a table captured before it could
  still dispatch a tool whose schema the model no longer has. The
  executable table and the request messages now always reflect the same
  state, so a trimmed tool is rejected with select guidance instead of
  executed.

* fix(agent-core): drop unused Tool import in dynamic-tools

* fix(agent-core): baseline compaction guard after post-compaction reinjection

The reinjected reminders (loadable-tools manifest, goal) are re-appended
after every compaction, but the nothing-new-since-compaction baseline was
captured before injectAfterCompaction. With a large manifest the guard
could re-trigger auto-compaction against a floor that cannot shrink.
Raise the baseline to the true post-compaction floor once reinjection
completes; the earlier capture stays as a fallback when reinjection
throws.

---------

Co-authored-by: fengchenchen <fengchenchen@moonshot.ai>
2026-07-06 15:51:08 +08:00
Kai
79b360c96a
feat(thinking): enable Preserved Thinking by default for kimi models (#1417)
Default `thinking.keep` to "all" when Thinking is on so prior `reasoning_content` is kept across turns. Add `[thinking] keep` to config.toml and keep `KIMI_MODEL_THINKING_KEEP` as an override (env > config > default); off-values disable it.
2026-07-06 15:27:08 +08:00
liruifengv
8c7e6da582
chore(changelog): enable thanks credit in changelog (#1418) 2026-07-06 15:26:03 +08:00
liruifengv
913d042208
fix(tui): keep input anchored after slash command menu closes (#1413)
* 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.
2026-07-06 15:18:27 +08:00
liruifengv
353c0885e5
chore: remove unused kimi-migration-legacy package (#1415)
This package only ever contained a package.json with no sources, dependencies, or scripts, and nothing in the repo imports it (the CLI uses @moonshot-ai/migration-legacy instead). Remove the directory and drop its entries from flake.nix and the changeset config, then refresh the lockfile.
2026-07-06 15:09:27 +08:00
liruifengv
fc259abdb4
fix(tui): complete @ file mentions across additional workspace roots with fd (#1408)
* 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
2026-07-06 15:02:02 +08:00
qer
e6e6dd53ce
fix(web): stop replaying the bubble entrance animation on session open (#1411) 2026-07-06 13:58:44 +08:00
liruifengv
1c817df1e5
fix(tui): include surrounding context in edit approval preview (#1410) 2026-07-06 13:50:23 +08:00
liruifengv
b9258ee07d
feat(tui): show compaction summary with Ctrl-O (#1346)
* feat(tui): show compaction summary with Ctrl-O

* docs: document compaction summary toggle

* fix(tui): preserve compaction summary expansion state

* fix(tui): preserve compaction summary expansion across replay and theme changes
2026-07-06 13:32:37 +08:00
qer
ce41f4b58d
fix(web): keep Sidebar single-root so collapse hides it (#1406)
A top-level Teleport in the sidebar template made the component multi-root, so v-show could not apply display:none and the collapsed sidebar stayed mounted at the rail width, squeezing the conversation. Move the teleport inside the aside so the sidebar is single-root again.
2026-07-06 12:39:58 +08:00
迷渡
4c43935e31
fix: show Windows session search shortcut (#1393) 2026-07-06 12:08:08 +08:00
qer
c5c6282f44
feat(web): render AskUserQuestion result as an option list (#1391)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(web): render AskUserQuestion result as an option list

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

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

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

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

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

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

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

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

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

* fix(web): localize AskUserQuestion result labels

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

- persist workDir into state.json so session dirs are self-describing and
  summaries do not depend on the index's one-way-hashed workDir
- relax readSessionIndex so a stale or non-absolute index workDir no longer
  drops an otherwise valid entry
- serialize in-process index appends to avoid torn jsonl lines
- add SessionStore.reindex() and run it once at server boot so the
  scan-free request path can find sessions whose index line is missing
  or stale

* chore: add changeset for session index rebuild

* fix(session): repair index entries with a stale workDir during reindex
2026-07-05 14:06:33 +08:00
Haozhe
ebdffc7df7
fix(gemini): fix Gemini tool calling and thought-signature round-trip (#1389)
- send tool declarations, system prompt, and sampling/thinking settings in the camelCase shape the Google SDK forwards so tool calls reach the model
- thread tool-call extras (thought signatures) through the loop tool-call event into context so Gemini 3 can resume a tool turn
- update and add tests for the corrected request shape and signature round-trip
2026-07-05 14:00:09 +08:00
qer
be7c9916b0
fix(web): refill attachments when editing a queued or undone message (#1357)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* fix(web): refill attachments when editing a queued or undone message

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

* fix(web): forward attachment refills through ChatDock

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

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

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

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

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

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

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

* chore: prefix web changeset entry

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

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

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

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

When an undone turn contains media with source.kind === 'url' (a URL but no fileId), loadAttachments used to fall through and drop it. Re-upload the URL (data: or http(s):) to obtain a fileId so URL-backed media is preserved on edit/resend; if the URL can't be fetched (CORS / non-2xx) the chip is dropped instead of shown as a misleading ready attachment.
2026-07-04 23:32:32 +08:00
liruifengv
fbb9dc3772
docs(changelog): fix tui revert wording in 0.22.3 (#1376)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-07-04 19:57:03 +08:00
liruifengv
d0f2994536
docs(changelog): sync 0.22.3 from apps/kimi-code/CHANGELOG.md (#1375) 2026-07-04 19:47:55 +08:00
github-actions[bot]
dfcd6c8ed5
ci: release packages (#1355)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-04 19:17:47 +08:00
liruifengv
7d40d55ac3
chore: downgrade unreleased changesets to patch (#1374) 2026-07-04 19:15:14 +08:00
Haozhe
d111c02ea0
fix(agent-core): cap background shell output to match foreground (#1372)
Background (detached) shell commands were exempt from the 16 MiB output
ceiling, so a runaway background command could fill the disk or crash
the process. Apply the same cap to background shell commands and stop
feeding the disk write chain once it trips. Scope the ceiling to
process tasks so subagent and user-question results, which are appended
once and must be persisted, are left untouched.
2026-07-04 18:48:04 +08:00
Haozhe
5394feaabb
feat: hold print-mode turn until background subagents drain (#1371)
* 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.
2026-07-04 18:44:44 +08:00
Haozhe
e715b1648c
docs(server): document --dangerous-bypass-auth and --keep-alive flags (#1373)
Add the two kimi server run flags introduced in #1368 to the CLI reference (en + zh), including a danger callout for the auth-bypass flag, and add the changeset so the next release notes the feature.
2026-07-04 18:33:22 +08:00
Haozhe
7db88b6f0c
feat(server): add --dangerous-bypass-auth and --keep-alive flags (#1368)
* feat(server): add --dangerous-bypass-auth and --keep-alive flags

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

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

- keep the token and skip the bypass notice when a daemon is reused, since
  the requested --dangerous-bypass-auth flag is not applied to the
  already-running server
- clear the cached dangerous_bypass_auth web state on HTTP 401 so a stale
  bypass value cannot hide the token prompt after the server restarts
  without the flag
2026-07-04 18:03:50 +08:00
liruifengv
23daf0f3c1
revert(pi-tui): restore upstream differential rendering behavior (#1367)
* fix(pi-tui): make the viewport anchor follow above-viewport content shifts

The anchor pins a buffer row index, but an above-viewport length change
shifts the content living at every index below it. The pinned window
then suddenly showed content further along (visible upward creep), and
the rows that slid above the window top were lost: never committed to
scrollback, which holds older bytes at those indices. During streaming,
every above-viewport net shrink (a finished agent row collapsing, a
merged step, a spinner line disappearing) permanently swallowed that
many rows, and the blank area under the input box kept growing.

doRender now scores two hypotheses for such frames — window stayed put
vs window content shifted by the length delta — and when the shift
explains the frame strictly better, moves the anchor with the content:
a pure shift re-anchors with no painting at all (the screen already
shows exactly that content), and a shift with local in-window changes
(spinner/timer rows) repaints the window at the shifted anchor.
Commit order stays continuous, so the exactly-once scrollback invariant
is preserved: no loss, no duplication.

Covered by e2e case07 (above-viewport shift), which fails on the
previous revision; the stale-content unit test now asserts the
follow-the-content window instead of the old swallowed-row behavior.

* revert(pi-tui): restore upstream differential rendering behavior

The fork's viewport/scrollback rendering patches (clamping the diff to
the visible viewport, viewport re-anchoring on collapse, the pinned
anchor with commit-on-advance, cursor visibility guarding, and the
content-shift anchor follow) accumulated interacting edge cases faster
than they could be stabilized: blank screens, duplicated scrollback
spans, vanished rows, and a growing blank area under the input box.

Revert src/tui.ts to the upstream 0.80.2 differential rendering
behavior: a change above the viewport triggers a destructive full
redraw again. Verified line-by-line against the upstream source — the
only remaining divergences are the TypeScript strict-mode syntax
adaptations and the narrow-terminal fixes (Container width clamping
and overwide-line truncation replacing the upstream crash-and-throw),
which are kept.

Also remove the rendering-bug e2e ledger and the shrink test suite
that specified the reverted behavior, restore the pre-fork rendering
tests (the transient-content test asserts the upstream full-redraw
behavior again), and drop the e2e glob from the test script. Editor
input-history and paste-burst changes are untouched.

Known trade-off, accepted for now: the original scroll-position yank
during streaming (destructive redraws emitting ESC[3J) returns; the
rendering rework will restart from this clean baseline.

* chore: downgrade the web thinking-effort changeset to patch
2026-07-04 17:57:43 +08:00
qer
26b90225d2
feat(web): support multi-level thinking effort selection (#1344)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(web): support multi-level thinking effort selection

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

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

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

Addresses Codex P1 review on modelThinking.ts.

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

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

Addresses Codex P2 review on Composer.vue.

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

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

Addresses Codex P2 review on Composer.vue.

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

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

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

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

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

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

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

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

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

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

* fix(web): ignore stale authenticated media fetches

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
2026-07-04 00:36:00 +08:00
liruifengv
36bb506a8f
docs(changelog): sync 0.22.2 from apps/kimi-code/CHANGELOG.md (#1354)
* docs(changelog): sync 0.22.2 from apps/kimi-code/CHANGELOG.md

* docs(agents): require web: prefix for web UI changelog entries
2026-07-04 00:18:02 +08:00
github-actions[bot]
9f4079106c
ci: release packages (#1334)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-03 23:51:39 +08:00
liruifengv
68ad686211
fix(pi-tui): stop scrollback duplication from viewport rewinds (#1353)
* fix(pi-tui): stop scrollback duplication from viewport rewinds

Rewinding the viewport anchor repaints rows that the terminal
scrollback already holds, and the next scroll commits them again —
every rewind duplicated its span. Two paths triggered it during
streaming oscillation (content shrinking then growing back): the
shrink re-anchor rewound immediately, and the clamped differential
path then painted the shifted content through the screen.

Rework the shrink/shift handling around a shared in-place viewport
repaint that never scrolls, with the anchor treated as the scrollback
high-water mark that must never move backward:

- Partial shrinks keep the anchor pinned: the content bottom hovers
  above a bounded blank gap that the next growth naturally fills. No
  rewind means duplication is impossible by construction.
- Only a collapse past the viewport top (compaction, clears) rewinds,
  as nothing sensible could be shown otherwise; the content has
  changed so drastically there that the repainted span is not
  recognizable as a duplicate.
- Above-viewport length changes repaint the visible window in place
  instead of painting through: nothing scrolls, scrollback keeps the
  stale old version, and the anchor only advances when the content
  outgrows the pinned window.
- Deleted-tail changes within a pinned viewport repaint at the pinned
  anchor instead of falling back to a destructive full render.

Pure appends keep flowing through the screen into scrollback, and
equal-length above-viewport changes keep the bounded clamped diff.

* fix(pi-tui): commit skipped rows on anchor advance and guard cursor visibility

Address two review findings on the pinned-anchor rendering:

- An anchor advance (growth past the pinned viewport combined with an
  above-viewport change) repainted the screen in place without
  scrolling, so the rows between the old and new anchor were never
  committed to scrollback and vanished. repaintViewport now paints from
  the old anchor and lets the paint loop scroll the skipped rows out,
  committing each exactly once with fresh content.

- positionHardwareCursor recorded hardwareCursorRow on a logical row
  outside the visible window when the cursor marker sat above a pinned
  viewport (tall editor after a deep shrink), desyncing every later
  differential move. It now hides the cursor and keeps the bookkeeping
  on the real cursor row when the marker is not visible.

Also add an e2e rendering-bug ledger (packages/pi-tui/e2e): one
xterm-emulated repro per production rendering bug, asserting the
renderer invariants (monotonic anchor, exactly-once commit, cursor
bookkeeping sync). The two findings above are case05/case06.

* ci: run the pi-tui node:test suite in a dedicated job

pi-tui's tests (unit + e2e) run on node:test, which the root vitest
run silently skips, so CI never executed them. Add a test-pi-tui job
that runs the package's test script.
2026-07-03 23:49:04 +08:00
liruifengv
e9db9cafcf
feat: record model response id in wire logs (#1349)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat: record model response id in wire logs

* chore: include sdk in response id changeset

* chore: include agent-core in response id changeset
2026-07-03 17:15:04 +08:00
Kai
02da587795
feat(cli): wait for background subagents before exiting kimi -p (#1347)
* 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.
2026-07-03 16:56:16 +08:00
liruifengv
3ed22e35a4
feat: redesign the subagent card (#1345)
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.
2026-07-03 16:54:21 +08:00
Kai
175b95f3af
fix(agent-core): route image-compression captions through hidden system reminders (#1348)
* fix(agent-core): route image-compression captions through hidden system reminders

Prompt ingestion (server upload/base64 route, TUI paste, ACP) annotates a
compressed image with an inline <system> caption inside the user's own
message. That raw markup rendered verbatim in every user-visible history
projection (TUI session replay, web UI) and leaked into session titles.

Split the caption out at the appendUserMessage chokepoint and deliver it
through the built-in system-reminder injection (origin
{kind: 'injection', variant: 'image_compression'}), which every UI already
hides. The model still receives the full note; ingestion sites and the wire
protocol are unchanged. Session titles/lastPrompt strip the caption the same
way. Tool-result captions (MCP) keep the established <system> convention.

Covered by unit tests plus an end-to-end smoke suite that drives
rpc.prompt/steer through the real turn pipeline and asserts the provider
wire request, stored history, replay records, and resume parity.

* chore: tighten changeset wording per gen-changesets conventions
2026-07-03 16:21:10 +08:00
Kai
021786f5a2
fix(kaos): enrich PATH from the user's login shell at startup (#1339)
* feat(agent-core): strengthen the language-matching rule in the default system prompt

* chore: refine changeset wording

* fix(kaos): enrich PATH from the user's login shell at startup

When kimi-code is launched from a context that skipped the user's shell
profile (GUI launchers, non-login parent shells), process.env.PATH misses
entries like /opt/homebrew/bin, so commands spawned by the Bash tool
cannot find user-installed tools such as gh.

LocalKaos.create() now probes the user's login shell once
($SHELL -l -c env, 5s timeout, memoised) and appends the missing PATH
entries to process.env.PATH. Existing entries keep their order and
priority; probe failures silently leave PATH untouched. Windows is
skipped: the problem is specific to POSIX login-shell profiles.

* fix(kaos): fall back to the account login shell when $SHELL is unset

launchd/daemon launches can leave $SHELL unset or blank — the very
contexts whose PATH is impoverished — so the login-shell PATH probe
would give up exactly where it matters most. Resolve the shell from the
OS user database (os.userInfo().shell) before giving up; lookups that
throw (uid without a database entry) or yield nologin shells degrade
silently as before.

* fix(kaos): preserve empty PATH components when merging login-shell PATH

POSIX command lookup treats an empty PATH component (leading colon,
trailing colon, or double colon) as the current directory. The merge
previously filtered those out of the current PATH and rewrote the value
even when nothing was appended, silently dropping cwd lookup for users
who rely on it.

Keep the current PATH string verbatim as the prefix, append only the
missing login-shell entries, and skip the env write entirely when the
login shell contributes nothing — an unset PATH stays unset, a set PATH
is never rewritten. Empty login-shell components are still never
imported.

* fix(kaos): only import absolute login-shell PATH entries

A `.` or relative component in the login-shell PATH is cwd-dependent
lookup with another spelling, and LocalKaos runs commands from arbitrary
workspace directories — importing one would let a command name resolve
from an untrusted project cwd. Tighten the merge's skip condition from
"empty" to "not absolute", which subsumes the empty-component check.

* fix(kaos): invoke the login-shell probe's env by absolute path

A bare `env` inside `$SHELL -l -c` resolves through the inherited PATH
from the workspace cwd. If that PATH carries a cwd-dependent component
(which the merge deliberately preserves), a repo-planted `env` binary
would run automatically at session startup and could feed the probe an
arbitrary PATH. /usr/bin/env is guaranteed on mainstream POSIX systems
and also bypasses profile function shadowing.
2026-07-03 15:20:38 +08:00
Kai
e2fe62a5ef
fix(agent-core): harden tool_use/tool_result exchange integrity (#1340)
Some checks are pending
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / build (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(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
2026-07-03 14:26:57 +08:00
liruifengv
9091627257
fix(tui): avoid submitting rapid multi-line paste bursts (#1305)
* fix(tui): avoid submitting rapid multi-line paste bursts

* chore(tui): remove paste burst settings picker

* fix(tui): reset paste burst on DEL backspace
2026-07-03 14:21:02 +08:00
qer
01b65bdddc
feat(web): open design-system easter egg at /design-system route (#1328)
* feat(web): open design-system easter egg at /design-system route

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* chore(nix): restore pnpmDeps hash after removing vue-router
2026-07-03 13:42:27 +08:00
Kai
84d8d5b063
feat(agent-core): make compaction notes capture a forward plan, not just the next step (#1342)
Compaction runs at the point of maximum context for the task, and the next
turn resumes with less. So the handoff note now records the plan for the
remaining work — upcoming steps, settled decisions, and foreseeable obstacles,
plus any work that can be pre-committed — instead of only the immediate next
command. Update the affected compaction snapshots and one hardcoded
input-token assertion (the instruction is ~163 tokens longer).
2026-07-03 13:38:09 +08:00
Kai
276407d2a4
feat(agent-core): strengthen the language-matching rule in the default system prompt (#1338)
* feat(agent-core): strengthen the language-matching rule in the default system prompt

* chore: refine changeset wording
2026-07-03 11:58:02 +08:00
wenhua020201-arch
4498ea9ee3
docs: use kimi-for-coding in model overrides example; drop dangling experimental refs (#1337)
* docs: use kimi-for-coding in model overrides example

kimi-for-coding is the stable public model ID users actually configure;
kimi-k2 is the underlying model name and shouldn't appear in the config
example. Demonstrate overrides with max_context_size and display_name.

* docs: drop dangling references to commented-out experimental section

The `## experimental` section was commented out when micro_compaction
was removed, but the top-level fields table and the intro sentence still
linked to the now-dead #experimental anchor. Remove those references.
2026-07-03 11:41:03 +08:00
liruifengv
4c1d0a1633
fix: hide the background updater console window on Windows (#1336)
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.
2026-07-03 11:32:31 +08:00
liruifengv
93f16c32d7
fix: run package-manager upgrades through a shell on Windows (#1332)
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.
2026-07-03 11:03:46 +08:00
liruifengv
ceeebc1f85
docs(changelog): sync 0.22.1 from apps/kimi-code/CHANGELOG.md (#1325)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-07-02 22:20:57 +08:00
github-actions[bot]
508384502d
ci: release packages (#1291)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-02 21:56:42 +08:00
qer
5441ad1838
feat(web): collapse loaded sessions back to the first page (#1322)
* feat(web): collapse loaded sessions back to the first page

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

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

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

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

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

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

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

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

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

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

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

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

Two fixes for the subscription cap:

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

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

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

Re-opening an evicted session deleted the stale-cursor marker before the snapshot ran. If the snapshot failed transiently, the marker was gone and a later re-open would fall back to subscribeToSessionEvents, resuming from a cursor that may have skipped per-session events while evicted. Read the marker instead of deleting it, and let syncSessionFromSnapshot clear it once the snapshot succeeds.
2026-07-02 21:48:47 +08:00
liruifengv
3ff401261f
chore: refine changeset wording (#1323)
Simplify the image compression and compaction handoff changesets, and set the image compression bump to patch.
2026-07-02 21:18:32 +08:00
liruifengv
b40bb71399
fix(pi-tui): repaint viewport in place when content collapses above it (#1315)
* 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
2026-07-02 20:52:37 +08:00
qer
e8ab7ca786
fix(web): stop session row from shifting on hover (#1319)
The session row swapped the relative time for the kebab on hover using display:none, which dropped the time from layout and appended the kebab at the end. Because the time is variable-width (2h / 5m / just now) and the kebab is a fixed 26px, the right region reflowed: status badges shifted and the title's truncation changed, causing visible jitter.

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

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

* fix(web): remove remaining ChatHeader tooltips

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

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

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

* chore: add changeset for stuck-tooltip fix

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

The session title is truncated with an ellipsis when it exceeds the header width, so the tooltip was the only way to read the full name from the header. Restoring it keeps the truncation-revealing hint while the redundant git/branch/open-PR tooltips stay removed.
2026-07-02 19:53:01 +08:00
Kai
78a058acd2
chore(agent-core): remove experimental micro compaction (#1317)
* chore(agent-core): remove experimental micro compaction

* fix(docs): drop micro compaction row from env-vars table
2026-07-02 19:50:51 +08:00
liruifengv
b40649b2ae
chore(tui): remove shortcut newline telemetry (#1311)
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.
2026-07-02 19:38:04 +08:00
Kai
4dd926b0ac
fix(agent-core): recover sessions bricked by orphan tool results (#1308)
* fix(agent-core): recover sessions bricked by orphan tool results

A stray `tool` message with no preceding assistant `tool_calls` permanently
bricked a session on OpenAI-compatible providers: every turn re-sent the same
malformed history and got a 400, and switching model/provider did not help.

Two independent gaps caused this:

- kosong did not recognize the OpenAI / DeepSeek / vLLM / Qwen phrasings of the
  tool-exchange structural 400 (`role 'tool' must be a response to a preceding
  message with 'tool_calls'` and the mirror `assistant message with 'tool_calls'
  must be followed by tool messages`), so the post-400 strict-resend fallback
  that drops the orphan never fired.

- The legacy-restore compaction path kept a verbatim tail
  `history.slice(compactedCount)`; when the cut landed inside a tool exchange the
  tail began with an orphan tool result whose assistant was summarized away. The
  normal projection does not repair a leading orphan, so the malformed history
  was baked in and re-sent every turn.

Recognize the additional phrasings so the strict resend un-bricks any session,
and trim leading tool results from the legacy-restore tail so the orphan is
never persisted in the first place.

* fix(agent-core): drop orphan tool results at the projection boundary

Rework the legacy-restore half of the fix based on review feedback: mutating
`_history` at restore time desyncs every consumer that models the history from
the wire records — the transcript reducer's fold length would overcount and
make MessageService skip unflushed live-tail messages.

Keep the restored history faithful to the wire records instead, and drop a
`tool` result whose call is nowhere in the history at the projection boundary,
on every request-building projection: the normal wire (`messages`), the
post-400 strict resend (`strictMessages`), and the compaction summarizer. An
orphan is wire-invalid on strict providers and useless to the model either
way, so it never reaches a provider — no longer relying on recognizing the
provider's 400 phrasing to recover. Fragment projections (e.g. token-estimating
a history slice) leave results untouched, since a matching call may
legitimately sit outside the slice.
2026-07-02 19:29:05 +08:00
Kai
329846c569
feat(agent-core): keep head and tail of user messages during compaction (#1313)
* 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
2026-07-02 19:24:37 +08:00
qer
d1275557f9
docs(web): translate design system to English and add anti-slop guidance (#1302)
* docs(web): add anti-slop design guidance inspired by taste-skill

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

* docs(web): resolve merge conflict with main

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

* docs(web): translate design system to English

Translate the entire design-system.html from Chinese to English, preserving all HTML structure, CSS, code blocks, SVGs, the scroll-spy script, and token values. The design system is now a single English document.
2026-07-02 19:16:04 +08:00
Kai
0fc0ae380b
feat(agent-core): announce image compression and keep originals readable (#1304)
* 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
2026-07-02 19:07:56 +08:00
liruifengv
77eb3a9fe4
feat(tui): include shell commands in input history (#1295)
* 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.
2026-07-02 17:59:26 +08:00
liruifengv
2639786ce5
fix(pi-tui): prevent crashes on very narrow terminals (#1303)
* docs: add pi-tui narrow-width fix plan

* fix(pi-tui): stop wordWrapLine infinite recursion on wide graphemes at width 1

* docs: extend pi-tui narrow-width plan with emoji grapheme regression coverage

* fix(pi-tui): clamp container render width to a minimum of 1

* fix(pi-tui): truncate overwide rendered lines instead of throwing

* perf(pi-tui): fast-path overwide line detection and enlarge width cache

* test(pi-tui): assert exact truncated viewport in overwide line test

* docs: record review amendments in pi-tui narrow-width plan

* test(pi-tui): add editor narrow-width regression tests

* docs: record task 4 review amendments in pi-tui narrow-width plan

* fix(pi-tui): guard blank-line padding against negative widths

* docs: record task 5 review amendments in pi-tui narrow-width plan

* docs(pi-tui): document local divergences from upstream

* chore: add changeset for pi-tui narrow width fixes

* docs(pi-tui): point Text guard test to its actual test file

* test(pi-tui): translate narrow-width test comments to English

* docs: remove internal pi-tui narrow-width plan

* docs(pi-tui): translate AGENTS.md to English
2026-07-02 17:14:11 +08:00
qer
c3653a1c50
refactor(web): show an up arrow on the composer send button (#1301)
* refactor(web): show an up arrow on the composer send button

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

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

* docs(web): show composer send button as an up arrow
2026-07-02 15:34:33 +08:00
Kai
021de5433b
feat(agent-core): align model-facing prompts with actual tool behavior (#1296)
* feat(agent-core): align model-facing prompts with actual tool behavior

A hunk-by-hunk accuracy pass over every model-visible prompt surface
(system.md, tool .md descriptions, zod describes, profile role prompts,
and injected reminder strings), with each claim verified against the
implementation and, where possible, empirically (ripgrep semantics).

Fix descriptions that drifted from the code:
- Grep `glob` matches against each file's absolute path, so
  `src/**/*.ts` silently matches nothing — document the working forms
- Glob `path` accepts relative paths; results are files-only
- FetchURL no longer promises a content-type-to-mode mapping the
  default provider does not honor
- cron: a pinned-date 5-field expression repeats yearly unless
  `recurring: false`; drop a bench-only env knob from cron-list
- skill `args` expansion covers $NAME/$1/$ARGUMENTS and the trailing
  ARGUMENTS: line; goal reminder no longer cites a nonexistent
  developer-message channel

Disclose enforced-but-silent behavior:
- cron fires deliver only while the session is idle; expressions with
  no fire within 5 years are rejected at create time
- VCS metadata directories are always excluded from Glob/Grep, even
  with include_ignored; sensitive-file guard exemptions
  (.env.example/.env.sample/.env.template, public SSH keys)
- large images may be downsampled while the <system> block reports
  original dimensions; subagent summaries under the length floor are
  sent back for expansion; background-disabled Agent calls are
  rejected before launch; AGENTS.md beyond ~32 KB triggers a
  performance warning (surfaced in the /init prompt)

Resolve cross-surface contradictions:
- AskUserQuestion background describe/envelope no longer teach polling
- AgentSwarm subagent_type documents that resume keeps original types
- bash.md scopes &&-chaining to dependent commands and steers
  independent read-only commands to parallel calls
- the shared system prompt no longer names tools that read-only
  subagent profiles lack

Add missing guidance:
- denied/rejected tool calls mean the user declined that action —
  adjust, don't retry or route around (root agent)
- plan subagent now knows it is read-only; coder subagent knows its
  final message is the entire handoff; explore subagent knows web
  tools are in scope
- gh CLI routing for GitHub-hosted work; FetchURL login-wall note;
  a dual-use content-safety boundary; scope discipline,
  surrounding-idiom, and dependency-verification norms; file:line
  citation convention; progress notes on long multi-phase tasks

* fix(agent-core): let the model fetch a background answer after the completion notice

In sessions with background persistence (any agent with a homedir), a
background question's answer is flushed to output.log and the completion
notification carries an <output-file> pointer, not the answer text. The
previous envelope wording ("use TaskOutput only to re-read the answer if
you missed the notification") gated the normal post-completion fetch
behind a missed-notification condition, so a model could acknowledge the
notice and continue without ever reading the user's answer.

Reword the envelope to state that the completion notice may carry a
pointer and to direct the model to read that file (or call TaskOutput
once) for the answer, while still forbidding polling before the user
responds. Align the background param describe the same way ("notified
automatically" rather than "the answer arrives", polling scoped to the
pending window).

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

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

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

* fix(web): address PR review feedback

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

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

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

Drop explicit .ts extensions in icon imports to satisfy oxlint, and read the design-system icon catalog directly from the generated icon data.
2026-07-02 14:20:17 +08:00
Kai
93ec6cb652
fix(kosong): recognize OpenAI-compatible tool_call_id 400 as a recoverable tool-exchange error (#1292)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Moonshot / Kimi (OpenAI-compatible) rejects a history whose tool message
references a tool_call_id with no matching tool_calls entry in the preceding
assistant message as `400 tool_call_id  is not found`. The
TOOL_EXCHANGE_ADJACENCY_MESSAGE_PATTERNS only covered Anthropic's
tool_use/tool_result phrasing, so isRecoverableRequestStructureError returned
false, the strict-resend fallback in executeLoopStep never fired, and the
session stayed permanently stuck re-sending the same rejected history every
turn (observed in the field after a manual compaction busted the prompt cache
and forced full revalidation of a latently misordered prefix).

Add the tool_call_id-anchored pattern so the whole recovery chain — strict
projection (adjacency repair, orphan-result drop, synthetic results) plus the
one-shot resend — now also covers the default provider. Covered by classifier
unit tests and an e2e resend-and-recover case.
2026-07-02 13:52:44 +08:00
Kai
ea55911062
feat(agent-core): sharpen the compaction handoff prompt (#1283)
* feat(agent-core): sharpen the compaction handoff prompt

Refine the first-person handoff note the model writes at compaction so it
preserves what actually gets dropped instead of what already survives:

- Lead with the intent of the latest request, not a verbatim re-transcription
  (the recent user messages are already kept verbatim beside the summary);
  name which request governs when several are in play.
- Carry forward tool results — the concrete values, key lines, schemas — not
  just the commands that produced them.
- Keep settled decisions separate from still-open questions, and name the
  context the next turn must go and re-check.
- Write in the conversation's language, keep the note proportional to the task,
  and don't re-transcribe the auto-attached TODO list.

Also correct the system prompt's description of the post-compaction shape: the
recent user messages come first, followed by a first-person summary (not a
rigidly sectioned report), and a newer kept message supersedes the summary.

Update the affected inline snapshots and the compaction request token count.

* fix(agent-core): preserve an oversized latest request in the handoff note

selectRecentUserMessages truncates a kept user message to the size cap,
keeping only its prefix, so when the latest request itself exceeds the cap
only its head survives verbatim beside the summary. Telling the summary
"don't re-transcribe, it survives verbatim" then permanently dropped the
tail — often the actual ask. Keep the intent-not-transcription guidance,
but require preserving the at-risk parts of a long current request.
2026-07-02 13:44:29 +08:00
Kai
c434b4c3e6
fix(agent-core): cap foreground shell output to prevent OOM crash (#1285)
* fix(agent-core): cap foreground shell output to prevent OOM crash

A foreground command that streams a very large or unbounded amount of output (e.g. `b3sum --length 18446744073709551615`) grew the live-output buffer until Node aborted with a JavaScript heap out-of-memory error (exit 134). The per-command output is now capped at 16 MiB: on breach the command is gracefully terminated (SIGTERM -> grace -> SIGKILL) and the result carries a message pointing at redirecting large output to a file. The per-task output ring buffer is also made O(1) per chunk (was O(n^2)), which previously starved the event loop and the foreground timeout. Background/detached tasks are exempt.

* fix(agent-core): stop buffering output after the foreground cap trips

After the 16 MiB foreground ceiling tripped, appendOutput still enqueued every subsequent chunk into the per-task disk write chain during the SIGTERM grace window. A producer that ignores SIGTERM could keep that chain — and the chunk strings each pending write retains — growing until SIGKILL, re-introducing the same out-of-memory risk the cap prevents. Once the cap has tripped, keep only the bounded in-memory ring buffer and stop feeding the disk write chain. Interrupt/timeout capture behaviour is unchanged (they do not set outputLimitTripped).
2026-07-02 13:42:39 +08:00
qer
3ea84a56e4
fix(web): prevent horizontal scrollbar in session search dialog (#1290)
The search sessions dialog used a column flex layout where the title and snippet had white-space: nowrap but no min-width: 0, so the flex items refused to shrink below their text width and overflowed, surfacing a horizontal scrollbar on the dialog body. Add min-width: 0 so they shrink and ellipsize instead.
2026-07-02 12:24:46 +08:00
liruifengv
a4d7a4ff2a
docs(changelog): sync 0.22.0 from apps/kimi-code/CHANGELOG.md (#1288) 2026-07-02 11:34:17 +08:00
github-actions[bot]
ba7f18b3fb
ci: release packages (#1268)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-02 11:12:20 +08:00
qer
bbda90af84
fix(web): hide conversation outline when it cannot expand (#1278)
Some checks are pending
Release / Publish native release assets (push) Blocked by required conditions
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
* fix(web): hide conversation outline when it cannot expand

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

When ConversationToc mounts while hidden (sessionLoading, mobile, or before a second user turn), navRef is null and the ResizeObserver is never set up, so fits stays true and the outline shows even in narrow layouts. Re-initialize the measurement whenever the nav is rendered.
2026-07-02 00:44:11 +08:00
Kai
074bb9ba13
fix(kosong): retry a dropped provider stream (terminated) on the Anthropic path (#1274)
A raw undici `terminated` error — an SSE/HTTP response body cut mid-flight,
common on long streaming responses — fell through convertAnthropicError to a
generic base ChatProviderError, which isRetryableGenerateError treats as fatal,
so it was never retried. Route raw non-SDK errors through the shared
classifyBaseApiError heuristic (already used by the OpenAI path) so a dropped
stream is classified as a retryable APIConnectionError and retried instead of
failing the turn.
2026-07-01 22:48:57 +08:00
liruifengv
a5db546d77
feat: add KIMI_MODEL_THINKING_EFFORT to force a thinking effort (#1275)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat: add KIMI_MODEL_THINKING_EFFORT to force a thinking effort

Send thinking effort only when the model declares it in support_efforts, and add the KIMI_MODEL_THINKING_EFFORT environment variable as an escape hatch to force a specific effort regardless of declared support.

* test: align thinking effort expectations with support_efforts gating

Update the kimi adapter e2e and compaction tests that asserted the previous pass-through behavior on models without support_efforts.
2026-07-01 21:20:27 +08:00
liruifengv
54703d9457
fix(tui): restore terminal state on crash and release leaked resources (#1272)
* 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.
2026-07-01 21:14:19 +08:00
qer
b905dd4910
feat(web): redesign web UI and add design system (#1258)
* feat: redesign web ui & add design system

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

Animate toast enter/leave, dialog open, and workspace-list and tool-row expand/collapse instead of snapping, and add the changesets covering the web redesign.

* fix(web): remove undo message exit animation

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

- toolRegistry matched the raw 'agent' name, but normalizeToolName folds
  agent/subagent into 'task', so agent calls fell through to GenericTool
  and lost the inline Open button for the subagent detail panel.
- Dialog's focus watcher only fired on change; callers that mount with
  open already true (Login, Settings, ...) never moved focus into the
  modal. Run it immediately so initial focus and restore-on-close work.

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

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

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

Nest the design-system Teleport inside the sidebar <aside> so the component has a single element root. App applies v-show to Sidebar, which needs an element root to attach to; the fragment root logged a "non-element root node" warning on every reactive update and the collapse did not take effect.

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

- Show the default-thinking switch as on whenever thinking is effectively enabled (enabled !== false), matching the core resolver.

- Route the grouped tool-call header and status through vue-i18n.

- Hide the subagent "Open detail" button when no matching task exists (e.g. a completed foreground subagent after a refresh).

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

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

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

Remove the design exploration mockups, screenshots, prompts, and notes that were accidentally committed under apps/kimi-web/design. Keep design-system.html, which the sidebar logo easter egg still references.

* fix(web): keep sessions on continuation failure, treat absent thinking as on

- Keep sessions already loaded from earlier pages when a continuation page fetch fails, instead of replacing the workspace with an empty page.

- Treat an absent thinking config as enabled in the settings toggle, matching the core resolver (thinking is on unless explicitly disabled).

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

Replace the 3-second long-press trigger with 10 consecutive clicks on the Kimi mark; the count resets after a short idle. The long-press was unreliable because pointerleave cancelled the timer on any drift.

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

phaseForTask now lets a terminal status (completed/failed/cancelled) override a stale subagentPhase, so a cancelled swarm member no longer stays live and suppresses the finished AgentSwarm card.

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

Switch the logo easter egg back to a long-press, shortened to 1 second, and make it reliable this time: use pointer capture plus touch-action:none so a slight drift no longer cancels the hold.

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

ActivityNotice renders for non-chat loading states (e.g. compaction), so it must use Spinner per the design-system rule that reserves MoonSpinner for the chat first-response state.

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

* docs(web): drop stale design README link

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

- Bind the model picker's search Input to searchRef so useDialogFocus moves focus into it on open instead of the dialog's close button.

- Use the defined --weight-semibold token for panel header titles (--weight-bold is not declared, so the shorthand was invalid).

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

Track open design-system Dialog instances in a shared count and include it in App.vue's anyOverlayOpen, so a dialog whose open state lives outside App.vue (such as the sidebar session search) captures Escape before the background side panel closes.

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

Foreground subagents are excluded from the dock task lists, so a session whose only task is a foreground subagent no longer renders an empty workbar above the composer.

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

A client that subscribes from a snapshot after subagent.spawned already fired never received the lifecycle taskCreated; the reducer only applies taskProgress to existing tasks, so assistant text deltas were dropped and the live subagent detail stayed blank. Emit taskCreated (via patchSubagent) before the text progress, mirroring the tool-progress path.

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

Plan, swarm, and goal modes were stored as global scalars on the web client and a single localStorage key each, so they leaked across sessions. Bind them to the active session via per-session maps, persist per session, and apply server status/events to the originating session so background sessions keep independent state.

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

When the web client subscribes after a subagent already spawned, the synthesized subagent task has no parentToolCallId, so the Agent tool's Open-detail button was hidden and the panel would not open. Fall back to the single unmapped subagent task when resolving the detail target in both the button visibility and the panel open paths.

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

Teleport the SessionRow kebab menu to body and anchor it with fixed positioning so the collapsing group-sessions list's overflow:hidden no longer clips the dropdown.

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

When starting the first prompt, apply the staged plan/swarm/goal modes to the just-created session's per-session maps by id instead of via the activeSessionId-based setters, so a session switch during the selectSession await can't drop the modes for this session or pollute another.

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

Replace the inconsistent confirmation patterns (native confirm(),
two-step menu arming, hand-rolled inline strips, bare buttons) with one
modal ConfirmDialog driven by a global useConfirmDialog() composable,
and consolidate the duplicated confirm/cancel i18n keys.

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

- Send button always sends/enqueues; interrupt moves to a separate red Stop
  button shown only while running, so the two can no longer be confused.
- Queued prompts now render inline at the tail of the transcript (after the
  running turn) instead of behind the dock panel: click to edit, remove, drag
  the grip to reorder, with image thumbnails and a "next up" marker.
- Remove the dock queue panel and the QueuePane component; Steer stays on
  Ctrl/Cmd+S.

* chore: add changeset for web queue UX

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

- Fix spurious errors when question/approval/task actions were already complete
- Add loading feedback to question and approval prompts; block double-clicks
- Make the question "Other" option selectable by row click and let Enter advance/submit
- Consolidate workspace section actions into an overflow menu
- Tighten markdown prose line-height and block spacing
- Recall input history only when the caret is at text start
2026-07-01 20:47:12 +08:00
liruifengv
7859b0afe8
feat(kimi-code): vendor @moonshot-ai/pi-tui (#1254)
* 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
2026-07-01 20:23:35 +08:00
liruifengv
3cb7936cf5
chore(telemetry): track conversation undo, shell mode, and effort changes (#1271)
* 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.
2026-07-01 20:04:31 +08:00
liruifengv
c070fbedde
feat: add model alias overrides (#1262)
* 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.
2026-07-01 19:57:13 +08:00
qer
0e279bfd7e
chore(kimi-desktop): rename installers to kcd-beta-alpha-crazy-internal (#1267)
* 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
2026-07-01 19:50:11 +08:00
Kai
ace7901066
feat(agent-core): compress oversized images before sending to the model (#1243)
* 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.
2026-07-01 19:36:48 +08:00
Kai
bf35f63c5d
fix(provider): honor base_url for google-genai and vertexai providers (#1269)
* fix(provider): honor base_url for google-genai and vertexai providers

The google-genai and vertexai provider types silently ignored a configured
base_url and always hit generativelanguage.googleapis.com (e.g. a Gemini-
compatible proxy URL + key could not be used). Plumb the endpoint through to
the @google/genai SDK via httpOptions.baseUrl:

- kosong: add baseUrl to GoogleGenAIOptions and inject it into the client's
  httpOptions alongside the existing headers (the SDK merges headers and
  overrides the base host).
- agent-core: forward provider.baseUrl in the google-genai and vertexai
  branches, with GOOGLE_GEMINI_BASE_URL / GOOGLE_VERTEX_BASE_URL env
  fallback. vertexai keeps deriving location from an aiplatform host.
- docs: document base_url for both providers, noting the host root only
  must be given because the SDK appends the API version itself.

Covered by unit tests asserting the URL reaches the kosong config and the
SDK client's httpOptions.

* fix(provider): use the effective base_url for vertex location detection

The vertexai branch forwarded the endpoint from config `base_url` OR the
GOOGLE_VERTEX_BASE_URL env fallback, but service-account detection
(`hasVertexAIServiceEnv` / `vertexAILocation`) still derived the region from
`provider.baseUrl` only. Supplying the regional endpoint via the env fallback
(with a project but no explicit GOOGLE_CLOUD_LOCATION) therefore left location
undefined and silently downgraded Vertex ADC to API-key Gemini routing.

Resolve the effective base URL once and use it for both forwarding and location
derivation, so the env fallback behaves exactly like `base_url`. Add a
changeset for the kosong + agent-core patch release.
2026-07-01 19:33:35 +08:00
Kai
ffe41ac752
fix(changeset): drop private kimi-code-docs from web-search changeset to unblock release (#1270)
The web-search-query-only changeset mixed @moonshot-ai/agent-core (published)
with kimi-code-docs, a private package without a version field that changesets
treats as ignored. Changesets rejects any changeset containing both ignored and
non-ignored packages, which broke the Release workflow's version step. Drop the
private docs entry; it never publishes and needs no changelog.
2026-07-01 19:28:20 +08:00
Kai
e47ca10267
feat(agent-core): slim WebSearch to query-only; fetch page content via FetchURL (#1260)
* feat(agent-core): slim WebSearch to query-only; fetch page content via FetchURL

The coding search backend no longer honors `limit`/`enable_page_crawling` and
always returns full page content, which was token-heavy and frequently
truncated. Realign the web tools around a search+fetch split:

- WebSearch request sends only `text_query`; the tool exposes only `query`.
- Search results drop inline page content and add the source site; full page
  content is now fetched on demand via FetchURL.
- Add a citation reminder to both WebSearch and FetchURL results (in the
  FetchURL front note so it survives body truncation).
- Update tool descriptions and reference docs accordingly.

* fix(agent-core): satisfy no-base-to-string lint and drop redundant | undefined

- Assert the exact serialized WebSearch request body instead of String()-coercing
  a BodyInit, fixing the type-aware no-base-to-string lint error.
- Drop redundant `| undefined` from WebSearchResult optional fields per AGENTS.md.
- Add changeset.

* chore(changeset): bump agent-core to minor for WebSearch input-contract change

Removing the `limit`/`include_content` tool inputs tightens a closed
(`additionalProperties: false`) schema, so previously-valid args are now
rejected — an incompatible change for a released package. Bump minor rather
than patch.
2026-07-01 18:46:15 +08:00
liruifengv
8cfb1657ad
fix(tui): reduce default transcript window to keep long sessions responsive (#1265)
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.
2026-07-01 18:41:27 +08:00
liruifengv
003733c751
fix(tui): hide redundant Off option for always-on effort models (#1264)
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.
2026-07-01 18:33:56 +08:00
liruifengv
62999caca3
docs(changelog): sync 0.21.1 from apps/kimi-code/CHANGELOG.md (#1259) 2026-07-01 15:15:12 +08:00
792 changed files with 73557 additions and 13564 deletions

View file

@ -11,6 +11,8 @@ description: Use when generating changesets in the kimi-code repository, includi
All other `@moonshot-ai/*` packages are treated as internal packages, including `@moonshot-ai/kimi-code-sdk`, `agent-core`, `kosong`, `kaos`, `kimi-code-oauth`, `kimi-telemetry`, and `migration-legacy`.
`@moonshot-ai/pi-tui` is a special internal package: it is a private fork (`private: true`) that is never published, but it keeps its own changelog through changesets. It is an exception to Core Rule 4 — see the dedicated section below.
## Core Rules
1. **Inspect the actual changes first.** Use `git status` / `git diff --name-only` to identify which packages were actually changed.
@ -145,7 +147,8 @@ Clarify session status typing for internal SDK callers.
`@moonshot-ai/kimi-web` is ignored by changesets and must **never** appear in a changeset frontmatter. Because the web app is bundled into the CLI release artifact, any web change that ships must list `@moonshot-ai/kimi-code` instead and describe the actual web-facing change in the text.
- If a PR contains both web UI changes and server API changes, split them into separate changesets so each entry has a focused description.
- Prefix the changelog entry text with `web: ` (for example `web: Fix the chat not scrolling to the bottom after sending a message.`) so the synced docs changelog can mark web UI entries. Apply this whenever the change is to the web project (`@moonshot-ai/kimi-web`).
- If a PR ships a web UI feature backed by server API changes that exist solely to power that feature, prefer a single `web:` entry describing what the web user gets. Do not add a separate server-API changeset unless the API has independent user value (a public endpoint that SDK or server consumers call directly). The docs changelog sync also deduplicates this pattern, but catching it here avoids duplicate changesets.
- Do not enumerate every micro-tweak; keep it to one sentence that captures what the web user gets.
Web-only fix:
@ -155,25 +158,54 @@ Web-only fix:
"@moonshot-ai/kimi-code": patch
---
Fix the web chat not scrolling to the bottom after sending a message.
web: Fix the chat not scrolling to the bottom after sending a message.
```
Web UI plus server APIs in the same PR (split into two changesets):
Web UI plus backing server APIs in the same PR (prefer a single `web:` entry; the API is plumbing):
```markdown
---
"@moonshot-ai/kimi-code": minor
---
Add the server-hosted web UI, including chat layout and session list behaviors.
web: Add the server-hosted web UI, including chat layout and session list behaviors.
```
Split into two changesets only when the API has independent user value on its own (for example, a public endpoint SDK consumers call directly). In that case add the web entry above plus a separate one such as `Add a public REST API to list archived sessions for SDK consumers.`
## `@moonshot-ai/pi-tui` changes
`@moonshot-ai/pi-tui` is a vendored fork that lives in `packages/pi-tui`. It is `private: true` and is never published, but it is **not** ignored by changesets: changesets versions it and writes `packages/pi-tui/CHANGELOG.md` so the fork keeps its own history. Because it is bundled into the CLI like other internal packages, it is an exception to Core Rule 4 — do **not** list `@moonshot-ai/kimi-code` for a change that only touches pi-tui.
- Changes that only affect pi-tui (build, package, strict-mode cleanup, renderer fixes): list `@moonshot-ai/pi-tui` only. No CLI changeset.
- If the same change is also user-visible in the CLI (for example a terminal rendering fix that CLI users can see), add a **separate** changeset that lists `@moonshot-ai/kimi-code` with CLI-focused wording, in addition to the pi-tui changeset. Do not mix both packages in one frontmatter — the two changelogs need different wording.
pi-tui-only change:
```markdown
---
"@moonshot-ai/pi-tui": patch
---
Export the package manifest so the bundled binary can locate its native assets.
```
pi-tui change that is also visible in the CLI (two separate changesets):
```markdown
---
"@moonshot-ai/pi-tui": patch
---
Clamp the differential render to the visible viewport so scrolling up during streaming no longer jumps to the top.
```
```markdown
---
"@moonshot-ai/kimi-code": minor
"@moonshot-ai/kimi-code": patch
---
Add the server REST and WebSocket APIs that power the web UI.
Fix the transcript jumping to the top when scrolling up through history during streaming output.
```
## Red Flags
@ -188,3 +220,6 @@ Add the server REST and WebSocket APIs that power the web UI.
- The wording claims more than the diff actually did.
- The CLI wording mentions internal package names, class names, or PR numbers.
- The entry includes real internal identifiers instead of neutral placeholders.
- A change that only touches `@moonshot-ai/pi-tui` lists `@moonshot-ai/kimi-code` instead of `@moonshot-ai/pi-tui`, or mixes both packages in one frontmatter.
- A web app change entry is missing the `web: ` prefix.
- A server/API changeset exists only to back a web feature that a `web:` changeset already describes (use one `web:` entry instead, unless the API has independent user value).

View file

@ -37,9 +37,10 @@ If the CLI changelog is not in the diff (for example an SDK-only release), stop
Process the version block exactly as `sync-changelog` does for the docs site, but only in memory:
- **Strip** (`sync-changelog` step 3): drop the H1, the `### Patch Changes` / `### Minor Changes` / `### Major Changes` subheadings, PR links, and commit-hash links; keep only each entry's body text.
- **Classify** (`sync-changelog` step 4): bucket into Features / Bug Fixes / Polish / Refactors / Other; order within each section by reader value.
- **Translate** (`sync-changelog` step 6): translate entry bodies to Chinese; section headings become 新功能 / 修复 / 优化 / 重构 / 其他.
- **Strip** (`sync-changelog` step 3): drop the H1, the `### Patch Changes` / `### Minor Changes` / `### Major Changes` subheadings, PR links, and commit-hash links; keep only each entry's body text. The `Thanks [@user](...)!` credit (including the multi-author form) must be removed every time. Within each entry, drop SDK-only and provider-internal sentences (SDK capability mapping / API exposure, provider wire-format mechanics, internal XML markers) and keep only the user-facing effect and required constraints.
- **Merge and deduplicate** (`sync-changelog` step 4): merge micro-tweaks to the same surface into one higher-level entry; when three or more fixes target the same UI area or the same class of problem, merge them into one higher-level fix entry (do not merge broad or genuinely distinct fixes); and drop a server/API entry that only backs a web feature already listed.
- **Classify** (`sync-changelog` step 4): bucket into Features / Bug Fixes / Polish / Refactors / Other; order within each section by reader value (in Polish, user-visible improvements before protocol/internal adjustments).
- **Translate** (`sync-changelog` step 6): translate entry bodies to Chinese; keep one sentence per entry with a parallel rhythm within a section; section headings become 新功能 / 修复 / 优化 / 重构 / 其他.
If an upstream entry is not in English, flag it and stop (changeset entries must be English).

View file

@ -93,13 +93,13 @@ Use upstream order: newest version first.
Upstream entries look like this:
```markdown
- [#317](https://github.com/...) [`2f51db4`](https://github.com/...) - Clean up lint warnings ...
- [#317](https://github.com/...) [`2f51db4`](https://github.com/...) Thanks [@user](https://github.com/...)! - Clean up lint warnings ...
```
Keep:
Changesets may add a `Thanks ...!` credit, but it must be removed every time. Keep:
- Version headings such as `## 0.2.0`.
- Only the body text of each entry, after the PR/hash decoration.
- Only the body text of each entry, after the PR/hash decoration and any `Thanks ...!` credit have been removed.
Remove:
@ -107,26 +107,49 @@ Remove:
- Changesets subheadings such as `### Patch Changes`, `### Minor Changes`, and `### Major Changes`.
- PR links such as `[#317](...)`.
- Commit hash links such as ``[`2f51db4`](...)``.
- The `Thanks [@user](...)!` credit, including the multi-author form `Thanks [@a](...), [@b](...)!`. Drop the whole `Thanks ...!` segment every time, regardless of whether the feature is enabled.
After stripping, each entry should be only:
After stripping, each entry is `- <body text>`.
Drop SDK-only and provider-internal detail. This changelog serves `@moonshot-ai/kimi-code` CLI and web users. Within an entry, keep only what CLI/web users can perceive, and remove sentences that document internals instead of user-visible behavior. Apply this on both the English and Chinese pages:
- Drop sentences about how the SDK maps a capability, builds model aliases, or exposes a flag through an API such as `getExperimentalFeatures()` — that belongs in the SDK changelog, not here.
- Drop provider / wire-format implementation mechanics (XML markers like `<tools_added>`, protocol field explanations, "the wire protocol is unchanged", cache-hit mechanics) unless they are the behavior a user perceives.
- Keep the user-facing effect and any constraints users must follow (for example "question texts must be unique").
Do not change facts or drop a real user-facing behavior — only trim the internal-only scaffolding. For over-long, internal-heavy entries, this trim applies on the English page too, not only in translation.
Web UI prefix: if the entry is a web UI change, prefix the body text with `web: ` so readers can tell it affects the web UI:
```markdown
- <body text>
- web: <body text>
```
An entry counts as a web UI change when its upstream commit touches `apps/kimi-web/`. Check with `git show --name-only <hash>` (the commit hash is the one stripped above). `gen-changesets` writes this prefix for web changes, so it is usually already present in upstream — preserve it when it is there, and add it when a web entry lacks it. When a commit touches both web and non-web code, use `web:` only if the user-facing change described by the entry is in the web UI. Keep the `web:` prefix on the Chinese page too — it is a scope marker, not translated text.
Upstream language rule: `gen-changesets` requires changelog entries to be English. If the upstream CLI changelog contains a non-English entry, stop and report it to the user. Do not silently rewrite it while syncing docs.
Public-text rule: do not copy real internal endpoints, key names, account names, or service names into docs changelogs. Replace examples with neutral placeholders such as `example.com`, `example.test`, or `YOUR_API_KEY` while preserving the user-visible meaning.
### 4. Classify Entries
### 4. Merge, Deduplicate, And Classify Entries
Before classifying, merge related entries and drop redundant ones from the user-facing changelog:
- **Merge micro-tweaks to the same surface.** Collapse several small tweaks to the same UI area or feature into one concise entry at the higher level. For example, "change the composer's default height" and "change the composer's default font" merge into "Polish the composer's default styling." Use the most specific common ancestor (composer, settings page, tool card, and so on). Classify the merged entry by its combined effect, and keep the `web:` prefix if the combined change is still web-facing.
- **Merge same-surface or same-kind fixes when you have three or more.** The `Bug Fixes` section tends to accumulate many narrow UI/polish fixes that read as noise when listed one by one. When three or more fixes target the same area (for example several tool cards in the TUI, or the web session/conversation surface) or the same class of problem (for example several "jumping/flickering/collapsing during streaming" fixes), merge them into one higher-level entry. Examples:
- "Fix the Bash tool card collapsing...", "Fix the Edit tool card jumping in height...", "Fix the Edit tool card flickering while its result streams in" → "Fix several TUI tool cards jumping, flickering, or collapsing in height when results stream in or end with short output."
- "Fix the collapsed sidebar not hiding...", "Stop the chat history from replaying its entrance animation...", "Fix tool components jumping the conversation when expanded/collapsed" → "web: Fix several layout and display glitches when switching sessions, including the collapsed sidebar not hiding, the chat history replaying its entrance animation, and tool components jumping the conversation."
- Keep `web:` if the merged fixes are all web-facing. Classify as `Bug Fixes`.
- **Do not over-merge.** Leave a fix standalone when it is broad, high-value, or genuinely distinct (for example model/provider tool-calling bugs, session-list corruption, file-completion gaps). Merging is for low-reader-value, similar-shape fixes that read as a wall of similar bullets.
- **Drop server/API plumbing covered by a web entry.** If one entry adds a web UI feature (for example, an Archived sessions page) and another entry only adds the server or REST/WebSocket endpoints that exist solely to power that web feature, keep the `web:` entry and drop the API entry. CLI and web users perceive the web page; the backing API is implementation detail with no independent user value on this changelog. Keep the API entry only when it has independent user value — a new public endpoint that SDK or server consumers call directly, or a capability usable outside the web feature. When unsure, keep both and let the reviewer decide.
The docs changelog uses five section types:
| English section | Chinese section | Meaning |
|---|---|---|
| `### Features` | `### 新功能` | New user-facing functionality, such as a new command, flag, mode, or capability that did not exist before |
| `### Bug Fixes` | `### 修复` | Fixes for behavior that was broken |
| `### Polish` | `### 优化` | User-visible improvements to existing functionality, including UX adjustments, behavior tweaks, and performance improvements that are not fixes or new capabilities |
| `### Bug Fixes` | `### 修复` | Fixes for behavior that was broken |
| `### Refactors` | `### 重构` | Internal changes with no user-visible behavior change, including build, CI, tests, dependency cleanup, and internal renames |
| `### Other` | `### 其他` | Anything that does not fit above, such as CDN/endpoint swaps and docs-related artifacts |
@ -140,6 +163,8 @@ Classification process:
Features vs. Polish: ask whether the entry introduces something the user could not do before. If yes (new command, flag, mode, viewer, or capability), use `Features`. If it only improves an existing surface (a UI panel that already existed, an existing prompt, an existing tool card, an existing payload pipeline), use `Polish`. Verbs like `Add` do not automatically mean `Features` — a small visual addition to an existing UI is still polish.
Default-behavior changes: changing the default value of an existing capability (for example flipping a feature on by default) is usually `Polish`, because the capability already existed. Use `Features` only when the new default materially changes the out-of-box experience for most users in a way they could not get before. When genuinely ambiguous, flag it and confirm with the reviewer rather than guessing.
Keyword hints:
- **Features**: `Add ... command/flag/option/mode/viewer`, `Introduce`, `Support`, `Allow`, `Enable`, `Implement`, `New ... command/flag/option`
@ -151,14 +176,15 @@ Keyword hints:
Within each version, section order is:
```text
Features → Bug Fixes → Polish → Refactors → Other
Features → Polish → Bug Fixes → Refactors → Other
```
Omit empty sections. Within each section, order entries by reader value, not upstream order:
1. Put the most valuable, obvious, and larger changes first.
2. Prefer broad user-visible features, workflow-changing fixes, high-frequency bugs, and large cross-cutting improvements over small polish, narrow edge cases, and internal cleanup.
3. If entries have similar value, preserve upstream order.
3. Within `Polish`, put directly user-visible UX or performance improvements (something users can see or feel) before protocol or internal-behavior adjustments (something that makes the model or pipeline behave more reliably but is invisible to users).
4. If entries have similar value, preserve upstream order.
Do not reword or exaggerate entries just to make them look more important; only reorder existing entries.
@ -238,6 +264,7 @@ Structural fidelity does not mean literal translation. The Chinese entries shoul
Guidelines:
- **One entry, one sentence.** Avoid chaining multiple effects with commas or semicolons. If the English entry is long, split it into shorter sentences or keep only the most important effect.
- **Drop SDK-only and provider-internal detail.** Apply the trim from step 3 while translating: keep the user-facing effect and required constraints, drop SDK-mapping sentences, provider / wire-format mechanics, and internal XML markers. A long internal entry should collapse to one short Chinese sentence about what the user gets.
- **Prefer common changelog verbs**: 新增、支持、修复、优化、改进、调整.
- **Avoid indirect "through... make..." structures**. Do not write "通过 X使 Y"; prefer direct cause-effect or just state the result.
- Bad: `通过缓存已渲染消息行,使终端在长篇对话中保持响应。`
@ -254,7 +281,8 @@ Guidelines:
- **Keep usage hints to one short clause**.
- Bad: `传入 --allowed-host 以允许额外的 host。例如 ... (多句展开)`
- Better: `例如 kimi web --allowed-host example.com。`
- **Do not translate technical identifiers**: keep command names, flag names, file names, env vars, and config keys as-is.
- **Do not translate technical identifiers**: keep command names, flag names, file names, env vars, config keys, and the `web:` scope prefix as-is.
- **Keep parallel rhythm within a section.** When several entries fix similar web surfaces (layout, animation, sizing), phrase them with a consistent structure (for example 修复 <问题>,现 <行为>) so the section reads as a tidy list rather than a mix of shapes.
Example — translating a feature entry:
@ -292,6 +320,7 @@ Check:
- Each section has the same number of entries on both pages.
- Within each section, the most valuable, obvious, and larger entries appear before smaller or narrower entries.
- PR links and commit hashes were stripped.
- No `Thanks ...!` credit remains (remove it every time).
- Real internal identifiers were replaced with neutral placeholders.
- There are no empty sections.
- Markdown indentation and blank lines are intact.
@ -397,6 +426,7 @@ Return the PR URL to the user when done.
- The English docs changelog is the source of truth.
- Never edit upstream `apps/kimi-code/CHANGELOG.md`.
- Do not backfill unreleased `.changeset/*.md` drafts into the docs site.
- Prefix web UI entries with `web: ` (when the upstream commit touches `apps/kimi-web/`), and keep the prefix on both the English and Chinese pages.
- If upstream wording is wrong, leave upstream alone and fix it in a future changeset.
- Always sync on a `docs/changelog-sync-*` branch and open a PR; never push changelog docs sync directly to `main`.
- Wait for the human review checkpoint before committing, pushing, or opening a PR.
@ -407,6 +437,10 @@ Return the PR URL to the user when done.
|---|---|
| Adding entries directly to the English docs page without reading upstream | Use `apps/kimi-code/CHANGELOG.md` as the source |
| Copying PR links or commit hashes into docs | Strip them; keep only body text |
| Leaving the `Thanks ...!` credit in docs | Remove it every time, including the multi-author form |
| Leaving near-duplicate micro-tweaks as separate bullets | Merge small tweaks to the same surface into one higher-level entry (e.g. composer height + font → composer's default styling) |
| Listing many narrow fixes to the same surface as separate bullets | When three or more fixes target the same UI area or the same class of problem, merge them into one higher-level fix entry; keep genuinely distinct or high-value fixes standalone |
| Listing a server/API entry that only backs a web feature already listed | Drop the API entry and keep the `web:` entry, unless the API has independent user value |
| Rewording upstream English entries | Upstream is frozen; copy the body text unless the user explicitly asks otherwise |
| Leaving English text untranslated in the Chinese page | The Chinese page must be fully Chinese except preserved technical terms |
| Editing upstream changelog text | Do not edit upstream |
@ -425,6 +459,7 @@ Return the PR URL to the user when done.
| Committing or opening a PR before the user skips review or confirms review is done | Wait at the human review checkpoint |
| Using curly quotes or half-width Chinese punctuation | Follow `docs/AGENTS.md` |
| Omitting the release date from a version heading, or guessing it | Add ` (YYYY-MM-DD)` (full-width `` in Chinese) taken from the published tag |
| Forgetting or translating the `web:` prefix on web UI entries | Prefix web UI entries (commit touches `apps/kimi-web/`) with `web: ` on both pages; keep the prefix as-is when translating |
## Stop Signals

View file

@ -29,7 +29,6 @@ All other workspace packages are private internal packages, are not published to
- `@moonshot-ai/vis`
- `@moonshot-ai/vis-server`
- `@moonshot-ai/vis-web`
- `kimi-migration-legacy`
Version impact from internal dependencies must be judged manually. The published artifacts for CLI and SDK bundle internal workspace packages into the artifact itself; runtime `dependencies` of published packages must not include any `@moonshot-ai/*` internal workspace packages.

View file

@ -1,5 +1,5 @@
{
"changelog": ["@changesets/changelog-github", { "repo": "MoonshotAI/kimi-code", "disableThanks": true }],
"changelog": ["@changesets/changelog-github", { "repo": "MoonshotAI/kimi-code" }],
"commit": false,
"fixed": [],
"linked": [],
@ -10,8 +10,7 @@
"@moonshot-ai/server-e2e",
"@moonshot-ai/vis",
"@moonshot-ai/vis-server",
"@moonshot-ai/vis-web",
"kimi-migration-legacy"
"@moonshot-ai/vis-web"
],
"snapshot": {
"useCalculatedVersion": true,

View file

@ -44,6 +44,24 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm run test
# pi-tui's suite runs on node:test (not vitest), so the root `pnpm run test`
# does not execute it; it needs its own job.
test-pi-tui:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm --filter @moonshot-ai/pi-tui test
test-windows:
runs-on: windows-latest
# Temporarily disabled while Windows tests are being stabilized.

View file

@ -37,7 +37,7 @@ jobs:
registry-url: "https://registry.npmjs.org"
- name: Upgrade npm for Trusted Publishing
run: npm install -g npm@latest
run: npm install -g npm@11
- name: Install dependencies
run: pnpm install --frozen-lockfile

13
.gitignore vendored
View file

@ -24,3 +24,16 @@ docker-compose.yml
docs/superpowers/
reports/
.superpowers/
/plan/
# Agent scratch / throwaway files - do not commit
.tmp/
HANDOVER*.md
HANDOFF*.md
handoff.md
handover.md
*-designs.html
*-design.html
*-mockup.html
*-demo.html
*-demos.html

View file

@ -150,6 +150,7 @@
"node_modules/",
"apps/*/scripts/",
"docs/smoke-archive/",
"packages/pi-tui/",
"*.generated.ts"
]
}

View file

@ -77,3 +77,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
- After finishing a task and before submitting a PR, you must run the `gen-changesets` skill (see `.agents/skills/gen-changesets/SKILL.md`) and generate a changeset under `.changeset/` according to its rules.
- When generating a changeset, **never** decide on a `major` bump on your own. When you judge a change to meet the major criteria (breaking changes, incompatible user configuration, renamed or removed commands/arguments, changed behavior semantics, etc.), you must stop and explain it to the user and ask for confirmation. **Only write `major` after the user has explicitly agreed.** Otherwise default to `minor` (and fall back to `patch` if `minor` is unclear). See the "Hard rule: confirm with the user before writing `major`" section in `.agents/skills/gen-changesets/SKILL.md` for details.
- Prefer importing via `import ... from '#/...'`, which serves the same purpose as `import ... from '@/...'`.
- Do not commit throwaway scratch or exploratory files. Never stage:
- Agent working notes or handoff/summary documents (e.g. `HANDOVER-*.md`, `HANDOFF-*.md`, `handoff.md`).
- Throwaway UI/UX prototypes or design mockups (e.g. `*-designs.html`, `*-mockup.html`, `*-demo(s).html`) at the repo root or under a `design/` folder. The only tracked `.html` files should be Vite `index.html` entrypoints.
Before committing or opening a PR, run `git status` and `git diff --staged --stat` and remove anything matching these patterns. Put scratch work under `.tmp/` (gitignored) instead of the repo root or the source tree.

1
CLAUDE.md Symbolic link
View file

@ -0,0 +1 @@
AGENTS.md

View file

@ -6,3 +6,6 @@ agents/
# next to it keeps `#/generated/vis-web-asset` type-resolvable on a fresh
# clone (before any build has produced the `.ts`).
src/generated/vis-web-asset.ts
# Copied from packages/pi-tui/native at build time by scripts/copy-native-assets.mjs
native/

View file

@ -1,5 +1,313 @@
# @moonshot-ai/kimi-code
## 0.23.4
### Patch Changes
- [#1501](https://github.com/MoonshotAI/kimi-code/pull/1501) [`b91099e`](https://github.com/MoonshotAI/kimi-code/commit/b91099ed7a2590d1afa4d6e3675671da52b7661c) Thanks [@liruifengv](https://github.com/liruifengv)! - Display Extra Usage (fuel pack) balance in `/usage` and `/status` commands.
- [#1517](https://github.com/MoonshotAI/kimi-code/pull/1517) [`173bdfd`](https://github.com/MoonshotAI/kimi-code/commit/173bdfdab1f484ed79927aeaac7dc8116d3fd346) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix resuming sessions whose original working directory no longer exists.
- [#1516](https://github.com/MoonshotAI/kimi-code/pull/1516) [`9fb1915`](https://github.com/MoonshotAI/kimi-code/commit/9fb19154accf6b6f7abfbf7a9820ccda517bc87e) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix prompt-mode goals so they run until completion and report invalid goal commands before sending prompts.
- [#1508](https://github.com/MoonshotAI/kimi-code/pull/1508) [`1bf2c9a`](https://github.com/MoonshotAI/kimi-code/commit/1bf2c9afee4643fbf6755f0b92fd60aa14240501) Thanks [@RealKai42](https://github.com/RealKai42)! - Keep image-heavy sessions within provider request-size limits: model-read images now honor a 256 KB per-image budget and a 2000px downscale cap (configurable via `[image]` in config.toml or `KIMI_IMAGE_*` env vars), oversized WebP is compressed as well, HEIC/HEIF reads are refused with a platform-matched conversion command instead of poisoning the session, and a request-too-large rejection (HTTP 413) now recovers automatically — the request and /compact both retry with older media replaced by text markers instead of failing the session.
- [#1519](https://github.com/MoonshotAI/kimi-code/pull/1519) [`170ae44`](https://github.com/MoonshotAI/kimi-code/commit/170ae4420526b6592d696cd597d1693dbd1a660b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Polish the session sidebar layout, colors, icons, and typography.
- [#1521](https://github.com/MoonshotAI/kimi-code/pull/1521) [`046b6c4`](https://github.com/MoonshotAI/kimi-code/commit/046b6c417581792933732c7ffe154e120c96171d) Thanks [@RealKai42](https://github.com/RealKai42)! - The `[image]` limits in config.toml now also apply to pasted images (CLI paste and ACP prompts), and each core now uses its own settings, so reloading one client's config no longer changes another client's image compression.
- [#1494](https://github.com/MoonshotAI/kimi-code/pull/1494) [`a354803`](https://github.com/MoonshotAI/kimi-code/commit/a3548035a8b6d25df9a11daab37a21daee1ef73f) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add a Kimi WebBridge entry to the Official tab of the /plugins panel that opens the WebBridge install page in your browser.
- [#1479](https://github.com/MoonshotAI/kimi-code/pull/1479) [`735922c`](https://github.com/MoonshotAI/kimi-code/commit/735922c291ec3d32d60da6af053f75e1c6179f92) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Add notifications when a tool needs approval, and improve notification reliability.
- [#1522](https://github.com/MoonshotAI/kimi-code/pull/1522) [`ec8dc34`](https://github.com/MoonshotAI/kimi-code/commit/ec8dc3456c1696a5eba6c37b6e26ef99837c35e2) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix an occasional "another turn is active" error when sending the first message of a new conversation, and show a starting state while it is being sent.
- [#1502](https://github.com/MoonshotAI/kimi-code/pull/1502) [`ad30a1c`](https://github.com/MoonshotAI/kimi-code/commit/ad30a1c6328327729221f9f5fc700b621dfef779) Thanks [@chengluyu](https://github.com/chengluyu)! - web: Polish the chat UI with Inter typography, localized labels, and tighter sidebar, composer, and menu styling.
## 0.23.3
### Patch Changes
- [#1506](https://github.com/MoonshotAI/kimi-code/pull/1506) [`e83511a`](https://github.com/MoonshotAI/kimi-code/commit/e83511a7118652a67676bbcfd41148907ad7b8de) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix a misleading "OAuth login expired" message shown when a model is not available for the current account.
## 0.23.2
### Patch Changes
- [#1489](https://github.com/MoonshotAI/kimi-code/pull/1489) [`2206d21`](https://github.com/MoonshotAI/kimi-code/commit/2206d21327129aa2331b6b159cfce61110b7f94f) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add the Vercel plugin to the bundled plugin marketplace. Run /plugins and select Vercel Plugin to install it.
- [#1477](https://github.com/MoonshotAI/kimi-code/pull/1477) [`150206a`](https://github.com/MoonshotAI/kimi-code/commit/150206a6f7027879df954e26736b4baa5d336235) Thanks [@chengluyu](https://github.com/chengluyu)! - Count the turn that starts an autonomous goal toward its goal turn usage.
- [#1483](https://github.com/MoonshotAI/kimi-code/pull/1483) [`f30781b`](https://github.com/MoonshotAI/kimi-code/commit/f30781bb273321f3e3bbb548a9d0724ab6299fc6) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix `kimi -p` runs exiting with code 0 when a turn fails.
- [#1466](https://github.com/MoonshotAI/kimi-code/pull/1466) [`063bce2`](https://github.com/MoonshotAI/kimi-code/commit/063bce2a2f52601abaa0d13173ab88371cbbe9ae) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix console windows flashing on Windows each time a hook runs.
- [#1474](https://github.com/MoonshotAI/kimi-code/pull/1474) [`11c6a37`](https://github.com/MoonshotAI/kimi-code/commit/11c6a37ce030f8e64de5c810da07ec7fab3b0615) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the connection error toast lingering after the WebSocket reconnects when returning from the background.
- [#1476](https://github.com/MoonshotAI/kimi-code/pull/1476) [`d1a964f`](https://github.com/MoonshotAI/kimi-code/commit/d1a964fba9b3dca902ea6f81bacaccc839955c03) Thanks [@chengluyu](https://github.com/chengluyu)! - Prevent autonomous goals from being paused by model-reported status updates.
- [#1481](https://github.com/MoonshotAI/kimi-code/pull/1481) [`1317000`](https://github.com/MoonshotAI/kimi-code/commit/131700097a732b97b3d17c5e2efa1c5a44b013ef) Thanks [@chengluyu](https://github.com/chengluyu)! - Tighten goal-mode guidance for blocked and complete status updates.
- [#1460](https://github.com/MoonshotAI/kimi-code/pull/1460) [`474ce28`](https://github.com/MoonshotAI/kimi-code/commit/474ce289dd39aa42d1a77a9a2e15531aee49aa15) Thanks [@RealKai42](https://github.com/RealKai42)! - Raise the image downscale cap from 2000px to 3000px, and fix swapped width/height for EXIF-rotated (portrait) photos in compression captions and media read notes so region readback coordinates map correctly.
- [#1467](https://github.com/MoonshotAI/kimi-code/pull/1467) [`ee38545`](https://github.com/MoonshotAI/kimi-code/commit/ee385456d0eda380fec067db92c025462db13f5a) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Compile icons at build time so the bundled web UI only carries the icons it renders.
- [#1471](https://github.com/MoonshotAI/kimi-code/pull/1471) [`9b76e5b`](https://github.com/MoonshotAI/kimi-code/commit/9b76e5bff631cceaeecb2b0cbc096533c5fdc8cc) Thanks [@starquakee](https://github.com/starquakee)! - Progressive tool disclosure (`select_tools`, experimental): compaction now discards the loaded tool schemas instead of re-injecting them. After a compaction the boundary announcement re-lists every loadable tool name and the model re-selects what it still needs; a from-memory call to a no-longer-loaded tool is rejected with guidance to select it first. This keeps the post-compaction context at its minimal users+summary floor and removes the schema-rebuild budget heuristics. No effect unless the `tool-select` experimental flag and a `select_tools`-capable model are active.
- [#1491](https://github.com/MoonshotAI/kimi-code/pull/1491) [`0cc9831`](https://github.com/MoonshotAI/kimi-code/commit/0cc9831a2f79d93903259bd3353e746abac01b67) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: The composer model switcher switches the active session's model as before and additionally bumps the global default model, so new sessions inherit the choice.
- [#1490](https://github.com/MoonshotAI/kimi-code/pull/1490) [`b30a45e`](https://github.com/MoonshotAI/kimi-code/commit/b30a45efecfa5ece4f4f10f2c5403ba097e7690b) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Press Enter to confirm in archive and other confirmation dialogs.
- [#1480](https://github.com/MoonshotAI/kimi-code/pull/1480) [`2ad0120`](https://github.com/MoonshotAI/kimi-code/commit/2ad0120c2a5c8383892e4da1ee7c6853926ed365) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Redesign the scheduled reminder UI.
- [#1492](https://github.com/MoonshotAI/kimi-code/pull/1492) [`b0809dd`](https://github.com/MoonshotAI/kimi-code/commit/b0809ddac833d8d920d95187f7ef64f97bafdbc6) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Show session skills in the slash menu as `/skill:<name>` so they are distinguishable from built-in commands; typing the bare skill name still works.
## 0.23.1
### Patch Changes
- [#1432](https://github.com/MoonshotAI/kimi-code/pull/1432) [`25a655c`](https://github.com/MoonshotAI/kimi-code/commit/25a655cf88b2f5861f9c0b7ea95ba9308f48d23a) Thanks [@RealKai42](https://github.com/RealKai42)! - Preserve prior turns' thinking by default on the Anthropic provider (Claude and Kimi's Anthropic-compatible mode), matching the Kimi default. Disable with `[thinking] keep = "off"` or `KIMI_MODEL_THINKING_KEEP=off`.
- [#1451](https://github.com/MoonshotAI/kimi-code/pull/1451) [`16dc940`](https://github.com/MoonshotAI/kimi-code/commit/16dc940834d9cc693b1f0022c4c70ef0004a6102) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Recover chat streaming after a stale background-tab WebSocket instead of requiring a page refresh.
- [#1456](https://github.com/MoonshotAI/kimi-code/pull/1456) [`e9ef939`](https://github.com/MoonshotAI/kimi-code/commit/e9ef9399d075461c7753d1f0467bd742a81cdbf6) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix goal completion and blocked updates to produce one final user-facing outcome summary from the tool result.
- [#1456](https://github.com/MoonshotAI/kimi-code/pull/1456) [`e9ef939`](https://github.com/MoonshotAI/kimi-code/commit/e9ef9399d075461c7753d1f0467bd742a81cdbf6) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix goal startup and queue handling so failed starts restore permission mode and queued goals wait behind new user messages.
- [#1456](https://github.com/MoonshotAI/kimi-code/pull/1456) [`e9ef939`](https://github.com/MoonshotAI/kimi-code/commit/e9ef9399d075461c7753d1f0467bd742a81cdbf6) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix goal token budgets to count model completion tokens and stop without extra continuation steps when the budget is exhausted.
- [#1452](https://github.com/MoonshotAI/kimi-code/pull/1452) [`244ec07`](https://github.com/MoonshotAI/kimi-code/commit/244ec077f98c2b498cee1d0002978b6963ccfd4d) Thanks [@sailist](https://github.com/sailist)! - Fix kimi -p abandoning background subagents that start late or run long, so their results reach the main agent.
- [#1457](https://github.com/MoonshotAI/kimi-code/pull/1457) [`260a807`](https://github.com/MoonshotAI/kimi-code/commit/260a80793a95d7796950a00bdc89cf99f8b196ad) Thanks [@liruifengv](https://github.com/liruifengv)! - Respect the --skills-dir flag in interactive mode.
- [#1445](https://github.com/MoonshotAI/kimi-code/pull/1445) [`809a88c`](https://github.com/MoonshotAI/kimi-code/commit/809a88cb34d2d5d02e43f030530bb1cd320b4a6a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix `/btw [<question>]` opening an empty side chat on the new-session screen.
- [#1445](https://github.com/MoonshotAI/kimi-code/pull/1445) [`809a88c`](https://github.com/MoonshotAI/kimi-code/commit/809a88cb34d2d5d02e43f030530bb1cd320b4a6a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix `/goal <objective>` silently doing nothing on the new-session screen.
- [#1445](https://github.com/MoonshotAI/kimi-code/pull/1445) [`809a88c`](https://github.com/MoonshotAI/kimi-code/commit/809a88cb34d2d5d02e43f030530bb1cd320b4a6a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix slash skill activations (for example `/pre-changelog`) silently doing nothing on the new-session screen.
- [#1437](https://github.com/MoonshotAI/kimi-code/pull/1437) [`743f66e`](https://github.com/MoonshotAI/kimi-code/commit/743f66e547279916d5e37454e78b11eb4b54dca3) Thanks [@RealKai42](https://github.com/RealKai42)! - Stop showing tool-produced `<system>` metadata in tool outputs; failed tools now show their own error text.
- [#1465](https://github.com/MoonshotAI/kimi-code/pull/1465) [`bfdbce5`](https://github.com/MoonshotAI/kimi-code/commit/bfdbce593f1dd667530cbfb5b10b5659b1968e48) Thanks [@liruifengv](https://github.com/liruifengv)! - Honor explicit Anthropic `max_output_size` settings instead of clamping them to built-in ceilings.
- [#1456](https://github.com/MoonshotAI/kimi-code/pull/1456) [`e9ef939`](https://github.com/MoonshotAI/kimi-code/commit/e9ef9399d075461c7753d1f0467bd742a81cdbf6) Thanks [@chengluyu](https://github.com/chengluyu)! - Keep goal tools available to the main agent and return clear messages for invalid goal-control calls.
- [#1463](https://github.com/MoonshotAI/kimi-code/pull/1463) [`03e78ae`](https://github.com/MoonshotAI/kimi-code/commit/03e78ae19063b38119f27b1bc89097a09614c0ce) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix newer Claude minor versions (e.g. Opus 4.8) defaulting to the family-baseline max output tokens; an uncatalogued minor now reuses the nearest earlier known version's limit.
- [#1456](https://github.com/MoonshotAI/kimi-code/pull/1456) [`e9ef939`](https://github.com/MoonshotAI/kimi-code/commit/e9ef9399d075461c7753d1f0467bd742a81cdbf6) Thanks [@chengluyu](https://github.com/chengluyu)! - Show long-running goal wall-clock budget reminders in hours.
- [#1456](https://github.com/MoonshotAI/kimi-code/pull/1456) [`e9ef939`](https://github.com/MoonshotAI/kimi-code/commit/e9ef9399d075461c7753d1f0467bd742a81cdbf6) Thanks [@chengluyu](https://github.com/chengluyu)! - Tighten goal-mode guidance so agents continue reasonable work across turns instead of ending goals prematurely.
- [#1450](https://github.com/MoonshotAI/kimi-code/pull/1450) [`7a65e0d`](https://github.com/MoonshotAI/kimi-code/commit/7a65e0d1c0da515dbd69f1266ba7e75713e0108e) Thanks [@liruifengv](https://github.com/liruifengv)! - Clarify the permission mode descriptions shown by `/permission`, `/auto`, and `/yolo`, and reorder `/auto` and `/yolo` in the command list.
- [#1448](https://github.com/MoonshotAI/kimi-code/pull/1448) [`65d3017`](https://github.com/MoonshotAI/kimi-code/commit/65d30177adc11a56bdbbe9fbc3c4b92f96efd6bb) Thanks [@RealKai42](https://github.com/RealKai42)! - Record a per-request trace in the session wire log, so model requests can be reconstructed for debugging. Not a user-facing feature.
## 0.23.0
### Minor Changes
- [#1417](https://github.com/MoonshotAI/kimi-code/pull/1417) [`79b360c`](https://github.com/MoonshotAI/kimi-code/commit/79b360c96ad5d0af6a8c1d3a8df73adc65254d7c) Thanks [@RealKai42](https://github.com/RealKai42)! - Enable Preserved Thinking by default for Kimi models when Thinking is on, keeping prior reasoning across turns. Set `[thinking] keep = "off"` in config.toml (or `KIMI_MODEL_THINKING_KEEP=off`) to disable it.
- [#1073](https://github.com/MoonshotAI/kimi-code/pull/1073) [`6c0ce09`](https://github.com/MoonshotAI/kimi-code/commit/6c0ce09414bbfd42d8991c88c89b82c088ff2099) Thanks [@sailist](https://github.com/sailist)! - Add server APIs to restore archived sessions and list only archived sessions.
- [#1369](https://github.com/MoonshotAI/kimi-code/pull/1369) [`f0896a5`](https://github.com/MoonshotAI/kimi-code/commit/f0896a53b01f7e5b9bf5b8f93d2cd7387d765f07) Thanks [@starquakee](https://github.com/starquakee)! - Add experimental progressive tool disclosure (`select_tools`). When the `tool-select` experimental flag is on and the active model declares the `select_tools` capability, MCP tool schemas stay out of the request's top-level `tools[]` (preserving the provider prompt cache); the model loads tools on demand by exact name via the new built-in `select_tools` tool, guided by `<tools_added>/<tools_removed>` announcements. Off by default and inert on models without the capability — behavior is unchanged until a supporting model is catalogued. The SDK additionally maps the `select_tools` capability when building model aliases from a catalog and reports the new flag through `getExperimentalFeatures()`.
- [#1073](https://github.com/MoonshotAI/kimi-code/pull/1073) [`6c0ce09`](https://github.com/MoonshotAI/kimi-code/commit/6c0ce09414bbfd42d8991c88c89b82c088ff2099) Thanks [@sailist](https://github.com/sailist)! - web: Add an Archived sessions page in Settings to browse and restore archived sessions. Open Settings → Archived to find it.
- [#1425](https://github.com/MoonshotAI/kimi-code/pull/1425) [`c5e3e80`](https://github.com/MoonshotAI/kimi-code/commit/c5e3e80041a763143934c271d2524ac555d48d2a) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Replace the swarm footer with a single inline tool card that shows live subagent progress and the aggregated result, and keep swarm badges stable after refresh.
### Patch Changes
- [#1414](https://github.com/MoonshotAI/kimi-code/pull/1414) [`c12f309`](https://github.com/MoonshotAI/kimi-code/commit/c12f30951f2c278bebb42d0f61d67946c42b9577) Thanks [@RealKai42](https://github.com/RealKai42)! - AskUserQuestion now feeds answers back to the model as question text and option labels (e.g. `{"answers":{"Which database?":"Postgres"}}`) instead of synthesized ids like `q_0`/`opt_0_1`, so the model no longer has to map positional ids back to the original options — the wire protocol is unchanged, clients still answer with option ids. Question texts must now be unique per call and option labels unique per question; the web transcript card resolves both the new label form and legacy id transcripts.
- [#1408](https://github.com/MoonshotAI/kimi-code/pull/1408) [`fc259ab`](https://github.com/MoonshotAI/kimi-code/commit/fc259abdb415fe9ac10132a142bdb5ce507ccda2) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix `@` file completion missing deeply nested files in large projects after adding extra workspace directories.
- [#1346](https://github.com/MoonshotAI/kimi-code/pull/1346) [`b9258ee`](https://github.com/MoonshotAI/kimi-code/commit/b9258ee07d32ff63afe9a2eb40fce6d136548fb2) Thanks [@liruifengv](https://github.com/liruifengv)! - Show compaction summaries in the TUI after compaction. Press Ctrl-O to show or hide the summary.
- [#1419](https://github.com/MoonshotAI/kimi-code/pull/1419) [`5ea3ec4`](https://github.com/MoonshotAI/kimi-code/commit/5ea3ec489e0a7d66b844c39ee65162fd6a8ed8b1) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix the Bash tool card collapsing in height when a multi-line command finishes with short output, and visually separate the command from its output.
- [#1410](https://github.com/MoonshotAI/kimi-code/pull/1410) [`1c817df`](https://github.com/MoonshotAI/kimi-code/commit/1c817df1e522f438d4392568b64fc039dc867031) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix the edit approval preview shown by ctrl+e to include surrounding context lines, matching the summary panel.
- [#1421](https://github.com/MoonshotAI/kimi-code/pull/1421) [`1de0286`](https://github.com/MoonshotAI/kimi-code/commit/1de028612c80c38cd6fbe4483c123ded57a0a678) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix the Edit tool card jumping in height and flickering while its result streams in.
- [#1389](https://github.com/MoonshotAI/kimi-code/pull/1389) [`ebdffc7`](https://github.com/MoonshotAI/kimi-code/commit/ebdffc7df7b89dcafcf62f5705eafb50bfbaf5ab) Thanks [@sailist](https://github.com/sailist)! - Fix tool calling with Google Gemini models, including Gemini 3 thinking-signature round-trips across turns.
- [#1393](https://github.com/MoonshotAI/kimi-code/pull/1393) [`4c43935`](https://github.com/MoonshotAI/kimi-code/commit/4c43935e31170140699a54cba631792872628655) Thanks [@justjavac](https://github.com/justjavac)! - web: Show the correct session search shortcut on Windows.
- [#1406](https://github.com/MoonshotAI/kimi-code/pull/1406) [`ce41f4b`](https://github.com/MoonshotAI/kimi-code/commit/ce41f4b58d128ae47b0312eab24a845bbc0d08a3) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the collapsed sidebar not hiding and squeezing the conversation layout.
- [#1413](https://github.com/MoonshotAI/kimi-code/pull/1413) [`913d042`](https://github.com/MoonshotAI/kimi-code/commit/913d042208b4bfe45dc13144c4797dba1cac5d05) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix the input box shifting upward after the slash command menu closes.
- [#1433](https://github.com/MoonshotAI/kimi-code/pull/1433) [`ac5b5e4`](https://github.com/MoonshotAI/kimi-code/commit/ac5b5e4cbfdb050817c9fce7e08dd3bdd8ea354e) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix tool components jumping the conversation when expanded or collapsed.
- [#1394](https://github.com/MoonshotAI/kimi-code/pull/1394) [`e95fc83`](https://github.com/MoonshotAI/kimi-code/commit/e95fc83cc295dccf3e4c748fba087530dba614b6) Thanks [@justjavac](https://github.com/justjavac)! - web: Fix the font size setting so chat text, composer text, and sidebar text follow the selected size.
- [#1411](https://github.com/MoonshotAI/kimi-code/pull/1411) [`e6e6dd5`](https://github.com/MoonshotAI/kimi-code/commit/e6e6dd53ce9106f47684534a91acb1a803d1ab07) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Stop the chat history from replaying its entrance animation every time a session is opened.
- [#1409](https://github.com/MoonshotAI/kimi-code/pull/1409) [`578f7d3`](https://github.com/MoonshotAI/kimi-code/commit/578f7d334c7919e5987229c157060b1daae30139) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix the end of a reply staying missing after reopening a session.
- [#1390](https://github.com/MoonshotAI/kimi-code/pull/1390) [`083d0ca`](https://github.com/MoonshotAI/kimi-code/commit/083d0caf0524ed9cc7978007cd0f342f6bd2917e) Thanks [@sailist](https://github.com/sailist)! - Fix sessions that exist on disk but were missing from the session list or returned 404 on direct access, by rebuilding the session index at server startup and keeping it consistent.
- [#1357](https://github.com/MoonshotAI/kimi-code/pull/1357) [`be7c991`](https://github.com/MoonshotAI/kimi-code/commit/be7c9916b019b19e057301c39bc7944fcac09414) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Fix queued media messages not loading back into the composer and keep attachments when undoing a message.
- [#1428](https://github.com/MoonshotAI/kimi-code/pull/1428) [`903e8ed`](https://github.com/MoonshotAI/kimi-code/commit/903e8ed93afc5b35d0fa1d33c86da2b3fae9ba9f) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Add an Archived sessions entry to the mobile settings sheet and clarify the archive confirmation to mention restoring from Settings.
- [#1391](https://github.com/MoonshotAI/kimi-code/pull/1391) [`c5c6282`](https://github.com/MoonshotAI/kimi-code/commit/c5c6282f447dba202c79cf0e3b7524712d2c2748) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Render AskUserQuestion answers as a readable option list with the chosen option(s) highlighted, instead of raw JSON.
- [#1423](https://github.com/MoonshotAI/kimi-code/pull/1423) [`fa6d198`](https://github.com/MoonshotAI/kimi-code/commit/fa6d198b0174ad76aa4ca3c0ea2ed45e099e521b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Fix an almost-invisible composer input caret and a washed-out strikethrough on completed todos.
- [#1436](https://github.com/MoonshotAI/kimi-code/pull/1436) [`a5fbcb7`](https://github.com/MoonshotAI/kimi-code/commit/a5fbcb75b4b3ab937536a7a2f621c0374812c753) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Keep the composer toolbar from clipping its controls on narrow windows and phones, with the context ring staying visible at every width.
- [#1426](https://github.com/MoonshotAI/kimi-code/pull/1426) [`2374bc4`](https://github.com/MoonshotAI/kimi-code/commit/2374bc41c35adc1d2e2b5116559946c8de1b98a8) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Show scheduled-reminder (cron) fires as notice cards in the chat instead of hiding them.
- [#1391](https://github.com/MoonshotAI/kimi-code/pull/1391) [`c5c6282`](https://github.com/MoonshotAI/kimi-code/commit/c5c6282f447dba202c79cf0e3b7524712d2c2748) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Align the markdown diff code block with the design system: code text keeps the normal ink colour while the sign and a soft row background carry the change, matching the ~/diff panel.
- [#1434](https://github.com/MoonshotAI/kimi-code/pull/1434) [`4aacddc`](https://github.com/MoonshotAI/kimi-code/commit/4aacddc43222d0a44f202360462617788ca75660) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Show the Kimi icon and clearer titles in web desktop notifications.
- [#1392](https://github.com/MoonshotAI/kimi-code/pull/1392) [`4963c90`](https://github.com/MoonshotAI/kimi-code/commit/4963c9016fa19d1e01f8dc938c8d250afec87965) Thanks [@sailist](https://github.com/sailist)! - web: Show available skills in the composer before a session is created.
- [#1438](https://github.com/MoonshotAI/kimi-code/pull/1438) [`d86fa38`](https://github.com/MoonshotAI/kimi-code/commit/d86fa38e119c5834fff13a67194efe8d62c117e1) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Prevent chat text from hyphenating at line breaks and render code without font ligatures.
- [#1391](https://github.com/MoonshotAI/kimi-code/pull/1391) [`c5c6282`](https://github.com/MoonshotAI/kimi-code/commit/c5c6282f447dba202c79cf0e3b7524712d2c2748) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: Drop the stray left indent in the tool-call card body so expanded content aligns with the header.
## 0.22.3
### Patch Changes
- [#1367](https://github.com/MoonshotAI/kimi-code/pull/1367) [`23daf0f`](https://github.com/MoonshotAI/kimi-code/commit/23daf0f3c199b4aaa9bd9388a2903d7827f98d32) - Revert the recent TUI transcript rendering changes to the original upstream behavior and fix related rendering issues.
- [#1343](https://github.com/MoonshotAI/kimi-code/pull/1343) [`ec758c7`](https://github.com/MoonshotAI/kimi-code/commit/ec758c747a95555847b8a0275ed0809010c7d5e7) - Add click-to-enlarge for images uploaded in the web chat. Click an image in a message to open it.
- [#1343](https://github.com/MoonshotAI/kimi-code/pull/1343) [`ec758c7`](https://github.com/MoonshotAI/kimi-code/commit/ec758c747a95555847b8a0275ed0809010c7d5e7) - Fix uploaded videos failing to play in the web chat.
- [#1371](https://github.com/MoonshotAI/kimi-code/pull/1371) [`5394fea`](https://github.com/MoonshotAI/kimi-code/commit/5394feaabb5d373fab046b3986b10a1180b4991d) - Wait for background subagents to finish and respond to their results before exiting in `kimi -p`, instead of ending the turn early.
- [#1373](https://github.com/MoonshotAI/kimi-code/pull/1373) [`e715b16`](https://github.com/MoonshotAI/kimi-code/commit/e715b1648c57bd0863edf859cb67db0327b7bb94) - Add `--dangerous-bypass-auth` and `--keep-alive` flags to `kimi server run`, so the server can run without a token on trusted networks and stay alive past the idle timeout.
- [#1344](https://github.com/MoonshotAI/kimi-code/pull/1344) [`26b9022`](https://github.com/MoonshotAI/kimi-code/commit/26b90225d21bd18f4f7e3b775f3f7f49034afad9) - Add a segmented thinking-level control in the web model picker for models that support multiple reasoning efforts. Open the composer model menu to choose a level.
## 0.22.2
### Patch Changes
- [#1353](https://github.com/MoonshotAI/kimi-code/pull/1353) [`68ad686`](https://github.com/MoonshotAI/kimi-code/commit/68ad686211760eb1c3e6b5c23eb28ace9009c17f) - Fix duplicated transcript content appearing in scrollback during streaming.
- [#1340](https://github.com/MoonshotAI/kimi-code/pull/1340) [`e2fe62a`](https://github.com/MoonshotAI/kimi-code/commit/e2fe62a5eff124b816a0f656355fc5bea85e3893) - Fix sessions silently dropping later user messages after a turn was interrupted between a tool call and its result.
- [#1342](https://github.com/MoonshotAI/kimi-code/pull/1342) [`84d8d5b`](https://github.com/MoonshotAI/kimi-code/commit/84d8d5b06399d29a9d8caba701835061c30a4817) - Have context-compaction notes capture a forward plan for the remaining work — upcoming steps, settled decisions, and foreseeable obstacles — instead of only the immediate next step, so the agent continues more coherently after auto-compaction.
- [#1340](https://github.com/MoonshotAI/kimi-code/pull/1340) [`e2fe62a`](https://github.com/MoonshotAI/kimi-code/commit/e2fe62a5eff124b816a0f656355fc5bea85e3893) - Fix requests being rejected by strict providers when the model emits duplicate tool call ids.
- [#1339](https://github.com/MoonshotAI/kimi-code/pull/1339) [`021786f`](https://github.com/MoonshotAI/kimi-code/commit/021786f5a201df5466a963d4d1ac915b3977582b) - Enrich PATH from the user's login shell at startup, so shell commands find user-installed tools (e.g. Homebrew's `gh`) even when kimi-code was launched without the full profile PATH.
- [#1336](https://github.com/MoonshotAI/kimi-code/pull/1336) [`4c1d0a1`](https://github.com/MoonshotAI/kimi-code/commit/4c1d0a1633c98ae5703addbf86ffe50b81545c08) - Keep automatic background updates from flashing a console window on Windows.
- [#1332](https://github.com/MoonshotAI/kimi-code/pull/1332) [`93f16c3`](https://github.com/MoonshotAI/kimi-code/commit/93f16c32d71d974f30c3ea3b1134691936ac5f53) - Fix `kimi upgrade` failing on Windows with a spawn error when installing the new version.
- [#1348](https://github.com/MoonshotAI/kimi-code/pull/1348) [`175b95f`](https://github.com/MoonshotAI/kimi-code/commit/175b95f3af684f7c5447967b9fe7c8a58b6ffe1b) - Fix compressed-image prompts leaking an internal `<system>` compression note into the visible message and the session title.
- [#1338](https://github.com/MoonshotAI/kimi-code/pull/1338) [`276407d`](https://github.com/MoonshotAI/kimi-code/commit/276407d2a46b03ce32cce02b73c5d485b1b02b17) - Promote the language-matching rule to a dedicated section in the system prompt, so replies and reasoning consistently follow the user's language through long English tool output, while repository artifacts keep project conventions.
- [#1347](https://github.com/MoonshotAI/kimi-code/pull/1347) [`02da587`](https://github.com/MoonshotAI/kimi-code/commit/02da5877953ce082826ba5ab1a1abd914d82b24a) - In `kimi -p` runs, wait for background subagents to finish before exiting when `background.keep_alive_on_exit` is enabled. Set `keep_alive_on_exit = true` to let concurrent background subagents complete.
- [#1349](https://github.com/MoonshotAI/kimi-code/pull/1349) [`e9db9ca`](https://github.com/MoonshotAI/kimi-code/commit/e9db9cafcf7a0d26122b2cac247d866d7724fd7a) - Record model response ids in session wire logs to make individual model requests easier to trace.
- [#1345](https://github.com/MoonshotAI/kimi-code/pull/1345) [`3ed22e3`](https://github.com/MoonshotAI/kimi-code/commit/3ed22e35a4ee09ce353e699406c6c994423ff39f) - Keep subagent cards at a stable height and show a live status spinner with a compact two-row activity window.
- [#1305](https://github.com/MoonshotAI/kimi-code/pull/1305) [`9091627`](https://github.com/MoonshotAI/kimi-code/commit/909162725770700efd3051f4cfa68156d9b84fa8) - Add a TUI preference to keep rapid multi-line pastes from submitting line by line when bracketed paste is unavailable. Set `disable_paste_burst = true` in `tui.toml` to turn it off.
- [#1328](https://github.com/MoonshotAI/kimi-code/pull/1328) [`01b65bd`](https://github.com/MoonshotAI/kimi-code/commit/01b65bdddc28c7c492096000103687f6a507e353) - Rebuild the web design-system easter egg as an in-app overlay that uses the app's real design tokens, so it stays in sync instead of drifting as a separate copy.
## 0.22.1
### Patch Changes
- [#1304](https://github.com/MoonshotAI/kimi-code/pull/1304) [`0fc0ae3`](https://github.com/MoonshotAI/kimi-code/commit/0fc0ae380b09aa96aad0eff1ae66f239e061d01a) - When large images are compressed, tell the model the original and delivered image details. Keep the original image available, and support cropped or full-resolution reads for fine details.
- [#1315](https://github.com/MoonshotAI/kimi-code/pull/1315) [`b40bb71`](https://github.com/MoonshotAI/kimi-code/commit/b40bb7139939eb2ba734ce5dd4871b894d7033e8) - Fix TUI rendering bugs that caused the screen to go blank and the input box to disappear.
- [#1303](https://github.com/MoonshotAI/kimi-code/pull/1303) [`2639786`](https://github.com/MoonshotAI/kimi-code/commit/2639786ce578f15c020a2c11c344797dae18de61) - Fix the TUI crashing when the terminal is resized to a very narrow width while the input contains CJK or emoji text.
- [#1315](https://github.com/MoonshotAI/kimi-code/pull/1315) [`b40bb71`](https://github.com/MoonshotAI/kimi-code/commit/b40bb7139939eb2ba734ce5dd4871b894d7033e8) - Clear the screen fully when starting a new session via /new, /clear, or a session switch.
- [#1301](https://github.com/MoonshotAI/kimi-code/pull/1301) [`c3653a1`](https://github.com/MoonshotAI/kimi-code/commit/c3653a1c50ffa3856484599e132980628eb9fca4) - Show an up arrow on the web composer send button.
- [#1290](https://github.com/MoonshotAI/kimi-code/pull/1290) [`3ea84a5`](https://github.com/MoonshotAI/kimi-code/commit/3ea84a56e4dfdeaddd58add5b269be0342f3f986) - Fix the session search dialog showing a horizontal scrollbar for long session titles or snippets.
- [#1316](https://github.com/MoonshotAI/kimi-code/pull/1316) [`5322c63`](https://github.com/MoonshotAI/kimi-code/commit/5322c638895a934c1ce220fefed54f5077d2a49e) - Fix web tooltips that could get stuck on screen when their trigger element is removed while open.
- [#1319](https://github.com/MoonshotAI/kimi-code/pull/1319) [`e8ab7ca`](https://github.com/MoonshotAI/kimi-code/commit/e8ab7ca78661de7f00a8196444be1db93e7c14b4) - Fix the sidebar session row shifting its title and status badges when hovered.
- [#1293](https://github.com/MoonshotAI/kimi-code/pull/1293) [`6a469b3`](https://github.com/MoonshotAI/kimi-code/commit/6a469b3e07022e56b29b1fd8a7c58df36b2111fe) - Refresh the web UI icon set and unify the message copy and undo button hover states and tooltips.
- [#1311](https://github.com/MoonshotAI/kimi-code/pull/1311) [`b40649b`](https://github.com/MoonshotAI/kimi-code/commit/b40649b2ae7a4b6a0aea04e32eba200555393064) - Remove duplicate newline-shortcut handling from the prompt editor.
- [#1317](https://github.com/MoonshotAI/kimi-code/pull/1317) [`78a058a`](https://github.com/MoonshotAI/kimi-code/commit/78a058acd2fc91de5cca0c1d66d415ee35884889) - Remove the experimental micro compaction feature and its toggle from the experiments panel.
- [#1283](https://github.com/MoonshotAI/kimi-code/pull/1283) [`ea55911`](https://github.com/MoonshotAI/kimi-code/commit/ea55911062eefcb0414cfddb84c8a4494c45f363) - Improve compaction handoff summaries for more reliable resumed sessions. They now keep the latest intent, key tool results, decisions, open questions, and context to re-check.
- [#1295](https://github.com/MoonshotAI/kimi-code/pull/1295) [`77eb3a9`](https://github.com/MoonshotAI/kimi-code/commit/77eb3a9fe40c93fa32e335f07160b8128355bab6) - Save shell commands to input history and recall them in bash mode. Press Up on an empty `!` prompt to browse previous shell commands.
- [#1316](https://github.com/MoonshotAI/kimi-code/pull/1316) [`5322c63`](https://github.com/MoonshotAI/kimi-code/commit/5322c638895a934c1ce220fefed54f5077d2a49e) - Trim redundant and incorrect tooltips in the web UI.
- [#1320](https://github.com/MoonshotAI/kimi-code/pull/1320) [`444e6b1`](https://github.com/MoonshotAI/kimi-code/commit/444e6b15f0e53b6c4d75d1bfdc0b35639dce6f4c) - Fix the web UI becoming sluggish after opening many sessions.
- [#1322](https://github.com/MoonshotAI/kimi-code/pull/1322) [`5441ad1`](https://github.com/MoonshotAI/kimi-code/commit/5441ad1838a5cfa1f3df0ca2ee1524e1433fb513) - Let the web sidebar collapse an expanded workspace session list back to its first page.
## 0.22.0
### Minor Changes
- [#1243](https://github.com/MoonshotAI/kimi-code/pull/1243) [`ace7901`](https://github.com/MoonshotAI/kimi-code/commit/ace79010669d19ad175bc25443b6efb41ca2e2ac) - Automatically compress oversized images before they reach the model. Whatever the source — pasted into the CLI, uploaded from the web/desktop client, sent over ACP, read via `ReadMediaFile`, or returned by an MCP tool — images are downsampled (longest edge ≤ 2000px) and re-encoded to fit a per-image byte budget, cutting vision-token cost and avoiding provider image-size errors. Screenshots stay lossless PNG and only degrade to JPEG when the byte budget cannot otherwise be met. Compression runs as an input-stage step at each ingestion point (while the content part is built), and guards against decompression bombs by skipping absurdly large pixel/byte payloads before decoding. Best-effort: if it fails for any reason the original image is sent unchanged.
- [#1262](https://github.com/MoonshotAI/kimi-code/pull/1262) [`c070fbe`](https://github.com/MoonshotAI/kimi-code/commit/c070fbeddeb1c147d8859a76046f9465f696c9cb) - Add model alias overrides so manual thinking effort levels and model metadata survive provider catalog refreshes. Set them under `[models."<alias>".overrides]`.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Refresh the web UI with a new design system, including updated colors, typography, spacing, light and dark palettes, restyled tooltips, and subtle enter/exit and expand/collapse animations.
### Patch Changes
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Show draft pull requests with a distinct draft status instead of displaying them as open.
- [#1254](https://github.com/MoonshotAI/kimi-code/pull/1254) [`7859b0a`](https://github.com/MoonshotAI/kimi-code/commit/7859b0afe8898852806e5a0c21b9dd52cb82f834) - Fix the transcript jumping to the top when scrolling up through history during streaming output.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Fix plan, swarm, and goal modes being shared across sessions in the web UI; each session now keeps its own toggles.
- [#1264](https://github.com/MoonshotAI/kimi-code/pull/1264) [`003733c`](https://github.com/MoonshotAI/kimi-code/commit/003733c751584ce30d8ebae4f5e608f0df049d32) - Hide the unsupported Off option in the /model thinking switcher for always-on models that already expose multiple effort levels.
- [#1272](https://github.com/MoonshotAI/kimi-code/pull/1272) [`54703d9`](https://github.com/MoonshotAI/kimi-code/commit/54703d9457dcda7bc782301fc2dbb41a2c8d7293) - Release pasted images and streaming timers once they are no longer shown, so memory stops growing in long sessions.
- [#1272](https://github.com/MoonshotAI/kimi-code/pull/1272) [`54703d9`](https://github.com/MoonshotAI/kimi-code/commit/54703d9457dcda7bc782301fc2dbb41a2c8d7293) - Fix the terminal being left in raw mode with a hidden cursor and disabled flow control after a crash or abrupt exit.
- [#1265](https://github.com/MoonshotAI/kimi-code/pull/1265) [`8cfb165`](https://github.com/MoonshotAI/kimi-code/commit/8cfb1657ad7bf525269df4ab6cf5c12aa1d406a9) - Reduce the default TUI transcript window to keep long sessions responsive.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Replace the Explore and Native theme options with a single chat layout and a Blue or Black accent-color setting.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Show time, duration, connection, and stack details in web error and warning toasts.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Fix an active workspace showing only its five most recent sessions on load, so it now keeps loading older sessions from the last 12 hours.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Reduce the web composer's default height for a more compact empty state, and fix ArrowUp recalling the previous message while editing a multi-line draft; ArrowUp now recalls only from the very start of the text and is disabled in the expanded editor.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Fix the Thinking-by-default setting not taking effect, so new sessions correctly start with thinking enabled.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Fix spurious errors from the web question, approval, and task actions when the action was already complete, and add loading feedback so each click is acknowledged immediately.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Show queued prompts inline below the running turn in the web chat, and split Stop into its own button so Send no longer interrupts.
- [#1278](https://github.com/MoonshotAI/kimi-code/pull/1278) [`bbda90a`](https://github.com/MoonshotAI/kimi-code/commit/bbda90af846ca66232158d2e9605d3d59a7e3a49) - Hide the conversation outline when there is not enough room to expand its labels, so it no longer clips against the window edge.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Show the conversation outline as one entry per user query that expands into a labeled list on hover.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Remove the fade-out animation when undoing a message in the web chat.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Improve session search with a Cmd/Ctrl+K palette that filters by title, workspace, and last prompt with highlighted matches. Press Cmd+K or Ctrl+K to open it.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Group consecutive tool calls into a collapsible stack with per-tool renderers, including diff line-count chips for edits and inline previews for image, video, and audio results.
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Use one consistent modal dialog for confirmations in the web UI (archive session, delete workspace, delete provider, undo message, and mode toggles).
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Add workspace sorting by manual order or last-edited time, plus collapse-all and expand-all controls, to the sidebar.
## 0.21.1
### Patch Changes

View file

@ -1,6 +1,6 @@
{
"name": "@moonshot-ai/kimi-code",
"version": "0.21.1",
"version": "0.23.4",
"description": "The Starting Point for Next-Gen Agents",
"license": "MIT",
"author": "Moonshot AI",
@ -28,6 +28,7 @@
"files": [
"dist",
"dist-web",
"native",
"scripts/postinstall.mjs",
"scripts/postinstall",
"README.md"
@ -48,7 +49,7 @@
"provenance": true
},
"scripts": {
"build": "pnpm -C ../kimi-web run build && tsdown && node scripts/copy-web-assets.mjs",
"build": "pnpm -C ../kimi-web run build && tsdown && node scripts/copy-native-assets.mjs && node scripts/copy-web-assets.mjs",
"prebuild": "node scripts/build-vis-asset.mjs",
"catalog:update": "node scripts/update-catalog.mjs --out dist/built-in-catalog.json",
"smoke": "node scripts/smoke.mjs",
@ -75,17 +76,16 @@
},
"optionalDependencies": {
"@mariozechner/clipboard": "^0.3.9",
"koffi": "^2.16.0",
"node-pty": "^1.1.0"
},
"devDependencies": {
"@earendil-works/pi-tui": "^0.74.0",
"@moonshot-ai/acp-adapter": "workspace:^",
"@moonshot-ai/kimi-code-oauth": "workspace:^",
"@moonshot-ai/kimi-code-sdk": "workspace:^",
"@moonshot-ai/kimi-telemetry": "workspace:^",
"@moonshot-ai/kimi-web": "workspace:^",
"@moonshot-ai/migration-legacy": "workspace:^",
"@moonshot-ai/pi-tui": "workspace:^",
"@moonshot-ai/server": "workspace:^",
"@moonshot-ai/vis-server": "workspace:^",
"@moonshot-ai/vis-web": "workspace:*",
@ -94,6 +94,7 @@
"chalk": "^5.4.1",
"cli-highlight": "^2.1.11",
"commander": "^13.1.0",
"jimp": "^1.6.1",
"pathe": "^2.0.3",
"postject": "1.0.0-alpha.6",
"semver": "^7.7.4",

View file

@ -0,0 +1,38 @@
import { cp, mkdir, rm, stat } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const repoRoot = resolve(appRoot, '../..');
const source = resolve(repoRoot, 'packages/pi-tui/native');
const target = resolve(appRoot, 'native');
// pi-tui ships platform-specific native helpers only for darwin/win32;
// Linux has no native helper, so there is nothing to copy for it.
const PLATFORMS = ['darwin', 'win32'];
async function assertPrebuilds(platform) {
const dir = resolve(source, platform, 'prebuilds');
try {
const info = await stat(dir);
if (!info.isDirectory()) {
throw new Error('not a directory');
}
} catch {
throw new Error(
`pi-tui native prebuilds were not found at ${dir}. Build or restore packages/pi-tui first.`,
);
}
return dir;
}
await rm(target, { recursive: true, force: true });
await mkdir(target, { recursive: true });
for (const platform of PLATFORMS) {
const srcPrebuilds = await assertPrebuilds(platform);
const dstPrebuilds = resolve(target, platform, 'prebuilds');
await cp(srcPrebuilds, dstPrebuilds, { recursive: true });
}
console.log(`Copied pi-tui native prebuilds to ${target}`);

View file

@ -2,13 +2,16 @@
import { spawn } from 'node:child_process';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { startPluginMarketplaceServer } from './dev-plugin-marketplace-server.mjs';
const require = createRequire(import.meta.url);
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
const APP_ROOT = resolve(SCRIPT_DIR, '..');
// Monorepo root. Used as the dev CLI's working directory so `make dev` opens
// the whole repo instead of just apps/kimi-code.
const REPO_ROOT = resolve(APP_ROOT, '../..');
// Runtime variable the CLI reads to locate the marketplace JSON.
const MARKETPLACE_ENV = 'KIMI_CODE_PLUGIN_MARKETPLACE_URL';
// Opt-in for dev: point this run at an external marketplace instead of a local one.
@ -48,14 +51,14 @@ const child = spawn(
// esbuild transform sees `experimentalDecorators: true` for DI parameter
// decorators in agent-core. Mirrors `dev:server` in package.json.
'--tsconfig',
'./tsconfig.dev.json',
resolve(APP_ROOT, 'tsconfig.dev.json'),
'--import',
'../../build/register-raw-text-loader.mjs',
'./src/main.ts',
pathToFileURL(resolve(REPO_ROOT, 'build/register-raw-text-loader.mjs')).href,
resolve(APP_ROOT, 'src/main.ts'),
...cliArgs,
],
{
cwd: APP_ROOT,
cwd: REPO_ROOT,
env,
stdio: 'inherit',
},

View file

@ -17,9 +17,7 @@ export const NATIVE_TARGETS = Object.freeze(
SUPPORTED_TARGETS.map((t) => {
const deps = resolveTargetDeps(t);
const clipboardTarget = deps.find((d) => d.id === 'clipboard-target')?.resolvedName;
const koffiNativeFile = deps.find((d) => d.id === 'koffi')?.nativeFileRelatives?.[0];
const koffiTriplet = koffiNativeFile?.match(/koffi\/([^/]+)\/koffi\.node$/)?.[1] ?? null;
return [t, { clipboardPackage: clipboardTarget, koffiTriplet }];
return [t, { clipboardPackage: clipboardTarget }];
}),
),
);
@ -161,16 +159,19 @@ async function collectPackageFiles({
packageName,
packageRoot,
includeNativeFiles,
includeEntryJs = true,
nativeFileRelatives = [],
}) {
const packageJsonPath = join(packageRoot, 'package.json');
const packageJson = await readJson(packageJsonPath);
const selected = new Set([packageJsonPath]);
const entry = resolvePackageEntry(packageRoot, packageJson);
if (entry !== null) {
selected.add(entry);
await addRuntimeDependencyFiles(packageRoot, entry, selected);
if (includeEntryJs) {
const entry = resolvePackageEntry(packageRoot, packageJson);
if (entry !== null) {
selected.add(entry);
await addRuntimeDependencyFiles(packageRoot, entry, selected);
}
}
for (const nativeFileRelative of nativeFileRelatives) {
@ -250,6 +251,7 @@ export async function collectNativeAssets({ appRoot, target }) {
packageName: dep.resolvedName,
packageRoot,
includeNativeFiles: dep.collect === 'native-files',
includeEntryJs: dep.collect !== 'native-file-only',
nativeFileRelatives: dep.nativeFileRelatives,
});
const result = await packageManifestEntries({

View file

@ -23,7 +23,7 @@ const optionalRuntimeRequires = new Set([
'utf-8-validate',
]);
const optionalRelativeRuntimeRequires = new Set(['./crypto/build/Release/sshcrypto.node']);
const handledNativeRuntimeRequires = new Set(['koffi']);
const handledNativeRuntimeRequires = new Set();
function isAllowedSpecifier(specifier) {
if (builtins.has(specifier) || specifier.startsWith('node:')) return true;

View file

@ -27,13 +27,16 @@ const clipboardSubpackageByTarget = Object.freeze({
'win32-x64': '@mariozechner/clipboard-win32-x64-msvc',
});
const koffiTripletByTarget = Object.freeze({
'darwin-arm64': 'darwin_arm64',
'darwin-x64': 'darwin_x64',
'linux-arm64': 'linux_arm64',
'linux-x64': 'linux_x64',
'win32-arm64': 'win32_arm64',
'win32-x64': 'win32_x64',
// pi-tui ships platform-specific native helpers (no Linux build):
// - darwin: Shift-modifier detection for Terminal.app Shift+Enter
// - win32: enable ENABLE_VIRTUAL_TERMINAL_INPUT so Shift+Tab is distinguishable
const piTuiNativeFileByTarget = Object.freeze({
'darwin-arm64': ['native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node'],
'darwin-x64': ['native/darwin/prebuilds/darwin-x64/darwin-modifiers.node'],
'linux-arm64': [],
'linux-x64': [],
'win32-arm64': ['native/win32/prebuilds/win32-arm64/win32-console-mode.node'],
'win32-x64': ['native/win32/prebuilds/win32-x64/win32-console-mode.node'],
});
export function isSupportedTarget(target) {
@ -45,13 +48,15 @@ export function isSupportedTarget(target) {
* @property {string} id stable internal id used for parent refs
* @property {(target: string) => string} name
* npm package name (may depend on target)
* @property {'js-only'|'native-files'|'js-and-native-file'|'virtual'} collect
* @property {'js-only'|'native-files'|'js-and-native-file'|'native-file-only'|'virtual'} collect
* @property {string|null} parent
* id of another registered dep this nests under (for pnpm),
* or null for top-level (resolvable from app root)
* @property {(target: string) => string[]} [nativeFileRelatives]
* explicit list of .node files relative to package root
* (used by 'js-and-native-file'; native-files mode auto-scans *.node)
* (used by 'js-and-native-file' and 'native-file-only';
* native-files mode auto-scans *.node). 'native-file-only' collects
* package.json + these .node files but skips the package entry JS.
*/
/** @type {readonly NativeDepDescriptor[]} */
@ -70,18 +75,14 @@ export const nativeDeps = Object.freeze([
},
{
id: 'pi-tui',
name: () => '@earendil-works/pi-tui',
// pi-tui is bundled into main.cjs at build time — we don't collect it as
// a native dep, only register it so koffi can declare it as parent.
collect: 'virtual',
name: () => '@moonshot-ai/pi-tui',
// pi-tui's JS is bundled into main.cjs, so only the platform-specific
// native helper (.node under native/) ships alongside the binary — its
// dist/ JS is intentionally NOT collected (it stays in the bundle). This
// keeps the SEA native-asset payload small. Linux has no native helper.
collect: 'native-file-only',
parent: null,
},
{
id: 'koffi',
name: () => 'koffi',
collect: 'js-and-native-file',
parent: 'pi-tui',
nativeFileRelatives: (target) => [`build/koffi/${koffiTripletByTarget[target]}/koffi.node`],
nativeFileRelatives: (target) => piTuiNativeFileByTarget[target] ?? [],
},
]);

View file

@ -46,13 +46,18 @@ const GOAL_PREFIX = /^\/goal(\s|$)/;
* Parses a headless prompt into a goal-create request, or `undefined` when the
* prompt is not a `/goal` create command (so the caller runs it as a normal
* prompt). Non-create goal subcommands are not supported headless and fall
* through to normal prompt handling.
* through to normal prompt handling. Malformed create commands throw instead of
* falling through, so validation errors are reported before anything is sent to
* the model.
*/
export function parseHeadlessGoalCreate(prompt: string): HeadlessGoalCreate | undefined {
const trimmed = prompt.trim();
if (!GOAL_PREFIX.test(trimmed)) return undefined;
const args = trimmed.replace(/^\/goal/, '').trim();
const parsed = parseGoalCommand(args);
if (parsed.kind === 'error') {
throw new Error(parsed.message);
}
if (parsed.kind !== 'create') return undefined;
return { objective: parsed.objective, replace: parsed.replace };
}

View file

@ -44,7 +44,11 @@ import { createKimiCodeHostIdentity } from './version';
*
* Used to bound shutdown so a wedged cleanup step can't keep a completed
* headless run alive, without silently swallowing a cleanup that fails fast. The
* timer is unref'd so it never keeps the loop alive on its own.
* timer stays ref'd so a cleanup step that suspends on an unref'd handle (e.g.
* telemetry's retry backoff when the network is blocked) can't drain the event
* loop and exit 0 before the rejection propagates the timer keeps the loop
* alive until it fires, then gives the rejection a chance to surface. A wedged
* cleanup is still bounded by `timeoutMs`, so this can't hang the run forever.
*/
async function raceWithTimeout(promise: Promise<void>, timeoutMs: number): Promise<void> {
let timedOut = false;
@ -61,7 +65,6 @@ async function raceWithTimeout(promise: Promise<void>, timeoutMs: number): Promi
timedOut = true;
resolve();
}, timeoutMs);
timer.unref?.();
});
try {
await Promise.race([guarded, timedOutSignal]);
@ -179,6 +182,7 @@ export async function runPrompt(
version,
uiMode: PROMPT_UI_MODE,
model: telemetryModel,
sessionId: session.id,
});
setCrashPhase('runtime');
@ -230,7 +234,7 @@ async function runHeadlessGoal(
try {
// The objective is sent as the normal prompt; goal continuation keeps the
// turn alive until a terminal state is reached.
await runPromptTurn(session, goal.objective, outputFormat, stdout, stderr);
await runPromptTurn(session, goal.objective, outputFormat, stdout, stderr, true);
} finally {
unsubscribeGoalEvents();
const snapshot = completedSnapshot ?? (await session.getGoal()).goal;
@ -338,6 +342,7 @@ async function resolvePromptSession(
model,
permission: 'auto',
additionalDirs: opts.addDirs?.length ? opts.addDirs : undefined,
drainAgentTasksOnStop: true,
});
installHeadlessHandlers(session);
return {
@ -427,9 +432,11 @@ function runPromptTurn(
outputFormat: PromptOutputFormat,
stdout: PromptOutput,
stderr: PromptOutput,
waitForGoalTerminal = false,
): Promise<void> {
let activeTurnId: number | undefined;
let activeAgentId: string | undefined;
let latestStartedTurnId: number | undefined;
const outputWriter =
outputFormat === 'stream-json'
? new PromptJsonWriter(stdout)
@ -464,6 +471,18 @@ function runPromptTurn(
}
activeTurnId = event.turnId;
activeAgentId = event.agentId;
latestStartedTurnId = event.turnId;
return;
}
if (
waitForGoalTerminal &&
event.type === 'goal.updated' &&
event.agentId === PROMPT_MAIN_AGENT_ID &&
activeTurnId === undefined &&
event.snapshot !== null &&
event.snapshot.status !== 'active'
) {
void finishCompletedTurn();
return;
}
if (
@ -510,7 +529,29 @@ function runPromptTurn(
return;
case 'turn.ended':
if (event.reason === 'completed') {
finish();
outputWriter.flushAssistant();
if (waitForGoalTerminal) {
const completedTurnId = event.turnId;
activeTurnId = undefined;
activeAgentId = undefined;
void (async () => {
try {
const { goal } = await session.getGoal();
if (
activeTurnId !== undefined ||
latestStartedTurnId !== completedTurnId
) {
return;
}
if (goal?.status === 'active') return;
await finishCompletedTurn();
} catch (error) {
finish(error instanceof Error ? error : new Error(String(error)));
}
})();
return;
}
void finishCompletedTurn();
return;
}
finish(new Error(formatTurnEndedFailure(event)));
@ -543,6 +584,20 @@ function runPromptTurn(
session.prompt(prompt).catch((error: unknown) => {
finish(error instanceof Error ? error : new Error(String(error)));
});
async function finishCompletedTurn(): Promise<void> {
// Flush the buffered assistant message before draining background tasks:
// in stream-json mode the final message is only emitted by finish(), so a
// long background wait would otherwise withhold the main turn's result
// until the drain settles.
outputWriter.flushAssistant();
try {
await session.waitForBackgroundTasksOnPrint();
} catch (error) {
log.warn('waitForBackgroundTasksOnPrint failed', { error });
}
finish();
}
});
}
@ -593,7 +648,9 @@ class PromptTranscriptWriter implements PromptTurnWriter {
writeToolResult(): void {}
flushAssistant(): void {}
flushAssistant(): void {
this.assistantWriter.finish();
}
discardAssistant(): void {}

View file

@ -1,4 +1,4 @@
import { execSync } from 'node:child_process';
import { execSync, spawnSync } from 'node:child_process';
import { homedir } from 'node:os';
import { join } from 'node:path';
@ -25,6 +25,7 @@ import { KimiTUI } from '#/tui/index';
import { currentTheme, getColorPalette } from '#/tui/theme';
import { combineStartupNotice } from '#/tui/utils/startup';
import { toTerminalHyperlink } from '#/utils/terminal-hyperlink';
import { restoreTerminalModes } from '#/utils/terminal-restore';
import type { CLIOptions } from './options';
import { createCliTelemetryBootstrap, initializeCliTelemetry } from './telemetry';
@ -61,6 +62,7 @@ export async function runShell(
const harness = createKimiHarness({
homeDir: telemetryBootstrap.homeDir,
identity: createKimiCodeHostIdentity(version),
skillDirs: opts.skillsDirs,
telemetry: telemetryClient,
onOAuthRefresh: (outcome) => {
if (outcome.success) {
@ -133,6 +135,59 @@ export async function runShell(
trackLifecycleForSession(tui.getCurrentSessionId(), event, properties);
};
let savedStty: string | undefined;
try {
// stty operates on the terminal behind stdin, so stdin must be the TTY —
// piping /dev/null (ignore) makes stty fail with "not a tty".
const saved = execSync('stty -g', {
encoding: 'utf8',
stdio: ['inherit', 'pipe', 'ignore'],
});
savedStty = typeof saved === 'string' ? saved.trim() : undefined;
execSync('stty -ixon', { stdio: ['inherit', 'ignore', 'ignore'] });
} catch {
/* ignore */
}
const restoreStty = (): void => {
if (savedStty === undefined) return;
const args = savedStty.split(/\s+/).filter((arg) => arg.length > 0);
if (args.length === 0) return;
spawnSync('stty', args, { stdio: ['inherit', 'ignore', 'ignore'] });
};
// If we crash without going through KimiTUI.stop(), the terminal is left in
// raw mode with a hidden cursor and XON/XOFF flow control disabled. Restore
// both before exiting so the user's shell is usable afterwards.
const emergencyExit = (exitCode: number): void => {
restoreTerminalModes();
restoreStty();
process.exit(exitCode);
};
const onUncaughtException = (error: unknown): void => {
try {
log.error('uncaughtException, restoring terminal and exiting', { error: String(error) });
} catch {
/* ignore */
}
emergencyExit(1);
};
const onUnhandledRejection = (reason: unknown): void => {
try {
log.error('unhandledRejection, restoring terminal and exiting', { reason: String(reason) });
} catch {
/* ignore */
}
emergencyExit(1);
};
process.on('uncaughtException', onUncaughtException);
process.on('unhandledRejection', onUnhandledRejection);
// Remove the crash handlers once the TUI exits cleanly so repeated runShell()
// calls in the same process (e.g. tests) don't accumulate process listeners.
const removeCrashHandlers = (): void => {
process.off('uncaughtException', onUncaughtException);
process.off('unhandledRejection', onUnhandledRejection);
};
tui.onExit = async (exitCode = 0) => {
const sessionId = tui.getCurrentSessionId();
const hasContent = tui.hasSessionContent();
@ -151,13 +206,10 @@ export async function runShell(
if (hints.length > 0) {
process.stderr.write(`\n${hints.join('\n')}\n`);
}
removeCrashHandlers();
restoreStty();
process.exit(exitCode);
};
try {
execSync('stty -ixon', { stdio: 'ignore' });
} catch {
/* ignore */
}
try {
const initStartedAt = Date.now();
await tui.start();
@ -171,6 +223,7 @@ export async function runShell(
mcp_ms: mcpMs,
});
} catch (error) {
removeCrashHandlers();
setCrashPhase('shutdown');
trackLifecycle('exit', { duration_ms: Date.now() - startedAt });
await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS });

View file

@ -59,6 +59,10 @@ export interface EnsureDaemonOptions {
allowRemoteShutdown?: boolean;
/** Keep the PTY `/api/v1/terminals/*` routes enabled on a non-loopback bind. */
allowRemoteTerminals?: boolean;
/** Disable bearer-token auth on every route (`--dangerous-bypass-auth`). */
dangerousBypassAuth?: boolean;
/** Keep the daemon alive instead of idle-killing it (`--keep-alive`). */
keepAlive?: boolean;
/** Extra `Host` header values to allow through the DNS-rebinding check. */
allowedHosts?: readonly string[];
/** Idle-shutdown grace in ms for the spawned daemon (daemon mode only). */
@ -202,6 +206,8 @@ interface SpawnDaemonChildOptions {
insecureNoTls?: boolean;
allowRemoteShutdown?: boolean;
allowRemoteTerminals?: boolean;
dangerousBypassAuth?: boolean;
keepAlive?: boolean;
allowedHosts?: readonly string[];
idleGraceMs?: number;
}
@ -235,6 +241,12 @@ export function spawnDaemonChild(options: SpawnDaemonChildOptions): ChildProcess
if (options.allowRemoteTerminals === true) {
args.push('--allow-remote-terminals');
}
if (options.dangerousBypassAuth === true) {
args.push('--dangerous-bypass-auth');
}
if (options.keepAlive === true) {
args.push('--keep-alive');
}
if (options.idleGraceMs !== undefined) {
args.push('--idle-grace-ms', String(options.idleGraceMs));
}
@ -320,6 +332,8 @@ export async function ensureDaemon(options: EnsureDaemonOptions = {}): Promise<E
insecureNoTls: options.insecureNoTls,
allowRemoteShutdown: options.allowRemoteShutdown,
allowRemoteTerminals: options.allowRemoteTerminals,
dangerousBypassAuth: options.dangerousBypassAuth,
keepAlive: options.keepAlive,
allowedHosts: options.allowedHosts,
idleGraceMs: options.idleGraceMs,
});

View file

@ -119,6 +119,11 @@ export function buildRunCommand(cmd: Command, options: { defaultOpen: boolean })
'--allowed-host <host...>',
'Extra Host header value to allow through the DNS-rebinding check. Repeat or comma-separate; a leading dot matches a domain suffix (e.g. .example.com).',
)
.option(
'--keep-alive',
'Keep the server running instead of exiting after 60s with no connected clients. Implied automatically by --host / --allowed-host, and always on in --foreground mode.',
false,
)
.option(
'--insecure-no-tls',
'Allow a non-loopback bind without a TLS-terminating reverse proxy. Defaults to true; only relevant for non-loopback binds.',
@ -134,6 +139,11 @@ export function buildRunCommand(cmd: Command, options: { defaultOpen: boolean })
'On a non-loopback bind, keep the PTY /api/v1/terminals/* routes enabled (default: disabled → 404). Remote shell is high risk.',
false,
)
.option(
'--dangerous-bypass-auth',
'Disable bearer-token auth on every REST and WebSocket route, and advertise it via /api/v1/meta so the web UI connects without a token. Only use on a trusted network or behind your own authenticating proxy.',
false,
)
.option(
'--log-level <level>',
`Server log level: ${VALID_LOG_LEVELS.join('|')}. Omit to keep logs off.`,
@ -183,13 +193,27 @@ export async function handleRunCommand(
await startServerDaemon(parsed);
return;
}
// Foreground is always keep-alive: a server attached to the operator's
// terminal must never idle-kill itself. Background daemons respect the
// derived `--keep-alive` flag.
const runOptions: ParsedServerOptions =
opts.foreground === true ? { ...parsed, keepAlive: true } : parsed;
// Resolve the persistent token once: it is printed in the ready banner and
// rides in the opened Web UI URL's `#token=` fragment (M5.5). Falls back to
// the plain origin / no token line when unavailable.
// the plain origin / no token line when unavailable. When auth is bypassed,
// the token is meaningless and is intentionally NOT shown or carried in the
// opened URL.
const writeReady = (result: { origin: string; reused?: boolean; host?: string }): void => {
const { origin } = result;
const host = result.host ?? parsed.host;
const token = deps.resolveToken?.();
// When a daemon is reused, this command's flags were NOT applied to the
// already-running server. Don't trust the requested `--dangerous-bypass-auth`
// for display/open: treat the server as token-protected so we never hide a
// token the user actually needs, nor claim bypass for a server that is
// authenticating. (Probing the running server's `/meta` would give its real
// mode; we conservatively assume non-bypass on reuse.)
const effectiveBypass = result.reused === true ? false : parsed.dangerousBypassAuth;
const token = effectiveBypass ? undefined : deps.resolveToken?.();
let output = '';
if (result.reused === true) {
// A daemon was already running, so this command's --host/--port/etc. did
@ -202,8 +226,9 @@ export async function handleRunCommand(
? formatReadyBanner(origin, host, {
token,
networkAddresses: deps.networkAddresses,
dangerousBypassAuth: effectiveBypass,
})
: formatReadyLine(origin, token);
: formatReadyLine(origin, token, effectiveBypass);
deps.stdout.write(output);
if (opts.open === true) {
deps.openUrl(token !== undefined ? buildWebUrl(origin, token) : origin);
@ -211,14 +236,14 @@ export async function handleRunCommand(
};
if (opts.foreground === true) {
const run = deps.startServerForeground ?? startServerForeground;
await run(parsed, {
await run(runOptions, {
onReady: (origin) => {
writeReady({ origin, reused: false, host: parsed.host });
},
});
return;
}
const result = await deps.startServerBackground(parsed);
const result = await deps.startServerBackground(runOptions);
writeReady(result);
}
@ -230,8 +255,30 @@ function formatReuseNotice(origin: string): string {
);
}
function formatReadyLine(origin: string, token: string | undefined): string {
return `Kimi server: ${buildOpenableUrl(origin, token)}\n`;
function formatReadyLine(
origin: string,
token: string | undefined,
dangerousBypassAuth = false,
): string {
const notice = dangerousBypassAuth
? `${formatDangerNoticeLines().join('\n')}\n`
: '';
return `${notice}Kimi server: ${buildOpenableUrl(origin, token)}\n`;
}
/**
* Red, impossible-to-miss notice emitted when `--dangerous-bypass-auth`
* disables the bearer-token gate. Shared by the full ready banner and the
* compact one-line output so the warning always shows regardless of log level.
*/
function formatDangerNoticeLines(): string[] {
const danger = (text: string): string => chalk.hex(darkColors.error)(text);
const dangerBold = (text: string): string => chalk.bold.hex(darkColors.error)(text);
return [
` ${dangerBold('⚠ DANGER: authentication is DISABLED (--dangerous-bypass-auth).')}`,
` ${danger('Anyone who can reach this port gets full access. Only continue if you understand the risk.')}`,
` ${danger(`If you are unsure, run `)}${dangerBold('kimi server kill')}${danger(' now to stop this process.')}`,
];
}
/**
@ -251,6 +298,8 @@ export async function startServerBackground(
insecureNoTls: options.insecureNoTls,
allowRemoteShutdown: options.allowRemoteShutdown,
allowRemoteTerminals: options.allowRemoteTerminals,
dangerousBypassAuth: options.dangerousBypassAuth,
keepAlive: options.keepAlive,
allowedHosts: options.allowedHosts,
idleGraceMs: options.idleGraceMs,
});
@ -296,14 +345,18 @@ async function runServerInProcess(
let running: RunningServer | undefined;
let stopping = false;
const idle = mode.daemon
? createIdleShutdownHandler({
graceMs: options.idleGraceMs,
onIdle: () => {
void shutdown('idle');
},
})
: undefined;
// Idle auto-shutdown is only for the on-demand personal daemon. It is skipped
// in foreground mode (`mode.daemon` is false) and whenever `--keep-alive` is
// set — explicitly, or implied by `--host` / `--allowed-host`.
const idle =
mode.daemon && !options.keepAlive
? createIdleShutdownHandler({
graceMs: options.idleGraceMs,
onIdle: () => {
void shutdown('idle');
},
})
: undefined;
async function shutdown(reason: string): Promise<void> {
if (stopping) return;
@ -330,6 +383,7 @@ async function runServerInProcess(
insecureNoTls: options.insecureNoTls,
allowRemoteShutdown: options.allowRemoteShutdown,
allowRemoteTerminals: options.allowRemoteTerminals,
dangerousBypassAuth: options.dangerousBypassAuth,
allowedHosts: options.allowedHosts,
webAssetsDir: serverWebAssetsDir(),
coreProcessOptions: {
@ -356,7 +410,9 @@ async function runServerInProcess(
});
const readyFields = mode.daemon
? { address: running.address, idleGraceMs: options.idleGraceMs }
? options.keepAlive
? { address: running.address, idleShutdown: 'disabled' as const }
: { address: running.address, idleGraceMs: options.idleGraceMs }
: { address: running.address };
running.logger.info(readyFields, mode.daemon ? 'daemon ready' : 'server ready');
@ -421,6 +477,8 @@ interface FormatReadyBannerOptions {
token?: string;
/** Non-loopback interface addresses to list for a wildcard bind. */
networkAddresses?: NetworkAddress[];
/** When true, render a red danger notice (auth is disabled). */
dangerousBypassAuth?: boolean;
}
function formatReadyBanner(
@ -452,6 +510,12 @@ function formatReadyBanner(
'',
];
if (opts.dangerousBypassAuth === true) {
// Red, impossible-to-miss notice: the bearer-token gate is off, so anyone
// who can reach this port gets full session / filesystem / shell access.
lines.push(...formatDangerNoticeLines(), '');
}
// Access links.
for (const { label: text, url: href } of accessUrlLines(
host,

View file

@ -50,8 +50,18 @@ export interface ParsedServerOptions {
allowRemoteShutdown: boolean;
/** Allow PTY `/api/v1/terminals/*` routes on a non-loopback bind. */
allowRemoteTerminals: boolean;
/** Disable bearer-token auth on every route (`--dangerous-bypass-auth`). */
dangerousBypassAuth: boolean;
/** Extra `Host` header values to allow through the DNS-rebinding check. */
allowedHosts: readonly string[];
/**
* Keep the server running instead of idle-killing it after 60s with no
* connected clients (`--keep-alive`). Also implied automatically by a
* non-default bind (`--host`) or a proxy/tunnel setup (`--allowed-host`),
* and always on in `--foreground` mode. Only the daemon mode consults this
* foreground never idle-kills regardless.
*/
keepAlive: boolean;
/** Internal: run as an idle-exiting background daemon instead of foreground. */
daemon: boolean;
/** Internal: idle-shutdown grace in ms (daemon mode only). */
@ -69,8 +79,12 @@ export interface ServerCliOptions {
allowRemoteShutdown?: boolean;
/** Allow remote terminals on a non-loopback bind (`--allow-remote-terminals`). */
allowRemoteTerminals?: boolean;
/** Disable bearer-token auth on every route (`--dangerous-bypass-auth`). */
dangerousBypassAuth?: boolean;
/** Extra `Host` header values to allow (`--allowed-host`). */
allowedHost?: string[];
/** Keep the server running instead of idle-killing it (`--keep-alive`). */
keepAlive?: boolean;
/** Internal flag set by the daemon spawner (`kimi web`). */
daemon?: boolean;
/** Internal flag set by the daemon spawner / tests. */
@ -78,15 +92,24 @@ export interface ServerCliOptions {
}
export function parseServerOptions(opts: ServerCliOptions): ParsedServerOptions {
const host = parseHost(opts.host);
const allowedHosts = parseAllowedHostArgs(opts.allowedHost);
// `--keep-alive` is explicit, but also implied by a non-default bind
// (`--host`) or a proxy/tunnel setup (`--allowed-host`). Foreground mode is
// forced keep-alive later in `handleRunCommand`.
const keepAlive =
opts.keepAlive === true || host !== DEFAULT_SERVER_HOST || allowedHosts.length > 0;
return {
host: parseHost(opts.host),
host,
port: parsePort(opts.port, '--port', DEFAULT_SERVER_PORT),
logLevel: parseLogLevel(opts.logLevel ?? DEFAULT_FOREGROUND_LOG_LEVEL),
debugEndpoints: opts.debugEndpoints === true,
insecureNoTls: opts.insecureNoTls !== false,
allowRemoteShutdown: opts.allowRemoteShutdown === true,
allowRemoteTerminals: opts.allowRemoteTerminals === true,
allowedHosts: parseAllowedHostArgs(opts.allowedHost),
dangerousBypassAuth: opts.dangerousBypassAuth === true,
allowedHosts,
keepAlive,
daemon: opts.daemon === true,
idleGraceMs: parseIdleGraceMs(opts.idleGraceMs),
};

View file

@ -32,6 +32,7 @@ export interface InitializeCliTelemetryOptions {
readonly version: string;
readonly uiMode: string;
readonly model?: string;
readonly sessionId?: string;
}
export function createCliTelemetryBootstrap(): CliTelemetryBootstrap {
@ -54,6 +55,7 @@ export function initializeCliTelemetry(options: InitializeCliTelemetryOptions):
version: options.version,
uiMode: options.uiMode,
model: options.model ?? options.config.defaultModel,
sessionId: options.sessionId,
getAccessToken: async () =>
(await options.harness.auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME)) ?? null,
});

View file

@ -488,7 +488,14 @@ export async function installUpdate(
): Promise<void> {
const { cmd, args } = spawnForSource(source, version, platform);
await new Promise<void>((resolve, reject) => {
const child = spawn(cmd, [...args], { stdio: 'inherit' });
// Windows package managers (npm/pnpm/yarn) are .cmd shims. Since the
// CVE-2024-27980 fix, Node throws EINVAL when spawning a .cmd/.bat without
// a shell, so run through the shell on win32. The version is a validated
// semver and the package name is a constant, so args are shell-safe.
const child = spawn(cmd, [...args], {
stdio: 'inherit',
shell: platform === 'win32' ? true : undefined,
});
child.once('error', reject);
child.once('exit', (code, signal) => {
if (code === 0) {
@ -596,7 +603,15 @@ async function startBackgroundInstall(
});
};
const child = spawn(cmd, [...args], { detached: true, stdio: 'ignore' });
const child = spawn(cmd, [...args], {
detached: true,
stdio: 'ignore',
shell: platform === 'win32' ? true : undefined,
// On Windows a detached child gets its own console window; with shell:true
// that window would flash during a passive background update. Hide it so
// the silent updater stays silent.
windowsHide: platform === 'win32' ? true : undefined,
});
child.once('error', () => { finish(false); });
child.once('exit', (code) => { finish(code === 0); });
child.unref();

View file

@ -172,6 +172,16 @@ export function main(): void {
}
})
.catch(async (error: unknown) => {
// Set the failure exit code synchronously, before any `await`. The
// terminal `process.exit(1)` below is our intended exit, but it sits
// behind `await logStartupFailure(...)`; by the time we reach that
// await, the failed run's `finally` cleanup has already torn down its
// ref'd handles (sockets, timers, background tasks). If the event loop
// drains during the await, Node exits on its own with the DEFAULT code
// 0 and `process.exit(1)` never runs — headless (`kimi -p`) failures
// would then exit 0 nondeterministically. Setting `process.exitCode`
// up front makes that drain-exit report failure too.
process.exitCode = 1;
const operation = opts.prompt !== undefined ? 'run prompt' : 'start shell';
await logStartupFailure(operation, error);
process.stderr.write(

View file

@ -11,7 +11,7 @@
* This file implements the ask, progress, and result phases. `beginMigration`
* drives the real runMigration flow (injectable for tests).
*/
import { Container, matchesKey, Key, truncateToWidth, type Focusable } from '@earendil-works/pi-tui';
import { Container, matchesKey, Key, truncateToWidth, type Focusable } from '@moonshot-ai/pi-tui';
import chalk from 'chalk';
import type { ColorPalette } from '#/tui/theme/colors';

View file

@ -1,6 +1,8 @@
import { existsSync } from 'node:fs';
import { createRequire } from 'node:module';
import { join } from 'node:path';
import { loadNativePackage } from './native-require';
import { getNativePackageRoot } from './native-assets';
type ModuleLoad = (request: string, parent: unknown, isMain: boolean) => unknown;
@ -10,7 +12,16 @@ interface ModuleWithLoad {
const nodeRequire = createRequire(import.meta.url);
let installed = false;
let loadingNativePackage = false;
// pi-tui loads its platform-specific native helpers via an absolute-path
// require() computed from import.meta.url / process.execPath
// (see pi-tui dist/terminal.js and dist/native-modifiers.js). In a SEA binary
// those .node files live in the native-asset cache, so redirect any absolute
// require of a pi-tui native helper to the cached copy.
//
// Path shape: native/<darwin|win32>/prebuilds/<arch>/<file>.node — note the
// two path segments after "prebuilds", so ".+" (not "[^/]+") is required.
const PI_TUI_NATIVE_PATTERN = /native[\\/](?:win32|darwin)[\\/]prebuilds[\\/].+\.node$/;
export function installNativeModuleHook(): void {
if (installed) return;
@ -26,13 +37,18 @@ export function installNativeModuleHook(): void {
parent: unknown,
isMain: boolean,
): unknown {
if (request === 'koffi' && !loadingNativePackage) {
loadingNativePackage = true;
try {
const pkg = loadNativePackage<unknown>('koffi');
if (pkg !== null) return pkg;
} finally {
loadingNativePackage = false;
if (
typeof request === 'string' &&
PI_TUI_NATIVE_PATTERN.test(request) &&
!existsSync(request)
) {
const pkgRoot = getNativePackageRoot('@moonshot-ai/pi-tui');
if (pkgRoot !== null) {
const match = request.match(PI_TUI_NATIVE_PATTERN);
if (match !== null) {
const redirected = join(pkgRoot, match[0]);
return originalLoad.call(this, redirected, parent, isMain);
}
}
}
return originalLoad.call(this, request, parent, isMain);

View file

@ -1,6 +1,38 @@
import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';
import { getEmbeddedNativeAssetManifest, getNativePackageRoot } from './native-assets';
const smokePackages = ['@mariozechner/clipboard', 'koffi'];
const smokePackages = ['@mariozechner/clipboard', '@moonshot-ai/pi-tui'];
// Verify pi-tui's native helper can actually be loaded through the module hook.
// pi-tui computes native helper paths from process.execPath and require()s them;
// those paths do not exist next to the SEA binary, so this only succeeds when
// installNativeModuleHook() redirects the require into the native-asset cache.
function smokePiTuiNativeLoad(): void {
const platform = process.platform;
const arch = process.arch;
let rel: string | undefined;
if (platform === 'darwin' && (arch === 'x64' || arch === 'arm64')) {
rel = join('native', 'darwin', 'prebuilds', `darwin-${arch}`, 'darwin-modifiers.node');
} else if (platform === 'win32' && (arch === 'x64' || arch === 'arm64')) {
rel = join('native', 'win32', 'prebuilds', `win32-${arch}`, 'win32-console-mode.node');
}
if (rel === undefined) return; // Linux: no native helper, nothing to load.
const req = createRequire(import.meta.url);
const bogusPath = join(dirname(process.execPath), rel);
const helper = req(bogusPath) as {
isModifierPressed?: unknown;
enableVirtualTerminalInput?: unknown;
};
const ok =
typeof helper.isModifierPressed === 'function' ||
typeof helper.enableVirtualTerminalInput === 'function';
if (!ok) {
throw new Error(`pi-tui native helper loaded but exports are unexpected: ${rel}`);
}
}
export function runNativeAssetSmokeIfRequested(): boolean {
if (process.env['KIMI_CODE_NATIVE_ASSET_SMOKE'] !== '1') return false;
@ -16,6 +48,7 @@ export function runNativeAssetSmokeIfRequested(): boolean {
throw new Error(`Native package is not available: ${packageName}`);
}
}
smokePiTuiNativeLoad();
process.stdout.write(`Native asset smoke passed: ${manifest.target}\n`);
process.exit(0);
} catch (error) {

View file

@ -1,4 +1,4 @@
import type { AutocompleteItem } from '@earendil-works/pi-tui';
import type { AutocompleteItem } from '@moonshot-ai/pi-tui';
/**
* A completable token (subcommand or flag) for a slash command's argument

View file

@ -1,10 +1,10 @@
import type {
ExperimentalFeatureState,
FlagId,
ModelAlias,
PermissionMode,
Session,
ThinkingEffort,
import {
effectiveModelAlias,
type ExperimentalFeatureState,
type ModelAlias,
type PermissionMode,
type Session,
type ThinkingEffort,
} from '@moonshot-ai/kimi-code-sdk';
import { EditorSelectorComponent } from '../components/dialogs/editor-selector';
@ -19,7 +19,7 @@ import { PermissionSelectorComponent } from '../components/dialogs/permission-se
import { SettingsSelectorComponent, type SettingsSelection } from '../components/dialogs/settings-selector';
import { ThemeSelectorComponent } from '../components/dialogs/theme-selector';
import { UpdatePreferenceSelectorComponent } from '../components/dialogs/update-preference-selector';
import { saveTuiConfig } from '../config';
import { DEFAULT_TUI_CONFIG, saveTuiConfig, type TuiConfig } from '../config';
import type { ThemeName } from '#/tui/theme';
import { currentTheme, isBuiltInTheme, lightColors, loadCustomThemeMerged } from '#/tui/theme';
import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui';
@ -35,6 +35,16 @@ import type { SlashCommandHost } from './dispatch';
const MODEL_PICKER_REFRESH_TIMEOUT_MS = 2_000;
function currentTuiConfig(host: SlashCommandHost): TuiConfig {
return {
theme: host.state.appState.theme,
editorCommand: host.state.appState.editorCommand,
disablePasteBurst: host.state.appState.disablePasteBurst ?? DEFAULT_TUI_CONFIG.disablePasteBurst,
notifications: host.state.appState.notifications,
upgrade: host.state.appState.upgrade,
};
}
export async function handlePlanCommand(host: SlashCommandHost, args: string): Promise<void> {
const session = host.session;
if (session === undefined) {
@ -97,7 +107,7 @@ export async function handleYoloCommand(host: SlashCommandHost, args: string): P
}
await session.setPermission('yolo');
host.setAppState({ permissionMode: 'yolo' });
host.showNotice('YOLO mode: ON', 'Workspace tools auto-approved.');
host.showNotice('YOLO mode: ON', 'AI auto-approves safe actions, asks for approval on risky ones.');
return;
}
@ -120,7 +130,7 @@ export async function handleYoloCommand(host: SlashCommandHost, args: string): P
} else {
await session.setPermission('yolo');
host.setAppState({ permissionMode: 'yolo' });
host.showNotice('YOLO mode: ON', 'Workspace tools auto-approved.');
host.showNotice('YOLO mode: ON', 'AI auto-approves safe actions, asks for approval on risky ones.');
}
}
@ -141,7 +151,7 @@ export async function handleAutoCommand(host: SlashCommandHost, args: string): P
}
await session.setPermission('auto');
host.setAppState({ permissionMode: 'auto' });
host.showNotice('Auto mode: ON', 'Tools auto-approved. Agent will not ask questions.');
host.showNotice('Auto mode: ON', 'Run all actions automatically, including risky ones.');
return;
}
@ -164,7 +174,7 @@ export async function handleAutoCommand(host: SlashCommandHost, args: string): P
} else {
await session.setPermission('auto');
host.setAppState({ permissionMode: 'auto' });
host.showNotice('Auto mode: ON', 'Tools auto-approved. Agent will not ask questions.');
host.showNotice('Auto mode: ON', 'Run all actions automatically, including risky ones.');
}
}
@ -224,10 +234,11 @@ export async function handleEffortCommand(host: SlashCommandHost, args: string):
host.showError('No model selected. Run /model to select one first.');
return;
}
const segments = segmentsFor(model);
const effective = effectiveModelAlias(model);
const segments = segmentsFor(effective);
const arg = args.trim().toLowerCase();
if (arg.length === 0) {
showEffortPicker(host, model, segments);
showEffortPicker(host, effective, segments);
return;
}
if (!segments.includes(arg)) {
@ -327,10 +338,8 @@ async function applyEditorChoice(host: SlashCommandHost, value: string): Promise
const editorCommand = value.length > 0 ? value : null;
try {
await saveTuiConfig({
theme: host.state.appState.theme,
...currentTuiConfig(host),
editorCommand,
notifications: host.state.appState.notifications,
upgrade: host.state.appState.upgrade,
});
} catch (error) {
host.showStatus(
@ -420,7 +429,11 @@ async function performModelSwitch(
host.track('model_switch', { model: alias });
}
if (effort !== prevEffort) {
host.track('thinking_toggle', { effort });
host.track('thinking_toggle', {
enabled: effort !== 'off',
effort,
from: prevEffort,
});
}
}
@ -508,10 +521,8 @@ async function applyThemeChoice(host: SlashCommandHost, theme: ThemeName): Promi
try {
await saveTuiConfig({
...currentTuiConfig(host),
theme,
editorCommand: host.state.appState.editorCommand,
notifications: host.state.appState.notifications,
upgrade: host.state.appState.upgrade,
});
} catch (error) {
host.showStatus(
@ -584,7 +595,7 @@ export async function applyExperimentalFeatureChanges(
return;
}
const experimental: Partial<Record<FlagId, boolean>> = {};
const experimental: Record<string, boolean> = {};
for (const change of changes) {
experimental[change.id] = change.enabled;
}
@ -651,9 +662,7 @@ export async function applyUpdatePreferenceChoice(
const upgrade = { autoInstall };
try {
await saveTuiConfig({
theme: host.state.appState.theme,
editorCommand: host.state.appState.editorCommand,
notifications: host.state.appState.notifications,
...currentTuiConfig(host as unknown as SlashCommandHost),
upgrade,
});
} catch (error) {

View file

@ -1,4 +1,4 @@
import type { Component, Focusable } from '@earendil-works/pi-tui';
import type { Component, Focusable } from '@moonshot-ai/pi-tui';
import type { DeviceAuthorization } from '@moonshot-ai/kimi-code-oauth';
import type { KimiHarness, Session } from '@moonshot-ai/kimi-code-sdk';

View file

@ -372,10 +372,19 @@ async function startGoalWithPermission(
choice: GoalStartPermissionChoice,
options: GoalStartOptions,
): Promise<void> {
if (choice !== host.state.appState.permissionMode && (choice === 'auto' || choice === 'yolo')) {
const previousMode = host.state.appState.permissionMode;
const switched =
choice !== previousMode && (choice === 'auto' || choice === 'yolo');
if (switched) {
if (!(await setPermissionForGoal(host, choice))) return;
}
await startGoal(host, parsed, options);
const started = await startGoal(host, parsed, options);
// The permission switch only exists to run this goal. If creation fails
// (e.g. a goal already exists and `replace` was not given), restore the
// previous mode so the session is not left more permissive than before.
if (!started && switched) {
await setPermissionForGoal(host, previousMode);
}
}
async function setPermissionForGoal(host: GoalCommandHost, mode: PermissionMode): Promise<boolean> {

View file

@ -9,6 +9,7 @@ import {
FEEDBACK_ISSUE_URL,
FEEDBACK_STATUS_CANCELLED,
FEEDBACK_STATUS_FALLBACK,
FEEDBACK_STATUS_NETWORK_ERROR,
FEEDBACK_STATUS_NOT_SIGNED_IN,
FEEDBACK_STATUS_SUBMITTING,
FEEDBACK_STATUS_SUCCESS,
@ -58,29 +59,43 @@ export async function handleFeedbackCommand(host: SlashCommandHost): Promise<voi
const version = withFeedbackVersionPrefix(host.state.appState.version);
const spinner = host.showLoginProgressSpinner(FEEDBACK_STATUS_SUBMITTING);
const res = await host.harness.auth.submitFeedback({
content: input.value,
sessionId: host.state.appState.sessionId,
version,
os: `${osType()} ${osRelease()}`,
model: host.state.appState.model.length > 0 ? host.state.appState.model : null,
});
// Guarantee the spinner's underlying setInterval is always cleared, even when
// submitFeedback or submitFeedbackWithAttachments throws — otherwise the
// interval (and its per-frame requestRender) leaks for the rest of the session.
let stopped = false;
const stopSpinner = (opts: { ok: boolean; label: string }): void => {
if (stopped) return;
stopped = true;
spinner.stop(opts);
};
try {
const res = await host.harness.auth.submitFeedback({
content: input.value,
sessionId: host.state.appState.sessionId,
version,
os: `${osType()} ${osRelease()}`,
model: host.state.appState.model.length > 0 ? host.state.appState.model : null,
});
if (res.kind !== 'ok') {
spinner.stop({ ok: false, label: res.message });
fallback(FEEDBACK_STATUS_FALLBACK);
return;
}
if (res.kind !== 'ok') {
stopSpinner({ ok: false, label: res.message });
fallback(FEEDBACK_STATUS_FALLBACK);
return;
}
// Stage 3: prepare and upload each requested attachment independently.
const attachmentFailed = await submitFeedbackWithAttachments(host, res.feedbackId, level);
// Stage 3: prepare and upload each requested attachment independently.
const attachmentFailed = await submitFeedbackWithAttachments(host, res.feedbackId, level);
spinner.stop({ ok: true, label: FEEDBACK_STATUS_SUCCESS });
host.showStatus(feedbackSessionLine(host.state.appState.sessionId));
host.showStatus(feedbackIdLine(res.feedbackId));
host.track(FEEDBACK_TELEMETRY_EVENT);
if (attachmentFailed) {
host.showStatus(FEEDBACK_STATUS_UPLOAD_FAILED);
stopSpinner({ ok: true, label: FEEDBACK_STATUS_SUCCESS });
host.showStatus(feedbackSessionLine(host.state.appState.sessionId));
host.showStatus(feedbackIdLine(res.feedbackId));
host.track(FEEDBACK_TELEMETRY_EVENT);
if (attachmentFailed) {
host.showStatus(FEEDBACK_STATUS_UPLOAD_FAILED);
}
} catch (error) {
stopSpinner({ ok: false, label: FEEDBACK_STATUS_NETWORK_ERROR });
throw error;
}
}
@ -198,5 +213,5 @@ async function loadManagedUsageReport(host: SlashCommandHost): Promise<ManagedUs
if (res.kind === 'error') {
return { error: res.message };
}
return { usage: { summary: res.summary, limits: res.limits } };
return { usage: { summary: res.summary, limits: res.limits, extraUsage: res.extraUsage } };
}

View file

@ -22,6 +22,7 @@ import { UsagePanelComponent } from '../components/messages/usage-panel';
import { formatErrorMessage } from '../utils/event-payload';
import { formatPluginSourceLabel, isOfficialPluginSource } from '../utils/plugin-source-label';
import { loadPluginMarketplace } from '#/utils/plugin-marketplace';
import { openUrl } from '#/utils/open-url';
import type { SlashCommandHost } from './dispatch';
interface ShowPluginsPickerOptions {
@ -411,6 +412,12 @@ async function handlePluginsPanelSelection(
isOfficialPluginSource(selection.source),
);
return;
case 'open-url':
host.restoreEditor();
openUrl(selection.url);
host.showStatus(`Opening the ${selection.label} page in your browser…`, 'success');
host.showStatus(`If it did not open, visit ${selection.url}`);
return;
}
}

View file

@ -2,7 +2,7 @@ import { readdirSync, statSync } from 'node:fs';
import { homedir } from 'node:os';
import { basename, dirname, join, relative, resolve } from 'pathe';
import type { AutocompleteItem } from '@earendil-works/pi-tui';
import type { AutocompleteItem } from '@moonshot-ai/pi-tui';
import { completeLeadingArg, type ArgCompletionSpec } from './complete-args';
import type { KimiSlashCommand, SlashCommandAvailability } from './types';
@ -136,15 +136,15 @@ export const BUILTIN_SLASH_COMMANDS = [
{
name: 'yolo',
aliases: ['yes'],
description: 'Toggle auto-approve mode',
priority: 100,
description: 'Toggle YOLO mode: AI auto-approves safe actions, asks for approval on risky ones.',
priority: 101,
availability: 'always',
},
{
name: 'auto',
aliases: [],
description: 'Toggle auto permission mode',
priority: 100,
description: 'Toggle Auto mode: run all actions automatically, including risky ones.',
priority: 99,
availability: 'always',
},
{

View file

@ -45,9 +45,11 @@ export async function applyReloadedTuiConfig(
host.refreshTerminalThemeTracking();
host.setAppState({
editorCommand: config.editorCommand,
disablePasteBurst: config.disablePasteBurst,
notifications: config.notifications,
upgrade: config.upgrade,
});
host.state.editor.setDisablePasteBurst(config.disablePasteBurst);
}
function applyRuntimeConfig(host: SlashCommandHost, config: KimiConfig): void {

View file

@ -1,4 +1,4 @@
import type { AutocompleteItem, SlashCommand } from '@earendil-works/pi-tui';
import type { AutocompleteItem, SlashCommand } from '@moonshot-ai/pi-tui';
import type { FlagId } from '@moonshot-ai/kimi-code-sdk';
export type SlashCommandAvailability = 'always' | 'idle-only';

View file

@ -1,4 +1,4 @@
import type { Component } from '@earendil-works/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import type { ContextMessage } from '@moonshot-ai/kimi-code-sdk';
import { isKimiError } from '@moonshot-ai/kimi-code-sdk';

View file

@ -1,5 +1,5 @@
import type { Component } from '@earendil-works/pi-tui';
import { visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import { visibleWidth, wrapTextWithAnsi } from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';
import type { BannerState } from '#/tui/types';

View file

@ -6,8 +6,8 @@
* active palette so theme switches take effect on the next render.
*/
import type { Component } from '@earendil-works/pi-tui';
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import { truncateToWidth, visibleWidth } from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';

View file

@ -6,9 +6,10 @@
* Line 2: context: XX.X% (tokens/max)
*/
import type { Component } from '@earendil-works/pi-tui';
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import { truncateToWidth, visibleWidth } from '@moonshot-ai/pi-tui';
import chalk from 'chalk';
import { effectiveModelAlias } from '@moonshot-ai/kimi-code-sdk';
import { ALL_TIPS, type ToolbarTip } from '#/tui/constant/tips';
import { isRainbowDancing, renderDanceFooterModel } from '#/tui/easter-eggs/dance';
@ -132,7 +133,8 @@ function formatBadgeElapsed(ms: number): string {
function modelDisplayName(state: AppState): string {
const model = state.availableModels[state.model];
return model?.displayName ?? model?.model ?? state.model;
const effective = model === undefined ? undefined : effectiveModelAlias(model);
return effective?.displayName ?? effective?.model ?? state.model;
}
function shortenCwd(path: string): string {
@ -263,7 +265,8 @@ export class FooterComponent implements Component {
const model = modelDisplayName(state);
if (model) {
const effort = state.thinkingEffort;
const currentModel = state.availableModels[state.model];
const rawCurrentModel = state.availableModels[state.model];
const currentModel = rawCurrentModel === undefined ? undefined : effectiveModelAlias(rawCurrentModel);
// Only effort-capable models (those declaring support_efforts) show the
// concrete effort; legacy boolean models keep the plain "thinking" suffix.
const hasEfforts = (currentModel?.supportEfforts?.length ?? 0) > 0;
@ -380,6 +383,13 @@ export class FooterComponent implements Component {
}
}
dispose(): void {
if (this.goalTimer !== null) {
clearInterval(this.goalTimer);
this.goalTimer = null;
}
}
private goalWallClockMs(goal: AppState['goal']): number | undefined {
if (goal === null || goal === undefined) return undefined;
if (goal.status !== 'active') return goal.wallClockMs;

View file

@ -9,8 +9,8 @@
* the edge and adding them would just churn the diff renderer.
*/
import { Container } from '@earendil-works/pi-tui';
import type { Component } from '@earendil-works/pi-tui';
import { Container } from '@moonshot-ai/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import { isRenderCacheEnabled } from '#/tui/utils/render-cache';

View file

@ -1,5 +1,5 @@
import { Text, visibleWidth } from '@earendil-works/pi-tui';
import type { TUI } from '@earendil-works/pi-tui';
import { Text, visibleWidth } from '@moonshot-ai/pi-tui';
import type { TUI } from '@moonshot-ai/pi-tui';
import {
BRAILLE_SPINNER_FRAMES,
@ -59,6 +59,10 @@ export class MoonLoader extends Text {
}
}
dispose(): void {
this.stop();
}
setLabel(label: string): void {
this.label = label;
this.updateDisplay();

View file

@ -9,8 +9,8 @@
* is issued.
*/
import type { Component } from '@earendil-works/pi-tui';
import { truncateToWidth } from '@earendil-works/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import { truncateToWidth } from '@moonshot-ai/pi-tui';
import chalk from 'chalk';
import { currentTheme } from '#/tui/theme';

View file

@ -3,10 +3,12 @@
* Renders a round-bordered box with the logo, session, model, and version.
*/
import type { Component } from '@earendil-works/pi-tui';
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import { truncateToWidth, visibleWidth } from '@moonshot-ai/pi-tui';
import chalk from 'chalk';
import { effectiveModelAlias } from '@moonshot-ai/kimi-code-sdk';
import { isRainbowDancing, renderDanceWelcomeHeader } from '#/tui/easter-eggs/dance';
import type { AppState } from '#/tui/types';
import { currentTheme } from '#/tui/theme';
@ -25,6 +27,7 @@ export class WelcomeComponent implements Component {
const primary = (s: string): string => chalk.hex(currentTheme.palette.primary)(s);
const isLoggedOut = !this.state.model;
const activeModel = this.state.availableModels[this.state.model];
const effectiveActiveModel = activeModel === undefined ? undefined : effectiveModelAlias(activeModel);
if (safeWidth < 24) {
const title = chalk.bold.hex(currentTheme.palette.primary)('Welcome to Kimi Code!');
@ -33,7 +36,7 @@ export class WelcomeComponent implements Component {
: chalk.hex(currentTheme.palette.textDim)('Send /help for help information.');
const model = isLoggedOut
? chalk.hex(currentTheme.palette.warning)('not set, run /login or /provider')
: (activeModel?.displayName ?? activeModel?.model ?? this.state.model);
: (effectiveActiveModel?.displayName ?? effectiveActiveModel?.model ?? this.state.model);
return ['', title, prompt, `Model: ${model}`].map((line) =>
truncateToWidth(line, safeWidth, '…'),
);
@ -71,7 +74,7 @@ export class WelcomeComponent implements Component {
const modelValue = isLoggedOut
? chalk.hex(currentTheme.palette.warning)('not set, run /login or /provider')
: (activeModel?.displayName ?? activeModel?.model ?? this.state.model);
: (effectiveActiveModel?.displayName ?? effectiveActiveModel?.model ?? this.state.model);
const infoLines = [
labelStyle('Directory: ') + this.state.workDir,

View file

@ -6,7 +6,7 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';

View file

@ -14,7 +14,7 @@ import {
truncateToWidth,
visibleWidth,
wrapTextWithAnsi,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';
import { highlightLines, langFromPath } from '#/tui/components/media/code-highlight';
import { renderDiffLinesClustered } from '#/tui/components/media/diff-preview';

View file

@ -24,10 +24,10 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { highlightLines, langFromPath } from '#/tui/components/media/code-highlight';
import { renderDiffLines } from '#/tui/components/media/diff-preview';
import { renderDiffLinesClustered } from '#/tui/components/media/diff-preview';
import type { DiffDisplayBlock, FileContentDisplayBlock } from '#/tui/reverse-rpc/types';
import { currentTheme } from '#/tui/theme';
import { printableChar } from '#/tui/utils/printable-key';
@ -218,17 +218,19 @@ function buildBody(block: ApprovalPreviewBlock): BuiltBody {
}
function buildDiffBody(block: DiffDisplayBlock): BuiltBody {
// renderDiffLines emits a `+N -M path` header on its first line followed
// by every changed line. We pull the header out into the viewer chrome so
// the body is purely scrollable diff content; this also means we don't
// double-render the path.
const rendered = renderDiffLines(
// renderDiffLinesClustered emits a `+N -M path` header on its first line
// followed by every changed line plus surrounding context. We pull the
// header out into the viewer chrome so the body is purely scrollable diff
// content; this also means we don't double-render the path.
const rendered = renderDiffLinesClustered(
block.old_text,
block.new_text,
block.path,
false,
block.old_start ?? 1,
block.new_start ?? 1,
{
contextLines: 3,
oldStart: block.old_start ?? 1,
newStart: block.new_start ?? 1,
},
);
const [header = '', ...rest] = rendered;
return { lines: rest, title: stripLeadingSpace(header) };

View file

@ -15,7 +15,7 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols';
import { currentTheme, type ColorToken } from '#/tui/theme';
import { printableChar } from '#/tui/utils/printable-key';

View file

@ -13,8 +13,8 @@
* reads the same "work in progress" signal across the UI.
*/
import { Container, Text, Spacer } from '@earendil-works/pi-tui';
import type { TUI } from '@earendil-works/pi-tui';
import { Container, Text, Spacer } from '@moonshot-ai/pi-tui';
import type { TUI } from '@moonshot-ai/pi-tui';
import { STATUS_BULLET } from '#/tui/constant/symbols';
import { currentTheme } from '#/tui/theme';
@ -24,6 +24,7 @@ const BLINK_INTERVAL = 500;
export class CompactionComponent extends Container {
private readonly ui: TUI | undefined;
private readonly headerText: Text;
private instructionText: Text | undefined;
private readonly instruction: string | undefined;
private readonly tip: string | undefined;
private blinkOn = true;
@ -32,6 +33,9 @@ export class CompactionComponent extends Container {
private canceled = false;
private tokensBefore: number | undefined;
private tokensAfter: number | undefined;
private summary: string | undefined;
private summaryText: Text | undefined;
private expanded = false;
constructor(ui?: TUI, instruction?: string | undefined, tip?: string) {
super();
@ -51,32 +55,48 @@ export class CompactionComponent extends Container {
private addInstructionChild(): void {
if (this.instruction !== undefined) {
this.addChild(new Text(currentTheme.dim(` ${this.instruction}`), 0, 0));
this.instructionText = new Text(currentTheme.dim(` ${this.instruction}`), 0, 0);
this.addChild(this.instructionText);
}
}
private removeInstructionChild(): void {
if (this.instructionText === undefined) return;
const index = this.children.indexOf(this.instructionText);
if (index !== -1) {
this.children.splice(index, 1);
}
this.instructionText = undefined;
}
override invalidate(): void {
// Repaint the header with the active palette (it caches ANSI codes).
this.headerText.setText(this.buildHeader());
// Rebuild instruction line with fresh theme colours.
if (this.instruction !== undefined) {
// Remove the last child if it is the instruction line (it is always
// added after headerText and Spacer).
if (this.children.length > 2) {
this.children.pop();
}
this.addInstructionChild();
// Rebuild instruction and summary text with fresh theme colours, preserving
// header → instruction → summary child order.
const expanded = this.expanded;
this.removeInstructionChild();
if (expanded) {
this.removeSummaryChild();
}
this.addInstructionChild();
if (expanded) {
this.addSummaryChild();
}
super.invalidate();
}
markDone(tokensBefore?: number, tokensAfter?: number): void {
markDone(tokensBefore?: number, tokensAfter?: number, summary?: string): void {
if (this.done || this.canceled) return;
this.done = true;
this.tokensBefore = tokensBefore;
this.tokensAfter = tokensAfter;
this.summary = summary;
this.stopBlink();
this.headerText.setText(this.buildHeader());
if (this.expanded) {
this.addSummaryChild();
}
this.ui?.requestRender();
}
@ -88,6 +108,39 @@ export class CompactionComponent extends Container {
this.ui?.requestRender();
}
setExpanded(expanded: boolean): void {
if (this.expanded === expanded) return;
this.expanded = expanded;
if (expanded) {
this.addSummaryChild();
} else {
this.removeSummaryChild();
}
this.headerText.setText(this.buildHeader());
this.ui?.requestRender();
}
private addSummaryChild(): void {
if (this.summaryText !== undefined || this.summary === undefined || this.summary.length === 0) {
return;
}
const indentedSummary = this.summary
.split('\n')
.map((line) => ` ${line}`)
.join('\n');
this.summaryText = new Text(currentTheme.dim(indentedSummary), 0, 0);
this.addChild(this.summaryText);
}
private removeSummaryChild(): void {
if (this.summaryText === undefined) return;
const index = this.children.indexOf(this.summaryText);
if (index !== -1) {
this.children.splice(index, 1);
}
this.summaryText = undefined;
}
dispose(): void {
this.stopBlink();
}
@ -100,7 +153,11 @@ export class CompactionComponent extends Container {
this.tokensBefore !== undefined && this.tokensAfter !== undefined
? currentTheme.dim(` (${String(this.tokensBefore)}${String(this.tokensAfter)} tokens)`)
: '';
return `${bullet}${label}${detail}`;
const shortcutHint =
this.summary !== undefined && this.summary.length > 0
? currentTheme.dim(` (Ctrl-O to ${this.expanded ? 'hide' : 'show'} compaction summary)`)
: '';
return `${bullet}${label}${detail}${shortcutHint}`;
}
if (this.canceled) {
const bullet = currentTheme.fg('warning', STATUS_BULLET);

View file

@ -17,7 +17,7 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';

View file

@ -4,7 +4,7 @@ import {
matchesKey,
truncateToWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import type { ThinkingEffort } from '@moonshot-ai/kimi-code-sdk';

View file

@ -5,7 +5,7 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import type { ExperimentalFeatureState } from '@moonshot-ai/kimi-code-sdk';
import { SELECT_POINTER } from '#/tui/constant/symbols';

View file

@ -19,7 +19,7 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';
export type FeedbackInputDialogResult =

View file

@ -6,7 +6,7 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import chalk from 'chalk';
import { SELECT_POINTER } from '#/tui/constant/symbols';

View file

@ -15,7 +15,7 @@ import {
decodeKittyPrintable,
type Focusable,
truncateToWidth,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';
export interface KeyboardShortcut {
@ -33,7 +33,7 @@ export interface HelpPanelCommand {
export const DEFAULT_KEYBOARD_SHORTCUTS: readonly KeyboardShortcut[] = [
{ keys: 'Shift-Tab', description: 'Toggle plan mode' },
{ keys: 'Ctrl-G', description: 'Edit in external editor ($VISUAL / $EDITOR)' },
{ keys: 'Ctrl-O', description: 'Toggle tool output expansion' },
{ keys: 'Ctrl-O', description: 'Toggle tool output / compaction summary expansion' },
{ keys: 'Ctrl-T', description: 'Expand / collapse the todo list (when truncated)' },
{ keys: 'Ctrl-S', description: 'Steer — inject a follow-up during streaming' },
{ keys: 'Shift-Enter / Ctrl-J', description: 'Insert newline' },

View file

@ -1,4 +1,4 @@
import type { ModelAlias, ThinkingEffort } from '@moonshot-ai/kimi-code-sdk';
import { effectiveModelAlias, type ModelAlias, type ThinkingEffort } from '@moonshot-ai/kimi-code-sdk';
import {
Container,
Key,
@ -6,7 +6,7 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { DEFAULT_OAUTH_PROVIDER_NAME, PRODUCT_NAME } from '#/constant/app';
import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols';
@ -37,7 +37,8 @@ export interface ModelSelection {
}
export function modelDisplayName(alias: string, model: ModelAlias | undefined): string {
return model?.displayName ?? model?.model ?? alias;
const effective = model === undefined ? undefined : effectiveModelAlias(model);
return effective?.displayName ?? effective?.model ?? alias;
}
export function providerDisplayName(provider: string): string {
@ -49,10 +50,13 @@ export function providerDisplayName(provider: string): string {
export function createModelChoiceOptions(
models: Record<string, ModelAlias>,
): readonly ChoiceOption[] {
return Object.entries(models).map(([alias, cfg]) => ({
value: alias,
label: `${modelDisplayName(alias, cfg)} (${providerDisplayName(cfg.provider)})`,
}));
return Object.entries(models).map(([alias, cfg]) => {
const effective = effectiveModelAlias(cfg);
return {
value: alias,
label: `${modelDisplayName(alias, effective)} (${providerDisplayName(effective.provider)})`,
};
});
}
export interface ModelSelectorOptions {
@ -78,9 +82,10 @@ export interface ModelSelectorOptions {
function createModelChoices(models: Record<string, ModelAlias>): readonly ModelChoice[] {
return Object.entries(models).map(([alias, cfg]) => {
const name = modelDisplayName(alias, cfg);
const provider = providerDisplayName(cfg.provider);
return { alias, model: cfg, name, provider, label: `${name} (${provider})` };
const effective = effectiveModelAlias(cfg);
const name = modelDisplayName(alias, effective);
const provider = providerDisplayName(effective.provider);
return { alias, model: effective, name, provider, label: `${name} (${provider})` };
});
}
@ -374,12 +379,6 @@ export class ModelSelectorComponent extends Container implements Focusable {
const segments = segmentsFor(choice.model);
const active = this.effectiveEffort(choice);
const rendered = segments.map((effort) => segment(effortLabel(effort), effort === active));
// Always-on models (including effort-capable ones) additionally surface an
// unsupported Off so it's explicit that thinking cannot be disabled — same
// shape as the legacy always-on control.
if (availability === 'always-on') {
rendered.push(unavailable('Off'));
}
return ` ${rendered.join(' ')}`;
}
}

View file

@ -6,20 +6,17 @@ const PERMISSION_OPTIONS: readonly ChoiceOption[] = [
{
value: 'manual',
label: 'Manual',
description:
'Ask before commands, edits, and other risky actions. Read/search tools run directly; session approval rules are respected.',
description: 'Approve every action yourself.',
},
{
value: 'auto',
label: 'Auto',
description:
'Run fully non-interactively. Tool actions are approved automatically, and agent questions are skipped so it can decide on its own.',
description: 'Run all actions automatically, including risky ones.',
},
{
value: 'yolo',
label: 'YOLO',
description:
'Automatically approve tool actions and plan transitions. The agent can still ask you explicit questions when your input is needed.',
description: 'AI decides which actions need your approval.',
},
];

View file

@ -6,7 +6,7 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import type { PluginInfo, PluginMcpServerInfo, PluginSummary } from '@moonshot-ai/kimi-code-sdk';
import chalk from 'chalk';
@ -28,6 +28,27 @@ const INSTALL_TRUST_EXIT = 'exit';
const INSTALL_TRUST_TRUST = 'trust';
const ELLIPSIS = '…';
// Hardcoded Web Bridge promotion: a built-in entry that always leads the
// Official tab, even when the marketplace catalog is unavailable. Selecting it
// opens the install page in the browser rather than installing from a source,
// because Web Bridge is a browser extension + daemon, not a plugin package.
const WEB_BRIDGE_URL = 'https://www.kimi.com/features/webbridge';
const WEB_BRIDGE_ENTRY: PluginMarketplaceEntry = {
id: 'kimi-webbridge',
displayName: 'Kimi WebBridge',
source: WEB_BRIDGE_URL,
tier: 'official',
homepage: WEB_BRIDGE_URL,
description: 'Control your real browser from Kimi Code — navigate, click, type, and screenshot',
};
// Only the hardcoded pinned row should open the WebBridge install page. Match
// by reference (not id) so a catalog entry on another tab that happens to
// reuse the same id still installs normally instead of being hijacked.
function isPinnedWebBridgeEntry(entry: PluginMarketplaceEntry): boolean {
return entry === WEB_BRIDGE_ENTRY;
}
interface PluginsOverviewItem {
readonly value: string;
readonly kind: 'plugin' | 'action';
@ -304,7 +325,8 @@ export type PluginsPanelSelection =
| { readonly kind: 'details'; readonly id: string }
| { readonly kind: 'reload' }
| { readonly kind: 'install'; readonly entry: PluginMarketplaceEntry }
| { readonly kind: 'install-source'; readonly source: string };
| { readonly kind: 'install-source'; readonly source: string }
| { readonly kind: 'open-url'; readonly url: string; readonly label: string };
export interface PluginsPanelOptions {
readonly installed: readonly PluginSummary[];
@ -402,7 +424,19 @@ export class PluginsPanelComponent extends Container implements Focusable {
}
private get officialEntries(): readonly PluginMarketplaceEntry[] {
return this.marketplaceEntries.filter((entry) => entry.tier === 'official');
// The hardcoded Web Bridge entry always leads the Official tab, even when
// the catalog is loading or unreachable. Dedupe by id so a catalog that
// also lists it does not render a second row.
return [WEB_BRIDGE_ENTRY, ...this.officialCatalogEntries];
}
private get officialCatalogEntries(): readonly PluginMarketplaceEntry[] {
// Dedupe by id (not reference): if the official catalog also lists
// kimi-webbridge, the pinned row already represents it, so suppress the
// catalog copy to avoid a duplicate row on the Official tab.
return this.marketplaceEntries.filter(
(entry) => entry.tier === 'official' && entry.id !== WEB_BRIDGE_ENTRY.id,
);
}
private get thirdPartyEntries(): readonly PluginMarketplaceEntry[] {
@ -516,6 +550,10 @@ export class PluginsPanelComponent extends Container implements Focusable {
if (matchesKey(data, Key.enter)) {
const entry = entries[this.selectedIndex];
if (entry === undefined) return;
if (isPinnedWebBridgeEntry(entry)) {
this.opts.onSelect({ kind: 'open-url', url: WEB_BRIDGE_URL, label: entry.displayName });
return;
}
this.opts.onSelect({ kind: 'install', entry });
}
}
@ -622,6 +660,7 @@ export class PluginsPanelComponent extends Container implements Focusable {
lines: string[],
width: number,
entries: readonly PluginMarketplaceEntry[],
indexOffset = 0,
): void {
const colors = currentTheme.palette;
if (this.market.status === 'loading' || this.market.status === 'idle') {
@ -637,7 +676,7 @@ export class PluginsPanelComponent extends Container implements Focusable {
lines.push(chalk.hex(colors.textMuted)(' No plugins found.'));
} else {
for (let i = 0; i < entries.length; i++) {
lines.push(...this.renderMarketplaceRow(entries[i]!, i, width));
lines.push(...this.renderMarketplaceRow(entries[i]!, i + indexOffset, width));
}
}
const installedCount = entries.filter((e) => this.opts.installedIds.has(e.id)).length;
@ -649,7 +688,11 @@ export class PluginsPanelComponent extends Container implements Focusable {
}
private renderOfficial(lines: string[], width: number): void {
this.renderMarketplaceTab(lines, width, this.officialEntries);
// Web Bridge is pinned above the catalog and stays visible while the
// catalog loads or errors, since it's built into the TUI rather than
// fetched. Catalog rows shift down by one index to match.
lines.push(...this.renderMarketplaceRow(WEB_BRIDGE_ENTRY, 0, width));
this.renderMarketplaceTab(lines, width, this.officialCatalogEntries, 1);
}
private renderThirdParty(lines: string[], width: number): void {
@ -662,7 +705,9 @@ export class PluginsPanelComponent extends Container implements Focusable {
const pointer = selected ? SELECT_POINTER : ' ';
const labelStyle = selected ? chalk.hex(colors.primary).bold : chalk.hex(colors.text);
const prefix = chalk.hex(selected ? colors.primary : colors.textDim)(` ${pointer} `);
const status = marketplaceEntryStatus(entry, this.installedVersions);
const status = isPinnedWebBridgeEntry(entry)
? 'open in browser'
: marketplaceEntryStatus(entry, this.installedVersions);
const line =
prefix + labelStyle(entry.displayName) + ' ' + marketplaceStatusStyle(status, colors)(status);
const descWidth = Math.max(1, width - 4);

View file

@ -42,7 +42,7 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { DEFAULT_OAUTH_PROVIDER_NAME } from '#/constant/app';
import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols';

View file

@ -15,7 +15,7 @@ import {
truncateToWidth,
visibleWidth,
wrapTextWithAnsi,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';
import type {

View file

@ -9,7 +9,7 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { formatSessionLabel } from '#/migration/index';
import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols';
import { currentTheme } from '#/tui/theme';

View file

@ -5,7 +5,7 @@ import {
visibleWidth,
type Component,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { SELECT_POINTER } from '#/tui/constant/symbols';
import { currentTheme } from '#/tui/theme';

View file

@ -20,7 +20,7 @@ import {
matchesKey,
truncateToWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';
import { renderTabStrip } from '#/tui/utils/tab-strip';

View file

@ -17,7 +17,7 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi-code-sdk';
import { currentTheme } from '#/tui/theme';

View file

@ -21,7 +21,7 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi-code-sdk';
import { SELECT_POINTER } from '@/tui/constant/symbols';

View file

@ -5,7 +5,7 @@ import {
truncateToWidth,
visibleWidth,
type Focusable,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { SELECT_POINTER } from '#/tui/constant/symbols';
import { currentTheme } from '#/tui/theme';

View file

@ -11,12 +11,12 @@ import {
visibleWidth,
type SelectItem,
type TUI,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';
import { createEditorTheme } from '#/tui/theme/pi-tui-theme';
import { printableChar } from '#/tui/utils/printable-key';
import { isInsideTmux } from '#/tui/utils/terminal-notification';
import { extractAtPrefix } from './file-mention-provider';
import { WrappingSelectList } from './wrapping-select-list';
@ -114,10 +114,8 @@ function stripSgr(s: string): string {
return s.replace(ANSI_SGR, '');
}
function getNewlineInput(data: string): string | undefined {
if (data === '\n' || data === '\u001B\r' || data === '\u001B[13;2~') return data;
if (matchesKey(data, Key.ctrl('j'))) return '\n';
return undefined;
interface CustomEditorOptions {
disablePasteBurst?: boolean;
}
export class CustomEditor extends Editor {
@ -137,7 +135,6 @@ export class CustomEditor extends Editor {
/** Return `true` to consume Ctrl+T (the todo list had overflow to toggle); return `false`/`undefined` to fall through to the editor default. */
public onToggleTodoExpand?: () => boolean;
public onUndo?: () => void;
public onInsertNewline?: () => void;
public onTextPaste?: () => void;
/**
* Called when is pressed in an empty editor. Return `true` to consume
@ -166,19 +163,20 @@ export class CustomEditor extends Editor {
private consumingPaste = false;
private consumeBuffer = '';
private argumentHints: ReadonlyMap<string, string> = new Map();
private autocompleteWasShowing = false;
setArgumentHints(hints: ReadonlyMap<string, string>): void {
this.argumentHints = hints;
}
constructor(tui: TUI) {
constructor(tui: TUI, options: CustomEditorOptions = {}) {
// paddingX: 4 reserves column 0 for the left vertical border (│),
// column 1 as a single space between border and prompt, column 2 for
// the `>` prompt token, and column 3 as the space between prompt and
// content. The right side mirrors with 3 padding columns and the right
// border at the last column.
const theme = createEditorTheme();
super(tui, theme, { paddingX: 4 });
super(tui, theme, { paddingX: 4, disablePasteBurst: options.disablePasteBurst });
// pi-tui keeps `createAutocompleteList` private; shadow it with an
// instance property so slash command menus render descriptions wrapped
@ -211,6 +209,16 @@ export class CustomEditor extends Editor {
};
}
override setDisablePasteBurst(disabled: boolean): void {
super.setDisablePasteBurst(disabled);
}
public setInputMode(mode: 'prompt' | 'bash'): void {
if (this.inputMode === mode) return;
this.inputMode = mode;
this.onInputModeChange?.(mode);
}
private expandPasteMarkerAtCursor(): boolean {
const { line, col } = this.getCursor();
const lines = this.getLines();
@ -250,7 +258,38 @@ export class CustomEditor extends Editor {
(this as unknown as AutocompleteInternals).cancelAutocomplete();
}
// Force a full re-render when the autocomplete dropdown closes, so the editor
// snaps back to the bottom instead of sitting where the taller dropdown left it.
// Only worthwhile when the session content already overflows one screen; below
// that a full clear + home would pull the editor to the top and leave a blank
// tail. Always skipped inside tmux, whose own reflow handles the shrink.
private requestFullRenderOnAutocompleteClose(): void {
if (isInsideTmux()) return;
const { columns, rows } = this.tui.terminal;
// Redraw when content fills or overflows the viewport. An exact fill (==
// rows) is safe to clear (no blank tail) and still needs the redraw: the
// differential renderer keeps the old viewport offset after a shrink.
if (this.tui.render(columns).length < rows) return;
this.tui.requestRender(true);
}
// Detect an autocomplete open→close edge from a render frame and force a full
// re-render. Running from render() (not handleInput) also catches asynchronous
// closes — e.g. Backspace deleting the leading `/`, where pi-tui only cancels
// the menu once the provider re-query resolves. The render request is deferred
// to a microtask so the overflow probe inside the helper does not re-enter
// render() synchronously.
private trackAutocompleteCloseForFullRender(): void {
const showing = this.isShowingAutocomplete();
const closed = this.autocompleteWasShowing && !showing;
this.autocompleteWasShowing = showing;
if (closed) {
queueMicrotask(() => this.requestFullRenderOnAutocompleteClose());
}
}
override render(width: number): string[] {
this.trackAutocompleteCloseForFullRender();
const lines = super.render(width);
if (lines.length < 3) return lines;
const firstContentIdx = 1;
@ -430,13 +469,6 @@ export class CustomEditor extends Editor {
return;
}
const newlineInput = getNewlineInput(normalized);
if (newlineInput !== undefined) {
this.onInsertNewline?.();
super.handleInput(newlineInput);
return;
}
if (matchesKey(normalized, Key.up)) {
if (this.getText().length === 0 && this.onUpArrowEmpty) {
if (this.onUpArrowEmpty()) return;
@ -600,10 +632,7 @@ function goalCommandPathRanges(
return ranges;
}
function readTokenRange(
visible: string,
start: number,
): { start: number; end: number } | null {
function readTokenRange(visible: string, start: number): { start: number; end: number } | null {
let tokenStart = start;
while (tokenStart < visible.length && isTokenSpace(visible[tokenStart])) tokenStart++;
if (tokenStart >= visible.length) return null;
@ -751,8 +780,7 @@ export function wrapWithSideBorders(
const firstCh = line[0];
const lastCh = line.at(-1);
const head = firstCh === ' ' ? paint('│') : (firstCh ?? '');
const tail =
line.length > 1 && lastCh === ' ' ? paint('│') : (lastCh ?? '');
const tail = line.length > 1 && lastCh === ' ' ? paint('│') : (lastCh ?? '');
if (line.length === 1) return head;
return head + line.slice(1, -1) + tail;
});

View file

@ -1,4 +1,4 @@
import { readdirSync, statSync } from 'node:fs';
import { accessSync, constants as fsConstants, readdirSync, statSync } from 'node:fs';
import { basename, join, resolve } from 'node:path';
import {
@ -8,7 +8,7 @@ import {
type AutocompleteProvider,
type AutocompleteSuggestions,
type SlashCommand,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
const PATH_DELIMITERS = new Set([' ', '\t', '"', "'", '=']);
const MAX_FALLBACK_SCAN = 2000;
@ -27,13 +27,13 @@ interface FsMentionCandidate {
/**
* Kimi wrapper around pi-tui's combined autocomplete provider.
*
* File / folder mention behavior uses pi-tui's fd-backed provider when fd is
* available and only the current working directory is involved. While managed fd
* is downloading, when it is unavailable, or when the session has additional
* roots, a small filesystem fallback keeps `@` file and folder completion usable
* across every root. Ordinary path completion is still handled by pi-tui's
* readdir-backed path completer. This wrapper also keeps Kimi-specific
* slash-command guards.
* File / folder mention behavior uses pi-tui's fd-backed provider whenever fd
* is available, fanning out across the working directory and any additional
* roots so `@` completion pushes the query down to fd instead of enumerating
* every file. A small filesystem fallback is used only while managed fd is
* downloading, when it is unavailable, or if fd fails to spawn. Ordinary path
* completion is still handled by pi-tui's readdir-backed path completer. This
* wrapper also keeps Kimi-specific slash-command guards.
*/
export class FileMentionProvider implements AutocompleteProvider {
private readonly inner: CombinedAutocompleteProvider;
@ -56,7 +56,7 @@ export class FileMentionProvider implements AutocompleteProvider {
expanded.push({ ...cmd, name: alias });
}
}
this.inner = new CombinedAutocompleteProvider(expanded, workDir, fdPath);
this.inner = new CombinedAutocompleteProvider(expanded, workDir, fdPath, this.additionalDirs);
}
async getSuggestions(
@ -75,7 +75,11 @@ export class FileMentionProvider implements AutocompleteProvider {
// runs, so the file list never opens.
const atPrefix = extractAtPrefix(textBeforeCursor);
if (atPrefix !== null) {
if (this.fdPath === null || this.additionalDirs.length > 0) {
// fd backs `@` completion across every root (cwd + additional dirs). Fall
// back to the filesystem scanner when fd is unavailable, not executable
// (e.g. the managed binary was removed or lost execute permission), or if
// spawning it fails below. A genuine fd no-match still returns null.
if (this.fdPath === null || !isExecutableFd(this.fdPath)) {
return getFsMentionSuggestions(
this.workDir,
this.additionalDirs,
@ -227,6 +231,21 @@ export function extractAtPrefix(text: string): string | null {
return text.slice(tokenStart);
}
function isExecutableFd(fdPath: string): boolean {
// Bare command names (for example "fd" discovered on the system PATH) are
// trusted: spawn resolves them through PATH. Only absolute/relative paths are
// probed, which is how the managed fd is referenced and which can go stale.
if (!fdPath.includes('/') && !fdPath.includes('\\')) {
return true;
}
try {
accessSync(fdPath, fsConstants.X_OK);
return true;
} catch {
return false;
}
}
/**
* Match the `/add-dir` directory completer, which skips every entry whose name
* starts with `.` (see registry.ts). pi-tui's path completer sets `label` to

View file

@ -6,7 +6,7 @@ import {
type SelectItem,
type SelectListLayoutOptions,
type SelectListTheme,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
// Mirror pi-tui's private select-list layout constants
// (dist/components/select-list.js); keep in sync when bumping pi-tui.

View file

@ -156,6 +156,8 @@ export interface ClusteredDiffOptions {
readonly maxLines?: number;
readonly isIncomplete?: boolean;
readonly expandKeyHint?: string;
readonly oldStart?: number;
readonly newStart?: number;
}
interface Cluster {
@ -239,7 +241,13 @@ export function renderDiffLinesClustered(
const s = makeDiffStyles();
const contextLines = opts.contextLines ?? 3;
const maxLines = opts.maxLines;
const diffLines = computeDiffLines(oldText, newText, 1, 1, opts.isIncomplete ?? false);
const diffLines = computeDiffLines(
oldText,
newText,
opts.oldStart ?? 1,
opts.newStart ?? 1,
opts.isIncomplete ?? false,
);
const { clusters, changedCount, addedCount, removedCount } = buildClusters(
diffLines,
contextLines,

View file

@ -12,7 +12,7 @@
* the viewport; pi-tui handles proportional scaling internally.
*/
import { Container, Image, Text, type ImageTheme, getCapabilities } from '@earendil-works/pi-tui';
import { Container, Image, Text, type ImageTheme, getCapabilities } from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';
import type { ImageAttachment } from '#/tui/utils/image-attachment-store';

View file

@ -15,8 +15,8 @@
* - Ungrouping is not implemented. Once formed, a group stays grouped.
*/
import type { TUI } from '@earendil-works/pi-tui';
import { Container, Spacer, Text } from '@earendil-works/pi-tui';
import type { TUI } from '@moonshot-ai/pi-tui';
import { Container, Spacer, Text } from '@moonshot-ai/pi-tui';
import { STATUS_BULLET } from '#/tui/constant/symbols';
import { currentTheme } from '#/tui/theme';

View file

@ -1,4 +1,4 @@
import { truncateToWidth, visibleWidth, type Component } from '@earendil-works/pi-tui';
import { truncateToWidth, visibleWidth, type Component } from '@moonshot-ai/pi-tui';
import chalk from 'chalk';
import {

View file

@ -5,7 +5,7 @@
* to align after the bullet.
*/
import { Container, Markdown, truncateToWidth, visibleWidth, type Component } from '@earendil-works/pi-tui';
import { Container, Markdown, truncateToWidth, visibleWidth, type Component } from '@moonshot-ai/pi-tui';
import { MESSAGE_INDENT } from '#/tui/constant/rendering';
import { STATUS_BULLET } from '#/tui/constant/symbols';

View file

@ -1,4 +1,4 @@
import { Text, truncateToWidth, type Component } from '@earendil-works/pi-tui';
import { Text, truncateToWidth, type Component } from '@moonshot-ai/pi-tui';
import { MESSAGE_INDENT } from '#/tui/constant/rendering';
import { FAILURE_MARK, STATUS_BULLET } from '#/tui/constant/symbols';

View file

@ -1,5 +1,5 @@
import type { Component } from '@earendil-works/pi-tui';
import { Spacer, Text, visibleWidth } from '@earendil-works/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import { Spacer, Text, visibleWidth } from '@moonshot-ai/pi-tui';
import { STATUS_BULLET } from '#/tui/constant/symbols';
import { currentTheme } from '#/tui/theme';

View file

@ -7,7 +7,7 @@
* the richer completion card (the `/goal` box), not this marker.
*/
import { truncateToWidth, type Component } from '@earendil-works/pi-tui';
import { truncateToWidth, type Component } from '@moonshot-ai/pi-tui';
import type { GoalChange } from '@moonshot-ai/kimi-code-sdk';
import { STATUS_BULLET } from '#/tui/constant/symbols';

View file

@ -19,7 +19,7 @@ import {
visibleWidth,
wrapTextWithAnsi,
type Component,
} from '@earendil-works/pi-tui';
} from '@moonshot-ai/pi-tui';
import type { GoalSnapshot, GoalStatus } from '@moonshot-ai/kimi-code-sdk';
import { MESSAGE_INDENT } from '#/tui/constant/rendering';

View file

@ -7,7 +7,7 @@
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { Markdown, truncateToWidth, visibleWidth, type Component, type MarkdownTheme } from '@earendil-works/pi-tui';
import { Markdown, truncateToWidth, visibleWidth, type Component, type MarkdownTheme } from '@moonshot-ai/pi-tui';
import chalk from 'chalk';
import { toTerminalHyperlink } from '#/utils/terminal-hyperlink';

View file

@ -11,7 +11,7 @@
* context; the TUI only consumes the `plugin_command.activated` event.
*/
import { Container, Text, Spacer } from '@earendil-works/pi-tui';
import { Container, Text, Spacer } from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';

View file

@ -20,8 +20,8 @@
* src/missing.ts · failed
*/
import type { TUI } from '@earendil-works/pi-tui';
import { Container, Spacer, Text } from '@earendil-works/pi-tui';
import type { TUI } from '@moonshot-ai/pi-tui';
import { Container, Spacer, Text } from '@moonshot-ai/pi-tui';
import { STATUS_BULLET } from '#/tui/constant/symbols';
import { currentTheme } from '#/tui/theme';

View file

@ -1,5 +1,5 @@
import type { Component } from '@earendil-works/pi-tui';
import { Container, Text } from '@earendil-works/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import { Container, Text } from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';
import type { ToolCallBlockData, ToolResultBlockData } from '#/tui/types';
@ -48,8 +48,15 @@ export class ShellExecutionComponent extends Container {
const allLines = command.split('\n');
const lines = previewLines === undefined ? allLines : allLines.slice(0, previewLines);
for (const [i, line] of lines.entries()) {
const prefix = i === 0 ? '$ ' : ' ';
this.addChild(new Text(currentTheme.dim(prefix + line), 2, 0));
// Distinguish the command (input) from the result (output): the `$`
// prompt uses the dedicated shell-mode hue, the command body uses
// `textDim`, and the result below is rendered one step dimmer in
// `textMuted` so the two stay separable without a connecting glyph.
const text =
i === 0
? currentTheme.fg('shellMode', '$ ') + currentTheme.dim(line)
: ` ${currentTheme.dim(line)}`;
this.addChild(new Text(text, 2, 0));
}
}
@ -68,24 +75,23 @@ export class ShellExecutionComponent extends Container {
maxLines: previewLines,
tail: tailOutput,
expandHint,
color: 'textMuted',
}),
);
}
}
export const shellExecutionResultRenderer: ResultRenderer = (
toolCall: ToolCallBlockData,
_toolCall: ToolCallBlockData,
result: ToolResultBlockData,
ctx,
): Component[] => [
// Result only. The command preview is owned by ToolCallComponent's
// buildCallPreview across the whole lifecycle (streaming, running, and
// done); rendering it here too would duplicate the command once the result
// lands.
new ShellExecutionComponent({
command: typeof toolCall.args['command'] === 'string' ? toolCall.args['command'] : '',
result,
expanded: ctx.expanded,
// Header truncates long bash commands to 60 chars. When the user expands
// the card with ctrl+o, reveal the full command (no line cap) so they
// can read what actually ran.
showCommand: ctx.expanded,
commandPreviewLines: undefined,
}),
];

View file

@ -1,4 +1,4 @@
import { Container, Text } from '@earendil-works/pi-tui';
import { Container, Text } from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';

View file

@ -12,7 +12,7 @@
* metadata.
*/
import { Container, Text, Spacer } from '@earendil-works/pi-tui';
import { Container, Text, Spacer } from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';
import type { SkillActivationTrigger } from '#/tui/types';

View file

@ -1,4 +1,4 @@
import { Container, Spacer, Text } from '@earendil-works/pi-tui';
import { Container, Spacer, Text } from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';
import type { ColorToken } from '#/tui/theme';

View file

@ -5,7 +5,13 @@
* separate from the TUI orchestration layer.
*/
import type { ModelAlias, PermissionMode, SessionStatus, ThinkingEffort } from '@moonshot-ai/kimi-code-sdk';
import {
effectiveModelAlias,
type ModelAlias,
type PermissionMode,
type SessionStatus,
type ThinkingEffort,
} from '@moonshot-ai/kimi-code-sdk';
import { PRODUCT_NAME } from '#/constant/app';
import { currentTheme } from '#/tui/theme';
@ -16,7 +22,11 @@ import {
safeUsageRatio,
} from '#/utils/usage/usage-format';
import { buildManagedUsageReportLines, type ManagedUsageReport } from './usage-panel';
import {
buildExtraUsageSection,
buildManagedUsageReportLines,
type ManagedUsageReport,
} from './usage-panel';
interface FieldRow {
readonly label: string;
@ -47,7 +57,8 @@ type Colorize = (text: string) => string;
function displayModelName(alias: string, models: Record<string, ModelAlias>): string {
const model = models[alias];
return model?.displayName ?? model?.model ?? alias;
const effective = model === undefined ? undefined : effectiveModelAlias(model);
return effective?.displayName ?? effective?.model ?? alias;
}
function formatModelStatus(options: StatusReportOptions): string {
@ -138,5 +149,16 @@ export function buildStatusReportLines(options: StatusReportOptions): string[] {
lines.push(...managedSection);
}
const extraSection = buildExtraUsageSection(
options.managedUsage?.extraUsage,
accent,
value,
muted,
);
if (extraSection.length > 0) {
lines.push('');
lines.push(...extraSection);
}
return lines;
}

View file

@ -1,4 +1,4 @@
import type { Component } from '@earendil-works/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';

View file

@ -1,4 +1,4 @@
import { truncateToWidth, type Component } from '@earendil-works/pi-tui';
import { truncateToWidth, type Component } from '@moonshot-ai/pi-tui';
import { STATUS_BULLET } from '#/tui/constant/symbols';
import { currentTheme } from '#/tui/theme';

View file

@ -5,7 +5,7 @@
* Supports expand/collapse via Ctrl+O (shared with tool output).
*/
import { Text, truncateToWidth, type Component, type TUI } from '@earendil-works/pi-tui';
import { Text, truncateToWidth, type Component, type TUI } from '@moonshot-ai/pi-tui';
import {
BRAILLE_SPINNER_FRAMES,

View file

@ -5,11 +5,13 @@
import { isAbsolute, relative, sep } from 'node:path';
import { Container, Spacer, Text, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
import type { Component, TUI } from '@earendil-works/pi-tui';
import { Container, Spacer, Text, truncateToWidth, visibleWidth } from '@moonshot-ai/pi-tui';
import type { Component, TUI } from '@moonshot-ai/pi-tui';
import { highlightLines, langFromPath } from '#/tui/components/media/code-highlight';
import { renderDiffLinesClustered } from '#/tui/components/media/diff-preview';
import {
BRAILLE_SPINNER_FRAMES,
BRAILLE_SPINNER_INTERVAL_MS,
COMMAND_PREVIEW_LINES,
RESULT_PREVIEW_LINES,
THINKING_PREVIEW_LINES,
@ -33,16 +35,14 @@ import { ShellExecutionComponent } from './shell-execution';
import { countNonEmptyLines, pickChip } from './tool-renderers/chip';
import { buildGoalToolHeader } from './tool-renderers/goal';
import { isGenericToolResult, pickResultRenderer } from './tool-renderers/registry';
import { TruncatedOutputComponent } from './tool-renderers/truncated';
const MAX_ARG_LENGTH = 60;
const MAX_SUB_TOOL_CALLS_SHOWN = 4;
const MAX_SINGLE_SUBAGENT_TOOL_ROWS = 4;
// Hanging indent for a sub-tool's previewed output, nested under its activity row.
const SUBAGENT_SUBTOOL_OUTPUT_INDENT = 6;
// Cap the Agent `description` in the single-subagent header so a long prompt
// cannot wrap the header onto a second row and break the card's stable height.
const MAX_SUBAGENT_DESCRIPTION_LENGTH = 60;
const APPROVED_PLAN_MARKER = '## Approved Plan:';
const STREAMING_PROGRESS_INTERVAL_MS = 1000;
const SUBAGENT_ELAPSED_INTERVAL_MS = 1000;
const PROGRESS_URL_RE = /https?:\/\/\S+/g;
const ABORTED_MARK = '⊘';
const MAX_LIVE_OUTPUT_CHARS = 50_000;
@ -474,6 +474,10 @@ class PrefixedWrappedLine implements Component {
// unwrapped paragraph scrolls within a fixed window instead of growing
// unbounded. The first kept row still gets `firstPrefix`.
private readonly tailLines?: number,
// When set, the output is padded with empty continuation rows until it
// reaches this many display rows, so a short paragraph still fills a
// fixed-height window. Applied after `tailLines`.
private readonly minLines?: number,
) { }
invalidate(): void {
@ -498,6 +502,9 @@ class PrefixedWrappedLine implements Component {
this.tailLines !== undefined && wrapped.length > this.tailLines
? wrapped.slice(wrapped.length - this.tailLines)
: wrapped;
if (this.minLines !== undefined) {
while (lines.length < this.minLines) lines.push('');
}
const rendered = lines
.map((line, index) =>
index === 0 ? `${this.firstPrefix}${line}` : `${this.continuationPrefix}${line}`,
@ -548,6 +555,9 @@ export class ToolCallComponent extends Container {
*/
private subagentText = '';
private subagentThinkingText = '';
/** Tracks whether the child agent's latest streamed delta was text or thinking,
* so the active window can follow whichever is currently live. */
private lastSubagentStreamKind: SubagentTextKind = 'text';
// ── Subagent lifecycle state from subagent.spawned/started/completed/failed ──
private subagentPhase: SubagentPhase | undefined;
/**
@ -577,6 +587,7 @@ export class ToolCallComponent extends Container {
private subagentElapsedTimer: ReturnType<typeof setInterval> | undefined;
private subagentStartedAtMs: number | undefined;
private subagentEndedAtMs: number | undefined;
private subagentSpinnerFrame = 0;
// ── Live progress lines ──────────────────────────────────────────
//
@ -1036,11 +1047,14 @@ export class ToolCallComponent extends Container {
this.stopSubagentElapsedTimer();
return;
}
// Drives both the braille spinner in the header and the elapsed-seconds
// refresh. Only the header text changes on a tick, so we avoid rebuilding
// the body (which would defeat the per-component render caches).
this.subagentSpinnerFrame = (this.subagentSpinnerFrame + 1) % BRAILLE_SPINNER_FRAMES.length;
this.headerText.setText(this.buildHeader());
this.invalidate();
this.notifySnapshotChange();
this.ui?.requestRender();
}, SUBAGENT_ELAPSED_INTERVAL_MS);
}, BRAILLE_SPINNER_INTERVAL_MS);
}
private stopSubagentElapsedTimer(): void {
@ -1260,6 +1274,7 @@ export class ToolCallComponent extends Container {
}
appendSubagentText(text: string, kind: SubagentTextKind = 'text'): void {
this.lastSubagentStreamKind = kind;
if (kind === 'thinking') {
this.subagentThinkingText += text;
} else {
@ -1429,6 +1444,20 @@ export class ToolCallComponent extends Container {
return `${bullet}${currentTheme.boldFg(tone, label)}`;
}
if (toolCall.name === 'Bash') {
// The command itself is rendered in the body (with a `$` prompt), so the
// header only names the action — repeating the command in parentheses
// would duplicate the body. Wording mirrors the other label-only headers
// (e.g. AskUserQuestion): the whole label takes the tone colour.
if (isTruncated) {
return `${bullet}${currentTheme.fg('error', 'Truncated')} ${currentTheme.boldFg('primary', 'Bash')}`;
}
const label = isFinished ? 'Ran a command' : 'Running a command';
const tone = isError ? 'error' : 'primary';
const chipStr = isFinished && result !== undefined ? this.buildHeaderChip(result) : '';
return `${bullet}${currentTheme.boldFg(tone, label)}${chipStr}`;
}
const goalHeader = buildGoalToolHeader({
toolCall,
result,
@ -1697,25 +1726,24 @@ export class ToolCallComponent extends Container {
private buildSingleSubagentHeader(): string {
const phase = this.getDerivedSubagentPhase();
const isFailed = phase === 'failed';
const isDone = phase === 'done';
const bullet = isFailed
? currentTheme.fg('error', '✗ ')
: isDone
? currentTheme.fg('success', STATUS_BULLET)
: currentTheme.fg('text', STATUS_BULLET);
const marker = this.buildSingleSubagentMarker(phase);
const labelText = formatSubagentLabel(this.subagentAgentName);
const label = currentTheme.boldFg('primary', labelText);
const status = this.formatSingleSubagentStatus(phase);
const description = str(this.toolCall.args['description']);
const rawDescription = str(this.toolCall.args['description']);
const description =
rawDescription.length > MAX_SUBAGENT_DESCRIPTION_LENGTH
? `${rawDescription.slice(0, MAX_SUBAGENT_DESCRIPTION_LENGTH - 1)}`
: rawDescription;
const descriptionPlain = description.length > 0 ? ` (${description})` : '';
const descriptionText = descriptionPlain.length > 0 ? currentTheme.dim(descriptionPlain) : '';
const statsText = this.formatSingleSubagentStatsText();
if (isDone) {
return `${bullet}${currentTheme.boldFg('success', labelText)} ${currentTheme.fg('success', `Completed${descriptionPlain}${statsText}`)}`;
return `${marker}${currentTheme.boldFg('success', labelText)} ${currentTheme.fg('success', `Completed${descriptionPlain}${statsText}`)}`;
}
const stats = currentTheme.dim(statsText);
return `${bullet}${label} ${status}${descriptionText}${stats}`;
return `${marker}${label} ${status}${descriptionText}${stats}`;
}
private formatSingleSubagentStatus(phase: SubagentPhase | undefined): string {
@ -1758,92 +1786,133 @@ export class ToolCallComponent extends Container {
return Math.max(0, Math.floor((end - this.subagentStartedAtMs) / 1000));
}
private buildSingleSubagentBlock(): void {
for (const activity of this.getRecentSubToolActivities()) {
const mark =
activity.phase === 'failed'
? currentTheme.fg('error', '✗')
: activity.phase === 'done'
? currentTheme.fg('success', '•')
: currentTheme.fg('text', '•');
const verb = activity.phase === 'ongoing' ? 'Using' : 'Used';
this.addChild(new Text(` ${mark} ${this.formatSubToolActivity(verb, activity)}`, 0, 0));
this.addSubToolOutputPreview(activity);
}
if (this.getDerivedSubagentPhase() === 'failed' && this.subagentError !== undefined) {
const errorLine = tailNonEmptyLines(this.subagentError, 1).at(-1);
if (errorLine !== undefined) {
this.addChild(
new PrefixedWrappedLine(
` ${currentTheme.fg('error', '└')} `,
' ',
currentTheme.fg('error', errorLine),
),
);
}
return;
}
const outputLine = tailNonEmptyLines(this.subagentText, 1).at(-1);
if (
this.getDerivedSubagentPhase() !== 'done' &&
this.subagentThinkingText.trim().length > 0
) {
// Scroll thinking within a fixed two-row window (width-aware), matching
// the main agent's live thinking instead of growing without bound.
this.addChild(
new PrefixedWrappedLine(
` ${currentTheme.dim('◌')} `,
' ',
currentTheme.dim(this.subagentThinkingText.trimEnd()),
THINKING_PREVIEW_LINES,
),
);
}
if (outputLine !== undefined) {
this.addChild(
new PrefixedWrappedLine(
` ${currentTheme.fg('text', '└')} `,
' ',
currentTheme.fg('text', outputLine),
),
);
}
private buildSingleSubagentMarker(phase: SubagentPhase | undefined): string {
if (phase === 'failed') return currentTheme.fg('error', '✗ ');
if (phase === 'done') return currentTheme.fg('success', STATUS_BULLET);
if (phase === 'backgrounded') return currentTheme.dim('◐ ');
// Active (queued / spawning / running): a braille spinner reads as alive
// where a static bullet looked frozen.
const frame = BRAILLE_SPINNER_FRAMES[this.subagentSpinnerFrame] ?? BRAILLE_SPINNER_FRAMES[0];
return currentTheme.fg('primary', `${frame} `);
}
private addSubToolOutputPreview(activity: SubToolActivity): void {
const output = activity.output;
if (output === undefined || output.trim().length === 0) return;
// Mirror the main agent: Bash and any tool without a dedicated renderer
// (every MCP tool included) get a truncated output preview. Recognized
// tools keep their compact activity row only.
if (activity.name !== 'Bash' && !isGenericToolResult(activity.name)) return;
this.addChild(
new TruncatedOutputComponent(output, {
// Subagent output is always fixed-truncated; it does not take part in
// the ctrl+o expand toggle, so don't advertise it either.
expanded: false,
expandHint: false,
isError: activity.phase === 'failed',
maxLines: RESULT_PREVIEW_LINES,
indent: SUBAGENT_SUBTOOL_OUTPUT_INDENT,
tail: activity.phase === 'ongoing',
}),
private buildSingleSubagentBlock(): void {
const phase = this.getDerivedSubagentPhase();
// Every state shares the same skeleton — header, a one-line tool summary,
// and a fixed two-row content window — so the card height is identical
// while running and after it finishes (no end-of-run shrink).
this.addChild(new Text(this.buildSingleSubagentSummaryLine(), 0, 0));
if (phase === 'failed') {
this.addChild(this.buildSingleSubagentResultWindow('error'));
return;
}
if (phase === 'done' || phase === 'backgrounded') {
this.addChild(this.buildSingleSubagentResultWindow('output'));
return;
}
this.addChild(this.buildSingleSubagentActiveWindow());
}
/** Most-recently-started sub-tool, preferring one that is still running. */
private getCurrentSubToolActivity(): SubToolActivity | undefined {
let latestOngoing: SubToolActivity | undefined;
let latest: SubToolActivity | undefined;
for (const activity of this.subToolActivities.values()) {
if (latest === undefined || activity.orderSeq > latest.orderSeq) latest = activity;
if (
activity.phase === 'ongoing' &&
(latestOngoing === undefined || activity.orderSeq > latestOngoing.orderSeq)
) {
latestOngoing = activity;
}
}
return latestOngoing ?? latest;
}
/**
* The single live stream shown in the active window. A running sub-tool with
* previewable output (Bash or any tool without a dedicated renderer) wins;
* otherwise the most-recently-updated of the child agent's text / thinking.
*/
private getActiveSubagentContent(): { text: string; tone: 'text' | 'thinking' } | undefined {
const current = this.getCurrentSubToolActivity();
if (
current?.phase === 'ongoing' &&
current.output !== undefined &&
current.output.trim().length > 0 &&
(current.name === 'Bash' || isGenericToolResult(current.name))
) {
return { text: current.output, tone: 'text' };
}
if (this.lastSubagentStreamKind === 'thinking' && this.subagentThinkingText.trim().length > 0) {
return { text: this.subagentThinkingText.trimEnd(), tone: 'thinking' };
}
if (this.subagentText.trim().length > 0) {
return { text: this.subagentText, tone: 'text' };
}
if (this.subagentThinkingText.trim().length > 0) {
return { text: this.subagentThinkingText.trimEnd(), tone: 'thinking' };
}
return undefined;
}
private buildSingleSubagentSummaryLine(): string {
const toolCount = this.subToolActivities.size;
const countLabel = `${String(toolCount)} tool${toolCount === 1 ? '' : 's'}`;
const current = this.getCurrentSubToolActivity();
if (current === undefined) {
return currentTheme.dim(` · ${countLabel}`);
}
const verb = current.phase === 'ongoing' ? 'Using' : 'Used';
const keyArg = extractKeyArgument(current.name, current.args, this.workspaceDir);
const nameCol = currentTheme.fg('primary', current.name);
const argCol = keyArg ? currentTheme.dim(` (${keyArg})`) : '';
const mark =
current.phase === 'failed'
? currentTheme.fg('error', ' ✗')
: current.phase === 'done'
? currentTheme.fg('success', ' ✓')
: '';
return `${currentTheme.dim(` · ${countLabel} · `)}${verb} ${nameCol}${argCol}${mark}`;
}
private buildSingleSubagentActiveWindow(): Component {
const gutter = currentTheme.dim('│');
const content = this.getActiveSubagentContent();
// Keep both tones muted: a bright `fg('text')` here flashed white whenever
// the window flipped between thinking and a brief text/tool-output segment.
const styled =
content === undefined
? currentTheme.dim('…')
: content.tone === 'thinking'
? currentTheme.dim(content.text)
: currentTheme.fg('textDim', content.text);
// Always exactly two rows (padded when short) so the live window matches
// the finished card's height.
return new PrefixedWrappedLine(
` ${gutter} `,
` ${gutter} `,
styled,
THINKING_PREVIEW_LINES,
THINKING_PREVIEW_LINES,
);
}
private getRecentSubToolActivities(): SubToolActivity[] {
return [...this.subToolActivities.values()]
.toSorted((a, b) => a.orderSeq - b.orderSeq)
.slice(-MAX_SINGLE_SUBAGENT_TOOL_ROWS);
}
private formatSubToolActivity(verb: string, activity: SubToolActivity): string {
const keyArg = extractKeyArgument(activity.name, activity.args, this.workspaceDir);
const nameCol = currentTheme.fg('primary', activity.name);
const argCol = keyArg ? currentTheme.dim(` (${keyArg})`) : '';
return `${verb} ${nameCol}${argCol}`;
private buildSingleSubagentResultWindow(kind: 'output' | 'error'): Component {
const gutter = currentTheme.dim('│');
const source = kind === 'error' ? this.subagentError : this.subagentText;
const text = source === undefined ? '' : tailNonEmptyLines(source, 2).join('\n');
const styled =
kind === 'error' ? currentTheme.fg('error', text) : currentTheme.fg('text', text);
return new PrefixedWrappedLine(
` ${gutter} `,
` ${gutter} `,
styled,
THINKING_PREVIEW_LINES,
THINKING_PREVIEW_LINES,
);
}
private buildCallPreview(): void {
@ -1866,7 +1935,14 @@ export class ToolCallComponent extends Container {
this.buildStreamingPreview(this.toolCall.streamingArguments);
return;
}
const shouldCap = this.result !== undefined && !this.expanded;
// Cap Edit's diff as soon as args finalize, not only when the result
// lands — mirroring Write's writeShouldCap below. Otherwise the render
// tick between finalized args (streamingArguments cleared by the
// `tool.call.started` payload) and the result draws the full diff, then
// snaps back to the cap: a height collapse that triggers pi-tui's full
// redraw and wipes scrollback. Streaming frames (streamingArguments set)
// still take buildStreamingPreview above and never reach here.
const shouldCap = !this.expanded;
if (name === 'Write') {
const content = str(this.toolCall.args['content']);
if (content.length === 0) return;
@ -1907,11 +1983,15 @@ export class ToolCallComponent extends Container {
for (const line of lines) {
this.addChild(new Text(line, 2, 0));
}
} else if (name === 'Bash' && this.result === undefined) {
// While a long-running Bash call is in-flight (args finalized, no result
// yet), surface its command in the body so the user can see what is
// running and expand it with ctrl+o. Once the result lands, buildContent's
// shellExecutionResultRenderer takes over command rendering.
} else if (name === 'Bash') {
// Surface the command in the body across the whole lifecycle — while
// streaming, running, and after the result lands. Keeping the collapsed
// command preview here (instead of yielding to the result renderer once
// the result lands) avoids a height collapse when a multi-line command
// finishes with short output: the command block stays put and only the
// live-output tail swaps for the result. Owned solely by buildCallPreview
// so the command never renders twice; shellExecutionResultRenderer
// renders the result only.
const command = str(this.toolCall.args['command']);
if (command.length === 0) return;
this.addChild(
@ -1984,7 +2064,7 @@ export class ToolCallComponent extends Container {
new ShellExecutionComponent({
command: cmd,
showCommand: true,
commandPreviewLines: COMMAND_PREVIEW_LINES,
commandPreviewLines: this.expanded ? undefined : COMMAND_PREVIEW_LINES,
}),
);
}
@ -2050,10 +2130,14 @@ export class ToolCallComponent extends Container {
return;
}
// Outputs that start with a `<system…>` tag are harness-injected
// reminders piggy-backing on a tool result. They are noise for the
// user, so suppress the body while keeping the header chip intact.
if (result.output.trimStart().startsWith('<system')) {
// Outputs that start with a `<system-reminder>` tag are harness-injected
// reminders piggy-backing on a tool result (e.g. a finalize hook rewrote
// the output). They are noise for the user, so suppress the body while
// keeping the header chip intact. Match the full reminder tag only: tool
// metadata no longer travels inside `output` (it rides the result's
// `note` side channel), so real output starting with a literal `<system>`
// is user data and must stay visible.
if (result.output.trimStart().startsWith('<system-reminder>')) {
return;
}

View file

@ -1,4 +1,4 @@
import { Text } from '@earendil-works/pi-tui';
import { Text } from '@moonshot-ai/pi-tui';
import { STATUS_BULLET } from '#/tui/constant/symbols';
import { currentTheme } from '#/tui/theme';

View file

@ -13,8 +13,8 @@
* message.
*/
import type { Component } from '@earendil-works/pi-tui';
import { Text } from '@earendil-works/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import { Text } from '@moonshot-ai/pi-tui';
import chalk from 'chalk';
import type { ChipProvider } from './chip';
@ -27,11 +27,9 @@ export interface ReadMediaSummary {
mimeType?: string;
bytes?: number;
url?: string;
originalSize?: string;
}
const PATH_TAG_RE = /^<(image|video)\s+path="([^"]+)">$/;
const ORIGINAL_SIZE_RE = /original size\s+(\d+x\d+px)/;
const DATA_URL_RE = /^data:([^;]+);base64,(.*)$/s;
function bytesFromBase64(b64: string): number {
@ -55,7 +53,6 @@ export function parseReadMediaOutput(output: string): ReadMediaSummary | null {
let mimeType: string | undefined;
let bytes: number | undefined;
let url: string | undefined;
let originalSize: string | undefined;
let foundMedia = false;
for (const raw of parsed) {
@ -64,15 +61,11 @@ export function parseReadMediaOutput(output: string): ReadMediaSummary | null {
const type = part['type'];
if (type === 'text' && typeof part['text'] === 'string') {
const text = part['text'];
const tag = PATH_TAG_RE.exec(text);
const tag = PATH_TAG_RE.exec(part['text']);
if (tag) {
kind = tag[1] as 'image' | 'video';
path = tag[2];
continue;
}
const size = ORIGINAL_SIZE_RE.exec(text);
if (size) originalSize = size[1];
continue;
}
@ -103,7 +96,6 @@ export function parseReadMediaOutput(output: string): ReadMediaSummary | null {
if (mimeType !== undefined) summary.mimeType = mimeType;
if (bytes !== undefined) summary.bytes = bytes;
if (url !== undefined) summary.url = url;
if (originalSize !== undefined) summary.originalSize = originalSize;
return summary;
}
@ -117,7 +109,6 @@ function metaSegments(summary: ReadMediaSummary): string[] {
const segs: string[] = [];
if (summary.mimeType !== undefined) segs.push(summary.mimeType);
if (summary.bytes !== undefined) segs.push(formatBytes(summary.bytes));
if (summary.originalSize !== undefined) segs.push(summary.originalSize);
return segs;
}

View file

@ -10,8 +10,8 @@
* sees the actual error message, not a synthetic summary.
*/
import type { Component } from '@earendil-works/pi-tui';
import { Text } from '@earendil-works/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import { Text } from '@moonshot-ai/pi-tui';
import chalk from 'chalk';
import { renderTruncated } from './truncated';

View file

@ -1,6 +1,7 @@
import { Text, truncateToWidth, type Component } from '@earendil-works/pi-tui';
import { Text, truncateToWidth, type Component } from '@moonshot-ai/pi-tui';
import { currentTheme } from '#/tui/theme';
import type { ColorPalette } from '#/tui/theme/colors';
import type { ResultRenderer } from './types';
import { PREVIEW_LINES } from './types';
@ -44,6 +45,10 @@ export class TruncatedOutputComponent implements Component {
// When true, collapsed rendering keeps the latest visual rows instead of
// the first rows. This is useful for live output from a running command.
tail?: boolean;
// Foreground colour for successful (non-error) output. Defaults to
// `textDim`; Bash passes `textMuted` so its result sits one shade below
// the `textDim` command. Error output always uses `error`.
color?: keyof ColorPalette;
},
) {
this.expanded = options.expanded;
@ -52,8 +57,9 @@ export class TruncatedOutputComponent implements Component {
this.expandHint = options.expandHint ?? true;
this.tail = options.tail ?? false;
const cleaned = trimTrailingEmptyLines(output.split('\n')).join('\n');
const successColor = options.color ?? 'textDim';
this.textComponent = new Text(
options.isError ? currentTheme.fg('error', cleaned) : currentTheme.dim(cleaned),
options.isError ? currentTheme.fg('error', cleaned) : currentTheme.fg(successColor, cleaned),
this.indent,
0,
);

View file

@ -1,4 +1,4 @@
import type { Component } from '@earendil-works/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import { RESULT_PREVIEW_LINES } from '#/tui/constant/rendering';
import type { ToolCallBlockData, ToolResultBlockData } from '#/tui/types';

View file

@ -4,8 +4,8 @@
* the pattern stays consistent across command-triggered panels.
*/
import type { Component } from '@earendil-works/pi-tui';
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
import type { Component } from '@moonshot-ai/pi-tui';
import { truncateToWidth, visibleWidth } from '@moonshot-ai/pi-tui';
import type { SessionUsage, TokenUsage } from '@moonshot-ai/kimi-code-sdk';
import {
@ -30,9 +30,19 @@ export interface ManagedUsageRow {
readonly resetHint?: string;
}
export interface BoosterWalletInfo {
readonly balanceCents: number;
readonly totalCents: number;
readonly monthlyChargeLimitEnabled: boolean;
readonly monthlyChargeLimitCents: number;
readonly monthlyUsedCents: number;
readonly currency: string;
}
export interface ManagedUsageReport {
readonly summary: ManagedUsageRow | null;
readonly limits: readonly ManagedUsageRow[];
readonly extraUsage?: BoosterWalletInfo | null;
}
export interface UsageReportOptions {
@ -121,8 +131,7 @@ function buildManagedUsageSection(
r.limit > 0 ? Math.max(0, Math.min(r.used / r.limit, 1)) : 0;
const labelWidth = Math.max(10, ...rows.map((r) => r.label.length));
const pctWidth = Math.max(...rows.map((r) => `${Math.round(usedRatio(r) * 100)}% used`.length));
const severityColor = (sev: 'ok' | 'warn' | 'danger'): 'success' | 'warning' | 'error' =>
sev === 'danger' ? 'error' : sev === 'warn' ? 'warning' : 'success';
const out: string[] = [accent('Plan usage')];
for (const row of rows) {
const ratioUsed = usedRatio(row);
@ -136,6 +145,91 @@ function buildManagedUsageSection(
return out;
}
function severityColor(sev: 'ok' | 'warn' | 'danger'): 'success' | 'warning' | 'error' {
return sev === 'danger' ? 'error' : sev === 'warn' ? 'warning' : 'success';
}
function currencySymbol(currency: string): string {
switch (currency.toUpperCase()) {
case 'CNY':
return '¥';
case 'USD':
return '$';
default:
return '';
}
}
interface CurrencyParts {
readonly symbol: string;
readonly number: string;
}
function formatCurrencyParts(cents: number, currency: string): CurrencyParts {
const symbol = currencySymbol(currency);
const main = cents / 100;
const formatted = main.toFixed(2);
return symbol.length > 0
? { symbol, number: formatted }
: { symbol: '', number: `${formatted} ${currency}` };
}
export function buildExtraUsageSection(
extraUsage: BoosterWalletInfo | undefined | null,
accent: Colorize,
value: Colorize,
muted: Colorize,
): string[] {
if (extraUsage === undefined || extraUsage === null) return [];
const hasMonthlyLimit =
extraUsage.monthlyChargeLimitEnabled && extraUsage.monthlyChargeLimitCents > 0;
const balance = formatCurrencyParts(extraUsage.balanceCents, extraUsage.currency);
const used = formatCurrencyParts(extraUsage.monthlyUsedCents, extraUsage.currency);
const rows: Array<{ label: string; symbol: string; number: string }> = [];
let barLine: string | null = null;
if (hasMonthlyLimit) {
const ratio = Math.max(
0,
Math.min(extraUsage.monthlyUsedCents / extraUsage.monthlyChargeLimitCents, 1),
);
const bar = renderProgressBar(ratio, 20);
barLine = ` ${currentTheme.fg(severityColor(ratioSeverity(ratio)), bar)}`;
const limit = formatCurrencyParts(extraUsage.monthlyChargeLimitCents, extraUsage.currency);
rows.push({ label: 'Used this month', ...used });
rows.push({ label: 'Monthly limit', ...limit });
rows.push({ label: 'Balance', ...balance });
} else {
rows.push({ label: 'Used this month', ...used });
rows.push({ label: 'Monthly limit', symbol: '', number: 'Unlimited' });
rows.push({ label: 'Balance', ...balance });
}
// `Used this month` is the longest label; size the column to the widest label
// so the currency symbol starts in the same column on every row.
const labelWidth = Math.max(...rows.map((r) => r.label.length));
// Right-align the numeric part of currency rows against each other so the
// decimal points line up (e.g. `¥ 50.00` / `¥200.00`). Text-only rows such as
// `Unlimited` carry no currency symbol, so they must not widen the numeric
// column — otherwise money values get padded with stray spaces.
const numberWidth = Math.max(
0,
...rows.filter((r) => r.symbol.length > 0).map((r) => visibleWidth(r.number)),
);
const row = (label: string, symbol: string, number: string): string => {
const cell = symbol.length > 0 ? symbol + number.padStart(numberWidth, ' ') : number;
return ` ${muted(label.padEnd(labelWidth, ' '))} ${value(cell)}`;
};
const lines: string[] = [accent('Extra Usage')];
if (barLine !== null) lines.push(barLine);
for (const r of rows) lines.push(row(r.label, r.symbol, r.number));
return lines;
}
export function buildManagedUsageReportLines(options: ManagedUsageReportLineOptions): string[] {
const accent = (text: string) => currentTheme.boldFg('primary', text);
const value = (text: string) => currentTheme.fg('text', text);
@ -157,8 +251,6 @@ export function buildUsageReportLines(options: UsageReportOptions): string[] {
const value = (text: string) => currentTheme.fg('text', text);
const muted = (text: string) => currentTheme.fg('textDim', text);
const errorStyle = (text: string) => currentTheme.fg('error', text);
const severityColor = (sev: 'ok' | 'warn' | 'danger'): 'success' | 'warning' | 'error' =>
sev === 'danger' ? 'error' : sev === 'warn' ? 'warning' : 'success';
const lines: string[] = [
accent('Session usage'),
@ -197,6 +289,17 @@ export function buildUsageReportLines(options: UsageReportOptions): string[] {
lines.push(...managedSection);
}
const extraSection = buildExtraUsageSection(
options.managedUsage?.extraUsage,
accent,
value,
muted,
);
if (extraSection.length > 0) {
lines.push('');
lines.push(...extraSection);
}
return lines;
}

Some files were not shown because too many files have changed in this diff Show more