@anmol-srv/sigil 0.11.0 → 0.12.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anmol-srv/sigil",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "description": "Local-first memory infrastructure for AI coding agents. One brain shared across Claude Code, Codex CLI, Cursor, Kiro, Continue, Cline, Windsurf, or any MCP client. Organized in pluggable pods, stored in your own Postgres. No cloud, no telemetry. Auto-captured from Claude Code via hooks; surfaced everywhere else as a 9-tool MCP server.",
6
6
  "bin": {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Central RPC client — the one door from the GUI to the daemon. Mirrors the
3
+ * cohort-live-web axios-wrapper convention: a single place that adds context
4
+ * and turns a structured daemon error ({code,message,hint}) into a toast.
5
+ *
6
+ * Pass { quiet: true } to suppress the auto-toast (e.g. when a caller renders
7
+ * the error inline). The thrown Error carries .code/.hint for callers.
8
+ */
9
+ import { toast } from './toast.js';
10
+
11
+ export async function rpc(method, params = {}, { quiet = false } = {}) {
12
+ let body;
13
+ try {
14
+ const res = await fetch('/api/v1/rpc', {
15
+ method: 'POST',
16
+ headers: { 'Content-Type': 'application/json' },
17
+ credentials: 'same-origin',
18
+ body: JSON.stringify({ method, params }),
19
+ });
20
+ body = await res.json();
21
+ } catch {
22
+ const e = {
23
+ code: 'NETWORK',
24
+ message: 'Could not reach the Sigil daemon.',
25
+ hint: 'Is it running? Try `sigil daemon status`.',
26
+ };
27
+ if (!quiet) toast({ variant: 'error', message: e.message, hint: e.hint, code: e.code });
28
+ throw Object.assign(new Error(e.message), e);
29
+ }
30
+
31
+ if (!body || body.ok !== true) {
32
+ const e = body?.error || { code: 'UNKNOWN', message: 'request failed' };
33
+ if (!quiet) toast({ variant: 'error', message: e.message, hint: e.hint, code: e.code });
34
+ throw Object.assign(new Error(e.message || 'request failed'), e);
35
+ }
36
+ return body.data;
37
+ }
@@ -5,45 +5,45 @@
5
5
  ════════════════════════════════════════════════════════════════════════ */
6
6
 
7
7
  :root {
8
- /* ── Color tokens (OKLCH-style flat dark) ──────────────────────────── */
9
- --bg-canvas: #07080a; /* page background */
10
- --bg-surface: #0f1115; /* card / panel */
11
- --bg-surface-2: #161920; /* nested panel / hover */
12
- --bg-surface-3: #1d2129; /* tertiary */
13
- --bg-overlay: rgba(7,8,10,0.78);
14
-
15
- --fg: #e8eaee;
16
- --fg-2: #b6bcc7;
17
- --fg-3: #828893;
18
- --fg-4: #565b66;
19
-
20
- --border: #1f2229;
21
- --border-2: #2c3038;
22
- --border-strong: #3a3f49;
23
-
24
- --accent: #5e8cff;
25
- --accent-2: #466ce0;
26
- --accent-fg: #ffffff;
27
-
28
- --ok: #4ec46c;
29
- --ok-bg: rgba(78,196,108,0.10);
30
- --warn: #e8a94a;
31
- --warn-bg: rgba(232,169,74,0.10);
32
- --err: #e26464;
33
- --err-bg: rgba(226,100,100,0.10);
34
- --info-bg: rgba(94,140,255,0.10);
35
-
36
- /* ── Spacing scale (multiples of 4px) ──────────────────────────────── */
37
- --s-1: 4px;
38
- --s-2: 8px;
39
- --s-3: 12px;
40
- --s-4: 16px;
41
- --s-5: 24px;
42
- --s-6: 32px;
43
- --s-7: 48px;
44
- --s-8: 64px;
45
-
46
- /* ── Type scale ────────────────────────────────────────────────────── */
8
+ /* ── Re-skinned onto the Sigil design system ───────────────────────────
9
+ design/colors_and_type.css (linked BEFORE this file) owns the brand:
10
+ palette, Geist/Geist Mono type, 4px spacing, sharp geometry. Here we
11
+ re-point this dashboard's local token names at those design tokens, so
12
+ every existing rule adopts the brand with no markup churn.
13
+
14
+ Names that ALSO exist in the design tokens (--fg-2/3/4, --border-2,
15
+ --border-strong, --ok, --warn, --font-mono, --focus-ring) are deliberately
16
+ NOT redefined here — the design values flow through unchanged. */
17
+
18
+ /* surfaces (app-local aliases → design surfaces) */
19
+ --bg-canvas: var(--bg-1);
20
+ --bg-surface: var(--surface-1);
21
+ --bg-surface-2: var(--surface-2);
22
+ --bg-surface-3: var(--surface-3);
23
+ --bg-overlay: rgba(8, 9, 11, 0.78);
24
+
25
+ /* text + borders */
26
+ --fg: var(--fg-1);
27
+ --border: var(--border-1);
28
+
29
+ /* brand / accent — the only chromatic UI color is brand blue */
30
+ --accent: var(--brand);
31
+ --accent-2: var(--brand-deep);
32
+ --accent-fg: #ffffff;
33
+
34
+ /* status: foreground --ok/--warn come from design tokens; backgrounds map
35
+ to the design's restrained tints (used only as small dot/row washes) */
36
+ --ok-bg: var(--ok-tint);
37
+ --warn-bg: var(--warn-tint);
38
+ --err: var(--danger);
39
+ --err-bg: var(--danger-tint);
40
+ --info-bg: var(--brand-tint);
41
+
42
+ /* spacing → design 4px grid */
43
+ --s-1: var(--sp-1); --s-2: var(--sp-2); --s-3: var(--sp-3); --s-4: var(--sp-4);
44
+ --s-5: var(--sp-5); --s-6: var(--sp-6); --s-7: var(--sp-7); --s-8: var(--sp-8);
45
+
46
+ /* type scale (px; hierarchy preserved) */
47
47
  --t-xs: 11px;
48
48
  --t-sm: 12px;
49
49
  --t-base: 13px;
@@ -55,21 +55,19 @@
55
55
  --lh: 1.5;
56
56
  --lh-tight: 1.25;
57
57
 
58
- /* ── Fonts ─────────────────────────────────────────────────────────── */
59
- --font-ui: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text",
60
- system-ui, "Segoe UI", Roboto, sans-serif;
61
- --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "Roboto Mono", monospace;
58
+ /* fonts Geist (UI) / Geist Mono (data). --font-mono flows from design. */
59
+ --font-ui: var(--font-sans);
62
60
 
63
- /* ── Layout ────────────────────────────────────────────────────────── */
64
- --container: 1200px;
65
- --sidebar: 232px;
66
- --header-h: 52px;
67
-
68
- /* ── Misc ──────────────────────────────────────────────────────────── */
69
- --focus-ring: 0 0 0 2px var(--accent);
61
+ /* layout header height matches the design's 64px top bar */
62
+ --container: 1200px;
63
+ --sidebar: 232px;
64
+ --header-h: var(--nav-h);
70
65
  }
71
66
 
72
67
  * { box-sizing: border-box; }
68
+ /* `hidden` must win over component display rules (e.g. .provider-card flex),
69
+ so JS-toggled cards/panes (Docker mode when unavailable) actually hide. */
70
+ [hidden] { display: none !important; }
73
71
  html, body { margin: 0; height: 100%; }
74
72
  body {
75
73
  background: var(--bg-canvas);
@@ -881,3 +879,69 @@ footer .footer-inner {
881
879
  .flex-row { display: flex; align-items: center; gap: var(--s-3); }
882
880
  .text-sm { font-size: var(--t-sm); }
883
881
  .text-xs { font-size: var(--t-xs); }
882
+
883
+ /* ════════════════════════════════════════════════════════════════════════
884
+ Design-system components (Phase 7/8) — toasts, connector cards, DB flow.
885
+ All sharp-edged, hairline-bordered, status as a 7px square + word.
886
+ ════════════════════════════════════════════════════════════════════════ */
887
+
888
+ /* ── Toasts ─────────────────────────────────────────────────────────────── */
889
+ .toast-stack {
890
+ position: fixed; top: var(--s-4); right: var(--s-4); z-index: 100;
891
+ display: flex; flex-direction: column; gap: var(--s-2); max-width: 400px;
892
+ }
893
+ .toast {
894
+ display: flex; gap: var(--s-3); align-items: flex-start;
895
+ background: var(--surface-1); border: 1px solid var(--border-2);
896
+ padding: var(--s-3) var(--s-4); box-shadow: var(--shadow-pop);
897
+ }
898
+ .toast-sq { width: 7px; height: 7px; margin-top: 6px; flex: none; background: var(--fg-3); }
899
+ .toast-error .toast-sq { background: var(--danger); }
900
+ .toast-success .toast-sq { background: var(--ok); }
901
+ .toast-info .toast-sq { background: var(--brand); }
902
+ .toast-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
903
+ .toast-msg { font-size: var(--t-base); color: var(--fg-1); line-height: 1.4; }
904
+ .toast-hint { font-size: var(--t-sm); color: var(--fg-3); line-height: 1.4; }
905
+ .toast-code {
906
+ align-self: flex-start; margin-top: 2px; font-family: var(--font-mono);
907
+ font-size: var(--t-xs); color: var(--fg-4); border: 1px solid var(--border-2);
908
+ padding: 1px 6px; border-radius: var(--radius-1);
909
+ }
910
+ .toast-x { background: none; border: none; color: var(--fg-4); cursor: pointer; font-size: 16px; line-height: 1; padding: 0 2px; }
911
+ .toast-x:hover { color: var(--fg-2); }
912
+
913
+ /* ── Connector cards (click-to-connect) ─────────────────────────────────── */
914
+ .connector-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--s-3); margin-top: var(--s-4); }
915
+ .connector-card {
916
+ display: flex; flex-direction: column; gap: var(--s-2);
917
+ border: 1px solid var(--border-2); background: var(--surface-1);
918
+ padding: var(--s-4); border-radius: var(--radius-0);
919
+ }
920
+ .connector-card .cc-top { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); }
921
+ .connector-card .cc-name { font-size: var(--t-md); font-weight: var(--w-semibold); color: var(--fg-1); }
922
+ .connector-card .cc-hint { font-size: var(--t-sm); color: var(--fg-3); line-height: 1.45; }
923
+ .connector-card.unavailable { opacity: 0.55; }
924
+ .connector-card .cc-actions { margin-top: var(--s-2); }
925
+
926
+ /* status square + word (shared) */
927
+ .status-dot { display: inline-flex; align-items: center; gap: 7px; font-size: var(--t-sm); font-weight: 500; }
928
+ .status-dot .sq { width: 7px; height: 7px; flex: none; background: var(--fg-3); }
929
+ .status-dot.ok { color: var(--ok); } .status-dot.ok .sq { background: var(--ok); }
930
+ .status-dot.warn { color: var(--warn); } .status-dot.warn .sq { background: var(--warn); }
931
+ .status-dot.danger { color: var(--danger); } .status-dot.danger .sq { background: var(--danger); }
932
+ .status-dot.muted { color: var(--fg-3); } .status-dot.muted .sq { background: var(--fg-4); }
933
+
934
+ /* ── DB guided flow (linear status rows, replaces button-toggling) ──────── */
935
+ .db-flow { border: 1px solid var(--border-2); background: var(--surface-1); margin-top: var(--s-4); }
936
+ .db-flow-row {
937
+ display: grid; grid-template-columns: 24px 1fr auto; align-items: center; gap: var(--s-3);
938
+ padding: var(--s-3) var(--s-4); border-bottom: 1px solid var(--border-1);
939
+ }
940
+ .db-flow-row:last-child { border-bottom: none; }
941
+ .db-flow-row .step-sq { width: 8px; height: 8px; background: var(--fg-4); justify-self: center; }
942
+ .db-flow-row.active .step-sq { background: var(--brand); }
943
+ .db-flow-row.done .step-sq { background: var(--ok); }
944
+ .db-flow-row.error .step-sq { background: var(--danger); }
945
+ .db-flow-row .step-label { font-size: var(--t-base); color: var(--fg-2); }
946
+ .db-flow-row.done .step-label, .db-flow-row.active .step-label { color: var(--fg-1); }
947
+ .db-flow-row .step-detail { font-family: var(--font-mono); font-size: var(--t-sm); color: var(--fg-3); }
@@ -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) throw new Error(body.error?.message || 'rpc error');
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: 'welcome', llmProvider: null, embProvider: null, llmProviders: [], embProviders: [] };
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
- // Update step list state — done/active/future
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 = order.indexOf(el.dataset.obStep);
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 provider lists when entering those steps
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
- // Pre-fill DB step's "next" enabled if already done
81
- if (state.steps.database.done) {
82
- $('#ob-db-next').disabled = false;
83
- }
84
- if (state.steps.llm.done) {
85
- $('#ob-llm-next').disabled = false;
86
- }
87
- if (state.steps.embedding.done) {
88
- $('#ob-emb-next').disabled = false;
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
- // ── DB step ──────────────────────────────────────────────────────────
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
- $('#ob-db-url').style.display = card.dataset.dbMode === 'url' ? '' : 'none';
104
- $('#ob-db-fields').style.display = card.dataset.dbMode === 'fields' ? '' : 'none';
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
- $('#db-mode-cards .provider-card')?.classList.add('selected');
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
- const isUrl = $('input[name="db-mode"]:checked').value === 'url';
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
- $('#ob-db-test')?.addEventListener('click', async () => {
121
- const out = $('#ob-db-result');
122
- out.hidden = false;
123
- out.className = 'result';
124
- out.textContent = 'testing…';
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
- const data = await rpc('testDbConnection', obDbParams());
127
- out.textContent = JSON.stringify(data, null, 2);
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
- $('#ob-db-install-pgv').hidden = true;
140
- $('#ob-db-migrate').hidden = true;
141
- $('#ob-db-next').disabled = true;
209
+ await runUrlFlow();
142
210
  }
143
- } catch (err) {
144
- out.textContent = `ERROR: ${err.message}`;
145
- out.classList.add('err');
211
+ } finally {
212
+ btn.disabled = false;
146
213
  }
147
214
  });
