kimi-code/apps/kimi-web
qer ec8dc3456c
fix(web): stop sending prompts into a busy turn on the web UI (#1522)
* fix(web): prevent duplicate first prompts and keep goal drives from looking idle

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

* chore: add changeset for web first-prompt fixes

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

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

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

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

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

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

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

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

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

* chore: align changeset with actual fix scope

* test(web): update profile-patch expectation for model field
2026-07-09 23:37:10 +08:00
..
public feat(web): open design-system easter egg at /design-system route (#1328) 2026-07-03 13:42:27 +08:00
scripts refactor(web): migrate icons to unplugin-icons (#1467) 2026-07-07 16:44:56 +08:00
src fix(web): stop sending prompts into a busy turn on the web UI (#1522) 2026-07-09 23:37:10 +08:00
test fix(web): stop sending prompts into a busy turn on the web UI (#1522) 2026-07-09 23:37:10 +08:00
AGENTS.md feat(web): open design-system easter egg at /design-system route (#1328) 2026-07-03 13:42:27 +08:00
CHANGELOG.md ci: release packages (#1061) 2026-06-26 02:29:53 +08:00
index.html feat(web): redesign web UI and add design system (#1258) 2026-07-01 20:47:12 +08:00
package.json style(web): polish the session sidebar (#1519) 2026-07-09 19:39:20 +08:00
README.md test(kimi-web): keep only pure logic unit tests (#959) 2026-06-22 14:57:27 +08:00
tsconfig.json refactor(web): migrate icons to unplugin-icons (#1467) 2026-07-07 16:44:56 +08:00
vite.config.ts style(web): polish the session sidebar (#1519) 2026-07-09 19:39:20 +08:00

Kimi Web

A browser client for Kimi Code — a peer to the TUI (apps/kimi-code) that talks to a local server over REST + WebSocket. Vue 3 + Vite + TypeScript.


Quick start

# 1) Against a REAL server (the server must be running and reachable)
WEB_PORT=5197 KIMI_SERVER_URL=http://192.168.97.91:58627 pnpm -C apps/kimi-web run dev
#   …or from the repo root:  pnpm dev:web   (uses the defaults below)

# 2) Offline / no server — a stub that fakes the server API + event stream
pnpm -C apps/kimi-web run dev:stub      # then run dev in another shell

# checks
pnpm -C apps/kimi-web run typecheck     # vue-tsc --noEmit
pnpm -C apps/kimi-web run test          # vitest (pure logic only)
pnpm -C apps/kimi-web run build         # vite build

How it connects to the server

The browser cannot reach the server cross-origin (no CORS), so Vite same-origin proxies /api/v1 (HTTP + WS) to the server (vite.config.ts):

env var default meaning
WEB_PORT 5175 port the dev server listens on
KIMI_SERVER_URL http://127.0.0.1:58627 where /api/v1 (and /api/v1/ws) is forwarded

Behind a corporate HTTP proxy, also set NO_PROXY=<server-host> (for example, NO_PROXY=127.0.0.1,localhost) so the proxy forward reaches the server directly.


Architecture

A strict one-direction data flow; components never touch the network or the reducer — they consume computed view props and call actions.

server (REST + WS)
  └─ src/api/daemon/client.ts      REST adapter  (envelope → AppX types)
  └─ src/api/daemon/ws.ts          WS frames → classify → projector/reducer
       └─ agentEventProjector.ts   RAW agent-core events → AppEvent[]
       └─ eventReducer.ts          AppEvent[] → state
  └─ src/composables/useKimiWebClient.ts   the ONLY place that imports api + state;
                                           exposes computed view props + actions
  └─ src/components/*.vue          render props, emit intents (no api access)

The directory name src/api/daemon/ is historical and kept to minimise diff churn; conceptually it is the server adapter.

  • Adapter (src/api/): wire types are snake_case; AppX types are camelCase. config.ts builds /api/v1 URLs.
  • Event projector (agentEventProjector.ts): the server streams raw agent-core events (no event. prefix). classifyFrame routes raw vs protocol (event.*) frames; the projector converts them to AppEvents.
  • i18n (src/i18n/): vue-i18n, en/zh, per-namespace flat camelCase keys. Detect order: localStorage('kimi-locale')navigator.languageen.

Server contract — non-obvious notes

The server's wire protocol has a few things that will bite you if forgotten:

  • Envelope: every response is { code, msg, data, request_id } and the HTTP status is always 200 — check code (0 = ok), not the status.
  • Prompts require five fields. POST /sessions/{id}/prompts must carry { content, model, thinking, permission_mode, plan_mode }. The web fills these from settings (model ← session/default_model, thinking/permission/plan ← the StatusLine controls). Sending only { content }40001 model ….
  • Creating a session needs a registered workspace. workspace_id must be a wd_<slug>_<hash> id that exists in the server's registry. Sessions get one auto-assigned by cwd, but it isn't registered until you POST /workspaces { root } (idempotent). The web registers on demand before createSession (otherwise: workspace not found: wd_…).
  • Persisted sessions are directly promptable — selecting an old session and sending a message just works; there is no :activate step.
  • Workspaces = real folders. GET/POST/PATCH/DELETE /workspaces, GET /fs:browse?path=, GET /fs:home back the rail + folder picker.

Release & deployment

Kimi Web is not published as a standalone package. It ships as the built-in web UI of the kimi CLI (apps/kimi-code).

Current release flow

  1. Developpnpm dev:web (or pnpm -C apps/kimi-web run dev).
  2. Buildpnpm -C apps/kimi-web run build produces apps/kimi-web/dist.
  3. Bundle into CLIpnpm -C apps/kimi-code run build runs scripts/copy-web-assets.mjs, which copies apps/kimi-web/dist into apps/kimi-code/dist-web.
  4. Publish — the root .github/workflows/release.yml publishes @moonshot-ai/kimi-code to npm; dist-web is listed in the package files array, so the built web assets travel with the CLI package.
  5. Servekimi server run / kimi web serves dist-web from the installed package.

The web UI does not display its own package version or build commit. It is bundled into the CLI package and follows the published @moonshot-ai/kimi-code release.

Suggested improvements

  • Keep the current coupling for now. Because Kimi Code is primarily a local CLI/server product, bundling the web UI into the CLI package keeps installs self-contained and avoids cross-origin/CORS complexity.
  • Add an independent web-deploy workflow only when needed. If a public standalone web deployment is required later, create .github/workflows/web-deploy.yml that builds apps/kimi-web and uploads dist/ to the chosen static host (S3/CloudFront, Cloudflare Pages, Vercel, etc.). Until then, do not maintain a separate deploy target.
  • Keep versioning owned by the CLI release. apps/kimi-web/package.json remains internal workspace metadata; do not surface it as a separate user version unless the web app becomes an independently published product.
  • Ensure the web build is exercised in CI. The root build script already builds every workspace, so pnpm run build in CI covers apps/kimi-web. Keep it that way; do not bypass the web build in release pipelines.