Skip to content

feat(web): introduce Kimi web app and daemon gateway#625

Open
sailist wants to merge 260 commits into
mainfrom
feat/web
Open

feat(web): introduce Kimi web app and daemon gateway#625
sailist wants to merge 260 commits into
mainfrom
feat/web

Conversation

@sailist

@sailist sailist commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue. This PR introduces the web client, daemon gateway, and supporting service foundation for browser-based Kimi Code sessions.

Summary

Adds a Vite/Vue Kimi web application backed by a local daemon REST/WebSocket gateway, wires the CLI kimi web and daemon entry points, and moves daemon-facing capabilities into reusable services with focused e2e and unit coverage.


1. Web Client Experience

Problem: Kimi Code currently has no browser UI for managing workspaces, sessions, prompts, tools, approvals, file diffs, and provider settings against a local daemon.

What was done:

  • Added apps/kimi-web with Vue components, i18n, API clients, state projection/reduction, markdown/image rendering, composer controls, responsive desktop/mobile layouts, onboarding, login, session, and workspace flows.
  • Added UI polish for message copying, image attachments, tool-call display, auto-scroll behavior, model name truncation, mobile toolbar/send button behavior, and modern theme layout.

2. Daemon Gateway And CLI Entry Points

Problem: A web UI needs stable daemon endpoints and a simple way for CLI users to launch the daemon-backed browser workflow.

What was done:

  • Added daemon REST/WS gateway routes for sessions, prompts, files, workspaces, approvals, questions, tasks, tools, auth, OAuth, metadata, model catalog, and static web assets.
  • Added CLI daemon and web subcommands plus scripts for web asset copying and daemon development restart flows.
  • Added OpenAPI route support and timeout, reconnect, and resync behavior needed by the web client.

3. Shared Services, Protocol Boundaries, And DI

Problem: Daemon-facing capabilities were spread across layers, making reuse and lifecycle management harder as web and daemon features expanded.

What was done:

  • Added shared service abstractions for file store, filesystem, prompt, message, session, auth/OAuth, model catalog, task, question, tool, event, logger, and workspace services.
  • Added DI/lifecycle primitives and test helpers in agent-core, and inverted protocol/core dependencies so protocol-facing code stays isolated from agent-core internals.

4. Verification, Docs, And Release Metadata

Problem: The new daemon/web flow needs coverage for REST/WS behavior and release metadata for affected packages.

What was done:

  • Added daemon e2e scenarios, Docker workflow support, and focused tests for prompt queue steering, replay, sessions, service wiring, route validation, and daemon APIs.
  • Added changesets for user-visible package changes and updated Kimi command reference docs.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

sailist and others added 30 commits June 10, 2026 14:09
…ides

- make InstantiationService.services private to prevent external mutation\n- add serviceOverrides to DaemonStartOptions for test injection\n- refactor e2e tests to use startup overrides instead of post-boot container hacks
- add per-session prompt queue: concurrent submits return queued status\n- add list/steer REST endpoints and protocol schemas\n- synthesize prompt.steered lifecycle event\n- add debug endpoint to inject active prompts for e2e testing\n- add daemon-e2e queue + steer invariant test\n- fix WS ping frame handling in daemon-e2e client
…p and expand DI lifecycle primitives

- remove defaultServicesModule() and services/src/module.ts; consume getSingletonServiceDescriptors() directly\n- update daemon service registrations and bootstrap to use registry descriptors\n- add DisposableMap, DisposableSet, disposable tracking, and disposeOnReturn to agent-core DI\n- update AGENTS.md with new registration patterns
- add Dockerfile and run-docker-e2e.sh for isolated docker-based e2e testing\n- add docker:e2e npm script with workspace-scoped runner names\n- add undoSession method to DaemonClient and HttpClient\n- add live and mocked tests for undoSession\n- update AGENTS.md and README.md with docker:e2e usage docs
…iring

