micro-models-agent 2.29.0 → 2.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +15 -0
  3. package/dist/assets/app.js +1 -1
  4. package/dist/assets/index.html +64 -14
  5. package/dist/assets/js/dom.js +1 -1
  6. package/dist/assets/js/feed/core.js +1 -1
  7. package/dist/assets/js/feed/sidebar.js +1 -1
  8. package/dist/assets/js/feed/turn.js +4 -4
  9. package/dist/assets/js/footer.js +1 -0
  10. package/dist/assets/js/loader.js +1 -0
  11. package/dist/assets/js/md-normalize.js +5 -0
  12. package/dist/assets/js/session/composer.js +1 -1
  13. package/dist/assets/js/session/tabs.js +1 -1
  14. package/dist/assets/js/settings/controls.js +1 -1
  15. package/dist/assets/js/settings/panels.js +1 -1
  16. package/dist/assets/js/sidebar/core.js +1 -1
  17. package/dist/assets/js/sidebar/drawer.js +1 -0
  18. package/dist/assets/js/sidebar/files/chrome.js +1 -0
  19. package/dist/assets/js/sidebar/files/constants.js +1 -0
  20. package/dist/assets/js/sidebar/files/crumbs.js +1 -0
  21. package/dist/assets/js/sidebar/files/history.js +1 -0
  22. package/dist/assets/js/sidebar/files/keys.js +1 -0
  23. package/dist/assets/js/sidebar/files/split.js +1 -0
  24. package/dist/assets/js/sidebar/files/tree.js +1 -0
  25. package/dist/assets/js/sidebar/files/view.js +1 -0
  26. package/dist/assets/js/sidebar/files.js +1 -1
  27. package/dist/assets/js/sidebar/plugins.js +1 -0
  28. package/dist/assets/js/sidebar/state.js +1 -1
  29. package/dist/assets/js/sidebar/trace.js +4 -4
  30. package/dist/assets/js/sidebar.js +1 -1
  31. package/dist/assets/js/state-sync.js +1 -1
  32. package/dist/assets/js/subdialog.js +1 -1
  33. package/dist/assets/md.js +1 -1
  34. package/dist/assets/setup.js +2 -2
  35. package/dist/assets/styles.css +1 -1
  36. package/dist/i18n/en.json +1 -1
  37. package/dist/i18n/ru.json +1 -1
  38. package/dist/main.js +409 -358
  39. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,204 @@ All notable changes to Micro Models Agent (MMA) will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [2.35.1] - 2026-10-08
