@anmol-srv/sigil 0.11.0 → 0.12.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/dist/cli.js +397 -339
- package/dist/daemon.js +198 -121
- package/dist/hooks/post-tool-use.js +18 -18
- package/dist/hooks/session-end.js +28 -28
- package/dist/hooks/stop.js +36 -36
- package/dist/hooks/user-prompt-submit.js +17 -17
- package/dist/server.js +26 -26
- package/package.json +1 -1
- package/src/gui/web/api.js +37 -0
- package/src/gui/web/app.css +114 -50
- package/src/gui/web/app.js +244 -92
- package/src/gui/web/components.js +90 -0
- package/src/gui/web/design/colors_and_type.css +178 -0
- package/src/gui/web/design/sigil-mark-mono.svg +8 -0
- package/src/gui/web/design/sigil-mark.svg +26 -0
- package/src/gui/web/index.html +64 -42
- package/src/gui/web/toast.js +62 -0
package/src/gui/web/app.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
// Sigil GUI — vanilla JS. Onboarding wizard + dashboard.
|
|
2
|
+
import { toast } from './toast.js';
|
|
3
|
+
import { connectorCard, dbFlowRow, setFlowRow } from './components.js';
|
|
4
|
+
|
|
2
5
|
const $ = (sel, root = document) => root.querySelector(sel);
|
|
3
6
|
const $$ = (sel, root = document) => root.querySelectorAll(sel);
|
|
4
7
|
|
|
8
|
+
// Onboarding machine step (SCREAMING_SNAKE) → wizard section id.
|
|
9
|
+
const MACHINE_TO_STEP = { CONNECTORS: 'connectors', PROVIDER: 'llm', EMBEDDING: 'embedding', DATABASE: 'database', FINISH: 'finish' };
|
|
10
|
+
async function persistStep(step, status, data = {}) {
|
|
11
|
+
try { await rpc('onboardingAdvance', { step, status, data }); }
|
|
12
|
+
catch (err) { /* non-fatal: state persistence is best-effort */ void err; }
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
// ── RPC ──────────────────────────────────────────────────────────────
|
|
6
16
|
async function rpc(method, params = {}) {
|
|
7
17
|
const res = await fetch('/api/v1/rpc', {
|
|
@@ -12,7 +22,10 @@ async function rpc(method, params = {}) {
|
|
|
12
22
|
});
|
|
13
23
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
14
24
|
const body = await res.json();
|
|
15
|
-
if (!body.ok)
|
|
25
|
+
if (!body.ok) {
|
|
26
|
+
const e = body.error || {};
|
|
27
|
+
throw Object.assign(new Error(e.message || 'rpc error'), { code: e.code, hint: e.hint });
|
|
28
|
+
}
|
|
16
29
|
return body.data;
|
|
17
30
|
}
|
|
18
31
|
|
|
@@ -45,27 +58,27 @@ async function copyToClipboard(text) {
|
|
|
45
58
|
// ════════════════════════════════════════════════════════════════════
|
|
46
59
|
// ONBOARDING WIZARD
|
|
47
60
|
// ════════════════════════════════════════════════════════════════════
|
|
48
|
-
const wizardState = { step: '
|
|
61
|
+
const wizardState = { step: 'connectors', llmProvider: null, embProvider: null, llmProviders: [], embProviders: [], connectorsLoaded: false, dbInit: false, connectedCount: 0 };
|
|
62
|
+
|
|
63
|
+
const STEP_ORDER = ['connectors', 'llm', 'embedding', 'database', 'finish'];
|
|
49
64
|
|
|
50
65
|
function setOnbStep(stepId) {
|
|
51
66
|
wizardState.step = stepId;
|
|
52
|
-
|
|
53
|
-
const order = ['welcome', 'database', 'llm', 'embedding', 'finish'];
|
|
54
|
-
const idx = order.indexOf(stepId);
|
|
67
|
+
const idx = STEP_ORDER.indexOf(stepId);
|
|
55
68
|
$$('.onboarding-step').forEach((el) => {
|
|
56
|
-
const i =
|
|
69
|
+
const i = STEP_ORDER.indexOf(el.dataset.obStep);
|
|
57
70
|
el.classList.remove('active', 'done', 'future');
|
|
58
71
|
if (i < idx) el.classList.add('done');
|
|
59
72
|
else if (i === idx) el.classList.add('active');
|
|
60
73
|
else el.classList.add('future');
|
|
61
74
|
});
|
|
62
|
-
// Show only the active step
|
|
63
75
|
$$('.wizard-step').forEach((el) => el.classList.toggle('active', el.dataset.step === stepId));
|
|
64
|
-
// Lazy-fetch
|
|
76
|
+
// Lazy-fetch per-step data when first entering a step.
|
|
77
|
+
if (stepId === 'connectors' && !wizardState.connectorsLoaded) loadConnectors();
|
|
65
78
|
if (stepId === 'llm' && !wizardState.llmProviders.length) loadLlmProviders();
|
|
66
79
|
if (stepId === 'embedding' && !wizardState.embProviders.length) loadEmbeddingProviders();
|
|
80
|
+
if (stepId === 'database' && !wizardState.dbInit) initDbStep();
|
|
67
81
|
if (stepId === 'finish') renderFinish();
|
|
68
|
-
// Scroll content to top
|
|
69
82
|
document.querySelector('.onboarding-content')?.scrollTo(0, 0);
|
|
70
83
|
}
|
|
71
84
|
|
|
@@ -77,37 +90,96 @@ async function loadOnboardingState() {
|
|
|
77
90
|
return;
|
|
78
91
|
}
|
|
79
92
|
$('#onboarding').hidden = false;
|
|
80
|
-
|
|
81
|
-
if (state.steps.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
} catch (err) {
|
|
91
|
-
// Could not reach daemon — show welcome anyway
|
|
93
|
+
if (state.steps.database.done) $('#ob-db-next').disabled = false;
|
|
94
|
+
if (state.steps.llm.done) $('#ob-llm-next').disabled = false;
|
|
95
|
+
if (state.steps.embedding.done) $('#ob-emb-next').disabled = false;
|
|
96
|
+
// Resume at the machine's current step (refresh mid-wizard → same place).
|
|
97
|
+
const resume = MACHINE_TO_STEP[state.machine?.currentStep];
|
|
98
|
+
if (resume && resume !== 'connectors') setOnbStep(resume);
|
|
99
|
+
else setOnbStep('connectors');
|
|
100
|
+
} catch {
|
|
101
|
+
// Could not reach daemon — show the first step anyway.
|
|
92
102
|
$('#onboarding').hidden = false;
|
|
93
103
|
}
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
// ──
|
|
106
|
+
// ── Connectors step ──────────────────────────────────────────────────
|
|
107
|
+
async function loadConnectors() {
|
|
108
|
+
wizardState.connectorsLoaded = true;
|
|
109
|
+
const host = $('#ob-connectors');
|
|
110
|
+
try {
|
|
111
|
+
const { connectors } = await rpc('listConnectors');
|
|
112
|
+
wizardState.connectedCount = connectors.filter((c) => c.status === 'connected').length;
|
|
113
|
+
renderConnectors(connectors);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
host.innerHTML = `<div class="muted">could not load connectors: ${escape(err.message)}</div>`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderConnectors(connectors) {
|
|
120
|
+
const host = $('#ob-connectors');
|
|
121
|
+
host.innerHTML = '';
|
|
122
|
+
connectors.forEach((c) => host.appendChild(connectorCard(c, onConnectorAction)));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function onConnectorAction(id, action) {
|
|
126
|
+
const host = $('#ob-connectors');
|
|
127
|
+
const card = host.querySelector(`[data-id="${id}"]`);
|
|
128
|
+
if (action === 'disconnect') {
|
|
129
|
+
try {
|
|
130
|
+
await rpc('disconnectConnector', { id });
|
|
131
|
+
toast({ variant: 'success', message: `${id} disconnected` });
|
|
132
|
+
} catch (err) { toast({ variant: 'error', message: err.message, hint: err.hint, code: err.code }); }
|
|
133
|
+
return loadConnectors();
|
|
134
|
+
}
|
|
135
|
+
// connect / retry → optimistic "connecting" card, then refresh.
|
|
136
|
+
if (card) card.replaceWith(connectorCard({ id, label: id, hint: '', uiState: 'connecting' }, onConnectorAction));
|
|
137
|
+
try {
|
|
138
|
+
await rpc('connectConnector', { id });
|
|
139
|
+
toast({ variant: 'success', message: `${id} connected` });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
toast({ variant: 'error', message: err.message || `could not connect ${id}`, hint: err.hint, code: err.code });
|
|
142
|
+
}
|
|
143
|
+
return loadConnectors();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── DB step (linear guided flow) ─────────────────────────────────────
|
|
147
|
+
function dbMode() {
|
|
148
|
+
return $('input[name="db-mode"]:checked')?.value || 'url';
|
|
149
|
+
}
|
|
150
|
+
|
|
97
151
|
$('#db-mode-cards')?.addEventListener('click', (e) => {
|
|
98
152
|
const card = e.target.closest('[data-db-mode]');
|
|
99
|
-
if (!card) return;
|
|
153
|
+
if (!card || card.hidden) return;
|
|
100
154
|
$$('#db-mode-cards .provider-card').forEach((c) => c.classList.remove('selected'));
|
|
101
155
|
card.classList.add('selected');
|
|
102
156
|
card.querySelector('input').checked = true;
|
|
103
|
-
|
|
104
|
-
$('#ob-db-
|
|
157
|
+
const mode = card.dataset.dbMode;
|
|
158
|
+
$('#ob-db-url').style.display = mode === 'url' ? '' : 'none';
|
|
159
|
+
$('#ob-db-fields').style.display = mode === 'fields' ? '' : 'none';
|
|
160
|
+
$('#ob-db-setup').textContent = mode === 'docker' ? 'Create local database' : 'Set up database';
|
|
105
161
|
});
|
|
106
|
-
|
|
162
|
+
|
|
163
|
+
// Probe Docker once; if present, surface the recommended "Local (automatic)"
|
|
164
|
+
// mode and select it by default. Otherwise leave URL selected.
|
|
165
|
+
async function initDbStep() {
|
|
166
|
+
wizardState.dbInit = true;
|
|
167
|
+
try {
|
|
168
|
+
const d = await rpc('dbDockerAvailable');
|
|
169
|
+
const note = $('#ob-db-docker-note');
|
|
170
|
+
if (d.available) {
|
|
171
|
+
const dockerCard = $('#ob-db-mode-docker');
|
|
172
|
+
dockerCard.hidden = false;
|
|
173
|
+
dockerCard.click();
|
|
174
|
+
} else if (note) {
|
|
175
|
+
note.hidden = false;
|
|
176
|
+
note.textContent = `Docker not detected (${d.reason || 'unavailable'}) — use a connection URL or local Postgres.`;
|
|
177
|
+
}
|
|
178
|
+
} catch { /* leave URL mode as the default */ }
|
|
179
|
+
}
|
|
107
180
|
|
|
108
181
|
function obDbParams() {
|
|
109
|
-
|
|
110
|
-
if (isUrl) return { url: $('#ob-db-url-input').value.trim() };
|
|
182
|
+
if (dbMode() === 'url') return { url: $('#ob-db-url-input').value.trim() };
|
|
111
183
|
return {
|
|
112
184
|
host: $('#ob-db-host').value.trim(),
|
|
113
185
|
port: Number($('#ob-db-port').value),
|
|
@@ -117,78 +189,97 @@ function obDbParams() {
|
|
|
117
189
|
};
|
|
118
190
|
}
|
|
119
191
|
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
192
|
+
function dbFlowInit(rows) {
|
|
193
|
+
const flow = $('#ob-db-flow');
|
|
194
|
+
flow.hidden = false;
|
|
195
|
+
flow.innerHTML = '';
|
|
196
|
+
rows.forEach(([id, label]) => flow.appendChild(dbFlowRow(id, label)));
|
|
197
|
+
return flow;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
$('#ob-db-setup')?.addEventListener('click', async () => {
|
|
201
|
+
const btn = $('#ob-db-setup');
|
|
202
|
+
btn.disabled = true;
|
|
203
|
+
$('#ob-db-next').disabled = true;
|
|
204
|
+
const mode = dbMode();
|
|
125
205
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
out.classList.add(data.ok ? 'ok' : 'err');
|
|
129
|
-
if (data.ok && !data.pgvector) {
|
|
130
|
-
$('#ob-db-install-pgv').hidden = false;
|
|
131
|
-
$('#ob-db-migrate').hidden = true;
|
|
132
|
-
$('#ob-db-next').disabled = true;
|
|
133
|
-
out.textContent += '\n\npgvector is not installed yet. Click "Install pgvector".';
|
|
134
|
-
} else if (data.ok) {
|
|
135
|
-
$('#ob-db-install-pgv').hidden = true;
|
|
136
|
-
$('#ob-db-migrate').hidden = false;
|
|
137
|
-
out.textContent += '\n\npgvector is installed. Click "Run migrations" to finish.';
|
|
206
|
+
if (mode === 'docker') {
|
|
207
|
+
await runDockerFlow();
|
|
138
208
|
} else {
|
|
139
|
-
|
|
140
|
-
$('#ob-db-migrate').hidden = true;
|
|
141
|
-
$('#ob-db-next').disabled = true;
|
|
209
|
+
await runUrlFlow();
|
|
142
210
|
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
out.classList.add('err');
|
|
211
|
+
} finally {
|
|
212
|
+
btn.disabled = false;
|
|
146
213
|
}
|
|
147
214
|
});
|
|
148
215
|
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
216
|
+
async function runDockerFlow() {
|
|
217
|
+
const flow = dbFlowInit([['provision', 'Create pgvector container'], ['migrate', 'Run migrations']]);
|
|
218
|
+
setFlowRow(flow, 'provision', { phase: 'active', detail: 'pulling image + starting…' });
|
|
152
219
|
try {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
});
|
|
220
|
+
const r = await rpc('dbProvisionDocker');
|
|
221
|
+
setFlowRow(flow, 'provision', { phase: 'done', detail: `${r.container} :${r.port}${r.reused ? ' (reused)' : ''}` });
|
|
222
|
+
setFlowRow(flow, 'migrate', { phase: 'done', detail: `${r.migrationsRan} migrations · pgvector ✓` });
|
|
223
|
+
await onDbReady({ pgvector: true, migrationsRan: r.migrationsRan, mode: 'docker' });
|
|
224
|
+
} catch (err) {
|
|
225
|
+
setFlowRow(flow, 'provision', { phase: 'error', detail: err.code || 'failed' });
|
|
226
|
+
toast({ variant: 'error', message: err.message, hint: err.hint, code: err.code });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
163
229
|
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
230
|
+
async function runUrlFlow() {
|
|
231
|
+
const params = obDbParams();
|
|
232
|
+
const flow = dbFlowInit([['test', 'Test connection'], ['pgvector', 'Enable pgvector'], ['migrate', 'Run migrations']]);
|
|
233
|
+
// 1. test
|
|
234
|
+
setFlowRow(flow, 'test', { phase: 'active', detail: 'connecting…' });
|
|
235
|
+
let test;
|
|
236
|
+
try {
|
|
237
|
+
test = await rpc('testDbConnection', params);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
setFlowRow(flow, 'test', { phase: 'error', detail: err.code || 'failed' });
|
|
240
|
+
return toast({ variant: 'error', message: err.message, hint: err.hint, code: err.code });
|
|
241
|
+
}
|
|
242
|
+
if (!test.ok) {
|
|
243
|
+
setFlowRow(flow, 'test', { phase: 'error', detail: test.code || test.stage || 'failed' });
|
|
244
|
+
return toast({ variant: 'error', message: test.error || 'connection failed', hint: test.fixHint, code: test.kind });
|
|
245
|
+
}
|
|
246
|
+
setFlowRow(flow, 'test', { phase: 'done', detail: `${test.provider} · ${test.connectMs}ms` });
|
|
247
|
+
// 2. pgvector
|
|
248
|
+
if (!test.pgvector) {
|
|
249
|
+
setFlowRow(flow, 'pgvector', { phase: 'active', detail: 'installing…' });
|
|
250
|
+
try {
|
|
251
|
+
const pg = await rpc('ensurePgvector', params);
|
|
252
|
+
if (!pg.ok || !pg.installed) throw Object.assign(new Error(pg.error || 'could not enable pgvector'), { hint: pg.fixHint });
|
|
253
|
+
setFlowRow(flow, 'pgvector', { phase: 'done', detail: pg.version ? `v${pg.version}` : 'enabled' });
|
|
254
|
+
} catch (err) {
|
|
255
|
+
setFlowRow(flow, 'pgvector', { phase: 'error', detail: err.code || 'failed' });
|
|
256
|
+
return toast({ variant: 'error', message: err.message, hint: err.hint, code: err.code });
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
setFlowRow(flow, 'pgvector', { phase: 'done', detail: 'already enabled' });
|
|
260
|
+
}
|
|
261
|
+
// 3. persist + migrate
|
|
262
|
+
setFlowRow(flow, 'migrate', { phase: 'active', detail: 'writing env + migrating…' });
|
|
167
263
|
try {
|
|
168
|
-
const params = obDbParams();
|
|
169
264
|
if (params.url) {
|
|
170
|
-
await rpc('writeEnv', { patch: {
|
|
171
|
-
SIGIL_DATABASE_URL: params.url,
|
|
172
|
-
SIGIL_DB_HOST: null, SIGIL_DB_PORT: null, SIGIL_DB_NAME: null, SIGIL_DB_USER: null, SIGIL_DB_PASSWORD: null,
|
|
173
|
-
} });
|
|
265
|
+
await rpc('writeEnv', { patch: { SIGIL_DATABASE_URL: params.url, SIGIL_DB_HOST: null, SIGIL_DB_PORT: null, SIGIL_DB_NAME: null, SIGIL_DB_USER: null, SIGIL_DB_PASSWORD: null } });
|
|
174
266
|
} else {
|
|
175
|
-
await rpc('writeEnv', { patch: {
|
|
176
|
-
SIGIL_DB_HOST: params.host, SIGIL_DB_PORT: String(params.port),
|
|
177
|
-
SIGIL_DB_NAME: params.database, SIGIL_DB_USER: params.user, SIGIL_DB_PASSWORD: params.password,
|
|
178
|
-
SIGIL_DATABASE_URL: null,
|
|
179
|
-
} });
|
|
267
|
+
await rpc('writeEnv', { patch: { SIGIL_DB_HOST: params.host, SIGIL_DB_PORT: String(params.port), SIGIL_DB_NAME: params.database, SIGIL_DB_USER: params.user, SIGIL_DB_PASSWORD: params.password, SIGIL_DATABASE_URL: null } });
|
|
180
268
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// pre-onboarding env (localhost:5432 by default).
|
|
185
|
-
const data = await rpc('runMigrations', params);
|
|
186
|
-
out.textContent += `\n✓ batch ${data.batchNo}: ${data.ran.length} migrations applied (${data.against})`;
|
|
187
|
-
$('#ob-db-next').disabled = false;
|
|
269
|
+
const m = await rpc('runMigrations', params);
|
|
270
|
+
setFlowRow(flow, 'migrate', { phase: 'done', detail: `batch ${m.batchNo} · ${m.ran.length} applied` });
|
|
271
|
+
await onDbReady({ pgvector: true, migrationsRan: m.ran.length, mode: dbMode() });
|
|
188
272
|
} catch (err) {
|
|
189
|
-
|
|
273
|
+
setFlowRow(flow, 'migrate', { phase: 'error', detail: err.code || 'failed' });
|
|
274
|
+
toast({ variant: 'error', message: err.message, hint: err.hint, code: err.code });
|
|
190
275
|
}
|
|
191
|
-
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function onDbReady(data) {
|
|
279
|
+
$('#ob-db-next').disabled = false;
|
|
280
|
+
toast({ variant: 'success', message: 'Database ready.' });
|
|
281
|
+
await persistStep('DATABASE', 'DONE', data);
|
|
282
|
+
}
|
|
192
283
|
|
|
193
284
|
// ── LLM provider step ───────────────────────────────────────────────
|
|
194
285
|
async function loadLlmProviders() {
|
|
@@ -243,9 +334,11 @@ $('#ob-llm-save')?.addEventListener('click', async () => {
|
|
|
243
334
|
out.classList.add('ok');
|
|
244
335
|
out.textContent += `\n✓ provider responded: "${test.response}"`;
|
|
245
336
|
$('#ob-llm-next').disabled = false;
|
|
337
|
+
await persistStep('PROVIDER', 'DONE', { llmProvider: wizardState.llmProvider });
|
|
246
338
|
} else {
|
|
247
339
|
out.classList.add('err');
|
|
248
340
|
out.textContent += `\n✗ test failed: ${test.error}`;
|
|
341
|
+
toast({ variant: 'error', message: test.error || 'LLM test failed', hint: test.fixHint, code: test.kind });
|
|
249
342
|
}
|
|
250
343
|
} catch (err) {
|
|
251
344
|
out.classList.add('err');
|
|
@@ -396,7 +489,10 @@ $('#ob-emb-save')?.addEventListener('click', async () => {
|
|
|
396
489
|
fields,
|
|
397
490
|
out: $('#ob-emb-result'),
|
|
398
491
|
conflictHost: $('#ob-emb-fields')?.parentElement,
|
|
399
|
-
onSuccess: () => {
|
|
492
|
+
onSuccess: () => {
|
|
493
|
+
$('#ob-emb-next').disabled = false;
|
|
494
|
+
persistStep('EMBEDDING', 'DONE', { provider: wizardState.embProvider });
|
|
495
|
+
},
|
|
400
496
|
});
|
|
401
497
|
if (!ok) $('#ob-emb-next').disabled = true;
|
|
402
498
|
});
|
|
@@ -416,16 +512,32 @@ async function renderFinish() {
|
|
|
416
512
|
} catch { /* ignore */ }
|
|
417
513
|
}
|
|
418
514
|
$('#ob-complete')?.addEventListener('click', async () => {
|
|
419
|
-
|
|
420
|
-
|
|
515
|
+
const installService = $('#ob-always-up')?.checked === true;
|
|
516
|
+
try {
|
|
517
|
+
const r = await rpc('markOnboardingComplete', { installService });
|
|
518
|
+
if (installService && r && r.serviceInstalled === false) {
|
|
519
|
+
toast({ variant: 'info', message: 'Could not install the always-up service on this platform.', hint: 'Sigil still auto-starts on first use; retry with `sigil service install`.' });
|
|
520
|
+
}
|
|
521
|
+
} catch { /* daemon restarts on complete — expected to drop */ }
|
|
421
522
|
$('#onboarding').hidden = true;
|
|
422
|
-
|
|
523
|
+
// Daemon is handing off (restart / service). Give it a moment, then refresh.
|
|
524
|
+
setTimeout(() => refreshHealth(), 1500);
|
|
423
525
|
});
|
|
424
526
|
|
|
425
527
|
// ── Navigation between wizard steps ──────────────────────────────────
|
|
426
528
|
document.addEventListener('click', (e) => {
|
|
427
529
|
const n = e.target.closest('[data-ob-next]');
|
|
428
|
-
if (n) {
|
|
530
|
+
if (n) {
|
|
531
|
+
e.preventDefault();
|
|
532
|
+
// Persist the step we're leaving (connectors is skippable — DONE if any
|
|
533
|
+
// tool connected, else SKIPPED). Provider/Embedding/Database persist on
|
|
534
|
+
// their own success handlers.
|
|
535
|
+
if (wizardState.step === 'connectors') {
|
|
536
|
+
persistStep('CONNECTORS', wizardState.connectedCount > 0 ? 'DONE' : 'SKIPPED', {});
|
|
537
|
+
}
|
|
538
|
+
setOnbStep(n.dataset.obNext);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
429
541
|
const b = e.target.closest('[data-ob-back]');
|
|
430
542
|
if (b) { e.preventDefault(); setOnbStep(b.dataset.obBack); return; }
|
|
431
543
|
});
|
|
@@ -450,7 +562,7 @@ function setRoute(name) {
|
|
|
450
562
|
if (name === 'health') refreshHealth();
|
|
451
563
|
if (name === 'kb') refreshKb();
|
|
452
564
|
if (name === 'methods') refreshMethods();
|
|
453
|
-
if (name === 'settings') refreshEnv();
|
|
565
|
+
if (name === 'settings') { refreshEnv(); refreshSettingsClients(); }
|
|
454
566
|
if (name === 'devices') refreshDevices();
|
|
455
567
|
if (name === 'activity') { ensureActivityWs(); loadTraces(); }
|
|
456
568
|
}
|
|
@@ -564,6 +676,46 @@ async function refreshEnv() {
|
|
|
564
676
|
}
|
|
565
677
|
}
|
|
566
678
|
|
|
679
|
+
// ── Settings: coding agents ──────────────────────────────────────────
|
|
680
|
+
// Same flow as the onboarding CONNECTORS step, surfaced post-onboarding so
|
|
681
|
+
// users who skipped the step (or completed setup before this card existed)
|
|
682
|
+
// can still wire up Claude Code / Cursor / Codex / Kiro / Hermes.
|
|
683
|
+
async function refreshSettingsClients() {
|
|
684
|
+
const host = $('#settings-connectors');
|
|
685
|
+
if (!host) return;
|
|
686
|
+
try {
|
|
687
|
+
const { connectors } = await rpc('listConnectors');
|
|
688
|
+
host.innerHTML = '';
|
|
689
|
+
if (!connectors.length) {
|
|
690
|
+
host.innerHTML = '<div class="muted">no agents registered</div>';
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
connectors.forEach((c) => host.appendChild(connectorCard(c, onSettingsClientAction)));
|
|
694
|
+
} catch (err) {
|
|
695
|
+
host.innerHTML = `<div class="muted">could not load agents: ${escape(err.message)}</div>`;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function onSettingsClientAction(id, action) {
|
|
700
|
+
const host = $('#settings-connectors');
|
|
701
|
+
const card = host?.querySelector(`[data-id="${id}"]`);
|
|
702
|
+
if (action === 'disconnect') {
|
|
703
|
+
try {
|
|
704
|
+
await rpc('disconnectConnector', { id });
|
|
705
|
+
toast({ variant: 'success', message: `${id} disconnected` });
|
|
706
|
+
} catch (err) { toast({ variant: 'error', message: err.message, hint: err.hint, code: err.code }); }
|
|
707
|
+
return refreshSettingsClients();
|
|
708
|
+
}
|
|
709
|
+
if (card) card.replaceWith(connectorCard({ id, label: id, hint: '', uiState: 'connecting' }, onSettingsClientAction));
|
|
710
|
+
try {
|
|
711
|
+
await rpc('connectConnector', { id });
|
|
712
|
+
toast({ variant: 'success', message: `${id} connected` });
|
|
713
|
+
} catch (err) {
|
|
714
|
+
toast({ variant: 'error', message: err.message || `could not connect ${id}`, hint: err.hint, code: err.code });
|
|
715
|
+
}
|
|
716
|
+
return refreshSettingsClients();
|
|
717
|
+
}
|
|
718
|
+
|
|
567
719
|
// ── Settings: live provider switcher (LLM + embedding) ───────────────
|
|
568
720
|
// Reuses the wizard's provider catalogs + the shared applyEmbeddingProvider
|
|
569
721
|
// gate. "Apply" persists config and restarts the daemon so the new pool /
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanilla design-system primitives (translated from Sigil.zip's ui_kit). Each
|
|
3
|
+
* returns a DOM node. Used by the onboarding wizard so every screen is composed
|
|
4
|
+
* from the same on-brand parts: status square+word, connector cards, DB-flow
|
|
5
|
+
* rows. No framework, no build step.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const E = (tag, cls, text) => {
|
|
9
|
+
const el = document.createElement(tag);
|
|
10
|
+
if (cls) el.className = cls;
|
|
11
|
+
if (text != null) el.textContent = text;
|
|
12
|
+
return el;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Status square + lowercase word. kind: ok|warn|danger|muted */
|
|
16
|
+
export function statusDot(kind, word) {
|
|
17
|
+
const wrap = E('span', `status-dot ${kind}`);
|
|
18
|
+
wrap.append(E('span', 'sq'), E('span', null, word));
|
|
19
|
+
return wrap;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CONNECTOR_STATUS = {
|
|
23
|
+
connected: { kind: 'ok', word: 'connected' },
|
|
24
|
+
available: { kind: 'muted', word: 'available' },
|
|
25
|
+
unavailable: { kind: 'muted', word: 'not installed' },
|
|
26
|
+
connecting: { kind: 'warn', word: 'connecting…' },
|
|
27
|
+
error: { kind: 'danger', word: 'error' },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Connector card. `onAction(id, action)` is called with action 'connect' |
|
|
32
|
+
* 'disconnect' | 'retry'. `state` overrides the derived status (e.g. while a
|
|
33
|
+
* connect is in flight pass 'connecting').
|
|
34
|
+
*/
|
|
35
|
+
export function connectorCard(c, onAction) {
|
|
36
|
+
const status = c.uiState || c.status;
|
|
37
|
+
const card = E('div', `connector-card ${status === 'unavailable' ? 'unavailable' : ''}`);
|
|
38
|
+
card.dataset.id = c.id;
|
|
39
|
+
|
|
40
|
+
const top = E('div', 'cc-top');
|
|
41
|
+
top.append(E('div', 'cc-name', c.label));
|
|
42
|
+
const meta = CONNECTOR_STATUS[status] || CONNECTOR_STATUS.available;
|
|
43
|
+
top.append(statusDot(meta.kind, meta.word));
|
|
44
|
+
card.append(top);
|
|
45
|
+
|
|
46
|
+
card.append(E('div', 'cc-hint', c.reason && status === 'error' ? c.reason : c.hint));
|
|
47
|
+
|
|
48
|
+
const actions = E('div', 'cc-actions');
|
|
49
|
+
if (status === 'connected') {
|
|
50
|
+
const b = E('button', 'btn danger', 'Disconnect');
|
|
51
|
+
b.type = 'button';
|
|
52
|
+
b.onclick = () => onAction(c.id, 'disconnect');
|
|
53
|
+
actions.append(b);
|
|
54
|
+
} else if (status === 'connecting') {
|
|
55
|
+
const b = E('button', 'btn', 'Connecting…');
|
|
56
|
+
b.type = 'button'; b.disabled = true;
|
|
57
|
+
actions.append(b);
|
|
58
|
+
} else if (status === 'unavailable') {
|
|
59
|
+
// no action — not installed on this machine
|
|
60
|
+
} else {
|
|
61
|
+
const b = E('button', status === 'error' ? 'btn' : 'btn primary', status === 'error' ? 'Retry' : 'Connect');
|
|
62
|
+
b.type = 'button';
|
|
63
|
+
b.onclick = () => onAction(c.id, status === 'error' ? 'retry' : 'connect');
|
|
64
|
+
actions.append(b);
|
|
65
|
+
}
|
|
66
|
+
card.append(actions);
|
|
67
|
+
return card;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A single row in the DB guided flow. phase: pending|active|done|error.
|
|
72
|
+
* Returns the row element; update via setFlowRow().
|
|
73
|
+
*/
|
|
74
|
+
export function dbFlowRow(id, label) {
|
|
75
|
+
const row = E('div', 'db-flow-row pending');
|
|
76
|
+
row.dataset.row = id;
|
|
77
|
+
row.append(E('span', 'step-sq'));
|
|
78
|
+
row.append(E('span', 'step-label', label));
|
|
79
|
+
row.append(E('span', 'step-detail', ''));
|
|
80
|
+
return row;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function setFlowRow(container, id, { phase, detail } = {}) {
|
|
84
|
+
const row = container.querySelector(`[data-row="${id}"]`);
|
|
85
|
+
if (!row) return;
|
|
86
|
+
if (phase) row.className = `db-flow-row ${phase}`;
|
|
87
|
+
if (detail != null) row.querySelector('.step-detail').textContent = detail;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { E };
|