@bahulam/code 0.1.10 → 0.1.12

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 (52) hide show
  1. package/package.json +5 -2
  2. package/src/agents/loader.mjs +26 -4
  3. package/src/auth/bahulam-auth.mjs +2 -13
  4. package/src/auth/tarang-auth.mjs +313 -0
  5. package/src/commands/agent.mjs +7 -7
  6. package/src/commands/plugin-manage.mjs +449 -0
  7. package/src/commands/plugin.mjs +247 -0
  8. package/src/config/cli-args.mjs +16 -0
  9. package/src/config/env.mjs +2 -0
  10. package/src/config/hook-runner.mjs +8 -8
  11. package/src/config/memory-loader.mjs +7 -3
  12. package/src/config/settings-loader.mjs +5 -3
  13. package/src/core/attachments.mjs +2 -2
  14. package/src/core/background-tasks.mjs +186 -0
  15. package/src/core/headless.mjs +59 -3
  16. package/src/core/local-agent.mjs +10 -1
  17. package/src/core/local-store.mjs +10 -10
  18. package/src/core/paths.mjs +10 -96
  19. package/src/core/policy-resolver.mjs +1 -1
  20. package/src/core/project-context-loader.mjs +2 -2
  21. package/src/core/risk-tier.mjs +1 -0
  22. package/src/core/stream-client.mjs +148 -1
  23. package/src/core/system-prompt.mjs +31 -12
  24. package/src/core/tool-executor.mjs +457 -12
  25. package/src/local-service/agent-relay.mjs +139 -12
  26. package/src/local-service/server.mjs +345 -20
  27. package/src/orchestration/approval.mjs +30 -0
  28. package/src/orchestration/completion-triggers.mjs +40 -0
  29. package/src/orchestration/dispatch.mjs +118 -0
  30. package/src/orchestration/events.mjs +19 -0
  31. package/src/orchestration/graph.mjs +126 -0
  32. package/src/orchestration/node-runner.mjs +193 -0
  33. package/src/orchestration/runner.mjs +200 -0
  34. package/src/plugins/executor.mjs +121 -0
  35. package/src/plugins/loader.mjs +123 -123
  36. package/src/plugins/manifest.mjs +291 -0
  37. package/src/plugins/preflight.mjs +227 -0
  38. package/src/plugins/registry.mjs +233 -0
  39. package/src/plugins/state.mjs +290 -0
  40. package/src/terminal/agents.mjs +26 -4
  41. package/src/terminal/init.mjs +2 -2
  42. package/src/terminal/main.mjs +83 -4
  43. package/src/terminal/repl-explore.mjs +1 -1
  44. package/src/terminal/repl-render.mjs +67 -12
  45. package/src/terminal/repl-state.mjs +4 -2
  46. package/src/terminal/repl.mjs +621 -99
  47. package/src/tools/agent.mjs +6 -2
  48. package/src/tools/analyze-image.mjs +1 -1
  49. package/src/tools/project-overview.mjs +7 -7
  50. package/src/tools/registry.mjs +88 -4
  51. package/src/ui/slash-commands.mjs +19 -1
  52. package/src/ui/sub-agent.mjs +14 -8
@@ -8,11 +8,13 @@
8
8
  import * as crypto from 'node:crypto';
9
9
  import * as fs from 'node:fs';
10
10
  import * as http from 'node:http';
11
+ import * as os from 'node:os';
11
12
  import * as path from 'node:path';
12
13
  import { fileURLToPath } from 'node:url';
13
14
  import { BahulamAuth } from '../auth/bahulam-auth.mjs';
14
15
  import { resolveWebUrl } from '../core/backend-url.mjs';
15
16
  import { LocalAgentRelay } from './agent-relay.mjs';
