@bahulam/code 0.1.11 → 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.
- package/package.json +1 -1
- package/src/auth/tarang-auth.mjs +313 -0
- package/src/commands/agent.mjs +7 -7
- package/src/config/cli-args.mjs +16 -0
- package/src/core/background-tasks.mjs +186 -0
- package/src/core/headless.mjs +54 -3
- package/src/core/local-agent.mjs +10 -1
- package/src/core/risk-tier.mjs +1 -0
- package/src/core/stream-client.mjs +95 -15
- package/src/core/tool-executor.mjs +219 -15
- package/src/local-service/agent-relay.mjs +1 -1
- package/src/local-service/server.mjs +116 -14
- package/src/orchestration/approval.mjs +30 -0
- package/src/orchestration/completion-triggers.mjs +40 -0
- package/src/orchestration/dispatch.mjs +118 -0
- package/src/orchestration/events.mjs +19 -0
- package/src/orchestration/graph.mjs +126 -0
- package/src/orchestration/node-runner.mjs +193 -0
- package/src/orchestration/runner.mjs +200 -0
- package/src/plugins/executor.mjs +2 -2
- package/src/plugins/manifest.mjs +27 -27
- package/src/plugins/preflight.mjs +8 -8
- package/src/terminal/agents.mjs +8 -3
- package/src/terminal/main.mjs +8 -2
- package/src/terminal/repl-render.mjs +65 -10
- package/src/terminal/repl-state.mjs +4 -2
- package/src/terminal/repl.mjs +572 -96
- package/src/tools/agent.mjs +6 -2
- package/src/tools/registry.mjs +88 -4
- package/src/ui/slash-commands.mjs +1 -1
- package/src/ui/sub-agent.mjs +14 -8
|
@@ -90,6 +90,14 @@ export async function startLocalWorkspaceService({
|
|
|
90
90
|
return event;
|
|
91
91
|
}
|
|
92
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
|
+
|
|
93
101
|
const server = http.createServer(async (req, res) => {
|
|
94
102
|
try {
|
|
95
103
|
assertLoopbackRequest(req);
|
|
@@ -138,6 +146,7 @@ export async function startLocalWorkspaceService({
|
|
|
138
146
|
host: loopbackHost,
|
|
139
147
|
close: () => new Promise((resolve) => {
|
|
140
148
|
emit('session_stopped', {});
|
|
149
|
+
stopStateWatcher();
|
|
141
150
|
const relay = agentRelay;
|
|
142
151
|
for (const client of sseClients) {
|
|
143
152
|
try {
|
|
@@ -154,6 +163,88 @@ export async function startLocalWorkspaceService({
|
|
|
154
163
|
};
|
|
155
164
|
}
|
|
156
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
|
+
|
|
157
248
|
async function routeRequest({ req, res, sessionId, token, events, sseClients, emit, getAgentRelay }) {
|
|
158
249
|
const url = new URL(req.url || '/', 'http://127.0.0.1');
|
|
159
250
|
|
|
@@ -965,26 +1056,26 @@ function workspaceHtml({ session, token }) {
|
|
|
965
1056
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
966
1057
|
<title>${title} - Bahulam Local</title>
|
|
967
1058
|
<style>
|
|
968
|
-
: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(
|
|
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}
|
|
969
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}
|
|
970
1061
|
button,textarea,input{font:inherit}button{color:inherit}.shell{display:flex;flex-direction:column;height:100vh;min-height:560px;background:var(--ws-bg)}
|
|
971
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}
|
|
972
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}
|
|
973
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}
|
|
974
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}
|
|
975
|
-
.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:
|
|
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}
|
|
976
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}
|
|
977
|
-
.icon-button{height:26px;min-width:26px;border:1px solid rgba(27,27,27,.10);border-radius:
|
|
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}
|
|
978
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}
|
|
979
|
-
.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:
|
|
980
|
-
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)}
|
|
981
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}
|
|
982
|
-
.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:
|
|
983
|
-
.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:
|
|
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,.
|
|
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}
|
|
985
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)}
|
|
986
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}
|
|
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:
|
|
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}
|
|
988
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)}
|
|
989
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}
|
|
990
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}
|
|
@@ -1138,6 +1229,7 @@ const queuedFollowupIds = new Set();
|
|
|
1138
1229
|
let historySelectionReady = false;
|
|
1139
1230
|
let historySessions = [];
|
|
1140
1231
|
let sessionMenuOpen = false;
|
|
1232
|
+
let activeBackendSessionId = null;
|
|
1141
1233
|
let traceCount = 0;
|
|
1142
1234
|
let pendingApproval = null;
|
|
1143
1235
|
let pendingApprovalEl = null;
|
|
@@ -2212,6 +2304,7 @@ function renderHistorySnapshot(history){
|
|
|
2212
2304
|
resetThread(history.backend_session_id ? 'No chat messages in this transcript yet.' : 'Ask about this local workspace.');
|
|
2213
2305
|
const messages=Array.isArray(history.messages)?history.messages:[];
|
|
2214
2306
|
const trace=Array.isArray(history.trace)?history.trace:[];
|
|
2307
|
+
activeBackendSessionId=history.backend_session_id || null;
|
|
2215
2308
|
if(history.backend_session_id){
|
|
2216
2309
|
document.getElementById('bridgeState').textContent='resumed';
|
|
2217
2310
|
document.getElementById('sendStatus').textContent='Resumed '+compactId(history.backend_session_id);
|
|
@@ -2359,11 +2452,12 @@ function renderSessionMenu(sessions){
|
|
|
2359
2452
|
const tools=(Array.isArray(s.tools)?s.tools:[]).map(t=>'<span class="session-chip">'+esc(t)+'</span>').join('');
|
|
2360
2453
|
const prompt=s.first_prompt || 'Untitled session';
|
|
2361
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(' · ');
|
|
2362
|
-
|
|
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>';
|
|
2363
2457
|
}).join('');
|
|
2364
2458
|
const empty=historySessions.length?'':'<div class="session-menu-empty">No previous sessions for this folder.</div>';
|
|
2365
|
-
menu.innerHTML='<div class="session-list"><
|
|
2366
|
-
menu.querySelector('#newHistorySession')
|
|
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);
|
|
2367
2461
|
menu.querySelectorAll('[data-resume-session]').forEach(btn=>btn.addEventListener('click',()=>resumeChat(btn.dataset.resumeSession)));
|
|
2368
2462
|
if(historySessions.length){
|
|
2369
2463
|
document.getElementById('bridgeState').textContent='choose session';
|
|
@@ -2400,6 +2494,7 @@ async function startNewChat(){
|
|
|
2400
2494
|
document.getElementById('sendStatus').textContent='Starting new chat...';
|
|
2401
2495
|
try {
|
|
2402
2496
|
const history=await api('/api/chat/new',{method:'POST'});
|
|
2497
|
+
activeBackendSessionId=null;
|
|
2403
2498
|
setSessionMenuOpen(false);
|
|
2404
2499
|
renderHistorySnapshot(history);
|
|
2405
2500
|
} catch (err) {
|
|
@@ -2672,10 +2767,14 @@ function handleRelayEvent(type, event){
|
|
|
2672
2767
|
}
|
|
2673
2768
|
if (type === 'agent_history_loaded') {
|
|
2674
2769
|
document.getElementById('bridgeState').textContent = 'resumed';
|
|
2675
|
-
if(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
|
+
}
|
|
2676
2774
|
return;
|
|
2677
2775
|
}
|
|
2678
2776
|
if (type === 'agent_history_new') {
|
|
2777
|
+
activeBackendSessionId=null;
|
|
2679
2778
|
document.getElementById('bridgeState').textContent = 'new session';
|
|
2680
2779
|
setSessionButtonLabel('Chat: New');
|
|
2681
2780
|
return;
|
|
@@ -2703,7 +2802,10 @@ function handleRelayEvent(type, event){
|
|
|
2703
2802
|
return;
|
|
2704
2803
|
}
|
|
2705
2804
|
if (type === 'agent_session') {
|
|
2706
|
-
if(data.session_id)
|
|
2805
|
+
if(data.session_id){
|
|
2806
|
+
activeBackendSessionId=data.session_id;
|
|
2807
|
+
setSessionButtonLabel('Chat: '+compactId(data.session_id));
|
|
2808
|
+
}
|
|
2707
2809
|
return;
|
|
2708
2810
|
}
|
|
2709
2811
|
if (type === 'agent_turn_started') {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { backgroundTasks } from '../core/background-tasks.mjs';
|
|
2
|
+
import { dispatch } from './dispatch.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wake-on-finish: a background job that declared on_complete fires a
|
|
6
|
+
* TriggerEvent through dispatch() when it exits — so a finished build can
|
|
7
|
+
* deterministically wake a verifier agent. Chain depth and cycle guards
|
|
8
|
+
* apply like any other trigger; a job started BY that agent whose own
|
|
9
|
+
* on_complete points back would be refused by the chain guard.
|
|
10
|
+
*
|
|
11
|
+
* buildCtx is called lazily at fire time so the dispatch context always
|
|
12
|
+
* reflects the live tool executor / registry.
|
|
13
|
+
*/
|
|
14
|
+
export function registerJobCompletionDispatch(buildCtx) {
|
|
15
|
+
return backgroundTasks.onExit((job) => {
|
|
16
|
+
const target = job?.on_complete?.target;
|
|
17
|
+
if (!target) return;
|
|
18
|
+
const instruction = job.on_complete.instruction
|
|
19
|
+
|| `Background job ${job.id} (${job.name}) finished: ${job.status}`
|
|
20
|
+
+ (job.exit_code != null ? ` (exit ${job.exit_code})` : '')
|
|
21
|
+
+ `. Review the output and act on it:\n${String(job.tail || '').slice(-4000)}`;
|
|
22
|
+
Promise.resolve()
|
|
23
|
+
.then(() => dispatch({
|
|
24
|
+
type: 'invoke',
|
|
25
|
+
source: `job:${job.id}`,
|
|
26
|
+
target,
|
|
27
|
+
params: { instruction },
|
|
28
|
+
channel: null,
|
|
29
|
+
initiator: { chain: [`job:${job.id}`] },
|
|
30
|
+
}, buildCtx()))
|
|
31
|
+
.then((outcome) => {
|
|
32
|
+
if (outcome && !outcome.dispatched) {
|
|
33
|
+
process.stderr.write(` on_complete for ${job.id} not dispatched: ${outcome.reason}\n`);
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
.catch((err) => {
|
|
37
|
+
process.stderr.write(` on_complete for ${job.id} failed: ${err?.message || err}\n`);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { runGraph } from './runner.mjs';
|
|
2
|
+
import { compileSingleAgentGraph, normalizeGraphSpec } from './graph.mjs';
|
|
3
|
+
|
|
4
|
+
const MAX_CHAIN_DEPTH = 3;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The single funnel for deterministic runs. Every surface (CLI, /run,
|
|
8
|
+
* headless, hooks, cron, daemon, programmatic) builds a TriggerEvent and
|
|
9
|
+
* calls dispatch(); nothing else calls runGraph() directly.
|
|
10
|
+
*
|
|
11
|
+
* TriggerEvent: {
|
|
12
|
+
* type: 'invoke'|'manual'|'hook'|'prompt_match'|'cron'|'remote',
|
|
13
|
+
* source: string, // e.g. 'cli:agent-run', 'hook:Stop'
|
|
14
|
+
* target: {kind:'agent'|'workflow', slug} | string, // bare names resolved below
|
|
15
|
+
* params: { instruction, ... },
|
|
16
|
+
* channel: 'local'|'server'|null, // null → derived from the target
|
|
17
|
+
* initiator: { chain: string[] } | null,
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
export async function dispatch(event, ctx) {
|
|
21
|
+
const chain = event.initiator?.chain || [];
|
|
22
|
+
if (chain.length >= MAX_CHAIN_DEPTH) {
|
|
23
|
+
return { dispatched: false, reason: `Trigger chain depth limit (${MAX_CHAIN_DEPTH}) reached` };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const resolved = resolveTarget(event.target, ctx);
|
|
27
|
+
if (!resolved) {
|
|
28
|
+
return { dispatched: false, reason: `Unknown target '${targetName(event.target)}'` };
|
|
29
|
+
}
|
|
30
|
+
if (chain.includes(resolved.key)) {
|
|
31
|
+
return { dispatched: false, reason: `Trigger cycle detected at '${resolved.key}'` };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const channel = event.channel || resolved.channel;
|
|
35
|
+
if (channel === 'server') {
|
|
36
|
+
// Synced server workflows keep their exact existing execution path.
|
|
37
|
+
const result = await ctx.toolExecutor.execute('workflow_run_multi', {
|
|
38
|
+
name: resolved.slug,
|
|
39
|
+
instruction: event.params?.instruction || '',
|
|
40
|
+
});
|
|
41
|
+
return { dispatched: true, channel, result };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const graph = resolved.kind === 'agent'
|
|
45
|
+
? compileSingleAgentGraph(resolved.agent, { instruction: event.params?.instruction || '' })
|
|
46
|
+
: resolved.graph;
|
|
47
|
+
|
|
48
|
+
const runCtx = { ...ctx, resolveAgent: slug => resolveAgentBySlug(slug, ctx) };
|
|
49
|
+
const iterator = runGraph(graph, { instruction: event.params?.instruction || '', params: event.params || {} }, runCtx, {
|
|
50
|
+
substrate: event.substrate,
|
|
51
|
+
signal: event.signal,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let result = null;
|
|
55
|
+
while (true) {
|
|
56
|
+
const { value, done } = await iterator.next();
|
|
57
|
+
if (done) { result = value; break; }
|
|
58
|
+
ctx.renderEvent?.(value);
|
|
59
|
+
}
|
|
60
|
+
return { dispatched: true, channel: 'local', result };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function targetName(target) {
|
|
64
|
+
return typeof target === 'string' ? target : `${target?.kind}:${target?.slug}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveAgentBySlug(slug, ctx) {
|
|
68
|
+
return (ctx.listRunnables?.() || []).find(agent => agent.slug === slug) || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Bare-name resolution order (uniform across surfaces, preserves today's
|
|
73
|
+
* /run behavior): project agent → global agent → builtin → allowlisted
|
|
74
|
+
* plugin agent → local workflow → synced server workflow fallback.
|
|
75
|
+
* listRunnables() already returns agents deduped in that precedence.
|
|
76
|
+
*/
|
|
77
|
+
function resolveTarget(target, ctx) {
|
|
78
|
+
const raw = typeof target === 'string' ? String(target || '').trim() : '';
|
|
79
|
+
const prefixed = raw.match(/^(agent|workflow):(.+)$/);
|
|
80
|
+
const typed = typeof target === 'object' && target?.kind
|
|
81
|
+
? target
|
|
82
|
+
: prefixed
|
|
83
|
+
? { kind: prefixed[1], slug: prefixed[2].trim() }
|
|
84
|
+
: null;
|
|
85
|
+
const name = typed ? typed.slug : raw;
|
|
86
|
+
if (!name) return null;
|
|
87
|
+
|
|
88
|
+
if (typed?.kind === 'agent' && typed.agent) {
|
|
89
|
+
// Caller already resolved the full definition (prompt, readOnly, tools).
|
|
90
|
+
return { kind: 'agent', slug: name, key: `agent:${name}`, agent: typed.agent, channel: 'local' };
|
|
91
|
+
}
|
|
92
|
+
if (!typed || typed.kind === 'agent') {
|
|
93
|
+
const agent = resolveAgentBySlug(name, ctx);
|
|
94
|
+
if (agent) return { kind: 'agent', slug: name, key: `agent:${name}`, agent, channel: 'local' };
|
|
95
|
+
if (typed) return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const workflows = ctx.listLocalWorkflows?.() || [];
|
|
99
|
+
const workflow = workflows.find(w => w.slug === name || w.name === name);
|
|
100
|
+
if (workflow) {
|
|
101
|
+
// Channel comes from the file: v2 files run locally; v1 files (no
|
|
102
|
+
// apiVersion) keep today's server execution so nothing silently flips.
|
|
103
|
+
const isV2 = /\/2$/.test(String(workflow.api_version || workflow.apiVersion || ''));
|
|
104
|
+
return {
|
|
105
|
+
kind: 'workflow',
|
|
106
|
+
slug: workflow.slug || name,
|
|
107
|
+
key: `workflow:${name}`,
|
|
108
|
+
graph: isV2 ? normalizeGraphSpec(workflow) : null,
|
|
109
|
+
channel: isV2 ? 'local' : 'server',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!typed || typed.kind === 'workflow') {
|
|
114
|
+
// Name may exist only as a synced server workflow — let the server path try.
|
|
115
|
+
return { kind: 'workflow', slug: name, key: `workflow:${name}`, graph: null, channel: 'server' };
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag every event from a node's execution with graph/node attribution so
|
|
3
|
+
* renderers group them as sub-agent work and history builders exclude
|
|
4
|
+
* them from the primary transcript (same treatment direct agent runs get).
|
|
5
|
+
*/
|
|
6
|
+
export async function* namespaceNodeEvents(iterable, { node, runId }) {
|
|
7
|
+
const slug = node.agent_slug || node.id;
|
|
8
|
+
for await (const event of iterable) {
|
|
9
|
+
const data = event.data && typeof event.data === 'object' ? event.data : {};
|
|
10
|
+
yield {
|
|
11
|
+
...event,
|
|
12
|
+
data: { ...data, internal: true, sub_agent: slug, graph_run_id: runId, node_id: node.id },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function graphEvent(type, runId, data = {}) {
|
|
18
|
+
return { type, data: { graph_run_id: runId, ...data } };
|
|
19
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphSpec — the one shape every runnable compiles to.
|
|
3
|
+
*
|
|
4
|
+
* A deterministic sub-agent run is a one-node graph (trigger → agent →
|
|
5
|
+
* output); a multi-agent workflow is the same graph with N agent nodes.
|
|
6
|
+
* Inputs accepted: an inline GraphSpec, a multi-workflow loader payload
|
|
7
|
+
* ({ graph: { nodes, edges } }), or a single agent definition via
|
|
8
|
+
* compileSingleAgentGraph().
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function compileSingleAgentGraph(agent, { instruction = '' } = {}) {
|
|
12
|
+
const slug = agent.slug || agent.name;
|
|
13
|
+
return {
|
|
14
|
+
name: slug,
|
|
15
|
+
channel: 'local',
|
|
16
|
+
pattern: 'sequential',
|
|
17
|
+
nodes: [
|
|
18
|
+
{ id: 'trigger', type: 'trigger' },
|
|
19
|
+
{ id: slug, type: 'agent', agent_slug: slug, agent, prompt: instruction },
|
|
20
|
+
{ id: 'output', type: 'output' },
|
|
21
|
+
],
|
|
22
|
+
edges: [
|
|
23
|
+
{ source: 'trigger', target: slug },
|
|
24
|
+
{ source: slug, target: 'output' },
|
|
25
|
+
],
|
|
26
|
+
global_params: {},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeGraphSpec(input) {
|
|
31
|
+
if (!input || typeof input !== 'object') throw new Error('Graph spec is required');
|
|
32
|
+
// Multi-workflow loader payload: { name, graph: { nodes, edges }, ... }
|
|
33
|
+
const source = input.graph && Array.isArray(input.graph.nodes) ? input.graph : input;
|
|
34
|
+
const nodes = (source.nodes || []).map(node => ({
|
|
35
|
+
id: node.id,
|
|
36
|
+
type: node.type,
|
|
37
|
+
agent_slug: node.agent_slug || node.data?.agent_slug || node.data?.user_agent_slug || null,
|
|
38
|
+
agent: node.agent || null,
|
|
39
|
+
prompt: node.prompt || node.data?.prompt || '',
|
|
40
|
+
model: normalizeModel(node.model ?? node.data?.model),
|
|
41
|
+
tools: Array.isArray(node.tools) ? node.tools : (Array.isArray(node.data?.tools) ? node.data.tools : []),
|
|
42
|
+
config: node.config || node.data?.config || {},
|
|
43
|
+
continue_on_error: Boolean(node.continue_on_error),
|
|
44
|
+
// job nodes: a process instead of an LLM
|
|
45
|
+
command: node.command || node.data?.command || null,
|
|
46
|
+
timeout_s: Number(node.timeout_s ?? node.data?.timeout_s) || null,
|
|
47
|
+
// service nodes: long-lived process (dev server); the node completes
|
|
48
|
+
// at READINESS, the process lives until the graph run ends.
|
|
49
|
+
ready: node.ready || node.data?.ready || null,
|
|
50
|
+
}));
|
|
51
|
+
const edges = (source.edges || []).map(edge => ({ source: edge.source, target: edge.target }));
|
|
52
|
+
return {
|
|
53
|
+
name: input.name || source.name || 'graph',
|
|
54
|
+
channel: input.channel || 'local',
|
|
55
|
+
pattern: input.pattern || input.orchestration_pattern || 'sequential',
|
|
56
|
+
nodes,
|
|
57
|
+
edges,
|
|
58
|
+
global_params: input.global_params || {},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeModel(value) {
|
|
63
|
+
const model = String(value || '').trim();
|
|
64
|
+
return !model || model === 'auto' ? null : model;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate the graph and return executable nodes (type 'agent' or 'job')
|
|
69
|
+
* in execution (topological) order. Throws before any LLM call on:
|
|
70
|
+
* unknown edge endpoints, cycles, unreachable nodes, agent nodes with no
|
|
71
|
+
* definition reference, or job nodes with no command.
|
|
72
|
+
*/
|
|
73
|
+
export function validateGraph(spec) {
|
|
74
|
+
const byId = new Map(spec.nodes.map(node => [node.id, node]));
|
|
75
|
+
for (const edge of spec.edges) {
|
|
76
|
+
if (!byId.has(edge.source)) throw new Error(`Edge references unknown node '${edge.source}'`);
|
|
77
|
+
if (!byId.has(edge.target)) throw new Error(`Edge references unknown node '${edge.target}'`);
|
|
78
|
+
}
|
|
79
|
+
const outgoing = new Map(spec.nodes.map(node => [node.id, []]));
|
|
80
|
+
const indegree = new Map(spec.nodes.map(node => [node.id, 0]));
|
|
81
|
+
for (const edge of spec.edges) {
|
|
82
|
+
outgoing.get(edge.source).push(edge.target);
|
|
83
|
+
indegree.set(edge.target, indegree.get(edge.target) + 1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Kahn topological sort — leftover nodes mean a cycle.
|
|
87
|
+
const queue = spec.nodes.filter(node => indegree.get(node.id) === 0).map(node => node.id);
|
|
88
|
+
const order = [];
|
|
89
|
+
while (queue.length) {
|
|
90
|
+
const id = queue.shift();
|
|
91
|
+
order.push(id);
|
|
92
|
+
for (const next of outgoing.get(id)) {
|
|
93
|
+
indegree.set(next, indegree.get(next) - 1);
|
|
94
|
+
if (indegree.get(next) === 0) queue.push(next);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (order.length !== spec.nodes.length) {
|
|
98
|
+
throw new Error(`Graph '${spec.name}' contains a cycle`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const reachable = new Set();
|
|
102
|
+
const stack = spec.nodes.filter(node => node.type === 'trigger').map(node => node.id);
|
|
103
|
+
while (stack.length) {
|
|
104
|
+
const id = stack.pop();
|
|
105
|
+
if (reachable.has(id)) continue;
|
|
106
|
+
reachable.add(id);
|
|
107
|
+
stack.push(...outgoing.get(id));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const executableNodes = order
|
|
111
|
+
.map(id => byId.get(id))
|
|
112
|
+
.filter(node => node.type === 'agent' || node.type === 'job' || node.type === 'service');
|
|
113
|
+
if (!executableNodes.length) throw new Error(`Graph '${spec.name}' has no agent, job, or service nodes`);
|
|
114
|
+
for (const node of executableNodes) {
|
|
115
|
+
if (!reachable.has(node.id)) {
|
|
116
|
+
throw new Error(`Node '${node.id}' is not reachable from the trigger`);
|
|
117
|
+
}
|
|
118
|
+
if (node.type === 'agent' && !node.agent && !node.agent_slug) {
|
|
119
|
+
throw new Error(`Agent node '${node.id}' has neither an inline agent nor an agent_slug`);
|
|
120
|
+
}
|
|
121
|
+
if ((node.type === 'job' || node.type === 'service') && !String(node.command || '').trim()) {
|
|
122
|
+
throw new Error(`${node.type === 'job' ? 'Job' : 'Service'} node '${node.id}' has no command`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return executableNodes;
|
|
126
|
+
}
|