UI/UX:
- Onboarding: welcome + language/theme prefs (Modern default, applied on start)
- Modern is the default theme; chat surface white; floating dock (input on top,
  status controls as functional pill-buttons below, ctx far right)
- Single-line composer with smaller send; empty-chat hint vertically centered
- Global connecting splash on first load; overlay drift fixes (viewport-unit sizing)
- Wide-screen reading-column cap; font-size pass (chrome up, chat = session list)
- Merged ~/files + ~/diff into one tab (Changed|All, list/tree)
- ThinkingBlock capped to ~3.5 lines with auto-scroll-to-latest
- Workspace rail title + branch on second line

Design system:
- Radius scale tokens (--r-xs/sm/md/lg = 6/8/12/16); unified component radii
- Moved Modern per-component overrides to global style.css (scoped :global() did
  not win the cascade — input/tabs/cards were silently un-styled)
- docs/design-system.html: tokens + live component reference

Backend wiring:
- POST /sessions/{id}/profile for model + runtime (thinking/permission/plan)
- GET /sessions/{id}/status; :compact / :fork; agent.status.updated
- Historical replay no longer re-streams (messagesToTurns dedup)
… drop web tests

- Font: switcher now applies site-wide (--mono + --sans); default Inter + font-smoothing
- Theme accent color toggle (Kimi blue / mono Vercel style)
- Hide system-injected user messages via origin in message metadata (TUI parity)
- Fix historical-session re-stream (markstream smooth-streaming gated on live streaming)
- Fix auto-scroll-to-bottom on opening a session (stick-to-bottom window over async load + late markdown render)
- Default-collapse thinking blocks in historical sessions
- Markdown badge images render at natural size
- Plus in-progress workspace/sidebar/rail/files/tasks UI work

Removes the apps/kimi-web test suite (WIP; restorable from history at e609a07).
- add sinon mocking, spying, and stubbing to TestInstantiationService (mock, spy, stubPromise, stubInstance)\n- add createServices factory for disposable test service collections\n- export IConstructorSignature for constructor signatures with DI service args\n- remove verbose JSDoc comments from DI core files\n- migrate test-instantiation tests from vitest vi.fn to sinon
…ove redundant JSDoc

- remove extensive JSDoc comments from event, lifecycle, and instantiationService\n- add new disposable utilities: RefCountedDisposable, ReferenceCollection, AsyncReferenceCollection, ImmortalReference, MandatoryMutableDisposable\n- change dispose() to throw collected errors instead of swallowing via onUnexpectedError\n- use combinedDisposable in Event.any and simplify Emitter internals\n- refactor InstantiationService.dispose to use centralized dispose() helper\n- update tests to match new error-throwing behavior
- add scenario 10 verifying REST + WS contract for steering queued prompts\n- cover debug active-prompt injection, queue assertions, steer response,\n  prompt.steered frame, content verification, and queue drain\n- update README scenario index
…ervices to shared package

- Move filesystem, fileStore, logger and workspace service implementations from daemon to shared services package\n- Update daemon routes and services to import from shared package\n- Move chokidar and ignore dependencies to services package\n- Rename daemon loggerService.ts to pinoLoggerService.ts
…1Routes module

- Extract inline /api/v1 route setup from start.ts into registerApiV1Routes\n- Centralize health check, meta, auth, sessions, messages, and all other route registrations\n- Reduce start.ts size and improve separation of concerns
…ract

Workspace names, path lines and session titles each derived their left
edge from unrelated magic numbers (terminal: session 5px left of the
workspace name; modern: 1px right because the .se inset margin was not
compensated; path line 2px off). Define one --sb-pad-x/--sb-gutter/
--sb-gap contract on .side and derive all three from it; drop the dead
:root block in SessionRow's scoped style. Verified in-browser: all
three text edges at x=34 in both themes.
A 9-agent sweep of all 36 components found ~110 terminal-styled remnants
still rendering under modern: sharp 0-4px corners on dialogs/menus/cards
(approval+question cards, slash/mention menus, statusline popover, all
six dialog shells, file/diff/task rows), --mono hardcoded on UI copy,
2px blue 'terminal stripe' dialog tops, and hardcoded colors bypassing
the token system. Adds a grouped de-terminalization layer to style.css
using the --r-* / --sans / --sh tokens; code, paths, commands and
timestamps deliberately keep --mono.

