- Replace emoji with inline SVG icons for attach/permission/plan buttons
- Unify attach-btn, perm-pill, toggle-pill styles (same padding/radius/font)
- Add SVG icons in permission dropdown rows
- Convert context bar from horizontal bar to circular SVG ring
- Strip provider prefix from model display name; fall back to models list lookup
- Load models eagerly on app init for friendly name resolution
- Increase toolbar and dropdown font sizes
- Increase textarea default height from 40px to 56px
- Remove plan/permission/ctx from model dropdown (keep only models + thinking)
- Fix ctx number spacing (remove spaces around slash)
2f229b3 added the image-resolving watcher above the defineProps call.
watch() invokes its getter synchronously to collect dependencies, so
the closure read props while the const was still in its temporal dead
zone — every Markdown mount threw 'Cannot access props before
initialization', crashing the whole chat transcript (session content
rendered blank, Vue warn loop in console). vue-tsc can't catch this
(the reference sits inside a closure). Verified by a mount test that
reproduced the ReferenceError before the move and passes after, plus
an in-browser check that session content renders again.
- add kimi web subcommand to open the daemon-hosted browser UI\n- refactor kimi daemon to start in background by default; add --foreground flag\n- serve web assets from daemon via new registerWebAssetRoutes\n- copy kimi-web build output into CLI bundle at build time\n- update docs for daemon and web commands\n- add unit tests for daemon and web command handlers
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).
- 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
- 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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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
- 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
- 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 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
- 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).
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)
- 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
- 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