* feat(web): open design-system easter egg at /design-system route
Replace the long-press-logo iframe overlay, which loaded a separately maintained static design-system.html, with a real /design-system route. The new view aliases to the product design tokens, so the design system is maintained in one place. Adds vue-router for the route, removes the duplicate static HTML copies, and exempts the showcase view from the style scanner.
* fix(web): lazy-load the design-system view
Address Codex review: load the 2.4k-line showcase via defineAsyncComponent so it is code-split and fetched only when /design-system is visited, instead of bloating the initial bundle for every load.
* chore(nix): update pnpmDeps hash for vue-router
Adding vue-router changed pnpm-lock.yaml, which invalidated the fetchPnpmDeps hash. Set it to the value reported by the nix build.
* fix(web): return to app root when closing the design system
In-page nav anchors push hash history entries, so router.back() only stepped through them and required multiple clicks to leave. Navigate to / directly; the client lives above the route so session state is preserved.
* feat(web): let /design-system bypass the auth gate
Render the design-system route ahead of the auth/server gates and skip the /login rewrite for it, so a direct deep link shows the showcase even before the app is OAuth-ready. The view is read-only and holds no user data, matching the old static page behavior.
* fix(web): make the design-system root scrollable
The route renders inside .app-shell (height:100dvh; overflow:hidden), so a position:fixed root could be clipped. Make .ds-page a flex item that fills the shell and scrolls internally, so later sections and hash navigation remain reachable.
* fix(web): preserve /design-system during initial session load
On load the app auto-selects the first session and rewrites the URL to /sessions/<id>, which clobbered a deep-linked /design-system. Skip the session URL write while on the design-system route so refreshing keeps the route.
* fix(web): restore the prior session URL when leaving the design system
Record the URL on entry to /design-system and navigate back to it on close, so a /sessions/<id> URL is preserved instead of falling back to /. This also keeps the earlier fix that sidesteps in-page hash anchors.
* fix(web): capture the real browser URL before opening the design system
Session URLs are rewritten via the native history API, so vue-router's from.fullPath can be stale ('/' after a session is selected). Read window.location on entry instead, falling back to / for a direct deep link.
* fix(web): sync the active session URL when leaving the design system
After a direct deep link to /design-system the app auto-selects a session but the address stays '/'. On close, fall back to the active session's canonical URL so the address bar matches the displayed session.
* fix(web): capture the design-system return path at logo entry
Capture window.location in the logo long-press handler instead of a navigation guard. The guard fired on browser Back/Forward too and overwrote the return path with the design-system URL itself; capturing only at the explicit entry action avoids that.
* fix(web): replace the design-system route when closing
Use router.replace instead of push for the captured return URL, so closing does not append a second app URL after /design-system and the browser Back button returns to the page before the easter egg.
* revert(web): drop the design-system auth-gate bypass
Keep /design-system behind the auth gate so it has a single in-app entry (logo long-press). This removes the deep-link machinery (auth exemption, active-session fallback) that drove most of the URL/session edge cases, while keeping the lazy-loaded route, the flex scroll fix, the logo-entry return-path capture, and replace-on-close.
* refactor(web): rebuild the design-system easter egg as an in-app overlay
The easter egg is a hidden, read-only spec viewer opened by long-pressing the logo; it does not need to be a URL route. Replace the vue-router approach with an overlay: Sidebar opens a lazy-loaded DesignSystemView in a body-teleported full-screen overlay, dismissed by the Back button or Escape. This removes vue-router, the route, the auth-gate exemption, the session-URL guards, and the return-path machinery — none of which the feature needs.
* chore(nix): restore pnpmDeps hash after removing vue-router
|
||
|---|---|---|
| .. | ||
| design | ||
| public | ||
| scripts | ||
| src | ||
| test | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| index.html | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
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;AppXtypes are camelCase.config.tsbuilds/api/v1URLs. - Event projector (
agentEventProjector.ts): the server streams raw agent-core events (noevent.prefix).classifyFrameroutes raw vs protocol (event.*) frames; the projector converts them toAppEvents. - i18n (
src/i18n/): vue-i18n, en/zh, per-namespace flat camelCase keys. Detect order:localStorage('kimi-locale')→navigator.language→en.
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 — checkcode(0 = ok), not the status. - Prompts require five fields.
POST /sessions/{id}/promptsmust 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_idmust be awd_<slug>_<hash>id that exists in the server's registry. Sessions get one auto-assigned by cwd, but it isn't registered until youPOST /workspaces { root }(idempotent). The web registers on demand beforecreateSession(otherwise:workspace not found: wd_…). - Persisted sessions are directly promptable — selecting an old session and
sending a message just works; there is no
:activatestep. - Workspaces = real folders.
GET/POST/PATCH/DELETE /workspaces,GET /fs:browse?path=,GET /fs:homeback 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
- Develop —
pnpm dev:web(orpnpm -C apps/kimi-web run dev). - Build —
pnpm -C apps/kimi-web run buildproducesapps/kimi-web/dist. - Bundle into CLI —
pnpm -C apps/kimi-code run buildrunsscripts/copy-web-assets.mjs, which copiesapps/kimi-web/distintoapps/kimi-code/dist-web. - Publish — the root
.github/workflows/release.ymlpublishes@moonshot-ai/kimi-codeto npm;dist-webis listed in the packagefilesarray, so the built web assets travel with the CLI package. - Serve —
kimi server run/kimi webservesdist-webfrom 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.ymlthat buildsapps/kimi-weband uploadsdist/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.jsonremains 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
buildscript already builds every workspace, sopnpm run buildin CI coversapps/kimi-web. Keep it that way; do not bypass the web build in release pipelines.