148
215
 
149
- $('#ob-db-install-pgv')?.addEventListener('click', async () => {
150
- const out = $('#ob-db-result');
151
- out.textContent += '\n\nInstalling pgvector…';
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 data = await rpc('ensurePgvector', obDbParams());
154
- if (data.ok && data.installed) {
155
- out.textContent += `\n✓ pgvector ${data.version} installed`;
156
- $('#ob-db-install-pgv').hidden = true;
157
- $('#ob-db-migrate').hidden = false;
158
- } else {
159
- out.textContent += `\n✗ ${data.error || 'unknown'} (${data.stage})`;
160
- }
161
- } catch (err) { out.textContent += `\nERROR: ${err.message}`; }
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
- $('#ob-db-migrate')?.addEventListener('click', async () => {
165
- const out = $('#ob-db-result');
166
- out.textContent += '\n\nPersisting connection to ~/.sigil/.env…';
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
- out.textContent += '\n✓ env written. Running migrations…';
182
- // Pass connection params so runMigrations uses a one-shot pool against
183
- // the new URL the daemon's existing pool is still bound to the
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
- out.textContent += `\n✗ ${err.message}`;
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: () => { $('#ob-emb-next').disabled = false; },
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
- try { await rpc('markOnboardingComplete'); }
420
- catch { /* ignore */ }
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
- refreshHealth();
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) { e.preventDefault(); setOnbStep(n.dataset.obNext); return; }
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
  });