Commit graph

278 commits

Author SHA1 Message Date
liruifengv
b51e13538d
ci: run unit tests on windows (#1037)
* ci: run unit tests on windows

* fix(migration-legacy): align workdir bucket key with agent-core

computeWorkdirBucket used a local node:path-based resolve that yields backslash-separated paths on Windows, while agent-core's encodeWorkDirKey uses pathe (forward slashes on every platform). The SHA-256 inputs diverged, so migrated sessions were written to a bucket that the session picker never reads, making them invisible on Windows.

Alias computeWorkdirBucket to encodeWorkDirKey so both sides stay byte-identical, drop the local slugify copy, and update the workdir-bucket test reference accordingly.

* test(acp-adapter): expect platform-native separators in e2e-fs path

The e2e-fs test asserted the fs/readTextFile wire path as the raw POSIX targetPath, but AcpKaos.toClientPath converts '/' to '\' when the inner LocalKaos reports pathClass 'win32' (Windows). On Windows the wire path became '\Users\test\x.ts' and the assertion failed.

Mirror toClientPath in the test: expect backslash separators on win32 and the raw path otherwise. Implementation is unchanged.

* test(sdk): normalize workDir and skillDir paths in session tests

SessionStore.create/list and the skill loader normalize paths through pathe (forward slashes). The SDK tests compared the resulting workDir and skill loaded-dir against raw mkdtemp / node:path strings, which use backslashes on Windows (and node:fs realpath also returns backslashes for the skill dir), failing three toMatchObject assertions.

Build the expected paths with agent-core's normalizeWorkDir so they match the internal pathe representation on every platform. The skill dir keeps its realpath() (the loader realpaths the root) and only normalizes separators.

* test(skill): normalize realpath to forward slashes in scanner tests

resolveSkillRoots normalizes every root.path through fs.realpath followed by replacing backslashes with forward slashes (scanner.ts). The scanner tests compared root.path against node:fs realpath directly, which returns backslashes on Windows, so twenty assertions failed (toEqual / toContain / toHaveLength) even though the resolved paths were identical.

Wrap realpath at the top of the test file to mirror the implementation's normalization, so every comparison uses the same forward-slash form on every platform.

* test: skip Unix-only permission tests on Windows

The Unix file-permission assertions (mode bits like 0o600 / 0o700 and chmod 000 making a path unreadable) have no equivalent on Windows, which uses ACLs; fs.chmod there can only toggle the read-only bit. These six tests failed on Windows with mismatched mode values or a missing 40411.

Skip them on win32 via it.skipIf(process.platform === 'win32'): oauth FileTokenStorage (0600 file, 0700 dir), agent-core BackgroundTaskPersistence (0700 tasks dir), agent-core createPerIdJsonStore (0700 subdir), migration-legacy atomicWrite (0600 file), and server fs:browse (chmod 000 -> 40411).

* test(tui): make platform-sensitive assertions cross-platform

The TUI implementations are already platform-aware (pathe-style paths, pathToFileURL, quoteShellArg cmd/POSIX quoting, Alt+V on Windows for paste expansion), but the tests hard-coded POSIX expectations and failed on Windows.

Align the assertions with the implementation's platform behavior: footer-goal-badge matches the '[goal' badge prefix instead of /goal/ (toolbar tips contain '/goal'); tool-call expects backslash relative paths on win32; plan-box builds the file:// URL via pathToFileURL; custom-editor sends Alt+V on win32 for paste expansion; file-mention-provider normalizes the expected description to forward slashes; kimi-tui-startup builds the resume command with quoteShellArg; kimi-tui-message-flow builds the expected install path with resolve().

* test: align path assertions with pathe on Windows

Several test suites asserted paths produced by node:path/node:os/node:fs against values that agent-core, node-sdk and kaos normalize through pathe (forward slashes). On Windows the two forms diverge (backslashes vs forward slashes), failing about 19 assertions.

Mirror the implementation's normalization in the assertions via a local toPosix helper (or agent-core's normalizeWorkDir), so expected paths use forward slashes on every platform: kaos LocalKaos, node-sdk export/list/resume/config/transport sessions, cli FileMentionProvider, and agent-core skill-session.

* test(native): build path expectations with node:path.resolve

paths.mjs builds every path with node:path.resolve, which yields backslash-separated absolute paths on Windows. The path-helpers tests asserted against template strings that mixed the backslash appRoot with forward-slash segments, so Object.is failed on Windows even though the strings looked identical.

Build the expectations with the same resolve(appRoot, ...) helper so the separators match on every platform.

* fix: make Windows CI tests pass across all packages

Fix the remaining Windows CI failures so the Windows test job can go green. The changes fall into a few categories:

- Path separators: agent-core/node-sdk/kaos normalize paths via pathe (forward slashes); align test expectations and a couple of implementations (native cache base, workspace registry) with that.

- Platform-only services: skip launchd/systemd manager suites on win32 (Windows uses schtasks).

- Process/signal lifecycle: skip or relax tests that rely on POSIX signals / SIGTERM semantics that Windows does not support.

- Hook shell syntax: rewrite hook test commands from POSIX shell (single quotes, semicolons, stderr redirects, if/then/fi) to node -e / .cjs files that run under cmd.exe.

- CRLF: make Bash tool description stripping tolerate CRLF line endings.

- Misc: realpath short-name divergence, port-retry timing, telemetry spawn, fs-watch timing, snapshot path normalization, etc.

* fix: remove unused basename import in workspaceRegistryService

Fix lint error (no-unused-vars): basename from node:path is no longer used after switching to posixBasename from pathe.

* fix: align resume harness pathClass and wait for banner state on Windows

Two more Windows CI fixes:

- createResumeNoSideEffectKaos now reports pathClass 'win32' on Windows so tool descriptions (e.g. Glob's Windows note) match the live agent in expectResumeMatches, fixing usage/description deep-equal drift.

- kimi-tui-startup once-banner test now waits for writeBannerDisplayState to land before asserting, since the atomic write can lag behind the render on Windows.

* fix: resolve remaining Windows unit test failures

Make the new Windows CI job green across agent-core, kaos, node-sdk and server:

- Align the resume harness kaos pathClass with the live agent so platform-conditional tool descriptions (Glob's Windows note) match in expectResumeMatches instead of drifting on win32.
- Rewrite hook commands in agent-core tests as cross-platform node one-liners; single-quote echo, >&2 and ';' do not work under cmd.exe.
- Add .gitattributes enforcing LF so raw-imported templates (e.g. the compaction instruction) produce byte-identical token counts on Windows and POSIX.
- Terminate the full process tree on Windows in both the hook runner and kaos (taskkill /T /F) so grandchildren cannot outlive their parent and keep the cwd locked.
- Normalize workDir path separators in two kimi-sdk session tests to match the stored canonical form.
- Avoid cmd.exe arg-quoting pitfalls in the kaos cmd.exe test, and run the Windows process-tree kill test from a script file with the pid path passed via argv.
- Give the first fs-git e2e test more time on Windows and retry the temp-dir cleanup; skip the fs-watch overflow-burst assertion on Windows where fs-event coalescing prevents the single-window spike.

* ci: retrigger checks

* fix: resolve remaining Windows failures after merging main

- Terminate the spawned git/gh process tree on Windows in FsGitService (taskkill /T /F on timeout) so a timed-out 'gh pr view' cannot leave a grandchild holding the workspace cwd, which made the fs-git e2e cleanup fail with EPERM.

- Give the fs:git_status e2e suite a longer timeout on Windows and retry the temp-dir cleanup longer to ride out the slower child-process teardown.

- Make the third-party plugin install trust test assert the resolved install path via node:path so it matches the Windows-resolved path (D:\tmp\...) as well as the POSIX one.

* fix: align workspace registry roots and harden fs-git cleanup on Windows

- workspace-registry test: compare normalized (forward-slash) roots, since the registry and session index both store workDir via pathe.resolve (forward slashes on every platform). realpath() yields backslashes on Windows and diverged from the stored root.

- fs-git e2e: bump the temp-dir cleanup retries and the afterEach timeout, since Windows child-process teardown after server.close() is asynchronous and can keep the workspace cwd locked for several seconds.

* test: stub openUrl in kimi-tui-message-flow feedback tests

The /feedback command falls back to openUrl(FEEDBACK_ISSUE_URL) when submission fails, which spawned a real browser window on every test run. Mock #/utils/open-url (matching the existing login/message-replay/server test convention) so the suite never opens a browser.

* test: harden fs-git e2e cleanup against Windows cwd locks

On Windows, git/gh child processes and the session core process can outlive server.close() and keep the temp workspace as their cwd, so rmSync fails with EPERM even after a long retry. Add rmSyncRobust that retries and, if the cwd is still locked, swallows EPERM/EBUSY on Windows — the OS reclaims the temp dir and a cleanup hiccup must not fail an otherwise-passing test.

* test: harden server e2e cleanup against async teardown races

server.close() does not fully await the server's asynchronous teardown, so on a loaded CI runner the temp home/workspace dirs can still be held or written to when the afterEach rmSync runs, failing with EPERM (Windows) or ENOTEMPTY (Linux). Use a rmSyncRobust helper (retry + swallow EPERM/EBUSY/ENOTEMPTY) in the fs-git and question e2e cleanup. Also fix a leftover `throw err` (renamed to `throw error`) that broke the typecheck.
2026-06-26 11:56:41 +08:00
github-actions[bot]
5f36e763ca
ci: release packages (#1061)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-26 02:29:53 +08:00
qer
6194d3fad3
perf(web): reuse sessions reference to stop sidebar re-render on streaming deltas (#1113) 2026-06-26 02:24:31 +08:00
qer
6a97d0bf43
feat(web): add a copy button to user messages (#1112)
* feat(web): add a copy button to user messages

Mirror the assistant per-message copy button on user turns, copying the message text with the same checkmark feedback.

* fix(web): place user message copy button with undo and time

Move the copy button out of the line-number role row into the modern bubble meta row, grouped with the undo and time buttons, with matching styling and tooltip.
2026-06-26 01:01:56 +08:00
qer
d554f9ac87
feat(web): show full accumulated subagent progress (#1109)
* feat(web): show full accumulated subagent progress

The subagent detail panel only showed the most recent 40 progress lines because the reducer truncated the accumulated output. Keep the full history so the panel reflects the entire process as it grows.

* fix(web): preserve subagent progress across task updates

The projector emits a taskCreated (without reducer-owned outputLines) right before every taskProgress, and the taskCreated branch replaced the task object outright, resetting outputLines to empty on each progress event. So the panel still only showed the latest chunk. Preserve the accumulated outputLines when replacing a task, and update the test to mirror the real taskCreated-before-taskProgress path.

* feat(web): clean up subagent progress text

Drop the noisy 'Started a step' line and summarize tool calls with a concise target (path / command / pattern) instead of the full JSON args, so the subagent progress panel shows what the subagent is actually doing.

* fix(web): strip numeric index from subagent tool result names

Some subagents name tool calls with a trailing index (e.g. Read_0, Bash_4), which surfaced in tool.result progress lines since the label did not resolve. Strip the index before resolving the label.

* fix(web): bound non-subagent task output and subagent progress text

Restore a tail cap for background bash/tool task output (which can grow without bound) while keeping subagent progress in full, and cap individual subagent tool.progress chunks so a single huge output cannot dominate the panel.

* feat(web): group and fold subagent progress output

Drop the noisy 'Finished' lines, group tool output under its call, and fold long output (first 5 + last 2 lines, expandable) so the subagent progress panel shows the call rhythm at a glance.
2026-06-26 00:22:12 +08:00
qer
18f7c34a07
feat(web): show edit diffs inline in tool call cards (#1103)
* feat(web): show edit diffs inline in tool call cards

Render a line-by-line diff inside Edit/Write tool cards in the web chat, reusing the existing diff-line style from the changes panel. The diff is built client-side from the tool input, so no protocol or server changes are needed; the header chip now shows real +/- counts.

* fix(web): extend diff line backgrounds through horizontal scroll

Diff rows were only as wide as the viewport, so the add/del background stopped where long lines overflowed and the area revealed by horizontal scroll had no color. Size each row to its content so the background paints the full line.

* fix(web): make all diff rows fill the longest line width

Size the diff container to the longest line and have every row fill it, so add/del backgrounds form one continuous band across the whole horizontal scroll instead of stopping at each line's own length.

* fix(web): resolve oxlint errors in diff line builder

Use Array.at(-1) and Array.from instead of index access and new Array(length), which oxlint flags as errors.

* fix(web): show tool output for failed edit/write calls

When an Edit/Write call fails, render the tool output (the failure reason) instead of the requested diff, so error cards explain why no change was applied.

* fix(web): fall back to tool output for replace_all and append edits

A single-pair diff misrepresents replace_all edits (which change many occurrences) and from-empty diffs misrepresent append writes (which extend an existing file). Skip the synthetic diff in those cases and render the truthful tool output instead.

* feat(web): open edit diffs in the right-side detail panel

Move the Edit/Write diff out of the inline tool-card body into the shared right-side detail layer, opened by clicking the card (like the subagent detail panel). The panel shows the line diff when it faithfully represents the operation, otherwise the tool output (replace_all, append, errors), so failed calls explain why no change was applied.

* feat(web): toggle detail panels closed when their trigger is clicked again

Clicking the same Edit/Write card, file link, or git-status area a second time now collapses the right-side detail panel, matching the existing thinking / compaction / subagent toggle behavior.

* fix(web): cap client-side edit diff to avoid freezing on large inputs

The line-level LCS allocates an (oldLines+1) x (newLines+1) matrix and runs eagerly when an Edit/Write card renders. For large edits (e.g. a 5k x 5k block, ~25M cells) this can freeze or exhaust the chat. Cap the matrix at 1M cells and fall back to the raw tool output beyond that.

* fix(web): keep the edit diff panel in sync with live tool state

Store only the tool id and re-derive the diff panel payload from the live tool call in the session turns, so a panel opened while the tool is still running reflects later status/output changes (e.g. an eventual error shows the failure output instead of the stale attempted diff).

* fix(web): do not render a synthetic diff for Write calls

Write only reports the new content, so the client cannot tell a new file from an overwrite of an existing one. A from-empty diff showed overwrites as all additions and no deletions, which is misleading. Fall back to the tool output for every Write (append already did).

* fix(web): also cap diff output rows, not just the LCS matrix

The matrix-size cap alone let through asymmetric edits (e.g. one line replaced by hundreds of thousands) whose small matrix still produced a huge row array that the chip computed on render. Cap each side's line count too, so the diff output is bounded.

* fix(web): keep edit/write cards expandable when the diff panel is unwired

Clicking an Edit/Write card opens the right-side diff panel, but the nested ChatPane in the side chat does not wire that event, so the click became a no-op and the card could no longer expand to show its output. Gate the panel behavior behind a toolDiffPanel prop (enabled only in the main conversation); elsewhere the cards expand inline as before.

* fix(web): close the tool diff panel when its target disappears

When a session resync removes the tool call an open diff panel is showing, toolDiffTarget becomes null but detailTarget stayed 'toolDiff', leaving an empty aside that Escape would not close. Gate sidePanelVisible on toolDiffVisible so the panel collapses once its target is gone.
2026-06-25 23:18:14 +08:00
qer
fe667d7c2e
fix(reload): re-inject plugin session-start reminder after /reload (#1086)
* fix(reload): re-inject plugin session-start reminder after /reload

Reload reloaded plugins and resumed the session, but the model kept seeing the stale plugin session-start reminder from before the reload, so plugin skill changes only took effect in a fresh session.

Append a fresh plugin_session_start reminder to the main agent after reload, gated on a new forcePluginSessionStartReminder flag that only the explicit /reload command sets, so config and experiment toggles that reuse the reload RPC do not spam the transcript.

* fix(reload): keep reload result fresh and neutralize stale plugin reminder

Append the plugin session-start reminder before constructing ResumeSessionResult so SDK callers reading getResumeState() see the refreshed plugin context instead of a pre-reload snapshot.

When a plugin with a prior plugin_session_start reminder is disabled or removed, append a neutralizing reminder so the model does not keep following stale plugin instructions.

* fix(reload): neutralize stale plugin reminder after compaction

A full compaction folds the discrete plugin_session_start reminder into a compaction_summary, so the origin-only scan no longer detects it. Also treat a compaction_summary in history as a signal to neutralize, so disabling or removing a plugin after compaction still emits a superseding 'no active plugin session starts' reminder.

* fix(reload): thread plugin reminder option through KimiHarness.reloadSession

KimiHarness.reloadSession is a public SDK entry point; forward forcePluginSessionStartReminder to both the active-session and RPC reload paths so SDK callers using the harness can opt into the refreshed plugin reminder too.
2026-06-25 23:16:55 +08:00
Haozhe
bf9b01e0d8
feat(server): make --host opt-in for LAN binding (#1105)
* feat(server): bind `kimi server` to 0.0.0.0 by default

- change DEFAULT_SERVER_HOST to 0.0.0.0 and default --insecure-no-tls on
  so a bare `kimi server run` clears the non-loopback TLS gate
- add LOCAL_SERVER_HOST (127.0.0.1) for connectable URLs when bound to the
  wildcard, used by lockConnectHost and the service status URL
- pin the OS-supervised install to --host 127.0.0.1 so the always-on
  service stays loopback-only

* feat(server): make --host opt-in for LAN binding

- default kimi server run/web binds 127.0.0.1 when --host is omitted
- treat bare --host as 0.0.0.0 while keeping --host <host> explicit
- update server help/banner text and host binding tests
2026-06-25 22:13:13 +08:00
liruifengv
2db5fc20ec
feat: add shell mode (!) to the CLI (#1079)
* feat: add shell mode (`!`) to the CLI

Add shell mode, letting users run shell commands directly from the prompt
with `!`. Output streams live into the transcript, supports backgrounding
(ctrl+b), cancellation (Esc / Ctrl+C), input queuing while running, and
enters the conversation context with resume support.

* feat(kimi-code): show shell mode label on editor border and add tip

Render a "! shell mode" label on the top-left of the editor border while the editor is in `!` bash mode, so the active mode is visible at a glance. Also add a rotating toolbar tip (`! to run a shell command`) to surface the feature.

* feat(kimi-code): refine shell mode queue, history, and display

- Keep `!` commands out of input history so they never resurface as bare text stripped of their `!`.

- Make `!` commands non-steerable: Ctrl-S skips them (they stay queued to run after the current task) and the steer hint is only shown when something is actually steerable.

- Render queued `!` commands with a `$` prompt and the shell-mode hue so they read as commands, not as text to send to the model.

- Echo executed shell commands with a `$` prompt instead of `!`.

* fix(kimi-code): sanitize shell output and harden rendering

Captured shell command output can contain terminal control sequences (colours, cursor moves, alternate-screen switches, OSC hyperlinks, carriage-return spinners, bells). pi-tui's Text passes strings straight to the terminal, so any unhandled sequence was executed by the terminal and fought with pi-tui's own cursor control, producing the blank-screen-plus-leftover-characters mess after running commands like pnpm dev or a nested TUI.

- Sanitize CSI (incl. private modes), OSC, single-char ESC and C0 control chars (keeping newline and tab) in both the finished/resume view (previously unsanitized) and the running tail.

- Make the sanitize, format, and ShellRunComponent render paths never-throw, and cap the live running buffer, so a misbehaving command cannot crash the TUI.

- Dispose transcript children on clear so ShellRunComponent's timer is released on /clear or session switch.

* fix(kimi-code): render shell command echo with $ instead of sparkles

The shell command echo is a 'user' transcript entry, so UserMessageComponent prefixed it with the USER_MESSAGE_BULLET (sparkles), producing 'sparkles $ command'.

Add an optional bullet override to UserMessageComponent / TranscriptEntry and set it to an empty string for the shell echo (both live and resume), so the '$ command' content sits at the leading column where the sparkles marker used to be. Normal user messages keep the sparkles bullet.

* fix(kimi-code): enter shell mode when pasting a !-prefixed command

The bash-mode trigger only handled the single ! keystroke, so a pasted !cmd was inserted as literal text in prompt mode and submitted as a normal message.

After pi-tui inserts pasted content, detect an empty-prompt buffer that now starts with !, switch to bash mode, and strip the leading ! so the buffer holds only the command, matching the typed ! path.

* fix(kimi-code): restore shell mode when recalling a queued command

recallLastQueued() dropped the queued item's mode, and the Up-arrow recall only restored the text. A queued ! command (queued while another command runs, which resets the editor to prompt mode) therefore came back as a normal prompt and was submitted as a message instead of a shell command.

Return the full QueuedMessage from recallLastQueued() and restore editor.inputMode (plus the onInputModeChange sync) from the recalled item's mode.

* feat(kimi-code): use violet as the shell mode color

Replace the claude-code-style magenta/rose shellMode token with a violet that is distinct from plan-mode blue, the user role amber, success green, error red, and the teal accent.

Custom themes that omit the token fall back to this new default via the base+overrides merge, so existing custom themes keep working unchanged.

* chore: refine the shell mode changeset

* docs: document shell mode

Add a Shell mode section to the interaction guide and list the ! and Ctrl+B shortcuts in the keyboard reference, in both English and Chinese.

* test(protocol): include shell events in volatile classification check

shell.output and shell.started were added as volatile event types for shell mode; update the snapshot test's volatile-type list and count accordingly.

* fix(agent-core): surface shell command failure reason with no output

When a ! shell command fails without producing stdout/stderr (non-zero exit with no output, timeout, spawn failure), the failure reason lived only in the tool result's output and the TUI showed '(no output)'. Fold it into stderr so the live view and replay show what went wrong.

* fix(kimi-code): decode CSI-u ! to enter shell mode

In terminals with the Kitty keyboard protocol (VSCode integrated terminal, Kitty), pressing ! arrives as a CSI-u sequence, so the raw normalized === '!' comparison never matched and shell mode could not be entered by typing !. Decode with printableChar before comparing, matching every other printable-key check in the TUI.

* fix(kimi-code): do not steer while a shell command is running

Ctrl-S steers queued input into the running turn, but a shell command is not an agent turn, so steering during streamingPhase === 'shell' would launch a turn before the command output is recorded. Keep Ctrl-S a no-op during shell runs; queued messages stay queued.

* fix(agent-core): escape bash tag delimiters in shell output

Shell command output is arbitrary text; if it contains a bash tag delimiter such as </bash-stdout>, the recorded pseudo-XML wrapper breaks and replay extracts the wrong slice. Escape the content when wrapping it in agent-core and unescape when extracting during replay, so output survives round-trip intact.

* docs: document the shellMode theme token

The shellMode color token was added to the palette but not propagated to its mirrors. Add it to the custom-theme docs token table, the theme JSON schema, and the custom-theme skill token list.

* feat(agent-core): reset background task deadline on detach

Add a resettable deadline timer to BackgroundManager and let tasks register a detach timeout; when a foreground task is moved to the background, its deadline resets to the background default counted from the detach moment.

Wire this into shell mode so ! commands run with a 3-minute foreground timeout and get 10 minutes once detached to the background, instead of staying bounded by the original 60-second foreground deadline.

* feat(agent-core): lower shell mode foreground timeout to 2 minutes
2026-06-25 21:24:53 +08:00
qer
3ea6ac278d
feat(web): render plan review card with plan body and approach choices (#1101)
* feat(web): render plan review card with plan body and approach choices

The ExitPlanMode plan_review approval in the web UI now renders the plan body as Markdown with one button per approach option, plus Revise and Reject-and-Exit, with the selected label threaded back to the server.

The approval header keeps APPROVAL REQUIRED and the minimize control on the title row and shows the plan path on a second line, and the plan body uses up to half the viewport height.

The ExitPlanMode tool card also gains a link to the plan file, currently hidden behind a flag until the server can read files outside the workspace.

* fix(web): hide misleading shortcut numbers on plan review actions

When a plan review has approach options, the option buttons already own [1]/[2]/[3]. Revise and Reject-and-Exit advertised the same numbers even though those keys approve an option, so hide their shortcut labels whenever options are present.
2026-06-25 19:25:23 +08:00
Haozhe
60dfb68a2d
feat(server): add bearer-token auth and safe host exposure (#1006)
* test(server): add API surface snapshot guardrail

Boot startServer on port 0 and snapshot the documented v1 route table derived from /openapi.json paths, plus the reachability of doc/meta endpoints (/healthz, /openapi.json, /asyncapi.json, /). Gives later auth/--host phases an intentional diff when routes change. M0 makes no production behavior change.

* test(server): add e2e server harness with token support

Add test/helpers/serverHarness.ts: boot() wraps startServer with an isolated lock + home dir and returns a handle (server, address, baseUrl, wsUrl, token, close) plus authedFetch/authedWs that carry Authorization: Bearer <token> (and the kimi-code.bearer.<token> WS subprotocol). serviceOverrides is the generic DI seam later phases use to inject a fixed-token auth service; IAuthTokenService is not referenced yet. closeAll() tears down every booted server and socket. M0 makes no production behavior change; typecheck-only gate.

* feat(server): add privateFiles 0600 atomic write/read utility

* feat(server): add per-start tokenStore

* feat(server): add env-based bcrypt password hash utility

* feat(server): add IAuthTokenService DI seam

* feat(server): add global onRequest auth hook with bypass + redaction

* fix(server): stop reflecting Host header in /asyncapi.json

* feat(server): add WS bearer subprotocol constant and parser

* feat(server): enforce bearer token auth on WS upgrade

* feat(server): add Host header allowlist middleware

* feat(server): add Origin/CORS middleware

* feat(server): wire Host/Origin checks into HTTP and WS

* feat(server): wire token auth, Host/Origin, and WS auth into start.ts

* fix(server): create lock file with 0600 permissions

* fix(server): suppress debug routes on non-loopback binds

* feat(kimi-code): read server token and send Authorization on CLI calls

* feat(kimi-code): inject server token into /web URL fragment

* feat(server): add bindClassify for loopback/lan/public classification

* feat(kimi-code): register --host flag and pass it through the daemon

* feat(server): require password and TLS opt-out on non-loopback binds

* feat(server): rate-limit repeated auth failures on non-loopback binds

* feat(server): disable shutdown and terminals on public binds by default

* feat(server): add security response headers on non-loopback binds

* test(server): cover LAN/public host-exposure hardening end to end

* docs(server): add deployment security and threat-model guide

* changeset: minor kimi-code for server auth and host exposure

* feat(kimi-web): add server bearer-token auth support

* fix: repair CI for server auth and host exposure

- Replace native @node-rs/bcrypt with pure-JS bcryptjs so the ESM CLI
  bundle and the SEA native bundle both build without native-addon
  require issues (node-rs/bcrypt broke the ESM smoke and the SEA
  check-bundle allowlist).
- Remove dead cleanup references (stopSpinner, authLogoBlinkTimer) in
  apps/kimi-web App.vue that failed vue-tsc.
- Fix lint: drop empty spread fallbacks in the e2e auth-header merge,
  void the intentionally-async WS upgrade listener, add missing
  assertions to satisfy jest/expect-expect, and convert a ternary
  statement to if/else.
- Send the bearer token in the snapshot perf/smoke tests so they pass
  under the new global auth hook.
- Refresh the pnpmDeps hash in flake.nix for the updated lockfile.

* feat(server): persist bearer token and add rotate-token command

- persist the server bearer token in <home>/server.token (0600) and reuse it across restarts instead of per-start server-<pid>.token
- add `kimi server rotate-token` to regenerate the token; the token store reloads on mtime/inode change so rotation applies without restart
- print the token and Vite-style Local/Network URLs in the startup banner
- allow non-loopback binds with bearer-token-only auth (password now optional) and update SECURITY.md
- surface daemon boot failures immediately with the exit reason and log tail instead of waiting for the spawn timeout

* feat(server): print full token URLs and re-print links after rotate

- Drop the ready-panel border so token URLs print in full for copying; keep the Kimi sprite beside the title.
- Re-print Local/Network access links after `server rotate-token` (host/port from the lock).
- Extract shared access-URL helpers into access-urls.ts.
- Unify link and token colors between the banner and rotate-token.

* feat(server): dim URL #token= fragment and de-highlight token

- Render the `#token=…` fragment in a dim gray so the host/port stands out in the banner and rotate-token links.
- De-highlight the standalone token; set it off with surrounding whitespace instead of color.
- Add splitTokenFragment helper.

* refactor(cli): polish server ready banner and rotate-token output

- move version onto the ready banner title line; drop the separate
  Ready:/Version: rows and the startup-time metric
- reorder rotate-token output so the new token sits between the
  invalidation note and the access links
- update server CLI tests for the new layout

* feat(server): warn on reuse and refine ready banner

- Warn when `server run` reuses an already-running daemon (its options are not applied) and show the running server's actual URLs.
- Show a `Network: off  use --host 0.0.0.0 to enable` hint on loopback binds.
- Move the version onto the title line and drop the startup-time metric.

* fix(web): relabel auth dialog to token and cover full page

- Relabel the server auth dialog from "password" to "token"; the server accepts the bearer token, with the password only as a fallback.
- Make the auth dialog overlay fully opaque so it covers the whole page instead of revealing the login page underneath.

* fix: resolve CI failures on web auth PR

- Replace chalk.yellow named color with chalk.hex(darkColors.warning)
  in the server reuse notice to satisfy the chalk named color guard.
- Update pnpmDeps hash in flake.nix to match the regenerated
  pnpm-lock.yaml so the Nix build succeeds.
- Retry rmSync in ws-broadcast e2e teardown to ride out EBUSY /
  ENOTEMPTY races while the server flushes files after close().

* test(server): update API surface snapshot for warnings route

The feat/web-auth branch adds GET /api/v1/sessions/{session_id}/warnings
(packages/server/src/routes/sessions.ts), so the API surface guardrail
snapshot needs to record the new documented v1 route.
2026-06-25 17:57:56 +08:00
qer
d6e524682d
perf(web): page session list per workspace on first load (#1084)
* perf(web): page session list per workspace on first load

Load only the first page of sessions per workspace instead of draining every session up front, so the initial request count scales with the number of workspaces rather than the total number of sessions. The per-workspace "show more" button now fetches the next page on demand, and searching lazily loads the full list so results stay complete.

To keep per-workspace paging working for sessions created with cwd only, the workspace registry now also surfaces directories that have sessions but were never explicitly registered.

* fix(web): trust server hasMore for session pagination

Stop deriving per-workspace hasMore from the workspace session_count. After a local archive/delete the count is stale (archiveSession only removes the local session), so loadedCount < total stayed true after the server returned its final page and re-fetched empty pages forever. The server's page.hasMore is authoritative for whether more pages exist, so use it directly and keep session_count only as a label total.

* fix(web): fall back to global session walk when no workspaces listed

When /workspaces is unavailable or empty on older or partially-failing daemons while /sessions still works, the per-workspace initial load produced no sessions and the sidebar rendered blank. Reuse the existing global walk as a fallback in that case so history still shows.

* fix(agent-core): keep workspace deletion durable

Record deleted workspace ids as tombstones in the registry and skip them during derived registration. Deleting a workspace only removes its registry entry (session buckets stay on disk by design), so without the tombstone the next derived-registration scan recreated the workspace, making deletion non-durable for any workspace with history. An explicit re-add clears the tombstone.

* fix(web): track session paging cursor per workspace

Compute the load-more cursor from the end of the last fetched page instead of the oldest loaded session. A deep-linked older session appended out of band would otherwise become the cursor, so the next page started after it and skipped every session between the first page and the deep link.

* fix(web): load more sessions in the mobile switcher

The mobile switcher still used the old local display-expansion logic, but each workspace now starts with only the first page of sessions. Wire its show-more button to loadMoreSessions (matching the desktop sidebar) so workspaces with more sessions can page beyond the first page on mobile.

* fix(agent-core): align derived workspace id with its session bucket

Session buckets are keyed by normalizeWorkDir (resolve, not realpath), so registering a derived workspace via createOrTouch (which realpaths) produced a different workspace id for a symlinked cwd, and per-workspace session lookups then read the wrong bucket and returned empty. Register derived workspaces with the resolved bucket key instead.

* fix(agent-core): skip archived-only buckets in derived registration

A bucket that only contains archived sessions would otherwise be registered as an empty workspace on a plain GET /workspaces, surfacing an empty sidebar group that the old session-derived fallback (which ignored archived sessions) kept hidden. Skip derived buckets with no active sessions.

* refactor(agent-core): derive workspaces from the session index on the fly

Stop persisting derived workspaces into the registry. Persisting made the registry a second data source that drifted from the session store (symlinked cwds, archived-only buckets, deleted/unmounted roots), each producing a bug. Instead compute derived workspaces fresh in list() from the session index and resolve their ids in resolveRoot() via the same index, so the session store stays the single source of truth. The deletion tombstone is kept so explicitly removed workspaces are not re-derived.

* fix(agent-core): tombstone derived workspaces on delete

After deriving workspaces from the session index, derived ids are valid list results but absent from the registry file, so delete() threw before writing the tombstone and DELETE on a derived workspace 404ed and reappeared on the next list(). Resolve the derived id and write the tombstone for it too.

* fix(web): use local session count once a workspace is fully loaded

mergedWorkspaces kept the server session_count as a floor even after a workspace had no more pages, so archiving the last session left the header showing 1 until a reload. Once a workspace is fully loaded (hasMore === false) the local count is exact, so prefer it; keep the server count as a floor only while pages remain.
2026-06-25 17:12:34 +08:00
qer
0030f76c5c
feat(tui): confirm before installing third-party plugins (#1088)
* feat(tui): confirm before installing third-party plugins

* chore: add changeset for third-party plugin install confirmation

* docs: note third-party plugin install confirmation prompt

* fix: harden third-party plugin install confirmation
2026-06-25 13:48:23 +08:00
qer
f1fad7222c
fix: reduce streaming stutter in the web chat (#1085)
Some checks are pending
CI / test (push) Waiting to run
CI / lint (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 / Publish native release assets (push) Blocked by required conditions
* fix(web): coalesce streaming token updates into one render per frame

* fix(server): disable Nagle on WebSocket socket for lower streaming latency

* fix(web): flush pending streaming deltas before re-subscribing

* fix(web): flush pending streaming deltas before forgetting a session
2026-06-25 13:06:38 +08:00
liruifengv
8ee5c0ff81
fix(kimi-code): avoid terminal focus flicker on Linux Wayland (#1094)
The focus-driven clipboard image hint spawned wl-paste/xclip on every
terminal focus event. On Wayland this perturbs seat focus and re-triggers
the focus event, creating a feedback loop that made the terminal window
repeatedly gain and lose focus and broke IME input.

Limit the probe to macOS and Windows, which use the in-process native
module and do not perturb focus. Image paste is unaffected.

Resolve #1090
2026-06-25 12:59:01 +08:00
qer
ea03f30e51
feat(web): render LaTeX math in chat via KaTeX (#1035)
* feat(web): render LaTeX math in chat via KaTeX

* fix(web): keep literal prose dollars out of KaTeX inline math

Enabling KaTeX turned plain prose with two dollar-prefixed tokens
(`Check $PATH before $HOME`, `costs $5 and $10`) into a single
inline formula, since markstream's $…$ tokenizer has no
"no whitespace inside the delimiters" rule.

Add a postTransformTokens guard that turns a single-$ inline span back
into literal text when its content starts or ends with whitespace. Real
inline math is written tight (`$E=mc^2$`, `$\frac{1}{2}$`), while
the prose false-positives always have whitespace inside the delimiters,
so this keeps inline/block math working while leaving prices, env vars,
and ranges as readable text. Code spans are already excluded by the
tokenizer, and running on the flat token stream also covers dollars
nested inside lists and blockquotes.

Addresses the Codex review comment on PR #1035.

* fix(web): reject compact currency ranges before rendering math

The literal-dollar guard only caught prose whose content had whitespace
inside the delimiters, so a compact range like `costs $5/$10` still
rendered `5/` as a formula and dropped the second dollar. (markstream's
own currency check rejects `-`/`~` ranges but not `/`.)

Extend the guard to also reject a single-$ span whose content is a
numeric amount with a trailing range connector (`/`, `-`, `~`,
en/em dash) -- a complete formula never ends in a dangling operator.
Scoped to digit-led content so symbolic math is left alone, and numeric
math that is not a range (`$5/2$`, `$5-2$`, `$0.5$`) still
renders. Added tests for the range cases and the non-range math.

Addresses the follow-up Codex review comment on PR #1035.

* fix(web): treat shell/path dollar pairs as literal text

Adjacent shell variables and PATH-like values (`Use \$HOME/bin:\$PATH`,
`\$PATH:\$HOME`) were still rendered as math, because the prose-dollar
guard only looked at the span's own content (whitespace inside the
delimiters, or a trailing numeric range connector) and never at what
touches the delimiters from the outside.

Replace the two bespoke heuristics with the two industry-standard rules,
now driven by the surrounding text tokens:

  - Pandoc (tex_math_dollars): no whitespace immediately inside the
    delimiters.
  - GitHub: each \$ must be bounded on its outer side by whitespace, a
    line boundary, or structural punctuation. A letter or digit there
    means a second prose token, so the span is literal text.

The GitHub outer-boundary rule subsumes the old numeric-range check (a
closing \$ in \$5/\$10 is followed by a digit) and also catches
shell/path cases Pandoc's inner rule misses. Normal math -- including
bare \$x\$, \$x^2\$., and (\$x^2\$) -- still renders. Added
tests for shell/path values and punctuation-wrapped math.

Addresses the third Codex review comment on PR #1035.

* fix(web): render math next to CJK punctuation and quotes

The outer-boundary guard only accepted ASCII punctuation, so a formula
followed by full-width punctuation or wrapped in typographic quotes was
misclassified as prose: `公式为 \$E=mc^2\$,其中` and `“\$x\$”`
showed raw dollars instead of rendering.

Invert the boundary check from an allow-list of ASCII punctuation to a
deny-list of ASCII letters/digits. A \$ glued to an ASCII letter/digit
still means a second prose token (\$PATH:\$HOME, \$5/\$10), but
whitespace, line boundaries, and every other character -- full-width
punctuation, CJK ideographs, curly quotes -- is now a valid math
boundary, which is the correct behavior for localized prose.

Addresses the fourth Codex review comment on PR #1035.

* fix(web): preserve later math after literal-dollar spans

A prose dollar in front of a real formula in the same inline run
(`costs $5 and formula $x$`, `Use \$HOME before $E=mc^2$`) exposed
the core limit of the token-level guard: markstream's tokenizer greedily
pairs the first literal \$ with the formula's opening \$ before any hook
runs, so converting that span back to text could only blank it -- the
later formula's opening \$ was already consumed and the formula rendered
as raw text.

Move the guard from postTransformTokens to a source-level preprocessor
that runs before tokenization. escapeProseDollars protects code spans,
fenced code blocks, and \$\$…\$\$ display math, then pairs single \$
delimiters using the Pandoc (tight delimiters) and GitHub-style
outer-boundary rules: any \$ without a valid partner is escaped as
\\\$, so the tokenizer leaves it literal while real formulas -- including
ones that come after a prose dollar -- still parse as math.

The component now preprocesses each markdown segment's text and the
postTransformTokens hook is gone. Rewrote the tests around the
string-in/string-out helper, including the prose-before-formula case,
code spans, fenced code, and block math.

Addresses the fifth Codex review comment on PR #1035.

* fix(web): protect indented code blocks before escaping dollars

The dollar-escaping preprocessor stashed fenced code blocks, inline code,
and display math, but not 4-space / tab indented code blocks. So a
snippet like `    echo \$HOME` had its dollar rewritten to `\\$HOME`,
and because Markdown renders backslashes literally inside code, the web
chat corrupted the code to show a stray backslash.

Add an indented-code regex and protect those lines too. Also make the
placeholder restore iterative, so nested protected regions (e.g. inline
code that looks like display math) restore correctly instead of leaving
a placeholder behind.

Addresses the sixth Codex review comment on PR #1035.

* fix(web): do not treat list-continuation lines as indented code

The indented-code regex protected every 4-space line, but inside a list
item a 4-space indent is a normal continuation paragraph, not a code
block (code under a list marker needs deeper indentation). So a message
like `- total\n    costs \$5 and \$10` had that nested line
stashed as "code", leaving its dollars un-escaped -- and the KaTeX
parser then rendered the price range as math.

Narrow the indented-code rule to a run of 4-space / tab lines that is
preceded by a blank line (or the start of the text). That still protects
real top-level indented code blocks and deeper-indented code inside
lists, while letting 4-space list-continuation lines get their dollars
escaped.

Addresses the seventh Codex review comment on PR #1035.

* refactor(web): render only $$…$$ display math, drop single-$ inline

Enable KaTeX for display math only: disable markstream's inline math rule
(`md.inline.ruler.disable('math')`) via customMarkdownIt, leaving the
`math_block` rule for $$…$$. Single $ now stays literal everywhere, so
prices, env vars, shell paths, and code are never mis-rendered as math --
with no escaping, no code detection, and no preprocessor.

This removes the escapeProseDollars normalization layer and all of its
code-protection machinery (the 8 review comments it attracted were
symptoms of trying to make a lax single-$ tokenizer behave). Display
$$…$$ math continues to render via KaTeX.

Changeset updated to describe display-math-only support.
2026-06-25 12:47:40 +08:00
qer
884b65a040
fix(web): coalesce snapshot reloads on resync (#1087)
Avoid concurrent session snapshot requests when resync_required fires repeatedly, while still allowing one queued rerun after the in-flight reload settles.
2026-06-25 11:49:05 +08:00
qer
3554f7e7d6
feat(plugins): source Superpowers from GitHub and show update badges (#1066)
Some checks are pending
CI / test (push) Waiting to run
CI / lint (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 / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(plugins): source Superpowers from GitHub and show update badges

Source the Superpowers plugin from its GitHub release (v6.0.3) instead of a vendored copy, and drop the explicit version field.

Derive marketplace entry versions from GitHub source URLs when the version field is omitted, keeping the source URL the single source of truth.

Show update badges for installed plugins on the /plugins Installed tab.

* docs(plugins): document Installed tab update badges

* fix(plugins): stamp GitHub source version in CDN catalog

Older CLIs only read the explicit marketplace version and cannot derive it from a GitHub source URL. When publishing the CDN catalog, stamp the version derived from a pinned GitHub source so those clients still surface update badges.

The source plugins/marketplace.json keeps no explicit version; the version is derived at build time instead.

* feat(plugins): resolve latest version for bare GitHub sources at runtime

Point the Superpowers marketplace entry at the bare GitHub repo URL so it tracks the latest release instead of a pinned tag.

When a marketplace entry omits version and its source is a bare GitHub repo URL, resolve the latest release tag at load time (via the /releases/latest redirect) to fill the version for update detection.

Revert the build-time version stamping; it is no longer needed. Older CLIs that only read the explicit catalog version will no longer see update badges for Superpowers, since the catalog no longer carries one.

* feat(plugins): make Enter update and add I for details on Installed tab

On the Installed tab, Enter now installs the available update when one is present, and falls back to opening plugin details otherwise.

Add the I key to always open plugin details, so details remain reachable when Enter is occupied by an update. Update the installed hint, docs and changeset accordingly.

* feat(plugins): show installing state inside the plugins panel

Move the "Installing … from marketplace" notice from a transient status message into the plugins panel itself, so the user sees progress in the interactive card while an install or update is in flight.

* feat(plugins): highlight reload hint and add dev:cli:marketplace

Highlight "Run /new or /reload to apply plugin changes." in warning color after plugin install and remove, and make the two notices symmetric.

Add a root dev:cli:marketplace script that points the dev CLI at the production marketplace instead of the local dev server.

* fix(plugins): dedupe install success notice

Drop the redundant showNotice on marketplace installs so the success message is shown only once, symmetric with remove.

* fix(plugins): reset installing state on install failure

When a marketplace or Custom-tab install rejects, clear the installing state and return to the list so the user can retry, instead of leaving the panel stuck on the one-way "Installing…" view.
2026-06-24 21:58:13 +08:00
liruifengv
a86bb9757d
fix(kimi-code): show clipboard image paste hint only for newly copied images (#1072)
* fix(kimi-code): show clipboard image paste hint only once per image

The footer hint repeated on every terminal focus whenever an image remained in the clipboard, which became noisy. Replace the 30s time-based cooldown with a per-image gate: the hint shows once for a given image and stays quiet until the clipboard is observed empty and a new image appears.

* fix(kimi-code): suppress clipboard image hint for images present at startup

The footer hint fired during initialization whenever an image was already in the clipboard, treating it as new. The first clipboard observation after start now only establishes a baseline, so only images copied during the session trigger the hint.

* fix(kimi-code): show hint for first image copied after startup

* fix(kimi-code): make clipboard image probe non-blocking

The startup baseline probe in ClipboardImageHintController calls clipboardHasImage(), which on Linux/WSL ran wl-paste/xclip/powershell via spawnSync. The probe only reaches its first await after those synchronous calls, so a slow or wedged helper could freeze the TUI launch for up to the 1s-2s tool timeouts even when the user never focuses with an image.

Add an async runCommandAsync built on spawn with timeout-based kill, and route the Linux/WSL image detection through it so the event loop is never blocked. Keep the synchronous runCommand for the explicit paste-read path.

* chore: add changeset for non-blocking clipboard probe

* chore: simplify clipboard image hint changeset
2026-06-24 21:07:14 +08:00
liruifengv
3aaf1e5803
fix(kimi-code): bump native clipboard dependency to fix Linux startup crash (#1075)
* fix(kimi-code): bump native clipboard dependency to fix Linux startup crash

* chore(nix): update pnpmDeps hash
2026-06-24 21:07:02 +08:00
liruifengv
75ca3b2160
feat(tui): add Ctrl+U/Ctrl+D paging in the task output viewer (#1078)
PgUp/PgDn are often captured by terminal or tmux scrollback, so add Ctrl+U and Ctrl+D as full-page up and down alternatives, matching the existing PgUp/PgDn behavior.
2026-06-24 21:00:40 +08:00
liruifengv
500677ab8b
fix(tui): clear editor draft on Ctrl-C during compaction (#1076)
When compaction is in progress and the editor has a draft, Ctrl-C now clears the draft first instead of cancelling compaction, matching the streaming behavior. The clear-text logic is shared between the compaction and streaming branches.
2026-06-24 20:30:20 +08:00
liruifengv
b62b3a147f
feat(kimi-code): show cache read details in debug timing (#1074)
* feat(kimi-code): show cache read details in debug timing

* chore: remove changeset
2026-06-24 19:39:42 +08:00
Haozhe
ff177155ca
fix(web): stop auto-dismissing pending questions and approvals on a timeout (#1070)
* fix(web): stop dismissing questions after a 60 second timeout

The server's question broker auto-expired AskUserQuestion requests after 60s, which dismissed the question even when the user simply needed more time. Remove the timeout, and the now-unused expires_at field, so a question stays pending until the user answers or explicitly dismisses it.
2026-06-24 19:15:06 +08:00
liruifengv
d18aa1666a
perf(tui): reuse streaming markdown instances (#1069) 2026-06-24 16:08:39 +08:00
ForgottenR
bbd8a1a947
fix(cli): resolve spawn EFTYPE on Windows for kimi web and /web (#903)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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 / Publish native release assets (push) Blocked by required conditions
* fix(cli): resolve spawn EFTYPE on Windows for kimi web and /web

* chore(changeset): clarify affected Windows installation methods

---------

Co-authored-by: qer <wbxl2000@outlook.com>
Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-06-24 15:27:00 +08:00
qer
5ef66ddfed
feat(tui): redesign /plugins as a tabbed panel (#1025)
* feat(tui): redesign /plugins as a tabbed panel

Split the /plugins manager into Installed / Official / Third-party /
Custom tabs. The Official and Third-party marketplace catalogs load
lazily, so /plugins opens instantly and keeps working offline, with
fetch failures shown inline instead of closing the panel. The tab strip
is shared with the /model provider tabs via the new renderTabStrip
helper.

* fix(tui): show untiered marketplace entries and update badges

Address Codex review feedback on the /plugins tab redesign:

- Untiered marketplace entries (no `tier` field) now appear on the
  Third-party tab instead of being invisible in both marketplace tabs.
- Installed plugins whose marketplace version is newer than the local
  version render an `update <local> → <latest>` badge again, and
  up-to-date plugins show `installed · v<version>` — restoring the
  update visibility the pre-redesign marketplace UI had.

* fix(tui): decode Space for installed-plugin toggle

In terminals that send printable keys via Kitty/CSI-u sequences (e.g. VS
Code's integrated terminal), the Space key arrives as a printable char
rather than a Key.space match, so the Installed-tab Space toggle silently
stopped working. Check both matchesKey(Key.space) and the decoded
printable char to match the MCP selector and other dialogs.

* fix(tui): open custom marketplaces on the Third-party tab

When `/plugins marketplace <source>` points at a custom catalog whose
entries omit `tier`, those entries are classified into the Third-party
tab. Opening on Official left the visible tab empty and Enter could not
install anything, unlike the old marketplace picker which showed all
entries from the supplied source. Open on Third-party when a custom
source is supplied; the default catalog still lands on Official.

* docs(plugins): drop open-url wording and hyphenate Shift-Tab

Address Codex review feedback:

- The marketplace Enter action is install/update only (open-url rows were
  removed), so say "install or update" instead of "open or install" and
  drop the leftover changeset sentence about setup URLs.
- Use `Shift-Tab` (hyphen) instead of `Shift+Tab` to match the docs
  typography convention.

* fix(tui): keep marketplace selection valid while loading

When the Official/Third-party catalog is still loading, `entries` is empty
and pressing ↓ computed `Math.min(-1, selectedIndex + 1)` = -1. The later
Enter then read `entries[-1]` and the first install silently did nothing.
Clamp the index to 0 while there are no entries.

* fix(tui): count tab separators in tab-strip fit check

renderTabStrip declared a strip to fit whenever the sum of tab cell widths
fit, but the returned string also inserts single spaces between tabs via
`segments.join(' ')`. At widths around 43-45 columns for a four-tab strip
this declared a fit while the joined line was wider, so the trailing tab
got truncated instead of showing the `<`/`>` scroll markers. Count the
inter-tab separators in both the full-fit check and the scrolling window
fit check.

* docs(plugins): fix Kimi Datasource redirect anchor

The datasource.md redirect pointed at ./plugins.html#kimi-datasource, but
plugins.md no longer has a `## Kimi Datasource` heading — it is now
`## Official Plugins`. Update the en/zh redirect targets and fallback
links to #official-plugins / #官方插件 so the link lands on an existing
anchor.

* docs(plugins): restore concise Kimi Datasource section

The `## Official Plugins` section had replaced the original
`## Kimi Datasource` section, leaving the datasource.md redirect pointing
at a missing anchor and the Datasource capabilities/usage unreachable.
Restore a concise `## Kimi Datasource` section (intro + OAuth login +
install steps + usage) in both en and zh so the #kimi-datasource anchor
is valid again and the content is reachable.

* docs(plugins): restore Installing-from-GitHub subheading

The tab-redesign rewrite had dropped the `### Installing from GitHub` /
`### 从 GitHub 安装` subheading and its lead sentence, leaving only the
four URL forms. Restore the heading and lead sentence in both en and zh.

* docs(plugins): expand Kimi Datasource and tidy marketplace docs

- Condense the Official / Third-party / Custom tab overview and trust-badge note

- Trim the custom marketplace JSON section to the minimal id + source shape

- Move and expand the Kimi Datasource section with install, usage, and coverage

* docs(plugins): fix heading style and drop Next steps section

- Use sentence case for the Datasource headings (How to use, What you can do)

- Rename the Datasource caveat heading to Billing and limitations / 计费与限制 to avoid a duplicate Notes / 注意事项 anchor

- Remove the Next steps section, which linked back to the on-page Datasource anchor

* fix(tui): repaint plugins panel from current theme palette

The /plugins panel and MCP selector captured a palette snapshot at construction. In auto theme mode, applyResolvedAutoTheme swaps currentTheme.palette and re-renders without remounting the open panel, so it kept stale colors until closed.

Read currentTheme.palette during render instead, drop the colors opt from both components and their call sites, and add a regression test that switches palettes on a mounted panel.

* fix(tui): repaint model tab strip from current theme palette

TabbedModelSelectorComponent cached a palette snapshot in opts and used it only for the tab strip. In auto theme mode the inner model list repaints from currentTheme but the strip kept the old colors until the dialog was closed.

Read currentTheme.palette on the render path instead, drop the colors opt and its three call sites, and add a regression test that switches palettes on a mounted selector and asserts the strip repaints. This removes the last palette snapshot among editor-replacement dialogs.
2026-06-24 13:12:28 +08:00
Kai
66640380eb
feat: replace silent AGENTS.md truncation with a visible warning (#1040)
Oversized AGENTS.md files are no longer silently truncated. The full
content is injected, and a warning is shown in the TUI status bar and the
web UI when the combined AGENTS.md size exceeds the recommended 32 KB.

A generic session-warnings API backs this so future warning types can be
added without changing the API surface.
2026-06-24 12:26:17 +08:00
github-actions[bot]
0bcd9843c1
ci: release packages (#997)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-24 12:13:38 +08:00
qer
98d3e5b71d
feat(web): stabilize and drag-reorder workspaces in the sidebar (#1047)
* feat(web): stabilize and drag-reorder workspaces in the sidebar

* fix(web): preserve dragged workspace order after refresh

* fix(web): float session to top of its group on new message

* fix(web): align workspace drop order with insertion marker

* fix(web): allow dropping a workspace after the last item

* fix(web): use reordered workspaces for active fallback

* fix(web): honor drag order for next-workspace fallback on removal
2026-06-24 12:06:32 +08:00
qer
b93e9365b6
fix(web): stop auto-approving plan reviews and sensitive files in yolo mode (#1056)
The web app ran a client-side policy that auto-approved every approval request in auto/yolo mode, including plan reviews, sensitive file access, and other asks the daemon intentionally sends for user confirmation. The daemon already resolves auto/yolo server-side, so drop the client-side auto-approve and let those requests reach the approval UI.
2026-06-24 11:21:21 +08:00
qer
ac1882fe28
feat(web): persist collapsed workspace groups to localStorage (#1045) 2026-06-23 22:45:24 +08:00
qer
9d197e0f67
fix(web): make clipboard copy work over plain HTTP (#1044)
* fix(web): make clipboard copy work over plain HTTP

The Clipboard API (navigator.clipboard) is only exposed in secure contexts. When the web UI is served over plain HTTP, every copy action threw synchronously and silently failed. Route all copy call sites through a helper that falls back to execCommand('copy') in insecure contexts, and surface success or failure feedback to the user.

* test: address review feedback and a flaky goal-badge test

- clipboard test: drop the jsdom environment and mock the small navigator/document surface in the default node environment, per the kimi-web "pure logic tests only" rule.

- footer-goal-badge test: assert the absence of the "[goal" badge instead of the bare "goal" substring, which could match a rotating working tip ("/goal ...") and fail depending on Date.now().
2026-06-23 22:21:10 +08:00
qer
27df39c7ed
fix(web): fall back to session abort for stale prompts (#1043)
* fix(web): fall back to session abort for stale prompts

* test(web): cover stale prompt abort fallback
2026-06-23 22:05:08 +08:00
qer
dc6b9ef02b
feat(web): show dev-mode indicator in sidebar (#1042)
Tint the sidebar logo yellow and append the connected backend host:port to the title when the page is served by the Vite dev server, so local development tabs are easy to tell apart. Inert in production.
2026-06-23 21:41:47 +08:00
liruifengv
be77d5da03
feat(kimi-code): show clipboard image paste hint in footer (#1028)
* feat(kimi-code): add lightweight clipboard image detection

* fix(clipboard): correct Linux X11 image detection and extract shared helpers

- Extract shared clipboard constants/helpers into clipboard-common.ts

- Fix Linux X11 branch calling macOS-only osascript

- Use native hasImage() on Linux X11, macOS, and Windows with fallbacks

- Compute xclip result once and reuse on Linux

- Add test coverage for unsupported MIME types, empty targets, failures, WSL, and native fallbacks

* fix(kimi-code): restore Wayland/WSL xclip fallback in clipboard image detection

* feat(kimi-code): add clipboard image hint controller

* fix(kimi-code): clipboard image hint focus race and cleanup

* fix(kimi-code): prevent clipboard image hint from clearing unrelated hints and stale reads

* fix(clipboard-image-hint): lifecycle issues and platform-dependent tests

* fix(kimi-code): invalidate pending clipboard hint read on stop

* feat(kimi-code): wire clipboard image hint controller into TUI

* style(kimi-code): wrap void expression in braces to fix lint warning

* style(kimi-code): prefer nullish coalescing in clipboard image detection

* chore: add changeset for clipboard image footer hint

* fix(kimi-code): let clipboard image hint observe non-consuming focus events

* fix(kimi-code): extend clipboard image hint display duration to 4 seconds

* docs: mention clipboard image footer hint in interaction guide

* chore: downgrade clipboard image hint changeset to patch

* Revert "docs: mention clipboard image footer hint in interaction guide"

This reverts commit 0fd50dcc9b.

* fix(cli): avoid treating copied Finder files as images on macOS

Filter file-like native clipboard formats in clipboardHasImage() before

calling native hasImage(), mirroring the guard already used by

readClipboardMedia(). This prevents copied Finder files from being

mis-detected as pasteable images because macOS exposes their

thumbnails as image data.

* fix(cli): align image detection with paste path on macOS and Windows

Remove osascript and PowerShell fallbacks from clipboardHasImage() on

macOS and Windows. The paste reader (readClipboardMedia()) only uses the

native clipboard module for images on those platforms, so detecting images

via methods the reader cannot consume produced misleading footer hints.

Linux fallbacks (wl-paste / xclip / PowerShell under WSL) remain because

they match the actual paste path.

* fix(tui): do not truncate inline image escape sequences

UserMessageComponent applied truncateToWidth() to every rendered line,

including the Kitty / iTerm2 inline image escape sequences produced by

ImageThumbnail. pi-tui treats the embedded base64 payload inside those

sequences as visible text, so truncation chopped the escape code and left

behind '0m...' garbage instead of the image.

Skip truncation for lines that contain an inline image protocol sequence;

the image already respects maxWidthCells via ImageThumbnail.

* fix(tui): clear stale rows when content shrinks

Enable pi-tui's setClearOnShrink so that when a tall inline image is

replaced by shorter content (e.g. after sending a message), the terminal

rows the image previously occupied are cleared. Without this, pi-tui's

differential renderer can leave behind artifacts such as duplicated input

boxes.

* chore: add changesets for inline image rendering fixes

* test(cli): stabilize pi-tui capability mocks in concurrent test runs

* test(cli): use setCapabilities instead of mocked getCapabilities
2026-06-23 20:49:01 +08:00
qer
866b91c8f5
refactor(web): group components into area subdirectories (#1036)
Move the 40 feature-specific components out of the flat components/ into
chat/, settings/, dialogs/, and mobile/ subdirectories, leaving 9 shared
layout components at the top level. Recompute every relative import (no
path alias in the web app), refresh the line-1 path comments, and update
the layout description in AGENTS.md.

No behavior change; typecheck / test / build / lint all pass.
2026-06-23 20:34:46 +08:00
liruifengv
b1e6b64319
feat(tui): show working tips behind composing spinner (#1033)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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 / Publish native release assets (push) Blocked by required conditions
* refactor(tui): extract toolbar tip constants to tui/constant/tips.ts

* feat(tui): add WORKING_TIPS subset to tip constants

* feat(tui): allow MoonLoader to render a dim tip suffix

* feat(tui): pass optional tip through ActivityPaneComponent

* feat(tui): show working tip behind composing spinner

* feat(tui): hide working tip when spinner line does not fit

* chore: add changeset for working tips

* feat(tui): guard setAvailableWidth to skip unchanged widths

* feat(tui): show working tips on moon loader and compaction

* feat(tui): use singular 'Tip:' label for loading tips

* fix(tui): keep the same loading tip across waiting/thinking/tool/composing within one turn

* feat: show contextual working tips behind loading spinners

- Add pickRandomWorkingTip() for per-step tip selection

- Cache tips by loading kind (moon/composing) so continuous tool bursts keep the same tip

- Update tip inventory with /web, /plugins, /goal, /sessions, etc.

- Add unit tests for random tip selection

* docs: update working-tips changeset summary
2026-06-23 20:12:55 +08:00
qer
603a7679de
refactor(web): extract attachment upload into a composable (#1034)
Move the image/video attachment state, the file-picker / paste / drag-drop
handlers, the upload machinery, the preview lightbox, and the paste-listener
+ object-URL cleanup lifecycle out of Composer into useAttachmentUpload.

The composer keeps handleSubmit / handleSteer (which read the attachments to
build the payload) and the hasUpload toolbar flag; it consumes the returned
refs and handlers directly. The destructured names match the originals so the
template bindings are unchanged. handleSubmit / handleSteer now call the
composable's clearAfterSubmit() to revoke object URLs and drop the list.

Composer.vue: 1937 -> 1787 lines. Adds unit tests for useAttachmentUpload. No
behavior change.
2026-06-23 19:52:03 +08:00
qer
2bfd6860e4
refactor(web): extract composer text + draft persistence into a composable (#1031)
Move the composer's text ref, textarea ref, autosize helper, the per-session
draft load/save watchers, and the loadForEdit handle into useComposerDraft.
The returned text/textareaRef/autosize refs are passed straight through to the
history / slash / mention composables as their deps, so the rest of the
component is unchanged.

Composer.vue: 1987 -> 1937 lines. Adds unit tests for useComposerDraft. No
behavior change.
2026-06-23 19:14:50 +08:00
qer
a753b0535e
fix(web): upgrade markstream-vue to 1.0.3 to fix blank nested code blocks (#1032)
* fix(web): upgrade markstream-vue to 1.0.3 to fix blank nested code blocks

* fix(web): update flake pnpmDeps hash after markstream upgrade
2026-06-23 19:09:42 +08:00
qer
661c1fbe5b
refactor(web): extract @-mention menu into a composable (#1030)
Move the @-mention menu's open/items/active/loading state, the @token
detection, debounced search, and insertion logic out of Composer into
useMentionMenu. The composer keeps the keydown orchestration (it also
juggles the slash menu and history recall) and consumes the returned refs
directly; the destructured refs are aliased back to the original names so
the rest of the component is unchanged.

Move the FileItem view type into types.ts (mirroring the FileData move) so
the .ts composable can import it without hitting the type-aware lint rule
against importing types from .vue files; MentionMenu re-exports it for the
existing .vue consumers.

Composer.vue: 2035 -> 1987 lines. Adds unit tests for useMentionMenu. No
behavior change.
2026-06-23 18:38:50 +08:00
qer
318c964f07
refactor(web): extract slash-command menu into a composable (#1026)
Move the slash menu's open/items/active state, the filter logic, and item
selection out of Composer into useSlashMenu. The composable takes the text
ref, textarea ref, autosize, a skills getter, and the emit/history-push
callbacks as deps.

The composer keeps the keydown orchestration (arrow keys, Enter/Tab, Escape)
because it also juggles the mention menu and history recall; it consumes the
returned open/items/active refs directly and calls update/select. The
destructured refs are aliased back to the original names so the rest of the
component is unchanged.

Composer.vue: 2058 -> 2035 lines. Adds unit tests for useSlashMenu. No
behavior change.
2026-06-23 18:17:48 +08:00
qer
83384ee6d4
fix(web): persist input history so recall works after the first message (#1015)
* fix(web): persist input history so recall works after the first message

The composer has two mutually-exclusive instances: the empty-session
composer and the docked composer. The first message of a new session is
sent by the empty composer, which unmounts as soon as the first turn
appears; the docked composer then mounted with an empty in-memory history,
so ArrowUp did nothing until a second message was sent. The history was
also lost on every page reload.

Persist the history to localStorage as a single global list and re-read it
on mount. Global (not per-session) because a new session has no id until
after the first submit, so per-session keys would not line up across the
empty -> docked handoff. Caps the list at 200 entries.

Adds persistence-focused unit tests (surviving a remount, the 200-entry
cap, and a malformed stored value).

* fix(web): record slash commands in input history too

Move the history.push call ahead of the slash-command branch so that known
commands (with or without args, e.g. /goal <task> or /model) are recorded
and can be recalled with ArrowUp, instead of only plain messages. Steer
already pushed; only the submit slash path was missing it.

* fix(web): record menu-selected slash commands in history

Bare slash commands picked from the slash menu (e.g. /model, /login) go
through selectSlashCommand and emit directly, never reaching handleSubmit,
so they were not recorded even after the typed-slash fix. Push the command
name before emitting. acceptsInput commands are still recorded later by
handleSubmit together with their argument.
2026-06-23 18:07:00 +08:00
liruifengv
9c553e4bf7
feat(tui): add Alt+S to switch model for current session only (#1020)
In the /model picker, Enter still switches the model and saves it as the
default; Alt+S now switches only for the current session without writing
to the config file.
2026-06-23 17:14:43 +08:00
qer
fb780fce96
refactor(web): extract input-history recall into a composable (#1011)
* refactor(web): extract input-history recall into a composable

Move the shell-style up/down recall of previously sent messages out of
Composer into useInputHistory. The composable owns the history list, the
browsing cursor, and the textarea caret/selection work needed to apply a
recalled entry, taking the text ref, textarea ref, and autosize as deps.

The composer keeps the keydown orchestration (which also juggles the slash
and mention menus) and calls into the composable for push / recall / caret /
browsing state.

Composer.vue: 2104 -> 2050 lines. No behavior change.

* test(web): cover useInputHistory recall behavior

Add unit tests for the extracted input-history composable: push dedup and
empty-skip, walking backward/forward through entries, restoring the live
draft (empty and non-empty), empty-history no-op, resetBrowsing, and the
caretAtFirstLine gate.
2026-06-23 17:01:17 +08:00
liruifengv
fd16ffb80a
fix(tui): fix Tab key completion in the editor (#1012)
Stop plain Tab from opening the file completion list when the autocomplete menu is closed; Tab now only accepts the selected item while the menu is open.

After Tab-completing a slash command name, reopen the menu to show its subcommands instead of falling back to file completions.
2026-06-23 16:51:12 +08:00
qer
a2650f85d4
refactor(web): extract ConversationToc from ConversationPane (#1010)
Move the beta conversation outline (proportional bubbles, viewport
indicator, hover tooltip) into a dedicated ConversationToc component.
The child owns the nav markup, the tooltip hover state, and its own
visibility (mobile / session-loading / single-turn), while the metric
derivation and scroll-driven viewport/active-turn tracking stay in the
pane because they are coupled to the scroll container.

ConversationPane.vue: 1613 -> 1422 lines. No behavior change.
2026-06-23 15:51:49 +08:00
liruifengv
e47de610e4
feat(tui): add ctrl+t to expand the todo list (#1009)
* feat(tui): add ctrl+t to expand the todo list

Toggle between the truncated view and the full list; the shortcut only takes effect while the list actually overflows.

* docs(keyboard): document ctrl+t todo expand shortcut

* chore(changeset): mark todo expand shortcut as patch

* docs(agents): clarify minor vs patch in gen-changesets skill

* fix(tui): clear pending exit when toggling the todo list
2026-06-23 15:49:07 +08:00
liruifengv
d70c3a8c01
fix(tui): support expanding bash command while running (#1004)
Render the command in the in-flight Bash card body so it is visible while the command runs, and let Ctrl+O expand the full command before the result arrives.
2026-06-23 15:28:49 +08:00