Also fixes real bugs the sweep surfaced:
- var(--blue-soft) was referenced in Composer + style.css but defined
  nowhere, so the Plan pill active state and permission-row highlight
  rendered with no background; replaced with the existing --soft token
- the modern .se row override also matched MobileTopBar's unrelated .se
  span, mis-spacing the mobile title path; now scoped to .sessions .se
- #1565C0 hardcoded in DiffView/FileTree/ChangedTree ignored the
  html[data-accent=mono] grayscale remap; routed through var(--blue)
- .gh-name hardcoded #000 instead of var(--ink)

Verified in-browser (modern + terminal): dialogs, slash menu, settings
popover, sidebar; terminal theme is untouched.
MobileSwitcherSheet: replace the old workspace-chips row + flat
active-workspace session list with the desktop sidebar's design —
collapsible workspace groups (folder icon + name + branch/path
sub-line + per-group new-session button) over all workspaces, plus a
'+ new workspace' top row. Session titles share an alignment contract
(--m-pad/--m-gutter/--m-gap) with the group headers, mirroring the
desktop --sb-* contract; the modern inset-pill override compensates
its margin so titles stay on the alignment line.

MobileSettingsSheet: add the desktop settings-popover capabilities
that had no mobile counterpart — theme + accent segmented toggles,
language switcher, and sign in/out.

Tested e2e in a real browser via a same-origin 375px iframe driving
the actual app + stub daemon: mobile shell activates, switcher shows
5 groups with correct session counts, group collapse toggles, session
tap switches the active session and closes the sheet, scrim closes,
theme/accent toggles flip html[data-theme] live, and 375/414/640
viewports show no horizontal overflow. (Initial 'stuck sheet' was the
background-tab rAF freeze, not an app bug — verified by patching rAF.)
vue-tsc passes.
Inside a hunk, a deleted SQL/Lua/Haskell comment line renders as
'--- comment' in unified diff output and matched the '--- ' file-header
pattern, flipping inHunk off and silently dropping the rest of the hunk
from the ~/diff view. Only 'diff --git' can end a hunk now; the other
header patterns are only honoured between hunks. Verified with a real
SQL-comment deletion diff.
…field

The daemon broadcasts approval payloads with tool_input_display
(packages/protocol/src/approval.ts) but the client only read a
non-existent 'display' field, so against the real daemon every approval
card fell through to the generic one-liner: file-edit approvals showed
no diff, shell approvals no command/cwd/danger info — users were
approving actions blind. Read tool_input_display first and keep
'display' as a fallback for the stub daemon's older shape.
toolUse blocks were stamped status 'ok' the moment they appeared
(unless awaiting approval), so every executing tool rendered a green
check that could later flip to a cross — the ToolCall spinner state was
unreachable in practice. Tools now start as 'running' and resolve to
ok/error when their toolResult is absorbed; turns that were ended by a
later message settle dangling tools back to 'ok' so aborted turns in
old transcripts don't spin forever. Behaviour verified for live,
historical-dangling, completed and error cases.
Three related silent-loss paths:
- Submitting while an image upload was in flight sent the prompt
  WITHOUT the image and cleared the chips; the composer now refuses to
  submit until uploads settle (text + chips stay put).
- Sending while the agent was busy enqueued only the prompt text —
  attachments were dropped with no warning. Queue entries are now
  structured {text, attachments} and the flush passes both through.
- A failed submit left the optimistic user message in the transcript
  looking delivered (until a reload silently removed it), and a failed
  queue flush dropped the prompt entirely. The optimistic message is
  now rolled back in the catch, and a failed flush re-queues the prompt
  at the head.
