@worca/app 0.0.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 +403 -0
- package/agents/clarify.meta.json +19 -0
- package/agents/decomposer.meta.json +21 -0
- package/agents/implementer.meta.json +20 -0
- package/agents/manualTestsChecklist.meta.json +18 -0
- package/agents/manualWebUiTesting.meta.json +18 -0
- package/agents/planReviewer.meta.json +19 -0
- package/agents/planner.meta.json +20 -0
- package/agents/refiner.meta.json +19 -0
- package/agents/reviewer.meta.json +19 -0
- package/agents/worca-cc-clarify.md +67 -0
- package/agents/worca-cc-code-reviewer.md +66 -0
- package/agents/worca-cc-decomposer.md +84 -0
- package/agents/worca-cc-implementer.md +69 -0
- package/agents/worca-cc-manual-tests-checklist.md +63 -0
- package/agents/worca-cc-manual-web-ui-testing.md +64 -0
- package/agents/worca-cc-plan-refiner.md +69 -0
- package/agents/worca-cc-plan-reviewer.md +70 -0
- package/agents/worca-cc-planner.md +70 -0
- package/agents/worca-cc-workspace-reviewer.md +56 -0
- package/agents/worca-cc-workspace-scanner.md +55 -0
- package/agents/workspaceReviewer.meta.json +20 -0
- package/agents/workspaceScanner.meta.json +18 -0
- package/package.json +61 -0
- package/scripts/install.mjs +209 -0
- package/skills/worca/SKILL.md +66 -0
- package/src/cli/worca-cc.mjs +1520 -0
- package/src/core/agent-gen.mjs +206 -0
- package/src/core/agent-registry.mjs +417 -0
- package/src/core/agent-store.mjs +143 -0
- package/src/core/artifacts.mjs +2019 -0
- package/src/core/channels.mjs +302 -0
- package/src/core/chat/allowlist.mjs +27 -0
- package/src/core/chat/channel-host.mjs +562 -0
- package/src/core/chat/channel-protocol.mjs +117 -0
- package/src/core/chat/channel-worker-child.mjs +211 -0
- package/src/core/chat/chat-context.mjs +66 -0
- package/src/core/chat/command-router.mjs +343 -0
- package/src/core/chat/notifier.mjs +120 -0
- package/src/core/chat/parser.mjs +30 -0
- package/src/core/chat/rate-limiter.mjs +133 -0
- package/src/core/chat/redact.mjs +27 -0
- package/src/core/chat/renderers.mjs +136 -0
- package/src/core/claude-runner.mjs +1356 -0
- package/src/core/config.mjs +882 -0
- package/src/core/cost-budget.mjs +103 -0
- package/src/core/db.mjs +864 -0
- package/src/core/fanout.mjs +48 -0
- package/src/core/folder-dialog.mjs +138 -0
- package/src/core/fs-browse.mjs +49 -0
- package/src/core/git-info.mjs +200 -0
- package/src/core/guardrail-store.mjs +204 -0
- package/src/core/guardrails.mjs +302 -0
- package/src/core/marketplaces.mjs +267 -0
- package/src/core/migrate-fs-to-db.mjs +612 -0
- package/src/core/model-env.mjs +74 -0
- package/src/core/orchestrator.mjs +4279 -0
- package/src/core/overview-agent.mjs +124 -0
- package/src/core/phases.mjs +1279 -0
- package/src/core/pipeline-delete.mjs +428 -0
- package/src/core/plugin-api.mjs +13 -0
- package/src/core/plugin-config.mjs +100 -0
- package/src/core/plugin-inventory.mjs +50 -0
- package/src/core/plugin-manifest.mjs +447 -0
- package/src/core/plugin-models.mjs +130 -0
- package/src/core/plugin-repo.mjs +303 -0
- package/src/core/plugin-shim-child.mjs +76 -0
- package/src/core/plugin-shim.mjs +197 -0
- package/src/core/plugin-store.mjs +485 -0
- package/src/core/plugin-workflows.mjs +179 -0
- package/src/core/plugins-lock.mjs +49 -0
- package/src/core/preflight-node.mjs +122 -0
- package/src/core/preflight.mjs +341 -0
- package/src/core/projects.mjs +157 -0
- package/src/core/protocol.mjs +257 -0
- package/src/core/recoverable-error.mjs +51 -0
- package/src/core/results.mjs +188 -0
- package/src/core/run-context.mjs +1375 -0
- package/src/core/run-log.mjs +64 -0
- package/src/core/run-manifest.mjs +317 -0
- package/src/core/runners.mjs +167 -0
- package/src/core/settings.mjs +682 -0
- package/src/core/skills.mjs +210 -0
- package/src/core/sources.mjs +232 -0
- package/src/core/stats.mjs +182 -0
- package/src/core/store.mjs +67 -0
- package/src/core/title.mjs +64 -0
- package/src/core/workflow-validator.mjs +185 -0
- package/src/core/workflows.mjs +568 -0
- package/src/core/workspace-scan.mjs +420 -0
- package/src/core/workspaces.mjs +353 -0
- package/src/core/worktree.mjs +708 -0
- package/src/feature.mjs +9 -0
- package/ui/public/app.js +10647 -0
- package/ui/public/assets/worca-favicon.png +0 -0
- package/ui/public/assets/worca-logo.png +0 -0
- package/ui/public/chat-settings-view.mjs +89 -0
- package/ui/public/composer-core.mjs +211 -0
- package/ui/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/ui/public/fonts/poppins-latin-400-normal.woff2 +0 -0
- package/ui/public/fonts/poppins-latin-500-normal.woff2 +0 -0
- package/ui/public/fonts/poppins-latin-600-normal.woff2 +0 -0
- package/ui/public/fonts/poppins-latin-700-normal.woff2 +0 -0
- package/ui/public/guardrails-view.mjs +244 -0
- package/ui/public/index.html +1145 -0
- package/ui/public/log-filter.mjs +81 -0
- package/ui/public/log-line.mjs +86 -0
- package/ui/public/models-view.mjs +433 -0
- package/ui/public/plugins-view.mjs +430 -0
- package/ui/public/results-view.mjs +121 -0
- package/ui/public/source-pane.mjs +156 -0
- package/ui/public/stats-view.mjs +523 -0
- package/ui/public/style.css +1557 -0
- package/ui/server.mjs +3573 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// ui/public/plugins-view.mjs
|
|
2
|
+
// Pure DOM renderers for the Plugins view. Every function takes the target
|
|
3
|
+
// `document` via opts (defaults to the browser global) and returns DETACHED
|
|
4
|
+
// elements — no fetch, no listeners outside the returned tree. app.js owns
|
|
5
|
+
// endpoint calls, the modal shell, and mounting; node:test drives these via jsdom.
|
|
6
|
+
|
|
7
|
+
function h(doc, tag, cls, text) {
|
|
8
|
+
const n = doc.createElement(tag);
|
|
9
|
+
if (cls) n.className = cls;
|
|
10
|
+
if (text != null) n.textContent = text;
|
|
11
|
+
return n;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sha7 = (sha) => (typeof sha === 'string' ? sha.slice(0, 7) : '');
|
|
15
|
+
|
|
16
|
+
// Inline feather-style icons for card action buttons (stroke follows currentColor).
|
|
17
|
+
const ICONS = {
|
|
18
|
+
bin: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>',
|
|
19
|
+
refresh: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// icon button: <button class=cls>[svg] label</button>. innerHTML is a fixed
|
|
23
|
+
// ICONS constant (never caller data), so no injection surface.
|
|
24
|
+
function iconBtn(doc, cls, icon, label) {
|
|
25
|
+
const b = h(doc, 'button', cls);
|
|
26
|
+
b.type = 'button';
|
|
27
|
+
b.innerHTML = ICONS[icon];
|
|
28
|
+
b.appendChild(doc.createTextNode(label));
|
|
29
|
+
return b;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// contributions -> "2 agents · 1 source · 1 skill". Arrays (listInstalledPlugins)
|
|
33
|
+
// or plain counts are both tolerated.
|
|
34
|
+
function contribSummary(c) {
|
|
35
|
+
const n = (v) => (Array.isArray(v) ? v.length : (Number.isFinite(v) ? v : 0));
|
|
36
|
+
const parts = [
|
|
37
|
+
[n(c && c.agents), 'agent'], [n(c && c.taskSources), 'source'],
|
|
38
|
+
[n(c && c.chatChannels), 'chat channel'], [n(c && c.models), 'model'],
|
|
39
|
+
[n(c && c.skills), 'skill'], [n(c && c.workflows), 'workflow'],
|
|
40
|
+
].filter(([k]) => k > 0).map(([k, w]) => `${k} ${w}${k > 1 ? 's' : ''}`);
|
|
41
|
+
return parts.join(' · ') || 'no contributions';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// renderPluginList(plugins, {channelStatus}) -> <div.pl-list> of cards.
|
|
45
|
+
// Action buttons + the enable checkbox carry data-name and a pl-* class so
|
|
46
|
+
// app.js can wire ONE delegated listener on the list container. channelStatus
|
|
47
|
+
// rows (GET /api/chat/status) add per-channel live badges; the badge carries
|
|
48
|
+
// data-channel-key="<plugin>/<id>" so a channel-status WS event can patch it
|
|
49
|
+
// in place.
|
|
50
|
+
export function renderPluginList(plugins, { doc = globalThis.document, channelStatus = [] } = {}) {
|
|
51
|
+
const root = h(doc, 'div', 'pl-list');
|
|
52
|
+
for (const p of plugins || []) {
|
|
53
|
+
const card = h(doc, 'section', 'card plugin-card');
|
|
54
|
+
card.dataset.name = p.name;
|
|
55
|
+
if (p.enabled === false) card.classList.add('pl-disabled');
|
|
56
|
+
const head = h(doc, 'div', 'pl-head');
|
|
57
|
+
head.appendChild(h(doc, 'b', 'pl-name', p.name));
|
|
58
|
+
head.appendChild(h(doc, 'span', 'pl-version mono', p.version || sha7(p.pinnedSha)));
|
|
59
|
+
if (p.linked) head.appendChild(h(doc, 'span', 'badge waiting pl-linked', 'linked'));
|
|
60
|
+
if (p.broken) head.appendChild(h(doc, 'span', 'badge red pl-broken', 'broken'));
|
|
61
|
+
const toggle = h(doc, 'label', 'pl-enable');
|
|
62
|
+
const cb = h(doc, 'input', 'pl-toggle');
|
|
63
|
+
cb.type = 'checkbox';
|
|
64
|
+
cb.checked = p.enabled !== false;
|
|
65
|
+
cb.dataset.name = p.name;
|
|
66
|
+
toggle.appendChild(cb);
|
|
67
|
+
toggle.appendChild(h(doc, 'span', '', p.enabled !== false ? 'enabled' : 'disabled'));
|
|
68
|
+
head.appendChild(toggle);
|
|
69
|
+
card.appendChild(head);
|
|
70
|
+
card.appendChild(h(doc, 'small', 'pl-contrib hint', contribSummary(p.contributions)));
|
|
71
|
+
if (p.repo || p.marketplaceName) {
|
|
72
|
+
const prov = [p.marketplaceName, p.repo].filter(Boolean).join(' · ');
|
|
73
|
+
card.appendChild(h(doc, 'small', 'pl-provenance hint mono',
|
|
74
|
+
p.pinnedSha ? `${prov} @ ${sha7(p.pinnedSha)}` : prov));
|
|
75
|
+
}
|
|
76
|
+
const chRows = (channelStatus || []).filter((c) => c.plugin === p.name);
|
|
77
|
+
if (chRows.length) {
|
|
78
|
+
const chans = h(doc, 'div', 'pl-channels');
|
|
79
|
+
for (const c of chRows) chans.appendChild(channelBadge(doc, c));
|
|
80
|
+
card.appendChild(chans);
|
|
81
|
+
}
|
|
82
|
+
const actions = h(doc, 'div', 'pl-actions');
|
|
83
|
+
for (const [cls, label] of [['pl-settings', 'Settings'], ['pl-doctor', 'Doctor'], ['pl-update', 'Update'], ['pl-remove', 'Remove']]) {
|
|
84
|
+
const b = h(doc, 'button', `btn-ghost ${cls}`, label);
|
|
85
|
+
b.type = 'button';
|
|
86
|
+
b.dataset.name = p.name;
|
|
87
|
+
actions.appendChild(b);
|
|
88
|
+
}
|
|
89
|
+
card.appendChild(actions);
|
|
90
|
+
root.appendChild(card);
|
|
91
|
+
}
|
|
92
|
+
if (!plugins || !plugins.length) {
|
|
93
|
+
root.appendChild(h(doc, 'div', 'hist-empty', 'No plugins installed. Browse Available below or add a marketplace.'));
|
|
94
|
+
}
|
|
95
|
+
return root;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// One live channel badge: dot color by connection state + platform label.
|
|
99
|
+
// Exported so app.js can re-render a single badge on a channel-status event.
|
|
100
|
+
export function channelBadge(doc, c) {
|
|
101
|
+
const stateCls = { connected: 'green', degraded: 'waiting', connecting: 'waiting' }[c.state] || 'red';
|
|
102
|
+
const b = h(doc, 'span', `badge ${stateCls} pl-channel`, `${c.displayName || c.channelId} · ${c.state}`);
|
|
103
|
+
b.dataset.channelKey = `${c.plugin}/${c.channelId}`;
|
|
104
|
+
if (c.detail) b.title = c.detail;
|
|
105
|
+
return b;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// renderOrphanList(orphans) -> <div.pl-orphans> of "leftover data" rows.
|
|
109
|
+
// Purge buttons carry data-name + .pl-purge-orphan for app.js's delegated
|
|
110
|
+
// listener. Empty/nullish input -> childless container (app.js skips mounting).
|
|
111
|
+
export function renderOrphanList(orphans, { doc = globalThis.document } = {}) {
|
|
112
|
+
const root = h(doc, 'div', 'pl-orphans');
|
|
113
|
+
if (!orphans || !orphans.length) return root;
|
|
114
|
+
root.appendChild(h(doc, 'h3', 'pl-orphans-title', 'Leftover data'));
|
|
115
|
+
for (const o of orphans) {
|
|
116
|
+
const row = h(doc, 'div', 'card pl-orphan-row');
|
|
117
|
+
row.dataset.name = o.name;
|
|
118
|
+
row.appendChild(h(doc, 'b', 'pl-name', o.name));
|
|
119
|
+
row.appendChild(h(doc, 'small', 'hint', 'uninstalled — config/secrets remain'));
|
|
120
|
+
const btn = h(doc, 'button', 'btn-ghost pl-purge-orphan', 'Purge');
|
|
121
|
+
btn.type = 'button';
|
|
122
|
+
btn.dataset.name = o.name;
|
|
123
|
+
row.appendChild(btn);
|
|
124
|
+
root.appendChild(row);
|
|
125
|
+
}
|
|
126
|
+
return root;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// renderInstallConsent(entry, inventory) — spec §6.1 "Will install" ceremony.
|
|
130
|
+
// entry: { name, repoUrl, sha } (a marketplace snapshot plugin + the marketplace's url/lastSync.sha);
|
|
131
|
+
// inventory: buildInstallInventory shape. Secrets render red; setup commands verbatim.
|
|
132
|
+
export function renderInstallConsent(entry, inventory, { doc = globalThis.document } = {}) {
|
|
133
|
+
const inv = inventory || {};
|
|
134
|
+
const root = h(doc, 'div', 'pl-consent');
|
|
135
|
+
root.appendChild(h(doc, 'div', 'pl-consent-src mono', `${entry.repoUrl} @ ${sha7(entry.sha)}`));
|
|
136
|
+
const section = (label) => {
|
|
137
|
+
const s = h(doc, 'div', 'pl-consent-sec');
|
|
138
|
+
s.appendChild(h(doc, 'div', 'pl-consent-h', label));
|
|
139
|
+
root.appendChild(s);
|
|
140
|
+
return s;
|
|
141
|
+
};
|
|
142
|
+
const agents = section(`Agents (${(inv.agents || []).length})`);
|
|
143
|
+
for (const a of inv.agents || []) {
|
|
144
|
+
agents.appendChild(h(doc, 'div', 'pl-consent-row mono',
|
|
145
|
+
`${a.key} — tools: ${(a.tools || []).join(', ') || 'none declared'}`));
|
|
146
|
+
}
|
|
147
|
+
const sources = section(`Task sources (${(inv.taskSources || []).length})`);
|
|
148
|
+
for (const s of inv.taskSources || []) {
|
|
149
|
+
const row = h(doc, 'div', 'pl-consent-row', s.displayName || s.id);
|
|
150
|
+
for (const key of s.secrets || []) row.appendChild(h(doc, 'span', 'pl-secret', `requests secret: ${key}`));
|
|
151
|
+
sources.appendChild(row);
|
|
152
|
+
}
|
|
153
|
+
if ((inv.chatChannels || []).length) {
|
|
154
|
+
const channels = section(`Chat channels (${inv.chatChannels.length})`);
|
|
155
|
+
for (const ch of inv.chatChannels) {
|
|
156
|
+
const dirs = [ch.inbound && 'inbound', ch.outbound && 'outbound'].filter(Boolean).join(' + ');
|
|
157
|
+
const row = h(doc, 'div', 'pl-consent-row',
|
|
158
|
+
`${ch.displayName || ch.id} (${ch.platform}, ${dirs}) — runs a persistent worker`);
|
|
159
|
+
for (const key of ch.secrets || []) row.appendChild(h(doc, 'span', 'pl-secret', `requests secret: ${key}`));
|
|
160
|
+
channels.appendChild(row);
|
|
161
|
+
}
|
|
162
|
+
channels.appendChild(h(doc, 'div', 'pl-consent-row pl-channel-warn',
|
|
163
|
+
'Inbound chat can pause/stop/approve runs: anyone holding the bot token or in an allowed chat controls worca-cc.'));
|
|
164
|
+
}
|
|
165
|
+
// Models (design §9.4): the base URL renders VERBATIM — a model's env can
|
|
166
|
+
// redirect all API traffic for that model, so the reviewer must see where.
|
|
167
|
+
if ((inv.models || []).length) {
|
|
168
|
+
const models = section(`Models (${inv.models.length})`);
|
|
169
|
+
for (const m of inv.models) {
|
|
170
|
+
const row = h(doc, 'div', 'pl-consent-row', `${m.label || m.id} `);
|
|
171
|
+
row.appendChild(h(doc, 'span', 'mono', `(${m.id})`));
|
|
172
|
+
if (m.baseUrl) row.appendChild(h(doc, 'span', 'pl-secret pl-baseurl', ` routes to: ${m.baseUrl}`));
|
|
173
|
+
if ((m.envKeys || []).length) row.appendChild(h(doc, 'small', 'hint', ` env: ${m.envKeys.join(', ')}`));
|
|
174
|
+
models.appendChild(row);
|
|
175
|
+
}
|
|
176
|
+
for (const s of inv.modelSecrets || []) {
|
|
177
|
+
models.appendChild(h(doc, 'div', 'pl-consent-row')).appendChild(
|
|
178
|
+
h(doc, 'span', 'pl-secret', `requests model secret: ${s.key}${s.label && s.label !== s.key ? ` (${s.label})` : ''}`));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const skills = section(`Skills (${(inv.skills || []).length})`);
|
|
182
|
+
for (const s of inv.skills || []) skills.appendChild(h(doc, 'div', 'pl-consent-row mono', s));
|
|
183
|
+
const wfs = section(`Workflows (${(inv.workflows || []).length})`);
|
|
184
|
+
for (const w of inv.workflows || []) wfs.appendChild(h(doc, 'div', 'pl-consent-row mono', w));
|
|
185
|
+
const setup = section('Setup');
|
|
186
|
+
setup.appendChild(h(doc, 'div', 'pl-consent-row',
|
|
187
|
+
inv.depCount == null ? 'no dependencies' : `${inv.depCount} npm dependencies (from lockfile)`));
|
|
188
|
+
for (const cmd of inv.setupCommands || []) setup.appendChild(h(doc, 'div', 'pl-consent-row mono pl-setup-cmd', cmd));
|
|
189
|
+
root.appendChild(h(doc, 'p', 'hint', 'Plugins run with your user privileges. Install only sources you trust.'));
|
|
190
|
+
return root;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// renderUpdatePreview(preview) — fetchCandidate result: pinned→candidate shas,
|
|
194
|
+
// commit log, diffstat, confirm button (.pl-confirm-update; app.js wires it).
|
|
195
|
+
// No new commits -> a plain up-to-date state: badge + hint, no shas/diffstat/button.
|
|
196
|
+
export function renderUpdatePreview(preview, { doc = globalThis.document } = {}) {
|
|
197
|
+
const p = preview || {};
|
|
198
|
+
const root = h(doc, 'div', 'pl-update');
|
|
199
|
+
if (!(p.commits || []).length) {
|
|
200
|
+
const row = h(doc, 'div', 'pl-uptodate');
|
|
201
|
+
row.appendChild(h(doc, 'span', 'badge green', 'up to date'));
|
|
202
|
+
const at = sha7(p.pinnedSha || p.candidateSha);
|
|
203
|
+
row.appendChild(h(doc, 'span', 'hint', `You are on the latest version${at ? ` (${at})` : ''}.`));
|
|
204
|
+
root.appendChild(row);
|
|
205
|
+
return root;
|
|
206
|
+
}
|
|
207
|
+
root.appendChild(h(doc, 'div', 'pl-update-shas mono', `${sha7(p.pinnedSha)} → ${sha7(p.candidateSha)}`));
|
|
208
|
+
const list = h(doc, 'div', 'pl-commits');
|
|
209
|
+
for (const c of p.commits) list.appendChild(h(doc, 'div', 'pl-commit mono', `${sha7(c.sha)} ${c.subject}`));
|
|
210
|
+
root.appendChild(list);
|
|
211
|
+
if (p.diffstat) root.appendChild(h(doc, 'pre', 'pl-diffstat mono', p.diffstat));
|
|
212
|
+
// Manifest delta — the §6.2 red-flag review lines: new secrets/agents/sources.
|
|
213
|
+
const d = p.manifestDelta || {};
|
|
214
|
+
const flags = [
|
|
215
|
+
...(d.newSecrets || []).map((k) => ['pl-delta-secret', `NEW SECRET requested: ${k}`]),
|
|
216
|
+
...(d.newTaskSources || []).map((s) => ['pl-delta', `new task source: ${s}`]),
|
|
217
|
+
...(d.newAgents || []).map((a) => ['pl-delta', `new agent: ${a}`]),
|
|
218
|
+
...(d.setupChanged ? [['pl-delta', 'setup commands changed']] : []),
|
|
219
|
+
// Model delta (design §9.4): an env change can silently reroute API traffic
|
|
220
|
+
// — red-flag it like a new secret.
|
|
221
|
+
...(d.envChangedModels || []).map((m) => ['pl-delta-secret', `MODEL ENV CHANGED (check its base URL): ${m}`]),
|
|
222
|
+
...(d.newModelSecrets || []).map((k) => ['pl-delta-secret', `NEW MODEL SECRET requested: ${k}`]),
|
|
223
|
+
...(d.newModels || []).map((m) => ['pl-delta', `new model: ${m}`]),
|
|
224
|
+
...(d.removedModels || []).map((m) => ['pl-delta', `removed model: ${m}`]),
|
|
225
|
+
];
|
|
226
|
+
if (flags.length) {
|
|
227
|
+
const box = h(doc, 'div', 'pl-manifest-delta');
|
|
228
|
+
for (const [cls, text] of flags) box.appendChild(h(doc, 'div', cls, text));
|
|
229
|
+
root.appendChild(box);
|
|
230
|
+
}
|
|
231
|
+
const btn = h(doc, 'button', 'btn btn-primary btn-mini pl-confirm-update', 'Apply update');
|
|
232
|
+
btn.type = 'button';
|
|
233
|
+
root.appendChild(btn);
|
|
234
|
+
return root;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// renderConfigForm(sections) — one <form.pl-config-form>
|
|
238
|
+
// per task source. secret:true fields (text-only per normalizeManifest) render
|
|
239
|
+
// type=password, NEVER prefilled; a stored value arrives redacted as {set:true}
|
|
240
|
+
// -> placeholder '(set)' + data-set="1" so collect can skip it untouched.
|
|
241
|
+
// Accepts the legacy array of sources OR the full { sources, channels } config
|
|
242
|
+
// payload. Channel forms carry data-channel-id (collectConfigForm routes the
|
|
243
|
+
// PUT accordingly) and a small platform heading so mixed plugins stay legible.
|
|
244
|
+
export function renderConfigForm(sections, { doc = globalThis.document } = {}) {
|
|
245
|
+
const root = h(doc, 'div', 'pl-config');
|
|
246
|
+
const sources = Array.isArray(sections) ? sections : (sections?.sources || []);
|
|
247
|
+
const channels = Array.isArray(sections) ? [] : (sections?.channels || []);
|
|
248
|
+
const rows = [
|
|
249
|
+
...sources.map((x) => ({ ...x, _kind: 'source' })),
|
|
250
|
+
...channels.map((x) => ({ ...x, _kind: 'channel' })),
|
|
251
|
+
];
|
|
252
|
+
for (const src of rows) {
|
|
253
|
+
const form = h(doc, 'form', 'pl-config-form');
|
|
254
|
+
if (src._kind === 'channel') {
|
|
255
|
+
form.dataset.channelId = src.id || '';
|
|
256
|
+
form.appendChild(h(doc, 'div', 'pl-config-h', `${src.displayName || src.id} (${src.platform || 'chat'} channel)`));
|
|
257
|
+
} else {
|
|
258
|
+
form.dataset.sourceId = src.id || '';
|
|
259
|
+
}
|
|
260
|
+
for (const f of src.schema || []) {
|
|
261
|
+
const field = h(doc, 'div', 'field');
|
|
262
|
+
field.appendChild(h(doc, 'label', '', f.label || f.key));
|
|
263
|
+
let input;
|
|
264
|
+
const val = (src.values || {})[f.key];
|
|
265
|
+
if (f.type === 'select') {
|
|
266
|
+
input = h(doc, 'select', 'select');
|
|
267
|
+
for (const o of f.options || []) {
|
|
268
|
+
const opt = h(doc, 'option', '', typeof o === 'object' ? (o.label ?? o.value) : String(o));
|
|
269
|
+
opt.value = typeof o === 'object' ? String(o.value) : String(o);
|
|
270
|
+
input.appendChild(opt);
|
|
271
|
+
}
|
|
272
|
+
if (typeof val === 'string') input.value = val;
|
|
273
|
+
} else if (f.secret) {
|
|
274
|
+
input = h(doc, 'input', 'input');
|
|
275
|
+
input.type = 'password';
|
|
276
|
+
input.value = '';
|
|
277
|
+
if (val && val.set === true) { input.placeholder = '(set)'; input.dataset.set = '1'; }
|
|
278
|
+
} else {
|
|
279
|
+
input = h(doc, 'input', 'input');
|
|
280
|
+
input.type = 'text';
|
|
281
|
+
input.value = typeof val === 'string' ? val : (f.default != null ? String(f.default) : '');
|
|
282
|
+
}
|
|
283
|
+
input.dataset.key = f.key;
|
|
284
|
+
if (f.required) input.dataset.required = '1';
|
|
285
|
+
field.appendChild(input);
|
|
286
|
+
if (f.help) field.appendChild(h(doc, 'small', 'hint', f.help));
|
|
287
|
+
form.appendChild(field);
|
|
288
|
+
}
|
|
289
|
+
root.appendChild(form);
|
|
290
|
+
}
|
|
291
|
+
return root;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// collectConfigForm(formEl) -> { sourceId | channelId, values }. An untouched
|
|
295
|
+
// {set:true} secret (data-set="1", still empty) is OMITTED — saving never
|
|
296
|
+
// wipes a secret. Channel forms carry data-channel-id instead of data-source-id.
|
|
297
|
+
export function collectConfigForm(formEl) {
|
|
298
|
+
const values = {};
|
|
299
|
+
for (const input of formEl.querySelectorAll('[data-key]')) {
|
|
300
|
+
if (input.dataset.set === '1' && input.value === '') continue;
|
|
301
|
+
values[input.dataset.key] = input.value;
|
|
302
|
+
}
|
|
303
|
+
if (formEl.dataset.channelId) return { channelId: formEl.dataset.channelId, values };
|
|
304
|
+
return { sourceId: formEl.dataset.sourceId || '', values };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// renderDoctorReport(report: {ok, checks:[{id,ok,detail}]}) — row per check.
|
|
308
|
+
export function renderDoctorReport(report, { doc = globalThis.document } = {}) {
|
|
309
|
+
const r = report || {};
|
|
310
|
+
const root = h(doc, 'div', 'pl-doctor-report');
|
|
311
|
+
root.appendChild(h(doc, 'div', `badge ${r.ok ? 'green' : 'red'}`, r.ok ? 'healthy' : 'problems found'));
|
|
312
|
+
for (const c of r.checks || []) {
|
|
313
|
+
const row = h(doc, 'div', 'pl-doc-row');
|
|
314
|
+
row.appendChild(h(doc, 'span', `badge ${c.ok ? 'green' : 'red'}`, c.ok ? 'ok' : 'fail'));
|
|
315
|
+
row.appendChild(h(doc, 'span', 'mono', c.id));
|
|
316
|
+
if (c.detail) row.appendChild(h(doc, 'span', 'hint', c.detail));
|
|
317
|
+
root.appendChild(row);
|
|
318
|
+
}
|
|
319
|
+
return root;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// renderReferences409(refs) — uninstall guard: who still references the plugin.
|
|
323
|
+
export function renderReferences409(refs, { doc = globalThis.document } = {}) {
|
|
324
|
+
const root = h(doc, 'div', 'pl-refs');
|
|
325
|
+
root.appendChild(h(doc, 'p', 'hint err', 'Cannot uninstall: still referenced by'));
|
|
326
|
+
const ul = h(doc, 'ul', 'pl-refs-list');
|
|
327
|
+
for (const ref of refs || []) {
|
|
328
|
+
let text;
|
|
329
|
+
if (typeof ref === 'string') {
|
|
330
|
+
text = ref;
|
|
331
|
+
} else if (Array.isArray(ref.nodes) || Array.isArray(ref.steps)) {
|
|
332
|
+
// A model-guard reference (design §9.4): { id, steps, nodes }.
|
|
333
|
+
const count = (ref.nodes || []).length + (ref.steps || []).length;
|
|
334
|
+
text = `model: ${ref.id} (${count} pipeline selection${count === 1 ? '' : 's'})`;
|
|
335
|
+
} else {
|
|
336
|
+
text = `${ref.type || 'workflow'}: ${ref.name || ref.id || JSON.stringify(ref)}`;
|
|
337
|
+
}
|
|
338
|
+
ul.appendChild(h(doc, 'li', 'mono', text));
|
|
339
|
+
}
|
|
340
|
+
root.appendChild(ul);
|
|
341
|
+
return root;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// relTime(iso) -> "just now"/"5m ago"/"3h ago"/"2d ago"/ISO date fallback. Pure,
|
|
345
|
+
// jsdom-safe (tests pass a fixed `now`). Exported for reuse + unit tests (C4).
|
|
346
|
+
export function relTime(iso, now = Date.now()) {
|
|
347
|
+
const t = Date.parse(iso);
|
|
348
|
+
if (!Number.isFinite(t)) return String(iso || 'unknown');
|
|
349
|
+
const s = Math.max(0, Math.round((now - t) / 1000));
|
|
350
|
+
if (s < 45) return 'just now';
|
|
351
|
+
const m = Math.round(s / 60); if (m < 60) return `${m}m ago`;
|
|
352
|
+
const h = Math.round(m / 60); if (h < 24) return `${h}h ago`;
|
|
353
|
+
const d = Math.round(h / 24); if (d < 30) return `${d}d ago`;
|
|
354
|
+
return String(iso).slice(0, 10);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// renderAvailableList(marketplaces) -> <div.pl-available> of installable cards
|
|
358
|
+
// across every marketplace snapshot (GET /api/marketplaces). Install buttons
|
|
359
|
+
// carry data-name + data-marketplace for app.js's delegated listener; installed
|
|
360
|
+
// plugins render a badge instead; a never-synced marketplace renders no button
|
|
361
|
+
// (there is no sha to pin yet — Refresh first).
|
|
362
|
+
export function renderAvailableList(marketplaces, { doc = globalThis.document } = {}) {
|
|
363
|
+
const root = h(doc, 'div', 'pl-available');
|
|
364
|
+
const mkts = marketplaces || [];
|
|
365
|
+
let cards = 0;
|
|
366
|
+
for (const m of mkts) {
|
|
367
|
+
for (const p of m.plugins || []) {
|
|
368
|
+
cards++;
|
|
369
|
+
const card = h(doc, 'section', 'card pl-avail-card');
|
|
370
|
+
card.dataset.name = p.name;
|
|
371
|
+
card.dataset.marketplace = m.id;
|
|
372
|
+
const head = h(doc, 'div', 'pl-head');
|
|
373
|
+
head.appendChild(h(doc, 'b', 'pl-name', p.name));
|
|
374
|
+
head.appendChild(h(doc, 'span', 'pl-version mono', p.version || sha7(m.lastSync && m.lastSync.sha)));
|
|
375
|
+
head.appendChild(h(doc, 'span', 'badge waiting pl-mkt-badge', m.name || m.id));
|
|
376
|
+
if (p.installed) {
|
|
377
|
+
head.appendChild(h(doc, 'span', 'badge green pl-installed', 'Installed'));
|
|
378
|
+
} else if (m.lastSync) {
|
|
379
|
+
const b = h(doc, 'button', 'btn btn-primary btn-mini pl-install-avail', 'Install…');
|
|
380
|
+
b.type = 'button';
|
|
381
|
+
b.dataset.name = p.name;
|
|
382
|
+
b.dataset.marketplace = m.id;
|
|
383
|
+
head.appendChild(b);
|
|
384
|
+
}
|
|
385
|
+
card.appendChild(head);
|
|
386
|
+
if (p.description) card.appendChild(h(doc, 'small', 'hint', p.description));
|
|
387
|
+
card.appendChild(h(doc, 'small', 'pl-avail-src hint mono', m.url)); // C10: unspoofable source url
|
|
388
|
+
root.appendChild(card);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!mkts.length) root.appendChild(h(doc, 'div', 'hist-empty', 'No marketplaces yet — add one below.'));
|
|
392
|
+
else if (!cards) root.appendChild(h(doc, 'div', 'hist-empty', 'No plugins discovered in your marketplaces.'));
|
|
393
|
+
return root;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// renderMarketplaceList(marketplaces) -> <div.pl-mkts> of registry rows with
|
|
397
|
+
// Refresh/Remove buttons (data-id). Sync failures surface as .pl-mkt-warning
|
|
398
|
+
// lines; the snapshot stays usable (stale) per spec §4.6.
|
|
399
|
+
export function renderMarketplaceList(marketplaces, { doc = globalThis.document, now = Date.now() } = {}) {
|
|
400
|
+
const root = h(doc, 'div', 'pl-mkts');
|
|
401
|
+
for (const m of marketplaces || []) {
|
|
402
|
+
const row = h(doc, 'div', 'card pl-mkt-row');
|
|
403
|
+
row.dataset.id = m.id;
|
|
404
|
+
const head = h(doc, 'div', 'pl-head');
|
|
405
|
+
head.appendChild(h(doc, 'b', 'pl-name', m.name || m.id));
|
|
406
|
+
if (m.builtin) head.appendChild(h(doc, 'span', 'badge waiting pl-mkt-builtin', 'built-in'));
|
|
407
|
+
const actions = h(doc, 'div', 'pl-mkt-actions');
|
|
408
|
+
for (const [cls, icon, label] of [
|
|
409
|
+
['btn-ghost pl-mkt-refresh', 'refresh', 'Refresh'],
|
|
410
|
+
['pl-remove pl-mkt-remove', 'bin', 'Remove'],
|
|
411
|
+
]) {
|
|
412
|
+
const b = iconBtn(doc, cls, icon, label);
|
|
413
|
+
b.dataset.id = m.id;
|
|
414
|
+
actions.appendChild(b);
|
|
415
|
+
}
|
|
416
|
+
head.appendChild(actions);
|
|
417
|
+
row.appendChild(head);
|
|
418
|
+
row.appendChild(h(doc, 'small', 'pl-mkt-url hint mono', m.url));
|
|
419
|
+
const n = (m.plugins || []).length;
|
|
420
|
+
row.appendChild(h(doc, 'small', 'pl-mkt-sync hint', m.lastSync
|
|
421
|
+
? `${sha7(m.lastSync.sha)} · synced ${relTime(m.lastSync.at, now)} · ${n} plugin${n === 1 ? '' : 's'}`
|
|
422
|
+
: 'never synced — refresh to discover plugins'));
|
|
423
|
+
for (const w of m.warnings || []) row.appendChild(h(doc, 'div', 'pl-mkt-warning hint err', w));
|
|
424
|
+
root.appendChild(row);
|
|
425
|
+
}
|
|
426
|
+
if (!(marketplaces || []).length) {
|
|
427
|
+
root.appendChild(h(doc, 'div', 'hist-empty', 'No marketplaces registered.'));
|
|
428
|
+
}
|
|
429
|
+
return root;
|
|
430
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// ui/public/results-view.mjs
|
|
2
|
+
// Pure shaping helpers for the run Results view. ESM so both the browser (app.js)
|
|
3
|
+
// and node:test can import them. DOM only via the explicit doc param — keep it
|
|
4
|
+
// unit-testable.
|
|
5
|
+
|
|
6
|
+
/** Human-readable summary chips from a Layer-1 results object. */
|
|
7
|
+
export function summaryChips(results) {
|
|
8
|
+
const s = (results && results.summary) || {};
|
|
9
|
+
const chips = [];
|
|
10
|
+
if (s.filesNew) chips.push(`${s.filesNew} new`);
|
|
11
|
+
if (s.filesChanged) chips.push(`${s.filesChanged} changed`);
|
|
12
|
+
if (s.filesDeleted) chips.push(`${s.filesDeleted} deleted`);
|
|
13
|
+
chips.push(`+${s.linesAdded || 0} / −${s.linesRemoved || 0}`);
|
|
14
|
+
chips.push(s.blockingIssues ? `${s.blockingIssues} to check` : 'Clean');
|
|
15
|
+
return chips;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Merge Layer-1 review checks with Layer-2 agent diffFindings. Review checks come
|
|
20
|
+
* first and are never dropped; agent findings are tagged origin:'agent' (+ isNew).
|
|
21
|
+
*/
|
|
22
|
+
export function mergeFindings(checks, diffFindings) {
|
|
23
|
+
const reviewSide = (checks || []).map((c) => ({ ...c, origin: c.origin || 'review' }));
|
|
24
|
+
const agentSide = (diffFindings || []).map((f) => ({
|
|
25
|
+
severity: f.severity, title: f.title, detail: f.detail,
|
|
26
|
+
location: f.file ? `${f.file}${f.line != null ? ':' + f.line : ''}` : '',
|
|
27
|
+
origin: 'agent', isNew: f.newVsReview === true,
|
|
28
|
+
}));
|
|
29
|
+
return [...reviewSide, ...agentSide];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The single status chip kept in the results header: "Clean" or "N to check". */
|
|
33
|
+
export function statusChip(results) {
|
|
34
|
+
const s = (results && results.summary) || {};
|
|
35
|
+
return s.blockingIssues ? `${s.blockingIssues} to check` : 'Clean';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Always-on header badges for the Diff dropdown: "N changed" + "N removed".
|
|
40
|
+
* Rendered even when zero (product spec). `changed` = modified files
|
|
41
|
+
* (summary.filesChanged); `removed` = deleted files (summary.filesDeleted).
|
|
42
|
+
* NOTE: filesChanged already includes deleted rows (bucketFiles routes 'D' into
|
|
43
|
+
* changedFiles); we intentionally preserve today's counting. `n` lets the caller
|
|
44
|
+
* grey a zero badge.
|
|
45
|
+
*/
|
|
46
|
+
export function diffBadges(results) {
|
|
47
|
+
const s = (results && results.summary) || {};
|
|
48
|
+
const changed = s.filesChanged || 0;
|
|
49
|
+
const removed = s.filesDeleted || 0;
|
|
50
|
+
return [
|
|
51
|
+
{ kind: 'changed', n: changed, text: `${changed} changed` },
|
|
52
|
+
{ kind: 'removed', n: removed, text: `${removed} removed` },
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Plugin provenance (spec §7.5, §9.3, §11) ───────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function _el(doc, tag, cls, text) {
|
|
59
|
+
const n = doc.createElement(tag);
|
|
60
|
+
if (cls) n.className = cls;
|
|
61
|
+
if (text != null) n.textContent = text;
|
|
62
|
+
return n;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Badge for a plugin-sourced pipeline row; null for prompt/markdown or a
|
|
66
|
+
* corrupt source_ref (old rows render statically, never throw — §11). */
|
|
67
|
+
export function sourceBadge(row, { doc = globalThis.document } = {}) {
|
|
68
|
+
if ((row?.source_type || 'prompt') !== 'plugin' || !row.source_ref) return null;
|
|
69
|
+
let ref;
|
|
70
|
+
try { ref = JSON.parse(row.source_ref); } catch { return null; }
|
|
71
|
+
if (!ref || !ref.plugin) return null;
|
|
72
|
+
const badge = _el(doc, 'span', 'src-badge');
|
|
73
|
+
badge.appendChild(_el(doc, 'span', 'src-badge-plugin', ref.plugin));
|
|
74
|
+
if (ref.url) {
|
|
75
|
+
const a = _el(doc, 'a', 'src-badge-task', ref.taskId || ref.title || ref.url);
|
|
76
|
+
a.href = ref.url;
|
|
77
|
+
a.target = '_blank';
|
|
78
|
+
a.rel = 'noreferrer';
|
|
79
|
+
badge.appendChild(a);
|
|
80
|
+
} else {
|
|
81
|
+
badge.appendChild(_el(doc, 'span', 'src-badge-task', ref.taskId || ref.title || ''));
|
|
82
|
+
}
|
|
83
|
+
return badge;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** "Report result" retry (§7.5): posts to the report-result endpoint, shows the
|
|
87
|
+
* outcome. `post(url)` is injected (app.js wraps fetch); the in-flight promise
|
|
88
|
+
* is kept on el._pending for deterministic tests. */
|
|
89
|
+
export function reportResultControl(pipelineId, { doc = globalThis.document, post } = {}) {
|
|
90
|
+
const wrap = _el(doc, 'span', 'src-report-wrap');
|
|
91
|
+
const btn = _el(doc, 'button', 'btn btn-ghost btn-mini src-report', 'Report result');
|
|
92
|
+
btn.type = 'button';
|
|
93
|
+
const status = _el(doc, 'span', 'src-report-status hint', '');
|
|
94
|
+
btn.addEventListener('click', () => {
|
|
95
|
+
btn.disabled = true;
|
|
96
|
+
status.textContent = 'reporting…';
|
|
97
|
+
wrap._pending = (async () => {
|
|
98
|
+
try {
|
|
99
|
+
const out = await post(`/api/pipelines/${encodeURIComponent(pipelineId)}/report-result`);
|
|
100
|
+
status.textContent = out && out.ok ? (out.skipped ? 'nothing to report' : 'reported ✓') : `failed: ${out?.error || 'unknown'}`;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
status.textContent = `failed: ${e.message}`;
|
|
103
|
+
} finally {
|
|
104
|
+
btn.disabled = false;
|
|
105
|
+
}
|
|
106
|
+
})();
|
|
107
|
+
});
|
|
108
|
+
wrap.append(btn, status);
|
|
109
|
+
return wrap;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Workflow-picker label (§9.3 badge, §6.5 disabled flag). enabledPluginNames =
|
|
113
|
+
* names of ENABLED installed plugins (from GET /api/plugins client-side); null
|
|
114
|
+
* = plugin list not known yet — show the plugin badge but skip the disabled flag. */
|
|
115
|
+
export function workflowPickerLabel(wf, enabledPluginNames = []) {
|
|
116
|
+
const origin = String(wf?.origin || '');
|
|
117
|
+
if (!origin.startsWith('plugin:')) return wf?.name || '';
|
|
118
|
+
const plugin = origin.slice('plugin:'.length);
|
|
119
|
+
const disabled = Array.isArray(enabledPluginNames) && !enabledPluginNames.includes(plugin);
|
|
120
|
+
return `${wf.name} [plugin: ${plugin}${disabled ? ' — disabled' : ''}]`;
|
|
121
|
+
}
|