@worca/app 1.0.0 → 1.1.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 +22 -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 +319 -45
- 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 +189 -21
- package/src/core/ask/catalog.mjs +111 -0
- package/src/core/ask/comment-deps.mjs +55 -0
- package/src/core/ask/events.mjs +506 -0
- package/src/core/ask/follow.mjs +107 -0
- package/src/core/ask/git-allowlist.mjs +226 -0
- package/src/core/ask/limits.mjs +54 -0
- package/src/core/ask/mcp-stdio.mjs +135 -0
- package/src/core/ask/models.mjs +125 -0
- package/src/core/ask/prompt.mjs +261 -0
- package/src/core/ask/proposal.mjs +170 -0
- package/src/core/ask/redact.mjs +30 -0
- package/src/core/ask/spawn.mjs +153 -0
- package/src/core/ask/store.mjs +360 -0
- package/src/core/ask/tool-deps.mjs +63 -0
- package/src/core/ask/tools.mjs +848 -0
- package/src/core/ask/turn.mjs +416 -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 +20 -3
- package/src/core/claude-runner.mjs +434 -57
- package/src/core/config.mjs +264 -41
- package/src/core/cost-budget.mjs +29 -2
- package/src/core/db.mjs +684 -47
- 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/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 +1065 -0
- package/src/core/graph/seed-templates.mjs +318 -0
- package/src/core/model-env.mjs +112 -8
- package/src/core/model-test.mjs +79 -0
- package/src/core/orchestrator.mjs +902 -4098
- 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 +77 -14
- 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 +3585 -0
- package/src/core/run-manifest.mjs +5 -1
- package/src/core/settings.mjs +109 -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 +4008 -1670
- package/ui/public/ask-markdown.mjs +145 -0
- package/ui/public/ask-model.mjs +264 -0
- package/ui/public/ask-panel.mjs +1880 -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 +269 -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 +1459 -229
- package/ui/public/syntax-highlight.mjs +270 -0
- package/ui/public/thinking-orb.mjs +110 -0
- package/ui/server.mjs +1667 -98
- 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
package/ui/public/log-filter.mjs
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// History "Live logs" panel. Extracted so the narrowing semantics are
|
|
5
5
|
// unit-testable without booting app.js (mirrors log-line.mjs).
|
|
6
6
|
//
|
|
7
|
-
// A filter is { source, level, step, cycle, search } where '' /
|
|
8
|
-
// "all". Every axis composes with AND.
|
|
7
|
+
// A filter is { source, level, step, node, execution, cycle, search } where '' /
|
|
8
|
+
// undefined means "all". Every axis composes with AND.
|
|
9
9
|
// - level: exact match; a record with no level counts as 'info' (the same
|
|
10
10
|
// default logLineClass applies when rendering).
|
|
11
11
|
// - source: matches the role itself AND its fanned-out sub-agents — a child
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
// "planner ▸ research auth" but not "plannerX".
|
|
14
14
|
// - step: matches String(rec.stepIndex). Lines with no step attribution
|
|
15
15
|
// (preflight, orchestrator, ui notices) only show when no step is chosen.
|
|
16
|
+
// - node: matches String(rec.nodeId) — the v2 graph node the line was attributed
|
|
17
|
+
// to; attribution-less lines only show when no node is chosen.
|
|
18
|
+
// - execution: matches String(rec.executionId) — ONE execution of one node
|
|
19
|
+
// (x:<nodeId>:<ordinal>), so a loop's second cycle can be read alone.
|
|
16
20
|
// - cycle: matches String(rec.cycle) — the feedback-loop rewind counter, which is
|
|
17
21
|
// orthogonal to step (a re-run keeps its stepIndex and bumps its cycle). Same
|
|
18
22
|
// attribution rule as step: cycle-less lines only show when no cycle is chosen.
|
|
@@ -31,6 +35,10 @@ export function compileLogFilter(filter) {
|
|
|
31
35
|
const source = filter.source || '';
|
|
32
36
|
const hasStep = filter.step !== undefined && filter.step !== '';
|
|
33
37
|
const step = hasStep ? String(filter.step) : '';
|
|
38
|
+
const hasNode = filter.node !== undefined && filter.node !== '';
|
|
39
|
+
const node = hasNode ? String(filter.node) : '';
|
|
40
|
+
const hasExec = filter.execution !== undefined && filter.execution !== '';
|
|
41
|
+
const execution = hasExec ? String(filter.execution) : '';
|
|
34
42
|
const hasCycle = filter.cycle !== undefined && filter.cycle !== '';
|
|
35
43
|
const cycle = hasCycle ? String(filter.cycle) : '';
|
|
36
44
|
const term = filter.search ? String(filter.search).toLowerCase() : '';
|
|
@@ -41,6 +49,8 @@ export function compileLogFilter(filter) {
|
|
|
41
49
|
if (src !== source && !src.startsWith(source + SUB_SEP)) return false;
|
|
42
50
|
}
|
|
43
51
|
if (hasStep && (rec.stepIndex == null || String(rec.stepIndex) !== step)) return false;
|
|
52
|
+
if (hasNode && (rec.nodeId == null || String(rec.nodeId) !== node)) return false;
|
|
53
|
+
if (hasExec && (rec.executionId == null || String(rec.executionId) !== execution)) return false;
|
|
44
54
|
if (hasCycle && (rec.cycle == null || String(rec.cycle) !== cycle)) return false;
|
|
45
55
|
if (term && !String(rec.text || '').toLowerCase().includes(term)) return false;
|
|
46
56
|
return true;
|
|
@@ -54,13 +64,17 @@ export function logLineVisible(rec, filter) {
|
|
|
54
64
|
// Distinct facet values the filter dropdowns offer, from the lines seen so far:
|
|
55
65
|
// sources collapsed to their parent role (sub-agents fold into the role they
|
|
56
66
|
// belong to), levels defaulted to 'info', steps = the stepIndex values present,
|
|
57
|
-
// cycles = the cycle values present
|
|
58
|
-
//
|
|
67
|
+
// cycles = the cycle values present, nodes = the v2 nodeId values present,
|
|
68
|
+
// executions = the v2 executionId values present. All sorted (steps/cycles
|
|
69
|
+
// numerically, ids lexically) for stable dropdowns. `search` is free text and
|
|
70
|
+
// has no facet.
|
|
59
71
|
export function logFacets(lines) {
|
|
60
72
|
const sources = new Set();
|
|
61
73
|
const levels = new Set();
|
|
62
74
|
const steps = new Set();
|
|
63
75
|
const cycles = new Set();
|
|
76
|
+
const nodes = new Set();
|
|
77
|
+
const executions = new Set();
|
|
64
78
|
for (const rec of lines || []) {
|
|
65
79
|
if (!rec) continue;
|
|
66
80
|
const src = rec.source || '';
|
|
@@ -71,11 +85,15 @@ export function logFacets(lines) {
|
|
|
71
85
|
levels.add(rec.level || 'info');
|
|
72
86
|
if (rec.stepIndex != null) steps.add(rec.stepIndex);
|
|
73
87
|
if (rec.cycle != null) cycles.add(rec.cycle);
|
|
88
|
+
if (rec.nodeId != null) nodes.add(rec.nodeId);
|
|
89
|
+
if (rec.executionId != null) executions.add(rec.executionId);
|
|
74
90
|
}
|
|
75
91
|
return {
|
|
76
92
|
sources: [...sources].sort(),
|
|
77
93
|
levels: [...levels].sort(),
|
|
78
94
|
steps: [...steps].sort((a, b) => a - b),
|
|
79
95
|
cycles: [...cycles].sort((a, b) => a - b),
|
|
96
|
+
nodes: [...nodes].sort(),
|
|
97
|
+
executions: [...executions].sort(),
|
|
80
98
|
};
|
|
81
99
|
}
|
package/ui/public/log-line.mjs
CHANGED
|
@@ -36,12 +36,11 @@ export function logLineText(rec) {
|
|
|
36
36
|
* needs the dashes. */
|
|
37
37
|
export function serializeLog(recs) {
|
|
38
38
|
const out = [];
|
|
39
|
-
|
|
39
|
+
const state = newCycleState();
|
|
40
40
|
for (const rec of recs || []) {
|
|
41
41
|
if (!rec) continue;
|
|
42
|
-
const sep = cycleSeparatorBefore(
|
|
42
|
+
const sep = cycleSeparatorBefore(state, rec);
|
|
43
43
|
if (sep) out.push(`── ${sep} ──`);
|
|
44
|
-
if (rec.cycle != null) prevCycle = rec.cycle;
|
|
45
44
|
out.push(logLineText(rec));
|
|
46
45
|
}
|
|
47
46
|
return out.join('\n');
|
|
@@ -52,35 +51,62 @@ export function serializeLog(recs) {
|
|
|
52
51
|
* and the History replay must agree on which fields survive, or an axis works
|
|
53
52
|
* in one pane while silently dead in the other (`cycle` was dropped by one of
|
|
54
53
|
* three hand-rolled copies of this projection). `cycle` drives the cycle
|
|
55
|
-
* picker AND the "── Cycle N ──" separators; `stream` is stderr provenance
|
|
56
|
-
*
|
|
54
|
+
* picker AND the "── Cycle N ──" separators; `stream` is stderr provenance;
|
|
55
|
+
* `nodeId` / `executionId` (v2 graph runs) drive the node select and the
|
|
56
|
+
* execution chip. Absent attribution stays ABSENT (the filters test `!= null`). */
|
|
57
57
|
export function projectLogRecord(rec) {
|
|
58
58
|
return {
|
|
59
59
|
source: rec.source, level: rec.level, text: rec.text, ts: rec.ts, sub: !!rec.sub,
|
|
60
60
|
...(rec.stepIndex != null ? { stepIndex: rec.stepIndex } : {}),
|
|
61
61
|
...(rec.cycle != null ? { cycle: rec.cycle } : {}),
|
|
62
|
+
...(rec.nodeId != null ? { nodeId: rec.nodeId } : {}),
|
|
63
|
+
...(rec.executionId != null ? { executionId: rec.executionId } : {}),
|
|
62
64
|
...(rec.stream ? { stream: rec.stream } : {}),
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
/** The separator cursor: nodeId -> the HIGHEST ordinal rendered for that node
|
|
69
|
+
* so far ('' buckets a record with no nodeId — orchestrator notices, and every
|
|
70
|
+
* v1-shaped log). One per pane, carried across a filter repaint so a live
|
|
71
|
+
* append after one agrees with it. */
|
|
72
|
+
export function newCycleState() {
|
|
73
|
+
return new Map();
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
/**
|
|
67
|
-
* The separator label to draw BEFORE `rec`, or null for none
|
|
77
|
+
* The separator label to draw BEFORE `rec`, or null for none — and the ONE
|
|
78
|
+
* place `state` advances.
|
|
79
|
+
*
|
|
80
|
+
* In v2 `cycle` is the PER-NODE ordinal (`orchestrator.mjs` stamps
|
|
81
|
+
* `cycle: ctx.ordinal` beside `nodeId`), NOT v1's pipeline-wide rewind counter.
|
|
82
|
+
* Two nodes stream concurrently at unrelated ordinals, so comparing `rec.cycle`
|
|
83
|
+
* to "the last cycle rendered by ANY node" drew a rule on nearly every
|
|
84
|
+
* alternation (MIN-37). The rule belongs where a NODE re-runs: `rec.cycle`
|
|
85
|
+
* strictly exceeds the highest ordinal that node has already rendered.
|
|
86
|
+
*
|
|
87
|
+
* Consequences that are deliberate:
|
|
88
|
+
* · a node's FIRST line never gets a header, whatever its ordinal — so a node
|
|
89
|
+
* that starts late at ordinal 1 draws nothing;
|
|
90
|
+
* · alternation between nodes draws nothing;
|
|
91
|
+
* · a cycle-less notice (artifact event, git line) landing exactly at a
|
|
92
|
+
* boundary neither draws nor masks a rule — it does not touch the state.
|
|
68
93
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* re-run is otherwise indistinguishable from its first pass. A rule drawn at the
|
|
72
|
-
* boundary makes that legible without the reader having to filter for it.
|
|
94
|
+
* The state must be advanced from RENDERED records only, never from the model,
|
|
95
|
+
* so a filter that hides an entire cycle cannot orphan a separator.
|
|
73
96
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* must come from rendered records, not the model, so a filter that hides an
|
|
78
|
-
* entire cycle cannot orphan a separator. null (no cycled record rendered yet)
|
|
79
|
-
* yields null: no leading "Cycle 1" header.
|
|
97
|
+
* @param {Map<string, number>} state from newCycleState(); mutated in place
|
|
98
|
+
* @param {object|null} rec
|
|
99
|
+
* @returns {string|null}
|
|
80
100
|
*/
|
|
81
|
-
export function cycleSeparatorBefore(
|
|
101
|
+
export function cycleSeparatorBefore(state, rec) {
|
|
82
102
|
if (!rec || rec.cycle == null) return null;
|
|
83
|
-
|
|
84
|
-
if (
|
|
103
|
+
const seen = state instanceof Map ? state : null;
|
|
104
|
+
if (!seen) return null;
|
|
105
|
+
const key = rec.nodeId == null ? '' : String(rec.nodeId);
|
|
106
|
+
const n = Number(rec.cycle);
|
|
107
|
+
if (!Number.isFinite(n)) return null;
|
|
108
|
+
const last = seen.get(key);
|
|
109
|
+
seen.set(key, last == null ? n : Math.max(last, n));
|
|
110
|
+
if (last == null || n <= last) return null;
|
|
85
111
|
return `Cycle ${rec.cycle}`;
|
|
86
112
|
}
|
|
@@ -29,6 +29,48 @@ export function envSummary(env) {
|
|
|
29
29
|
return keys.includes('ANTHROPIC_BASE_URL') ? `${n} · routes via base URL` : n;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** The per-Mtok rate keys, in editor order, with their display labels. Mirrors
|
|
33
|
+
* settings.mjs COST_RATE_KEYS — the server is the validator, this is the form. */
|
|
34
|
+
export const COST_RATES = [
|
|
35
|
+
['input', 'Input'],
|
|
36
|
+
['output', 'Output'],
|
|
37
|
+
['cacheRead', 'Cache read'],
|
|
38
|
+
['cacheWrite', 'Cache write (5m)'],
|
|
39
|
+
['cacheWrite1h', 'Cache write (1h)'],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/** One-line pricing summary for a card, or '' when the model has no override
|
|
43
|
+
* (the overwhelming default — the CLI's own figure is trusted). */
|
|
44
|
+
export function costSummary(cost) {
|
|
45
|
+
if (!cost || typeof cost !== 'object') return '';
|
|
46
|
+
if (cost.free) return 'priced free';
|
|
47
|
+
const p = cost.perMtok;
|
|
48
|
+
if (!p || typeof p !== 'object') return '';
|
|
49
|
+
const set = COST_RATES.filter(([k]) => p[k] != null);
|
|
50
|
+
if (!set.length) return '';
|
|
51
|
+
return `${set.map(([k, lab]) => `${lab.toLowerCase()} $${p[k]}`).join(' · ')} /Mtok`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A free id for a duplicate of `id`: `<id>-copy`, then `-copy-2`, `-copy-3`…
|
|
56
|
+
* Compared case-insensitively against EVERY id the catalog knows (global,
|
|
57
|
+
* plugin, built-in, legacy) — the add would be rejected for colliding with any
|
|
58
|
+
* of them, and a suggestion the server refuses is worse than no suggestion.
|
|
59
|
+
* @param {string} id the source model's id
|
|
60
|
+
* @param {Iterable<string>} takenIds
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function suggestDuplicateId(id, takenIds = []) {
|
|
64
|
+
const taken = new Set([...takenIds].map((t) => String(t).toLowerCase()));
|
|
65
|
+
const base = `${id}-copy`;
|
|
66
|
+
if (!taken.has(base.toLowerCase())) return base;
|
|
67
|
+
for (let n = 2; n < 1000; n += 1) {
|
|
68
|
+
const c = `${base}-${n}`;
|
|
69
|
+
if (!taken.has(c.toLowerCase())) return c;
|
|
70
|
+
}
|
|
71
|
+
return base; // 998 copies of one model: let the server reject the duplicate
|
|
72
|
+
}
|
|
73
|
+
|
|
32
74
|
/**
|
|
33
75
|
* The Models view body: global entries (editable), the selected project's
|
|
34
76
|
* legacy custom models (promotable), and the built-in catalog (read-only).
|
|
@@ -60,10 +102,15 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], pred
|
|
|
60
102
|
head.appendChild(h(doc, 'b', 'mv-name', m.label || m.id));
|
|
61
103
|
if (predefLc.has(m.id.toLowerCase())) head.appendChild(h(doc, 'span', 'badge violet mv-shadow', 'overrides built-in'));
|
|
62
104
|
else if (pluginLc.has(m.id.toLowerCase())) head.appendChild(h(doc, 'span', 'badge violet mv-shadow', 'overrides plugin'));
|
|
63
|
-
|
|
105
|
+
// The §4.6 "unreliable" badge is meaningless once an override GOVERNS this
|
|
106
|
+
// model's spend — and the backend only lifts the stored flag on the model's
|
|
107
|
+
// next result event, so suppress it here the moment pricing is pinned.
|
|
108
|
+
if (m.costUnreliable && !m.cost) head.appendChild(h(doc, 'span', 'badge waiting mv-cost', 'cost not verified'));
|
|
109
|
+
if (m.cost) head.appendChild(h(doc, 'span', 'badge violet mv-cost-pinned', m.cost.free ? 'free' : 'priced'));
|
|
64
110
|
body.appendChild(head);
|
|
65
|
-
const bits = [m.id, effortsSummary(m.efforts, efforts), envSummary(m.env)].filter(Boolean);
|
|
111
|
+
const bits = [m.id, effortsSummary(m.efforts, efforts), envSummary(m.env), costSummary(m.cost)].filter(Boolean);
|
|
66
112
|
body.appendChild(h(doc, 'small', 'mv-summary hint', bits.join(' — ')));
|
|
113
|
+
body.appendChild(h(doc, 'small', 'mv-test-result hint')); // app.js paints the Test outcome here
|
|
67
114
|
card.appendChild(body);
|
|
68
115
|
const del = h(doc, 'button', 'btn-ghost mv-delete', 'Delete');
|
|
69
116
|
del.type = 'button'; del.dataset.id = m.id;
|
|
@@ -71,6 +118,15 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], pred
|
|
|
71
118
|
const edit = h(doc, 'button', 'btn-ghost mv-edit', 'Edit');
|
|
72
119
|
edit.type = 'button'; edit.dataset.id = m.id;
|
|
73
120
|
card.appendChild(edit);
|
|
121
|
+
// Duplicate opens a CREATE editor seeded from this entry (app.js fetches the
|
|
122
|
+
// raw env values first — the card only ever holds masked ones). The point is
|
|
123
|
+
// deriving a sibling: same routing, one parameter changed.
|
|
124
|
+
const dup = h(doc, 'button', 'btn-ghost mv-duplicate', 'Duplicate');
|
|
125
|
+
dup.type = 'button'; dup.dataset.id = m.id;
|
|
126
|
+
card.appendChild(dup);
|
|
127
|
+
const tst = h(doc, 'button', 'btn-ghost mv-test', 'Test');
|
|
128
|
+
tst.type = 'button'; tst.dataset.id = m.id;
|
|
129
|
+
card.appendChild(tst);
|
|
74
130
|
yours.appendChild(card);
|
|
75
131
|
}
|
|
76
132
|
root.appendChild(yours);
|
|
@@ -113,18 +169,30 @@ export function renderModelsList({ globals = [], legacy = [], plugins = [], pred
|
|
|
113
169
|
head.appendChild(h(doc, 'b', 'mv-name', m.label || m.id));
|
|
114
170
|
head.appendChild(h(doc, 'span', 'badge waiting mv-origin', `plugin: ${m.plugin}`));
|
|
115
171
|
if (globalLc.has(m.id.toLowerCase())) head.appendChild(h(doc, 'span', 'badge violet mv-shadowed', 'overridden by your copy'));
|
|
116
|
-
|
|
172
|
+
// Same rule as a global card: a manifest-pinned price governs the spend,
|
|
173
|
+
// so the §4.6 "unreliable" flag says nothing about it.
|
|
174
|
+
if (m.costUnreliable && !m.cost) head.appendChild(h(doc, 'span', 'badge waiting mv-cost', 'cost not verified'));
|
|
175
|
+
if (m.cost) head.appendChild(h(doc, 'span', 'badge violet mv-cost-pinned', m.cost.free ? 'free' : 'priced'));
|
|
117
176
|
body.appendChild(head);
|
|
118
|
-
const bits = [m.id, effortsSummary(m.efforts, efforts), envSummary(m.env)].filter(Boolean);
|
|
177
|
+
const bits = [m.id, effortsSummary(m.efforts, efforts), envSummary(m.env), costSummary(m.cost)].filter(Boolean);
|
|
119
178
|
body.appendChild(h(doc, 'small', 'mv-summary hint', bits.join(' — ')));
|
|
120
179
|
for (const s of m.secrets || []) {
|
|
121
180
|
body.appendChild(h(doc, 'small', `mv-secret hint${s.set ? '' : ' err'}`,
|
|
122
181
|
s.set ? `secret ${s.key}: set` : `secret ${s.key}: NOT SET — configure it in the plugin's settings`));
|
|
123
182
|
}
|
|
183
|
+
body.appendChild(h(doc, 'small', 'mv-test-result hint')); // app.js paints the Test outcome here
|
|
124
184
|
card.appendChild(body);
|
|
125
185
|
const copy = h(doc, 'button', 'btn-ghost mv-copy', 'Edit a copy');
|
|
126
186
|
copy.type = 'button'; copy.dataset.id = m.id; copy.dataset.plugin = m.plugin;
|
|
127
187
|
card.appendChild(copy);
|
|
188
|
+
const tst = h(doc, 'button', 'btn-ghost mv-test', 'Test');
|
|
189
|
+
tst.type = 'button'; tst.dataset.id = m.id; tst.dataset.plugin = m.plugin;
|
|
190
|
+
if ((m.secrets || []).some((s) => !s.set)) {
|
|
191
|
+
// A test would only prove what the NOT SET line already says.
|
|
192
|
+
tst.disabled = true;
|
|
193
|
+
tst.title = 'set the plugin secret first';
|
|
194
|
+
}
|
|
195
|
+
card.appendChild(tst);
|
|
128
196
|
plug.appendChild(card);
|
|
129
197
|
}
|
|
130
198
|
root.appendChild(plug);
|
|
@@ -205,7 +273,7 @@ export function renderModelEditor(model, efforts, { doc = globalThis.document }
|
|
|
205
273
|
idInput.placeholder = 'claude-opus-4-8, glm-4.7, a fine-tune id…';
|
|
206
274
|
idInput.value = editing ? model.id : '';
|
|
207
275
|
idInput.disabled = editing; // the id IS the reference — delete + re-add to rename
|
|
208
|
-
grid.appendChild(field('Model id (
|
|
276
|
+
grid.appendChild(field('Model id (worca’s handle; sent to claude --model unless ANTHROPIC_MODEL overrides)', idInput,
|
|
209
277
|
editing ? '' : 'Use a built-in id to override that built-in.'));
|
|
210
278
|
|
|
211
279
|
const labelInput = h(doc, 'input', 'input mv-label');
|
|
@@ -232,7 +300,7 @@ export function renderModelEditor(model, efforts, { doc = globalThis.document }
|
|
|
232
300
|
const add = h(doc, 'button', 'btn-ghost mv-env-add', '+ env var');
|
|
233
301
|
add.type = 'button';
|
|
234
302
|
grid.appendChild(field('Routing env (merged into the claude spawn for this model)', envWrap,
|
|
235
|
-
'e.g. ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN. Stored values show masked; leave masked to keep. WORCA_* and process keys are reserved.'));
|
|
303
|
+
'e.g. ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN. ANTHROPIC_MODEL sets the wire id sent to --model (the id above stays worca’s handle). Stored values show masked; leave masked to keep. WORCA_* and process keys are reserved.'));
|
|
236
304
|
const envBtns = h(doc, 'div', 'mv-env-btns');
|
|
237
305
|
envBtns.appendChild(add);
|
|
238
306
|
if (rows.length) {
|
|
@@ -245,7 +313,48 @@ export function renderModelEditor(model, efforts, { doc = globalThis.document }
|
|
|
245
313
|
}
|
|
246
314
|
grid.appendChild(envBtns);
|
|
247
315
|
|
|
316
|
+
// ── Pricing (opt-in per-model cost override, config.mjs resolveModelCost) ──
|
|
317
|
+
// The CLI computes total_cost_usd from its OWN table keyed on the model NAME,
|
|
318
|
+
// so an on-prem/proxied endpoint gets a fabricated figure worca cannot tell
|
|
319
|
+
// from a real one. Three mutually exclusive modes; the default is unchanged
|
|
320
|
+
// behavior. Rendered detached like everything else here — app.js wires the
|
|
321
|
+
// mode change to applyCostMode.
|
|
322
|
+
const costWrap = h(doc, 'div', 'mv-cost-edit');
|
|
323
|
+
const modes = h(doc, 'div', 'mv-cost-modes');
|
|
324
|
+
const groupName = `mv-cost-mode-${editing ? model.id : 'new'}`;
|
|
325
|
+
for (const [value, text] of [
|
|
326
|
+
['cli', 'Trust the CLI'],
|
|
327
|
+
['free', 'Free ($0)'],
|
|
328
|
+
['perMtok', 'Per million tokens'],
|
|
329
|
+
]) {
|
|
330
|
+
const lab = h(doc, 'label', 'mv-cost-mode');
|
|
331
|
+
const rb = h(doc, 'input', 'mv-cost-mode-rb');
|
|
332
|
+
rb.type = 'radio'; rb.name = groupName; rb.value = value;
|
|
333
|
+
lab.appendChild(rb);
|
|
334
|
+
lab.appendChild(h(doc, 'span', null, text));
|
|
335
|
+
modes.appendChild(lab);
|
|
336
|
+
}
|
|
337
|
+
costWrap.appendChild(modes);
|
|
338
|
+
|
|
339
|
+
const rates = h(doc, 'div', 'mv-cost-rates');
|
|
340
|
+
for (const [key, labelText] of COST_RATES) {
|
|
341
|
+
const lab = h(doc, 'label', 'mv-cost-rate');
|
|
342
|
+
lab.appendChild(h(doc, 'span', 'mv-cost-rate-label', labelText));
|
|
343
|
+
const inp = h(doc, 'input', 'input mv-cost-rate-in');
|
|
344
|
+
inp.type = 'number'; inp.min = '0'; inp.step = '0.01'; inp.placeholder = '0';
|
|
345
|
+
inp.dataset.rate = key;
|
|
346
|
+
lab.appendChild(inp);
|
|
347
|
+
lab.appendChild(h(doc, 'span', 'mv-cost-rate-unit', '$/Mtok'));
|
|
348
|
+
rates.appendChild(lab);
|
|
349
|
+
}
|
|
350
|
+
costWrap.appendChild(rates);
|
|
351
|
+
grid.appendChild(field('Pricing', costWrap,
|
|
352
|
+
'Only for an endpoint the CLI prices by NAME rather than from what the endpoint reports — an on-prem or proxied model. '
|
|
353
|
+
+ 'Trust the CLI is the default and leaves spend exactly as today. Blank rates count as $0, except Cache write (1h), '
|
|
354
|
+
+ 'which falls back to the 5m rate when blank.'));
|
|
355
|
+
|
|
248
356
|
root.appendChild(grid);
|
|
357
|
+
setModelCost(root, editing ? model.cost : null); // grid is attached now — the block is reachable from root
|
|
249
358
|
const msg = h(doc, 'p', 'form-msg mv-editor-msg');
|
|
250
359
|
msg.setAttribute('aria-live', 'polite');
|
|
251
360
|
root.appendChild(msg);
|
|
@@ -260,6 +369,41 @@ export function renderModelEditor(model, efforts, { doc = globalThis.document }
|
|
|
260
369
|
return root;
|
|
261
370
|
}
|
|
262
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Load a `cost` override into an editor: pick the matching mode and fill the
|
|
374
|
+
* rates. The ONE place that maps a stored cost onto the form, shared by the
|
|
375
|
+
* initial render and by "Edit a copy"'s prefill of a plugin model's pricing —
|
|
376
|
+
* a second implementation of this mapping is exactly how the two would drift.
|
|
377
|
+
* Safe on an editor with no pricing block. Pass null/undefined for no override.
|
|
378
|
+
* @param {Element} rootEl the .mv-editor root
|
|
379
|
+
* @param {{free?:boolean, perMtok?:Record<string,number>}|null} [cost]
|
|
380
|
+
*/
|
|
381
|
+
export function setModelCost(rootEl, cost) {
|
|
382
|
+
const rates = rootEl && rootEl.querySelector('.mv-cost-rates');
|
|
383
|
+
if (!rates) return;
|
|
384
|
+
const mode = !cost ? 'cli' : (cost.free ? 'free' : 'perMtok');
|
|
385
|
+
for (const rb of rootEl.querySelectorAll('.mv-cost-mode-rb')) rb.checked = rb.value === mode;
|
|
386
|
+
const p = cost && cost.perMtok && typeof cost.perMtok === 'object' ? cost.perMtok : {};
|
|
387
|
+
for (const inp of rootEl.querySelectorAll('.mv-cost-rate-in')) {
|
|
388
|
+
const v = p[inp.dataset.rate];
|
|
389
|
+
inp.value = v == null ? '' : String(v);
|
|
390
|
+
}
|
|
391
|
+
applyCostMode(rootEl);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Show the per-Mtok rate inputs only in that mode. The single place that knows
|
|
396
|
+
* the rule, so the initial render and the delegated `change` handler in app.js
|
|
397
|
+
* can never drift apart. Safe on an editor with no pricing block.
|
|
398
|
+
* @param {Element} rootEl the .mv-editor root
|
|
399
|
+
*/
|
|
400
|
+
export function applyCostMode(rootEl) {
|
|
401
|
+
const rates = rootEl && rootEl.querySelector('.mv-cost-rates');
|
|
402
|
+
if (!rates) return;
|
|
403
|
+
const picked = rootEl.querySelector('.mv-cost-mode-rb:checked');
|
|
404
|
+
rates.hidden = (picked ? picked.value : 'cli') !== 'perMtok';
|
|
405
|
+
}
|
|
406
|
+
|
|
263
407
|
/** app.js exposes envRow creation to the delegated mv-env-add handler. */
|
|
264
408
|
export function makeEnvRow({ doc = globalThis.document } = {}) {
|
|
265
409
|
return envRow(doc);
|
|
@@ -294,12 +438,30 @@ export function collectModelEditor(rootEl) {
|
|
|
294
438
|
}
|
|
295
439
|
}
|
|
296
440
|
|
|
441
|
+
// Pricing. The editor renders the STORED override, so it round-trips: a user
|
|
442
|
+
// who came to change a label leaves it untouched. 'cli' is therefore an
|
|
443
|
+
// explicit CLEAR (null), not an omission — it is what the form now shows.
|
|
444
|
+
// Rate values are sent raw for the server to validate (settings.mjs
|
|
445
|
+
// assertModelCost owns the rules; duplicating them here would let them drift).
|
|
446
|
+
const mode = rootEl.querySelector('.mv-cost-mode-rb:checked')?.value || 'cli';
|
|
447
|
+
let cost = null;
|
|
448
|
+
if (mode === 'free') cost = { free: true };
|
|
449
|
+
else if (mode === 'perMtok') {
|
|
450
|
+
const perMtok = {};
|
|
451
|
+
for (const inp of rootEl.querySelectorAll('.mv-cost-rate-in')) {
|
|
452
|
+
const raw = (inp.value ?? '').trim();
|
|
453
|
+
if (raw !== '') perMtok[inp.dataset.rate] = Number(raw);
|
|
454
|
+
}
|
|
455
|
+
cost = { perMtok }; // empty -> the server rejects it by name, surfaced in the form
|
|
456
|
+
}
|
|
457
|
+
|
|
297
458
|
const body = {
|
|
298
459
|
...(editing ? {} : { id }),
|
|
299
460
|
label,
|
|
300
461
|
// All boxes checked = the full set = store the default (empty).
|
|
301
462
|
efforts: efforts.length === allCount ? [] : efforts,
|
|
302
463
|
env,
|
|
464
|
+
cost,
|
|
303
465
|
};
|
|
304
466
|
return { id: editing ? id : null, body };
|
|
305
467
|
}
|
|
@@ -331,13 +493,13 @@ export function renderExportWizard(globals, { doc = globalThis.document } = {})
|
|
|
331
493
|
|
|
332
494
|
const pick = step('1 — Models to include', 'Only your global models can be shared.');
|
|
333
495
|
for (const m of globals) {
|
|
334
|
-
const lab = h(doc, 'label', 'mvx-model');
|
|
496
|
+
const lab = h(doc, 'label', 'mvx-model opt-row');
|
|
335
497
|
const cb = h(doc, 'input', 'mvx-model-cb');
|
|
336
498
|
cb.type = 'checkbox'; cb.value = m.id;
|
|
337
499
|
cb.dataset.envKeys = Object.keys(m.env || {}).join('\n');
|
|
338
500
|
lab.appendChild(cb);
|
|
339
|
-
lab.appendChild(h(doc, 'span',
|
|
340
|
-
lab.appendChild(h(doc, 'small', 'hint', m.id));
|
|
501
|
+
lab.appendChild(h(doc, 'span', 'opt-name', `${m.label || m.id} `));
|
|
502
|
+
lab.appendChild(h(doc, 'small', 'hint opt-sub', m.id));
|
|
341
503
|
pick.appendChild(lab);
|
|
342
504
|
}
|
|
343
505
|
if (!globals.length) pick.appendChild(h(doc, 'div', 'hist-empty', 'No global models to share yet.'));
|
|
@@ -57,17 +57,33 @@ export function renderPluginList(plugins, { doc = globalThis.document, channelSt
|
|
|
57
57
|
head.appendChild(h(doc, 'b', 'pl-name', p.name));
|
|
58
58
|
head.appendChild(h(doc, 'span', 'pl-version mono', p.version || sha7(p.pinnedSha)));
|
|
59
59
|
if (p.linked) head.appendChild(h(doc, 'span', 'badge waiting pl-linked', 'linked'));
|
|
60
|
-
if (p.
|
|
60
|
+
if (p.apiMismatch) head.appendChild(h(doc, 'span', 'badge amber pl-api-mismatch', 'needs update'));
|
|
61
|
+
else if (p.broken) head.appendChild(h(doc, 'span', 'badge red pl-broken', 'broken'));
|
|
62
|
+
// Enabling a plugin acts on a live system the moment it flips, so it reads
|
|
63
|
+
// as a switch. `.switch` MUST be the input's immediate next sibling — the
|
|
64
|
+
// `.sw-input:checked + .switch` rule is what paints the on state.
|
|
61
65
|
const toggle = h(doc, 'label', 'pl-enable');
|
|
62
|
-
const cb = h(doc, 'input', 'pl-toggle');
|
|
66
|
+
const cb = h(doc, 'input', 'sw-input pl-toggle');
|
|
63
67
|
cb.type = 'checkbox';
|
|
64
68
|
cb.checked = p.enabled !== false;
|
|
65
69
|
cb.dataset.name = p.name;
|
|
70
|
+
cb.setAttribute('aria-label', `Enable ${p.name}`);
|
|
66
71
|
toggle.appendChild(cb);
|
|
72
|
+
toggle.appendChild(h(doc, 'span', 'switch switch-sm'));
|
|
67
73
|
toggle.appendChild(h(doc, 'span', '', p.enabled !== false ? 'enabled' : 'disabled'));
|
|
68
74
|
head.appendChild(toggle);
|
|
69
75
|
card.appendChild(head);
|
|
70
76
|
card.appendChild(h(doc, 'small', 'pl-contrib hint', contribSummary(p.contributions)));
|
|
77
|
+
if (p.apiMismatch) card.appendChild(h(doc, 'small', 'pl-api-note hint err', p.apiMismatch.message || ''));
|
|
78
|
+
// Contributions worca refused to load. Same note treatment as the API note:
|
|
79
|
+
// the contributions line above counts what the plugin SHIPS, so without this
|
|
80
|
+
// the card claims an agent or a template that exists nowhere.
|
|
81
|
+
if (p.ignored && p.ignored.length) {
|
|
82
|
+
const n = p.ignored.length;
|
|
83
|
+
card.appendChild(h(doc, 'small', 'pl-ignored-note hint err',
|
|
84
|
+
`${n} contribution${n > 1 ? 's' : ''} ignored: `
|
|
85
|
+
+ p.ignored.map((i) => `${i.file} — ${i.reason}`).join('; ')));
|
|
86
|
+
}
|
|
71
87
|
if (p.repo || p.marketplaceName) {
|
|
72
88
|
const prov = [p.marketplaceName, p.repo].filter(Boolean).join(' · ');
|
|
73
89
|
card.appendChild(h(doc, 'small', 'pl-provenance hint mono',
|
|
@@ -234,6 +250,37 @@ export function renderUpdatePreview(preview, { doc = globalThis.document } = {})
|
|
|
234
250
|
return root;
|
|
235
251
|
}
|
|
236
252
|
|
|
253
|
+
// The roster row for a multiProfile source: which configuration is being
|
|
254
|
+
// edited, plus add/remove. app.js owns the actions (each is a server round trip
|
|
255
|
+
// that re-opens the pane), so this only renders the controls and marks them.
|
|
256
|
+
function profileBar(doc, src) {
|
|
257
|
+
const bar = h(doc, 'div', 'pl-profile-bar');
|
|
258
|
+
bar.dataset.sourceId = src.id || '';
|
|
259
|
+
bar.appendChild(h(doc, 'label', '', 'Profile'));
|
|
260
|
+
const sel = h(doc, 'select', 'select pl-profile-sel');
|
|
261
|
+
sel.dataset.sourceId = src.id || '';
|
|
262
|
+
for (const p of src.profiles || []) {
|
|
263
|
+
const id = typeof p === 'string' ? p : String(p.id || '');
|
|
264
|
+
const label = typeof p === 'string' ? '' : (p.label || '');
|
|
265
|
+
const opt = h(doc, 'option', '', label ? `${label} (${id})` : id);
|
|
266
|
+
opt.value = id;
|
|
267
|
+
sel.appendChild(opt);
|
|
268
|
+
}
|
|
269
|
+
if (src.profile) sel.value = src.profile;
|
|
270
|
+
if (!(src.profiles || []).length) sel.disabled = true;
|
|
271
|
+
bar.appendChild(sel);
|
|
272
|
+
const add = h(doc, 'button', 'btn btn-ghost btn-mini pl-profile-add', 'Add');
|
|
273
|
+
add.type = 'button';
|
|
274
|
+
add.dataset.sourceId = src.id || '';
|
|
275
|
+
bar.appendChild(add);
|
|
276
|
+
const del = h(doc, 'button', 'btn btn-ghost btn-mini pl-profile-del', 'Remove');
|
|
277
|
+
del.type = 'button';
|
|
278
|
+
del.dataset.sourceId = src.id || '';
|
|
279
|
+
if (!src.profile) del.disabled = true;
|
|
280
|
+
bar.appendChild(del);
|
|
281
|
+
return bar;
|
|
282
|
+
}
|
|
283
|
+
|
|
237
284
|
// renderConfigForm(sections) — one <form.pl-config-form>
|
|
238
285
|
// per task source. secret:true fields (text-only per normalizeManifest) render
|
|
239
286
|
// type=password, NEVER prefilled; a stored value arrives redacted as {set:true}
|
|
@@ -250,12 +297,25 @@ export function renderConfigForm(sections, { doc = globalThis.document } = {}) {
|
|
|
250
297
|
...channels.map((x) => ({ ...x, _kind: 'channel' })),
|
|
251
298
|
];
|
|
252
299
|
for (const src of rows) {
|
|
300
|
+
// A multiProfile source is a ROSTER of configurations, not one form: the
|
|
301
|
+
// bar picks which profile the form below edits. Rendered before the form so
|
|
302
|
+
// "which instance am I editing" is answered above the fields, not after.
|
|
303
|
+
if (src._kind === 'source' && src.multiProfile) {
|
|
304
|
+
root.appendChild(profileBar(doc, src));
|
|
305
|
+
// Nothing to write into: creating the profile is a separate call, and the
|
|
306
|
+
// server rejects a save without one. Offer the add button, not a form.
|
|
307
|
+
if (!src.profile) {
|
|
308
|
+
root.appendChild(h(doc, 'p', 'hint', 'No profiles yet — add one to configure this source.'));
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
253
312
|
const form = h(doc, 'form', 'pl-config-form');
|
|
254
313
|
if (src._kind === 'channel') {
|
|
255
314
|
form.dataset.channelId = src.id || '';
|
|
256
315
|
form.appendChild(h(doc, 'div', 'pl-config-h', `${src.displayName || src.id} (${src.platform || 'chat'} channel)`));
|
|
257
316
|
} else {
|
|
258
317
|
form.dataset.sourceId = src.id || '';
|
|
318
|
+
if (src.multiProfile) form.dataset.profile = src.profile;
|
|
259
319
|
}
|
|
260
320
|
for (const f of src.schema || []) {
|
|
261
321
|
const field = h(doc, 'div', 'field');
|
|
@@ -291,7 +351,9 @@ export function renderConfigForm(sections, { doc = globalThis.document } = {}) {
|
|
|
291
351
|
return root;
|
|
292
352
|
}
|
|
293
353
|
|
|
294
|
-
// collectConfigForm(formEl) -> { sourceId | channelId, values }
|
|
354
|
+
// collectConfigForm(formEl) -> { sourceId | channelId, values } (+ profile for
|
|
355
|
+
// a multiProfile source; the key is ABSENT otherwise, so a single-profile save
|
|
356
|
+
// stays byte-identical to what it sent before profiles existed). An untouched
|
|
295
357
|
// {set:true} secret (data-set="1", still empty) is OMITTED — saving never
|
|
296
358
|
// wipes a secret. Channel forms carry data-channel-id instead of data-source-id.
|
|
297
359
|
export function collectConfigForm(formEl) {
|
|
@@ -301,7 +363,47 @@ export function collectConfigForm(formEl) {
|
|
|
301
363
|
values[input.dataset.key] = input.value;
|
|
302
364
|
}
|
|
303
365
|
if (formEl.dataset.channelId) return { channelId: formEl.dataset.channelId, values };
|
|
304
|
-
|
|
366
|
+
const out = { sourceId: formEl.dataset.sourceId || '', values };
|
|
367
|
+
if (formEl.dataset.profile) out.profile = formEl.dataset.profile;
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// renderConnectResult(result) — outcome of a source's validateConfig, rendered
|
|
372
|
+
// under the settings form. Three states, because setup can legitimately be
|
|
373
|
+
// mid-flight: connected (ok), waiting (ok:false + pending — the caller keeps
|
|
374
|
+
// polling), or failed (ok:false + errors[], each pinned to its config field).
|
|
375
|
+
// A connector may return no envelope at all on a transport error; `result`
|
|
376
|
+
// then carries { error: { kind, message } } from the /api/sources/call frame.
|
|
377
|
+
export function renderConnectResult(result, { doc = globalThis.document } = {}) {
|
|
378
|
+
const r = result || {};
|
|
379
|
+
const root = h(doc, 'div', 'pl-connect-result');
|
|
380
|
+
if (r.ok) {
|
|
381
|
+
root.appendChild(h(doc, 'div', 'badge green', 'connected'));
|
|
382
|
+
if (r.identity) root.appendChild(h(doc, 'span', 'hint', `signed in as ${r.identity}`));
|
|
383
|
+
// Which server/project the connector actually resolved — a mistyped sample
|
|
384
|
+
// URL otherwise connects successfully to the wrong instance.
|
|
385
|
+
if (r.instance && r.instance.baseUrl) {
|
|
386
|
+
const where = r.instance.project ? `${r.instance.baseUrl} · ${r.instance.project}` : r.instance.baseUrl;
|
|
387
|
+
root.appendChild(h(doc, 'div', 'hint mono', where));
|
|
388
|
+
}
|
|
389
|
+
return root;
|
|
390
|
+
}
|
|
391
|
+
if (r.pending) {
|
|
392
|
+
root.appendChild(h(doc, 'div', 'badge', 'waiting'));
|
|
393
|
+
root.appendChild(h(doc, 'span', 'hint', r.message || 'Waiting…'));
|
|
394
|
+
return root;
|
|
395
|
+
}
|
|
396
|
+
root.appendChild(h(doc, 'div', 'badge red', 'not connected'));
|
|
397
|
+
const errors = Array.isArray(r.errors) && r.errors.length
|
|
398
|
+
? r.errors
|
|
399
|
+
: [{ message: (r.error && r.error.message) || r.message || 'connection failed' }];
|
|
400
|
+
for (const e of errors) {
|
|
401
|
+
const row = h(doc, 'div', 'pl-connect-err');
|
|
402
|
+
if (e.field) row.appendChild(h(doc, 'span', 'mono', e.field));
|
|
403
|
+
row.appendChild(h(doc, 'span', 'hint err', e.message || ''));
|
|
404
|
+
root.appendChild(row);
|
|
405
|
+
}
|
|
406
|
+
return root;
|
|
305
407
|
}
|
|
306
408
|
|
|
307
409
|
// renderDoctorReport(report: {ok, checks:[{id,ok,detail}]}) — row per check.
|