@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/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 +203 -91
- 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 +54 -42
- package/src/gui/web/toast.js +62 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anmol-srv/sigil",
|
|
3
|
-
"version": "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
|
+
}
|
package/src/gui/web/app.css
CHANGED
|
@@ -5,45 +5,45 @@
|
|
|
5
5
|
════════════════════════════════════════════════════════════════════════ */
|
|
6
6
|
|
|
7
7
|
:root {
|
|
8
|
-
/* ──
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
--
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
--
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
|
|
34
|
-
--
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
--
|
|
38
|
-
--
|
|
39
|
-
--
|
|
40
|
-
--
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
--s-
|
|
44
|
-
--s-8:
|
|
45
|
-
|
|
46
|
-
/*
|
|
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
|
-
/*
|
|
59
|
-
--font-ui:
|
|
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
|
-
/*
|
|
64
|
-
--container:
|
|
65
|
-
--sidebar:
|
|
66
|
-
--header-h:
|
|
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); }
|
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
|
});
|