fetch() was issued with no signal anywhere, so a hung connection (the
half-open TCP you get after a network change — the same scenario that
kills the WS) left the promise pending for minutes. submitPrompt sets
the per-session in-flight flag before awaiting, and that flag is only
cleared on settle, so one hung submit silently routed every subsequent
prompt into the queue until the browser's own socket timeout fired.
AbortSignal.timeout(30s) turns the hang into the existing
DaemonNetworkError path (with a jsdom-safe fallback), which already
cleans up the in-flight state.
The only turn-end cleanup was a watch(activity) callback, and activity
is computed from the ACTIVE session — so a session that finished while
the user was looking at another one never had its in-flight flag
cleared or its queue flushed. Switching back didn't help (idle → idle
is not a watch transition): the session was bricked — permanent
'sending…' placeholder and every new prompt silently enqueued forever,
recoverable only by a page reload that also discarded the queue.

Cleanup + queue flush now run from the WS sessionStatusChanged → idle
event for the session the event names (background sessions included);
git/runtime status refreshes still only run for the on-screen session.
DaemonEventSocket had no reconnect logic at all (the close() docstring
mentioned 'reconnect attempts' that never existed): one daemon restart,
laptop sleep or network blip permanently killed all live updates —
replies, approvals, questions and status changes never arrived again
and the only recovery was a full page reload. connectEventsIfNeeded's
eventConn guard made the loss unrecoverable from above.

onclose now schedules connect() with exponential backoff + jitter
(1s..30s, reset on a successful hello); the kept subscriptions map is
replayed via client_hello on reconnect and a too-large seq gap is
handled by the existing resync_required path. close() still stops
everything. Verified against a live WS server: kill → backoff →
reconnect → subscriptions re-sent with their lastSeq.
Three projection bugs that corrupted live streaming:

- Every sidebar click re-subscribed the session, and the subscribe
  wrapper unconditionally projector.reset() — wiping the turn/prompt
  bindings, after which every remaining delta/tool event of the
  in-flight turn was silently discarded (turn.step.started hard-bailed
  on the missing promptId, so it never self-healed). Re-subscribing no
  longer resets; only the resync path (which reloads messages) does.

- turn.step.started and tool.result now synthesize a promptId when the
  binding is missing (mid-turn join after reconnect/resync), mirroring
  turn.started, so the rest of the turn renders instead of vanishing.

- The projector emitted messages/content by reference and then mutated
  them in place (slot.text += delta) while the reducer also appended
  the delta to the same object — doubling the first streamed chunk of
  every text/thinking block. Emits now clone the content objects.
Neither component is imported or rendered anywhere since the sidebar
redesign (verified by repo-wide grep): 649 + 485 lines of unreachable
UI. Worse than dead weight, both contained stale forks of live code —
WorkspaceRail duplicated the settings popover that now lives in
Sidebar.vue (already missing its codeFont/accent additions), and
StatusLine duplicated the Composer-toolbar controls with a diverged
permission color mapping — so edits could land in the dead copy and
silently no-op. Also removes the five workspace.* i18n keys only the
rail used and updates the comments that still pointed at StatusLine.
The follow-to-bottom gate was an atBottom position snapshot updated by
scroll events, which broke in three ways users hit daily:

- the scroll event fired by our own pin could observe a view that had
  already grown past the 80px threshold mid-stream (thinking / tool
  phases) and flip the gate off — the view stopped above the newest
  content and a 'new messages' pill appeared without any user scroll;
- QuestionCard replaces the Composer in the bottom dock OUTSIDE the
  scroller, so its appearance (and the composer growing via queue strip
  / attachments / multiline input) shrank the scroll viewport without
  producing a single scroll or mutation event — nothing re-pinned and
  the newest message stayed hidden behind the dock;
- sending a prompt while scrolled up only raised the pill.

