@bahulam/code 0.1.10 → 0.1.11
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.
- package/package.json +5 -2
- package/src/agents/loader.mjs +26 -4
- package/src/auth/bahulam-auth.mjs +2 -13
- package/src/commands/plugin-manage.mjs +449 -0
- package/src/commands/plugin.mjs +247 -0
- package/src/config/env.mjs +2 -0
- package/src/config/hook-runner.mjs +8 -8
- package/src/config/memory-loader.mjs +7 -3
- package/src/config/settings-loader.mjs +5 -3
- package/src/core/attachments.mjs +2 -2
- package/src/core/headless.mjs +6 -1
- package/src/core/local-store.mjs +10 -10
- package/src/core/paths.mjs +10 -96
- package/src/core/policy-resolver.mjs +1 -1
- package/src/core/project-context-loader.mjs +2 -2
- package/src/core/stream-client.mjs +68 -1
- package/src/core/system-prompt.mjs +31 -12
- package/src/core/tool-executor.mjs +251 -10
- package/src/local-service/agent-relay.mjs +139 -12
- package/src/local-service/server.mjs +230 -7
- package/src/plugins/executor.mjs +121 -0
- package/src/plugins/loader.mjs +123 -123
- package/src/plugins/manifest.mjs +291 -0
- package/src/plugins/preflight.mjs +227 -0
- package/src/plugins/registry.mjs +233 -0
- package/src/plugins/state.mjs +290 -0
- package/src/terminal/agents.mjs +18 -1
- package/src/terminal/init.mjs +2 -2
- package/src/terminal/main.mjs +75 -2
- package/src/terminal/repl-explore.mjs +1 -1
- package/src/terminal/repl-render.mjs +2 -2
- package/src/terminal/repl.mjs +49 -3
- package/src/tools/analyze-image.mjs +1 -1
- package/src/tools/project-overview.mjs +7 -7
- package/src/ui/slash-commands.mjs +18 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
9
10
|
import { createRequire } from 'node:module';
|
|
10
11
|
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
11
12
|
import { AgentHistoryTurnBuilder } from '../core/agent-history.mjs';
|
|
@@ -23,6 +24,66 @@ import { BrowserApprovalManager } from './approval-bridge.mjs';
|
|
|
23
24
|
const __require = createRequire(import.meta.url);
|
|
24
25
|
const VERSION = __require('../../package.json').version;
|
|
25
26
|
|
|
27
|
+
function envList(name) {
|
|
28
|
+
return String(process.env[name] || '')
|
|
29
|
+
.split(',')
|
|
30
|
+
.map(item => item.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function activePluginNamesFromSession(session = {}) {
|
|
35
|
+
const names = [];
|
|
36
|
+
if (session.plugin?.name) names.push(session.plugin.name);
|
|
37
|
+
if (session.plugin?.metadata_name) names.push(session.plugin.metadata_name);
|
|
38
|
+
const plugins = Array.isArray(session.plugins)
|
|
39
|
+
? session.plugins
|
|
40
|
+
: (Array.isArray(session.active_plugins) ? session.active_plugins : []);
|
|
41
|
+
for (const plugin of plugins) {
|
|
42
|
+
if (typeof plugin === 'string') names.push(plugin);
|
|
43
|
+
else if (plugin?.name) names.push(plugin.name);
|
|
44
|
+
else if (plugin?.metadata_name) names.push(plugin.metadata_name);
|
|
45
|
+
}
|
|
46
|
+
return [...new Set(names.map(name => String(name).trim()).filter(Boolean))];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pluginScanDirsFromSession(session = {}) {
|
|
50
|
+
const dirs = [];
|
|
51
|
+
const addPluginParent = (plugin) => {
|
|
52
|
+
const pluginDir = String(plugin?.plugin_dir || plugin?.dir || '').trim();
|
|
53
|
+
if (pluginDir) dirs.push(path.dirname(pluginDir));
|
|
54
|
+
};
|
|
55
|
+
if (session.plugin) addPluginParent(session.plugin);
|
|
56
|
+
const plugins = Array.isArray(session.plugins)
|
|
57
|
+
? session.plugins
|
|
58
|
+
: (Array.isArray(session.active_plugins) ? session.active_plugins : []);
|
|
59
|
+
for (const plugin of plugins) {
|
|
60
|
+
if (plugin && typeof plugin === 'object') addPluginParent(plugin);
|
|
61
|
+
}
|
|
62
|
+
return [...new Set(dirs.filter(Boolean))];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* ${VAR} expansion for MCP server configs — args, env values, headers.
|
|
67
|
+
* Matches how the existing ~/.bahulam/config.json mcpServers block is
|
|
68
|
+
* handled so a plugin author with a working Claude Desktop MCP config
|
|
69
|
+
* gets the same env-var behavior after drop-in.
|
|
70
|
+
*/
|
|
71
|
+
function _expandEnvInMcpConfig(config) {
|
|
72
|
+
const sub = (v) => typeof v === 'string'
|
|
73
|
+
? v.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => process.env[name] ?? '')
|
|
74
|
+
: v;
|
|
75
|
+
const walk = (v) => {
|
|
76
|
+
if (Array.isArray(v)) return v.map(walk);
|
|
77
|
+
if (v && typeof v === 'object') {
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const [k, val] of Object.entries(v)) out[k] = walk(val);
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
return sub(v);
|
|
83
|
+
};
|
|
84
|
+
return walk(config);
|
|
85
|
+
}
|
|
86
|
+
|
|
26
87
|
export class LocalAgentRelay {
|
|
27
88
|
constructor({ session, emit } = {}) {
|
|
28
89
|
if (!session?.id) throw new Error('LocalAgentRelay requires a session');
|
|
@@ -69,17 +130,16 @@ export class LocalAgentRelay {
|
|
|
69
130
|
err.code = 'CONFLICT';
|
|
70
131
|
throw err;
|
|
71
132
|
}
|
|
72
|
-
if (this.turnCount > 0) {
|
|
73
|
-
const err = new Error('Start a new local workspace session to switch history after a turn has run');
|
|
74
|
-
err.code = 'CONFLICT';
|
|
75
|
-
throw err;
|
|
76
|
-
}
|
|
77
133
|
this.resumeLoaded = true;
|
|
78
134
|
this.resumeSessionId = null;
|
|
135
|
+
this.turnCount = 0;
|
|
79
136
|
this.displayHistory = [];
|
|
80
137
|
this.agentHistory = [];
|
|
81
138
|
this.jsonlWriter = null;
|
|
82
|
-
if (this.client)
|
|
139
|
+
if (this.client) {
|
|
140
|
+
this.client.sessionId = null;
|
|
141
|
+
this.client.currentTaskId = null;
|
|
142
|
+
}
|
|
83
143
|
this.emit('agent_history_new', {
|
|
84
144
|
root_path: this.session.root_path,
|
|
85
145
|
});
|
|
@@ -104,11 +164,6 @@ export class LocalAgentRelay {
|
|
|
104
164
|
err.code = 'CONFLICT';
|
|
105
165
|
throw err;
|
|
106
166
|
}
|
|
107
|
-
if (this.turnCount > 0 && this.resumeSessionId !== requestedSessionId) {
|
|
108
|
-
const err = new Error('Start a new local workspace session to switch history after a turn has run');
|
|
109
|
-
err.code = 'CONFLICT';
|
|
110
|
-
throw err;
|
|
111
|
-
}
|
|
112
167
|
if (this.resumeLoaded && this.resumeSessionId === requestedSessionId) return this._historySnapshot();
|
|
113
168
|
|
|
114
169
|
try {
|
|
@@ -131,6 +186,7 @@ export class LocalAgentRelay {
|
|
|
131
186
|
this.displayHistory = history.displayHistory || [];
|
|
132
187
|
this.agentHistory = history.agentHistory || [];
|
|
133
188
|
this.resumeLoaded = true;
|
|
189
|
+
this.turnCount = 0;
|
|
134
190
|
if (this.client) this.client.sessionId = this.resumeSessionId;
|
|
135
191
|
if (this.jsonlWriter?.sessionId && this.jsonlWriter.sessionId !== this.resumeSessionId) {
|
|
136
192
|
this.jsonlWriter = null;
|
|
@@ -344,6 +400,14 @@ export class LocalAgentRelay {
|
|
|
344
400
|
|
|
345
401
|
async close() {
|
|
346
402
|
this.approvalManager?.rejectAll?.();
|
|
403
|
+
// Tear down plugin-owned MCP clients — the executor's registered
|
|
404
|
+
// <server>.<tool> entries are removed here so a rerun in the same
|
|
405
|
+
// process picks up any manifest edits cleanly.
|
|
406
|
+
for (const { plugin, name, client } of (this._mcpClients || [])) {
|
|
407
|
+
try { this.toolExecutor?.unregisterMcpServer?.(plugin, name); } catch { /* ignore */ }
|
|
408
|
+
try { await client.disconnect(); } catch { /* ignore */ }
|
|
409
|
+
}
|
|
410
|
+
this._mcpClients = [];
|
|
347
411
|
try {
|
|
348
412
|
await this.jsonlWriter?.close?.();
|
|
349
413
|
} catch {}
|
|
@@ -501,6 +565,14 @@ export class LocalAgentRelay {
|
|
|
501
565
|
return payload;
|
|
502
566
|
}
|
|
503
567
|
|
|
568
|
+
async executeTool(name, args = {}) {
|
|
569
|
+
await this._ensureReady();
|
|
570
|
+
if (!this.toolExecutor) {
|
|
571
|
+
throw new Error('Tool executor is not initialized');
|
|
572
|
+
}
|
|
573
|
+
return this.toolExecutor.execute(name, args);
|
|
574
|
+
}
|
|
575
|
+
|
|
504
576
|
async _ensureReady() {
|
|
505
577
|
if (this.ready) return this.ready;
|
|
506
578
|
this.ready = this._initialize().catch((err) => {
|
|
@@ -526,10 +598,63 @@ export class LocalAgentRelay {
|
|
|
526
598
|
process.chdir(this.session.root_path);
|
|
527
599
|
}
|
|
528
600
|
|
|
529
|
-
const
|
|
601
|
+
const { PluginRegistry } = await import('../plugins/registry.mjs');
|
|
602
|
+
const activePlugins = activePluginNamesFromSession(this.session);
|
|
603
|
+
const pluginDirs = pluginScanDirsFromSession(this.session);
|
|
604
|
+
const pluginRegistry = new PluginRegistry({
|
|
605
|
+
disabled: envList('BAHULAM_DISABLE_PLUGINS'),
|
|
606
|
+
enabled: activePlugins,
|
|
607
|
+
...(pluginDirs.length ? { pluginDirs } : {}),
|
|
608
|
+
});
|
|
609
|
+
pluginRegistry.scan();
|
|
610
|
+
|
|
611
|
+
// Reactive pulse: every plugin state write bubbles up here and lands
|
|
612
|
+
// on the shared SSE bus as `plugin_state_changed`. Views subscribed
|
|
613
|
+
// to /api/events re-render as the agent works — the Shared
|
|
614
|
+
// Blackboard's live-update moment. `this.emit` is the same event
|
|
615
|
+
// sink server.mjs uses for tool_call / tool_done / agent events.
|
|
616
|
+
const stateEmit = (evt) => {
|
|
617
|
+
try { this.emit('plugin_state_changed', evt); }
|
|
618
|
+
catch { /* never let SSE failure break a tool call */ }
|
|
619
|
+
};
|
|
620
|
+
const toolExecutor = createToolExecutor({ pluginRegistry, stateEmit });
|
|
530
621
|
await toolExecutor.waitForAutoRegister?.();
|
|
531
622
|
await toolExecutor.registerProjectRoots?.([this.session.root_path], { forceRefresh: false });
|
|
532
623
|
|
|
624
|
+
// Plugin=MCP+UX: every plugin can declare mcpServers in its
|
|
625
|
+
// plugin.yaml (or a sibling mcp.json). Spawn each, discover its
|
|
626
|
+
// tools, and register them as `<server>.<tool>` in the shared
|
|
627
|
+
// executor. Failure of one server never blocks the workspace —
|
|
628
|
+
// its tools drop out, everything else keeps running.
|
|
629
|
+
this._mcpClients = []; // {plugin, name, client} — for teardown on close()
|
|
630
|
+
for (const entry of pluginRegistry.listMcpServers?.() || []) {
|
|
631
|
+
try {
|
|
632
|
+
const { McpClient } = await import('../mcp/client.mjs');
|
|
633
|
+
const client = new McpClient(_expandEnvInMcpConfig(entry.config));
|
|
634
|
+
await client.connect();
|
|
635
|
+
const tools = await client.listTools();
|
|
636
|
+
let registered = 0;
|
|
637
|
+
for (const t of tools) {
|
|
638
|
+
if (toolExecutor.registerMcpTool?.(entry.plugin, entry.name, t.name, client, t.inputSchema)) {
|
|
639
|
+
registered++;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
this._mcpClients.push({ plugin: entry.plugin, name: entry.name, client });
|
|
643
|
+
this.emit('plugin_mcp_ready', {
|
|
644
|
+
plugin: entry.plugin,
|
|
645
|
+
server: entry.name,
|
|
646
|
+
tools: registered,
|
|
647
|
+
});
|
|
648
|
+
} catch (err) {
|
|
649
|
+
// Never fatal: the plugin's JS tools + views still work.
|
|
650
|
+
this.emit('plugin_mcp_failed', {
|
|
651
|
+
plugin: entry.plugin,
|
|
652
|
+
server: entry.name,
|
|
653
|
+
error: String(err.message || err),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
533
658
|
const approval = new BrowserApprovalManager({
|
|
534
659
|
emit: this.emit,
|
|
535
660
|
cwd: this.session.root_path,
|
|
@@ -545,6 +670,7 @@ export class LocalAgentRelay {
|
|
|
545
670
|
toolExecutor,
|
|
546
671
|
approvalManager: approval,
|
|
547
672
|
mode: 'remote',
|
|
673
|
+
pluginRegistry,
|
|
548
674
|
});
|
|
549
675
|
if (this.resumeSessionId) this.client.sessionId = this.resumeSessionId;
|
|
550
676
|
this._ensureJsonlWriter();
|
|
@@ -553,6 +679,7 @@ export class LocalAgentRelay {
|
|
|
553
679
|
backend_url: creds.backendUrl,
|
|
554
680
|
root_path: this.session.root_path,
|
|
555
681
|
tools: toolExecutor.listTools?.().length || 0,
|
|
682
|
+
plugins: pluginRegistry.list?.().map(plugin => plugin.metadata?.name).filter(Boolean) || [],
|
|
556
683
|
backend_session_id: this.client.sessionId || null,
|
|
557
684
|
approval_mode: this.approvalAutoMode ? 'auto' : 'ask',
|
|
558
685
|
});
|
|
@@ -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,
|
|
@@ -420,18 +422,193 @@ async function routeRequest({ req, res, sessionId, token, events, sseClients, em
|
|
|
420
422
|
|
|
421
423
|
if (req.method === 'POST' && url.pathname === '/api/tools/execute') {
|
|
422
424
|
const body = await readJsonBody(req);
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
error: '
|
|
427
|
-
|
|
428
|
-
}
|
|
425
|
+
const name = String(body.name || '').trim();
|
|
426
|
+
const args = body.args || {};
|
|
427
|
+
if (!name) {
|
|
428
|
+
sendJson(res, 400, { ok: false, error: 'tool name is required' });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
emit('tool_execution_requested', { name });
|
|
432
|
+
try {
|
|
433
|
+
const relay = getAgentRelay(session);
|
|
434
|
+
const result = await relay.executeTool(name, args);
|
|
435
|
+
sendJson(res, 200, { ok: true, result });
|
|
436
|
+
} catch (err) {
|
|
437
|
+
sendJson(res, 500, { ok: false, error: err.message || String(err) });
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Plugin workspace views (PRD-101 §4.6): panels declared under
|
|
443
|
+
// spec.workspace.views in plugin.yaml, rendered as sandboxed iframes.
|
|
444
|
+
// Generic infra — ANY installed plugin (project-local or ~/.bahulam)
|
|
445
|
+
// contributes tabs; its whole directory is served statically so views
|
|
446
|
+
// can ship css/js/assets with relative paths.
|
|
447
|
+
if (req.method === 'GET' && url.pathname === '/api/plugin-views') {
|
|
448
|
+
const views = getPluginViews(session).map(v => ({
|
|
449
|
+
plugin: v.plugin,
|
|
450
|
+
name: v.name,
|
|
451
|
+
url: `/plugin-view/${encodeURIComponent(v.plugin)}/${v.source.split('/').map(encodeURIComponent).join('/')}`,
|
|
452
|
+
}));
|
|
453
|
+
sendJson(res, 200, { ok: true, views });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Shared-blackboard direct-access API for plugin views. The tool
|
|
458
|
+
// executor already injects `options.state` for agent-driven writes;
|
|
459
|
+
// this endpoint gives browser-side views the same surface so a human
|
|
460
|
+
// clicking a button hits the exact same store the agent uses.
|
|
461
|
+
//
|
|
462
|
+
// POST /api/plugin-state/<plugin> body: {op, ...args}
|
|
463
|
+
// ops: get|set|patch|append|list|keys|delete|query
|
|
464
|
+
//
|
|
465
|
+
// Writes emit a `plugin_state_changed` SSE event on the shared bus,
|
|
466
|
+
// so any view watching /api/events re-renders live.
|
|
467
|
+
const pluginStateMatch = req.method === 'POST'
|
|
468
|
+
? url.pathname.match(/^\/api\/plugin-state\/([^/]+)$/)
|
|
469
|
+
: null;
|
|
470
|
+
if (pluginStateMatch) {
|
|
471
|
+
const pluginName = decodeURIComponent(pluginStateMatch[1]);
|
|
472
|
+
// Scope guard: `bahulam plugin <name>` sessions are pinned to one
|
|
473
|
+
// plugin. Refuse cross-plugin state access so a hostile view can't
|
|
474
|
+
// read another plugin's DB via a hand-crafted URL.
|
|
475
|
+
const scoped = session?.plugin?.name;
|
|
476
|
+
if (scoped && String(scoped).toLowerCase() !== pluginName.toLowerCase()) {
|
|
477
|
+
sendJson(res, 403, { ok: false, error: 'plugin_scope_mismatch' });
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (!getPluginDirs(session).has(pluginName)) {
|
|
481
|
+
sendJson(res, 404, { ok: false, error: 'plugin_not_found' });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
let body;
|
|
485
|
+
try { body = await readJsonBody(req); }
|
|
486
|
+
catch (err) { sendJson(res, 400, { ok: false, error: 'bad_body', message: err.message }); return; }
|
|
487
|
+
const op = String(body.op || '').trim();
|
|
488
|
+
if (!op) { sendJson(res, 400, { ok: false, error: 'op_required' }); return; }
|
|
489
|
+
try {
|
|
490
|
+
const { makePluginState } = await import('../plugins/state.mjs');
|
|
491
|
+
const state = makePluginState(pluginName, {
|
|
492
|
+
emit: (evt) => { try { emit('plugin_state_changed', evt); } catch { /* ignore */ } },
|
|
493
|
+
});
|
|
494
|
+
const result = runStateOp(state, op, body);
|
|
495
|
+
sendJson(res, 200, { ok: true, result });
|
|
496
|
+
} catch (err) {
|
|
497
|
+
sendJson(res, 500, { ok: false, error: 'state_op_failed', message: err.message });
|
|
498
|
+
}
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const pluginViewMatch = req.method === 'GET'
|
|
503
|
+
? url.pathname.match(/^\/plugin-view\/([^/]+)\/(.+)$/)
|
|
504
|
+
: null;
|
|
505
|
+
if (pluginViewMatch) {
|
|
506
|
+
const pluginName = decodeURIComponent(pluginViewMatch[1]);
|
|
507
|
+
const relPath = pluginViewMatch[2].split('/').map(decodeURIComponent).join('/');
|
|
508
|
+
const dir = getPluginDirs(session).get(pluginName);
|
|
509
|
+
if (!dir) {
|
|
510
|
+
sendJson(res, 404, { ok: false, error: 'plugin_not_found' });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const root = path.resolve(dir);
|
|
514
|
+
const abs = path.resolve(root, relPath);
|
|
515
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
516
|
+
sendJson(res, 403, { ok: false, error: 'forbidden' });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
const content = fs.readFileSync(abs);
|
|
521
|
+
res.writeHead(200, { 'Content-Type': pluginViewContentType(abs) });
|
|
522
|
+
res.end(content);
|
|
523
|
+
} catch {
|
|
524
|
+
sendJson(res, 404, { ok: false, error: 'plugin_view_file_missing' });
|
|
525
|
+
}
|
|
429
526
|
return;
|
|
430
527
|
}
|
|
431
528
|
|
|
432
529
|
sendJson(res, 404, { ok: false, error: 'not_found' });
|
|
433
530
|
}
|
|
434
531
|
|
|
532
|
+
// Short-TTL registry cache, keyed by session so `bahulam plugin <name>`
|
|
533
|
+
// only surfaces the scoped plugin's views. A generic `workspace open`
|
|
534
|
+
// session (no session.plugin) sees every installed plugin — that's the
|
|
535
|
+
// developer/multi-plugin experience.
|
|
536
|
+
const _pluginScan = new Map();
|
|
537
|
+
function scanPlugins(session) {
|
|
538
|
+
const scope = session?.plugin?.name ? String(session.plugin.name).toLowerCase() : '__all__';
|
|
539
|
+
const cached = _pluginScan.get(scope);
|
|
540
|
+
if (cached && Date.now() - cached.at < 5000) return cached;
|
|
541
|
+
const views = [];
|
|
542
|
+
const dirs = new Map();
|
|
543
|
+
try {
|
|
544
|
+
const registry = new PluginRegistry({
|
|
545
|
+
pluginDirs: [
|
|
546
|
+
path.join(session.root_path || process.cwd(), '.bahulam', 'plugins'),
|
|
547
|
+
path.join(os.homedir(), '.bahulam', 'plugins'),
|
|
548
|
+
],
|
|
549
|
+
}).scan();
|
|
550
|
+
for (const manifest of registry.list()) {
|
|
551
|
+
const pluginName = manifest.metadata?.name || '';
|
|
552
|
+
if (!pluginName || !manifest._dir) continue;
|
|
553
|
+
if (scope !== '__all__' && pluginName.toLowerCase() !== scope) continue;
|
|
554
|
+
dirs.set(pluginName, manifest._dir);
|
|
555
|
+
(manifest.spec?.workspace?.views || []).forEach((view) => {
|
|
556
|
+
const source = String(view?.source || '').trim().replace(/^\.\//, '');
|
|
557
|
+
if (!source) return;
|
|
558
|
+
views.push({
|
|
559
|
+
plugin: pluginName,
|
|
560
|
+
name: String(view?.name || '').trim() || pluginName,
|
|
561
|
+
source,
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
} catch {
|
|
566
|
+
// plugin scan failures must never break the workspace
|
|
567
|
+
}
|
|
568
|
+
const entry = { at: Date.now(), views, dirs };
|
|
569
|
+
_pluginScan.set(scope, entry);
|
|
570
|
+
return entry;
|
|
571
|
+
}
|
|
572
|
+
function getPluginViews(session) { return scanPlugins(session).views; }
|
|
573
|
+
function getPluginDirs(session) { return scanPlugins(session).dirs; }
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Dispatch a POST /api/plugin-state op onto the plugin's state proxy.
|
|
577
|
+
* Kept as a plain switch so failures are per-op and the router surfaces
|
|
578
|
+
* a clean 500 with the offending op name rather than a stack trace.
|
|
579
|
+
*/
|
|
580
|
+
function runStateOp(state, op, body) {
|
|
581
|
+
switch (op) {
|
|
582
|
+
case 'get': return state.get(body.key, body.fallback ?? null);
|
|
583
|
+
case 'set': return state.set(body.key, body.value);
|
|
584
|
+
case 'patch': return state.patch(body.key, body.partial ?? body.value);
|
|
585
|
+
case 'delete': return state.delete(body.key);
|
|
586
|
+
case 'keys': return state.keys();
|
|
587
|
+
case 'append': return state.append(body.stream, body.payload);
|
|
588
|
+
case 'list': return state.list(body.stream, { limit: body.limit, order: body.order });
|
|
589
|
+
case 'query': return state.query(body.sql, body.params || []);
|
|
590
|
+
default: throw new Error(`unknown state op: ${op}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Real mime types for plugin views — unlike contentTypeForPath (file
|
|
595
|
+
// viewer), views MUST execute, so html/css/js keep their native types.
|
|
596
|
+
// Plugin code runs client-side by user consent (same trust as shell).
|
|
597
|
+
const PLUGIN_VIEW_MIME = {
|
|
598
|
+
html: 'text/html; charset=utf-8', htm: 'text/html; charset=utf-8',
|
|
599
|
+
css: 'text/css; charset=utf-8',
|
|
600
|
+
js: 'text/javascript; charset=utf-8', mjs: 'text/javascript; charset=utf-8',
|
|
601
|
+
json: 'application/json', map: 'application/json',
|
|
602
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
|
|
603
|
+
webp: 'image/webp', svg: 'image/svg+xml', ico: 'image/x-icon',
|
|
604
|
+
woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf',
|
|
605
|
+
wasm: 'application/wasm',
|
|
606
|
+
};
|
|
607
|
+
function pluginViewContentType(filePath) {
|
|
608
|
+
const ext = path.extname(String(filePath || '')).slice(1).toLowerCase();
|
|
609
|
+
return PLUGIN_VIEW_MIME[ext] || 'application/octet-stream';
|
|
610
|
+
}
|
|
611
|
+
|
|
435
612
|
function requireAuthorizedSession(sessionId, token, req, url) {
|
|
436
613
|
const session = loadLocalWorkspaceSession(sessionId);
|
|
437
614
|
if (!session) {
|
|
@@ -807,7 +984,7 @@ main.hide-files{grid-template-columns:0 0 minmax(360px,1fr) 6px var(--right-w)}m
|
|
|
807
984
|
.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}
|
|
808
985
|
.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
986
|
.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}
|
|
987
|
+
.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}.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:8px;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:5px;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:10px var(--mono)}.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}
|
|
811
988
|
.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
989
|
.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
990
|
.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 +1034,10 @@ main.hide-files{grid-template-columns:0 0 minmax(360px,1fr) 6px var(--right-w)}m
|
|
|
857
1034
|
<button class="menu-item" type="button">Edit</button>
|
|
858
1035
|
<button class="menu-item" type="button">Run</button>
|
|
859
1036
|
<button class="menu-item" type="button">Tools</button>
|
|
1037
|
+
<div class="plugins-menu-wrap" id="pluginsMenuWrap" hidden>
|
|
1038
|
+
<button class="menu-item" id="pluginsMenuButton" type="button" aria-haspopup="menu" aria-expanded="false">Plugins</button>
|
|
1039
|
+
<div class="plugins-dropdown" id="pluginsDropdown" hidden role="menu"></div>
|
|
1040
|
+
</div>
|
|
860
1041
|
<div class="menubar-spacer"></div>
|
|
861
1042
|
<div class="session-menu-wrap">
|
|
862
1043
|
<button class="menu-item session-menu-button" id="sessionMenuButton" type="button" aria-haspopup="dialog" aria-expanded="false">Chat: New</button>
|
|
@@ -1387,6 +1568,7 @@ async function loadDir(path){
|
|
|
1387
1568
|
}
|
|
1388
1569
|
}
|
|
1389
1570
|
async function openFile(path, knownData){
|
|
1571
|
+
if(path&&path.startsWith('plugin://')){openPluginView(path);return;}
|
|
1390
1572
|
currentPath=path;
|
|
1391
1573
|
if(!openTabs.some(t=>t.path===path))openTabs.push({path,name:basename(path)});
|
|
1392
1574
|
activePath=path;
|
|
@@ -2280,6 +2462,46 @@ function setAgentTab(tab){
|
|
|
2280
2462
|
btn.setAttribute('aria-selected', String(active));
|
|
2281
2463
|
});
|
|
2282
2464
|
}
|
|
2465
|
+
// Plugin views render as CENTRAL panel tabs (like files), keyed plugin://
|
|
2466
|
+
// Discoverable via the top "Plugins" menu; tabs are also opened at boot.
|
|
2467
|
+
const pluginViews=new Map();
|
|
2468
|
+
async function loadPluginViews(){
|
|
2469
|
+
try{
|
|
2470
|
+
const data=await api('/api/plugin-views');
|
|
2471
|
+
const views=data.views||[];
|
|
2472
|
+
if(!views.length)return;
|
|
2473
|
+
const wrap=document.getElementById('pluginsMenuWrap');
|
|
2474
|
+
const dropdown=document.getElementById('pluginsDropdown');
|
|
2475
|
+
const button=document.getElementById('pluginsMenuButton');
|
|
2476
|
+
views.forEach(view=>{
|
|
2477
|
+
const key='plugin://'+view.plugin+'/'+(view.name||'view');
|
|
2478
|
+
pluginViews.set(key,view);
|
|
2479
|
+
if(!openTabs.some(t=>t.path===key))openTabs.push({path:key,name:view.name||view.plugin});
|
|
2480
|
+
const item=document.createElement('button');
|
|
2481
|
+
item.type='button';item.setAttribute('role','menuitem');
|
|
2482
|
+
item.innerHTML='<span>'+esc(view.name||view.plugin)+'</span><span class="plugin-origin">'+esc(view.plugin)+'</span>';
|
|
2483
|
+
item.addEventListener('click',()=>{dropdown.hidden=true;button.setAttribute('aria-expanded','false');openPluginView(key);});
|
|
2484
|
+
dropdown.appendChild(item);
|
|
2485
|
+
});
|
|
2486
|
+
if(wrap)wrap.hidden=false;
|
|
2487
|
+
if(button&&dropdown){
|
|
2488
|
+
button.addEventListener('click',(e)=>{e.stopPropagation();const open=dropdown.hidden;dropdown.hidden=!open;button.setAttribute('aria-expanded',String(open));});
|
|
2489
|
+
document.addEventListener('click',()=>{if(!dropdown.hidden){dropdown.hidden=true;button.setAttribute('aria-expanded','false');}});
|
|
2490
|
+
}
|
|
2491
|
+
renderTabs();
|
|
2492
|
+
}catch{}
|
|
2493
|
+
}
|
|
2494
|
+
function openPluginView(key){
|
|
2495
|
+
const view=pluginViews.get(key);
|
|
2496
|
+
if(!view)return;
|
|
2497
|
+
currentPath=key;
|
|
2498
|
+
activePath=key;
|
|
2499
|
+
if(!openTabs.some(t=>t.path===key))openTabs.push({path:key,name:view.name||view.plugin});
|
|
2500
|
+
renderTabs();
|
|
2501
|
+
renderExplorer();
|
|
2502
|
+
renderViewer('<div class="file-header"><span>⚙</span><span class="path">'+esc(view.plugin)+' · '+esc(view.name)+'</span></div>'+
|
|
2503
|
+
'<div class="frame-preview"><iframe sandbox="allow-scripts allow-same-origin allow-forms" src="'+esc(view.url)+'?token='+encodeURIComponent(token)+'"></iframe></div>');
|
|
2504
|
+
}
|
|
2283
2505
|
function setTurnRunning(running){
|
|
2284
2506
|
turnRunning=Boolean(running);
|
|
2285
2507
|
if(!turnRunning)turnCancelling=false;
|
|
@@ -2627,6 +2849,7 @@ renderTabs();
|
|
|
2627
2849
|
renderEmptyViewer();
|
|
2628
2850
|
loadApprovalMode();
|
|
2629
2851
|
loadSessionChoices();
|
|
2852
|
+
loadPluginViews();
|
|
2630
2853
|
loadDir('.').then(()=>{if(currentPath&¤tPath!=='.')openFile(currentPath).catch(()=>{});});
|
|
2631
2854
|
</script>
|
|
2632
2855
|
</body>
|