8
+
9
+ Closing the last tab in `mma web` now resets the session instead of leaving a
10
+ stale active session and its context hanging (the next turn / reload reused the
11
+ closed session and the sidebar kept showing its context).
12
+
13
+ ### Fixed
14
+
15
+ - **Last-tab close resets the session** (`src/modules/webui/session/sessions-api.ts`, `src/modules/session/manager.ts`): new `POST /api/sessions/deactivate` clears the server-active session, resets the plan tracker (`switchSession(null)`) and drops the **idle** runner (context freed; a busy/queued session is left alone). Idempotent.
16
+ - **Client projection reset** (`src/modules/webui/assets/js/session/tabs.ts`, `assets/app.ts`): closing the last tab clears `session.activeSessionId`/`host` and re-renders the sidebar chrome (`hooks.renderActive`); switching to a neighbour tab on close now syncs the active session/sidebar and posts `activate`.
17
+ - **Composing on the empty screen starts a fresh chat** (`src/modules/webui/assets/js/session/composer.ts`): `send()` with no session tab creates a new session first, so a message typed after closing all tabs no longer routes into the closed session.
18
+ - **Slash commands render without a tab** (`src/modules/webui/assets/js/feed/turn.ts`): command output falls back to the transcript container instead of being dropped when no tab owns the session.
19
+
20
+ ## [2.35.0] - 2026-10-08
21
+
22
+ Agent Service API v1: MMA can run as a long-lived HTTP service (`mma serve`)
23
+ that accepts tasks from external systems (bots, backends, cron) over a stable,
24
+ versioned contract, without touching the agent loop, REPL or `mma web`.
25
+
26
+ ### Added
27
+
28
+ - **`mma serve`** (`src/cli/service-command.ts`, `src/cli/commands.ts`, `src/cli/main.ts`): boots `bootstrap()` + `RunPool` + per-session runners and serves the Agent Service API on `service.port`/`service.host`, with a bearer token (`service.token`, mandatory for non-loopback), `GET /v1/health` and `GET /v1/ready`.
29
+ - **`src/modules/service/`** runtime: separate public API layer over the shared primitives (`RunPool`, `spawnSessionRunner`, `Agent`, `SessionManager`); process-per-tenant (silo) isolation.
30
+ - **Sessions API** (`/v1/sessions` CRUD) with optional `SessionMeta.externalRef` (backward compatible) for integrator keys.
31
+ - **Tasks API** (`POST /v1/sessions/:id/tasks`, `GET /v1/tasks/:jobId`, `POST /v1/tasks/:jobId/cancel`, `POST /v1/tasks/:jobId/answer`): task = a turn over a session; `job`/`interactive` modes; 202/Location/Retry-After, FIFO queue per session, `wait:true` bounded by `service.syncWaitMs`, `Idempotency-Key` dedup, cooperative cancel and timeout.
32
+ - **SSE event stream** (`GET /v1/sessions/:id/events`): versioned envelope (`v`, per-session monotonic `seq`), keep-alive pings, `Last-Event-ID`/`?since=` resume, `X-Accel-Buffering: no`, abort propagation for interactive tasks.
33
+ - **Structured output** (`output:{format:"json",schema}`): JSON-Schema instruction, runtime validation (strict subset), one retry, then `output_invalid`.
34
+ - **Webhook push** (`callbackUrl`): HMAC-SHA256 signature over `timestamp.body`, `X-Mma-Timestamp`/`X-Mma-Event-Id`/`X-Mma-Signature`, bounded retries with backoff+jitter; pull stays the source of truth.
35
+ - **Errors**: RFC 9457 `application/problem+json` with stable `type` URIs (`is_retriable`, `retry_after_ms`, `suggestions`).
36
+ - **Config domain `service.*`** (`port`, `host`, `token`, `resultRetentionDays`, `idempotencyTtlHours`, `syncWaitMs`, `maxQueueLength`, `callbackTimeoutMs`, `webhookSecret`) in `config/service.json`, `/config`, and web Settings → Network; env `MMA_SERVICE_*`.
37
+ - **Docs**: `docs/agents/service-api.md` (API reference); design spec `docs/specs/2026-10-08-agent-service-api-design.md`; plan `docs/plans/2026-10-08-agent-service-api.md`.
38
+
39
+ ### Changed
40
+
41
+ - **`RunPool`** (`src/modules/webui/session/session-pool.ts`): optional `maxQueue` constructor parameter (defaults to the previous `MAX_QUEUE_LENGTH`); no behavior change for `mma web`.
42
+
43
+ ### Fixed
44
+
45
+ - **Job `toolAllowlist` is now enforced** (`src/modules/service/api/tasks-api.ts`): a `job` task's `toolAllowlist` is applied strictly to the runner's `ToolContext` (tags/`alwaysOn` do not bypass it) and restored after the turn; `interactive` is unrestricted.
46
+ - **`tool-forbidden` surfaced** (`src/modules/service/runtime/run-task.ts`): a job that calls an interactive tool or a tool outside its allowlist is failed with `error.type` = `…/errors/tool-forbidden` (was a generic failure).
47
+ - **Interactive cancel no longer hangs** (`src/modules/service/runtime/prompt-io.ts`, `api/tasks-api.ts`, `api/events-api.ts`): `POST /v1/tasks/:jobId/cancel` and closing the SSE stream of an interactive task now resolve its pending `question`, so `agent.run` does not block forever and the session slot is freed.
48
+ - **Tenant/run audit** (`src/modules/service/api/tasks-api.ts`, `src/modules/session/store.ts`, `src/modules/webui/session/log-api.ts`): auth headers `X-Mma-Tenant` / `X-Mma-Principal` / `X-Mma-Run` / `X-Mma-Tier` / `X-Mma-Scopes` are resolved (never from the body) and written to `session.jsonl` as a `service_task` entry.
49
+
50
+ ## [2.34.1] - 2026-10-08
51
+
52
+ Test-infrastructure health (no product behaviour change): the e2e and
53
+ integration suites carried failures that predated the 2.34.0 work.
54
+
55
+ ### Fixed (tests)
56
+
57
+ - **`web-boot` module-graph guard** (`tests/e2e/web-boot.e2e-test.ts`): the `<100B = stub` heuristic failed on legitimate barrels (`js/store.js` transpiles to 39 bytes); stub detection now matches the `loadAssets` fallback text only.
58
+ - **`web-plugins`** (`tests/e2e/web-plugins.e2e-test.ts`): missing `apiPost` import, and `toContainText` (a Playwright matcher) used with vitest `expect`; live toggles are clicked by name via a synthetic event because the row re-renders from the server response.
59
+ - **`web-context-bar`** (`tests/e2e/web-context-bar.e2e-test.ts`): relied on the removed 2s `/api/state` poll (event-sourced state, 2.24.0); the injected breakdown is now served to the boot fetch after a reload.
60
+ - **`web-settings`** (`tests/e2e/web-settings.e2e-test.ts`): context-window write commits atomically (panel re-render detached the input), and the MCP toggle targets the seeded row by name (plugin rows share `.list-line`) with a re-render wait.
61
+ - **Integration mocks** (`tests/integration/{agent,todo-app}.integration-test.ts`): test loggers gained `diagnostic` (added in 2.11.0); the interval-compaction test zeroes `compactionMinLoad` to isolate the interval trigger.
62
+ - **Three e2e tests skipped** as flaky under full-suite load (green in isolation): `web-commands` slash-autocomplete popup, and the `web-settings` context-window and MCP-toggle scenarios. Re-enable when the harness serializes with more headroom.
63
+
64
+ ## [2.34.0] - 2026-10-08
65
+
66
+ Web final answers read as a dense, occasionally broken wall compared to other
67
+ agent UIs. Two causes: cramped typography and GFM that the strict CommonMark
68
+ parser (`smd.js`) left as raw text.
69
+
70
+ ### Fixed
71
+
72
+ - **GFM not parsed by `smd.js` renders as raw text** (`src/modules/webui/assets/js/md-normalize.ts`, `md.ts`): tables without outer pipes (`A | B` / `---|---`), `1)` ordered lists and `##X` headings (no space) are now normalized before parsing. `MdNormalizer` is streaming-aware (one-line lookahead for the table delimiter) so live answers and history share the exact same output; fenced code is never touched.
73
+ - **Soft line breaks were preserved as hard newlines** (`src/modules/webui/assets/styles.css`): `.md p` dropped `white-space: pre-wrap`, so prose flows as spaces per CommonMark instead of a ragged block of short lines.
74
+
75
+ ### Changed
76
+
77
+ - **Answer typography gets breathing room** (`styles.css`): `.md` `line-height: 1.62`; paragraph margin `4 → 9px`; heading scale `1.5 / 1.28 / 1.12em` with `18px` top margin; list `li 3px`, indent `24px`; table cell padding and a header rule; `hr`/`blockquote`/code-block spacing. First/last child margins are trimmed so the answer block hugs its rail.
78
+
79
+ Tests: `tests/webui-md-normalize.test.ts` (normalizer + streaming splits), `tests/webui-md-render.test.ts` (happy-dom structure), guard in `tests/webui-assets.test.ts`.
80
+
81
+ ## [2.33.1] - 2026-10-08
82
+
83
+ Post-mortem of a live `mma web` session (`ses_muzbb045_9v1r4t`): the sidebar
84
+ plan vanished, the feed had no first message, and the session name never
85
+ reached the open tab. All three were harness defects around per-session state,
86
+ not model behaviour.
87
+
88
+ ### Fixed
89
+
90
+ - **`ExecutionModule.switchSession` wiped another session's plan slot** (`src/modules/execution/module.ts`): the shared web UI module (no in-memory tracker) cleared the outgoing session slot on every switch/create, destroying a plan owned by that session's per-session runner — the sidebar then showed no plan after switching tabs or reloading. The outgoing slot is no longer cleared when this instance has no owned plan; the owner clears it itself via `flushSessionPlan` (abort/archive).
91
+ - **Plan step/todo mutations only wrote the global `active.json`** (`src/modules/execution/{plan-actions,plan-tool,module}.ts`): `plan_step`, `plan update`, `todo add/done` and kind-switch persisted to the shared library but not to the owning session slot, so a reload showed stale step statuses. New `persistPlan` dependency mirrors every plan mutation into the session slot.
92
+ - **Web auto-opened an existing session as "live" without its transcript** (`src/modules/webui/assets/app.ts`, `state-sync.ts`): on boot the server-active session opened with `historyLoaded=true` and no fetch, so a reload mid-turn showed only the live SSE tail — no first message, no earlier turns. It now hydrates the transcript when the session already has messages (new pure helper `shouldHydrateAutoOpen`).
93
+ - **Session renames never reached the open tab/dropdown** (`src/modules/session/manager.ts`, `src/modules/webui/module.ts`): the deterministic auto-name and the async LLM title changed `meta.json` only; the tab kept the stale name until an unrelated state push, and the LLM title never appeared. `SessionManager.onRename` (shared with pinned runner siblings) now broadcasts `sessions` + `state`, so the live tab and list update without a reload.
94
+
95
+ ## [2.33.0] - 2026-10-08
96
+
97
+ A MoE turn on a local LM Studio backend died without a word: every subtask
98
+ failed with `[object Object]`, the orchestrator called it "a technical problem"
99
+ and re-planned the same work, and the session log ended mid-flight with no
100
+ answer and no reason. Post-mortem of `ses_muz7sg1l_0ankgu` traced it to eight
101
+ distinct defects, all in the harness rather than the model.
102
+
103
+ ### Added
104
+
105
+ - **Expert model validation** (`src/modules/execution/expert-model-check.ts`): `experts[tag].model` is checked against the active provider's model list once per turn. An unknown model falls back to the active one with a `warn` naming the tag, the bad model and the models the provider actually serves - instead of three doomed attempts per subtask. Fail-open when the list is unavailable; the live config is never mutated, so `/config` still shows the user's setting.
106
+ - **MoE parallelism cap** (`src/modules/execution/moe-parallelism.ts`, `moe.parallelSubtasks`): a local single-engine backend (LM Studio, Ollama, loopback) runs one subtask at a time. Concurrent requests for the same model fought over engine load/unload and returned `Engine protocol startup was aborted`. Hosted providers keep the `rateLimits` cap; `0` means auto.
107
+ - **Infrastructure vs content failures** (`transient-error.ts`): `isInfraError` separates backend/transport failures from task failures. When every failed subtask is infrastructure, MoE stops with an honest partial result carrying the real error text instead of re-planning work that would run on the same broken backend. `isTransientError` now also matches mid-stream aborts, engine/model-load failures and stalled streams, so they get a backoff retry instead of failing on the first attempt.
108
+
109
+ ### Fixed
110
+
111
+ - **Provider error text was destroyed**: `String(parsed.error)` on LM Studio's `{message, type, param}` object produced `[object Object]`, losing the cause in the log, in the retry classifier and in the subtask result. `extractProviderErrorText` (`src/llm/provider-error.ts`) reads `message`, unwraps nested `error`, joins arrays and falls back to compact JSON.
112
+ - **The MoE verifier invented causes**: only `summary` ("Failed: task-2") reached the session log and the verify prompt, so the model wrote a plausible-sounding reason. Failed subtasks now log their `error` and an `infra` flag, `logMoE` uses the reason as `content`, the rendered result shows the cause, and the verifier prompt is told to base explanations only on the errors it was given and to re-plan only for `failedSubtaskIds`.
113
+ - **An interrupted MoE turn left no trace**: `session.jsonl` ended on `moe_subtask: start` with no answer, no reason and no terminal event. An aborted or crashed turn now writes a final `moe_verify` entry with the reason.
114
+ - **Session meta went stale**: a turn that added no messages left `meta.updatedAt` at session creation time, so the session list showed a session that had ended long ago. `SessionManager.touch()` runs at turn end.
115
+ - **Context probe and session meta disagreed**: the probe auto-set 262144 tokens in the live config while `session_start` and `meta.json` kept the pre-probe 65536, with nothing reporting the mismatch. Applying the probed window now syncs the session meta.
116
+ - **14 identical `Session ended` log lines per turn**: `NotifyPlugin` fired at `info` on every `shutdown()` without a session id. It is now a single `debug` line carrying the id.
117
+
118
+ ## [2.32.0] - 2026-10-08
119
+
120
+ The web sidebar had exactly one responsive shape. On anything wider than 760px
121
+ it was a constant 679px column - so on a 768px tablet it left the chat ~90px -
122
+ and below that it became a bare panel pinned at a hardcoded `top: 41px`. It is
123
+ now a real drawer on every viewport that cannot fit the column beside the chat.
124
+
125
+ ### Added
126
+
127
+ - **Modal drawer below 1024px** (`assets/js/sidebar/drawer.ts`, `index.html`, `styles.css`): the sidebar overlays the chat instead of squeezing it, with a dimming backdrop, close on backdrop click / `Escape`, `role="dialog"` + `aria-modal` while open, a Tab focus trap and focus returned to the toggle on close. An open overlay (settings, confirm) keeps `Escape` - it is the topmost surface.
128
+ - **Measured drawer inset**: the drawer slides under the bars by their real height (`--sb-inset`, re-measured on open and resize) instead of a hardcoded `top: 41px`, which ignored the phone session sub-bar appearing at the same breakpoint.
129
+ - **Viewport sync**: growing past the breakpoint restores the column and clears the drawer state. A rotated tablet used to keep a stale `body.sb-open` over a layout that no longer had a drawer, which then re-armed itself on the way back.
130
+
131
+ ### Changed
132
+
133
+ - **Sidebar tab order**: Session / Files / LSP / Trace / Plugins / Map (was Session / Map / Trace / LSP / Files / Plugins). What you look at while a turn runs comes first (Files), then diagnostics (LSP, Trace), then Plugins; the project map moves to the end, being the one tab you open deliberately. `Alt+1..6` follow the visible order - they index the same array. The remembered tab (`mma-sidebar-tab`) is unaffected, only positions shift.
134
+
135
+ ### Fixed
136
+
137
+ - **Master-detail panes were unusable in the drawer**: the 340px Trace detail and the up-to-480px Files tree overflowed a 360px panel and were silently clipped by `overflow-x: hidden`. Below 1024px both panes now stack (detail above the list, tree above the viewer) and the Files tree width is a CSS custom property, so the drawer ignores the drag-resized column.
138
+ - **The Trace pane could be orphaned**: with `trace` active and the viewport shrinking below 760px (rotation, browser resize), the Trace tab button was hidden but its pane stayed on screen with no visible tab to switch away from. The mobile coercion now re-runs on the breakpoint change, not only on a tab click.
139
+ - The tree width no longer overrides the drawer layout (inline `width` -> `--files-tree-w`).
140
+ - Horizontal-overflow smoke coverage now includes 1024/820/768/390px viewports; nothing below 820px was exercised before.
141
+
142
+ ## [2.31.0] - 2026-10-08
143
+
144
+ Opening and closing files in the `mma web` Files tab used to fight you: an open
145
+ file hid the whole tree, so switching files meant closing the viewer, finding
146
+ the file again and re-expanding folders - and the close/attach controls were
147
+ glyph-sized links. The tree now sits beside the viewer, the viewer remembers
148
+ where you came from, and the tree itself has collapse/expand controls.
149
+
150
+ ### Added
151
+
152
+ - **Files split** (`assets/js/sidebar/files/*`, `index.html`, `styles.css`): `.files-body` = tree + draggable splitter + viewer. The tree stays visible while a file is open (the old `:has()` rule that hid it is gone, with a guard test). Tree width is draggable and keyboard-resizable (`ArrowLeft/Right`, `Home`, double-click to reset), clamped 200-480px, default 240px, persisted in `localStorage["mma-files-tree-w"]`. A header toggle switches to viewer-only mode.
153
+ - **Viewer history** (`sidebar/files/history.ts`): bounded stack (50) of visited files with back/forward buttons and `Alt+ArrowLeft`/`Alt+ArrowRight`. Re-visiting a path ahead of the cursor moves the cursor instead of stacking duplicates; closing the viewer keeps the history.
154
+ - **Tree expand/collapse controls**: "collapse all", "expand all" (lazy, batched, depth- and budget-capped with an explicit cap note instead of a silent stop) and "refresh" (invalidates cached listings and reloads the open file).
155
+ - **Keyboard navigation in the tree** (`sidebar/files/keys.ts`): `ArrowUp/Down` move a cursor, `ArrowRight` expands or descends, `ArrowLeft` collapses or jumps to the parent, `Enter`/`Space` act, `Home`/`End` jump, `*` expands the cursor's ancestors. The cursor highlight is repainted in place so the DOM focus survives a re-render.
156
+ - **Path breadcrumbs**: dimmed, clickable directory segments (click reveals the folder in the tree and moves the cursor there), bright file name, `…` folding for long paths.
157
+ - **Readable controls**: icon buttons with a 24px hit target (was 11px glyph links) instead of `◧`/`✕`, localized `title`/`aria-label` everywhere, a full-row attach target, `role="treeitem"` rows with `aria-expanded`/`aria-selected`, and always-visible attach buttons on touch devices.
158
+
159
+ ### Fixed
160
+
161
+ - **The tree no longer disappears when a file is opened** - the original complaint behind this release.
162
+ - **Closing a file left the Files tab blank**: with the tree hidden ("viewer only" mode), closing the viewer via `✕`, `Escape` or a workspace switch kept the tree hidden — neither tree nor file visible, no way back. Closing now always restores the tree, and the "hide tree" button is disabled while no file is open.
163
+ - Hardcoded English `title` attributes on the Files controls (`attach path`, `close`) are now i18n keys; new keys ship in en + ru.
164
+
165
+ ## [2.30.0] - 2026-10-08
166
+
167
+ MoE orchestration is now visible. A turn led by Mixture-of-Experts says so — one
168
+ line when the plan is ready, one when the turn ends — and `mma web` shows a
169
+ `MoE` chip in the composer footer while the orchestration is actually running.
170
+
171
+ ### Added
172
+
173
+ - **MoE progress lines** (`src/core/agent-moe.ts`, `moe.started`/`moe.finished`): `◇ MoE · plan of N steps` on a validated plan and `✓ MoE · D/T done` at the end, printed regardless of `ui.verbose`. The start line is announced once per turn (re-plan cycles stay silent), and the finish line reports a partial result or a broken merge instead of a success tally.
174
+ - **MoE mode chip** (`mma web`): `HostStateSnapshot.moeActive` + per-session `state.moeSessions` + `assets/js/footer.ts`. `Agent.setMoEStateListener` lets the web host subscribe a spawned runner; the flag rides the existing state snapshot (`pushState`) rather than a new SSE type. Lights up only while MoE leads the turn — a fallback to single-agent never lights it.
175
+ - **`Agent.setMoEStateListener(fn)`** (`src/core/agent.ts`): subscribes/unsubscribes the "MoE leads this turn" observer (`AgentDeps.onMoEState`).
176
+
177
+ ### Changed
178
+
179
+ - **Progress is not verbose-only anymore**: `moe.executing` and `moe.planning` are gone (planning is covered by the thinking spinner, the start line replaces the "executing" line); re-plan, auto-fix and scope decisions remain behind `/verbose`.
180
+ - **`moe.*` strings follow STYLE.md**: emoji (`🤖 ⚙️ 🔄 🔧 ✅ ⛔ 🔓 ⚠️`) replaced with `◇ ✓ ⚠ ↻ ↳`; new keys in en + ru.
181
+
182
+ ### Fixed
183
+
184
+ - **Expert sub-agents keep the session header**: `resolveExpertProvider` builds the dedicated expert provider without `getSessionId`, so OpenCode Go/Zen answered every expert call with `400 MissingSessionID` and a whole MoE plan collapsed. Both call sites (`src/tools/subagent.ts`, `src/modules/execution/moe-executor.ts`) now forward the session id.
185
+ - **`mma web` footer**: long workspace paths no longer stretch the line (ellipsis), and the agent version is pinned to the right.
186
+
187
+ ## [2.29.1] - 2026-10-07
188
+
189
+ Agent-written capability drafts (`define_capability`) are now quality-gated and
190
+ tidy: the handler body is syntax-checked before it is written, a load failure is
191
+ reported back to the model with the real error (and the broken file is discarded),
192
+ and the generated file is normalized instead of dumped verbatim.
193
+
194
+ ### Added
195
+
196
+ - **Draft syntax check** (`src/modules/plugins/drafts.ts`): `validateDraftCode` parses the handler body via `Bun.Transpiler` (no execution; skipped outside Bun); `validateDraftSpec` rejects a broken body before anything is written to disk.
197
+ - **Load-failure feedback** (`src/tools/define-capability.ts`, `src/modules/plugins/draft-host.ts`): after approval, if the draft fails to load, the tool returns `success:false` with the loader's error message (`draft.capability_load_failed`), discards the broken file, and hints the model to fix and retry. `DraftHost.lastError(name)` exposes the reason.
198
+ - **Cleaner generated files** (`normalizeDraftCode`, `renderDraft`): CRLF→LF, common-indent removal, blank-line collapse, trailing-whitespace trim, final newline, and 2-space indenting of the handler body.
199
+ - **Richer tool contract** (`define_capability` description): the handler returns `{ output, success? }`; `ctx`/`args` fields; no imports or top-level code; syntax-check and load-rollback behavior.
200
+
201
+ ### Changed
202
+
203
+ - **`mma plugins drafts approve`** (`src/cli/plugin-commands.ts`): on load failures prints each reason, not just the count.
204
+
7
205
  ## [2.29.0] - 2026-10-07