17
+ import { PluginRegistry } from '../plugins/registry.mjs';
16
18
  import {
17
19
  DEFAULT_MAX_RAW_BYTES,
18
20
  contentTypeForPath,
@@ -88,6 +90,14 @@ export async function startLocalWorkspaceService({
88
90
  return event;
89
91
  }
90
92
 
93
+ // Reactive shared canvas across PROCESS boundaries: in-process state
94
+ // writes (workspace relay, view buttons) emit plugin_state_changed
95
+ // directly, but writes from the terminal REPL, headless runs, or graph
96
+ // job nodes happen in OTHER processes and would leave open views
97
+ // stale. Watching the state files themselves means every writer —
98
+ // whoever and wherever — pulses every open view.
99
+ const stopStateWatcher = installPluginStateWatcher({ emit, events });
100
+
91
101
  const server = http.createServer(async (req, res) => {
92
102
  try {
93
103
  assertLoopbackRequest(req);
@@ -136,6 +146,7 @@ export async function startLocalWorkspaceService({
136
146
  host: loopbackHost,
137
147
  close: () => new Promise((resolve) => {
138
148
  emit('session_stopped', {});
149
+ stopStateWatcher();
139
150
  const relay = agentRelay;
140
151
  for (const client of sseClients) {
141
152
  try {
@@ -152,6 +163,88 @@ export async function startLocalWorkspaceService({
152
163
  };
153
164
  }
154
165
 
166
+ /**
167
+ * Watch ~/.bahulam/data/<plugin>/ for state.db changes and emit coarse
168
+ * plugin_state_changed events. SQLite WAL files change on every commit,
169
+ * so any writer in any process pulses the views. Coarse shape
170
+ * {kind:'*', target:'*', source:'fswatch'} — views re-query state (they
171
+ * already fetch-on-event, so coarseness only costs one extra query).
172
+ * In-process writes already emitted a precise event; the suppression
173
+ * window keeps the watcher from double-pulsing those.
174
+ */
175
+ function installPluginStateWatcher({ emit, events }) {
176
+ const dataRoot = path.join(os.homedir(), '.bahulam', 'data');
177
+ const watchers = new Map(); // dir → FSWatcher
178
+ const timers = new Map(); // plugin → debounce timer
179
+ const DEBOUNCE_MS = 150;
180
+ const SUPPRESS_MS = 500;
181
+
182
+ const recentlyEmittedInProcess = (plugin) => {
183
+ const cutoff = Date.now() - SUPPRESS_MS;
184
+ for (let i = events.length - 1; i >= 0 && i >= events.length - 25; i--) {
185
+ const evt = events[i];
186
+ if (Date.parse(evt.ts) < cutoff) break;
187
+ if (evt.type === 'plugin_state_changed'
188
+ && evt.data?.plugin === plugin
189
+ && evt.data?.source !== 'fswatch') return true;
190
+ }
191
+ return false;
192
+ };
193
+
194
+ const pulse = (plugin) => {
195
+ if (timers.has(plugin)) clearTimeout(timers.get(plugin));
196
+ timers.set(plugin, setTimeout(() => {
197
+ timers.delete(plugin);
198
+ if (recentlyEmittedInProcess(plugin)) return;
199
+ try {
200
+ emit('plugin_state_changed', { plugin, kind: '*', target: '*', source: 'fswatch' });
201
+ } catch { /* SSE bus is best-effort */ }
202
+ }, DEBOUNCE_MS));
203
+ };
204
+
205
+ const watchPluginDir = (plugin) => {
206
+ const dir = path.join(dataRoot, plugin);
207
+ if (watchers.has(dir)) return;
208
+ try {
209
+ const watcher = fs.watch(dir, () => pulse(plugin));
210
+ watcher.on('error', () => {
211
+ try { watcher.close(); } catch { /* already closed */ }
212
+ watchers.delete(dir);
213
+ });
214
+ watchers.set(dir, watcher);
215
+ } catch { /* dir may have vanished between scan and watch */ }
216
+ };
217
+
218
+ const scan = () => {
219
+ let entries = [];
220
+ try {
221
+ entries = fs.readdirSync(dataRoot, { withFileTypes: true });
222
+ } catch { return; }
223
+ for (const entry of entries) {
224
+ if (entry.isDirectory()) watchPluginDir(entry.name);
225
+ }
226
+ };
227
+
228
+ try { fs.mkdirSync(dataRoot, { recursive: true }); } catch { /* best effort */ }
229
+ scan();
230
+ let rootWatcher = null;
231
+ try {
232
+ // New plugins create their data dir on first write — pick them up live.
233
+ rootWatcher = fs.watch(dataRoot, () => scan());
234
+ rootWatcher.on('error', () => { /* root watch is best-effort */ });
235
+ } catch { /* unsupported fs — views fall back to in-process events only */ }
236
+
237
+ return () => {
238
+ if (rootWatcher) { try { rootWatcher.close(); } catch { /* closed */ } }
239
+ for (const watcher of watchers.values()) {
240
+ try { watcher.close(); } catch { /* closed */ }
241
+ }
242
+ for (const timer of timers.values()) clearTimeout(timer);
243
+ watchers.clear();
244
+ timers.clear();
245
+ };
246
+ }
247
+
155
248
  async function routeRequest({ req, res, sessionId, token, events, sseClients, emit, getAgentRelay }) {
156
249
  const url = new URL(req.url || '/', 'http://127.0.0.1');
157
250
 
@@ -420,18 +513,193 @@ async function routeRequest({ req, res, sessionId, token, events, sseClients, em
420
513
 
421
514
  if (req.method === 'POST' && url.pathname === '/api/tools/execute') {
422
515
  const body = await readJsonBody(req);
423
- emit('tool_execution_requested', { name: body.name || null });
424
- sendJson(res, 501, {
425
- ok: false,
426
- error: 'local_tool_bridge_not_wired',
427
- message: 'Tool execution will be routed through the local service permission layer in the next slice.',
428
- });
516
+ const name = String(body.name || '').trim();
517
+ const args = body.args || {};
518
+ if (!name) {
519
+ sendJson(res, 400, { ok: false, error: 'tool name is required' });
520
+ return;
521
+ }
522
+ emit('tool_execution_requested', { name });
523
+ try {
524
+ const relay = getAgentRelay(session);
525
+ const result = await relay.executeTool(name, args);
526
+ sendJson(res, 200, { ok: true, result });
527
+ } catch (err) {
528
+ sendJson(res, 500, { ok: false, error: err.message || String(err) });
529
+ }
530
+ return;
531
+ }
532
+
533
+ // Plugin workspace views (PRD-101 §4.6): panels declared under
534
+ // spec.workspace.views in plugin.yaml, rendered as sandboxed iframes.
535
+ // Generic infra — ANY installed plugin (project-local or ~/.bahulam)
536
+ // contributes tabs; its whole directory is served statically so views
537
+ // can ship css/js/assets with relative paths.
538
+ if (req.method === 'GET' && url.pathname === '/api/plugin-views') {
539
+ const views = getPluginViews(session).map(v => ({
540
+ plugin: v.plugin,
541
+ name: v.name,
542
+ url: `/plugin-view/${encodeURIComponent(v.plugin)}/${v.source.split('/').map(encodeURIComponent).join('/')}`,
543
+ }));
544
+ sendJson(res, 200, { ok: true, views });
545
+ return;
546
+ }
547
+
548
+ // Shared-blackboard direct-access API for plugin views. The tool
549
+ // executor already injects `options.state` for agent-driven writes;
550
+ // this endpoint gives browser-side views the same surface so a human
551
+ // clicking a button hits the exact same store the agent uses.
552
+ //
553
+ // POST /api/plugin-state/<plugin> body: {op, ...args}
554
+ // ops: get|set|patch|append|list|keys|delete|query
555
+ //
556
+ // Writes emit a `plugin_state_changed` SSE event on the shared bus,
557
+ // so any view watching /api/events re-renders live.
558
+ const pluginStateMatch = req.method === 'POST'
559
+ ? url.pathname.match(/^\/api\/plugin-state\/([^/]+)$/)
560
+ : null;
561
+ if (pluginStateMatch) {
562
+ const pluginName = decodeURIComponent(pluginStateMatch[1]);
563
+ // Scope guard: `bahulam plugin <name>` sessions are pinned to one
564
+ // plugin. Refuse cross-plugin state access so a hostile view can't
565
+ // read another plugin's DB via a hand-crafted URL.
566
+ const scoped = session?.plugin?.name;
567
+ if (scoped && String(scoped).toLowerCase() !== pluginName.toLowerCase()) {
568
+ sendJson(res, 403, { ok: false, error: 'plugin_scope_mismatch' });
569
+ return;
570
+ }
571
+ if (!getPluginDirs(session).has(pluginName)) {
572
+ sendJson(res, 404, { ok: false, error: 'plugin_not_found' });
573
+ return;
574
+ }
575
+ let body;
576
+ try { body = await readJsonBody(req); }
577
+ catch (err) { sendJson(res, 400, { ok: false, error: 'bad_body', message: err.message }); return; }
578
+ const op = String(body.op || '').trim();
579
+ if (!op) { sendJson(res, 400, { ok: false, error: 'op_required' }); return; }
580
+ try {
581
+ const { makePluginState } = await import('../plugins/state.mjs');
582
+ const state = makePluginState(pluginName, {
583
+ emit: (evt) => { try { emit('plugin_state_changed', evt); } catch { /* ignore */ } },
584
+ });
585
+ const result = runStateOp(state, op, body);
586
+ sendJson(res, 200, { ok: true, result });
587
+ } catch (err) {
588
+ sendJson(res, 500, { ok: false, error: 'state_op_failed', message: err.message });
589
+ }
590
+ return;
591
+ }
592
+
593
+ const pluginViewMatch = req.method === 'GET'
594
+ ? url.pathname.match(/^\/plugin-view\/([^/]+)\/(.+)$/)
595
+ : null;
596
+ if (pluginViewMatch) {
597
+ const pluginName = decodeURIComponent(pluginViewMatch[1]);
598
+ const relPath = pluginViewMatch[2].split('/').map(decodeURIComponent).join('/');
599
+ const dir = getPluginDirs(session).get(pluginName);
600
+ if (!dir) {
601
+ sendJson(res, 404, { ok: false, error: 'plugin_not_found' });
602
+ return;
603
+ }
604
+ const root = path.resolve(dir);
605
+ const abs = path.resolve(root, relPath);
606
+ if (abs !== root && !abs.startsWith(root + path.sep)) {
607
+ sendJson(res, 403, { ok: false, error: 'forbidden' });
608
+ return;
609
+ }
610
+ try {
611
+ const content = fs.readFileSync(abs);
612
+ res.writeHead(200, { 'Content-Type': pluginViewContentType(abs) });
613
+ res.end(content);
614
+ } catch {
615
+ sendJson(res, 404, { ok: false, error: 'plugin_view_file_missing' });
616
+ }
429
617
  return;
430
618
  }
431
619
 
432
620
  sendJson(res, 404, { ok: false, error: 'not_found' });
433
621
  }
434
622
 
623
+ // Short-TTL registry cache, keyed by session so `bahulam plugin <name>`
624
+ // only surfaces the scoped plugin's views. A generic `workspace open`
625
+ // session (no session.plugin) sees every installed plugin — that's the
626
+ // developer/multi-plugin experience.
627
+ const _pluginScan = new Map();
628
+ function scanPlugins(session) {
629
+ const scope = session?.plugin?.name ? String(session.plugin.name).toLowerCase() : '__all__';
630
+ const cached = _pluginScan.get(scope);
631
+ if (cached && Date.now() - cached.at < 5000) return cached;
632
+ const views = [];
633
+ const dirs = new Map();
634
+ try {
635
+ const registry = new PluginRegistry({
636
+ pluginDirs: [
637
+ path.join(session.root_path || process.cwd(), '.bahulam', 'plugins'),
638
+ path.join(os.homedir(), '.bahulam', 'plugins'),
639
+ ],
640
+ }).scan();
641
+ for (const manifest of registry.list()) {
642
+ const pluginName = manifest.metadata?.name || '';
643
+ if (!pluginName || !manifest._dir) continue;
644
+ if (scope !== '__all__' && pluginName.toLowerCase() !== scope) continue;
645
+ dirs.set(pluginName, manifest._dir);
646
+ (manifest.spec?.workspace?.views || []).forEach((view) => {
647
+ const source = String(view?.source || '').trim().replace(/^\.\//, '');
648
+ if (!source) return;
649
+ views.push({
650
+ plugin: pluginName,
651
+ name: String(view?.name || '').trim() || pluginName,
652
+ source,
653
+ });
654
+ });
655
+ }
656
+ } catch {
657
+ // plugin scan failures must never break the workspace
658
+ }
659
+ const entry = { at: Date.now(), views, dirs };
660
+ _pluginScan.set(scope, entry);
661
+ return entry;
662
+ }
663
+ function getPluginViews(session) { return scanPlugins(session).views; }
664
+ function getPluginDirs(session) { return scanPlugins(session).dirs; }
665
+
666
+ /**
667
+ * Dispatch a POST /api/plugin-state op onto the plugin's state proxy.
668
+ * Kept as a plain switch so failures are per-op and the router surfaces
669
+ * a clean 500 with the offending op name rather than a stack trace.
670
+ */
671
+ function runStateOp(state, op, body) {
672
+ switch (op) {
673
+ case 'get': return state.get(body.key, body.fallback ?? null);
674
+ case 'set': return state.set(body.key, body.value);
675
+ case 'patch': return state.patch(body.key, body.partial ?? body.value);
676
+ case 'delete': return state.delete(body.key);
677
+ case 'keys': return state.keys();
678
+ case 'append': return state.append(body.stream, body.payload);
679
+ case 'list': return state.list(body.stream, { limit: body.limit, order: body.order });
680
+ case 'query': return state.query(body.sql, body.params || []);
681
+ default: throw new Error(`unknown state op: ${op}`);
682
+ }
683
+ }
684
+
685
+ // Real mime types for plugin views — unlike contentTypeForPath (file
686
+ // viewer), views MUST execute, so html/css/js keep their native types.
687
+ // Plugin code runs client-side by user consent (same trust as shell).
688
+ const PLUGIN_VIEW_MIME = {
689
+ html: 'text/html; charset=utf-8', htm: 'text/html; charset=utf-8',
690
+ css: 'text/css; charset=utf-8',
691
+ js: 'text/javascript; charset=utf-8', mjs: 'text/javascript; charset=utf-8',
692
+ json: 'application/json', map: 'application/json',
693
+ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
694
+ webp: 'image/webp', svg: 'image/svg+xml', ico: 'image/x-icon',
695
+ woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf',
696
+ wasm: 'application/wasm',
697
+ };
698
+ function pluginViewContentType(filePath) {
699
+ const ext = path.extname(String(filePath || '')).slice(1).toLowerCase();
700
+ return PLUGIN_VIEW_MIME[ext] || 'application/octet-stream';
701
+ }
702
+
435
703
  function requireAuthorizedSession(sessionId, token, req, url) {
436
704
  const session = loadLocalWorkspaceSession(sessionId);
437
705
  if (!session) {
@@ -788,26 +1056,26 @@ function workspaceHtml({ session, token }) {
788
1056
  <meta name="viewport" content="width=device-width,initial-scale=1">
789
1057
  <title>${title} - Bahulam Local</title>
790
1058
  <style>
791
- :root{color-scheme:light;--ws-foreground:#1B1B1B;--ws-muted:rgba(27,27,27,.52);--ws-faint:rgba(27,27,27,.3);--ws-border:rgba(27,27,27,.10);--ws-border-subtle:rgba(27,27,27,.06);--ws-bg:#FFFDF7;--ws-surface:#F7F5EF;--ws-panel:#fff;--ws-tab:#FAF9F5;--ws-primary:#0891B2;--ws-hover:rgba(27,27,27,.04);--ws-active:rgba(27,27,27,.08);--ws-success:#047857;--ws-success-bg:#ECFDF5;--ws-error:#B91C1C;--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;--sans:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
1059
+ :root{color-scheme:light;--ws-foreground:#1B1B1B;--ws-muted:rgba(27,27,27,.52);--ws-faint:rgba(27,27,27,.3);--ws-border:rgba(27,27,27,.10);--ws-border-subtle:rgba(27,27,27,.06);--ws-bg:#FFFDF7;--ws-surface:#F7F5EF;--ws-panel:#fff;--ws-tab:#FAF9F5;--ws-primary:#0891B2;--ws-primary-soft:rgba(8,145,178,.10);--ws-hover:rgba(27,27,27,.04);--ws-active:rgba(8,145,178,.10);--ws-success:#047857;--ws-success-bg:#ECFDF5;--ws-error:#B91C1C;--ws-radius-sm:5px;--ws-radius-md:8px;--ws-radius-lg:10px;--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;--sans:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
792
1060
  *{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--ws-bg);color:var(--ws-foreground);font:13px/1.45 var(--sans);overflow:hidden}
793
1061
  button,textarea,input{font:inherit}button{color:inherit}.shell{display:flex;flex-direction:column;height:100vh;min-height:560px;background:var(--ws-bg)}
794
1062
  .global-bar{height:40px;display:flex;align-items:center;gap:12px;border-bottom:1px solid rgba(27,27,27,.08);background:#FFFDF7;padding:0 16px;flex:0 0 auto;min-width:0}
795
1063
  .dashboard-link{height:28px;display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;border-radius:6px;padding:0;color:rgba(27,27,27,.40);font-size:12px;text-decoration:none;cursor:pointer;transition:color .15s ease;white-space:nowrap}.dashboard-link:hover{color:rgba(27,27,27,.70)}.dashboard-link svg{width:14px;height:14px;flex:0 0 auto}.brand-divider{width:1px;height:16px;background:rgba(27,27,27,.10);flex:0 0 auto}
796
1064
  .ide-brand{height:28px;display:flex;align-items:center;gap:6px;text-decoration:none;color:var(--ws-foreground);white-space:nowrap}.brand-mark{width:20px;height:20px;border-radius:5px;object-fit:contain;display:block}.brand-title{font:800 12px/1 var(--mono);letter-spacing:-.01em;color:#1B1B1B}
797
1065
  .global-spacer{flex:1;min-width:10px}.global-links{display:flex;align-items:center;gap:10px;min-width:0;overflow:hidden}.global-link{color:rgba(27,27,27,.46);font-size:12px;text-decoration:none;white-space:nowrap}.global-link:hover{color:rgba(27,27,27,.78)}.auth-chip{height:22px;display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(27,27,27,.10);border-radius:999px;background:#fff;padding:0 8px;color:rgba(27,27,27,.46);font-size:11px;white-space:nowrap}.auth-chip:before{content:"";width:6px;height:6px;border-radius:999px;background:#A8A29E}.auth-chip.signed-in{border-color:#BBF7D0;background:#ECFDF5;color:#047857}.auth-chip.signed-in:before{background:#10B981}
798
- .topbar{height:38px;display:flex;align-items:center;gap:12px;border-bottom:1px solid rgba(27,27,27,.06);background:rgba(255,255,255,.80);backdrop-filter:blur(10px);padding:0 16px;flex:0 0 auto}.terminal-icon{width:16px;height:16px;color:rgba(27,27,27,.30);flex:0 0 auto}.workspace-name{font-size:14px;font-weight:500;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.workspace-kind{font-size:12px;color:rgba(27,27,27,.30)}.top-actions{margin-left:auto;display:flex;align-items:center;gap:6px;min-width:0}
1066
+ .topbar{height:38px;display:flex;align-items:center;gap:12px;border-bottom:1px solid rgba(27,27,27,.06);background:rgba(255,255,255,.80);backdrop-filter:blur(10px);padding:0 16px;flex:0 0 auto}.terminal-icon{width:16px;height:16px;color:rgba(27,27,27,.30);flex:0 0 auto}.workspace-name{font-size:13px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.workspace-kind{font-size:12px;color:rgba(27,27,27,.42)}.top-actions{margin-left:auto;display:flex;align-items:center;gap:6px;min-width:0}
799
1067
  .badge{display:inline-flex;align-items:center;gap:6px;border-radius:999px;border:1px solid var(--ws-border);background:#fff;padding:2px 8px;font-size:10px;font-weight:500;color:rgba(27,27,27,.58);white-space:nowrap}.badge.local{border-color:#BBF7D0;background:var(--ws-success-bg);color:var(--ws-success)}.badge.local:before{content:"";width:6px;height:6px;border-radius:999px;background:#10B981}
800
- .icon-button{height:26px;min-width:26px;border:1px solid rgba(27,27,27,.10);border-radius:6px;background:#fff;color:rgba(27,27,27,.64);display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 8px;cursor:pointer;font-size:11px;font-weight:500;transition:background .15s ease,color .15s ease,border-color .15s ease}.icon-button:hover{background:#F7F5EF;color:var(--ws-foreground)}.icon-button[aria-pressed="true"]{background:rgba(27,27,27,.08);border-color:rgba(27,27,27,.10);color:#1B1B1B}.icon-button svg{width:14px;height:14px}
1068
+ .icon-button{height:26px;min-width:26px;border:1px solid rgba(27,27,27,.10);border-radius:var(--ws-radius-md);background:#fff;color:rgba(27,27,27,.64);display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 8px;cursor:pointer;font-size:12px;font-weight:500;transition:background .15s ease,color .15s ease,border-color .15s ease}.icon-button:hover{background:#F7F5EF;color:var(--ws-foreground)}.icon-button[aria-pressed="true"]{background:var(--ws-primary-soft);border-color:rgba(8,145,178,.22);color:#0E7490}.icon-button svg{width:14px;height:14px}
801
1069
  .auto-toggle{height:26px;display:inline-flex;align-items:center;gap:7px;border:1px solid rgba(27,27,27,.10);border-radius:999px;background:#fff;padding:0 8px 0 6px;color:rgba(27,27,27,.55);font-size:11px;font-weight:650;cursor:pointer;white-space:nowrap}.auto-toggle:hover{background:#F7F5EF;color:rgba(27,27,27,.76)}.auto-toggle input{position:absolute;opacity:0;pointer-events:none}.toggle-track{width:28px;height:16px;border-radius:999px;background:#D6D3CB;position:relative;transition:background .15s ease}.toggle-track:before{content:"";position:absolute;width:12px;height:12px;border-radius:999px;left:2px;top:2px;background:#fff;box-shadow:0 1px 2px rgba(27,27,27,.25);transition:transform .15s ease}.auto-toggle input:checked+.toggle-track{background:#0891B2}.auto-toggle input:checked+.toggle-track:before{transform:translateX(12px)}.auto-toggle input:focus-visible+.toggle-track{outline:2px solid rgba(8,145,178,.35);outline-offset:2px}
802
- .menubar{height:28px;display:flex;align-items:center;gap:2px;border-bottom:1px solid rgba(27,27,27,.06);background:#F7F5EF;padding:0 12px;flex:0 0 auto;user-select:none;position:relative;z-index:20}.menu-item{height:22px;border:0;background:transparent;border-radius:5px;padding:0 8px;color:rgba(27,27,27,.58);font-size:12px;cursor:pointer}.menu-item:hover{background:var(--ws-hover);color:rgba(27,27,27,.86)}.menubar-spacer{flex:1}.session-menu-wrap{display:flex;align-items:center}.session-menu-button{max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-modal-backdrop,.approval-modal-backdrop{position:fixed;inset:0;z-index:80;display:flex;align-items:flex-start;justify-content:center;background:rgba(27,27,27,.28);padding:92px 16px 24px}.approval-modal-backdrop{z-index:90;background:rgba(27,27,27,.36)}.session-modal-backdrop[hidden],.approval-modal-backdrop[hidden]{display:none}.session-dialog,.approval-dialog{width:min(620px,100%);max-height:min(620px,calc(100vh - 128px));display:flex;flex-direction:column;overflow:hidden;border:1px solid rgba(27,27,27,.12);border-radius:8px;background:#FFFDF7;box-shadow:0 24px 70px rgba(27,27,27,.22)}.approval-dialog{width:min(680px,100%)}.session-dialog-head,.approval-dialog-head{height:48px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--ws-border-subtle);padding:0 12px}.session-dialog-title,.approval-dialog-title{font-size:13px;font-weight:750;color:rgba(27,27,27,.80)}.session-dialog-subtitle,.approval-dialog-subtitle{margin-top:1px;font:10px var(--mono);color:rgba(27,27,27,.38);max-width:560px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-menu{display:block;overflow:auto;padding:6px}.session-menu-empty{padding:10px;color:rgba(27,27,27,.42);font-size:12px}.approval-body{padding:12px;overflow:auto}.approval-subject{border:1px solid var(--ws-border);border-radius:7px;background:#fff;padding:10px;font:12px/1.45 var(--mono);color:rgba(27,27,27,.72);white-space:pre-wrap;overflow-wrap:anywhere}.approval-reason{margin-top:10px;color:rgba(27,27,27,.58);font-size:12px;line-height:1.5}.approval-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;border-top:1px solid var(--ws-border-subtle);padding:10px 12px;background:#F7F5EF}.approval-actions .danger{border-color:rgba(185,28,28,.24);color:#B91C1C}.approval-actions .primary-action{background:#1B1B1B;color:#FFFDF7;border-color:#1B1B1B}.approval-inline{width:100%;max-width:960px;border:1px solid rgba(217,119,6,.22);border-radius:10px;background:#FFFBEB;overflow:hidden;box-shadow:0 8px 24px rgba(146,64,14,.08)}.approval-inline.done{border-color:rgba(27,27,27,.08);background:#F7F5EF;box-shadow:none}.approval-inline-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 11px;border-bottom:1px solid rgba(217,119,6,.14)}.approval-inline-title{font-size:12px;font-weight:800;color:#92400E}.approval-inline-meta{font:10px var(--mono);color:rgba(146,64,14,.62);white-space:nowrap}.approval-inline-subject{margin:10px 11px 0;border:1px solid rgba(217,119,6,.18);border-radius:7px;background:#fff;padding:9px;font:11px/1.45 var(--mono);color:rgba(27,27,27,.74);white-space:pre-wrap;overflow-wrap:anywhere;max-height:180px;overflow:auto}.approval-inline-reason{padding:8px 11px 10px;color:rgba(27,27,27,.58);font-size:12px;line-height:1.45}.approval-inline-actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;padding:9px 11px;border-top:1px solid rgba(217,119,6,.12);background:rgba(255,255,255,.58)}
803
- main{--left-w:256px;--right-w:480px;display:grid;grid-template-columns:var(--left-w) 6px minmax(360px,1fr) 6px var(--right-w);flex:1;min-height:0}.panel{min-width:0;background:var(--ws-panel);overflow:hidden}.files{background:rgba(255,255,255,.52)}.work{display:flex;flex-direction:column;background:#fff}.agent{background:rgba(255,255,255,.58);display:flex;flex-direction:column}.resizer{background:transparent;border-left:1px solid var(--ws-border-subtle);border-right:1px solid transparent;cursor:col-resize;position:relative}.resizer:hover,.resizer.dragging{background:rgba(8,145,178,.08);border-left-color:rgba(8,145,178,.25)}
1070
+ .menubar{height:28px;display:flex;align-items:center;gap:2px;border-bottom:1px solid rgba(27,27,27,.06);background:#F7F5EF;padding:0 12px;flex:0 0 auto;user-select:none;position:relative;z-index:20}.menu-item{height:22px;border:0;background:transparent;border-radius:var(--ws-radius-sm);padding:0 8px;color:rgba(27,27,27,.58);font-size:12px;font-weight:500;cursor:pointer}.menu-item:hover{background:var(--ws-hover);color:rgba(27,27,27,.86)}.menubar-spacer{flex:1}.session-menu-wrap{display:flex;align-items:center}.session-menu-button{max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-modal-backdrop,.approval-modal-backdrop{position:fixed;inset:0;z-index:80;display:flex;align-items:flex-start;justify-content:center;background:rgba(27,27,27,.28);padding:92px 16px 24px}.approval-modal-backdrop{z-index:90;background:rgba(27,27,27,.36)}.session-modal-backdrop[hidden],.approval-modal-backdrop[hidden]{display:none}.session-dialog,.approval-dialog{width:min(620px,100%);max-height:min(620px,calc(100vh - 128px));display:flex;flex-direction:column;overflow:hidden;border:1px solid rgba(27,27,27,.12);border-radius:var(--ws-radius-md);background:#FFFDF7;box-shadow:0 24px 70px rgba(27,27,27,.22)}.approval-dialog{width:min(680px,100%)}.session-dialog-head,.approval-dialog-head{height:48px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--ws-border-subtle);padding:0 12px}.session-dialog-title,.approval-dialog-title{font-size:13px;font-weight:600;color:rgba(27,27,27,.84)}.session-dialog-subtitle,.approval-dialog-subtitle{margin-top:1px;font:12px/1.45 var(--sans);color:rgba(27,27,27,.46);max-width:560px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-menu{display:block;overflow:auto;padding:6px}.session-menu-empty{padding:10px;color:rgba(27,27,27,.42);font-size:12px}.approval-body{padding:12px;overflow:auto}.approval-subject{border:1px solid var(--ws-border);border-radius:var(--ws-radius-md);background:#fff;padding:10px;font:12px/1.45 var(--mono);color:rgba(27,27,27,.72);white-space:pre-wrap;overflow-wrap:anywhere}.approval-reason{margin-top:10px;color:rgba(27,27,27,.58);font-size:12px;line-height:1.5}.approval-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;border-top:1px solid var(--ws-border-subtle);padding:10px 12px;background:#F7F5EF}.approval-actions .danger{border-color:rgba(185,28,28,.24);color:#B91C1C}.approval-actions .primary-action{background:#1B1B1B;color:#FFFDF7;border-color:#1B1B1B}.approval-inline{width:100%;max-width:960px;border:1px solid rgba(217,119,6,.22);border-radius:var(--ws-radius-md);background:#FFFBEB;overflow:hidden;box-shadow:0 8px 24px rgba(146,64,14,.08)}.approval-inline.done{border-color:rgba(27,27,27,.08);background:#F7F5EF;box-shadow:none}.approval-inline-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 11px;border-bottom:1px solid rgba(217,119,6,.14)}.approval-inline-title{font-size:13px;font-weight:600;color:#92400E}.approval-inline-meta{font:11px var(--sans);color:rgba(146,64,14,.62);white-space:nowrap}.approval-inline-subject{margin:10px 11px 0;border:1px solid rgba(217,119,6,.18);border-radius:var(--ws-radius-md);background:#fff;padding:9px;font:11px/1.45 var(--mono);color:rgba(27,27,27,.74);white-space:pre-wrap;overflow-wrap:anywhere;max-height:180px;overflow:auto}.approval-inline-reason{padding:8px 11px 10px;color:rgba(27,27,27,.58);font-size:12px;line-height:1.45}.approval-inline-actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;padding:9px 11px;border-top:1px solid rgba(217,119,6,.12);background:rgba(255,255,255,.58)}
1071
+ main{--left-w:256px;--right-w:480px;display:grid;grid-template-columns:var(--left-w) 6px minmax(360px,1fr) 6px var(--right-w);flex:1;min-height:0}.panel{min-width:0;background:var(--ws-panel);overflow:hidden}.files{grid-column:1;background:rgba(255,255,255,.52)}.left-resizer{grid-column:2}.work{grid-column:3;display:flex;flex-direction:column;background:#fff}.right-resizer{grid-column:4}.agent{grid-column:5;background:rgba(255,255,255,.58);display:flex;flex-direction:column}.resizer{background:transparent;border-left:1px solid var(--ws-border-subtle);border-right:1px solid transparent;cursor:col-resize;position:relative}.resizer:hover,.resizer.dragging{background:rgba(8,145,178,.08);border-left-color:rgba(8,145,178,.25)}
804
1072
  main.hide-files{grid-template-columns:0 0 minmax(360px,1fr) 6px var(--right-w)}main.hide-files .files,main.hide-files .left-resizer{display:none}main.hide-agent{grid-template-columns:var(--left-w) 6px minmax(360px,1fr) 0 0}main.hide-agent .agent,main.hide-agent .right-resizer{display:none}
805
- .section-head{height:36px;border-bottom:1px solid var(--ws-border-subtle);display:flex;align-items:center;justify-content:space-between;padding:0 12px;color:var(--ws-faint);font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;background:rgba(255,255,255,.45)}.section-actions{display:flex;align-items:center;gap:4px}.section-button{height:20px;border:0;background:transparent;border-radius:5px;color:rgba(27,27,27,.35);cursor:pointer;padding:0 4px}.section-button:hover{background:var(--ws-hover);color:rgba(27,27,27,.70)}
806
- .file-list{padding:6px;overflow:auto;height:calc(100% - 36px)}.row{display:flex;align-items:center;gap:5px;width:100%;height:27px;border:0;background:transparent;text-align:left;border-radius:6px;padding:0 8px;color:rgba(27,27,27,.62);cursor:pointer;font-size:12px;line-height:1}.row:hover{background:var(--ws-hover);color:rgba(27,27,27,.84)}.row.selected{background:var(--ws-active);color:var(--ws-foreground)}.row:focus-visible{outline:2px solid rgba(8,145,178,.35);outline-offset:1px}.chev{width:12px;text-align:center;color:rgba(27,27,27,.32);font:11px var(--mono)}.icon{width:16px;display:inline-flex;align-items:center;justify-content:center;color:var(--ws-faint);font-size:11px;font-family:var(--mono)}.name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sub{color:rgba(27,27,27,.28);font:10px var(--mono);margin-left:auto}.loading-row{height:24px;padding-left:32px;color:rgba(27,27,27,.30);font:11px var(--mono)}
807
- .tabbar{height:36px;display:flex;align-items:center;border-bottom:1px solid var(--ws-border-subtle);background:var(--ws-tab);flex:0 0 auto;overflow-x:auto}.tab{height:100%;display:flex;align-items:center;gap:7px;border:0;border-right:1px solid var(--ws-border-subtle);padding:0 9px;background:transparent;color:rgba(27,27,27,.45);font-size:11px;cursor:pointer;max-width:220px;min-width:90px}.tab.active{background:#fff;color:var(--ws-foreground);box-shadow:inset 0 -1.5px 0 var(--ws-foreground)}.tab:hover{background:rgba(27,27,27,.03);color:rgba(27,27,27,.75)}.tab-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tab-close{border:0;background:transparent;border-radius:4px;color:rgba(27,27,27,.28);cursor:pointer;padding:0 2px}.tab-close:hover{background:rgba(27,27,27,.07);color:rgba(27,27,27,.70)}.tab-muted{flex:1;height:100%;border-left:1px solid var(--ws-border-subtle);min-width:24px}
1073
+ .section-head{height:36px;border-bottom:1px solid var(--ws-border-subtle);display:flex;align-items:center;justify-content:space-between;padding:0 12px;color:var(--ws-faint);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;background:rgba(255,255,255,.45)}.section-actions{display:flex;align-items:center;gap:4px}.section-button{height:20px;border:0;background:transparent;border-radius:var(--ws-radius-sm);color:rgba(27,27,27,.35);cursor:pointer;padding:0 4px}.section-button:hover{background:var(--ws-hover);color:rgba(27,27,27,.70)}
1074
+ .file-list{padding:6px;overflow:auto;height:calc(100% - 36px)}.row{display:flex;align-items:center;gap:5px;width:100%;height:27px;border:0;background:transparent;text-align:left;border-radius:var(--ws-radius-sm);padding:0 8px;color:rgba(27,27,27,.62);cursor:pointer;font-size:13px;line-height:1}.row:hover{background:var(--ws-hover);color:rgba(27,27,27,.84)}.row.selected{background:var(--ws-active);color:var(--ws-foreground)}.row:focus-visible{outline:2px solid rgba(8,145,178,.35);outline-offset:1px}.chev{width:12px;text-align:center;color:rgba(27,27,27,.32);font:11px var(--mono)}.icon{width:16px;display:inline-flex;align-items:center;justify-content:center;color:var(--ws-faint);font-size:11px;font-family:var(--mono)}.name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sub{color:rgba(27,27,27,.34);font:11px var(--sans);margin-left:auto}.loading-row{height:24px;padding-left:32px;color:rgba(27,27,27,.38);font:12px var(--sans)}
1075
+ .tabbar{height:36px;display:flex;align-items:center;border-bottom:1px solid var(--ws-border-subtle);background:var(--ws-tab);flex:0 0 auto;overflow-x:auto}.tab{height:100%;display:flex;align-items:center;gap:7px;border:0;border-right:1px solid var(--ws-border-subtle);padding:0 9px;background:transparent;color:rgba(27,27,27,.48);font-size:13px;font-weight:500;cursor:pointer;max-width:220px;min-width:90px}.tab.active{background:#fff;color:var(--ws-foreground);font-weight:600;box-shadow:inset 0 -1.5px 0 var(--ws-primary)}.tab:hover{background:rgba(27,27,27,.03);color:rgba(27,27,27,.75)}.tab-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tab-close{border:0;background:transparent;border-radius:var(--ws-radius-sm);color:rgba(27,27,27,.28);cursor:pointer;padding:0 2px}.tab-close:hover{background:rgba(27,27,27,.07);color:rgba(27,27,27,.70)}.tab-muted{flex:1;height:100%;border-left:1px solid var(--ws-border-subtle);min-width:24px}
808
1076
  .viewer{flex:1;min-height:0;overflow:hidden;background:#fff;display:flex;flex-direction:column}.file-header{min-height:34px;flex:0 0 auto;border-bottom:1px solid var(--ws-border-subtle);display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:5px 12px;background:rgba(27,27,27,.015);font:12px var(--mono);color:rgba(27,27,27,.50)}.path{min-width:120px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-chip{display:inline-flex;align-items:center;gap:5px;border:1px solid rgba(27,27,27,.08);border-radius:999px;background:#fff;padding:1px 7px;font:10px var(--sans);color:rgba(27,27,27,.48);white-space:nowrap}.file-chip.warn{border-color:#FDE68A;background:#FFFBEB;color:#92400E}.file-actions{margin-left:auto;display:flex;align-items:center;gap:6px;min-width:0;flex-wrap:wrap}.file-action{font:11px var(--sans);color:rgba(27,27,27,.50);text-decoration:none;border:1px solid var(--ws-border);border-radius:6px;padding:2px 7px;background:#fff;cursor:pointer}.file-action:hover{color:var(--ws-foreground);background:var(--ws-surface)}.file-icon-action{width:24px;height:24px;min-width:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.file-icon-action svg{width:13px;height:13px;display:block}select.file-action{height:23px;max-width:190px;padding:1px 24px 1px 7px}.empty{color:var(--ws-faint);padding:24px;font-size:12px}.error{color:var(--ws-error)}
809
1077
  .monaco-host{flex:1;min-height:0}.code-fallback{flex:1;min-height:0;overflow:auto}.code-wrap{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:start;min-height:100%;font:12px/1.55 var(--mono)}.line-nums{user-select:none;text-align:right;padding:14px 10px 14px 14px;color:rgba(27,27,27,.25);background:#FAF9F5;border-right:1px solid var(--ws-border-subtle);white-space:pre}.code-pre{margin:0;padding:14px;white-space:pre;overflow:auto;color:#1F2937;background:#fff;min-height:100%}.markdown-preview{flex:1;min-height:0;overflow:auto;max-width:920px;padding:24px 28px;color:rgba(27,27,27,.86);font-size:14px;line-height:1.65}.markdown-preview h1,.markdown-preview h2,.markdown-preview h3{line-height:1.2;margin:18px 0 8px}.markdown-preview p{margin:0 0 12px}.markdown-preview code,.message-content code{font-family:var(--mono);font-size:.92em;background:rgba(27,27,27,.06);border-radius:4px;padding:1px 4px}.markdown-preview pre,.message-content pre{overflow:auto;background:#0D1117;color:#E5E7EB;border-radius:7px;padding:12px}.image-stage{flex:1;min-height:0;display:flex;flex-direction:column;background:#F8F7F2}.image-toolbar{height:34px;flex:0 0 auto;display:flex;align-items:center;justify-content:flex-end;gap:6px;padding:0 10px;border-bottom:1px solid var(--ws-border-subtle);background:#fff}.image-preview{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:18px;overflow:auto}.image-preview img{max-width:100%;max-height:100%;object-fit:contain;border:1px solid var(--ws-border);background:#fff;box-shadow:0 10px 30px rgba(27,27,27,.10);transform-origin:center center}.frame-preview{flex:1;min-height:0;background:#F8F7F2}.frame-preview iframe{width:100%;height:100%;border:0;background:#fff}.table-preview{flex:1;min-height:0;padding:18px;overflow:auto}.table-preview .table-meta{margin:0 0 10px;color:rgba(27,27,27,.42);font:11px var(--mono)}.table-preview table{border-collapse:collapse;font-size:12px;background:#fff}.table-preview th,.table-preview td{border:1px solid var(--ws-border);padding:5px 7px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.table-preview th{background:#F7F5EF;text-align:left;color:rgba(27,27,27,.65);position:sticky;top:0}.table-preview td.formula-cell{background:#F0F9FF;color:#075985}.diagram-preview{flex:1;min-height:0;padding:18px;display:grid;gap:14px;max-width:none;overflow:auto}.mermaid-card{border:1px solid var(--ws-border);border-radius:8px;background:#fff;overflow:hidden;display:flex;flex-direction:column}.mermaid-output{padding:18px;overflow:auto;min-height:220px;display:flex;align-items:center;justify-content:center}.mermaid-output svg{width:100%;max-width:100%;height:auto;display:block}.mermaid-source{margin:0;border-top:1px solid var(--ws-border-subtle);border-radius:0;background:#FAF9F5;color:rgba(27,27,27,.68);font:11px/1.45 var(--mono);max-height:220px;overflow:auto;padding:10px 12px;white-space:pre}.viewer-note{flex:1;min-height:0;overflow:auto;display:flex;align-items:center;justify-content:center;padding:24px;background:#FAF9F5}.viewer-note-card{width:min(520px,100%);max-height:100%;overflow:auto;border:1px solid var(--ws-border);border-radius:8px;background:#fff;padding:18px;box-shadow:0 10px 34px rgba(27,27,27,.05)}.viewer-note-title{font-size:14px;font-weight:750;color:rgba(27,27,27,.84)}.viewer-note-body{margin-top:8px;color:rgba(27,27,27,.56);font-size:12px;line-height:1.55}.viewer-note-actions{margin-top:14px;display:flex;gap:8px;flex-wrap:wrap}.binary-preview{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:24px;color:rgba(27,27,27,.50);text-align:center}
810
- .chat-head{height:36px;border-bottom:1px solid var(--ws-border-subtle);display:flex;align-items:center;gap:8px;padding:0 12px;background:rgba(255,255,255,.55)}.chat-title{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:rgba(27,27,27,.35)}.chat-subtitle{min-width:0;flex:1;font-size:10px;color:rgba(27,27,27,.28);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.agent-tabs{height:32px;display:flex;align-items:center;border-bottom:1px solid var(--ws-border-subtle);background:#F7F5EF;padding:0 8px;gap:3px}.agent-tab{height:24px;border:0;border-radius:6px;background:transparent;color:rgba(27,27,27,.45);font-size:11px;font-weight:650;padding:0 9px;cursor:pointer}.agent-tab:hover{background:var(--ws-hover);color:rgba(27,27,27,.76)}.agent-tab.active{background:#fff;color:var(--ws-foreground);box-shadow:0 0 0 1px rgba(27,27,27,.06)}.chat-body{display:flex;flex-direction:column;flex:1;min-height:0}.chat-pane{display:none;flex-direction:column;flex:1;min-height:0}.chat-pane.active{display:flex}.thread{flex:1;min-height:0;overflow:auto;padding:16px 14px 10px;background:rgba(255,255,255,.40)}.thread-inner{display:flex;flex-direction:column;gap:12px}.empty-chat{display:flex;height:100%;align-items:center;justify-content:center;text-align:center;color:rgba(27,27,27,.35);font-size:12px}.session-list{display:flex;flex-direction:column;gap:2px}.session-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:center;border-radius:6px;padding:8px}.session-row:hover{background:rgba(27,27,27,.035)}.session-main{min-width:0}.session-prompt{font-size:12px;color:rgba(27,27,27,.82);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-meta{margin-top:3px;font:10px/1.4 var(--mono);color:rgba(27,27,27,.38);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-tools{margin-top:5px;display:flex;gap:4px;flex-wrap:wrap}.session-chip{border-radius:999px;border:1px solid rgba(27,27,27,.07);background:#F7F5EF;padding:1px 6px;font:10px var(--mono);color:rgba(27,27,27,.48)}.msg{display:flex}.msg.user{justify-content:flex-end}.msg.assistant{justify-content:flex-start}.bubble{max-width:86%;border-radius:16px;padding:10px 12px;font-size:13px;line-height:1.55;word-break:break-word}.user .bubble{background:#1B1B1B;color:#FFFDF7}.message-attachments{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.message-attachment{max-width:100%;display:inline-flex;align-items:center;gap:5px;border:1px solid rgba(255,255,255,.20);border-radius:999px;background:rgba(255,255,255,.10);color:#FFFDF7;padding:2px 7px;cursor:pointer}.message-attachment span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}.message-attachment small{font:10px var(--mono);opacity:.68}.message-attachment:hover{background:rgba(255,255,255,.17)}.assistant-stack{width:100%;max-width:960px;display:flex;flex-direction:column;gap:8px}.assistant-bubble{display:none;max-width:94%;border-radius:16px;background:#F7F5EF;padding:11px 13px;color:rgba(27,27,27,.86);font-size:13px;line-height:1.6}.assistant-bubble:not(:empty){display:block}.message-content p{margin:0 0 10px}.message-content p:last-child{margin-bottom:0}.message-content ul{margin:0 0 10px 18px;padding:0}.message-content li{margin:2px 0}
1078
+ .chat-head{height:36px;border-bottom:1px solid var(--ws-border-subtle);display:flex;align-items:center;gap:8px;padding:0 12px;background:rgba(255,255,255,.55)}.chat-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:rgba(27,27,27,.42)}.chat-subtitle{min-width:0;flex:1;font-size:12px;color:rgba(27,27,27,.42);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.agent-tabs{height:34px;display:flex;align-items:center;border-bottom:1px solid var(--ws-border-subtle);background:#F7F5EF;padding:0 8px;gap:3px}.agent-tab{height:26px;border:0;border-radius:var(--ws-radius-md);background:transparent;color:rgba(27,27,27,.48);font-size:13px;font-weight:500;padding:0 9px;cursor:pointer}.agent-tab:hover{background:var(--ws-hover);color:rgba(27,27,27,.76)}.agent-tab.active{background:#fff;color:var(--ws-foreground);font-weight:600;box-shadow:0 0 0 1px rgba(27,27,27,.06)}.chat-body{display:flex;flex-direction:column;flex:1;min-height:0}.chat-pane{display:none;flex-direction:column;flex:1;min-height:0}.chat-pane.active{display:flex}.plugins-menu-wrap{position:relative}.plugins-menu-wrap[hidden]{display:none}.plugins-dropdown{position:absolute;top:26px;left:0;z-index:60;min-width:210px;background:#fff;border:1px solid var(--ws-border);border-radius:var(--ws-radius-md);box-shadow:0 12px 34px rgba(27,27,27,.14);padding:4px;display:flex;flex-direction:column}.plugins-dropdown[hidden]{display:none}.plugins-dropdown button{border:0;background:transparent;text-align:left;font:12px var(--sans);color:rgba(27,27,27,.78);padding:6px 9px;border-radius:var(--ws-radius-sm);cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:10px}.plugins-dropdown button:hover{background:var(--ws-hover)}.plugins-dropdown .plugin-origin{color:var(--ws-faint);font:11px var(--sans)}.thread{flex:1;min-height:0;overflow:auto;padding:16px 14px 10px;background:rgba(255,255,255,.40)}.thread-inner{display:flex;flex-direction:column;gap:12px}.empty-chat{display:flex;height:100%;align-items:center;justify-content:center;text-align:center;color:rgba(27,27,27,.42);font-size:12px}.session-list{display:flex;flex-direction:column;gap:2px}.session-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:center;width:100%;border:0;background:transparent;text-align:left;border-radius:var(--ws-radius-md);padding:8px;color:inherit;cursor:pointer}.session-row:hover{background:rgba(27,27,27,.035)}.session-row.selected{background:var(--ws-active);box-shadow:inset 2px 0 0 var(--ws-primary)}.session-row:focus-visible{outline:2px solid rgba(8,145,178,.35);outline-offset:1px}.session-main{min-width:0}.session-prompt{display:block;font-size:13px;font-weight:500;color:rgba(27,27,27,.84);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-meta{display:block;margin-top:3px;font:12px/1.4 var(--sans);color:rgba(27,27,27,.46);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-tools{margin-top:5px;display:flex;gap:4px;flex-wrap:wrap}.session-chip{border-radius:999px;border:1px solid rgba(27,27,27,.07);background:#F7F5EF;padding:1px 6px;font:11px var(--sans);color:rgba(27,27,27,.52)}.session-action{height:24px;display:inline-flex;align-items:center;justify-content:center;border:1px solid rgba(27,27,27,.10);border-radius:var(--ws-radius-md);background:#fff;color:rgba(27,27,27,.62);padding:0 8px;font-size:12px;font-weight:500}.session-row:hover .session-action{background:#F7F5EF;color:var(--ws-foreground)}.msg{display:flex}.msg.user{justify-content:flex-end}.msg.assistant{justify-content:flex-start}.bubble{max-width:86%;border-radius:16px;padding:10px 12px;font-size:13px;line-height:1.55;word-break:break-word}.user .bubble{background:#1B1B1B;color:#FFFDF7}.message-attachments{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.message-attachment{max-width:100%;display:inline-flex;align-items:center;gap:5px;border:1px solid rgba(255,255,255,.20);border-radius:999px;background:rgba(255,255,255,.10);color:#FFFDF7;padding:2px 7px;cursor:pointer}.message-attachment span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}.message-attachment small{font:11px var(--sans);opacity:.68}.message-attachment:hover{background:rgba(255,255,255,.17)}.assistant-stack{width:100%;max-width:960px;display:flex;flex-direction:column;gap:8px}.assistant-bubble{display:none;max-width:94%;border-radius:16px;background:#F7F5EF;padding:11px 13px;color:rgba(27,27,27,.86);font-size:13px;line-height:1.6}.assistant-bubble:not(:empty){display:block}.message-content p{margin:0 0 10px}.message-content p:last-child{margin-bottom:0}.message-content ul{margin:0 0 10px 18px;padding:0}.message-content li{margin:2px 0}
811
1079
  .activity-card{display:none;overflow:hidden;border:1px solid var(--ws-border);border-radius:8px;background:#FFFDF7;font-size:12px}.activity-card.active{display:block}.activity-head{height:28px;width:100%;border:0;background:transparent;display:flex;align-items:center;gap:8px;padding:0 9px;cursor:pointer;color:rgba(27,27,27,.68)}.activity-head:hover{background:#F7F5EF}.activity-label{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left;font-size:11px;font-weight:650}.activity-rollup{font-size:10px;color:rgba(27,27,27,.38)}.activity-rows{border-top:1px solid rgba(27,27,27,.05);padding:7px 9px;max-height:96px;overflow:auto;display:flex;flex-direction:column;gap:5px}.activity-card.expanded .activity-rows{max-height:360px}.activity-row{display:flex;align-items:flex-start;gap:7px;color:rgba(27,27,27,.65);font-size:11px;line-height:1.35}.activity-row .status-dot{width:8px;height:8px;border-radius:999px;background:rgba(27,27,27,.18);margin-top:4px;flex:0 0 auto}.activity-row.running .status-dot{background:#0891B2}.activity-row.done .status-dot{background:#059669}.activity-row.error .status-dot{background:#DC2626}.activity-row.approval .status-dot{background:#D97706}.activity-row.thinking{font-style:italic;color:rgba(27,27,27,.48)}.activity-main{display:block;min-width:0;flex:1}.activity-message{display:block}.activity-row pre{display:none;margin:4px 0 0;max-height:120px;overflow:auto;border-radius:6px;background:rgba(27,27,27,.04);padding:6px;font:10px/1.4 var(--mono);color:rgba(27,27,27,.62);white-space:pre-wrap}.activity-card.expanded .activity-row pre{display:block}.activity-approval{margin-top:7px;border:1px solid rgba(217,119,6,.20);border-radius:7px;background:#FFFBEB;overflow:hidden;transition:opacity .85s ease,transform .85s ease,max-height .85s ease,margin .85s ease}.activity-approval.fading{opacity:0;transform:translateY(-4px);max-height:0;margin:0;pointer-events:none}.activity-approval-head{display:flex;align-items:center;justify-content:space-between;gap:8px;border-bottom:1px solid rgba(217,119,6,.12);padding:6px 8px}.activity-approval-title{font-size:11px;font-weight:800;color:#92400E}.activity-approval-meta{font:10px var(--mono);color:rgba(146,64,14,.58);white-space:nowrap}.activity-approval-subject{margin:7px 8px 0;border:1px solid rgba(217,119,6,.14);border-radius:5px;background:#fff;padding:7px;font:10px/1.45 var(--mono);color:rgba(27,27,27,.72);white-space:pre-wrap;overflow-wrap:anywhere;max-height:130px;overflow:auto}.activity-approval-reason{padding:6px 8px 8px;color:rgba(27,27,27,.56);font-size:11px;line-height:1.45}.activity-approval-actions{display:flex;justify-content:flex-end;gap:6px;flex-wrap:wrap;border-top:1px solid rgba(217,119,6,.12);background:rgba(255,255,255,.58);padding:7px 8px}.activity-result{margin-top:6px}.trace{display:block;flex:1;min-height:0;overflow:auto;background:#fff;padding:8px}.trace-row{font:11px/1.45 var(--mono);border-bottom:1px solid rgba(27,27,27,.05);padding:7px 4px;color:rgba(27,27,27,.52);white-space:pre-wrap;overflow-wrap:anywhere}.trace-row b{color:var(--ws-primary)}
812
1080
  .notebook-preview{flex:1;min-height:0;overflow:auto;background:#F7F5EF;padding:14px 14px 40px;display:flex;flex-direction:column;gap:10px}.notebook-meta{font:11px var(--mono);color:rgba(27,27,27,.42);padding:0 6px}.notebook-cell{border:1px solid var(--ws-border-subtle);border-radius:8px;background:#fff;overflow:hidden;box-shadow:0 1px 0 rgba(27,27,27,.02)}.notebook-cell.code{display:grid;grid-template-columns:64px minmax(0,1fr);padding:0;align-items:stretch}.notebook-cell.markdown{padding:16px 20px}.notebook-cell.raw{background:#F7F5EF;padding:12px 16px}.notebook-cell.raw .notebook-cell-body{padding:0}.notebook-cell.raw pre{margin:0;white-space:pre-wrap;font:12px/1.5 var(--mono);color:rgba(27,27,27,.72)}.notebook-cell-body{padding:0}.notebook-markdown{font-size:14px;line-height:1.65;color:rgba(27,27,27,.86)}.notebook-markdown h1,.notebook-markdown h2,.notebook-markdown h3{line-height:1.2;margin:14px 0 8px;color:rgba(27,27,27,.92)}.notebook-markdown h1{font-size:22px;border-bottom:1px solid var(--ws-border-subtle);padding-bottom:4px}.notebook-markdown p{margin:0 0 10px}.notebook-markdown code{font-family:var(--mono);font-size:.92em;background:rgba(27,27,27,.06);border-radius:4px;padding:1px 4px}.notebook-markdown pre{overflow:auto;background:#0D1117;color:#E5E7EB;border-radius:6px;padding:10px}.notebook-prompt{padding:12px 10px 12px 10px;font:11px/1.4 var(--mono);text-align:right;user-select:none;color:#4B6BFB;background:transparent;border-right:1px solid var(--ws-border-subtle);white-space:nowrap;display:flex;align-items:flex-start;justify-content:flex-end}.notebook-prompt.out{color:#A21818}.notebook-prompt.empty{color:transparent;pointer-events:none}.notebook-prompt.in::before{content:"In "}.notebook-prompt.out::before{content:"Out "}.notebook-cell-monaco{background:#FAFBFC;min-height:52px;position:relative;overflow:hidden;padding:6px 0}.notebook-cell-monaco .monaco-editor,.notebook-cell-monaco .monaco-editor .overflow-guard,.notebook-cell-monaco .monaco-editor-background,.notebook-cell-monaco .margin{background:#FAFBFC!important}.notebook-code-fallback{margin:0;padding:8px 12px;background:transparent;color:#1F2937;font:12px/1.5 var(--mono);white-space:pre;overflow:auto}.notebook-cell-outputs{grid-column:1 / -1;display:grid;grid-template-columns:64px minmax(0,1fr);background:#fff;border-top:1px solid var(--ws-border-subtle)}.notebook-output-item{display:contents}.notebook-output-item+.notebook-output-item>.notebook-prompt,.notebook-output-item+.notebook-output-item>.notebook-output-body{border-top:1px solid var(--ws-border-subtle)}.notebook-output-body{padding:8px 12px;overflow:auto;min-width:0;background:#fff}.notebook-stream{margin:0;font:12px/1.5 var(--mono);color:#1F2937;white-space:pre-wrap;word-break:break-word;background:transparent;padding:0}.notebook-stream.stderr{color:#7F1D1D;background:#FEF2F2;padding:6px 8px;border-radius:5px}.notebook-error{margin:0;font:12px/1.5 var(--mono);color:#7F1D1D;background:#FEF2F2;padding:8px 10px;border-radius:5px;white-space:pre-wrap;word-break:break-word;overflow:auto}.notebook-plain{margin:0;font:12px/1.5 var(--mono);color:#1F2937;white-space:pre-wrap;word-break:break-word;background:transparent;padding:0}.notebook-json{margin:0;font:12px/1.5 var(--mono);color:#0F172A;background:#F8FAFC;padding:8px 10px;border-radius:5px;white-space:pre;overflow:auto}.notebook-output-body img{max-width:100%;height:auto;display:block;background:#fff;border-radius:4px}.notebook-output-body iframe{width:100%;min-height:220px;border:1px solid var(--ws-border-subtle);border-radius:5px;background:#fff}.notebook-latex{font-size:14px;color:rgba(27,27,27,.90);padding:6px 0;overflow:auto}.katex-display{margin:6px 0!important}.viewer.maximized{position:fixed;inset:12px;z-index:70;border:1px solid rgba(27,27,27,.12);border-radius:8px;box-shadow:0 24px 80px rgba(27,27,27,.28);background:#fff}.viewer.maximized .diagram-preview,.viewer.maximized .markdown-preview{max-width:none}.viewer.maximized .mermaid-output{min-height:calc(100vh - 220px)}.composer{border-top:1px solid var(--ws-border-subtle);background:#FFFDF7;padding:10px 12px}.composer-box{border:1px solid var(--ws-border);border-radius:10px;background:#fff;overflow:hidden}.upload-tray{display:flex;gap:6px;flex-wrap:wrap;padding:8px 9px 0}.upload-tray[hidden]{display:none}.upload-chip{height:24px;display:inline-flex;align-items:center;gap:6px;max-width:100%;border:1px solid rgba(8,145,178,.18);border-radius:999px;background:#F0F9FF;color:#075985;padding:0 4px 0 8px;font-size:11px}.upload-chip-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:210px}.upload-chip-meta{font:10px var(--mono);color:rgba(7,89,133,.58)}.upload-chip button{width:18px;height:18px;border:0;border-radius:999px;background:transparent;color:rgba(7,89,133,.52);cursor:pointer;padding:0}.upload-chip button:hover{background:rgba(8,145,178,.12);color:#075985}textarea{display:block;width:100%;min-height:84px;max-height:240px;resize:vertical;border:0;padding:10px 11px;background:#fff;color:var(--ws-foreground);outline:none}.composer-actions{height:34px;border-top:1px solid var(--ws-border-subtle);display:flex;align-items:center;justify-content:space-between;padding:0 8px}.composer-left,.composer-right{display:flex;align-items:center;gap:6px;min-width:0}.composer-right{margin-left:auto}.status{font-size:11px;color:var(--ws-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:8px}.stop-button{height:24px;border:1px solid rgba(185,28,28,.20);border-radius:6px;background:#FEF2F2;color:#B91C1C;font-size:11px;font-weight:750;padding:0 8px;display:inline-flex;align-items:center;gap:5px;cursor:pointer}.stop-button svg{width:12px;height:12px}.stop-button:hover{background:#FEE2E2;border-color:rgba(185,28,28,.30)}.stop-button:disabled{opacity:.55;cursor:not-allowed}.stop-button[hidden]{display:none}button.primary{height:24px;border:0;border-radius:6px;background:var(--ws-foreground);color:#fff;font-size:11px;font-weight:750;padding:0 10px;cursor:pointer}button.primary:hover{background:#2B2B2B}button.primary:disabled{opacity:.45;cursor:not-allowed}
813
1081
  .approval-inline{transition:opacity .85s ease,transform .85s ease,max-height .85s ease,margin .85s ease}.approval-inline.fading{opacity:0;transform:translateY(-4px);max-height:0;margin:0;pointer-events:none}.approval-inline-actions .approval-approve{background:#ECFDF5;border-color:#A7F3D0;color:#047857}.approval-inline-actions .approval-approve:hover{background:#D1FAE5;color:#065F46}.approval-inline-actions .approval-reject{background:#FEF2F2;border-color:#FECACA;color:#B91C1C}.approval-inline-actions .approval-reject:hover{background:#FEE2E2;color:#991B1B}.approval-inline-actions .approval-secondary{background:#EFF6FF;border-color:#BFDBFE;color:#1D4ED8}.approval-inline-actions .approval-secondary:hover{background:#DBEAFE;color:#1E40AF}.approval-result-line{display:inline-flex;max-width:100%;align-items:center;gap:7px;border:1px solid rgba(27,27,27,.08);border-radius:999px;background:#F7F5EF;color:rgba(27,27,27,.58);padding:5px 9px;font:11px/1.2 var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.approval-result-line.approved{background:#ECFDF5;border-color:#A7F3D0;color:#047857}.approval-result-line.denied{background:#FEF2F2;border-color:#FECACA;color:#B91C1C}.approval-result-tool{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:420px}
@@ -857,6 +1125,10 @@ main.hide-files{grid-template-columns:0 0 minmax(360px,1fr) 6px var(--right-w)}m
857
1125
  <button class="menu-item" type="button">Edit</button>
858
1126
  <button class="menu-item" type="button">Run</button>
859
1127
  <button class="menu-item" type="button">Tools</button>
1128
+ <div class="plugins-menu-wrap" id="pluginsMenuWrap" hidden>
1129
+ <button class="menu-item" id="pluginsMenuButton" type="button" aria-haspopup="menu" aria-expanded="false">Plugins</button>
1130
+ <div class="plugins-dropdown" id="pluginsDropdown" hidden role="menu"></div>
1131
+ </div>
860
1132
  <div class="menubar-spacer"></div>
861
1133
  <div class="session-menu-wrap">
862
1134
  <button class="menu-item session-menu-button" id="sessionMenuButton" type="button" aria-haspopup="dialog" aria-expanded="false">Chat: New</button>
@@ -957,6 +1229,7 @@ const queuedFollowupIds = new Set();
957
1229
  let historySelectionReady = false;
958
1230
  let historySessions = [];
959
1231
  let sessionMenuOpen = false;
1232
+ let activeBackendSessionId = null;
960
1233
  let traceCount = 0;
961
1234
  let pendingApproval = null;
962
1235
  let pendingApprovalEl = null;
@@ -1387,6 +1660,7 @@ async function loadDir(path){
1387
1660
  }
1388
1661
  }
1389
1662
  async function openFile(path, knownData){
1663
+ if(path&&path.startsWith('plugin://')){openPluginView(path);return;}
1390
1664
  currentPath=path;
1391
1665
  if(!openTabs.some(t=>t.path===path))openTabs.push({path,name:basename(path)});
1392
1666
  activePath=path;
@@ -2030,6 +2304,7 @@ function renderHistorySnapshot(history){
2030
2304
  resetThread(history.backend_session_id ? 'No chat messages in this transcript yet.' : 'Ask about this local workspace.');
2031
2305
  const messages=Array.isArray(history.messages)?history.messages:[];
2032
2306
  const trace=Array.isArray(history.trace)?history.trace:[];
2307
+ activeBackendSessionId=history.backend_session_id || null;
2033
2308
  if(history.backend_session_id){
2034
2309
  document.getElementById('bridgeState').textContent='resumed';
2035
2310
  document.getElementById('sendStatus').textContent='Resumed '+compactId(history.backend_session_id);
@@ -2177,11 +2452,12 @@ function renderSessionMenu(sessions){
2177
2452
  const tools=(Array.isArray(s.tools)?s.tools:[]).map(t=>'<span class="session-chip">'+esc(t)+'</span>').join('');
2178
2453
  const prompt=s.first_prompt || 'Untitled session';
2179
2454
  const meta=[formatSessionTime(s.last_activity_at||s.started_at), (s.user_messages||0)+' user', (s.assistant_messages||0)+' assistant', s.status||'unknown', compactId(s.session_id)].filter(Boolean).join(' · ');
2180
- return '<div class="session-row"><div class="session-main"><div class="session-prompt">'+esc(prompt)+'</div><div class="session-meta">'+esc(meta)+'</div>'+(tools?'<div class="session-tools">'+tools+'</div>':'')+'</div><button class="icon-button" type="button" data-resume-session="'+esc(s.session_id)+'">Resume</button></div>';
2455
+ const active=s.session_id===activeBackendSessionId;
2456
+ return '<button class="session-row '+(active?'selected':'')+'" type="button" data-resume-session="'+esc(s.session_id)+'" '+(active?'aria-current="true"':'')+'><span class="session-main"><span class="session-prompt">'+esc(prompt)+'</span><span class="session-meta">'+esc(meta)+'</span>'+(tools?'<span class="session-tools">'+tools+'</span>':'')+'</span><span class="session-action">'+(active?'Current':'Resume')+'</span></button>';
2181
2457
  }).join('');
2182
2458
  const empty=historySessions.length?'':'<div class="session-menu-empty">No previous sessions for this folder.</div>';
2183
- menu.innerHTML='<div class="session-list"><div class="session-row"><div class="session-main"><div class="session-prompt">New chat</div><div class="session-meta">Start without loading prior folder history</div></div><button class="icon-button" type="button" id="newHistorySession">Start</button></div>'+empty+rows+'</div>';
2184
- menu.querySelector('#newHistorySession').addEventListener('click',startNewChat);
2459
+ menu.innerHTML='<div class="session-list"><button class="session-row '+(!activeBackendSessionId?'selected':'')+'" type="button" id="newHistorySession" '+(!activeBackendSessionId?'aria-current="true"':'')+'><span class="session-main"><span class="session-prompt">New chat</span><span class="session-meta">Start without loading prior folder history</span></span><span class="session-action">Start</span></button>'+empty+rows+'</div>';
2460
+ menu.querySelector('#newHistorySession')?.addEventListener('click',startNewChat);
2185
2461
  menu.querySelectorAll('[data-resume-session]').forEach(btn=>btn.addEventListener('click',()=>resumeChat(btn.dataset.resumeSession)));
2186
2462
  if(historySessions.length){
2187
2463
  document.getElementById('bridgeState').textContent='choose session';
@@ -2218,6 +2494,7 @@ async function startNewChat(){
2218
2494
  document.getElementById('sendStatus').textContent='Starting new chat...';
2219
2495
  try {
2220
2496
  const history=await api('/api/chat/new',{method:'POST'});
2497
+ activeBackendSessionId=null;
2221
2498
  setSessionMenuOpen(false);
2222
2499
  renderHistorySnapshot(history);
2223
2500
  } catch (err) {
@@ -2280,6 +2557,46 @@ function setAgentTab(tab){
2280
2557
  btn.setAttribute('aria-selected', String(active));
2281
2558
  });
2282
2559
  }
2560
+ // Plugin views render as CENTRAL panel tabs (like files), keyed plugin://
2561
+ // Discoverable via the top "Plugins" menu; tabs are also opened at boot.
2562
+ const pluginViews=new Map();
2563
+ async function loadPluginViews(){
2564
+ try{
2565
+ const data=await api('/api/plugin-views');
2566
+ const views=data.views||[];
2567
+ if(!views.length)return;
2568
+ const wrap=document.getElementById('pluginsMenuWrap');
2569
+ const dropdown=document.getElementById('pluginsDropdown');
2570
+ const button=document.getElementById('pluginsMenuButton');
2571
+ views.forEach(view=>{
2572
+ const key='plugin://'+view.plugin+'/'+(view.name||'view');
2573
+ pluginViews.set(key,view);
2574
+ if(!openTabs.some(t=>t.path===key))openTabs.push({path:key,name:view.name||view.plugin});
2575
+ const item=document.createElement('button');
2576
+ item.type='button';item.setAttribute('role','menuitem');
2577
+ item.innerHTML='<span>'+esc(view.name||view.plugin)+'</span><span class="plugin-origin">'+esc(view.plugin)+'</span>';
2578
+ item.addEventListener('click',()=>{dropdown.hidden=true;button.setAttribute('aria-expanded','false');openPluginView(key);});
2579
+ dropdown.appendChild(item);
2580
+ });
2581
+ if(wrap)wrap.hidden=false;
2582
+ if(button&&dropdown){
2583
+ button.addEventListener('click',(e)=>{e.stopPropagation();const open=dropdown.hidden;dropdown.hidden=!open;button.setAttribute('aria-expanded',String(open));});
2584
+ document.addEventListener('click',()=>{if(!dropdown.hidden){dropdown.hidden=true;button.setAttribute('aria-expanded','false');}});
2585
+ }
2586
+ renderTabs();
2587
+ }catch{}
2588
+ }
2589
+ function openPluginView(key){
2590
+ const view=pluginViews.get(key);
2591
+ if(!view)return;
2592
+ currentPath=key;
2593
+ activePath=key;
2594
+ if(!openTabs.some(t=>t.path===key))openTabs.push({path:key,name:view.name||view.plugin});
2595
+ renderTabs();
2596
+ renderExplorer();
2597
+ renderViewer('<div class="file-header"><span>⚙</span><span class="path">'+esc(view.plugin)+' · '+esc(view.name)+'</span></div>'+
2598
+ '<div class="frame-preview"><iframe sandbox="allow-scripts allow-same-origin allow-forms" src="'+esc(view.url)+'?token='+encodeURIComponent(token)+'"></iframe></div>');
2599
+ }
2283
2600
  function setTurnRunning(running){
2284
2601
  turnRunning=Boolean(running);
2285
2602
  if(!turnRunning)turnCancelling=false;
@@ -2450,10 +2767,14 @@ function handleRelayEvent(type, event){
2450
2767
  }
2451
2768
  if (type === 'agent_history_loaded') {
2452
2769
  document.getElementById('bridgeState').textContent = 'resumed';
2453
- if(data.backend_session_id) setSessionButtonLabel('Chat: '+compactId(data.backend_session_id));
2770
+ if(data.backend_session_id){
2771
+ activeBackendSessionId=data.backend_session_id;
2772
+ setSessionButtonLabel('Chat: '+compactId(data.backend_session_id));
2773
+ }
2454
2774
  return;
2455
2775
  }
2456
2776
  if (type === 'agent_history_new') {
2777
+ activeBackendSessionId=null;
2457
2778
  document.getElementById('bridgeState').textContent = 'new session';
2458
2779
  setSessionButtonLabel('Chat: New');
2459
2780
  return;
@@ -2481,7 +2802,10 @@ function handleRelayEvent(type, event){
2481
2802
  return;
2482
2803
  }
2483
2804
  if (type === 'agent_session') {
2484
- if(data.session_id) setSessionButtonLabel('Chat: '+compactId(data.session_id));
2805
+ if(data.session_id){
2806
+ activeBackendSessionId=data.session_id;
2807
+ setSessionButtonLabel('Chat: '+compactId(data.session_id));
2808
+ }
2485
2809
  return;
2486
2810
  }
2487
2811
  if (type === 'agent_turn_started') {
@@ -2627,6 +2951,7 @@ renderTabs();
2627
2951
  renderEmptyViewer();
2628
2952
  loadApprovalMode();
2629
2953
  loadSessionChoices();
2954
+ loadPluginViews();
2630
2955
  loadDir('.').then(()=>{if(currentPath&&currentPath!=='.')openFile(currentPath).catch(()=>{});});
2631
2956
  </script>
2632
2957
  </body>
@@ -0,0 +1,30 @@
1
+ import { canonicalToolName } from '../terminal/agents.mjs';
2
+
3
+ // Baseline read tools every graph run may use regardless of member agents.
4
+ const BASE_READ_TOOLS = ['read_file', 'search_code', 'list_files', 'get_file_info', 'get_project_overview'];
5
+
6
+ /**
7
+ * Derive the approval scope for a graph run from the union of its member
8
+ * agents' effective tool allowlists (node.tools narrows agent.tools when
9
+ * present), replacing any hardcoded per-run tool list. Destructive
10
+ * escalation is never pre-approved.
11
+ */
12
+ export function deriveApprovalScope(agentNodes, resolveAgent) {
13
+ const allowed = new Set(BASE_READ_TOOLS);
14
+ for (const node of agentNodes) {
15
+ if (node.type === 'job' || node.type === 'service') {
16
+ allowed.add('shell');
17
+ continue;
18
+ }
19
+ const agent = node.agent || resolveAgent(node.agent_slug);
20
+ if (!agent) continue;
21
+ const agentTools = (Array.isArray(agent.tools) ? agent.tools : []).map(canonicalToolName);
22
+ const nodeTools = (Array.isArray(node.tools) && node.tools.length)
23
+ ? new Set(node.tools.map(canonicalToolName))
24
+ : null;
25
+ for (const tool of agentTools) {
26
+ if (!nodeTools || nodeTools.has(tool)) allowed.add(tool);
27
+ }
28
+ }
29
+ return { allowed_tools: [...allowed].sort(), allow_destructive: false };
30
+ }