@worca/app 1.0.0 → 1.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -9
- package/agents/clarify.meta.json +4 -4
- package/agents/decomposer.meta.json +5 -5
- package/agents/implementer.meta.json +15 -5
- package/agents/manualTestsChecklist.meta.json +5 -4
- package/agents/manualWebUiTesting.meta.json +9 -4
- package/agents/planReviewer.meta.json +12 -4
- package/agents/planner.meta.json +12 -5
- package/agents/refiner.meta.json +15 -4
- package/agents/reviewer.meta.json +14 -4
- package/agents/worca-cc-clarify.md +7 -0
- package/agents/worca-cc-code-reviewer.md +11 -6
- package/agents/worca-cc-decomposer.md +7 -0
- package/agents/worca-cc-implementer.md +9 -0
- package/agents/worca-cc-manual-tests-checklist.md +8 -5
- package/agents/worca-cc-manual-web-ui-testing.md +10 -6
- package/agents/worca-cc-plan-refiner.md +11 -6
- package/agents/worca-cc-plan-reviewer.md +10 -7
- package/agents/worca-cc-planner.md +9 -0
- package/agents/worca-cc-workspace-reviewer.md +11 -4
- package/agents/worca-cc-workspace-scanner.md +8 -4
- package/agents/workspaceReviewer.meta.json +15 -4
- package/agents/workspaceScanner.meta.json +5 -4
- package/package.json +8 -2
- package/skills/worca/SKILL.md +5 -5
- package/src/cli/render.mjs +148 -0
- package/src/cli/worca-cc.mjs +386 -56
- package/src/core/agent-gen.mjs +69 -31
- package/src/core/agent-registry.mjs +124 -144
- package/src/core/agent-store.mjs +164 -4
- package/src/core/artifacts.mjs +199 -23
- package/src/core/ask/attachment-kind.mjs +95 -0
- package/src/core/ask/catalog.mjs +111 -0
- package/src/core/ask/comment-deps.mjs +55 -0
- package/src/core/ask/events.mjs +545 -0
- package/src/core/ask/follow.mjs +113 -0
- package/src/core/ask/git-allowlist.mjs +226 -0
- package/src/core/ask/limits.mjs +57 -0
- package/src/core/ask/mcp-stdio.mjs +135 -0
- package/src/core/ask/models.mjs +125 -0
- package/src/core/ask/prompt.mjs +286 -0
- package/src/core/ask/proposal.mjs +170 -0
- package/src/core/ask/redact.mjs +30 -0
- package/src/core/ask/spawn.mjs +156 -0
- package/src/core/ask/store.mjs +438 -0
- package/src/core/ask/tool-deps.mjs +87 -0
- package/src/core/ask/tools.mjs +879 -0
- package/src/core/ask/turn.mjs +462 -0
- package/src/core/ask/worktree-deps.mjs +27 -0
- package/src/core/ask/worktrees.mjs +285 -0
- package/src/core/chat/command-router.mjs +28 -7
- package/src/core/chat/notifier.mjs +6 -1
- package/src/core/chat/renderers.mjs +15 -8
- package/src/core/claude-runner.mjs +541 -62
- package/src/core/config.mjs +310 -44
- package/src/core/cost-budget.mjs +29 -2
- package/src/core/db.mjs +773 -53
- package/src/core/diff-anchor.mjs +213 -0
- package/src/core/diff-comments.mjs +273 -0
- package/src/core/engine-select.mjs +32 -0
- package/src/core/failure-policy.mjs +201 -0
- package/src/core/git-info.mjs +49 -10
- package/src/core/graph/builtin-workflows.mjs +51 -0
- package/src/core/graph/executor.mjs +894 -0
- package/src/core/graph/registry-ports.mjs +12 -0
- package/src/core/graph/scheduler.mjs +1072 -0
- package/src/core/graph/seed-templates.mjs +318 -0
- package/src/core/host-guard.mjs +271 -0
- package/src/core/model-env.mjs +180 -8
- package/src/core/model-test.mjs +79 -0
- package/src/core/orchestrator.mjs +994 -4097
- package/src/core/overview-agent.mjs +15 -3
- package/src/core/phases.mjs +208 -537
- package/src/core/pipeline-delete.mjs +13 -2
- package/src/core/plugin-api.mjs +8 -3
- package/src/core/plugin-config.mjs +178 -28
- package/src/core/plugin-inventory.mjs +6 -2
- package/src/core/plugin-manifest.mjs +199 -11
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-repo.mjs +16 -4
- package/src/core/plugin-shim-child.mjs +9 -3
- package/src/core/plugin-shim.mjs +80 -17
- package/src/core/plugin-store.mjs +236 -29
- package/src/core/plugin-workflows.mjs +90 -41
- package/src/core/preflight.mjs +135 -3
- package/src/core/projects.mjs +7 -5
- package/src/core/protocol.mjs +8 -35
- package/src/core/recoverable-error.mjs +1 -1
- package/src/core/run-harness.mjs +3934 -0
- package/src/core/run-manifest.mjs +5 -1
- package/src/core/settings.mjs +184 -13
- package/src/core/skills.mjs +10 -3
- package/src/core/source-bindings.mjs +175 -0
- package/src/core/sources.mjs +87 -25
- package/src/core/stats.mjs +25 -6
- package/src/core/title.mjs +51 -4
- package/src/core/workflows.mjs +358 -259
- package/src/core/workspace-scan.mjs +4 -0
- package/src/core/worktree.mjs +98 -7
- package/src/shared/graph/agent-meta.mjs +278 -0
- package/src/shared/graph/constants.mjs +105 -0
- package/src/shared/graph/geometry.mjs +157 -0
- package/src/shared/graph/layout.mjs +134 -0
- package/src/shared/graph/loops.mjs +130 -0
- package/src/shared/graph/manifest.mjs +257 -0
- package/src/shared/graph/ports.mjs +153 -0
- package/src/shared/graph/route.mjs +397 -0
- package/src/shared/graph/template.mjs +165 -0
- package/src/shared/graph/thumbnail.mjs +67 -0
- package/src/shared/graph/validate.mjs +491 -0
- package/src/shared/graph/verdict.mjs +41 -0
- package/ui/public/app.js +4240 -1682
- package/ui/public/ask-markdown.mjs +145 -0
- package/ui/public/ask-model.mjs +317 -0
- package/ui/public/ask-panel.mjs +2129 -0
- package/ui/public/chat-settings-view.mjs +6 -2
- package/ui/public/diff-view.mjs +66 -11
- package/ui/public/file-tree.mjs +305 -0
- package/ui/public/graph/composer.mjs +889 -0
- package/ui/public/graph/inspector.mjs +183 -0
- package/ui/public/graph/model.mjs +37 -0
- package/ui/public/graph/palette.mjs +144 -0
- package/ui/public/graph/run-decor.mjs +410 -0
- package/ui/public/graph/run-hosts.mjs +201 -0
- package/ui/public/graph/save-dialog.mjs +56 -0
- package/ui/public/graph/view.mjs +858 -0
- package/ui/public/guardrails-view.mjs +4 -2
- package/ui/public/hljs-loader.mjs +180 -0
- package/ui/public/index.html +311 -265
- package/ui/public/log-filter.mjs +22 -4
- package/ui/public/log-line.mjs +45 -19
- package/ui/public/models-view.mjs +171 -9
- package/ui/public/plugins-view.mjs +106 -4
- package/ui/public/source-pane.mjs +190 -8
- package/ui/public/stats-view.mjs +81 -1
- package/ui/public/style.css +1487 -229
- package/ui/public/syntax-highlight.mjs +270 -0
- package/ui/public/thinking-orb.mjs +110 -0
- package/ui/server.mjs +1894 -104
- package/src/core/channels.mjs +0 -302
- package/src/core/runners.mjs +0 -167
- package/src/core/workflow-validator.mjs +0 -185
- package/ui/public/composer-core.mjs +0 -211
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// ui/public/graph/inspector.mjs
|
|
2
|
+
// Pure DOM renderers for the composer's floating rail. Every function takes the
|
|
3
|
+
// target `document` via opts and returns a DETACHED element — no fetch, no
|
|
4
|
+
// listeners. composer.mjs mounts the result and binds ONE delegated `change`
|
|
5
|
+
// listener, routing on `data-field`. Capability rows are gated by META
|
|
6
|
+
// BOOLEANS: a new agent's sidecar drives its panel with no UI change.
|
|
7
|
+
import { resolveOrOutType } from '../../../src/shared/graph/ports.mjs';
|
|
8
|
+
|
|
9
|
+
const ARITY_KINDS = new Set(['and', 'or', 'combine']);
|
|
10
|
+
const FLOW_TITLES = { task: 'Task', end: 'End', and: 'AND', or: 'OR', combine: 'Combine' };
|
|
11
|
+
const FLOW_BLURB = {
|
|
12
|
+
task: 'The pipeline entry: the prompt and its attached files.',
|
|
13
|
+
end: 'The pipeline sink. A token arriving here completes the run.',
|
|
14
|
+
and: 'Fires when ALL of its inputs are fresh. Payloads are discarded — pure sequencing.',
|
|
15
|
+
or: 'Fires on ANY fresh input and forwards the freshest payload.',
|
|
16
|
+
combine: 'Joins its md inputs into one document, in port order.',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const h = (doc, tag, cls, text) => { const n = doc.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
|
|
20
|
+
const field = (doc, cls, label) => { const w = h(doc, 'div', `ins-f ${cls}`); w.appendChild(h(doc, 'label', 'ins-label', label)); return w; };
|
|
21
|
+
|
|
22
|
+
function select(doc, cls, name, label, items, value, { disabled = false, title = '' } = {}) {
|
|
23
|
+
const wrap = field(doc, cls, label);
|
|
24
|
+
if (title) wrap.title = title;
|
|
25
|
+
const sel = h(doc, 'select', 'ins-select');
|
|
26
|
+
sel.dataset.field = name;
|
|
27
|
+
sel.disabled = Boolean(disabled);
|
|
28
|
+
for (const opt of items) {
|
|
29
|
+
const o = doc.createElement('option');
|
|
30
|
+
o.value = opt.value; o.textContent = opt.text;
|
|
31
|
+
if (opt.value === (value == null ? '' : String(value))) o.selected = true;
|
|
32
|
+
sel.appendChild(o);
|
|
33
|
+
}
|
|
34
|
+
wrap.appendChild(sel);
|
|
35
|
+
return wrap;
|
|
36
|
+
}
|
|
37
|
+
function toggle(doc, cls, name, label, hint, { checked = false, disabled = false, title = '' } = {}) {
|
|
38
|
+
const row = h(doc, 'div', `ins-tog ${cls}`);
|
|
39
|
+
if (title) row.title = title;
|
|
40
|
+
const box = doc.createElement('input');
|
|
41
|
+
box.type = 'checkbox'; box.dataset.field = name; box.checked = Boolean(checked); box.disabled = Boolean(disabled);
|
|
42
|
+
const body = h(doc, 'span', 'ins-tog-b');
|
|
43
|
+
body.appendChild(h(doc, 'span', 'ins-tog-t', label));
|
|
44
|
+
if (hint) body.appendChild(h(doc, 'small', 'ins-tog-h', hint));
|
|
45
|
+
row.append(box, body);
|
|
46
|
+
return row;
|
|
47
|
+
}
|
|
48
|
+
function head(doc, title, sub) {
|
|
49
|
+
const w = h(doc, 'div', 'ins-head');
|
|
50
|
+
w.appendChild(h(doc, 'div', 'ins-name', title));
|
|
51
|
+
if (sub) w.appendChild(h(doc, 'div', 'ins-sub', sub));
|
|
52
|
+
return w;
|
|
53
|
+
}
|
|
54
|
+
function number(doc, cls, name, label, value, min) {
|
|
55
|
+
const wrap = field(doc, cls, label);
|
|
56
|
+
const input = doc.createElement('input');
|
|
57
|
+
input.type = 'number'; input.className = 'ins-number'; input.dataset.field = name;
|
|
58
|
+
input.min = String(min); input.step = '1'; input.value = String(value);
|
|
59
|
+
wrap.appendChild(input);
|
|
60
|
+
return wrap;
|
|
61
|
+
}
|
|
62
|
+
/** Read-only listing of a node's resolved ports. */
|
|
63
|
+
function portList(doc, ports) {
|
|
64
|
+
const wrap = h(doc, 'div', 'ins-ports');
|
|
65
|
+
const zone = (label, list, dir) => {
|
|
66
|
+
if (!list.length) return;
|
|
67
|
+
wrap.appendChild(h(doc, 'div', 'ins-zone', label));
|
|
68
|
+
const ul = h(doc, 'div', 'ins-plist');
|
|
69
|
+
for (const p of list) {
|
|
70
|
+
const item = h(doc, 'div', `ins-pitem${p.synthetic ? ' gate' : ''}`);
|
|
71
|
+
item.appendChild(h(doc, 'i', p.synthetic ? 'gdot' : `dot ${p.type}`));
|
|
72
|
+
item.appendChild(h(doc, 'span', 'pn', p.id));
|
|
73
|
+
const bits = [p.type];
|
|
74
|
+
if (p.synthetic) bits.push('engine');
|
|
75
|
+
else if (dir === 'in') bits.push(p.loop ? 'loop' : (p.required === false ? 'optional' : 'required'));
|
|
76
|
+
else if (p.when && p.when !== 'always') bits.push(`on ${p.when}`);
|
|
77
|
+
if (p.expands) bits.push('fan-out');
|
|
78
|
+
item.appendChild(h(doc, 'span', 'pt mla', bits.join(' · ')));
|
|
79
|
+
ul.appendChild(item);
|
|
80
|
+
}
|
|
81
|
+
wrap.appendChild(ul);
|
|
82
|
+
};
|
|
83
|
+
zone('Inputs', ports.inputs, 'in');
|
|
84
|
+
zone('Outputs', ports.outputs, 'out');
|
|
85
|
+
return wrap;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function renderNodeInspector(node, { template, portsFn, meta = null, models = [], efforts = [], subagentModels = [], doc = globalThis.document } = {}) {
|
|
89
|
+
const ports = portsFn(node) || { inputs: [], outputs: [] };
|
|
90
|
+
const root = h(doc, 'div', `ins-panel ins-${node.kind === 'agent' ? 'agent' : `flow ins-${node.kind}`}`);
|
|
91
|
+
root.dataset.nodeId = node.id;
|
|
92
|
+
const body = h(doc, 'div', 'ins-body-in');
|
|
93
|
+
|
|
94
|
+
if (node.kind === 'agent') {
|
|
95
|
+
root.appendChild(head(doc, (meta && meta.displayName) || node.key || node.id, `${node.key} · ${node.id}`));
|
|
96
|
+
body.appendChild(select(doc, 'ins-model', 'model', 'Model',
|
|
97
|
+
[{ value: '', text: 'inherit' }, ...models.map((m) => ({ value: m.id, text: m.label || m.id }))], node.config.model));
|
|
98
|
+
body.appendChild(select(doc, 'ins-effort', 'effort', 'Effort',
|
|
99
|
+
[{ value: '', text: 'default' }, ...efforts.map((e) => ({ value: e, text: e }))], node.config.effort));
|
|
100
|
+
if (meta && meta.fanOut) {
|
|
101
|
+
body.appendChild(toggle(doc, 'ins-fanout', 'fanOut', 'Research fan-out', 'parallel research sub-agents',
|
|
102
|
+
{ checked: node.config.fanOut === true }));
|
|
103
|
+
// What this node's sub-agents run on. Gated by the SAME meta flag as the
|
|
104
|
+
// toggle above: an agent that cannot fan out has no children to place.
|
|
105
|
+
// '' = unset — the run resolves the auto default (a per-spawn choice
|
|
106
|
+
// rubric in the agent's prompt), so the blank option says so; 'inherit'
|
|
107
|
+
// is the stored opt-out (children ride the CLI's own resolution).
|
|
108
|
+
// A ROUTED model (custom-endpoint catalog entry) locks the control: the
|
|
109
|
+
// run degrades every stored value to the same-endpoint policy, so
|
|
110
|
+
// offering aliases here would be a lie. The stored value is kept — the
|
|
111
|
+
// per-field change handler writes only the field the user edited, and
|
|
112
|
+
// the locked option carries the stored value — and the full control
|
|
113
|
+
// returns when the node moves back to a plain model.
|
|
114
|
+
const chosen = node.config.model ? models.find((m) => m && m.id === node.config.model) : null;
|
|
115
|
+
body.appendChild(chosen && chosen.routed
|
|
116
|
+
? select(doc, 'ins-subagent', 'subagentModel', 'Sub-agent model',
|
|
117
|
+
[{ value: node.config.subagentModel || '', text: 'same endpoint (locked)' }],
|
|
118
|
+
node.config.subagentModel || '',
|
|
119
|
+
{ disabled: true, title: 'This model routes to a custom endpoint; its sub-agents run the same model. Pick a non-routed model to edit.' })
|
|
120
|
+
: select(doc, 'ins-subagent', 'subagentModel', 'Sub-agent model',
|
|
121
|
+
[{ value: '', text: 'default (agent picks)' },
|
|
122
|
+
...subagentModels.map((m) => ({ value: m, text: m === 'auto' ? 'agent picks' : m }))],
|
|
123
|
+
node.config.subagentModel));
|
|
124
|
+
}
|
|
125
|
+
if (meta && meta.asksQuestions) {
|
|
126
|
+
const locked = Boolean(meta.questionsLocked);
|
|
127
|
+
const saved = node.config.askQuestions;
|
|
128
|
+
body.appendChild(toggle(doc, 'ins-questions', 'askQuestions', 'Ask questions', 'pauses the run for input', {
|
|
129
|
+
checked: locked ? Boolean(meta.questionsDefault) : (typeof saved === 'boolean' ? saved : Boolean(meta.questionsDefault)),
|
|
130
|
+
disabled: locked,
|
|
131
|
+
title: locked ? (meta.questionsDefault ? 'Always on for this agent' : 'Always off for this agent') : '',
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
body.appendChild(toggle(doc, 'ins-awaitall', 'awaitAll', 'Await all inputs', 'gate until every wire fires',
|
|
135
|
+
{ checked: node.config.awaitAll === true }));
|
|
136
|
+
} else {
|
|
137
|
+
root.appendChild(head(doc, FLOW_TITLES[node.kind] || node.kind, node.id));
|
|
138
|
+
body.appendChild(h(doc, 'p', 'ins-blurb', FLOW_BLURB[node.kind] || ''));
|
|
139
|
+
if (ARITY_KINDS.has(node.kind)) {
|
|
140
|
+
// "Input count", not "Inputs": the read-only port listing below already
|
|
141
|
+
// carries an `Inputs` heading, and two of them in one 308px rail read as
|
|
142
|
+
// one control and its own list.
|
|
143
|
+
body.appendChild(number(doc, 'ins-arity', 'arity', 'Input count', Number.isInteger(node.config.arity) ? node.config.arity : 2, 2));
|
|
144
|
+
}
|
|
145
|
+
if (node.kind === 'or') {
|
|
146
|
+
const resolved = resolveOrOutType(template, portsFn, node.id, new Set());
|
|
147
|
+
body.appendChild(h(doc, 'div', 'ins-resolved', resolved ? `forwards: ${resolved}` : 'unresolved'));
|
|
148
|
+
}
|
|
149
|
+
if (node.kind === 'task') {
|
|
150
|
+
body.appendChild(toggle(doc, 'ins-seed', 'planStoreSeed', 'Seed the plan store', 'treat an attached plan as the run’s plan',
|
|
151
|
+
{ checked: node.config.planStoreSeed === true }));
|
|
152
|
+
}
|
|
153
|
+
if (node.kind === 'end') body.appendChild(h(doc, 'div', 'ins-result', ''));
|
|
154
|
+
}
|
|
155
|
+
body.appendChild(h(doc, 'div', 'ins-sep'));
|
|
156
|
+
body.appendChild(portList(doc, ports));
|
|
157
|
+
root.appendChild(body);
|
|
158
|
+
return root;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Loop wires carry the per-wire cycle budget; a plain wire must NOT expose one
|
|
162
|
+
* (maxCycles on a non-loop wire is V13's error). */
|
|
163
|
+
export function renderWireInspector(wire, { loop = false, doc = globalThis.document } = {}) {
|
|
164
|
+
const root = h(doc, 'div', `ins-panel ins-wire${loop ? ' ins-loop' : ''}`);
|
|
165
|
+
root.dataset.wireId = wire.id;
|
|
166
|
+
root.appendChild(head(doc, loop ? 'Loop wire' : 'Wire', `${wire.from.node}.${wire.from.port} → ${wire.to.node}.${wire.to.port}`));
|
|
167
|
+
const body = h(doc, 'div', 'ins-body-in');
|
|
168
|
+
if (loop) {
|
|
169
|
+
body.appendChild(number(doc, 'ins-maxcycles', 'maxCycles', 'Max cycles',
|
|
170
|
+
wire.config && Number.isInteger(wire.config.maxCycles) ? wire.config.maxCycles : 3, 1));
|
|
171
|
+
body.appendChild(h(doc, 'small', 'ins-hint', 'How many times this loop may re-deliver before the gate asks.'));
|
|
172
|
+
} else {
|
|
173
|
+
body.appendChild(h(doc, 'p', 'ins-blurb', 'Plain data wire. Delete it to rewire the target input.'));
|
|
174
|
+
}
|
|
175
|
+
root.appendChild(body);
|
|
176
|
+
return root;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function renderEmptyInspector({ doc = globalThis.document } = {}) {
|
|
180
|
+
const root = h(doc, 'div', 'ins-panel ins-empty');
|
|
181
|
+
root.appendChild(h(doc, 'p', 'ins-blurb', 'Select a node or a wire to configure it.'));
|
|
182
|
+
return root;
|
|
183
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// ui/public/graph/model.mjs (depth 3)
|
|
2
|
+
// The browser's door to the ONE graph model. ui/public cannot import src/core,
|
|
3
|
+
// but src/shared/** is pure ESM served at /src/shared, so these specifiers
|
|
4
|
+
// resolve to the SAME module instance in Node, jsdom and Chrome alike: three
|
|
5
|
+
// `..` because this file sits three levels below the repo root. Absolute
|
|
6
|
+
// specifiers ('/src/shared/...') are FORBIDDEN — they break the Node ESM
|
|
7
|
+
// resolver the UI tests use. NOTHING may be redefined here: this file is
|
|
8
|
+
// re-exports only, and test/shared-graph-single-source.test.mjs asserts the
|
|
9
|
+
// function IDENTITY against the shared modules.
|
|
10
|
+
export {
|
|
11
|
+
flowPorts, portsFnFor, portsOf, findPort, typeCompatible, resolveOrOutType,
|
|
12
|
+
inboundWires, outboundWires, firedOutputs,
|
|
13
|
+
} from '../../../src/shared/graph/ports.mjs';
|
|
14
|
+
export { tarjanSccs, classifyLoops } from '../../../src/shared/graph/loops.mjs';
|
|
15
|
+
export { RULES, validateGraph, formatIssue } from '../../../src/shared/graph/validate.mjs';
|
|
16
|
+
export {
|
|
17
|
+
normalizeTemplate, serializeTemplate, newNode, newWire, mintId, canWire,
|
|
18
|
+
removeNode, removeWire, nodeById, wireById,
|
|
19
|
+
} from '../../../src/shared/graph/template.mjs';
|
|
20
|
+
export {
|
|
21
|
+
NODE_W, HEAD_H, ROW_H, SEP_H, PAD_T, PAD_B, BORDER, DOT, FOOT_H, EXEC_ROW_H, SNAP,
|
|
22
|
+
PORT_HIT_R, WIRE_HIT_TOL, ZOOM_MIN, ZOOM_MAX, ZOOM_K, ROW0, GEOMETRY_CSS_VARS,
|
|
23
|
+
injectGeometry, nodeSize, portAnchor, snap,
|
|
24
|
+
hitNode, hitPort, graphBounds, fitBounds,
|
|
25
|
+
} from '../../../src/shared/graph/geometry.mjs';
|
|
26
|
+
export {
|
|
27
|
+
routeWire, routeAll, separateRoutes, routePathD, routeMid, hitRoute,
|
|
28
|
+
ROUTE_CLEARANCE, ROUTE_STUB, ROUTE_RADIUS, ROUTE_LANE_GAP,
|
|
29
|
+
} from '../../../src/shared/graph/route.mjs'; // depth 3: ui/public/graph -> repo root
|
|
30
|
+
export { rankNodes, autoLayout } from '../../../src/shared/graph/layout.mjs';
|
|
31
|
+
export { thumbnailSvg } from '../../../src/shared/graph/thumbnail.mjs';
|
|
32
|
+
export { normalizeAgentMeta, validateMetaV2, indexByKey, derivePortSummary } from '../../../src/shared/graph/agent-meta.mjs';
|
|
33
|
+
export { buildGraphManifest, manifestPortsFn, manifestTemplate, sanitizeIcon, UI_PHASE } from '../../../src/shared/graph/manifest.mjs';
|
|
34
|
+
export {
|
|
35
|
+
TEMPLATE_VERSION, KINDS, FLOW_KINDS, PORT_TYPES, AWAIT_PORT, TASK_PORTS, END_PORTS,
|
|
36
|
+
gatePorts, DEFAULT_MAX_CYCLES, MAX_PORTS_PER_SIDE, LIMITS, NODE_ID_RE, WIRE_ID_RE, PORT_ID_RE,
|
|
37
|
+
} from '../../../src/shared/graph/constants.mjs';
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// ui/public/graph/palette.mjs
|
|
2
|
+
// The rail's Agents tab: one DISCLOSURE per domain (plus the PINNED Flow group),
|
|
3
|
+
// a filter, and agent pills stacked one per row. Pure DOM + one delegated
|
|
4
|
+
// controller; it never touches the template — it calls back into the composer,
|
|
5
|
+
// which owns every mutation.
|
|
6
|
+
export const FLOW_GROUP = 'flow';
|
|
7
|
+
/** Flow pills advertise their ports the way agent pills do (pill line 2). */
|
|
8
|
+
export const FLOW_PORT_LINE = {
|
|
9
|
+
task: 'source · out task',
|
|
10
|
+
end: 'in result · terminal',
|
|
11
|
+
and: 'in in1..inN · out out',
|
|
12
|
+
or: 'in in1..inN · out out',
|
|
13
|
+
combine: 'in in1, in2 · out out',
|
|
14
|
+
};
|
|
15
|
+
export const FLOW_PILLS = Object.freeze([
|
|
16
|
+
Object.freeze({ kind: 'task', displayName: 'Task', description: 'The pipeline entry: the prompt and its attached files.' }),
|
|
17
|
+
Object.freeze({ kind: 'end', displayName: 'End', description: 'The pipeline sink. A token arriving here completes the run.' }),
|
|
18
|
+
Object.freeze({ kind: 'and', displayName: 'AND', description: 'Fires when ALL of its inputs are fresh. Payloads discarded — pure sequencing.' }),
|
|
19
|
+
Object.freeze({ kind: 'or', displayName: 'OR', description: 'Fires on ANY fresh input and forwards the freshest payload.' }),
|
|
20
|
+
Object.freeze({ kind: 'combine', displayName: 'Combine', description: 'Joins its md inputs into one document, in port order.' }),
|
|
21
|
+
]);
|
|
22
|
+
const SINGLETON_KINDS = new Set(['task', 'end']);
|
|
23
|
+
|
|
24
|
+
/** Pill line 2: META port ids only. The synthesized `await` gate is never listed —
|
|
25
|
+
* it is not part of the agent's declared contract and every agent has one. */
|
|
26
|
+
export function portLineOf(entry) {
|
|
27
|
+
const ids = (ports) => (Array.isArray(ports) ? ports : []).filter((p) => p && !p.synthetic).map((p) => p.id);
|
|
28
|
+
const reads = ids(entry && entry.inputs);
|
|
29
|
+
const writes = ids(entry && entry.outputs);
|
|
30
|
+
return [reads.length ? `in ${reads.join(', ')}` : '', writes.length ? `out ${writes.join(', ')}` : ''].filter(Boolean).join(' · ');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Ordered groups: every non-shared, non-general domain in first-seen order,
|
|
34
|
+
* then `general`, then the pinned Flow group. `shared` agents are folded into
|
|
35
|
+
* every domain group; `placeable:false` agents are dropped everywhere (that is
|
|
36
|
+
* how workspaceScanner never reaches a canvas). Empty groups are omitted. */
|
|
37
|
+
export function paletteEntries(agents, { placedKinds = [] } = {}) {
|
|
38
|
+
const list = (Array.isArray(agents) ? agents : []).filter((a) => a && a.placeable !== false)
|
|
39
|
+
.map((a) => ({ ...a, order: typeof a.order === 'number' ? a.order : 99, domain: a.domain || 'general' }));
|
|
40
|
+
const shared = list.filter((a) => a.domain === 'shared');
|
|
41
|
+
const byOrder = (x, y) => x.order - y.order || String(x.key).localeCompare(String(y.key));
|
|
42
|
+
const domains = [];
|
|
43
|
+
for (const a of list) if (a.domain !== 'shared' && a.domain !== 'general' && !domains.includes(a.domain)) domains.push(a.domain);
|
|
44
|
+
domains.push('general');
|
|
45
|
+
const groups = domains
|
|
46
|
+
.map((domain) => ({ domain, flow: false, agents: [...shared, ...list.filter((a) => a.domain === domain)].sort(byOrder) }))
|
|
47
|
+
.filter((g) => g.agents.length);
|
|
48
|
+
const placed = new Set(placedKinds);
|
|
49
|
+
groups.push({
|
|
50
|
+
domain: FLOW_GROUP, flow: true,
|
|
51
|
+
agents: FLOW_PILLS.map((p) => ({ ...p, portLine: FLOW_PORT_LINE[p.kind] || '', disabled: SINGLETON_KINDS.has(p.kind) && placed.has(p.kind) })),
|
|
52
|
+
});
|
|
53
|
+
return groups;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function h(doc, tag, cls, text) {
|
|
57
|
+
const n = doc.createElement(tag);
|
|
58
|
+
if (cls) n.className = cls;
|
|
59
|
+
if (text != null) n.textContent = text;
|
|
60
|
+
return n;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pill(doc, entry) {
|
|
64
|
+
const btn = h(doc, 'button', 'ap');
|
|
65
|
+
btn.type = 'button';
|
|
66
|
+
if (entry.key) btn.dataset.key = entry.key; else btn.dataset.kind = entry.kind;
|
|
67
|
+
btn.disabled = Boolean(entry.disabled);
|
|
68
|
+
if (entry.disabled) btn.classList.add('dim');
|
|
69
|
+
const dot = h(doc, 'span', 'd');
|
|
70
|
+
dot.dataset.color = entry.kind ? 'flow' : (entry.color || 'blue');
|
|
71
|
+
const body = h(doc, 'span', 'b');
|
|
72
|
+
body.append(h(doc, 'span', 'n', entry.displayName || entry.key || entry.kind),
|
|
73
|
+
h(doc, 'span', 'p pt', entry.portLine != null ? entry.portLine : portLineOf(entry)));
|
|
74
|
+
btn.append(dot, body);
|
|
75
|
+
if (entry.disabled) btn.appendChild(h(doc, 'span', 'chip', '1 placed'));
|
|
76
|
+
if (entry.description) btn.title = entry.description;
|
|
77
|
+
return btn;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The disclosure caret. Turned by CSS off `aria-expanded`, so the header's
|
|
81
|
+
* state can never disagree with the pills it hides. */
|
|
82
|
+
function chevron(doc) {
|
|
83
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
84
|
+
const svg = doc.createElementNS(NS, 'svg');
|
|
85
|
+
for (const [k, v] of [['class', 'chev'], ['width', '13'], ['height', '13'], ['viewBox', '0 0 24 24'],
|
|
86
|
+
['fill', 'none'], ['stroke', 'currentColor'], ['stroke-width', '2.6'], ['aria-hidden', 'true']]) svg.setAttribute(k, v);
|
|
87
|
+
const path = doc.createElementNS(NS, 'path');
|
|
88
|
+
path.setAttribute('d', 'M6 9l6 6 6-6');
|
|
89
|
+
path.setAttribute('stroke-linecap', 'round');
|
|
90
|
+
path.setAttribute('stroke-linejoin', 'round');
|
|
91
|
+
svg.appendChild(path);
|
|
92
|
+
return svg;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function renderPalette(host, { agents = [], placedKinds = [], collapsed = new Set(), query = '', doc = globalThis.document } = {}) {
|
|
96
|
+
if (!host) return;
|
|
97
|
+
const groups = paletteEntries(agents, { placedKinds });
|
|
98
|
+
const frag = doc.createDocumentFragment();
|
|
99
|
+
for (const g of groups) {
|
|
100
|
+
const sec = h(doc, 'section', `pal-group${g.flow ? ' pal-pinned' : ''}`);
|
|
101
|
+
sec.dataset.domain = g.domain;
|
|
102
|
+
// The group HEAD is the control: there is no separate chip row, so a domain
|
|
103
|
+
// can only be reached — and only be folded away — through its own header.
|
|
104
|
+
const head = h(doc, 'button', 'pal-grp');
|
|
105
|
+
head.type = 'button';
|
|
106
|
+
head.dataset.domain = g.domain;
|
|
107
|
+
head.append(chevron(doc), h(doc, 'span', 'lab', g.flow ? 'Flow' : g.domain), h(doc, 'span', 'chip', String(g.agents.length)));
|
|
108
|
+
if (g.flow) head.appendChild(h(doc, 'span', 'chip pinned-tag', 'pinned'));
|
|
109
|
+
const pills = h(doc, 'div', 'pills');
|
|
110
|
+
for (const a of g.agents) pills.appendChild(pill(doc, a));
|
|
111
|
+
sec.append(head, pills);
|
|
112
|
+
frag.appendChild(sec);
|
|
113
|
+
}
|
|
114
|
+
// host IS the scroll container: replaceChildren collapses scrollHeight, which
|
|
115
|
+
// clamps scrollTop to 0 and would bounce the list to the top after every
|
|
116
|
+
// spawn, throwing the pinned Flow group out of reach.
|
|
117
|
+
const keep = host.scrollTop;
|
|
118
|
+
host.replaceChildren(frag);
|
|
119
|
+
if (keep) host.scrollTop = keep;
|
|
120
|
+
applyFilter(host, query, collapsed);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Hides non-matching pills, then settles each group's disclosure. A LIVE QUERY
|
|
124
|
+
* force-expands every matching group: a hit folded away inside a collapsed
|
|
125
|
+
* header reads as a broken filter. Clearing the query restores the collapse. */
|
|
126
|
+
export function applyFilter(host, query, collapsed = new Set()) {
|
|
127
|
+
if (!host) return;
|
|
128
|
+
const q = String(query || '').trim().toLowerCase();
|
|
129
|
+
for (const sec of host.querySelectorAll('.pal-group')) {
|
|
130
|
+
let any = false;
|
|
131
|
+
for (const btn of sec.querySelectorAll('.ap')) {
|
|
132
|
+
const hay = `${btn.querySelector('.n').textContent} ${btn.dataset.key || btn.dataset.kind || ''} ${btn.querySelector('.p').textContent}`.toLowerCase();
|
|
133
|
+
const show = !q || hay.includes(q);
|
|
134
|
+
btn.hidden = !show;
|
|
135
|
+
if (show) any = true;
|
|
136
|
+
}
|
|
137
|
+
const open = Boolean(q) || !collapsed.has(sec.dataset.domain);
|
|
138
|
+
sec.hidden = !any;
|
|
139
|
+
const head = sec.querySelector('.pal-grp');
|
|
140
|
+
if (head) head.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
141
|
+
const pills = sec.querySelector('.pills');
|
|
142
|
+
if (pills) pills.hidden = !open;
|
|
143
|
+
}
|
|
144
|
+
}
|