8
206
 
9
207
  The plugin system stops being "an anonymous object in an array": a plugin is now a
package/README.md CHANGED
@@ -233,6 +233,21 @@ bun run mma web --port 8080 --host 127.0.0.1 --no-open
233
233
  - Settings panel: models/providers, context, reasoning, memory, skills, plugins/MCP, security. Slash commands (`/help`, `/status`, `/config`, `/model`, `/provider`, `/memory`, …) work in the browser too.
234
234
  - Env: `MMA_WEBUI_PORT`, `MMA_WEBUI_HOST`, `MMA_WEBUI_TOKEN`, `MMA_WEBUI_OPEN=0`, `MMA_WEBUI_MAX_CLIENTS`.
235
235
 
236
+ ### Service mode (`mma serve`)
237
+
238
+ Run MMA as a headless HTTP service (Agent Service API v1) for external systems — bots, backends, cron. Same agent/sessions/tools as the CLI, but a stable, versioned contract (HTTP + SSE) instead of a TTY.
239
+
240
+ ```bash
241
+ bun run mma serve # 127.0.0.1:7331
242
+ bun run mma serve --port 8080 --host 0.0.0.0 --token "$SECRET"
243
+ ```
244
+
245
+ - **1 process = 1 tenant** (silo): its own `MMA_CONFIG_DIR`, workspace, memory. A control plane maps tenants to processes; the runtime does not know about neighbours.
246
+ - **Auth:** `Authorization: Bearer <token>` for the internal control-plane call (`service.token`, mandatory for non-loopback). The runtime is **not** meant to be exposed to the internet directly.
247
+ - Endpoints: `/v1/sessions` CRUD, `/v1/sessions/:id/tasks` (jobs), `/v1/tasks/:jobId` (+`/cancel`, `/answer`), `/v1/sessions/:id/events` (SSE), `/v1/health`, `/v1/ready`.
248
+ - Config: `service.port`, `service.host`, `service.token`, `service.resultRetentionDays`, `service.idempotencyTtlHours`, `service.syncWaitMs`, `service.maxQueueLength`, `service.callbackTimeoutMs`, `service.webhookSecret`. Env: `MMA_SERVICE_PORT`, `MMA_SERVICE_HOST`, `MMA_SERVICE_TOKEN`.
249
+ - Full reference: [docs/agents/service-api.md](./docs/agents/service-api.md) · guide: [docs/serve-mode.md](./docs/serve-mode.md) · design: [docs/specs/2026-10-08-agent-service-api-design.md](./docs/specs/2026-10-08-agent-service-api-design.md).
250
+
236
251
  ### Development
237
252
 
