spectoflow 0.19.0 → 0.20.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 -5
- package/bin/spectoflow.js +13 -8
- package/lib/adapters.js +48 -1
- package/lib/update.js +7 -1
- package/package.json +1 -1
- package/templates/README.md +6 -4
- package/templates/dashboard/public/app.js +76 -9
- package/templates/dashboard/public/i18n.js +48 -36
- package/templates/dashboard/public/index.html +33 -16
- package/templates/dashboard/public/styles.css +10 -3
- package/templates/dashboard/runner.js +14 -2
- package/templates/dashboard/server.js +37 -0
- package/templates/dashboard/summarize.js +52 -0
- package/templates/lib/agents-registry.js +57 -0
package/README.md
CHANGED
|
@@ -20,6 +20,14 @@ An **agent-agnostic** spec-driven development framework with a **real-time local
|
|
|
20
20
|
You speak in plain language; the framework classifies your intent and runs the right workflow. No
|
|
21
21
|
ceremonial command to start.
|
|
22
22
|
|
|
23
|
+
**Works with whichever coding agent you have** — Claude Code, Codex, Cursor, Gemini CLI, OpenCode,
|
|
24
|
+
Kiro CLI, Antigravity, and Kimi CLI are auto-detected at `init`. The dashboard's topbar always shows
|
|
25
|
+
the **active agent**, front and center, with a switcher: pick another and it's verified as genuinely
|
|
26
|
+
installed before activating (a red **"No agent found"** if none is) — it never silently activates
|
|
27
|
+
something that isn't there. (Kimi CLI doesn't yet ship a non-interactive run mode, so spectoflow can
|
|
28
|
+
set it as active but won't try to spawn it — the agent pickers that launch a run grey it out rather
|
|
29
|
+
than hide it.)
|
|
30
|
+
|
|
23
31
|
## Install
|
|
24
32
|
|
|
25
33
|
```bash
|
|
@@ -39,13 +47,17 @@ Every command works both ways — `spectoflow <cmd>` when installed globally, or
|
|
|
39
47
|
|
|
40
48
|
```
|
|
41
49
|
spectoflow init [dir] [--agent=claude,codex] scaffold a project (auto-detects agents; wires Playwright MCP)
|
|
42
|
-
spectoflow update [--dry-run]
|
|
50
|
+
spectoflow update [--dry-run] [--force|-f] refresh framework files to this kit version
|
|
43
51
|
spectoflow status progress + whether the dashboard is running
|
|
44
52
|
|
|
45
53
|
spectoflow dashboard [--port=NNNN] start the control plane in the background (hands the prompt back)
|
|
46
54
|
spectoflow dashboard status is it running? (url + pid)
|
|
47
55
|
spectoflow dashboard stop (or: stop) stop the running dashboard
|
|
48
56
|
spectoflow dashboard restart stop then start
|
|
57
|
+
spectoflow dashboard create "..." | --auto generate a custom dashboard
|
|
58
|
+
|
|
59
|
+
spectoflow skill create "..." | --auto generate a project skill
|
|
60
|
+
spectoflow agent create "..." | --auto generate a project agent
|
|
49
61
|
|
|
50
62
|
spectoflow list agents, skills and the workflow at a glance
|
|
51
63
|
spectoflow agents list the team personas
|
|
@@ -79,7 +91,10 @@ update` refreshes **framework-owned** files (engine, dashboard, `AGENTS.md`, `ca
|
|
|
79
91
|
`policy.md`, default agents & skills) to the CLI's version, while **preserving your work** —
|
|
80
92
|
`config.json`, `workflow.md`, `specs/`, `plans/`, and any agent/skill you created or edited are never
|
|
81
93
|
touched. A file you edited is preserved and its new version is written next to it as `<file>.new`
|
|
82
|
-
for you to merge by hand. Add `--dry-run` to preview
|
|
94
|
+
for you to merge by hand. Add `--dry-run` to preview, or `--force`/`-f` to overwrite a diverged file
|
|
95
|
+
in place instead of dropping a `.new` — use it once you're sure you have no local edits worth keeping
|
|
96
|
+
in that file (e.g. it's been stuck diverged since an earlier update); it still never touches
|
|
97
|
+
`config.json`, `workflow.md`, `specs/` or `plans/`.
|
|
83
98
|
|
|
84
99
|
First refresh the kit, then run update from your project — the flow is the same whichever way you
|
|
85
100
|
installed:
|
|
@@ -154,9 +169,11 @@ A floating **💬 chat widget** (bottom-right, redesigned) and the **Chat** tab
|
|
|
154
169
|
itself by printing `::spectoflow role=… kind=… msg=…` sentinels, which become labelled messages
|
|
155
170
|
(analyst / developer / qa …); other output streams raw. The board refreshes live as it edits plans.
|
|
156
171
|
Either surface can also **Orchestrate** the enabled workflow: each step runs its agent, gated by mode
|
|
157
|
-
+ policy. The
|
|
158
|
-
|
|
159
|
-
|
|
172
|
+
+ policy. The Chat tab can also **Summarize** the recent log into one digest (via the active agent)
|
|
173
|
+
or **Clear** it outright, when it's grown long. The Agents & Skills drawer is served by the one
|
|
174
|
+
read-only endpoint, `GET /api/agentfile?path=` (scoped to `.spectoflow/agents/**` +
|
|
175
|
+
`.spectoflow/skills/**`, path-traversal-safe) — the framework's only other server surface is
|
|
176
|
+
unchanged.
|
|
160
177
|
|
|
161
178
|
**Customize.** Settings → **Customize** lets you extend the project's own spectoflow install: add a
|
|
162
179
|
dashboard, a skill, or an agent by describing what you want (or hit **Auto** to have the agent survey
|
package/bin/spectoflow.js
CHANGED
|
@@ -226,10 +226,11 @@ function update() {
|
|
|
226
226
|
return console.log('No spectoflow project here. Run: spectoflow init');
|
|
227
227
|
}
|
|
228
228
|
const dryRun = argv.includes('--dry-run');
|
|
229
|
-
const
|
|
229
|
+
const force = argv.includes('--force') || argv.includes('-f');
|
|
230
|
+
const r = require('../lib/update').runUpdate({ projectRoot: root, templatesDir: TPL, version: VERSION, dryRun, force });
|
|
230
231
|
|
|
231
232
|
const from = r.fromVersion || 'unknown';
|
|
232
|
-
const changed = r.refreshed.length + r.created.length + r.adopted.length + r.newSidecar.length;
|
|
233
|
+
const changed = r.refreshed.length + r.created.length + r.adopted.length + r.newSidecar.length + r.forced.length;
|
|
233
234
|
const row = (sym, label, list, painter, note) => {
|
|
234
235
|
if (!list.length) return;
|
|
235
236
|
const n = c.dim(String(list.length).padStart(2));
|
|
@@ -237,18 +238,19 @@ function update() {
|
|
|
237
238
|
console.log(` ${sym} ${painter(label.padEnd(9))} ${n} ${detail}`);
|
|
238
239
|
};
|
|
239
240
|
console.log(logo());
|
|
240
|
-
console.log(` ${c.bold('spectoflow update')} ${c.dim(from)} ${c.amber('→')} ${c.bold(r.toVersion)}${dryRun ? c.dim(' (dry-run)') : ''}`);
|
|
241
|
+
console.log(` ${c.bold('spectoflow update')} ${c.dim(from)} ${c.amber('→')} ${c.bold(r.toVersion)}${dryRun ? c.dim(' (dry-run)') : ''}${force ? c.y(' (force)') : ''}`);
|
|
241
242
|
console.log('');
|
|
242
243
|
row(c.g('✓'), 'refreshed', r.refreshed, c.g);
|
|
243
244
|
row(c.cy('+'), 'created', r.created, c.cy);
|
|
244
245
|
row(c.b('~'), 'adopted', r.adopted, c.b);
|
|
245
|
-
row(c.y('!'), '
|
|
246
|
+
row(c.y('!'), 'forced', r.forced, c.y, 'overwrote a diverged file — its previous content is gone');
|
|
247
|
+
row(c.y('!'), '.new', r.newSidecar, c.y, 'you edited these — new version saved as *.new, merge by hand (or re-run with --force)');
|
|
246
248
|
if (r.unchanged.length) console.log(` ${c.dim('·')} ${c.dim('unchanged'.padEnd(9))} ${c.dim(String(r.unchanged.length).padStart(2))}`);
|
|
247
249
|
console.log(` ${c.dim('=')} ${c.dim('preserved'.padEnd(9))} ${c.dim('config.json · workflow.md · specs/ · plans/ · your custom agents & skills')}`);
|
|
248
250
|
console.log('');
|
|
249
251
|
if (dryRun) console.log(` ${c.dim('(dry-run — nothing was written)')}`);
|
|
250
252
|
else console.log(` ${changed ? c.g('✓ Done') : c.dim('Already up to date')}${changed ? c.dim(` · ${changed} file(s) changed`) : ''}`);
|
|
251
|
-
if (r.newSidecar.length && !dryRun) console.log(` ${c.y('→')} ${c.dim(`${r.newSidecar.length} *.new file(s) to review and merge`)}`);
|
|
253
|
+
if (r.newSidecar.length && !dryRun) console.log(` ${c.y('→')} ${c.dim(`${r.newSidecar.length} *.new file(s) to review and merge — or re-run: spectoflow update --force`)}`);
|
|
252
254
|
console.log('');
|
|
253
255
|
}
|
|
254
256
|
|
|
@@ -442,7 +444,7 @@ ${c.dim('Usage:')} spectoflow ${c.g('<command>')} ${c.dim('[options]')} ${c.di
|
|
|
442
444
|
|
|
443
445
|
${c.bold('Project')}
|
|
444
446
|
${c.g('init')} ${c.dim('[dir] [--agent=a,b]')} scaffold a project (auto-detects agents; wires Playwright MCP)
|
|
445
|
-
${c.g('update')} ${c.dim('[--dry-run]')}
|
|
447
|
+
${c.g('update')} ${c.dim('[--dry-run|--force]')} refresh framework files to this kit version
|
|
446
448
|
${c.g('status')} progress + whether the dashboard is running
|
|
447
449
|
|
|
448
450
|
${c.bold('Dashboard')}
|
|
@@ -476,11 +478,14 @@ const HELP = {
|
|
|
476
478
|
shims; override with ${c.g('--agent=claude,codex')}. Also wires ${c.bold('Playwright MCP')} into the
|
|
477
479
|
project's ${c.dim('.mcp.json')} (idempotent — never touches an existing entry).
|
|
478
480
|
${c.dim('An existing CLAUDE.md is preserved as CLAUDE.md.tomerge for you to merge on first run.')}`,
|
|
479
|
-
update: `${c.bold('spectoflow update')} ${c.dim('[--dry-run]')}\n
|
|
481
|
+
update: `${c.bold('spectoflow update')} ${c.dim('[--dry-run] [--force|-f]')}\n
|
|
480
482
|
Refresh framework-owned files (engine, dashboard, default agents & skills, AGENTS.md, policy…)
|
|
481
483
|
to this CLI's version, ${c.bold('preserving your work')}: config.json, workflow.md, specs/, plans/
|
|
482
484
|
and any agent/skill you edited are never overwritten (an edited file's new version lands as
|
|
483
|
-
${c.dim('*.new')} for you to merge). ${c.g('--dry-run')} previews without writing
|
|
485
|
+
${c.dim('*.new')} for you to merge). ${c.g('--dry-run')} previews without writing.
|
|
486
|
+
${c.g('--force')} (${c.g('-f')}) overwrites a diverged file in place instead of dropping a ${c.dim('*.new')}
|
|
487
|
+
— use it when you know you have no local edits worth keeping (e.g. a file stuck diverged from an
|
|
488
|
+
earlier update). It never touches config.json, workflow.md, specs/ or plans/.`,
|
|
484
489
|
dashboard: `${c.bold('spectoflow dashboard')} ${c.dim('[--port=NNNN] [status|stop|restart|create]')}\n
|
|
485
490
|
Start the local control plane in the ${c.bold('background')} (default ${c.dim('4319')} or
|
|
486
491
|
${c.dim('$SPECTOFLOW_PORT')}) and hand the prompt back. Subcommands:
|
package/lib/adapters.js
CHANGED
|
@@ -67,6 +67,21 @@ Argument: \`$ARGUMENTS\`
|
|
|
67
67
|
`;
|
|
68
68
|
|
|
69
69
|
// Priority order = which agent becomes the default when several are detected.
|
|
70
|
+
//
|
|
71
|
+
// `headless: false` marks an agent spectoflow can DETECT and let you set as active, but never spawn
|
|
72
|
+
// itself — no confirmed non-interactive one-shot mode (prompt as trailing arg → stdout → exit) as of
|
|
73
|
+
// this research pass, so `runner` is null on purpose (a fabricated command would just fail silently
|
|
74
|
+
// for real users). The dashboard still lists it and shows why Run/Orchestrate/Summarize are disabled
|
|
75
|
+
// for it, rather than hiding it — you can drive it yourself in its own terminal meanwhile, and
|
|
76
|
+
// `config.json → runners` is still yours to hand-wire a command into if one ships later.
|
|
77
|
+
//
|
|
78
|
+
// Left out of KNOWN_AGENTS entirely (not even headless:false) — researched, not re-added on a whim:
|
|
79
|
+
// DeepSeek Harness (deepseek-ai/deepseek-harness). It has no single installable binary to detect —
|
|
80
|
+
// it's invoked as `npx @deepseek-ai/dsh <subcommand>`, is explicitly a local WEB-APP framework (not a
|
|
81
|
+
// terminal chat/prompt tool like the others), and ships as an August-2026 "developer preview" whose
|
|
82
|
+
// APIs the project itself says will change. Detecting "is npx present" would not mean deepseek-harness
|
|
83
|
+
// is; it doesn't fit this registry's detection model. Revisit once it (or Kimi) publishes a real
|
|
84
|
+
// one-shot flag, or wire a custom command into config.json → runners by hand in the meantime.
|
|
70
85
|
const REGISTRY = [
|
|
71
86
|
{
|
|
72
87
|
id: 'claude',
|
|
@@ -75,26 +90,58 @@ const REGISTRY = [
|
|
|
75
90
|
{ path: '.claude/commands/spectoflow.md', content: SLASH_CMD },
|
|
76
91
|
],
|
|
77
92
|
runner: 'claude -p --permission-mode acceptEdits',
|
|
93
|
+
headless: true,
|
|
78
94
|
detect: { bin: 'claude', dirs: ['.claude'] },
|
|
79
95
|
},
|
|
80
96
|
{
|
|
81
97
|
id: 'codex',
|
|
82
98
|
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
83
99
|
runner: 'codex exec',
|
|
100
|
+
headless: true,
|
|
84
101
|
detect: { bin: 'codex', dirs: ['.codex'] },
|
|
85
102
|
},
|
|
86
103
|
{
|
|
87
104
|
id: 'cursor',
|
|
88
105
|
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
89
106
|
runner: 'cursor-agent -p',
|
|
107
|
+
headless: true,
|
|
90
108
|
detect: { bin: 'cursor-agent', dirs: ['.cursor'] },
|
|
91
109
|
},
|
|
92
110
|
{
|
|
93
111
|
id: 'gemini',
|
|
94
112
|
entries: [{ path: 'GEMINI.md', content: GEMINI_MD }],
|
|
95
113
|
runner: 'gemini -p',
|
|
114
|
+
headless: true,
|
|
96
115
|
detect: { bin: 'gemini', dirs: ['.gemini'] },
|
|
97
116
|
},
|
|
117
|
+
{
|
|
118
|
+
id: 'opencode',
|
|
119
|
+
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
120
|
+
runner: 'opencode run --quiet',
|
|
121
|
+
headless: true,
|
|
122
|
+
detect: { bin: 'opencode', dirs: ['.opencode'] },
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'kiro',
|
|
126
|
+
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
127
|
+
runner: 'kiro-cli chat --no-interactive --trust-all-tools',
|
|
128
|
+
headless: true,
|
|
129
|
+
detect: { bin: 'kiro-cli', dirs: ['.kiro'] },
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'antigravity',
|
|
133
|
+
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
134
|
+
runner: 'agy -p',
|
|
135
|
+
headless: true,
|
|
136
|
+
detect: { bin: 'agy' },
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'kimi',
|
|
140
|
+
entries: [{ path: 'AGENTS.md', content: ROOT_AGENTS_MD }],
|
|
141
|
+
runner: null, // no confirmed non-interactive one-shot mode — see the note above REGISTRY
|
|
142
|
+
headless: false,
|
|
143
|
+
detect: { bin: 'kimi', dirs: [] },
|
|
144
|
+
},
|
|
98
145
|
];
|
|
99
146
|
|
|
100
147
|
const byId = (id) => REGISTRY.find((a) => a.id === id);
|
|
@@ -124,7 +171,7 @@ function defaultRunners(agents) {
|
|
|
124
171
|
const out = {};
|
|
125
172
|
for (const id of agents) {
|
|
126
173
|
const a = byId(id);
|
|
127
|
-
if (a) out[id] = a.runner;
|
|
174
|
+
if (a && a.runner) out[id] = a.runner; // headless:false agents (e.g. kimi) have no runner to seed
|
|
128
175
|
}
|
|
129
176
|
return out;
|
|
130
177
|
}
|
package/lib/update.js
CHANGED
|
@@ -20,18 +20,20 @@ function toDisk(sf, rel) {
|
|
|
20
20
|
return path.join(sf, rel.split('/').join(path.sep));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function runUpdate({ projectRoot, templatesDir, version, dryRun = false }) {
|
|
23
|
+
function runUpdate({ projectRoot, templatesDir, version, dryRun = false, force = false }) {
|
|
24
24
|
const sf = path.join(projectRoot, '.spectoflow');
|
|
25
25
|
const prev = manifest.readManifest(sf);
|
|
26
26
|
const report = {
|
|
27
27
|
fromVersion: prev ? prev.version : null,
|
|
28
28
|
toVersion: version,
|
|
29
29
|
dryRun,
|
|
30
|
+
force,
|
|
30
31
|
created: [],
|
|
31
32
|
refreshed: [],
|
|
32
33
|
newSidecar: [],
|
|
33
34
|
adopted: [],
|
|
34
35
|
unchanged: [],
|
|
36
|
+
forced: [],
|
|
35
37
|
};
|
|
36
38
|
const baseline = (prev && prev.files) || {};
|
|
37
39
|
const nextFiles = {}; // manifest to write after this run
|
|
@@ -64,6 +66,10 @@ function runUpdate({ projectRoot, templatesDir, version, dryRun = false }) {
|
|
|
64
66
|
write(diskPath, newBuf); // untouched framework file → safe to refresh
|
|
65
67
|
report.refreshed.push(rel);
|
|
66
68
|
nextFiles[rel] = newHash;
|
|
69
|
+
} else if (force) {
|
|
70
|
+
write(diskPath, newBuf); // --force: overwrite the diverged file too, no .new sidecar
|
|
71
|
+
report.forced.push(rel);
|
|
72
|
+
nextFiles[rel] = newHash;
|
|
67
73
|
} else {
|
|
68
74
|
write(diskPath + '.new', newBuf); // user-edited or legacy-divergent → offer, never overwrite
|
|
69
75
|
report.newSidecar.push(rel);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectoflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "Agent-agnostic spec-driven development framework + real-time local control plane. Markdown artifacts, intent router, workflow-by-scope.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spec-driven-development",
|
package/templates/README.md
CHANGED
|
@@ -28,16 +28,18 @@ sit at the project root and just point back here.
|
|
|
28
28
|
```
|
|
29
29
|
- **See what you got:** `spectoflow list` (agents, skills & workflow at a glance), or `spectoflow
|
|
30
30
|
agents` / `spectoflow skills` / `spectoflow workflow`. Append `-h` to any command for its help.
|
|
31
|
-
- **Change how it runs** in the dashboard's **
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
- **Change how it runs** in the dashboard's **Personalize** tab (the **active agent**, always shown
|
|
32
|
+
in the topbar too — switching is verified against what's actually installed before it activates —
|
|
33
|
+
plus autonomy mode, output language, and the dashboard **design**), or by editing `config.json`.
|
|
34
|
+
- **Extend spectoflow itself** from Personalize, further down the same tab: describe a project-specific dashboard,
|
|
34
35
|
skill, or agent (or hit **Auto** to have it propose candidates from your project), and it's generated
|
|
35
36
|
for you — a dashboard appears in the nav immediately, a skill/agent follows the same gold-standard
|
|
36
37
|
shape as the shipped ones and is marked `origin: user-generated`. Same thing from the terminal:
|
|
37
38
|
`spectoflow skill create "<description>"` / `agent create` / `dashboard create` (each also takes
|
|
38
39
|
`--auto`).
|
|
39
40
|
- **Update the framework** to a newer kit: `spectoflow update` (preserves your edits; a file you
|
|
40
|
-
changed is kept and its new version is written next to it as `*.new`
|
|
41
|
+
changed is kept and its new version is written next to it as `*.new` — `--force`/`-f` overwrites it
|
|
42
|
+
in place instead, once you're sure you have nothing worth keeping there).
|
|
41
43
|
|
|
42
44
|
## Where your work lives
|
|
43
45
|
|
|
@@ -95,7 +95,8 @@ function setChat(open){
|
|
|
95
95
|
$('#chat').setAttribute('aria-hidden', open?'false':'true');
|
|
96
96
|
$('#chatFab').classList.toggle('is-open',open);
|
|
97
97
|
try{ localStorage.setItem('spf-chat', open?'1':'0'); }catch{}
|
|
98
|
-
|
|
98
|
+
// Land where you can actually type, not wherever the log happened to be scrolled last.
|
|
99
|
+
if(open) setTimeout(()=>{ scrollChat($('#chatLog')); $('#runPrompt').focus(); },60);
|
|
99
100
|
}
|
|
100
101
|
// doRun/doOrchestrate default to the floating widget's textarea/select; the Chat tab has its own
|
|
101
102
|
// #tabRunPrompt/#tabRunAgent (an id can't be shared by two elements) and passes them explicitly —
|
|
@@ -114,6 +115,17 @@ async function doOrchestrate(promptEl){
|
|
|
114
115
|
promptEl.value='';
|
|
115
116
|
}
|
|
116
117
|
async function approve(decision){ await fetch('/api/orchestrate/approve',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({decision})}); }
|
|
118
|
+
// ---- chat context management: condense the log via the agent, or wipe it (Chat tab only — the
|
|
119
|
+
// floating widget stays "quick access", full controls live where there's room to read them) ----
|
|
120
|
+
async function summarizeChat(agentEl){
|
|
121
|
+
const agent=(agentEl||$('#tabRunAgent'))?.value;
|
|
122
|
+
flash();
|
|
123
|
+
await fetch('/api/chat/summarize',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent})});
|
|
124
|
+
}
|
|
125
|
+
async function clearChat(){
|
|
126
|
+
flash();
|
|
127
|
+
await fetch('/api/chat/clear',{method:'POST'});
|
|
128
|
+
}
|
|
117
129
|
async function patchTask(id,patch){ flash(); await fetch('/api/task/'+encodeURIComponent(id),{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(patch)}); }
|
|
118
130
|
async function addComment(id,text,action){ flash(); await fetch('/api/task/'+encodeURIComponent(id)+'/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text,action})}); }
|
|
119
131
|
async function toggleStep(name){ flash(); await fetch('/api/workflow/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name})}); }
|
|
@@ -128,11 +140,10 @@ function render(){
|
|
|
128
140
|
// in renderSettings() below (called on every render tick) and saved via saveSettings().
|
|
129
141
|
// agent select — widget (#runAgent) and Chat tab (#tabRunAgent) each get their own populated
|
|
130
142
|
// <select>, since an id can't be shared by two elements; same option list, same source of truth.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
143
|
+
// Shows every known agent (not just ones already in config.json → runners — a freshly-installed
|
|
144
|
+
// agent works the first time, runner.js falls back to the registry default), disabling any that
|
|
145
|
+
// isn't genuinely installed or can't run headless (e.g. kimi).
|
|
146
|
+
[$('#runAgent'),$('#tabRunAgent')].forEach(sel=>fillAgentSelect(sel,P.knownAgents||[],runnableAgentIds(),c.agent));
|
|
136
147
|
const s=SpectoStats.stats(P);
|
|
137
148
|
const meterFill=$('#globalMeterFill');
|
|
138
149
|
if(meterFill) meterFill.style.width=(s.pct||0)+'%';
|
|
@@ -701,6 +712,53 @@ function fillLangSelect(sel,lang){
|
|
|
701
712
|
}
|
|
702
713
|
function setLangSelect(lang){ fillLangSelect($('#setLang'),lang); fillLangSelect($('#topLang'),lang); }
|
|
703
714
|
function setModeSelects(mode){ [$('#setMode'),$('#topMode')].forEach(s=>{ if(s) s.value=mode; }); }
|
|
715
|
+
|
|
716
|
+
// ---- active agent: always visible (topbar #topAgent) + editable (Settings #setAgent). Never lets
|
|
717
|
+
// the user activate an agent that isn't actually installed — options for a not-installed known agent
|
|
718
|
+
// are present but disabled, and P.installedAgents itself comes from the server testing each agent's
|
|
719
|
+
// real CLI (bin on PATH, or the project's own config dir), not a guess.
|
|
720
|
+
// ids that can actually be SPAWNED right now: installed AND headless-capable (a known agent like
|
|
721
|
+
// kimi can be "installed" and even the active agent, but nothing can run it non-interactively).
|
|
722
|
+
function runnableAgentIds(){
|
|
723
|
+
const installed=P.installedAgents||[];
|
|
724
|
+
return (P.knownAgents||[]).filter(a=>a.headless&&installed.includes(a.id)).map(a=>a.id);
|
|
725
|
+
}
|
|
726
|
+
function fillAgentSelect(sel,known,installed,active){
|
|
727
|
+
if(!sel) return;
|
|
728
|
+
const empty=!installed.length;
|
|
729
|
+
sel.classList.toggle('is-empty',empty);
|
|
730
|
+
sel.disabled=empty;
|
|
731
|
+
if(empty){ sel.innerHTML=''; const o=document.createElement('option'); o.value=''; o.textContent=t('topbar.agent.none'); sel.append(o); return; }
|
|
732
|
+
const sig=known.map(a=>a.id+':'+(installed.includes(a.id)?1:0)).join(',');
|
|
733
|
+
if(sel.dataset.sig!==sig){
|
|
734
|
+
sel.innerHTML='';
|
|
735
|
+
known.forEach(a=>{
|
|
736
|
+
const o=document.createElement('option'); o.value=a.id; o.textContent=a.label;
|
|
737
|
+
if(!installed.includes(a.id)) o.disabled=true;
|
|
738
|
+
sel.append(o);
|
|
739
|
+
});
|
|
740
|
+
sel.dataset.sig=sig;
|
|
741
|
+
}
|
|
742
|
+
if(active) sel.value=active;
|
|
743
|
+
}
|
|
744
|
+
function setAgentSelects(){
|
|
745
|
+
const c=(P&&P.config)||{};
|
|
746
|
+
const known=P.knownAgents||[], installed=P.installedAgents||[];
|
|
747
|
+
fillAgentSelect($('#topAgent'),known,installed,c.agent);
|
|
748
|
+
fillAgentSelect($('#setAgent'),known,installed,c.agent);
|
|
749
|
+
const hint=$('#setAgentHint'); if(hint){ const empty=!installed.length; hint.hidden=!empty; hint.classList.toggle('is-empty',empty); if(empty) hint.textContent=t('topbar.agent.none'); }
|
|
750
|
+
}
|
|
751
|
+
function showAgentError(msg){
|
|
752
|
+
const hint=$('#setAgentHint'); if(!hint) return;
|
|
753
|
+
hint.hidden=false; hint.classList.add('is-empty'); hint.textContent=msg;
|
|
754
|
+
setTimeout(()=>{ if(P&&P.installedAgents&&P.installedAgents.length) hint.hidden=true; },4000);
|
|
755
|
+
}
|
|
756
|
+
async function saveAgent(id){
|
|
757
|
+
if(!id) return;
|
|
758
|
+
flash();
|
|
759
|
+
const r=await fetch('/api/settings',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent:id})});
|
|
760
|
+
if(!r.ok){ const body=await r.json().catch(()=>({})); showAgentError(body.error||t('topbar.agent.none')); setAgentSelects(); return; }
|
|
761
|
+
}
|
|
704
762
|
// ---- design skins (data-design) — switchable, persisted per viewer + as the project default ----
|
|
705
763
|
function currentDesign(){ return document.documentElement.getAttribute('data-design')||'console'; }
|
|
706
764
|
function applyDesign(id){ document.documentElement.setAttribute('data-design',id); try{ localStorage.setItem('spf-design',id); }catch{} }
|
|
@@ -710,6 +768,7 @@ function renderSettings(){
|
|
|
710
768
|
const c=(P&&P.config)||{};
|
|
711
769
|
setModeSelects(c.mode||'semi');
|
|
712
770
|
setLangSelect(c.language||'en');
|
|
771
|
+
setAgentSelects();
|
|
713
772
|
// design switcher — options from the DESIGNS registry (designs.js)
|
|
714
773
|
const dsel=$('#setDesign');
|
|
715
774
|
if(dsel){
|
|
@@ -883,9 +942,7 @@ function renderCustomize(){
|
|
|
883
942
|
const form=el('div','cz-form');
|
|
884
943
|
const ta=el('textarea','chat-ta'); ta.placeholder=t('customize.describePh');
|
|
885
944
|
const sel=el('select','chat-agent');
|
|
886
|
-
|
|
887
|
-
runners.forEach((k)=>{ const o=document.createElement('option'); o.value=k; o.textContent=k; sel.append(o); });
|
|
888
|
-
if(P.config&&P.config.agent) sel.value=P.config.agent;
|
|
945
|
+
fillAgentSelect(sel,P.knownAgents||[],runnableAgentIds(),P.config&&P.config.agent);
|
|
889
946
|
const actions=el('div','cz-form-actions');
|
|
890
947
|
const autoBtn=el('button','btn',t('customize.auto'));
|
|
891
948
|
const goBtn=el('button','btn primary',t('customize.generate'));
|
|
@@ -925,6 +982,10 @@ function navigateTab(tabId,push){
|
|
|
925
982
|
}
|
|
926
983
|
applyActiveTab();
|
|
927
984
|
closeNav(); // a tab pick closes the mobile menu
|
|
985
|
+
// Landing on Chat: go straight to where you can read the latest and type, not wherever the log
|
|
986
|
+
// was scrolled to last (only on an actual switch INTO the tab — applyActiveTab() alone runs on
|
|
987
|
+
// every SSE render tick too, and re-scrolling/re-focusing there would fight the user's typing).
|
|
988
|
+
if(tabId==='chat') setTimeout(()=>{ scrollChat($('#chatTabLog')); $('#tabRunPrompt').focus(); },60);
|
|
928
989
|
}
|
|
929
990
|
function closeNav(){ document.body.classList.remove('nav-open'); const nt=$('#navToggle'); if(nt) nt.setAttribute('aria-expanded','false'); }
|
|
930
991
|
|
|
@@ -1212,6 +1273,8 @@ $('#setLang').addEventListener('change',onLangSelectChange);
|
|
|
1212
1273
|
const topModeSel=$('#topMode'); if(topModeSel) topModeSel.addEventListener('change',onModeSelectChange);
|
|
1213
1274
|
const topLangSel=$('#topLang'); if(topLangSel) topLangSel.addEventListener('change',onLangSelectChange);
|
|
1214
1275
|
const setDesignSel=$('#setDesign'); if(setDesignSel) setDesignSel.addEventListener('change',()=>saveDesign(setDesignSel.value));
|
|
1276
|
+
const topAgentSel=$('#topAgent'); if(topAgentSel) topAgentSel.addEventListener('change',(e)=>saveAgent(e.target.value));
|
|
1277
|
+
const setAgentSel=$('#setAgent'); if(setAgentSel) setAgentSel.addEventListener('change',(e)=>saveAgent(e.target.value));
|
|
1215
1278
|
// theme
|
|
1216
1279
|
(function(){ const s=localStorage.getItem('spf-theme'); if(s)document.documentElement.setAttribute('data-theme',s);
|
|
1217
1280
|
$('#themeToggle').addEventListener('click',()=>{ const c=document.documentElement.getAttribute('data-theme'); const n=c==='dark'?'light':'dark'; document.documentElement.setAttribute('data-theme',n); localStorage.setItem('spf-theme',n); }); })();
|
|
@@ -1242,10 +1305,14 @@ $('#chatClose').addEventListener('click',()=> setChat(false));
|
|
|
1242
1305
|
try{ if(localStorage.getItem('spf-chat')==='1') setChat(true); }catch{}
|
|
1243
1306
|
$('#runBtn').addEventListener('click',()=>doRun());
|
|
1244
1307
|
$('#orchBtn').addEventListener('click',()=>doOrchestrate());
|
|
1308
|
+
$('#widgetSummarizeBtn').addEventListener('click',()=>summarizeChat($('#runAgent')));
|
|
1309
|
+
$('#widgetClearBtn').addEventListener('click',clearChat);
|
|
1245
1310
|
$('#runPrompt').addEventListener('keydown',e=>{ if((e.metaKey||e.ctrlKey)&&e.key==='Enter')doRun(); });
|
|
1246
1311
|
// Chat tab — same doRun/doOrchestrate/approve, its own textarea/select (#tabRunPrompt/#tabRunAgent)
|
|
1247
1312
|
$('#tabRunBtn').addEventListener('click',()=>doRun($('#tabRunPrompt'),$('#tabRunAgent')));
|
|
1248
1313
|
$('#tabOrchBtn').addEventListener('click',()=>doOrchestrate($('#tabRunPrompt')));
|
|
1314
|
+
$('#tabSummarizeBtn').addEventListener('click',()=>summarizeChat($('#tabRunAgent')));
|
|
1315
|
+
$('#tabClearBtn').addEventListener('click',clearChat);
|
|
1249
1316
|
$('#tabRunPrompt').addEventListener('keydown',e=>{ if((e.metaKey||e.ctrlKey)&&e.key==='Enter')doRun($('#tabRunPrompt'),$('#tabRunAgent')); });
|
|
1250
1317
|
$('#drawerClose').addEventListener('click',closeDrawer);
|
|
1251
1318
|
$('#drawerScrim').addEventListener('click',closeDrawer);
|
|
@@ -20,18 +20,20 @@
|
|
|
20
20
|
const I18N = {
|
|
21
21
|
en: {
|
|
22
22
|
'nav.board':'Board','nav.requests':'Requests','nav.attention':'Attention','nav.backlog':'Backlog',
|
|
23
|
-
'nav.workflow':'Workflow','nav.team':'Agents & Skills','nav.chat':'Chat','nav.info':'Info','nav.settings':'
|
|
23
|
+
'nav.workflow':'Workflow','nav.team':'Agents & Skills','nav.chat':'Chat','nav.info':'Info','nav.settings':'Personalize',
|
|
24
24
|
'status.todo':'To do','status.in_progress':'In progress','status.to_validate':'To validate',
|
|
25
25
|
'status.to_analyze':'To analyze','status.done':'Done','status.blocked':'Blocked',
|
|
26
26
|
'filter.all':'All','filter.open':'Open','filter.resolved':'Resolved',
|
|
27
|
-
'action.run':'Run','action.send':'Send','action.orchestrate':'Orchestrate','action.approve':'Approve',
|
|
27
|
+
'action.run':'Run','action.send':'Send','action.orchestrate':'Orchestrate','action.approve':'Approve','action.summarize':'Summarize','action.clear':'Clear',
|
|
28
28
|
'action.cancel':'Cancel','action.add':'Add','action.edit':'Edit','action.delete':'Delete',
|
|
29
29
|
'action.resolve':'Resolve','action.reopen':'Reopen','action.validateToTask':'Validate → task',
|
|
30
30
|
'action.addToAnalyze':'Add + to analyze',
|
|
31
31
|
'topbar.navToggle':'Toggle menu','topbar.run.title':'Open the run chat',
|
|
32
32
|
'topbar.theme.toggle':'Toggle light / dark theme','topbar.theme.toggle.title':'Toggle light / dark',
|
|
33
|
-
'topbar.mode.title':'Autonomy mode — change from here or in
|
|
34
|
-
'topbar.lang.title':'Output language — change from here or in
|
|
33
|
+
'topbar.mode.title':'Autonomy mode — change from here or in Personalize',
|
|
34
|
+
'topbar.lang.title':'Output language — change from here or in Personalize',
|
|
35
|
+
'topbar.agent.title':'Active agent — change from here or in Personalize',
|
|
36
|
+
'topbar.agent.none':'No agent found',
|
|
35
37
|
'board.filterPlaceholder':'Filter tasks…','board.expandAll':'Expand all','board.collapseAll':'Collapse all',
|
|
36
38
|
'board.expandAllTitle':'Expand or collapse all phases','board.viewList':'List','board.viewKanban':'Kanban',
|
|
37
39
|
'board.viewList.title':'Grouped by phase','board.viewKanban.title':'Columns by status',
|
|
@@ -88,7 +90,7 @@ en: {
|
|
|
88
90
|
'chat.tabSub':'Full conversation with the runner — the same run as the widget, more room to read it.',
|
|
89
91
|
'chat.idle':'Type a request — the agent runs headless in this project with full memory (<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.',
|
|
90
92
|
'chat.inputPlaceholder':'e.g. Add a login feature with email + password',
|
|
91
|
-
'chat.orchestrateTitle':'Walk the enabled workflow',
|
|
93
|
+
'chat.orchestrateTitle':'Walk the enabled workflow','chat.summarizeTitle':'Condense the recent activity into a summary','chat.clearTitle':'Clear the chat log',
|
|
92
94
|
'chat.warn':'⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files & run commands.',
|
|
93
95
|
'info.title':'Info','info.sub':'Project overview — configuration, counts, specs and active workflow.',
|
|
94
96
|
'info.project':'Project','info.projectType':'Project type','info.mode':'Mode','info.language':'Language',
|
|
@@ -107,7 +109,7 @@ en: {
|
|
|
107
109
|
'task.failingPassing':'{f} failing, {p} passing','task.comments':'Comments','task.noComments':'No comments.',
|
|
108
110
|
'task.addCommentPlaceholder':'Add a comment, a remark, feedback…',
|
|
109
111
|
'task.toAnalyzeHint':'"To analyze" moves the task back so the agent picks it up next round.',
|
|
110
|
-
'customize.title':'
|
|
112
|
+
'customize.title':'Extend spectoflow','customize.sub':'Add project-specific dashboards, skills and agents — describe what you want, or let Auto propose candidates from this project.',
|
|
111
113
|
'customize.dashboards':'Dashboards','customize.skills':'Skills','customize.agents':'Agents',
|
|
112
114
|
'customize.add.dashboard':'Add dashboard','customize.add.skill':'Add skill','customize.add.agent':'Add agent',
|
|
113
115
|
'customize.empty.dashboard':'No custom dashboards yet.','customize.empty.skill':'No custom skills yet.','customize.empty.agent':'No custom agents yet.',
|
|
@@ -115,18 +117,20 @@ en: {
|
|
|
115
117
|
},
|
|
116
118
|
fr: {
|
|
117
119
|
'nav.board':'Tableau','nav.requests':'Demandes','nav.attention':'Attention','nav.backlog':'Backlog',
|
|
118
|
-
'nav.workflow':'Workflow','nav.team':'Agents et compétences','nav.chat':'Chat','nav.info':'Infos','nav.settings':'
|
|
120
|
+
'nav.workflow':'Workflow','nav.team':'Agents et compétences','nav.chat':'Chat','nav.info':'Infos','nav.settings':'Personnalisation',
|
|
119
121
|
'status.todo':'À faire','status.in_progress':'En cours','status.to_validate':'À valider',
|
|
120
122
|
'status.to_analyze':'À analyser','status.done':'Terminé','status.blocked':'Bloqué',
|
|
121
123
|
'filter.all':'Tous','filter.open':'Ouverts','filter.resolved':'Résolus',
|
|
122
|
-
'action.run':'Lancer','action.send':'Envoyer','action.orchestrate':'Orchestrer','action.approve':'Approuver',
|
|
124
|
+
'action.run':'Lancer','action.send':'Envoyer','action.orchestrate':'Orchestrer','action.approve':'Approuver','action.summarize':'Résumer','action.clear':'Effacer',
|
|
123
125
|
'action.cancel':'Annuler','action.add':'Ajouter','action.edit':'Modifier','action.delete':'Supprimer',
|
|
124
126
|
'action.resolve':'Résoudre','action.reopen':'Rouvrir','action.validateToTask':'Valider → tâche',
|
|
125
127
|
'action.addToAnalyze':'Ajouter + à analyser',
|
|
126
128
|
'topbar.navToggle':'Afficher le menu','topbar.run.title':'Ouvrir le chat d’exécution',
|
|
127
129
|
'topbar.theme.toggle':'Basculer thème clair / sombre','topbar.theme.toggle.title':'Basculer clair / sombre',
|
|
128
|
-
'topbar.mode.title':'Mode d’autonomie — modifiable ici ou dans
|
|
129
|
-
'topbar.lang.title':'Langue de sortie — modifiable ici ou dans
|
|
130
|
+
'topbar.mode.title':'Mode d’autonomie — modifiable ici ou dans Personnalisation',
|
|
131
|
+
'topbar.lang.title':'Langue de sortie — modifiable ici ou dans Personnalisation',
|
|
132
|
+
'topbar.agent.title':'Agent actif — modifiable ici ou dans Personnalisation',
|
|
133
|
+
'topbar.agent.none':'Aucun agent trouvé',
|
|
130
134
|
'board.filterPlaceholder':'Filtrer les tâches…','board.expandAll':'Tout déplier','board.collapseAll':'Tout replier',
|
|
131
135
|
'board.expandAllTitle':'Déplier ou replier toutes les phases','board.viewList':'Liste','board.viewKanban':'Kanban',
|
|
132
136
|
'board.viewList.title':'Regroupé par phase','board.viewKanban.title':'Colonnes par statut',
|
|
@@ -183,7 +187,7 @@ fr: {
|
|
|
183
187
|
'chat.tabSub':'Conversation complète avec l’exécuteur — la même exécution que le widget, avec plus de place pour la lire.',
|
|
184
188
|
'chat.idle':'Tapez une demande — l’agent s’exécute sans supervision dans ce projet avec toute sa mémoire (<code>CLAUDE.md → AGENTS.md</code>) et met le tableau à jour en direct.',
|
|
185
189
|
'chat.inputPlaceholder':'ex. Ajouter une fonctionnalité de connexion par email + mot de passe',
|
|
186
|
-
'chat.orchestrateTitle':'Parcourir le workflow activé',
|
|
190
|
+
'chat.orchestrateTitle':'Parcourir le workflow activé','chat.summarizeTitle':'Condenser l’activité récente en un résumé','chat.clearTitle':'Effacer le journal de discussion',
|
|
187
191
|
'chat.warn':'⚠ Lance un agent réel (<code>config.json → runners</code>) qui peut modifier des fichiers et exécuter des commandes.',
|
|
188
192
|
'info.title':'Infos','info.sub':'Vue d’ensemble du projet — configuration, comptages, specs et workflow actif.',
|
|
189
193
|
'info.project':'Projet','info.projectType':'Type de projet','info.mode':'Mode','info.language':'Langue',
|
|
@@ -202,7 +206,7 @@ fr: {
|
|
|
202
206
|
'task.failingPassing':'{f} en échec, {p} réussi(s)','task.comments':'Commentaires','task.noComments':'Aucun commentaire.',
|
|
203
207
|
'task.addCommentPlaceholder':'Ajouter un commentaire, une remarque, un retour…',
|
|
204
208
|
'task.toAnalyzeHint':'« À analyser » renvoie la tâche pour que l’agent la reprenne au tour suivant.',
|
|
205
|
-
'customize.title':'
|
|
209
|
+
'customize.title':'Étendre spectoflow','customize.sub':'Ajoutez des dashboards, compétences et agents propres au projet — décrivez ce que vous voulez, ou laissez Auto proposer des pistes à partir de ce projet.',
|
|
206
210
|
'customize.dashboards':'Dashboards','customize.skills':'Compétences','customize.agents':'Agents',
|
|
207
211
|
'customize.add.dashboard':'Ajouter un dashboard','customize.add.skill':'Ajouter une compétence','customize.add.agent':'Ajouter un agent',
|
|
208
212
|
'customize.empty.dashboard':'Aucun dashboard personnalisé pour l’instant.','customize.empty.skill':'Aucune compétence personnalisée pour l’instant.','customize.empty.agent':'Aucun agent personnalisé pour l’instant.',
|
|
@@ -210,18 +214,20 @@ fr: {
|
|
|
210
214
|
},
|
|
211
215
|
es: {
|
|
212
216
|
'nav.board':'Tablero','nav.requests':'Solicitudes','nav.attention':'Atención','nav.backlog':'Backlog',
|
|
213
|
-
'nav.workflow':'Workflow','nav.team':'Agentes y habilidades','nav.chat':'Chat','nav.info':'Info','nav.settings':'
|
|
217
|
+
'nav.workflow':'Workflow','nav.team':'Agentes y habilidades','nav.chat':'Chat','nav.info':'Info','nav.settings':'Personalizar',
|
|
214
218
|
'status.todo':'Por hacer','status.in_progress':'En curso','status.to_validate':'Por validar',
|
|
215
219
|
'status.to_analyze':'Por analizar','status.done':'Hecho','status.blocked':'Bloqueado',
|
|
216
220
|
'filter.all':'Todos','filter.open':'Abiertos','filter.resolved':'Resueltos',
|
|
217
|
-
'action.run':'Ejecutar','action.send':'Enviar','action.orchestrate':'Orquestar','action.approve':'Aprobar',
|
|
221
|
+
'action.run':'Ejecutar','action.send':'Enviar','action.orchestrate':'Orquestar','action.approve':'Aprobar','action.summarize':'Resumir','action.clear':'Borrar',
|
|
218
222
|
'action.cancel':'Cancelar','action.add':'Añadir','action.edit':'Editar','action.delete':'Eliminar',
|
|
219
223
|
'action.resolve':'Resolver','action.reopen':'Reabrir','action.validateToTask':'Validar → tarea',
|
|
220
224
|
'action.addToAnalyze':'Añadir + por analizar',
|
|
221
225
|
'topbar.navToggle':'Mostrar menú','topbar.run.title':'Abrir el chat de ejecución',
|
|
222
226
|
'topbar.theme.toggle':'Cambiar tema claro / oscuro','topbar.theme.toggle.title':'Cambiar claro / oscuro',
|
|
223
|
-
'topbar.mode.title':'Modo de autonomía — se cambia aquí o en
|
|
224
|
-
'topbar.lang.title':'Idioma de salida — se cambia aquí o en
|
|
227
|
+
'topbar.mode.title':'Modo de autonomía — se cambia aquí o en Personalizar',
|
|
228
|
+
'topbar.lang.title':'Idioma de salida — se cambia aquí o en Personalizar',
|
|
229
|
+
'topbar.agent.title':'Agente activo — se cambia aquí o en Personalizar',
|
|
230
|
+
'topbar.agent.none':'No se encontró ningún agente',
|
|
225
231
|
'board.filterPlaceholder':'Filtrar tareas…','board.expandAll':'Expandir todo','board.collapseAll':'Colapsar todo',
|
|
226
232
|
'board.expandAllTitle':'Expandir o colapsar todas las fases','board.viewList':'Lista','board.viewKanban':'Kanban',
|
|
227
233
|
'board.viewList.title':'Agrupado por fase','board.viewKanban.title':'Columnas por estado',
|
|
@@ -278,7 +284,7 @@ es: {
|
|
|
278
284
|
'chat.tabSub':'Conversación completa con el ejecutor — la misma ejecución que el widget, con más espacio para leerla.',
|
|
279
285
|
'chat.idle':'Escribe una solicitud — el agente se ejecuta sin supervisión en este proyecto con toda su memoria (<code>CLAUDE.md → AGENTS.md</code>) y actualiza el tablero en vivo.',
|
|
280
286
|
'chat.inputPlaceholder':'p. ej. Añadir un inicio de sesión con email + contraseña',
|
|
281
|
-
'chat.orchestrateTitle':'Recorrer el workflow activado',
|
|
287
|
+
'chat.orchestrateTitle':'Recorrer el workflow activado','chat.summarizeTitle':'Condensar la actividad reciente en un resumen','chat.clearTitle':'Borrar el registro del chat',
|
|
282
288
|
'chat.warn':'⚠ Lanza un agente real (<code>config.json → runners</code>) que puede modificar archivos y ejecutar comandos.',
|
|
283
289
|
'info.title':'Info','info.sub':'Visión general del proyecto — configuración, recuentos, specs y workflow activo.',
|
|
284
290
|
'info.project':'Proyecto','info.projectType':'Tipo de proyecto','info.mode':'Modo','info.language':'Idioma',
|
|
@@ -297,7 +303,7 @@ es: {
|
|
|
297
303
|
'task.failingPassing':'{f} fallando, {p} superadas','task.comments':'Comentarios','task.noComments':'Sin comentarios.',
|
|
298
304
|
'task.addCommentPlaceholder':'Añade un comentario, una observación, feedback…',
|
|
299
305
|
'task.toAnalyzeHint':'«Por analizar» devuelve la tarea para que el agente la retome en la siguiente ronda.',
|
|
300
|
-
'customize.title':'
|
|
306
|
+
'customize.title':'Ampliar spectoflow','customize.sub':'Añade dashboards, habilidades y agentes propios del proyecto — describe lo que quieres, o deja que Auto proponga candidatos a partir de este proyecto.',
|
|
301
307
|
'customize.dashboards':'Dashboards','customize.skills':'Habilidades','customize.agents':'Agentes',
|
|
302
308
|
'customize.add.dashboard':'Añadir dashboard','customize.add.skill':'Añadir habilidad','customize.add.agent':'Añadir agente',
|
|
303
309
|
'customize.empty.dashboard':'Aún no hay dashboards personalizados.','customize.empty.skill':'Aún no hay habilidades personalizadas.','customize.empty.agent':'Aún no hay agentes personalizados.',
|
|
@@ -305,18 +311,20 @@ es: {
|
|
|
305
311
|
},
|
|
306
312
|
de: {
|
|
307
313
|
'nav.board':'Board','nav.requests':'Anfragen','nav.attention':'Hinweise','nav.backlog':'Backlog',
|
|
308
|
-
'nav.workflow':'Workflow','nav.team':'Agenten & Skills','nav.chat':'Chat','nav.info':'Info','nav.settings':'
|
|
314
|
+
'nav.workflow':'Workflow','nav.team':'Agenten & Skills','nav.chat':'Chat','nav.info':'Info','nav.settings':'Personalisieren',
|
|
309
315
|
'status.todo':'Offen','status.in_progress':'In Arbeit','status.to_validate':'Zu prüfen',
|
|
310
316
|
'status.to_analyze':'Zu analysieren','status.done':'Erledigt','status.blocked':'Blockiert',
|
|
311
317
|
'filter.all':'Alle','filter.open':'Offen','filter.resolved':'Erledigt',
|
|
312
|
-
'action.run':'Start','action.send':'Senden','action.orchestrate':'Orchestrieren','action.approve':'Freigeben',
|
|
318
|
+
'action.run':'Start','action.send':'Senden','action.orchestrate':'Orchestrieren','action.approve':'Freigeben','action.summarize':'Zusammenfassen','action.clear':'Löschen',
|
|
313
319
|
'action.cancel':'Abbrechen','action.add':'Hinzufügen','action.edit':'Bearbeiten','action.delete':'Löschen',
|
|
314
320
|
'action.resolve':'Erledigen','action.reopen':'Wieder öffnen','action.validateToTask':'Bestätigen → Aufgabe',
|
|
315
321
|
'action.addToAnalyze':'Hinzufügen + zu analysieren',
|
|
316
322
|
'topbar.navToggle':'Menü anzeigen','topbar.run.title':'Ausführungs-Chat öffnen',
|
|
317
323
|
'topbar.theme.toggle':'Hell-/Dunkelmodus umschalten','topbar.theme.toggle.title':'Hell / Dunkel umschalten',
|
|
318
|
-
'topbar.mode.title':'Autonomiemodus — hier oder in
|
|
319
|
-
'topbar.lang.title':'Ausgabesprache — hier oder in
|
|
324
|
+
'topbar.mode.title':'Autonomiemodus — hier oder in Personalisieren änderbar',
|
|
325
|
+
'topbar.lang.title':'Ausgabesprache — hier oder in Personalisieren änderbar',
|
|
326
|
+
'topbar.agent.title':'Aktiver Agent — hier oder in Personalisieren änderbar',
|
|
327
|
+
'topbar.agent.none':'Kein Agent gefunden',
|
|
320
328
|
'board.filterPlaceholder':'Aufgaben filtern…','board.expandAll':'Alle ausklappen','board.collapseAll':'Alle einklappen',
|
|
321
329
|
'board.expandAllTitle':'Alle Phasen ein- oder ausklappen','board.viewList':'Liste','board.viewKanban':'Kanban',
|
|
322
330
|
'board.viewList.title':'Nach Phase gruppiert','board.viewKanban.title':'Spalten nach Status',
|
|
@@ -373,7 +381,7 @@ de: {
|
|
|
373
381
|
'chat.tabSub':'Vollständiges Gespräch mit dem Runner — derselbe Lauf wie im Widget, mit mehr Platz zum Lesen.',
|
|
374
382
|
'chat.idle':'Geben Sie eine Anfrage ein — der Agent läuft eigenständig in diesem Projekt mit vollem Gedächtnis (<code>CLAUDE.md → AGENTS.md</code>) und aktualisiert das Board live.',
|
|
375
383
|
'chat.inputPlaceholder':'z. B. Login mit E-Mail + Passwort hinzufügen',
|
|
376
|
-
'chat.orchestrateTitle':'Den aktivierten Workflow durchlaufen',
|
|
384
|
+
'chat.orchestrateTitle':'Den aktivierten Workflow durchlaufen','chat.summarizeTitle':'Die letzten Aktivitäten zu einer Zusammenfassung verdichten','chat.clearTitle':'Chat-Verlauf löschen',
|
|
377
385
|
'chat.warn':'⚠ Startet einen echten Agenten (<code>config.json → runners</code>), der Dateien ändern und Befehle ausführen kann.',
|
|
378
386
|
'info.title':'Info','info.sub':'Projektübersicht — Konfiguration, Zahlen, Specs und aktiver Workflow.',
|
|
379
387
|
'info.project':'Projekt','info.projectType':'Projekttyp','info.mode':'Modus','info.language':'Sprache',
|
|
@@ -392,7 +400,7 @@ de: {
|
|
|
392
400
|
'task.failingPassing':'{f} fehlgeschlagen, {p} bestanden','task.comments':'Kommentare','task.noComments':'Keine Kommentare.',
|
|
393
401
|
'task.addCommentPlaceholder':'Kommentar, Anmerkung oder Feedback hinzufügen…',
|
|
394
402
|
'task.toAnalyzeHint':'„Zu analysieren“ gibt die Aufgabe zurück, damit der Agent sie in der nächsten Runde aufgreift.',
|
|
395
|
-
'customize.title':'
|
|
403
|
+
'customize.title':'spectoflow erweitern','customize.sub':'Fügen Sie projektspezifische Dashboards, Skills und Agenten hinzu — beschreiben Sie, was Sie wollen, oder lassen Sie Auto Kandidaten aus diesem Projekt vorschlagen.',
|
|
396
404
|
'customize.dashboards':'Dashboards','customize.skills':'Skills','customize.agents':'Agenten',
|
|
397
405
|
'customize.add.dashboard':'Dashboard hinzufügen','customize.add.skill':'Skill hinzufügen','customize.add.agent':'Agent hinzufügen',
|
|
398
406
|
'customize.empty.dashboard':'Noch keine eigenen Dashboards.','customize.empty.skill':'Noch keine eigenen Skills.','customize.empty.agent':'Noch keine eigenen Agenten.',
|
|
@@ -400,18 +408,20 @@ de: {
|
|
|
400
408
|
},
|
|
401
409
|
pt: {
|
|
402
410
|
'nav.board':'Painel','nav.requests':'Pedidos','nav.attention':'Atenção','nav.backlog':'Backlog',
|
|
403
|
-
'nav.workflow':'Workflow','nav.team':'Agentes e habilidades','nav.chat':'Chat','nav.info':'Info','nav.settings':'
|
|
411
|
+
'nav.workflow':'Workflow','nav.team':'Agentes e habilidades','nav.chat':'Chat','nav.info':'Info','nav.settings':'Personalizar',
|
|
404
412
|
'status.todo':'A fazer','status.in_progress':'Em curso','status.to_validate':'Por validar',
|
|
405
413
|
'status.to_analyze':'Por analisar','status.done':'Concluído','status.blocked':'Bloqueado',
|
|
406
414
|
'filter.all':'Todos','filter.open':'Abertos','filter.resolved':'Resolvidos',
|
|
407
|
-
'action.run':'Executar','action.send':'Enviar','action.orchestrate':'Orquestrar','action.approve':'Aprovar',
|
|
415
|
+
'action.run':'Executar','action.send':'Enviar','action.orchestrate':'Orquestrar','action.approve':'Aprovar','action.summarize':'Resumir','action.clear':'Limpar',
|
|
408
416
|
'action.cancel':'Cancelar','action.add':'Adicionar','action.edit':'Editar','action.delete':'Eliminar',
|
|
409
417
|
'action.resolve':'Resolver','action.reopen':'Reabrir','action.validateToTask':'Validar → tarefa',
|
|
410
418
|
'action.addToAnalyze':'Adicionar + por analisar',
|
|
411
419
|
'topbar.navToggle':'Mostrar menu','topbar.run.title':'Abrir o chat de execução',
|
|
412
420
|
'topbar.theme.toggle':'Alternar tema claro / escuro','topbar.theme.toggle.title':'Alternar claro / escuro',
|
|
413
|
-
'topbar.mode.title':'Modo de autonomia — altere aqui ou em
|
|
414
|
-
'topbar.lang.title':'Idioma de saída — altere aqui ou em
|
|
421
|
+
'topbar.mode.title':'Modo de autonomia — altere aqui ou em Personalizar',
|
|
422
|
+
'topbar.lang.title':'Idioma de saída — altere aqui ou em Personalizar',
|
|
423
|
+
'topbar.agent.title':'Agente ativo — altere aqui ou em Personalizar',
|
|
424
|
+
'topbar.agent.none':'Nenhum agente encontrado',
|
|
415
425
|
'board.filterPlaceholder':'Filtrar tarefas…','board.expandAll':'Expandir tudo','board.collapseAll':'Recolher tudo',
|
|
416
426
|
'board.expandAllTitle':'Expandir ou recolher todas as fases','board.viewList':'Lista','board.viewKanban':'Kanban',
|
|
417
427
|
'board.viewList.title':'Agrupado por fase','board.viewKanban.title':'Colunas por estado',
|
|
@@ -468,7 +478,7 @@ pt: {
|
|
|
468
478
|
'chat.tabSub':'Conversa completa com o executor — a mesma execução do widget, com mais espaço para ler.',
|
|
469
479
|
'chat.idle':'Escreva um pedido — o agente corre sem supervisão neste projeto com toda a sua memória (<code>CLAUDE.md → AGENTS.md</code>) e atualiza o painel em direto.',
|
|
470
480
|
'chat.inputPlaceholder':'ex. Adicionar login com email + palavra-passe',
|
|
471
|
-
'chat.orchestrateTitle':'Percorrer o workflow ativado',
|
|
481
|
+
'chat.orchestrateTitle':'Percorrer o workflow ativado','chat.summarizeTitle':'Condensar a atividade recente num resumo','chat.clearTitle':'Limpar o registo do chat',
|
|
472
482
|
'chat.warn':'⚠ Inicia um agente real (<code>config.json → runners</code>) que pode alterar ficheiros e executar comandos.',
|
|
473
483
|
'info.title':'Info','info.sub':'Visão geral do projeto — configuração, contagens, specs e workflow ativo.',
|
|
474
484
|
'info.project':'Projeto','info.projectType':'Tipo de projeto','info.mode':'Modo','info.language':'Idioma',
|
|
@@ -487,7 +497,7 @@ pt: {
|
|
|
487
497
|
'task.failingPassing':'{f} a falhar, {p} bem-sucedido(s)','task.comments':'Comentários','task.noComments':'Sem comentários.',
|
|
488
498
|
'task.addCommentPlaceholder':'Adicione um comentário, uma observação, feedback…',
|
|
489
499
|
'task.toAnalyzeHint':'«Por analisar» devolve a tarefa para o agente a retomar na ronda seguinte.',
|
|
490
|
-
'customize.title':'
|
|
500
|
+
'customize.title':'Estender o spectoflow','customize.sub':'Adicione dashboards, habilidades e agentes específicos do projeto — descreva o que quer, ou deixe o Auto propor candidatos a partir deste projeto.',
|
|
491
501
|
'customize.dashboards':'Dashboards','customize.skills':'Habilidades','customize.agents':'Agentes',
|
|
492
502
|
'customize.add.dashboard':'Adicionar dashboard','customize.add.skill':'Adicionar habilidade','customize.add.agent':'Adicionar agente',
|
|
493
503
|
'customize.empty.dashboard':'Ainda sem dashboards personalizados.','customize.empty.skill':'Ainda sem habilidades personalizadas.','customize.empty.agent':'Ainda sem agentes personalizados.',
|
|
@@ -495,18 +505,20 @@ pt: {
|
|
|
495
505
|
},
|
|
496
506
|
it: {
|
|
497
507
|
'nav.board':'Bacheca','nav.requests':'Richieste','nav.attention':'Attenzione','nav.backlog':'Backlog',
|
|
498
|
-
'nav.workflow':'Workflow','nav.team':'Agenti e competenze','nav.chat':'Chat','nav.info':'Info','nav.settings':'
|
|
508
|
+
'nav.workflow':'Workflow','nav.team':'Agenti e competenze','nav.chat':'Chat','nav.info':'Info','nav.settings':'Personalizza',
|
|
499
509
|
'status.todo':'Da fare','status.in_progress':'In corso','status.to_validate':'Da convalidare',
|
|
500
510
|
'status.to_analyze':'Da analizzare','status.done':'Fatto','status.blocked':'Bloccato',
|
|
501
511
|
'filter.all':'Tutti','filter.open':'Aperti','filter.resolved':'Risolti',
|
|
502
|
-
'action.run':'Avvia','action.send':'Invia','action.orchestrate':'Orchestra','action.approve':'Approva',
|
|
512
|
+
'action.run':'Avvia','action.send':'Invia','action.orchestrate':'Orchestra','action.approve':'Approva','action.summarize':'Riassumi','action.clear':'Cancella',
|
|
503
513
|
'action.cancel':'Annulla','action.add':'Aggiungi','action.edit':'Modifica','action.delete':'Elimina',
|
|
504
514
|
'action.resolve':'Risolvi','action.reopen':'Riapri','action.validateToTask':'Convalida → attività',
|
|
505
515
|
'action.addToAnalyze':'Aggiungi + da analizzare',
|
|
506
516
|
'topbar.navToggle':'Mostra menu','topbar.run.title':'Apri la chat di esecuzione',
|
|
507
517
|
'topbar.theme.toggle':'Passa a tema chiaro / scuro','topbar.theme.toggle.title':'Chiaro / scuro',
|
|
508
|
-
'topbar.mode.title':'Modalità di autonomia — modificabile qui o
|
|
509
|
-
'topbar.lang.title':'Lingua di output — modificabile qui o
|
|
518
|
+
'topbar.mode.title':'Modalità di autonomia — modificabile qui o in Personalizza',
|
|
519
|
+
'topbar.lang.title':'Lingua di output — modificabile qui o in Personalizza',
|
|
520
|
+
'topbar.agent.title':'Agente attivo — modificabile qui o in Personalizza',
|
|
521
|
+
'topbar.agent.none':'Nessun agente trovato',
|
|
510
522
|
'board.filterPlaceholder':'Filtra attività…','board.expandAll':'Espandi tutto','board.collapseAll':'Comprimi tutto',
|
|
511
523
|
'board.expandAllTitle':'Espandi o comprimi tutte le fasi','board.viewList':'Elenco','board.viewKanban':'Kanban',
|
|
512
524
|
'board.viewList.title':'Raggruppato per fase','board.viewKanban.title':'Colonne per stato',
|
|
@@ -563,7 +575,7 @@ it: {
|
|
|
563
575
|
'chat.tabSub':'Conversazione completa con l’esecutore — la stessa esecuzione del widget, con più spazio per leggerla.',
|
|
564
576
|
'chat.idle':'Digita una richiesta — l’agente viene eseguito senza supervisione in questo progetto con tutta la sua memoria (<code>CLAUDE.md → AGENTS.md</code>) e aggiorna la bacheca in diretta.',
|
|
565
577
|
'chat.inputPlaceholder':'es. Aggiungi un login con email + password',
|
|
566
|
-
'chat.orchestrateTitle':'Percorri il workflow attivato',
|
|
578
|
+
'chat.orchestrateTitle':'Percorri il workflow attivato','chat.summarizeTitle':'Condensa l’attività recente in un riassunto','chat.clearTitle':'Cancella il registro della chat',
|
|
567
579
|
'chat.warn':'⚠ Avvia un agente reale (<code>config.json → runners</code>) che può modificare file ed eseguire comandi.',
|
|
568
580
|
'info.title':'Info','info.sub':'Panoramica del progetto — configurazione, conteggi, specs e workflow attivo.',
|
|
569
581
|
'info.project':'Progetto','info.projectType':'Tipo di progetto','info.mode':'Modalità','info.language':'Lingua',
|
|
@@ -582,7 +594,7 @@ it: {
|
|
|
582
594
|
'task.failingPassing':'{f} falliti, {p} superati','task.comments':'Commenti','task.noComments':'Nessun commento.',
|
|
583
595
|
'task.addCommentPlaceholder':'Aggiungi un commento, un’osservazione, un feedback…',
|
|
584
596
|
'task.toAnalyzeHint':'«Da analizzare» rimanda l’attività così l’agente la riprende al giro successivo.',
|
|
585
|
-
'customize.title':'
|
|
597
|
+
'customize.title':'Estendi spectoflow','customize.sub':'Aggiungi dashboard, skill e agenti specifici del progetto — descrivi cosa vuoi, oppure lascia che Auto proponga candidati a partire da questo progetto.',
|
|
586
598
|
'customize.dashboards':'Dashboard','customize.skills':'Skill','customize.agents':'Agenti',
|
|
587
599
|
'customize.add.dashboard':'Aggiungi dashboard','customize.add.skill':'Aggiungi skill','customize.add.agent':'Aggiungi agente',
|
|
588
600
|
'customize.empty.dashboard':'Ancora nessuna dashboard personalizzata.','customize.empty.skill':'Ancora nessuna skill personalizzata.','customize.empty.agent':'Ancora nessun agente personalizzato.',
|
|
@@ -26,13 +26,15 @@
|
|
|
26
26
|
<span class="brand-project" id="projectName">—</span>
|
|
27
27
|
</div>
|
|
28
28
|
<div class="brand-sub" id="brandSub">
|
|
29
|
-
<select id="
|
|
29
|
+
<select id="topAgent" class="brand-mini-select brand-agent-select" data-i18n-title="topbar.agent.title" data-i18n-aria="info.activeAgent" aria-label="Active agent" title="Active agent — change from here or in Personalize"></select>
|
|
30
|
+
<span class="brand-sub-sep">·</span>
|
|
31
|
+
<select id="topMode" class="brand-mini-select" data-i18n-title="topbar.mode.title" data-i18n-aria="field.mode" aria-label="Autonomy mode" title="Autonomy mode — change from here or in Personalize">
|
|
30
32
|
<option value="autopilot">autopilot</option>
|
|
31
33
|
<option value="semi">semi</option>
|
|
32
34
|
<option value="manual">manual</option>
|
|
33
35
|
</select>
|
|
34
36
|
<span class="brand-sub-sep">·</span>
|
|
35
|
-
<select id="topLang" class="brand-mini-select" data-i18n-title="topbar.lang.title" data-i18n-aria="field.lang" aria-label="Output language" title="Output language — change from here or in
|
|
37
|
+
<select id="topLang" class="brand-mini-select" data-i18n-title="topbar.lang.title" data-i18n-aria="field.lang" aria-label="Output language" title="Output language — change from here or in Personalize">
|
|
36
38
|
<option value="en">en</option>
|
|
37
39
|
<option value="fr">fr</option>
|
|
38
40
|
<option value="es">es</option>
|
|
@@ -47,15 +49,15 @@
|
|
|
47
49
|
</div>
|
|
48
50
|
</div>
|
|
49
51
|
<nav class="tabs" id="tabs">
|
|
50
|
-
<button class="tab is-active" data-tab="board"><span class="tab-ico" data-icon="board"></span><span class="tab-label" data-i18n="nav.board">Board</span></button>
|
|
51
|
-
<button class="tab" data-tab="chat"><span class="tab-ico" data-icon="chat"></span><span class="tab-label" data-i18n="nav.chat">Chat</span></button>
|
|
52
|
-
<button class="tab" data-tab="requests"><span class="tab-ico" data-icon="requests"></span><span class="tab-label" data-i18n="nav.requests">Requests</span></button>
|
|
53
|
-
<button class="tab" data-tab="attention"><span class="tab-ico" data-icon="attention"></span><span class="tab-label" data-i18n="nav.attention">Attention</span><span class="tab-badge" id="attnBadge" hidden>0</span></button>
|
|
54
|
-
<button class="tab" data-tab="backlog"><span class="tab-ico" data-icon="backlog"></span><span class="tab-label" data-i18n="nav.backlog">Backlog</span></button>
|
|
55
|
-
<button class="tab" data-tab="workflow"><span class="tab-ico" data-icon="workflow"></span><span class="tab-label" data-i18n="nav.workflow">Workflow</span></button>
|
|
56
|
-
<button class="tab" data-tab="team"><span class="tab-ico" data-icon="agents"></span><span class="tab-label" data-i18n="nav.team">Agents & Skills</span></button>
|
|
57
|
-
<button class="tab" data-tab="info"><span class="tab-ico" data-icon="info"></span><span class="tab-label" data-i18n="nav.info">Info</span></button>
|
|
58
|
-
<button class="tab" data-tab="settings"><span class="tab-ico" data-icon="settings"></span><span class="tab-label" data-i18n="nav.settings">
|
|
52
|
+
<button class="tab is-active" data-tab="board" data-i18n-title="nav.board" title="Board"><span class="tab-ico" data-icon="board"></span><span class="tab-label" data-i18n="nav.board">Board</span></button>
|
|
53
|
+
<button class="tab" data-tab="chat" data-i18n-title="nav.chat" title="Chat"><span class="tab-ico" data-icon="chat"></span><span class="tab-label" data-i18n="nav.chat">Chat</span></button>
|
|
54
|
+
<button class="tab" data-tab="requests" data-i18n-title="nav.requests" title="Requests"><span class="tab-ico" data-icon="requests"></span><span class="tab-label" data-i18n="nav.requests">Requests</span></button>
|
|
55
|
+
<button class="tab" data-tab="attention" data-i18n-title="nav.attention" title="Attention"><span class="tab-ico" data-icon="attention"></span><span class="tab-label" data-i18n="nav.attention">Attention</span><span class="tab-badge" id="attnBadge" hidden>0</span></button>
|
|
56
|
+
<button class="tab" data-tab="backlog" data-i18n-title="nav.backlog" title="Backlog"><span class="tab-ico" data-icon="backlog"></span><span class="tab-label" data-i18n="nav.backlog">Backlog</span></button>
|
|
57
|
+
<button class="tab" data-tab="workflow" data-i18n-title="nav.workflow" title="Workflow"><span class="tab-ico" data-icon="workflow"></span><span class="tab-label" data-i18n="nav.workflow">Workflow</span></button>
|
|
58
|
+
<button class="tab" data-tab="team" data-i18n-title="nav.team" title="Agents & Skills"><span class="tab-ico" data-icon="agents"></span><span class="tab-label" data-i18n="nav.team">Agents & Skills</span></button>
|
|
59
|
+
<button class="tab" data-tab="info" data-i18n-title="nav.info" title="Info"><span class="tab-ico" data-icon="info"></span><span class="tab-label" data-i18n="nav.info">Info</span></button>
|
|
60
|
+
<button class="tab" data-tab="settings" data-i18n-title="nav.settings" title="Personalize"><span class="tab-ico" data-icon="settings"></span><span class="tab-label" data-i18n="nav.settings">Personalize</span></button>
|
|
59
61
|
</nav>
|
|
60
62
|
<div class="top-right">
|
|
61
63
|
<span class="sync" id="sync"><span class="sync-dot"></span><span id="syncLabel">live</span></span>
|
|
@@ -199,12 +201,18 @@
|
|
|
199
201
|
<h2 class="panel-title" data-i18n="nav.chat">Chat</h2>
|
|
200
202
|
<p class="panel-sub" data-i18n="chat.tabSub">Full conversation with the runner — the same run as the widget, more room to read it.</p>
|
|
201
203
|
</div>
|
|
202
|
-
<select id="tabRunAgent" class="chat-agent" title="Agent"></select>
|
|
203
204
|
</div>
|
|
204
205
|
<div class="chat-tab-log" id="chatTabLog">
|
|
205
206
|
<div class="chat-idle" data-i18n-html="chat.idle">Type a request — the agent runs headless in this project with full memory
|
|
206
207
|
(<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.</div>
|
|
207
208
|
</div>
|
|
209
|
+
<!-- Toolbar right above the input, not up in the header — this is where you're looking
|
|
210
|
+
when you reach for Summarize/Clear or switch which agent the next message runs on. -->
|
|
211
|
+
<div class="chat-tab-toolbar">
|
|
212
|
+
<button id="tabSummarizeBtn" class="mini-btn" data-i18n-title="chat.summarizeTitle" data-i18n="action.summarize" title="Condense the recent activity into a summary">Summarize</button>
|
|
213
|
+
<button id="tabClearBtn" class="mini-btn" data-i18n-title="chat.clearTitle" data-i18n="action.clear" title="Clear the chat log">Clear</button>
|
|
214
|
+
<select id="tabRunAgent" class="chat-agent" title="Agent"></select>
|
|
215
|
+
</div>
|
|
208
216
|
<div class="chat-tab-input">
|
|
209
217
|
<textarea id="tabRunPrompt" class="chat-ta" data-i18n-ph="chat.inputPlaceholder" placeholder="e.g. Add a login feature with email + password"></textarea>
|
|
210
218
|
<div class="chat-tab-actions">
|
|
@@ -228,9 +236,14 @@
|
|
|
228
236
|
<!-- SETTINGS (change autonomy mode + output language → config.json) -->
|
|
229
237
|
<section class="panel" data-panel="settings">
|
|
230
238
|
<div class="settings-wrap">
|
|
231
|
-
<h2 class="panel-title" data-i18n="nav.settings">
|
|
239
|
+
<h2 class="panel-title" data-i18n="nav.settings">Personalize</h2>
|
|
232
240
|
<p class="panel-sub" data-i18n-html="settings.sub">Change how spectoflow runs. Writes <code>.spectoflow/config.json</code> — the agent picks it up on its next run.</p>
|
|
233
241
|
<div class="settings-card">
|
|
242
|
+
<label class="settings-field">
|
|
243
|
+
<span class="settings-field-label" data-i18n="info.activeAgent">Active agent</span>
|
|
244
|
+
<select id="setAgent" class="settings-select"></select>
|
|
245
|
+
<span class="settings-field-hint is-empty" id="setAgentHint" data-i18n="topbar.agent.none" hidden>No agent found</span>
|
|
246
|
+
</label>
|
|
234
247
|
<label class="settings-field">
|
|
235
248
|
<span class="settings-field-label" data-i18n="field.design">Dashboard design</span>
|
|
236
249
|
<select id="setDesign" class="settings-select"></select>
|
|
@@ -261,7 +274,7 @@
|
|
|
261
274
|
<!-- Customize: project-specific dashboards, skills and agents — described or auto-proposed,
|
|
262
275
|
generated by the configured agent through the same Run/Chat pipeline as any other ask. -->
|
|
263
276
|
<div class="cz-wrap">
|
|
264
|
-
<h2 class="panel-title" data-i18n="customize.title">
|
|
277
|
+
<h2 class="panel-title" data-i18n="customize.title">Extend spectoflow</h2>
|
|
265
278
|
<p class="panel-sub" data-i18n="customize.sub">Add project-specific dashboards, skills and agents — describe what you want, or let Auto propose candidates from this project.</p>
|
|
266
279
|
<div id="czRoot"></div>
|
|
267
280
|
</div>
|
|
@@ -283,7 +296,7 @@
|
|
|
283
296
|
<div class="footer-right">
|
|
284
297
|
<a href="https://github.com/georgesmomo/spectoflow" target="_blank" rel="noopener">GitHub</a>
|
|
285
298
|
<a href="https://www.npmjs.com/package/spectoflow" target="_blank" rel="noopener">npm</a>
|
|
286
|
-
<button class="footer-link" id="footerSettings" data-i18n="nav.settings">
|
|
299
|
+
<button class="footer-link" id="footerSettings" data-i18n="nav.settings">Personalize</button>
|
|
287
300
|
</div>
|
|
288
301
|
</footer>
|
|
289
302
|
|
|
@@ -295,13 +308,17 @@
|
|
|
295
308
|
<span class="chat-title" data-i18n="chat.widgetTitle">Run an agent</span>
|
|
296
309
|
<span class="chat-sub" data-i18n="chat.widgetSub">Quick access · full view in the Chat tab</span>
|
|
297
310
|
</div>
|
|
298
|
-
<select id="runAgent" class="chat-agent" title="Agent"></select>
|
|
299
311
|
<button class="chat-close" id="chatClose" aria-label="Close">×</button>
|
|
300
312
|
</div>
|
|
301
313
|
<div class="chat-log" id="chatLog">
|
|
302
314
|
<div class="chat-idle" data-i18n-html="chat.idle">Type a request — the agent runs headless in this project with full memory
|
|
303
315
|
(<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.</div>
|
|
304
316
|
</div>
|
|
317
|
+
<div class="chat-toolbar">
|
|
318
|
+
<button id="widgetSummarizeBtn" class="mini-btn" data-i18n-title="chat.summarizeTitle" data-i18n="action.summarize" title="Condense the recent activity into a summary">Summarize</button>
|
|
319
|
+
<button id="widgetClearBtn" class="mini-btn" data-i18n-title="chat.clearTitle" data-i18n="action.clear" title="Clear the chat log">Clear</button>
|
|
320
|
+
<select id="runAgent" class="chat-agent" title="Agent"></select>
|
|
321
|
+
</div>
|
|
305
322
|
<div class="chat-input">
|
|
306
323
|
<textarea id="runPrompt" class="chat-ta" data-i18n-ph="chat.inputPlaceholder" placeholder="e.g. Add a login feature with email + password"></textarea>
|
|
307
324
|
<button id="runBtn" class="btn primary chat-send" data-i18n="action.send">Send</button>
|
|
@@ -40,6 +40,9 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
40
40
|
}
|
|
41
41
|
.brand-mini-select:hover,.brand-mini-select:focus-visible { color:var(--ink); border-color:var(--line); background-color:var(--surface-2); outline:none; }
|
|
42
42
|
.brand-mini-select option { color:var(--ink); background:var(--surface); text-transform:none; }
|
|
43
|
+
.brand-agent-select { font-weight:700; }
|
|
44
|
+
.brand-agent-select.is-empty { color:var(--s-blocked); pointer-events:none; }
|
|
45
|
+
.brand-agent-select.is-empty:hover { color:var(--s-blocked); border-color:transparent; background-color:transparent; }
|
|
43
46
|
.progress-meter { width:76px; height:5px; border-radius:999px; background:var(--surface-2); overflow:hidden; flex-shrink:0; margin-left:4px; }
|
|
44
47
|
.progress-meter-fill { height:100%; width:0%; background:linear-gradient(90deg,var(--cool),var(--signal)); border-radius:999px; transition:width .5s ease; }
|
|
45
48
|
.tabs { justify-self:center; display:flex; gap:2px; min-width:0; max-width:100%; overflow-x:auto; overflow-y:hidden; scrollbar-width:none; }
|
|
@@ -375,7 +378,8 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
375
378
|
.chat-title { font-weight:700; font-size:15px; }
|
|
376
379
|
.chat-sub { font-size:10px; color:var(--faint); text-transform:uppercase; letter-spacing:.04em; }
|
|
377
380
|
.chat-agent { margin-left:auto; flex-shrink:0; font:inherit; font-size:12px; background:var(--surface); color:var(--ink); border:1px solid var(--line); border-radius:7px; padding:5px 8px; }
|
|
378
|
-
.chat-
|
|
381
|
+
.chat-toolbar { display:flex; align-items:center; gap:8px; padding:8px 14px; border-top:1px solid var(--line); flex-wrap:wrap; }
|
|
382
|
+
.chat-close { width:28px; height:28px; flex-shrink:0; margin-left:auto; border:1px solid var(--line); background:var(--surface-2); color:var(--muted); border-radius:8px; font-size:17px; cursor:pointer; transition:border-color .15s,color .15s; }
|
|
379
383
|
.chat-close:hover { border-color:color-mix(in srgb,var(--s-blocked) 45%,var(--line)); color:var(--ink); }
|
|
380
384
|
.chat-log { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:11px; }
|
|
381
385
|
.chat-idle { color:var(--faint); font-style:italic; font-size:12.5px; }
|
|
@@ -411,9 +415,10 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
|
|
|
411
415
|
.chat-tab-head { display:flex; align-items:flex-start; gap:14px; margin-bottom:14px; }
|
|
412
416
|
.chat-tab-head .panel-title { margin:0; }
|
|
413
417
|
.chat-tab-head .panel-sub { margin:2px 0 0; }
|
|
414
|
-
.chat-tab-head .chat-agent { margin-left:auto; flex-shrink:0; }
|
|
415
418
|
.chat-tab-log { flex:1; min-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:20px; box-shadow:var(--shadow); }
|
|
416
|
-
.chat-tab-
|
|
419
|
+
.chat-tab-toolbar { display:flex; align-items:center; gap:8px; margin-top:10px; flex-wrap:wrap; }
|
|
420
|
+
.chat-tab-toolbar .chat-agent { margin-left:auto; flex-shrink:0; }
|
|
421
|
+
.chat-tab-input { display:flex; gap:10px; margin-top:10px; }
|
|
417
422
|
.chat-tab-input .chat-ta { flex:1; min-height:56px; max-height:180px; }
|
|
418
423
|
.chat-tab-actions { display:flex; flex-direction:column; gap:8px; flex-shrink:0; }
|
|
419
424
|
.chat-tab-wrap .chat-warn { margin:8px 0 0; }
|
|
@@ -549,6 +554,8 @@ body.booting .ring-svg circle:last-of-type { transform-origin:center; animation:
|
|
|
549
554
|
.settings-field-label { font-size:11px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
|
|
550
555
|
.settings-select { font-family:inherit; font-size:14px; padding:9px 11px; border:1px solid var(--line); border-radius:9px; background:var(--surface-2); color:var(--ink); cursor:pointer; }
|
|
551
556
|
.settings-select:focus { outline:2px solid var(--signal); outline-offset:1px; }
|
|
557
|
+
.settings-field-hint { font-size:12px; }
|
|
558
|
+
.settings-field-hint.is-empty { color:var(--s-blocked); }
|
|
552
559
|
.settings-saved { align-self:flex-start; font-size:12px; color:var(--s-done); font-family:var(--mono); }
|
|
553
560
|
.settings-saved[hidden] { display:none; }
|
|
554
561
|
.settings-readonly { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); overflow:hidden; }
|
|
@@ -9,6 +9,18 @@
|
|
|
9
9
|
*/
|
|
10
10
|
const { spawn } = require('child_process');
|
|
11
11
|
const store = require('../lib/store');
|
|
12
|
+
const agentsRegistry = require('../lib/agents-registry');
|
|
13
|
+
|
|
14
|
+
// The command to run `which`: config.json's own runners map first (an explicit user choice always
|
|
15
|
+
// wins), falling back to the registry's default for a known, headless-capable, genuinely-installed
|
|
16
|
+
// agent — so picking one from a per-message agent list works the first time, before it's ever been
|
|
17
|
+
// the project's "active agent" and had a runner seeded into config.json for it.
|
|
18
|
+
function resolveRunnerCommand(root, cfg, which, opts) {
|
|
19
|
+
if (cfg.runners && cfg.runners[which]) return cfg.runners[which];
|
|
20
|
+
const known = agentsRegistry.KNOWN_AGENTS.find((a) => a.id === which);
|
|
21
|
+
if (known && known.runner && agentsRegistry.isAgentInstalled(which, root, opts)) return known.runner;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
12
24
|
|
|
13
25
|
function runStart(root, run) {
|
|
14
26
|
const rt = store.readRuntime(root); rt.agents = rt.agents || []; rt.agents.push(run); store.writeRuntime(root, rt);
|
|
@@ -50,7 +62,7 @@ function pushAttention(root, text, by) {
|
|
|
50
62
|
function startRun(root, { prompt, agent, logPrompt = true }, emit) {
|
|
51
63
|
const cfg = store.readConfig(root);
|
|
52
64
|
const which = agent || cfg.agent || 'claude';
|
|
53
|
-
const cmdStr = cfg
|
|
65
|
+
const cmdStr = resolveRunnerCommand(root, cfg, which);
|
|
54
66
|
if (!cmdStr) return { error: `No runner configured for "${which}".` };
|
|
55
67
|
const parts = cmdStr.split(/\s+/).filter(Boolean);
|
|
56
68
|
const runId = 'r' + Date.now().toString(36);
|
|
@@ -97,4 +109,4 @@ function startRun(root, { prompt, agent, logPrompt = true }, emit) {
|
|
|
97
109
|
return { runId, child };
|
|
98
110
|
}
|
|
99
111
|
|
|
100
|
-
module.exports = { startRun };
|
|
112
|
+
module.exports = { startRun, resolveRunnerCommand };
|
|
@@ -10,7 +10,9 @@ const fs = require('fs');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const store = require('../lib/store');
|
|
12
12
|
const { startRun } = require('./runner');
|
|
13
|
+
const { runSummarize } = require('./summarize');
|
|
13
14
|
const orchestrator = require('./orchestrator');
|
|
15
|
+
const agentsRegistry = require('../lib/agents-registry');
|
|
14
16
|
|
|
15
17
|
const PORT = process.env.SPECTOFLOW_PORT ? Number(process.env.SPECTOFLOW_PORT) : 4319;
|
|
16
18
|
const PUBLIC = path.join(__dirname, 'public');
|
|
@@ -30,6 +32,11 @@ function project(){
|
|
|
30
32
|
const p = store.readProject(ROOT);
|
|
31
33
|
const v = frameworkVersion(); if (v) p.version = v;
|
|
32
34
|
p.projectName = path.basename(ROOT); // the actual project folder, always shown in the topbar
|
|
35
|
+
// Known vs. actually-installed agents — the topbar switcher needs both: the full list to offer,
|
|
36
|
+
// and which ones are real (bin on PATH, or the project already has that agent's config dir) so it
|
|
37
|
+
// can refuse to activate one that isn't there.
|
|
38
|
+
p.knownAgents = agentsRegistry.KNOWN_AGENTS.map((a) => ({ id: a.id, label: a.label, headless: a.headless }));
|
|
39
|
+
p.installedAgents = agentsRegistry.installedAgents(ROOT);
|
|
33
40
|
return p;
|
|
34
41
|
}
|
|
35
42
|
function sendJSON(res,code,obj){ res.writeHead(code,{'Content-Type':'application/json; charset=utf-8'}); res.end(JSON.stringify(obj)); }
|
|
@@ -44,6 +51,23 @@ function writeConfig(patch){
|
|
|
44
51
|
if (patch.mode && ['autopilot','semi','manual'].includes(patch.mode)) cfg.mode = patch.mode;
|
|
45
52
|
if (typeof patch.language === 'string' && patch.language.trim()) cfg.language = patch.language.trim();
|
|
46
53
|
if (typeof patch.design === 'string' && /^[a-z0-9-]{1,40}$/.test(patch.design)) cfg.design = patch.design;
|
|
54
|
+
if (typeof patch.agent === 'string' && patch.agent.trim()) {
|
|
55
|
+
const id = patch.agent.trim();
|
|
56
|
+
// Never activate an agent whose CLI isn't actually there — a picked-but-absent agent would just
|
|
57
|
+
// fail silently the next time something tries to run it.
|
|
58
|
+
if (!agentsRegistry.isAgentInstalled(id, ROOT)) {
|
|
59
|
+
const known = agentsRegistry.KNOWN_AGENTS.find((a) => a.id === id);
|
|
60
|
+
const label = known ? known.label : id;
|
|
61
|
+
throw new Error(`${label} isn't installed here (its command wasn't found on PATH). Install it, then try again.`);
|
|
62
|
+
}
|
|
63
|
+
cfg.agent = id;
|
|
64
|
+
// Seed a default runner if this agent was never configured (e.g. installed after init/update).
|
|
65
|
+
// A headless:false agent (e.g. kimi) has no runner to seed — it can still be the active agent,
|
|
66
|
+
// it just can't be spawned by Run/Orchestrate/Summarize (disabled client-side; runner.js and
|
|
67
|
+
// summarize.js also refuse server-side either way).
|
|
68
|
+
const known = agentsRegistry.KNOWN_AGENTS.find((a) => a.id === id);
|
|
69
|
+
if (known && known.runner) { cfg.runners = cfg.runners || {}; if (!cfg.runners[id]) cfg.runners[id] = known.runner; }
|
|
70
|
+
}
|
|
47
71
|
fs.writeFileSync(cp, JSON.stringify(cfg, null, 2) + '\n');
|
|
48
72
|
return cfg;
|
|
49
73
|
}
|
|
@@ -143,6 +167,19 @@ const server = http.createServer(async (req,res)=>{
|
|
|
143
167
|
return sendJSON(res,200,{runId:r.runId});
|
|
144
168
|
}
|
|
145
169
|
|
|
170
|
+
// ---- chat context management: condense the log via the agent, or wipe it ----
|
|
171
|
+
if (p === '/api/chat/summarize' && req.method === 'POST') {
|
|
172
|
+
const { agent } = await body(req);
|
|
173
|
+
const r = runSummarize(ROOT, { agent }, emit);
|
|
174
|
+
if (r.error) return sendJSON(res, 400, { error: r.error });
|
|
175
|
+
return sendJSON(res, 200, { ok: true });
|
|
176
|
+
}
|
|
177
|
+
if (p === '/api/chat/clear' && req.method === 'POST') {
|
|
178
|
+
const rt = store.readRuntime(ROOT); rt.messages = []; store.writeRuntime(ROOT, rt);
|
|
179
|
+
emit({ type: 'change' });
|
|
180
|
+
return sendJSON(res, 200, { ok: true });
|
|
181
|
+
}
|
|
182
|
+
|
|
146
183
|
// ---- orchestrator ----
|
|
147
184
|
if (p === '/api/orchestrate' && req.method === 'POST') {
|
|
148
185
|
const { request } = await body(req);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Chat "Summarize" — condenses the recent group-chat log into one digest message, via the same
|
|
4
|
+
* configured agent runner (kept separate from runner.js since it captures the child's raw stdout as
|
|
5
|
+
* one summary, not sentinel-parsed lines; unit-testable without an HTTP server, same as runner.js).
|
|
6
|
+
*/
|
|
7
|
+
const { spawn } = require('child_process');
|
|
8
|
+
const store = require('../lib/store');
|
|
9
|
+
const { resolveRunnerCommand } = require('./runner');
|
|
10
|
+
|
|
11
|
+
const DEFAULT_LIMIT = 40;
|
|
12
|
+
|
|
13
|
+
// "role: text" lines, oldest first, capped to the most recent `limit` entries.
|
|
14
|
+
function formatLog(messages, limit = DEFAULT_LIMIT) {
|
|
15
|
+
return (messages || []).slice(-limit).map((m) => `${m.role}: ${m.text}`).join('\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Summarizes runtime.messages (excluding prior summaries, so re-summarizing doesn't compound) into
|
|
19
|
+
// one new message of kind 'summary'. Returns { child } on success or { error } — mirrors startRun's
|
|
20
|
+
// shape, without the sentinel-line streaming runner.js does (this is a one-shot digest, not a task run).
|
|
21
|
+
function runSummarize(root, { agent } = {}, emit) {
|
|
22
|
+
const cfg = store.readConfig(root);
|
|
23
|
+
const which = agent || cfg.agent || 'claude';
|
|
24
|
+
const cmdStr = resolveRunnerCommand(root, cfg, which);
|
|
25
|
+
if (!cmdStr) return { error: `No runner configured for "${which}".` };
|
|
26
|
+
|
|
27
|
+
const rt = store.readRuntime(root);
|
|
28
|
+
const messages = (rt.messages || []).filter((m) => m.kind !== 'summary');
|
|
29
|
+
if (!messages.length) return { error: 'Nothing to summarize yet.' };
|
|
30
|
+
|
|
31
|
+
const prompt = 'Summarize this project\'s recent activity log in 3-6 concise bullet points — '
|
|
32
|
+
+ 'what was built, decided, or is blocked. Reply with the summary only, no preamble or sentinel lines.'
|
|
33
|
+
+ '\n\n' + formatLog(messages);
|
|
34
|
+
|
|
35
|
+
const parts = cmdStr.split(/\s+/).filter(Boolean);
|
|
36
|
+
let child;
|
|
37
|
+
try { child = spawn(parts[0], [...parts.slice(1), prompt], { cwd: root, env: process.env }); }
|
|
38
|
+
catch (e) { return { error: e.message }; }
|
|
39
|
+
try { child.stdin && child.stdin.end(); } catch {}
|
|
40
|
+
|
|
41
|
+
let out = '';
|
|
42
|
+
child.stdout && child.stdout.on('data', (d) => { out += d.toString(); });
|
|
43
|
+
child.stderr && child.stderr.on('data', (d) => { out += d.toString(); });
|
|
44
|
+
child.on('close', (code) => {
|
|
45
|
+
const text = out.trim() || (code === 0 ? '(no output)' : `summarize failed (exit ${code})`);
|
|
46
|
+
const m = store.appendMessage(root, { role: which, kind: 'summary', text, agent: which });
|
|
47
|
+
if (emit) { emit({ type: 'message', message: m }); emit({ type: 'change' }); }
|
|
48
|
+
});
|
|
49
|
+
return { child };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { runSummarize, formatLog };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* The dashboard's own view of "which coding agents exist and is one actually installed" — a small,
|
|
4
|
+
* self-contained subset of this package's lib/adapters.js (the richer install-time registry with
|
|
5
|
+
* memory-file content). Duplicated rather than shared: .spectoflow/ must be self-contained (ships
|
|
6
|
+
* into every project), while lib/adapters.js does not ship there. test/agents-registry.test.js
|
|
7
|
+
* guards the two id/bin/runner sets from drifting apart.
|
|
8
|
+
*/
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// headless:false = detectable and selectable as the active agent, but spectoflow never spawns it
|
|
13
|
+
// itself (no confirmed non-interactive one-shot mode) — Run/Orchestrate/Summarize stay disabled for
|
|
14
|
+
// it client-side; runner/summarize.js also refuse server-side (defense in depth). See the longer
|
|
15
|
+
// rationale above lib/adapters.js's REGISTRY, the richer install-time twin of this list.
|
|
16
|
+
const KNOWN_AGENTS = [
|
|
17
|
+
{ id: 'claude', label: 'Claude Code', bin: 'claude', dirs: ['.claude'], runner: 'claude -p --permission-mode acceptEdits', headless: true },
|
|
18
|
+
{ id: 'codex', label: 'Codex', bin: 'codex', dirs: ['.codex'], runner: 'codex exec', headless: true },
|
|
19
|
+
{ id: 'cursor', label: 'Cursor', bin: 'cursor-agent', dirs: ['.cursor'], runner: 'cursor-agent -p', headless: true },
|
|
20
|
+
{ id: 'gemini', label: 'Gemini CLI', bin: 'gemini', dirs: ['.gemini'], runner: 'gemini -p', headless: true },
|
|
21
|
+
{ id: 'opencode', label: 'OpenCode', bin: 'opencode', dirs: ['.opencode'], runner: 'opencode run --quiet', headless: true },
|
|
22
|
+
{ id: 'kiro', label: 'Kiro CLI', bin: 'kiro-cli', dirs: ['.kiro'], runner: 'kiro-cli chat --no-interactive --trust-all-tools', headless: true },
|
|
23
|
+
{ id: 'antigravity', label: 'Antigravity', bin: 'agy', dirs: [], runner: 'agy -p', headless: true },
|
|
24
|
+
{ id: 'kimi', label: 'Kimi CLI', bin: 'kimi', dirs: [], runner: null, headless: false },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Is `bin` an executable resolvable on PATH? On win32, an extension from PATHEXT is required, so we
|
|
28
|
+
// try each; we also try the bare name (covers test fixtures and extensionless shims).
|
|
29
|
+
function binOnPath(bin, { env = process.env, platform = process.platform } = {}) {
|
|
30
|
+
const raw = env.PATH || env.Path || '';
|
|
31
|
+
const dirs = raw.split(path.delimiter).filter(Boolean);
|
|
32
|
+
const exts =
|
|
33
|
+
platform === 'win32' ? ['', ...(env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)] : [''];
|
|
34
|
+
for (const d of dirs) {
|
|
35
|
+
for (const e of exts) {
|
|
36
|
+
if (fs.existsSync(path.join(d, bin + e))) return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// True if `id` looks genuinely installed: its bin resolves on PATH, or the project already has its
|
|
43
|
+
// config dir (a project can be set up for an agent whose bin isn't on THIS machine's PATH, e.g. a
|
|
44
|
+
// remote/CI runner). Unknown ids are never "installed".
|
|
45
|
+
function isAgentInstalled(id, projectRoot, opts) {
|
|
46
|
+
const a = KNOWN_AGENTS.find((x) => x.id === id);
|
|
47
|
+
if (!a) return false;
|
|
48
|
+
if (a.bin && binOnPath(a.bin, opts)) return true;
|
|
49
|
+
return (a.dirs || []).some((d) => fs.existsSync(path.join(projectRoot, d)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ids of every known agent actually installed for this project, in KNOWN_AGENTS (priority) order.
|
|
53
|
+
function installedAgents(projectRoot, opts) {
|
|
54
|
+
return KNOWN_AGENTS.filter((a) => isAgentInstalled(a.id, projectRoot, opts)).map((a) => a.id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { KNOWN_AGENTS, binOnPath, isAgentInstalled, installedAgents };
|