following is now an intent flag: it turns off ONLY when the user
scrolls up out of the bottom zone (our own scrolls always move down,
so an upward scrollTop is always user intent; sub-80px drifts never
break it), and back on when they return, click the pill, send a
prompt, answer a question, or switch session/tab. ResizeObservers on
the dock, the scroller and the content column re-pin on pure layout
changes (the QuestionCard case and image loads), without raising the
pill; the 1200ms stick-window machinery is replaced by the flag.
Also re-pins on visibilitychange (background tabs freeze rAF).

Verified e2e against the stub daemon: full-stream follow stayed within
1px across thinking/tool/approval phases (155 samples); mid-stream
scroll-up stopped following and raised the pill; sending and answering
while scrolled up force-pinned; QuestionCard appearance kept the view
pinned (max 1px); content-collapse clamp events did not break follow.
- Replace `@moonshot-ai/kimi-code-sdk` with `@moonshot-ai/agent-core` in protocol\n- Remove `@moonshot-ai/kimi-code-sdk` from services dependencies\n- Introduce internal `managedAuth` facade in services to replace `KimiAuthFacade`\n- Add compile-time assertions that neither package references the node SDK
- move event payloads and tool display schemas from agent-core into protocol\n- make agent-core depend on protocol instead of the reverse\n- remove alias workarounds in protocol build config\n- add changeset for agent-core, protocol, and kimi-code
A prompt queued while the agent is busy can carry image attachments
with no text; the queue strip rendered its bare text — an empty string,
so the row was just a blank button next to a remove cross. Queue items
now expose {text, attachmentCount}: image-only prompts render an
'image ×N' placeholder with a small badge, and any item carrying
images disables the load-back-into-input edit action (the uploaded
files can't be restored to the composer; editing would silently drop
them — remove stays available). Verified with component render tests
for the three shapes (image-only / text-only / text+images).
wbxl2000 and others added 26 commits June 11, 2026 17:14
- fix swagger-ui resolution path to packages/server\n- add agent-core ./session/store package export\n- handle non-string action/message in lifecycle formatHuman\n- improve exec error message serialization\n- fix test error objects and mock return values\n- include missing event types in SDK type tests\n- update flake.nix dependency hash
- sort apps/kimi-code devDependencies alphabetically
- add fast-json-stringify/lib/serializer and fast-json-stringify/lib/validator to optional runtime requires
- add package.json placeholders for packages/daemon and packages/kimi-migration-legacy
…d docs

- update existing changeset names and contents from daemon to server\n- rename changeset files to replace daemon with server\n- update services AGENTS.md and DI README references
Add a third UI theme "kimi" implementing the official Kimi design
language (Quiet Utility) with token-exact values from kimi-design-skill
tokens.json v0.2.0:

- Interaction accent = kimiDark (black in light / white in dark);
  kimiBlue stays reserved for brand/data-viz (::selection only)
- Gray user bubbles (bubbleGrayPc), flat tool cards and dialogs; shadows
  only on the composer (shadow.inputDefault) and floating menus
  (shadow.small); dark elevated surfaces use background.tertiary
- PingFang SC for UI, Geist Mono for code (bundled JetBrains fallback)
- Modern's de-terminalization rules are shared via
  :is(html[data-theme="modern"], html[data-theme="kimi"]) — identical
  specificity, so Modern renders exactly as before; kimi token blocks
  sit after the data-accent rules and pin the accent (the accent picker
  is hidden while kimi is active)

Theme pickers (sidebar popover, mobile sheet) and onboarding now offer
Modern/Kimi only. Terminal remains the CSS baseline and a persisted
'terminal' choice still loads; it's just no longer offered in the UI.

Also: file-preview pane default width now follows half the window.
add pino-pretty to apps/kimi-code dependencies to fix server startup when pretty logging is enabled\nadd regression test verifying the dependency is declared\ninclude changeset for patch release
…ort startup failure

- switch createServerLogger from pino transport target to in-process pretty stream\n- update changeset to cover @moonshot-ai/server\n- add test asserting pretty logger does not use ThreadStream\n- rename cli server test description
…rols