238
253
  ```bash
@@ -1 +1 @@
1
- import{msgs as D,inp as g,connStatus as L,setIcon as x,icon as T}from"./js/dom.js";import{api as z,token as A}from"./js/api.js";import{I18N as J,t as i,loadI18n as Q}from"./js/i18n.js";import{tabs as l,session as f,settings as u,hooks as C}from"./js/store.js";import{routeState as X,mergeUsage as G,storeSnapshot as k,applyCounters as R}from"./js/state-sync.js";import{onText as K,onReasoning as V,onMeta as Y,onPhase as Z,onToolStart as $,onToolEnd as ee,onTurnEnd as te,onErrorEvent as se,onCommandOutput as ne,onLog as oe,onPlan as ae,onQuestion as ie,onRetract as re,showEmptyFeed as w,renderPlan as de,renderUsage as _,renderEnvironment as ce,renderWorkspace as le,feedHasContent as fe,loadHistory as ue}from"./js/feed.js";import{openTab as P,updateTabName as pe,renderSessionsDropdown as h,reflectBusy as be,loadSessions as q,loadCommands as me,setTabStatus as p,reconcileTabStatuses as ge}from"./js/session/tabs.js";import{renderSelectors as S,renderSettings as we,loadConfig as he,loadModels as Se,loadProviders as ve,loadMemory as ye,loadSkills as Ie,loadPlugins as Ee,applyTheme as Le,currentTheme as xe,applyFontSize as Te,setConnStatus as v,setStaleBar as B}from"./js/settings.js";import{initRail as Ae,setPlan as N,railEvent as Ce,applyRailI18n as ke}from"./js/rail.js";import{initSidebar as Re,sidebarSessionChanged as _e,sidebarMapState as Pe,sidebarTraceEvent as y,sidebarTurnEnd as qe,sidebarBusy as M,sidebarMapIndicator as Be,sidebarPullLog as Ne,sidebarSessionId as Me,setSessionLoading as He,sidebarLspState as Ue,sidebarLspEvent as Fe,applySidebarI18n as H,initFiles as We,sidebarFilesState as Oe,sidebarFilesChanged as U}from"./js/sidebar.js";import{insertIntoDraft as je}from"./js/session/composer.js";document.querySelectorAll("[data-collapse]").forEach(s=>{const e=s.querySelector(".arrow");e&&(e.textContent="",e.appendChild(T("chevron-down"))),s.addEventListener("click",()=>{const o=s.nextElementSibling;if(!o)return;const a=o.classList.toggle("collapsed");s.setAttribute("aria-expanded",a?"false":"true");const r=s.querySelector(".arrow");r&&(r.textContent="",r.appendChild(T(a?"chevron-right":"chevron-down")))})});const b=document.getElementById("toggleSidebar");b&&(b.addEventListener("click",()=>{const s=document.body.classList.toggle("sb-open");b.setAttribute("aria-expanded",s?"true":"false")}),document.addEventListener("keydown",s=>{if(s.key!=="Escape"||!document.body.classList.contains("sb-open"))return;const e=document.activeElement?String(document.activeElement.tagName):"";e==="TEXTAREA"||e==="INPUT"||(document.body.classList.remove("sb-open"),b.setAttribute("aria-expanded","false"))}));let I=-1;function m(){const s=l.tabs.find(r=>r.id===l.activeTabId)||null,e=s&&s.state||{};f.host=e;const o=e.sessionId||s&&s.id||null;f.activeSessionId=o,l.tabs.length||w(),de(e.plan),N(f.busy&&e?e.plan:null),_(e),typeof e.contextWindow=="number"&&u.config&&u.config.contextWindow!==e.contextWindow&&(u.config.contextWindow=e.contextWindow,S()),ce(e),Pe(e),Be(e),Ue(),Oe(e);const a=typeof e.filesRevision=="number"?e.filesRevision:void 0;if(a!==void 0&&a!==I){const r=I===-1;I=a,!r&&a>0&&U()}_e(o),le(e),h(),He(!1),B(!!e.i18nStale)}function De(s){const e=X(s);be(),ge(),s.state&&typeof s.state.sessionId=="string"&&pe(s.state.sessionId,s.state.sessionName),e&&m()}async function F(s){const e=s?"/api/state?session="+encodeURIComponent(s):"/api/state",o=await z(e);return o.ok&&o.data?o.data:null}async function W(s){const e=l.tabs.find(a=>a.id===s);if(!e)return;if(e.state){l.activeTabId===s&&m();return}const o=await F(s);o&&(k(o.state),l.activeTabId===s&&(R(o),m()))}async function O(){const s=l.activeTabId,e=await F(s);if(!e||l.activeTabId!==s)return;const o=e.state;!s&&o&&o.sessionId&&!l.tabs.some(a=>a.id===o.sessionId)&&P(o.sessionId,o.sessionName||String(o.sessionId),"session",!1),k(o),R(e),m()}C.ensureTabState=W;function ze(){for(const e of l.tabs)e.kind==="session"&&(e.state=null,W(e.id));O(),q();const s=l.tabs.find(e=>e.id===l.activeTabId);s&&s.kind==="session"&&!fe(s.feed)&&(s.historyLoaded=!1,ue(s))}function Je(){const s="/events"+(A?"?token="+encodeURIComponent(A):""),e=new EventSource(s);let o=!1;e.onopen=()=>{o&&(o=!1,v(!1),ze())},e.onerror=t=>{t&&typeof t.data=="string"||(o=!0,v(!0))};const a=t=>{try{return JSON.parse(t.data)}catch{return null}},r=t=>!t.sessionId||t.sessionId===Me(),c=[["text",t=>K(t.chunk||"",t.sessionId)],["meta",t=>Y(t.chunk||"",t.sessionId,t.kind)],["reasoning",t=>V(t.chunk||"",t.sessionId)],["phase",t=>Z(t.phase,t.sessionId)],["tool_start",$],["tool_end",ee],["plan",ae],["question",ie],["turn_end",t=>{te(t),r(t)&&(qe(t.turnSeq),M(!1),Ne())}],["log",oe],["retract",t=>re(t.reason||"",t.sessionId)]];for(const[t,n]of c)e.addEventListener(t,d=>{const E=a(d);E&&n(E)});for(const t of["tool_start","tool_end","text","plan"])e.addEventListener(t,n=>{const d=a(n);d&&(t==="plan"?N(d):Ce(t,d.name))});for(const t of["text","reasoning","phase","tool_start","plan"])e.addEventListener(t,n=>{const d=a(n);d&&d.sessionId&&p(d.sessionId,"working")});e.addEventListener("question",t=>{const n=a(t);n&&n.sessionId&&p(n.sessionId,"waiting")}),e.addEventListener("turn_end",t=>{const n=a(t);n&&n.sessionId&&p(n.sessionId,n.success===!1?"failed":"idle")});for(const t of["tool_start","tool_end","phase","plan","question","meta","log","retract","index"])e.addEventListener(t,n=>{const d=a(n);d&&r(d)&&y(t,d)});e.addEventListener("text",t=>{const n=a(t);n&&r(n)&&y("text",n)}),e.addEventListener("lsp",t=>{const n=a(t);n&&Fe(n)}),e.addEventListener("files",()=>U()),e.addEventListener("command",t=>{const n=a(t);n&&typeof n.text=="string"&&ne(n.text,n.sessionId)}),e.addEventListener("state",t=>{const n=a(t);n&&(M(!!n.busy),De(n))}),e.addEventListener("sessions",t=>{const n=a(t);n&&Array.isArray(n.sessions)&&(f.sessions=n.sessions,h())}),e.addEventListener("usage",t=>{const n=a(t);if(!n||!n.usage||typeof n.usage!="object")return;const d=n.usageSessionId||n.sessionId||null;G(d,n.usage)&&_(f.host)}),e.addEventListener("error",t=>{if(!t.data)return;const n=a(t);n&&(n.sessionId&&p(n.sessionId,"failed"),se(n),y("error",n))})}function j(){document.documentElement.lang=J.locale;const s=(r,c)=>{const t=document.getElementById(r);t&&(t.textContent=c)};s("planHeaderLabel",i("web.sidebar.plan")),s("usageHeaderLabel",i("web.sidebar.session")+" · "+i("web.sidebar.usage")),s("envHeaderLabel",i("web.sidebar.env")),s("settingsTitle",i("web.settings.title")),s("sessActiveHeader",i("web.sessions.active")),s("sessRecentHeader",i("web.sessions.recent")),s("hintEnter",i("web.input.enter")),s("hintShift",i("web.input.shift_enter")),s("hintEsc",i("web.input.esc")),s("hintAttach",i("web.input.attach"));const e={newChat:["plus",i("web.topbar.new_chat")],sessBtn:["menu",i("web.topbar.all_sessions")],toggleSidebar:["panel",i("web.topbar.sidebar")],openSettings:["settings",i("web.topbar.settings")],attachBtn:["image",i("web.attach.title")],closeSettings:["x",i("web.topbar.close")],sessNew:["plus",i("web.sessions.new")],sessRefresh:["refresh",i("web.sessions.refresh")],sessPrev:["chevron-left",i("web.sessbar.prev")],sessNext:["chevron-right",i("web.sessbar.next")],sessClose:["x",i("web.topbar.close")]};Object.keys(e).forEach(r=>{const c=document.getElementById(r);if(!c)return;const[t,n]=e[r];x(c,t,r==="closeSettings"||r==="sessNew"||r==="sessRefresh"?n:null),c.title=n,c.setAttribute("aria-label",n)});const o=document.getElementById("settingsIcon");o&&x(o,"settings",null),g&&(g.placeholder=i("web.input.placeholder"),g.setAttribute("aria-label",i("web.input.placeholder"))),document.querySelectorAll("#setMenu .mi").forEach(r=>{const c=r.dataset.p;c&&(r.textContent=i("web.settings."+c))});const a=document.getElementById("setSearch");a&&(a.setAttribute("placeholder",i("web.settings.search_placeholder")),a.setAttribute("aria-label",i("web.settings.search_placeholder"))),document.querySelectorAll(".content-col .panel").forEach(r=>{const c=r.querySelector("h3"),t=r.id.replace(/^p-/,"");c&&t&&(c.textContent=i("web.settings."+t))}),S(),h(),ke(),H(),D.querySelector(".empty")&&w(),L&&!L.hidden&&v(!0),B(void 0,!0)}async function Qe(){const s=new URLSearchParams(location.search).get("session");if(window.MmaSetup)try{if(await window.MmaSetup.maybeOpen())return}catch{}await Q(),await me(),Le(xe()),Te(),j(),w(),S(),we(),await he(),Ae(u.config&&u.config.ui?u.config.ui.activityRail:void 0),Re(),H(),We({insertPath:e=>je("`"+e+"` ")}),await Se(),await ve(),await q(),await ye(),await Ie(),await Ee();try{await O()}catch(e){console.error("initial refresh failed",e)}if(s){const e=f.sessions.find(o=>o.id===s);e&&P(s,e.name||String(s),"session",!0)}Je()}C.applyStaticI18n=j,Qe();
1
+ import{msgs as K,inp as S,connStatus as k,setIcon as _,icon as A}from"./js/dom.js";import{api as V,token as R}from"./js/api.js";import{I18N as X,t as a,loadI18n as Y}from"./js/i18n.js";import{tabs as l,session as u,settings as b,hooks as v}from"./js/store.js";import{routeState as Z,mergeUsage as $,storeSnapshot as q,applyCounters as P,shouldHydrateAutoOpen as ee}from"./js/state-sync.js";import{onText as te,onReasoning as se,onMeta as ne,onPhase as oe,onToolStart as ie,onToolEnd as ae,onTurnEnd as re,onErrorEvent as de,onCommandOutput as ce,onLog as le,onPlan as fe,onQuestion as ue,onRetract as pe,showEmptyFeed as y,renderPlan as be,renderUsage as B,renderEnvironment as me,renderWorkspace as he,feedHasContent as we,loadHistory as ge}from"./js/feed.js";import{openTab as F,updateTabName as Se,renderSessionsDropdown as I,reflectBusy as ve,loadSessions as H,loadCommands as ye,setTabStatus as w,reconcileTabStatuses as Ie}from"./js/session/tabs.js";import{renderSelectors as E,renderSettings as Ee,loadConfig as Le,loadModels as xe,loadProviders as Te,loadMemory as Ce,loadSkills as ke,loadPlugins as _e,applyTheme as Ae,currentTheme as Re,applyFontSize as qe,setConnStatus as L,setStaleBar as M}from"./js/settings.js";import{initRail as Pe,setPlan as N,railEvent as Be,applyRailI18n as Fe}from"./js/rail.js";import{initSidebar as He,sidebarSessionChanged as Me,sidebarMapState as Ne,sidebarTraceEvent as x,sidebarTurnEnd as Oe,sidebarBusy as O,sidebarMapIndicator as Ue,sidebarPullLog as We,sidebarSessionId as De,setSessionLoading as je,sidebarLspState as ze,sidebarLspEvent as Je,applySidebarI18n as U,initFiles as Qe,sidebarFilesState as Ge,sidebarFilesChanged as W,sidebarPluginsState as Ke,sidebarPluginsEvent as Ve,initSidebarDrawer as Xe}from"./js/sidebar.js";import{insertIntoDraft as Ye}from"./js/session/composer.js";import{initCursor as Ze,createFace as $e,setFaceState as D,loaderStateFor as et}from"./js/loader.js";let g=null,T=0;function m(s){T++,D(g,s)}function tt(){const s=document.getElementById("composerFace");s&&(g=$e("idle","sm"),s.replaceChildren(g))}document.querySelectorAll("[data-collapse]").forEach(s=>{const e=s.querySelector(".arrow");e&&(e.textContent="",e.appendChild(A("chevron-down"))),s.addEventListener("click",()=>{const i=s.nextElementSibling;if(!i)return;const o=i.classList.toggle("collapsed");s.setAttribute("aria-expanded",o?"false":"true");const r=s.querySelector(".arrow");r&&(r.textContent="",r.appendChild(A(o?"chevron-right":"chevron-down")))})});let C=-1;function h(){const s=l.tabs.find(r=>r.id===l.activeTabId)||null,e=s&&s.state||{};u.host=e;const i=e.sessionId||s&&s.id||null;u.activeSessionId=i,l.tabs.length||y(),be(e.plan),N(u.busy&&e?e.plan:null),B(e),typeof e.contextWindow=="number"&&b.config&&b.config.contextWindow!==e.contextWindow&&(b.config.contextWindow=e.contextWindow,E()),me(e),Ne(e),Ue(e),ze(),Ge(e),Ke();const o=typeof e.filesRevision=="number"?e.filesRevision:void 0;if(o!==void 0&&o!==C){const r=C===-1;C=o,!r&&o>0&&W()}Me(i),he(e),I(),je(!1),M(!!e.i18nStale)}function st(s){const e=Z(s);ve(),Ie(),s.state&&typeof s.state.sessionId=="string"&&Se(s.state.sessionId,s.state.sessionName),e&&h()}async function j(s){const e=s?"/api/state?session="+encodeURIComponent(s):"/api/state",i=await V(e);return i.ok&&i.data?i.data:null}async function z(s){const e=l.tabs.find(o=>o.id===s);if(!e)return;if(e.state){l.activeTabId===s&&h();return}const i=await j(s);i&&(q(i.state),l.activeTabId===s&&(P(i),h()))}async function J(){const s=l.activeTabId,e=await j(s);if(!e||l.activeTabId!==s)return;const i=e.state;if(!s&&i&&i.sessionId&&!l.tabs.some(o=>o.id===i.sessionId)){const o=ee(u.sessions,i.sessionId);F(i.sessionId,i.sessionName||String(i.sessionId),"session",o)}q(i),P(e),h()}v.ensureTabState=z,v.renderActive=h;function nt(){for(const e of l.tabs)e.kind==="session"&&(e.state=null,z(e.id));J(),H();const s=l.tabs.find(e=>e.id===l.activeTabId);s&&s.kind==="session"&&!we(s.feed)&&(s.historyLoaded=!1,ge(s))}function ot(){const s="/events"+(R?"?token="+encodeURIComponent(R):""),e=new EventSource(s);let i=!1;e.onopen=()=>{i&&(i=!1,L(!1),nt())},e.onerror=t=>{t&&typeof t.data=="string"||(i=!0,L(!0))};const o=t=>{try{return JSON.parse(t.data)}catch{return null}},r=t=>!t.sessionId||t.sessionId===De(),c=[["text",t=>te(t.chunk||"",t.sessionId)],["meta",t=>ne(t.chunk||"",t.sessionId,t.kind)],["reasoning",t=>se(t.chunk||"",t.sessionId)],["phase",t=>oe(t.phase,t.sessionId)],["tool_start",ie],["tool_end",ae],["plan",fe],["question",ue],["turn_end",t=>{re(t),r(t)&&(Oe(t.turnSeq),O(!1),We())}],["log",le],["retract",t=>pe(t.reason||"",t.sessionId)]];for(const[t,n]of c)e.addEventListener(t,d=>{const p=o(d);p&&n(p)});for(const t of["tool_start","tool_end","text","plan"])e.addEventListener(t,n=>{const d=o(n);d&&(t==="plan"?N(d):Be(t,d.name))});const f=[["tool_start","tool_start"],["tool_end","tool_end"],["text","text"],["question","question"],["error","error"]];for(const[t,n]of f)e.addEventListener(t,d=>{const p=o(d);p&&m(et({type:n,name:typeof p.name=="string"?p.name:""}))});e.addEventListener("phase",t=>{const n=o(t),d=n&&typeof n.phase=="string"?n.phase:"";m(d==="composing"?"type":d==="tool"?"run":"think")}),e.addEventListener("reasoning",()=>m("think")),e.addEventListener("plan",()=>m("plan")),e.addEventListener("turn_end",t=>{const n=o(t);m(n&&n.success===!1?"error":"done");const d=T;window.setTimeout(()=>{d===T&&D(g,"idle")},1500)});for(const t of["text","reasoning","phase","tool_start","plan"])e.addEventListener(t,n=>{const d=o(n);d&&d.sessionId&&w(d.sessionId,"working")});e.addEventListener("question",t=>{const n=o(t);n&&n.sessionId&&w(n.sessionId,"waiting")}),e.addEventListener("turn_end",t=>{const n=o(t);n&&n.sessionId&&w(n.sessionId,n.success===!1?"failed":"idle")});for(const t of["tool_start","tool_end","phase","plan","question","meta","log","retract","index"])e.addEventListener(t,n=>{const d=o(n);d&&r(d)&&x(t,d)});e.addEventListener("text",t=>{const n=o(t);n&&r(n)&&x("text",n)}),e.addEventListener("lsp",t=>{const n=o(t);n&&Je(n)}),e.addEventListener("files",()=>W()),e.addEventListener("plugins",()=>Ve()),e.addEventListener("command",t=>{const n=o(t);n&&typeof n.text=="string"&&ce(n.text,n.sessionId)}),e.addEventListener("state",t=>{const n=o(t);n&&(O(!!n.busy),st(n))}),e.addEventListener("sessions",t=>{const n=o(t);n&&Array.isArray(n.sessions)&&(u.sessions=n.sessions,I())}),e.addEventListener("usage",t=>{const n=o(t);if(!n||!n.usage||typeof n.usage!="object")return;const d=n.usageSessionId||n.sessionId||null;$(d,n.usage)&&B(u.host)}),e.addEventListener("error",t=>{if(!t.data)return;const n=o(t);n&&(n.sessionId&&w(n.sessionId,"failed"),de(n),x("error",n))})}function Q(){document.documentElement.lang=X.locale;const s=(r,c)=>{const f=document.getElementById(r);f&&(f.textContent=c)};s("planHeaderLabel",a("web.sidebar.plan")),s("usageHeaderLabel",a("web.sidebar.session")+" · "+a("web.sidebar.usage")),s("envHeaderLabel",a("web.sidebar.env")),s("settingsTitle",a("web.settings.title")),s("sessActiveHeader",a("web.sessions.active")),s("sessRecentHeader",a("web.sessions.recent")),s("hintEnter",a("web.input.enter")),s("hintShift",a("web.input.shift_enter")),s("hintEsc",a("web.input.esc")),s("hintAttach",a("web.input.attach")),s("bootCap",a("web.loader.boot"));const e={newChat:["plus",a("web.topbar.new_chat")],sessBtn:["menu",a("web.topbar.all_sessions")],toggleSidebar:["panel",a("web.topbar.sidebar")],openSettings:["settings",a("web.topbar.settings")],attachBtn:["image",a("web.attach.title")],closeSettings:["x",a("web.topbar.close")],sessNew:["plus",a("web.sessions.new")],sessRefresh:["refresh",a("web.sessions.refresh")],sessPrev:["chevron-left",a("web.sessbar.prev")],sessNext:["chevron-right",a("web.sessbar.next")],sessClose:["x",a("web.topbar.close")]};Object.keys(e).forEach(r=>{const c=document.getElementById(r);if(!c)return;const[f,t]=e[r];_(c,f,r==="closeSettings"||r==="sessNew"||r==="sessRefresh"?t:null),c.title=t,c.setAttribute("aria-label",t)});const i=document.getElementById("settingsIcon");i&&_(i,"settings",null),S&&(S.placeholder=a("web.input.placeholder"),S.setAttribute("aria-label",a("web.input.placeholder"))),document.querySelectorAll("#setMenu .mi").forEach(r=>{const c=r.dataset.p;c&&(r.textContent=a("web.settings."+c))});const o=document.getElementById("setSearch");o&&(o.setAttribute("placeholder",a("web.settings.search_placeholder")),o.setAttribute("aria-label",a("web.settings.search_placeholder"))),document.querySelectorAll(".content-col .panel").forEach(r=>{const c=r.querySelector("h3"),f=r.id.replace(/^p-/,"");c&&f&&(c.textContent=a("web.settings."+f))}),E(),I(),Fe(),U(),K.querySelector(".empty")&&y(),k&&!k.hidden&&L(!0),M(void 0,!0)}async function it(){Ze();const s=new URLSearchParams(location.search).get("session");if(window.MmaSetup)try{if(await window.MmaSetup.maybeOpen()){G();return}}catch{}try{await Y(),await ye(),Ae(Re()),qe(),Q(),y(),E(),Ee(),await Le(),Pe(b.config&&b.config.ui?b.config.ui.activityRail:void 0),tt(),He(),Xe(),U(),Qe({insertPath:e=>Ye("`"+e+"` ")}),await xe(),await Te(),await H(),await Ce(),await ke(),await _e();try{await J()}catch(e){console.error("initial refresh failed",e)}if(s){const e=u.sessions.find(i=>i.id===s);e&&F(s,e.name||String(s),"session",!0)}}finally{G()}ot()}function G(){const s=document.getElementById("bootSplash");!s||s.hidden||(s.classList.add("hide"),window.setTimeout(()=>{s.hidden=!0},500))}v.applyStaticI18n=Q,it();
@@ -9,8 +9,29 @@
9
9
  </head>
10
10
  <body>
11
11
 
12
+ <script>
13
+ // Apply the stored theme before the boot splash paints (no dark flash on light).
14
+ (function () {
15
+ try {
16
+ var theme = localStorage.getItem("mma-theme") || "auto";
17
+ var light = theme === "light" ||
18
+ (theme === "auto" && window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches);
19
+ if (light) document.body.classList.add("light");
20
+ } catch (e) { /* ignore */ }
21
+ })();
22
+ </script>
23
+
12
24
  <div class="setup-gate" id="setupGate" hidden></div>
13
25
 
26
+ <div class="splash" id="bootSplash" role="status" aria-live="polite">
27
+ <span class="mdl bs-face c-dim st-load" aria-hidden="true"><span class="eyes"><i class="eye left"></i><i class="eye right"></i></span></span>
28
+ <div class="bs-cap">
29
+ <div class="l1" id="bootCap">connecting…</div>
30
+ <div class="l2">micro-models-agent</div>
31
+ </div>
32
+ <div class="bs-prog" aria-hidden="true"><i></i></div>
33
+ </div>
34
+
14
35
  <div class="bar-top">
15
36
  <button class="icon-btn primary" id="newChat" title="new chat" aria-label="new chat">+</button>
16
37
  <div class="dd" id="sessDD">
@@ -66,6 +87,7 @@
66
87
  </div>
67
88
  </div>
68
89
  <div class="selectors">
90
+ <span class="composer-face" id="composerFace" aria-hidden="true"></span>
69
91
  <div class="sel" data-sel="provider">
70
92
  <button id="selProviderBtn">provider: <b>—</b> ▾</button>
71
93
  <div class="menu" id="selProviderMenu"></div>
@@ -104,26 +126,31 @@
104
126
  <span class="sb-ico" data-icon="session" aria-hidden="true"></span>
105
127
  <span class="sb-dot" aria-hidden="true"></span>
106
128
  </button>
107
- <button class="sb-tab" type="button" role="tab" data-tab="map" aria-selected="false" aria-controls="pane-map" title="Map">
108
- <span class="sb-ico" data-icon="map" aria-hidden="true"></span>
129
+ <button class="sb-tab" type="button" role="tab" data-tab="files" aria-selected="false" aria-controls="pane-files" title="Files">
130
+ <span class="sb-ico" data-icon="files" aria-hidden="true"></span>
131
+ <span class="sb-dot" aria-hidden="true"></span>
132
+ </button>
133
+ <button class="sb-tab" type="button" role="tab" data-tab="lsp" aria-selected="false" aria-controls="pane-lsp" title="LSP">
134
+ <span class="sb-ico" data-icon="lsp" aria-hidden="true"></span>
109
135
  <span class="sb-dot" aria-hidden="true"></span>
110
136
  </button>
111
137
  <button class="sb-tab" type="button" role="tab" data-tab="trace" aria-selected="false" aria-controls="pane-trace" title="Trace">
112
138
  <span class="sb-ico" data-icon="trace" aria-hidden="true"></span>
113
139
  <span class="sb-dot" aria-hidden="true"></span>
114
140
  </button>
115
- <button class="sb-tab" type="button" role="tab" data-tab="lsp" aria-selected="false" aria-controls="pane-lsp" title="LSP">
116
- <span class="sb-ico" data-icon="lsp" aria-hidden="true"></span>
141
+ <button class="sb-tab" type="button" role="tab" data-tab="plugins" aria-selected="false" aria-controls="pane-plugins" title="Plugins">
142
+ <span class="sb-ico" data-icon="plugins" aria-hidden="true"></span>
117
143
  <span class="sb-dot" aria-hidden="true"></span>
118
144
  </button>
119
- <button class="sb-tab" type="button" role="tab" data-tab="files" aria-selected="false" aria-controls="pane-files" title="Files">
120
- <span class="sb-ico" data-icon="files" aria-hidden="true"></span>
145
+ <button class="sb-tab" type="button" role="tab" data-tab="map" aria-selected="false" aria-controls="pane-map" title="Map">
146
+ <span class="sb-ico" data-icon="map" aria-hidden="true"></span>
121
147
  <span class="sb-dot" aria-hidden="true"></span>
122
148
  </button>
123
149
  </div>
124
150
  <div class="sb-name" id="sbName"><span class="sb-ico" data-icon="session" aria-hidden="true"></span><span id="sbNameText">Session</span></div>
125
151
 
126
152
  <div class="sb-pane" id="pane-session" role="tabpanel" data-pane="session">
153
+ <div class="sb-loadface" aria-hidden="true"><span class="mdl c-dim st-think"><span class="eyes"><i class="eye left"></i><i class="eye right"></i></span></span></div>
127
154
  <div class="sec plan-sec">
128
155
  <button class="sec-h" type="button" data-collapse aria-expanded="true"><span class="arrow">▾</span> <span id="planHeaderLabel">plan</span> <span class="n" id="planCount"></span></button>
129
156
  <div class="sec-body" id="plan"></div>
@@ -145,6 +172,7 @@
145
172
  </div>
146
173
 
147
174
  <div class="sb-pane" id="pane-trace" role="tabpanel" data-pane="trace" hidden>
175
+ <div class="trc-face" aria-hidden="true"></div>
148
176
  <div class="trace-list" id="traceList"></div>
149
177
  <div class="trc-detail" id="traceDetail" hidden>
150
178
  <div class="detail-h"><span class="g" id="detailGlyph">◇</span><span id="detailTitle"></span><button class="x linkbtn" type="button" id="detailClose">✕</button></div>
@@ -158,21 +186,43 @@
158
186
  </div>
159
187
  </div>
160
188
 
189
+ <div class="sb-pane" id="pane-plugins" role="tabpanel" data-pane="plugins" hidden>
190
+ <div class="sec map-sec">
191
+ <div class="sec-body" id="pluginsBody"></div>
192
+ </div>
193
+ </div>
194
+
161
195
  <div class="sb-pane" id="pane-files" role="tabpanel" data-pane="files" hidden>
162
- <div class="files-tree" id="filesTree"></div>
163
- <div class="files-detail" id="filesDetail" hidden>
164
- <div class="detail-h">
165
- <span class="g" id="filesGlyph" aria-hidden="true">◆</span>
166
- <span id="filesTitle"></span>
167
- <button class="x linkbtn" type="button" id="filesAttach" title="attach path">◧</button>
168
- <button class="x linkbtn" type="button" id="filesClose" title="close">✕</button>
196
+ <div class="files-body">
197
+ <div class="files-side" id="filesSide">
198
+ <div class="files-bar" id="filesBar">
199
+ <button class="fbtn" type="button" id="filesCollapseAll" aria-label="collapse all"></button>
200
+ <button class="fbtn" type="button" id="filesExpandAll" aria-label="expand all"></button>
201
+ <button class="fbtn" type="button" id="filesRefresh" aria-label="refresh"></button>
202
+ </div>
203
+ <div class="files-tree" id="filesTree" role="tree" tabindex="0" aria-label="files"></div>
204
+ </div>
205
+ <div class="files-splitter" id="filesSplitter" role="separator" aria-orientation="vertical" tabindex="0" title="tree width"></div>
206
+ <div class="files-detail" id="filesDetail" hidden>
207
+ <div class="detail-h">
208
+ <button class="fbtn" type="button" id="filesBack" aria-label="back" disabled></button>
209
+ <button class="fbtn" type="button" id="filesFwd" aria-label="forward" disabled></button>
210
+ <span class="fcrumbs" id="filesCrumbs"></span>
211
+ <button class="fbtn" type="button" id="filesAttach" aria-label="attach path"></button>
212
+ <button class="fbtn" type="button" id="filesWide" aria-label="hide tree"></button>
213
+ <button class="fbtn" type="button" id="filesClose" aria-label="close"></button>
214
+ </div>
215
+ <div class="detail-b" id="filesBody"></div>
169
216
  </div>
170
- <div class="detail-b" id="filesBody"></div>
171
217
  </div>
172
218
  </div>
173
219
  </div>
174
220
  </div>
175
221
 
222
+ <!-- Drawer backdrop for ≤1024px, where the sidebar overlays the chat
223
+ (visible only under body.sb-open, see styles.css + sidebar/drawer.ts). -->
224
+ <div class="sb-backdrop" id="sbBackdrop"></div>
225
+
176
226
  <div class="overlay" id="overlay">
177
227
  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="settingsTitle">
178
228
  <div class="modal-top"><span class="modal-top-icon" id="settingsIcon" aria-hidden="true"></span> <span id="settingsTitle">settings</span> <span class="saved" id="saveIndicator" role="status" aria-live="polite"></span> <button class="x linkbtn" id="closeSettings" type="button">✕ close</button></div>
@@ -1 +1 @@
1
- export const msgs=document.getElementById("msgs"),inp=document.getElementById("inp"),tabstripEl=document.getElementById("tabstrip"),planEl=document.getElementById("plan"),planCountEl=document.getElementById("planCount"),usageEl=document.getElementById("usage"),envEl=document.getElementById("env"),wsLine=document.getElementById("wsLine"),saveIndicator=document.getElementById("saveIndicator"),attachStrip=document.getElementById("attachStrip"),attachErr=document.getElementById("attachErr"),imgPicker=document.getElementById("imgPicker"),connStatus=document.getElementById("connStatus"),staleBar=document.getElementById("staleBar"),liveEl=document.getElementById("live"),sendBtn=document.getElementById("sendBtn");export function announce(e){!liveEl||!e||(liveEl.textContent="",setTimeout(()=>{liveEl.textContent=e},30))}export function el(e,a,t){const n=document.createElement(e);return a&&(n.className=a),t!=null&&(n.textContent=String(t)),n}let i=null,s=!1;function p(){return i||(i=el("div","ctip"),i.setAttribute("role","tooltip"),document.body.appendChild(i)),i}function c(){i&&i.classList.remove("show")}function h(){s||(s=!0,window.addEventListener("scroll",c,!0),window.addEventListener("resize",c))}export function attachTooltip(e,a){const t=()=>{const n=p();n.replaceChildren(a()),n.classList.add("show");const o=e.getBoundingClientRect(),r=n.getBoundingClientRect();let d=o.left+o.width/2-r.width/2;d=Math.max(6,Math.min(d,window.innerWidth-r.width-6));let l=o.top-r.height-7;l<6&&(l=o.bottom+7),n.style.left=d+"px",n.style.top=l+"px"};e.addEventListener("mouseenter",t),e.addEventListener("mouseleave",c),e.addEventListener("focus",t),e.addEventListener("blur",c),h()}export const SVG_NS="http://www.w3.org/2000/svg",ICONS={plus:[["path",{d:"M5 12h14"}],["path",{d:"M12 5v14"}]],menu:[["line",{x1:4,y1:6,x2:20,y2:6}],["line",{x1:4,y1:12,x2:20,y2:12}],["line",{x1:4,y1:18,x2:20,y2:18}]],settings:[["path",{d:"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"}],["circle",{cx:12,cy:12,r:3}]],image:[["rect",{width:18,height:18,x:3,y:3,rx:2,ry:2}],["circle",{cx:9,cy:9,r:2}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"}]],x:[["path",{d:"M18 6 6 18"}],["path",{d:"m6 6 12 12"}]],trash:[["path",{d:"M3 6h18"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"}],["line",{x1:10,y1:11,x2:10,y2:17}],["line",{x1:14,y1:11,x2:14,y2:17}]],refresh:[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"}],["path",{d:"M21 3v5h-5"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"}],["path",{d:"M8 21H3v-5"}]],"chevron-down":[["path",{d:"m6 9 6 6 6-6"}]],"chevron-left":[["path",{d:"m15 18-6-6 6-6"}]],"chevron-right":[["path",{d:"m9 18 6-6-6-6"}]],search:[["circle",{cx:11,cy:11,r:8}],["path",{d:"m21 21-4.3-4.3"}]],"list-checks":[["path",{d:"m3 17 2 2 4-4"}],["path",{d:"m3 7 2 2 4-4"}],["path",{d:"M13 6h8"}],["path",{d:"M13 12h8"}],["path",{d:"M13 18h8"}]],map:[["path",{d:"M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z"}],["path",{d:"M15 5.764v15"}],["path",{d:"M9 3.236v15"}]],activity:[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"}]],"arrow-up":[["path",{d:"M12 19V5"}],["path",{d:"m5 12 7-7 7 7"}]],panel:[["rect",{width:18,height:18,x:3,y:3,rx:2,ry:2}],["line",{x1:15,y1:3,x2:15,y2:21}]],square:[["rect",{width:14,height:14,x:5,y:5,rx:2}]],copy:[["rect",{width:14,height:14,x:8,y:8,rx:2,ry:2}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],check:[["path",{d:"M20 6 9 17l-5-5"}]],info:[["circle",{cx:12,cy:12,r:10}],["path",{d:"M12 16v-4"}],["path",{d:"M12 8h.01"}]],stethoscope:[["path",{d:"M11 2v2"}],["path",{d:"M5 2v2"}],["path",{d:"M5 3H4a2 2 0 0 0-2 2v4a6 6 0 0 0 12 0V5a2 2 0 0 0-2-2h-1"}],["path",{d:"M8 15a6 6 0 0 0 12 0v-3"}],["circle",{cx:20,cy:10,r:2}]],"folder-tree":[["path",{d:"M20 10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2.5a1 1 0 0 1-.8-.4l-.9-1.2A1 1 0 0 0 15 3h-2a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"}],["path",{d:"M20 21a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2.9a1 1 0 0 1-.88-.55l-.42-.85a1 1 0 0 0-.9-.6H13a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"}],["path",{d:"M3 5a2 2 0 0 1 2-2h3"}],["path",{d:"M3 3v13a3 3 0 0 0 3 3h4"}]],folder:[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}]],file:[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}]],paperclip:[["path",{d:"m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"}]]};export function icon(e,a){const t=document.createElementNS(SVG_NS,"svg");return t.setAttribute("viewBox","0 0 24 24"),t.setAttribute("fill","none"),t.setAttribute("stroke","currentColor"),t.setAttribute("stroke-width","2"),t.setAttribute("stroke-linecap","round"),t.setAttribute("stroke-linejoin","round"),t.setAttribute("aria-hidden","true"),t.setAttribute("focusable","false"),t.setAttribute("class","svg-icon"+(a?" "+a:"")),(ICONS[e]||[]).forEach(([n,o])=>{const r=document.createElementNS(SVG_NS,n);Object.keys(o).forEach(d=>r.setAttribute(d,String(o[d]))),t.appendChild(r)}),t}export function setIcon(e,a,t){return e?(e.textContent="",e.appendChild(icon(a)),t!=null&&e.appendChild(el("span","",t)),e):null}export function makeToggle(e,a){const t=el("button","toggle"+(e?" on":""));return t.type="button",t.setAttribute("role","switch"),t.setAttribute("aria-checked",e?"true":"false"),t.addEventListener("click",()=>{const n=!t.classList.contains("on");a(n)&&(t.classList.toggle("on",n),t.setAttribute("aria-checked",n?"true":"false"))}),t}export function link(e,a,t){const n=el("button","linkbtn"+(a?" "+a:""),e);return n.type="button",n.addEventListener("click",t),n}export function inlineTextNodes(e){const a=document.createDocumentFragment();return String(e??"").replace(/^-\s*/,"").split("**").forEach((n,o)=>{if(n!=="")if(o%2===1){const r=document.createElement("b");r.textContent=n,a.appendChild(r)}else a.appendChild(document.createTextNode(n))}),a}
1
+ export const msgs=document.getElementById("msgs"),inp=document.getElementById("inp"),tabstripEl=document.getElementById("tabstrip"),planEl=document.getElementById("plan"),planCountEl=document.getElementById("planCount"),usageEl=document.getElementById("usage"),envEl=document.getElementById("env"),wsLine=document.getElementById("wsLine"),saveIndicator=document.getElementById("saveIndicator"),attachStrip=document.getElementById("attachStrip"),attachErr=document.getElementById("attachErr"),imgPicker=document.getElementById("imgPicker"),connStatus=document.getElementById("connStatus"),staleBar=document.getElementById("staleBar"),liveEl=document.getElementById("live"),sendBtn=document.getElementById("sendBtn");export function announce(e){!liveEl||!e||(liveEl.textContent="",setTimeout(()=>{liveEl.textContent=e},30))}export function el(e,a,t){const n=document.createElement(e);return a&&(n.className=a),t!=null&&(n.textContent=String(t)),n}let d=null,h=!1;function p(){return d||(d=el("div","ctip"),d.setAttribute("role","tooltip"),document.body.appendChild(d)),d}function c(){d&&d.classList.remove("show")}function s(){h||(h=!0,window.addEventListener("scroll",c,!0),window.addEventListener("resize",c))}export function attachTooltip(e,a){const t=()=>{const n=p();n.replaceChildren(a()),n.classList.add("show");const r=e.getBoundingClientRect(),o=n.getBoundingClientRect();let i=r.left+r.width/2-o.width/2;i=Math.max(6,Math.min(i,window.innerWidth-o.width-6));let l=r.top-o.height-7;l<6&&(l=r.bottom+7),n.style.left=i+"px",n.style.top=l+"px"};e.addEventListener("mouseenter",t),e.addEventListener("mouseleave",c),e.addEventListener("focus",t),e.addEventListener("blur",c),s()}export const SVG_NS="http://www.w3.org/2000/svg",ICONS={plus:[["path",{d:"M5 12h14"}],["path",{d:"M12 5v14"}]],menu:[["line",{x1:4,y1:6,x2:20,y2:6}],["line",{x1:4,y1:12,x2:20,y2:12}],["line",{x1:4,y1:18,x2:20,y2:18}]],settings:[["path",{d:"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"}],["circle",{cx:12,cy:12,r:3}]],image:[["rect",{width:18,height:18,x:3,y:3,rx:2,ry:2}],["circle",{cx:9,cy:9,r:2}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"}]],x:[["path",{d:"M18 6 6 18"}],["path",{d:"m6 6 12 12"}]],trash:[["path",{d:"M3 6h18"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"}],["line",{x1:10,y1:11,x2:10,y2:17}],["line",{x1:14,y1:11,x2:14,y2:17}]],refresh:[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"}],["path",{d:"M21 3v5h-5"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"}],["path",{d:"M8 21H3v-5"}]],"chevron-down":[["path",{d:"m6 9 6 6 6-6"}]],"chevron-left":[["path",{d:"m15 18-6-6 6-6"}]],"chevron-right":[["path",{d:"m9 18 6-6-6-6"}]],search:[["circle",{cx:11,cy:11,r:8}],["path",{d:"m21 21-4.3-4.3"}]],"list-checks":[["path",{d:"m3 17 2 2 4-4"}],["path",{d:"m3 7 2 2 4-4"}],["path",{d:"M13 6h8"}],["path",{d:"M13 12h8"}],["path",{d:"M13 18h8"}]],"layout-grid":[["rect",{width:7,height:7,x:3,y:3,rx:1}],["rect",{width:7,height:7,x:14,y:3,rx:1}],["rect",{width:7,height:7,x:14,y:14,rx:1}],["rect",{width:7,height:7,x:3,y:14,rx:1}]],code:[["path",{d:"m16 18 6-6-6-6"}],["path",{d:"m8 6-6 6 6 6"}]],plug:[["path",{d:"M12 22v-5"}],["path",{d:"M9 8V2"}],["path",{d:"M15 8V2"}],["path",{d:"M18 8v5a6 6 0 0 1-12 0V8Z"}]],activity:[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"}]],"arrow-up":[["path",{d:"M12 19V5"}],["path",{d:"m5 12 7-7 7 7"}]],"arrow-left":[["path",{d:"M19 12H5"}],["path",{d:"m12 19-7-7 7-7"}]],"arrow-right":[["path",{d:"M5 12h14"}],["path",{d:"m12 5 7 7-7 7"}]],"chevrons-down-up":[["path",{d:"m7 15 5 5 5-5"}],["path",{d:"m7 9 5-5 5 5"}]],"chevrons-up-down":[["path",{d:"m7 9 5-5 5 5"}],["path",{d:"m7 15 5 5 5-5"}]],panel:[["rect",{width:18,height:18,x:3,y:3,rx:2,ry:2}],["line",{x1:15,y1:3,x2:15,y2:21}]],"panel-left":[["rect",{width:18,height:18,x:3,y:3,rx:2,ry:2}],["line",{x1:9,y1:3,x2:9,y2:21}]],square:[["rect",{width:14,height:14,x:5,y:5,rx:2}]],copy:[["rect",{width:14,height:14,x:8,y:8,rx:2,ry:2}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],check:[["path",{d:"M20 6 9 17l-5-5"}]],info:[["circle",{cx:12,cy:12,r:10}],["path",{d:"M12 16v-4"}],["path",{d:"M12 8h.01"}]],"folder-tree":[["path",{d:"M20 10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2.5a1 1 0 0 1-.8-.4l-.9-1.2A1 1 0 0 0 15 3h-2a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"}],["path",{d:"M20 21a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2.9a1 1 0 0 1-.88-.55l-.42-.85a1 1 0 0 0-.9-.6H13a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"}],["path",{d:"M3 5a2 2 0 0 1 2-2h3"}],["path",{d:"M3 3v13a3 3 0 0 0 3 3h4"}]],folder:[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}]],file:[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4"}]],paperclip:[["path",{d:"m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"}]]};export function icon(e,a){const t=document.createElementNS(SVG_NS,"svg");return t.setAttribute("viewBox","0 0 24 24"),t.setAttribute("fill","none"),t.setAttribute("stroke","currentColor"),t.setAttribute("stroke-width","2"),t.setAttribute("stroke-linecap","round"),t.setAttribute("stroke-linejoin","round"),t.setAttribute("aria-hidden","true"),t.setAttribute("focusable","false"),t.setAttribute("class","svg-icon"+(a?" "+a:"")),(ICONS[e]||[]).forEach(([n,r])=>{const o=document.createElementNS(SVG_NS,n);Object.keys(r).forEach(i=>o.setAttribute(i,String(r[i]))),t.appendChild(o)}),t}export function setIcon(e,a,t){return e?(e.textContent="",e.appendChild(icon(a)),t!=null&&e.appendChild(el("span","",t)),e):null}export function makeToggle(e,a){const t=el("button","toggle"+(e?" on":""));return t.type="button",t.setAttribute("role","switch"),t.setAttribute("aria-checked",e?"true":"false"),t.addEventListener("click",()=>{const n=!t.classList.contains("on");a(n)&&(t.classList.toggle("on",n),t.setAttribute("aria-checked",n?"true":"false"))}),t}export function link(e,a,t){const n=el("button","linkbtn"+(a?" "+a:""),e);return n.type="button",n.addEventListener("click",t),n}export function inlineTextNodes(e){const a=document.createDocumentFragment();return String(e??"").replace(/^-\s*/,"").split("**").forEach((n,r)=>{if(n!=="")if(r%2===1){const o=document.createElement("b");o.textContent=n,a.appendChild(o)}else a.appendChild(document.createTextNode(n))}),a}
@@ -1 +1 @@
1
- import{msgs as s,el as i,setIcon as C}from"../dom.js";import{t as l,pluralCount as w}from"../i18n.js";import{feed as o,tabs as a}from"../store.js";import{formatMs as x,verbosity as T}from"../format.js";import{copyText as v}from"../../md.js";export function mdOpts(t){return{copyLabel:l("web.code.copy"),copiedLabel:l("web.code.copied"),copyFailedLabel:l("web.code.copy_failed"),animate:t===!0}}export function buildThinkBlock(t,e){const n=i("div","think"+(e===!1?"":" live")),r=i("button","think-h");r.type="button",r.setAttribute("aria-expanded","false"),r.appendChild(i("span","g","✦")),r.appendChild(i("span","lab",l("web.action.thinking"))),t&&r.appendChild(i("span","dur",""));const d=i("span","tog");C(d,"chevron-right",l("web.action.show")),r.appendChild(d);const p=i("div","think-body hidden");return n.appendChild(r),n.appendChild(p),{wrap:n,head:r,body:p}}export function copyAction(t,e){const n=i("button","act-btn",l(e));return n.type="button",n.addEventListener("click",()=>{v(t).then(r=>{n.textContent=r?"✓ "+l("web.code.copied"):"⚠ "+l("web.code.copy_failed"),window.setTimeout(()=>{n.textContent=l(e)},1400)})}),n}export function appendActionsRow(t,e,n){const r=String(e??""),d=String(n??"");if(!t||!r.trim()&&!d.trim())return;const p=i("div","msg-actions");r.trim()&&p.appendChild(copyAction(r,"web.msg.copy")),d.trim()&&p.appendChild(copyAction(d,"web.msg.copy_md")),appendTo(t,p)}export function insertAfter(t,e){t&&t.parentNode?t.parentNode.insertBefore(e,t.nextSibling):appendTo(liveFeed(),e)}function b(){const t=i("div","empty");return t.appendChild(i("span","g","✦")),t.appendChild(document.createTextNode(" "+l("web.empty.title"))),t.appendChild(document.createElement("br")),t.appendChild(i("span","hint",l("web.empty.hint"))),t}export function makeFeed(){const t=i("div","feed");return t.appendChild(b()),t}export function showEmptyFeed(){s.replaceChildren(b()),u=!0,f(),syncUrl()}export function clearFeed(t){if(!t)return;t.replaceChildren(b()),u=!0,f();const e=a.tabs.find(n=>n.feed===t);e&&(e.planCard=null),t===activeFeed()&&scroll()}export function activeFeed(){const t=a.tabs.find(e=>e.id===a.activeTabId);return t?t.feed:null}export function liveFeed(){const t=a.tabs.find(e=>e.id===a.liveTabId)||a.tabs.find(e=>e.id===a.activeTabId);return t?t.feed:null}export function feedForEvent(t){if(t){const e=a.tabs.find(n=>n.id===t);if(e)return e.feed}return liveFeed()}function h(){return s.scrollHeight-s.scrollTop-s.clientHeight<140}let u=!0;export function scroll(t){t||u||h()?(s.scrollTop=s.scrollHeight,u=!0,f()):m()}export function scrollToBottom(t){if(u=!0,f(),t&&typeof s.scrollTo=="function")try{s.scrollTo({top:s.scrollHeight,behavior:"smooth"});return}catch{}s.scrollTop=s.scrollHeight}export function isFollowing(){return u}export function feedHasContent(t){return!!t.querySelector(".u, .md, .ans, .code, .act, .think, .metaln, .errline, .sysline, .detail")}export function syncUrl(){const t=new URL(location.href),e=a.tabs.find(n=>n.id===a.activeTabId);e&&e.kind==="session"?t.searchParams.set("session",e.id):t.searchParams.delete("session"),history.replaceState(null,"",t)}let c=null;function g(){if(c)return c;const t=document.querySelector(".chat");return t?(c=i("button","to-latest"),c.type="button",c.hidden=!0,c.addEventListener("click",()=>scrollToBottom(!0)),t.appendChild(c),c):null}function m(){const t=g();t&&(t.textContent="↓ "+l("web.scroll.latest"),t.hidden=!1,s.classList.add("unfollowed"))}function f(){c&&(c.hidden=!0),s.classList.remove("unfollowed")}s?.addEventListener("scroll",()=>{const t=h();u=t,s.classList.toggle("scrolled",s.scrollTop>24),t?f():liveFeed()&&feedHasContent(liveFeed())&&m()});export function appendTo(t,e){return t&&(t.appendChild(e),t===activeFeed()&&scroll()),e}export function clearEmpty(t){const e=t&&t.querySelector(".empty");e&&e.remove()}export function clearBusy(t){if(o.busyTimer&&(clearInterval(o.busyTimer),o.busyTimer=null),t=t||liveFeed(),!t)return;const e=t.querySelector(".busy");e&&e.remove()}export function showBusy(t){if(t=t||liveFeed(),!t||o.curMd||o.curThink||(clearEmpty(t),t.querySelector(".busy")))return;o.busyTimer&&(clearInterval(o.busyTimer),o.busyTimer=null);const e=i("div","busy");e.appendChild(i("span","spin","✦")),e.appendChild(i("span","lab",l("web.action.thinking")));const n=i("span","busytime","· 0s");e.appendChild(n),e.appendChild(i("span","","· "+l("web.input.esc"))),appendTo(t,e);const r=Date.now();o.busyTimer=setInterval(()=>{if(!e.parentNode){clearInterval(o.busyTimer),o.busyTimer=null;return}n.textContent="· "+Math.round((Date.now()-r)/1e3)+"s"},1e3)}export function flushRun(){if(o.runCount<=0){y();return}if(T()==="quiet"&&o.runReconOnly&&o.runFails===0&&o.runCount>1){const t=o.runNodes.slice();t.forEach(r=>r.classList.add("fold-out")),window.setTimeout(()=>t.forEach(r=>r.remove()),160);const e=i("div","grouped");e.appendChild(i("span","pipe","│"));const n=x(o.runDuration);e.appendChild(document.createTextNode(" "+w("web.action.actions",o.runCount)+" ✓"+(n?" · "+n:""))),appendTo(liveFeed(),e)}y()}function y(){o.runCount=0,o.runFails=0,o.runReconOnly=!0,o.runNodes=[],o.runDuration=0}
1
+ import{msgs as s,el as r,setIcon as C}from"../dom.js";import{t as l,pluralCount as w}from"../i18n.js";import{feed as o,tabs as c}from"../store.js";import{formatMs as x,verbosity as T}from"../format.js";import{copyText as v}from"../../md.js";import{createFace as g}from"../loader.js";export function mdOpts(t){return{copyLabel:l("web.code.copy"),copiedLabel:l("web.code.copied"),copyFailedLabel:l("web.code.copy_failed"),animate:t===!0}}export function buildThinkBlock(t,e){const n=r("div","think"+(e===!1?"":" live")),i=r("button","think-h");i.type="button",i.setAttribute("aria-expanded","false"),i.appendChild(r("span","g","✦")),i.appendChild(r("span","lab",l("web.action.thinking"))),t&&i.appendChild(r("span","dur",""));const d=r("span","tog");C(d,"chevron-right",l("web.action.show")),i.appendChild(d);const p=r("div","think-body hidden");return n.appendChild(i),n.appendChild(p),{wrap:n,head:i,body:p}}export function copyAction(t,e){const n=r("button","act-btn",l(e));return n.type="button",n.addEventListener("click",()=>{v(t).then(i=>{n.textContent=i?"✓ "+l("web.code.copied"):"⚠ "+l("web.code.copy_failed"),window.setTimeout(()=>{n.textContent=l(e)},1400)})}),n}export function appendActionsRow(t,e,n){const i=String(e??""),d=String(n??"");if(!t||!i.trim()&&!d.trim())return;const p=r("div","msg-actions");i.trim()&&p.appendChild(copyAction(i,"web.msg.copy")),d.trim()&&p.appendChild(copyAction(d,"web.msg.copy_md")),appendTo(t,p)}export function insertAfter(t,e){t&&t.parentNode?t.parentNode.insertBefore(e,t.nextSibling):appendTo(liveFeed(),e)}function b(){const t=r("div","empty");return t.appendChild(r("span","g","✦")),t.appendChild(document.createTextNode(" "+l("web.empty.title"))),t.appendChild(document.createElement("br")),t.appendChild(r("span","hint",l("web.empty.hint"))),t}export function makeFeed(){const t=r("div","feed");return t.appendChild(b()),t}export function showEmptyFeed(){s.replaceChildren(b()),u=!0,f(),syncUrl()}export function clearFeed(t){if(!t)return;t.replaceChildren(b()),u=!0,f();const e=c.tabs.find(n=>n.feed===t);e&&(e.planCard=null),t===activeFeed()&&scroll()}export function activeFeed(){const t=c.tabs.find(e=>e.id===c.activeTabId);return t?t.feed:null}export function liveFeed(){const t=c.tabs.find(e=>e.id===c.liveTabId)||c.tabs.find(e=>e.id===c.activeTabId);return t?t.feed:null}export function feedForEvent(t){if(t){const e=c.tabs.find(n=>n.id===t);if(e)return e.feed}return liveFeed()}function h(){return s.scrollHeight-s.scrollTop-s.clientHeight<140}let u=!0;export function scroll(t){t||u||h()?(s.scrollTop=s.scrollHeight,u=!0,f()):m()}export function scrollToBottom(t){if(u=!0,f(),t&&typeof s.scrollTo=="function")try{s.scrollTo({top:s.scrollHeight,behavior:"smooth"});return}catch{}s.scrollTop=s.scrollHeight}export function isFollowing(){return u}export function feedHasContent(t){return!!t.querySelector(".u, .md, .ans, .code, .act, .think, .metaln, .errline, .sysline, .detail")}export function syncUrl(){const t=new URL(location.href),e=c.tabs.find(n=>n.id===c.activeTabId);e&&e.kind==="session"?t.searchParams.set("session",e.id):t.searchParams.delete("session"),history.replaceState(null,"",t)}let a=null;function L(){if(a)return a;const t=document.querySelector(".chat");return t?(a=r("button","to-latest"),a.type="button",a.hidden=!0,a.addEventListener("click",()=>scrollToBottom(!0)),t.appendChild(a),a):null}function m(){const t=L();t&&(t.textContent="↓ "+l("web.scroll.latest"),t.hidden=!1,s.classList.add("unfollowed"))}function f(){a&&(a.hidden=!0),s.classList.remove("unfollowed")}s?.addEventListener("scroll",()=>{const t=h();u=t,s.classList.toggle("scrolled",s.scrollTop>24),t?f():liveFeed()&&feedHasContent(liveFeed())&&m()});export function appendTo(t,e){return t&&(t.appendChild(e),t===activeFeed()&&scroll()),e}export function clearEmpty(t){const e=t&&t.querySelector(".empty");e&&e.remove()}export function clearBusy(t){if(o.busyTimer&&(clearInterval(o.busyTimer),o.busyTimer=null),t=t||liveFeed(),!t)return;const e=t.querySelector(".busy");e&&e.remove()}export function showBusy(t){if(t=t||liveFeed(),!t||o.curMd||o.curThink||(clearEmpty(t),t.querySelector(".busy")))return;o.busyTimer&&(clearInterval(o.busyTimer),o.busyTimer=null);const e=r("div","busy");e.appendChild(g("think","sm")),e.appendChild(r("span","lab",l("web.action.thinking")));const n=r("span","busytime","· 0s");e.appendChild(n),e.appendChild(r("span","","· "+l("web.input.esc"))),appendTo(t,e);const i=Date.now();o.busyTimer=setInterval(()=>{if(!e.parentNode){clearInterval(o.busyTimer),o.busyTimer=null;return}n.textContent="· "+Math.round((Date.now()-i)/1e3)+"s"},1e3)}export function flushRun(){if(o.runCount<=0){y();return}if(T()==="quiet"&&o.runReconOnly&&o.runFails===0&&o.runCount>1){const t=o.runNodes.slice();t.forEach(i=>i.classList.add("fold-out")),window.setTimeout(()=>t.forEach(i=>i.remove()),160);const e=r("div","grouped");e.appendChild(r("span","pipe","│"));const n=x(o.runDuration);e.appendChild(document.createTextNode(" "+w("web.action.actions",o.runCount)+" ✓"+(n?" · "+n:""))),appendTo(liveFeed(),e)}y()}function y(){o.runCount=0,o.runFails=0,o.runReconOnly=!0,o.runNodes=[],o.runDuration=0}
@@ -1 +1 @@
1
- import{usageEl as p,envEl as x,wsLine as m,el as s,icon as v,attachTooltip as k}from"../dom.js";import{I18N as S,t as a,pluralCount as y}from"../i18n.js";import{feed as g,settings as E}from"../store.js";import{fmtTokens as b}from"../format.js";import{copyText as L}from"../../md.js";function f(e,o,n){const t=s("div","kv");return t.appendChild(s("span","",e)),t.appendChild(s("b",n||"",o)),t}function _(e){return[["seg-system","web.usage.part_system",e.system],["seg-tools","web.usage.part_tools",e.tools],["seg-history","web.usage.part_history",e.history],["seg-reserve","web.usage.part_reserve",e.reserve],["seg-free","web.usage.part_free",e.free]]}function M(e,o,n,t){const i=document.createDocumentFragment();return i.appendChild(s("i","ctip-dot "+e)),i.appendChild(s("span","ctip-label",a(o))),i.appendChild(s("b","",b(n))),i.appendChild(s("span","ctip-pct",Math.round(n/t*100)+"%")),i}const U=new Map;function A(e,o,n){const t=U.get(o);if(U.set(o,n),t===void 0||t===n){e.style.flexGrow=String(n);return}e.style.flexGrow=String(t);const i=()=>{e.style.flexGrow=String(n)};typeof requestAnimationFrame=="function"?requestAnimationFrame(i):i()}function I(e,o,n){const t=s("div","bar segbar"),i=e.contextBreakdown;if(!i||typeof i.window!="number"||i.window<=0){const d=s("i");return d.style.flex="0 0 "+Math.min(100,Math.round(o/n*100))+"%",t.appendChild(d),t}const r=Math.max(i.window,i.used+i.reserve);for(const[d,l,c]of _(i)){if(!c||c<=0)continue;const u=s("i",d);u.style.flexBasis="0",A(u,d,c),u.tabIndex=0,u.setAttribute("aria-label",[a(l),b(c),Math.round(c/r*100)+"%"].join(" · ")),k(u,()=>M(d,l,c,r)),t.appendChild(u)}return t}function R(e){const o=s("div","seglegend"),n=e.contextBreakdown;if(!n)return o;const t=Math.max(n.window,n.used+n.reserve);for(const[i,r,d]of _(n)){if(!d||d<=0)continue;const l=s("span","segl-item");l.tabIndex=0,l.setAttribute("aria-label",[a(r),b(d),Math.round(d/t*100)+"%"].join(" · ")),l.appendChild(s("i","segl-dot "+i)),l.appendChild(s("span","segl-label",a(r))),l.appendChild(s("b","",b(d))),k(l,()=>M(i,r,d,t)),o.appendChild(l)}return o}function T(e){const o=s("div","kv");o.appendChild(s("span","",a("web.usage.session")));const n=s("span","kv-val");if(!e)n.appendChild(s("b","","—"));else{n.appendChild(s("b","",e));const t=s("button","copy-btn");t.type="button",t.title=a("web.usage.copy_id"),t.setAttribute("aria-label",a("web.usage.copy_id")),t.appendChild(v("copy")),t.addEventListener("click",()=>{L(e).then(i=>{t.replaceChildren(v(i?"check":"copy")),setTimeout(()=>t.replaceChildren(v("copy")),1500)})}),n.appendChild(t)}return o.appendChild(n),o}export function renderUsage(e){const o=E.config||{},n=JSON.stringify([e.sessionId,e.model,e.contextUsed,e.contextLimit,e.cacheHitRate,e.costUsd,e.compactionCount,e.contextQuality,e.contextProbe,e.contextBreakdown,o.model,o.contextWindow,S.locale]);if(n===g.lastUsageSig)return;g.lastUsageSig=n,p.textContent="";const t=e.sessionId?String(e.sessionId):"";p.appendChild(T(t)),p.appendChild(f(a("web.usage.model"),e.model||o.model||"—"));const i=typeof e.contextUsed=="number"?e.contextUsed:null,r=typeof e.contextLimit=="number"?e.contextLimit:o.contextWindow||null;p.appendChild(f(a("web.usage.context"),i!=null&&r?b(i)+" / "+b(r):"—")),i!=null&&r&&(p.appendChild(I(e,i,r)),p.appendChild(R(e)));const d=typeof e.cacheHitRate=="number"?e.cacheHitRate:null;p.appendChild(f(a("web.usage.cache"),d!=null?Math.round(d*100)+"%":"—",d!=null?"":"dim")),p.appendChild(f(a("web.usage.cost"),typeof e.costUsd=="number"?"$"+e.costUsd.toFixed(4):"—")),p.appendChild(f(a("web.usage.compactions"),"⟳ "+(e.compactionCount||0),e.compactionCount?"wn":"dim"));const l=typeof e.contextQuality=="number"?e.contextQuality:null;p.appendChild(f(a("web.usage.quality"),l!=null?Math.round(l)+"%":"—",l!=null?"":"dim"));let c="—",u="dim";const w=e.contextProbe;if(w&&typeof w.actual=="number"){const h=b(w.actual),C=e.contextLimit||o.contextWindow||null;C&&w.actual<C?(c=a("web.usage.probe_low",{actual:h,configured:b(C)}),u="wn"):(c=a("web.usage.probe_loaded",{actual:h}),u="")}p.appendChild(f(a("web.usage.probe"),c,u))}export function renderWorkspace(e){const o=e.workspace||"";if(m.dataset.ws===o||(m.dataset.ws=o,m.textContent="",m.hidden=!o,!o))return;m.appendChild(s("span","ws-k",a("web.env.dir")));const n=s("span","ws-v",o);n.title=o,m.appendChild(n)}function B(e,o){const n=s("div","env-item");return n.appendChild(s("div","env-k",e)),o.forEach(t=>{const i=s("div","env-v",t);i.title=t,n.appendChild(i)}),n}export function renderEnvironment(e){const o=!!(e.instructions&&e.instructions.disabled),n=e.instructions&&!o?e.instructions.files||[]:[],t=e.projectMap||null,i=JSON.stringify([o,n,t,S.locale]);i!==g.lastEnvSig&&(g.lastEnvSig=i,x.textContent="",x.appendChild(B(a("web.env.agents"),o?[a("web.env.agents_disabled")]:n.length?n:[a("web.env.agents_none")])),x.appendChild(B(a("web.env.map"),t&&t.indexed?[[y("web.env.files",t.files),y("web.env.symbols",t.symbols),y("web.env.tokens",t.tokens)].join(" · ")]:[a("web.env.map_not_indexed")])))}
1
+ import{usageEl as c,envEl as x,wsLine as L,el as s,icon as C,attachTooltip as h}from"../dom.js";import{I18N as k,t as a,pluralCount as v}from"../i18n.js";import{feed as g,settings as A}from"../store.js";import{renderFooter as B}from"../footer.js";import{fmtTokens as b}from"../format.js";import{copyText as E}from"../../md.js";function m(e,i,n){const t=s("div","kv");return t.appendChild(s("span","",e)),t.appendChild(s("b",n||"",i)),t}function S(e){return[["seg-system","web.usage.part_system",e.system],["seg-tools","web.usage.part_tools",e.tools],["seg-history","web.usage.part_history",e.history],["seg-reserve","web.usage.part_reserve",e.reserve],["seg-free","web.usage.part_free",e.free]]}function _(e,i,n,t){const o=document.createDocumentFragment();return o.appendChild(s("i","ctip-dot "+e)),o.appendChild(s("span","ctip-label",a(i))),o.appendChild(s("b","",b(n))),o.appendChild(s("span","ctip-pct",Math.round(n/t*100)+"%")),o}const M=new Map;function F(e,i,n){const t=M.get(i);if(M.set(i,n),t===void 0||t===n){e.style.flexGrow=String(n);return}e.style.flexGrow=String(t);const o=()=>{e.style.flexGrow=String(n)};typeof requestAnimationFrame=="function"?requestAnimationFrame(o):o()}function I(e,i,n){const t=s("div","bar segbar"),o=e.contextBreakdown;if(!o||typeof o.window!="number"||o.window<=0){const l=s("i");return l.style.flex="0 0 "+Math.min(100,Math.round(i/n*100))+"%",t.appendChild(l),t}const d=Math.max(o.window,o.used+o.reserve);for(const[l,r,p]of S(o)){if(!p||p<=0)continue;const u=s("i",l);u.style.flexBasis="0",F(u,l,p),u.tabIndex=0,u.setAttribute("aria-label",[a(r),b(p),Math.round(p/d*100)+"%"].join(" · ")),h(u,()=>_(l,r,p,d)),t.appendChild(u)}return t}function R(e){const i=s("div","seglegend"),n=e.contextBreakdown;if(!n)return i;const t=Math.max(n.window,n.used+n.reserve);for(const[o,d,l]of S(n)){if(!l||l<=0)continue;const r=s("span","segl-item");r.tabIndex=0,r.setAttribute("aria-label",[a(d),b(l),Math.round(l/t*100)+"%"].join(" · ")),r.appendChild(s("i","segl-dot "+o)),r.appendChild(s("span","segl-label",a(d))),r.appendChild(s("b","",b(l))),h(r,()=>_(o,d,l,t)),i.appendChild(r)}return i}function T(e){const i=s("div","kv");i.appendChild(s("span","",a("web.usage.session")));const n=s("span","kv-val");if(!e)n.appendChild(s("b","","—"));else{n.appendChild(s("b","",e));const t=s("button","copy-btn");t.type="button",t.title=a("web.usage.copy_id"),t.setAttribute("aria-label",a("web.usage.copy_id")),t.appendChild(C("copy")),t.addEventListener("click",()=>{E(e).then(o=>{t.replaceChildren(C(o?"check":"copy")),setTimeout(()=>t.replaceChildren(C("copy")),1500)})}),n.appendChild(t)}return i.appendChild(n),i}export function renderUsage(e){const i=A.config||{},n=JSON.stringify([e.sessionId,e.model,e.contextUsed,e.contextLimit,e.cacheHitRate,e.costUsd,e.compactionCount,e.contextQuality,e.contextProbe,e.contextBreakdown,i.model,i.contextWindow,k.locale]);if(n===g.lastUsageSig)return;g.lastUsageSig=n,c.textContent="";const t=e.sessionId?String(e.sessionId):"";c.appendChild(T(t)),c.appendChild(m(a("web.usage.model"),e.model||i.model||"—"));const o=typeof e.contextUsed=="number"?e.contextUsed:null,d=typeof e.contextLimit=="number"?e.contextLimit:i.contextWindow||null;c.appendChild(m(a("web.usage.context"),o!=null&&d?b(o)+" / "+b(d):"—")),o!=null&&d&&(c.appendChild(I(e,o,d)),c.appendChild(R(e)));const l=typeof e.cacheHitRate=="number"?e.cacheHitRate:null;c.appendChild(m(a("web.usage.cache"),l!=null?Math.round(l*100)+"%":"—",l!=null?"":"dim")),c.appendChild(m(a("web.usage.cost"),typeof e.costUsd=="number"?"$"+e.costUsd.toFixed(4):"—")),c.appendChild(m(a("web.usage.compactions"),"⟳ "+(e.compactionCount||0),e.compactionCount?"wn":"dim"));const r=typeof e.contextQuality=="number"?e.contextQuality:null;c.appendChild(m(a("web.usage.quality"),r!=null?Math.round(r)+"%":"—",r!=null?"":"dim"));let p="—",u="dim";const f=e.contextProbe;if(f&&typeof f.actual=="number"){const y=b(f.actual),w=e.contextLimit||i.contextWindow||null;w&&f.actual<w?(p=a("web.usage.probe_low",{actual:y,configured:b(w)}),u="wn"):(p=a("web.usage.probe_loaded",{actual:y}),u="")}c.appendChild(m(a("web.usage.probe"),p,u))}export function renderWorkspace(e){B(L,{workspace:e.workspace,version:e.version,moeActive:e.moeActive===!0,dirLabel:a("web.env.dir")},s)}function U(e,i){const n=s("div","env-item");return n.appendChild(s("div","env-k",e)),i.forEach(t=>{const o=s("div","env-v",t);o.title=t,n.appendChild(o)}),n}export function renderEnvironment(e){const i=!!(e.instructions&&e.instructions.disabled),n=e.instructions&&!i?e.instructions.files||[]:[],t=e.projectMap||null,o=JSON.stringify([i,n,t,k.locale]);o!==g.lastEnvSig&&(g.lastEnvSig=o,x.textContent="",x.appendChild(U(a("web.env.agents"),i?[a("web.env.agents_disabled")]:n.length?n:[a("web.env.agents_none")])),x.appendChild(U(a("web.env.map"),t&&t.indexed?[[v("web.env.files",t.files),v("web.env.symbols",t.symbols),v("web.env.tokens",t.tokens)].join(" · ")]:[a("web.env.map_not_indexed")])))}