- add terminal protocol schemas (REST and WebSocket controls)\n- add TerminalService with node-pty backend and frame buffering\n- add REST routes for terminal CRUD operations\n- add WebSocket terminal attach, input, resize, and close handlers\n- add e2e and unit tests for terminal lifecycle and I/O
- collect and embed server web assets into the native SEA binary\n- add web-assets manifest and collection scripts for native builds\n- make swagger/swagger-ui registration conditional in server start\n- enrich lifecycle commands with service URL, running state, log path, and notes\n- add --no-open flag to control auto-opening web UI after install\n- introduce ServiceUnavailableError and ProgramServiceManager\n- update service managers (launchd, systemd, schtasks) with new status fields
- add host field to LockContents and acquireLock options\n- record bind host in lock file during server startup\n- remove JSDoc block comments across service manager modules\n- update start.test.ts to assert host is persisted in lock
- Open the active server URL when `kimi server run` finds an existing local server\n- Distinguish background vs foreground server mode in conflict message\n- Show mode-appropriate stop command (`kimi server stop` or `pkill`)\n- Always include host in lock file contents\n- Add tests for already-running server handling
The pnpm patch for list-nested code blocks hanging in production builds
is reverted in favor of an upstream fix; the verified recipe is archived
in reports/markstream-nested-codeblock-fix.md.

Upstream report: Simon-He95/markstream-vue#498
…ted sessions

- add wire.jsonl transcript reader (readWireRecords, readWireTranscript, reduceWireRecords)\n- rewrite MessageService to use wire log instead of live context history\n- add createdAtMsOverride to toProtocolMessage for wire-derived timestamps\n- add transcript cache with LRU eviction and mtime-based invalidation\n- append unflushed live tail when memory is ahead of the wire file\n- degrade to live context view on wire read/parse failures\n- add comprehensive tests for compaction, undo, clear, blob rehydration
- add GET /sessions/{session_id}/skills and POST .../{skill_name}:activate routes\n- add skill protocol schemas (SkillDescriptor, list/activate DTOs) and error codes\n- add ISkillService/SkillService to list and activate skills via core RPC\n- map skill errors to protocol envelopes (40415, 40912)\n- add end-to-end tests covering list, activate, unknown session/skill, and unsupported action
Enable with ?debug=1 or localStorage kimi-web.debug=1. A ring buffer
(1000 entries, redacted secrets, truncated payloads) records every REST
call (method/path/requestId/status/envelope code/duration) and WS frame
(lifecycle, outbound control frames, inbound events with session/seq/
offset) as a side channel — request ordering and error handling are
unchanged. The panel offers a filterable timeline, per-entry JSON with
copy, JSONL export, and per-session/event-type aggregation.
A user turn whose origin metadata marks it as a skill_activation
(trigger user-slash) no longer dumps the raw XML block into the chat:
the turn shows only the user-provided args plus an "activated skill"
label with the skill name.
…tate

The onboarding composer (workspace draft) has no backend session yet, so
setModel() hit the early return and the pick was silently dropped. The
pick is now remembered as draftModel: the status line reflects it
immediately, and the first prompt passes it to createSession so the new
session starts on the chosen model.

Also keep the user's model when the daemon echoes model as '' — both in
/status refreshes and in snapshot syncs racing an optimistic setModel —
and guard the createSession echo the same way.
wbxl2000 and others added 3 commits June 12, 2026 13:40
- unconditionally expose /openapi.json and /asyncapi.json\n- add --swagger flag to kimi server run\n- mount Swagger UI at /documentation only when --swagger is passed\n- generate AsyncAPI document from ws-control schemas\n- update tests and docs
- remove --host option from server run/install/web commands (force 127.0.0.1)\n- default foreground logs to silent; add styled ready banner when logs off\n- change foreground stop suggestion from pkill to kill -TERM <pid>\n- update service install plans, tests, and bilingual docs accordingly
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants