reasonix 0.12.8 → 0.12.13
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/dashboard/app.css +123 -0
- package/dashboard/app.js +486 -25
- package/dist/cli/index.js +8226 -7980
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +29 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dashboard/app.js
CHANGED
|
@@ -1164,11 +1164,54 @@ function ChatPanel() {
|
|
|
1164
1164
|
// the input area.
|
|
1165
1165
|
const [stats, setStats] = useState(null);
|
|
1166
1166
|
const [overviewModel, setOverviewModel] = useState(null);
|
|
1167
|
+
// Whether the project has a built semantic index. Null = unknown
|
|
1168
|
+
// (poll hasn't landed) or non-attached. False = no index → show the
|
|
1169
|
+
// dismissible banner. True = index built → hide it.
|
|
1170
|
+
const [semanticIndex, setSemanticIndex] = useState(null);
|
|
1171
|
+
const [semanticBannerDismissed, setSemanticBannerDismissed] = useState(() => {
|
|
1172
|
+
try {
|
|
1173
|
+
return localStorage.getItem("rx.semanticBannerDismissed") === "1";
|
|
1174
|
+
} catch {
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
useEffect(() => {
|
|
1179
|
+
try {
|
|
1180
|
+
localStorage.setItem("rx.semanticBannerDismissed", semanticBannerDismissed ? "1" : "0");
|
|
1181
|
+
} catch {
|
|
1182
|
+
/* ignore */
|
|
1183
|
+
}
|
|
1184
|
+
}, [semanticBannerDismissed]);
|
|
1185
|
+
// Wall-clock timestamp the current turn started at — populated when
|
|
1186
|
+
// busy flips true, cleared when it flips false. Drives the "elapsed
|
|
1187
|
+
// Ns" readout in the in-flight indicator. Refreshed once per second
|
|
1188
|
+
// by `nowTick` so the seconds counter ticks visibly even between
|
|
1189
|
+
// SSE deltas.
|
|
1190
|
+
const [turnStartedAt, setTurnStartedAt] = useState(null);
|
|
1191
|
+
const [nowTick, setNowTick] = useState(0);
|
|
1192
|
+
useEffect(() => {
|
|
1193
|
+
if (!busy) return;
|
|
1194
|
+
const id = setInterval(() => setNowTick((n) => n + 1), 500);
|
|
1195
|
+
return () => clearInterval(id);
|
|
1196
|
+
}, [busy]);
|
|
1197
|
+
useEffect(() => {
|
|
1198
|
+
if (busy) {
|
|
1199
|
+
if (!turnStartedAt) setTurnStartedAt(Date.now());
|
|
1200
|
+
} else {
|
|
1201
|
+
setTurnStartedAt(null);
|
|
1202
|
+
}
|
|
1203
|
+
}, [busy, turnStartedAt]);
|
|
1167
1204
|
// Sticks to bottom only while the user is already near the bottom.
|
|
1168
1205
|
// Once they scroll up to read older content the streaming deltas no
|
|
1169
1206
|
// longer yank the view back. Re-armed when they scroll back to the
|
|
1170
1207
|
// bottom on their own. 80px threshold absorbs sub-pixel rounding.
|
|
1171
1208
|
const shouldAutoScroll = useRef(true);
|
|
1209
|
+
// Ref to the scrollable feed container so we don't have to rely on
|
|
1210
|
+
// a global querySelector (which would race the conditional render
|
|
1211
|
+
// — `.chat-feed` only mounts when at least one message is present).
|
|
1212
|
+
// The feed is now always rendered, so `feedRef.current` is set on
|
|
1213
|
+
// first paint and the scroll listener attaches once.
|
|
1214
|
+
const feedRef = useRef(null);
|
|
1172
1215
|
|
|
1173
1216
|
// Initial snapshot — messages + busy + any modal already up.
|
|
1174
1217
|
useEffect(() => {
|
|
@@ -1379,10 +1422,22 @@ function ChatPanel() {
|
|
|
1379
1422
|
// immediately. The threshold is generous enough that overshoot
|
|
1380
1423
|
// (smooth-scroll rebound, sub-pixel rounding) doesn't accidentally
|
|
1381
1424
|
// re-arm tracking when the user is barely above bottom.
|
|
1425
|
+
//
|
|
1426
|
+
// We also distinguish *user* scroll events from auto-scroll's own
|
|
1427
|
+
// programmatic `scrollTop = scrollHeight` writes. Without that gate
|
|
1428
|
+
// the auto-scroll effect would briefly snap to bottom, fire its
|
|
1429
|
+
// own scroll event, re-set shouldAutoScroll = true, then wonder
|
|
1430
|
+
// why the user complained that they couldn't scroll up — because
|
|
1431
|
+
// every wheel-up was racing against the next delta's auto-snap.
|
|
1432
|
+
// We mark the ref as `auto-scrolling` for one tick around the
|
|
1433
|
+
// programmatic write; the listener ignores events it sees during
|
|
1434
|
+
// that window.
|
|
1435
|
+
const autoScrollInFlight = useRef(false);
|
|
1382
1436
|
useEffect(() => {
|
|
1383
|
-
const el =
|
|
1437
|
+
const el = feedRef.current;
|
|
1384
1438
|
if (!el) return;
|
|
1385
1439
|
const onScroll = () => {
|
|
1440
|
+
if (autoScrollInFlight.current) return;
|
|
1386
1441
|
const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
1387
1442
|
shouldAutoScroll.current = distFromBottom < 80;
|
|
1388
1443
|
};
|
|
@@ -1394,8 +1449,16 @@ function ChatPanel() {
|
|
|
1394
1449
|
// deltas no longer yank the view back; manual wheel/drag wins.
|
|
1395
1450
|
useEffect(() => {
|
|
1396
1451
|
if (!shouldAutoScroll.current) return;
|
|
1397
|
-
const el =
|
|
1398
|
-
if (el)
|
|
1452
|
+
const el = feedRef.current;
|
|
1453
|
+
if (!el) return;
|
|
1454
|
+
autoScrollInFlight.current = true;
|
|
1455
|
+
el.scrollTop = el.scrollHeight;
|
|
1456
|
+
// Clear the gate after the browser has had a chance to fire the
|
|
1457
|
+
// resulting scroll event (microtask-ish — rAF is overkill, a 0ms
|
|
1458
|
+
// setTimeout is enough to land after the synchronous handler).
|
|
1459
|
+
setTimeout(() => {
|
|
1460
|
+
autoScrollInFlight.current = false;
|
|
1461
|
+
}, 0);
|
|
1399
1462
|
}, [messages, streaming]);
|
|
1400
1463
|
|
|
1401
1464
|
const allMessages = streaming
|
|
@@ -1440,6 +1503,7 @@ function ChatPanel() {
|
|
|
1440
1503
|
setEffortLocal(o.reasoningEffort ?? null);
|
|
1441
1504
|
setStats(o.stats ?? null);
|
|
1442
1505
|
setOverviewModel(o.model ?? null);
|
|
1506
|
+
setSemanticIndex(o.semanticIndexExists);
|
|
1443
1507
|
} catch {
|
|
1444
1508
|
/* swallow */
|
|
1445
1509
|
}
|
|
@@ -1574,11 +1638,31 @@ function ChatPanel() {
|
|
|
1574
1638
|
</div>
|
|
1575
1639
|
|
|
1576
1640
|
${
|
|
1577
|
-
busy
|
|
1578
|
-
? html`<div class="chat-status"><span class="
|
|
1579
|
-
:
|
|
1580
|
-
|
|
1581
|
-
|
|
1641
|
+
!busy && statusLine
|
|
1642
|
+
? html`<div class="chat-status"><span class="muted">${statusLine}</span></div>`
|
|
1643
|
+
: null
|
|
1644
|
+
}
|
|
1645
|
+
${
|
|
1646
|
+
semanticIndex === false && !semanticBannerDismissed
|
|
1647
|
+
? html`<div class="chat-banner">
|
|
1648
|
+
<span class="chat-banner-icon">≈</span>
|
|
1649
|
+
<span class="chat-banner-text">
|
|
1650
|
+
<strong>Semantic search isn't enabled for this project.</strong>
|
|
1651
|
+
<span class="muted">
|
|
1652
|
+
Build the index once and the model can find code by meaning ("where do we handle auth failures?") instead of grep on exact strings.
|
|
1653
|
+
</span>
|
|
1654
|
+
</span>
|
|
1655
|
+
<button
|
|
1656
|
+
class="primary"
|
|
1657
|
+
onClick=${() => appBus.dispatchEvent(new CustomEvent("navigate-tab", { detail: { tabId: "semantic" } }))}
|
|
1658
|
+
>Build it →</button>
|
|
1659
|
+
<button
|
|
1660
|
+
class="chat-banner-close"
|
|
1661
|
+
onClick=${() => setSemanticBannerDismissed(true)}
|
|
1662
|
+
title="dismiss (don't show again)"
|
|
1663
|
+
>×</button>
|
|
1664
|
+
</div>`
|
|
1665
|
+
: null
|
|
1582
1666
|
}
|
|
1583
1667
|
${error ? html`<div class="notice err">${error}</div>` : null}
|
|
1584
1668
|
|
|
@@ -1596,23 +1680,23 @@ function ChatPanel() {
|
|
|
1596
1680
|
: null
|
|
1597
1681
|
}
|
|
1598
1682
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1683
|
+
<div class="chat-feed" ref=${feedRef}>
|
|
1684
|
+
${
|
|
1685
|
+
allMessages.length === 0
|
|
1686
|
+
? html`<div class="chat-empty">
|
|
1687
|
+
No conversation yet. Send a prompt below to begin.
|
|
1688
|
+
</div>`
|
|
1689
|
+
: allMessages.map(
|
|
1690
|
+
(m) => html`
|
|
1691
|
+
<${ChatMessage}
|
|
1692
|
+
key=${m.id}
|
|
1693
|
+
msg=${m}
|
|
1694
|
+
streaming=${streaming && streaming.id === m.id}
|
|
1695
|
+
/>
|
|
1696
|
+
`,
|
|
1697
|
+
)
|
|
1698
|
+
}
|
|
1699
|
+
</div>
|
|
1616
1700
|
|
|
1617
1701
|
<div class="chat-input-area">
|
|
1618
1702
|
<textarea
|
|
@@ -1636,11 +1720,65 @@ function ChatPanel() {
|
|
|
1636
1720
|
</div>
|
|
1637
1721
|
</div>
|
|
1638
1722
|
|
|
1723
|
+
${
|
|
1724
|
+
busy
|
|
1725
|
+
? html`<${InFlightRow}
|
|
1726
|
+
streaming=${streaming}
|
|
1727
|
+
startedAt=${turnStartedAt}
|
|
1728
|
+
statusLine=${statusLine}
|
|
1729
|
+
onAbort=${abort}
|
|
1730
|
+
tick=${nowTick}
|
|
1731
|
+
/>`
|
|
1732
|
+
: null
|
|
1733
|
+
}
|
|
1639
1734
|
<${ChatStatusBar} stats=${stats} model=${overviewModel} />
|
|
1640
1735
|
</div>
|
|
1641
1736
|
`;
|
|
1642
1737
|
}
|
|
1643
1738
|
|
|
1739
|
+
// Live "what's the model doing right now" strip. Lives just above the
|
|
1740
|
+
// ChatStatusBar so the user's eyes don't have to leave the input area
|
|
1741
|
+
// to see whether the turn is alive — ticks every 500ms via the parent's
|
|
1742
|
+
// nowTick so the seconds counter shows visible motion even when the
|
|
1743
|
+
// SSE stream is silent (model thinking, waiting on a tool, etc).
|
|
1744
|
+
function InFlightRow({ streaming, startedAt, statusLine, onAbort, tick: _tick }) {
|
|
1745
|
+
const elapsedMs = startedAt ? Date.now() - startedAt : 0;
|
|
1746
|
+
const elapsed = (elapsedMs / 1000).toFixed(1);
|
|
1747
|
+
const reasoningLen = streaming?.reasoning?.length ?? 0;
|
|
1748
|
+
const textLen = streaming?.text?.length ?? 0;
|
|
1749
|
+
const phase =
|
|
1750
|
+
reasoningLen > 0 && textLen === 0 ? "thinking" : textLen > 0 ? "streaming" : "waiting";
|
|
1751
|
+
return html`
|
|
1752
|
+
<div class="chat-inflight">
|
|
1753
|
+
<span class="spinner"></span>
|
|
1754
|
+
<span class="chat-inflight-phase">${phase}</span>
|
|
1755
|
+
<span class="chat-inflight-sep">·</span>
|
|
1756
|
+
<span class="muted">${elapsed}s</span>
|
|
1757
|
+
${
|
|
1758
|
+
textLen > 0 || reasoningLen > 0
|
|
1759
|
+
? html`
|
|
1760
|
+
<span class="chat-inflight-sep">·</span>
|
|
1761
|
+
<span class="muted">
|
|
1762
|
+
${reasoningLen > 0 ? html`reasoning ${reasoningLen.toLocaleString()} ch` : null}
|
|
1763
|
+
${reasoningLen > 0 && textLen > 0 ? html`<span> · </span>` : null}
|
|
1764
|
+
${textLen > 0 ? html`out ${textLen.toLocaleString()} ch` : null}
|
|
1765
|
+
</span>
|
|
1766
|
+
`
|
|
1767
|
+
: null
|
|
1768
|
+
}
|
|
1769
|
+
${
|
|
1770
|
+
statusLine
|
|
1771
|
+
? html`
|
|
1772
|
+
<span class="chat-inflight-sep">·</span>
|
|
1773
|
+
<span class="muted">${statusLine}</span>
|
|
1774
|
+
`
|
|
1775
|
+
: null
|
|
1776
|
+
}
|
|
1777
|
+
<button class="chat-inflight-abort" onClick=${onAbort}>Abort (Esc)</button>
|
|
1778
|
+
</div>
|
|
1779
|
+
`;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1644
1782
|
// ---------- Chat status bar ----------
|
|
1645
1783
|
//
|
|
1646
1784
|
// Mirrors the TUI's StatsPanel — turn / session cost, cache hit %,
|
|
@@ -2722,6 +2860,321 @@ function SkillsPanel() {
|
|
|
2722
2860
|
|
|
2723
2861
|
// ---------- MCP ----------
|
|
2724
2862
|
|
|
2863
|
+
// ---------- Semantic index ----------
|
|
2864
|
+
|
|
2865
|
+
function SemanticPanel() {
|
|
2866
|
+
const [data, setData] = useState(null);
|
|
2867
|
+
const [error, setError] = useState(null);
|
|
2868
|
+
const [busy, setBusy] = useState(false);
|
|
2869
|
+
const [info, setInfo] = useState(null);
|
|
2870
|
+
|
|
2871
|
+
const load = useCallback(async () => {
|
|
2872
|
+
try {
|
|
2873
|
+
const r = await api("/semantic");
|
|
2874
|
+
setData(r);
|
|
2875
|
+
} catch (err) {
|
|
2876
|
+
setError(err.message);
|
|
2877
|
+
}
|
|
2878
|
+
}, []);
|
|
2879
|
+
|
|
2880
|
+
// Poll fast while a job is running OR while ollama is pulling a
|
|
2881
|
+
// model (the latest-line readout updates every few hundred ms during
|
|
2882
|
+
// a download). Slow when idle so the panel doesn't burn network just
|
|
2883
|
+
// sitting open in a tab.
|
|
2884
|
+
useEffect(() => {
|
|
2885
|
+
load();
|
|
2886
|
+
const phase = data?.job?.phase;
|
|
2887
|
+
const running = phase === "scan" || phase === "embed" || phase === "write";
|
|
2888
|
+
const pulling = data?.pull?.status === "pulling";
|
|
2889
|
+
const ms = running || pulling ? 1200 : 5000;
|
|
2890
|
+
const id = setInterval(load, ms);
|
|
2891
|
+
return () => clearInterval(id);
|
|
2892
|
+
}, [load, data?.job?.phase, data?.pull?.status]);
|
|
2893
|
+
|
|
2894
|
+
const start = useCallback(
|
|
2895
|
+
async (rebuild) => {
|
|
2896
|
+
setBusy(true);
|
|
2897
|
+
setError(null);
|
|
2898
|
+
setInfo(null);
|
|
2899
|
+
try {
|
|
2900
|
+
await api("/semantic/start", { method: "POST", body: { rebuild: !!rebuild } });
|
|
2901
|
+
setInfo(rebuild ? "rebuild started" : "incremental index started");
|
|
2902
|
+
await load();
|
|
2903
|
+
} catch (err) {
|
|
2904
|
+
setError(err.message);
|
|
2905
|
+
} finally {
|
|
2906
|
+
setBusy(false);
|
|
2907
|
+
}
|
|
2908
|
+
},
|
|
2909
|
+
[load],
|
|
2910
|
+
);
|
|
2911
|
+
|
|
2912
|
+
const stop = useCallback(async () => {
|
|
2913
|
+
setBusy(true);
|
|
2914
|
+
setError(null);
|
|
2915
|
+
try {
|
|
2916
|
+
await api("/semantic/stop", { method: "POST", body: {} });
|
|
2917
|
+
setInfo("stopping requested — current chunk batch will finish first");
|
|
2918
|
+
await load();
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
setError(err.message);
|
|
2921
|
+
} finally {
|
|
2922
|
+
setBusy(false);
|
|
2923
|
+
}
|
|
2924
|
+
}, [load]);
|
|
2925
|
+
|
|
2926
|
+
const startDaemon = useCallback(async () => {
|
|
2927
|
+
setBusy(true);
|
|
2928
|
+
setError(null);
|
|
2929
|
+
setInfo("starting ollama daemon (15s timeout)…");
|
|
2930
|
+
try {
|
|
2931
|
+
const r = await api("/semantic/ollama/start", { method: "POST", body: {} });
|
|
2932
|
+
setInfo(
|
|
2933
|
+
r.ready ? "daemon is up" : "daemon didn't come up in time — check `ollama serve` manually",
|
|
2934
|
+
);
|
|
2935
|
+
await load();
|
|
2936
|
+
} catch (err) {
|
|
2937
|
+
setError(err.message);
|
|
2938
|
+
} finally {
|
|
2939
|
+
setBusy(false);
|
|
2940
|
+
}
|
|
2941
|
+
}, [load]);
|
|
2942
|
+
|
|
2943
|
+
const pullModel = useCallback(
|
|
2944
|
+
async (model) => {
|
|
2945
|
+
setBusy(true);
|
|
2946
|
+
setError(null);
|
|
2947
|
+
setInfo(`pulling ${model} — this may take a few minutes on first install`);
|
|
2948
|
+
try {
|
|
2949
|
+
await api("/semantic/ollama/pull", { method: "POST", body: { model } });
|
|
2950
|
+
await load();
|
|
2951
|
+
} catch (err) {
|
|
2952
|
+
setError(err.message);
|
|
2953
|
+
} finally {
|
|
2954
|
+
setBusy(false);
|
|
2955
|
+
}
|
|
2956
|
+
},
|
|
2957
|
+
[load],
|
|
2958
|
+
);
|
|
2959
|
+
|
|
2960
|
+
if (!data && !error) return html`<div class="boot">loading semantic status…</div>`;
|
|
2961
|
+
if (error && !data) return html`<div class="notice err">${error}</div>`;
|
|
2962
|
+
|
|
2963
|
+
if (data && !data.attached) {
|
|
2964
|
+
return html`
|
|
2965
|
+
<div>
|
|
2966
|
+
<div class="panel-header">
|
|
2967
|
+
<h2 class="panel-title">Semantic</h2>
|
|
2968
|
+
<span class="panel-subtitle">code-mode required</span>
|
|
2969
|
+
</div>
|
|
2970
|
+
<div class="empty">${data.reason}</div>
|
|
2971
|
+
</div>
|
|
2972
|
+
`;
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
const job = data.job;
|
|
2976
|
+
const phase = job?.phase;
|
|
2977
|
+
const running = phase === "scan" || phase === "embed" || phase === "write";
|
|
2978
|
+
const pull = data.pull;
|
|
2979
|
+
const pulling = pull?.status === "pulling";
|
|
2980
|
+
|
|
2981
|
+
// Tri-state Ollama check. Each level gates the next:
|
|
2982
|
+
// binary missing → user must install (we won't run a package
|
|
2983
|
+
// manager on their behalf).
|
|
2984
|
+
// daemon down → one-click start (`ollama serve`).
|
|
2985
|
+
// model missing → one-click pull.
|
|
2986
|
+
// all good → ready to index.
|
|
2987
|
+
const o = data.ollama ?? {};
|
|
2988
|
+
const binaryFound = o.binaryFound === true;
|
|
2989
|
+
const daemonRunning = o.daemonRunning === true;
|
|
2990
|
+
const modelPulled = o.modelPulled === true;
|
|
2991
|
+
const modelName = o.modelName ?? "nomic-embed-text";
|
|
2992
|
+
const installedModels = o.installedModels ?? [];
|
|
2993
|
+
const ready = binaryFound && daemonRunning && modelPulled;
|
|
2994
|
+
|
|
2995
|
+
return html`
|
|
2996
|
+
<div>
|
|
2997
|
+
<div class="panel-header">
|
|
2998
|
+
<h2 class="panel-title">Semantic</h2>
|
|
2999
|
+
<span class="panel-subtitle">${data.index.exists ? "index built" : "no index yet"}</span>
|
|
3000
|
+
</div>
|
|
3001
|
+
${info ? html`<div class="notice">${info}</div>` : null}
|
|
3002
|
+
${error ? html`<div class="notice err">${error}</div>` : null}
|
|
3003
|
+
|
|
3004
|
+
<div class="section-title">Status</div>
|
|
3005
|
+
<div class="kv">
|
|
3006
|
+
<div><span class="kv-key">project</span><code>${data.root}</code></div>
|
|
3007
|
+
<div>
|
|
3008
|
+
<span class="kv-key">ollama</span>
|
|
3009
|
+
${
|
|
3010
|
+
binaryFound
|
|
3011
|
+
? daemonRunning
|
|
3012
|
+
? html`<span class="pill pill-ok">reachable</span><span class="muted" style="margin-left: 8px;">${installedModels.length} model(s)${
|
|
3013
|
+
installedModels.length > 0
|
|
3014
|
+
? ` · ${installedModels.slice(0, 3).join(", ")}${installedModels.length > 3 ? "…" : ""}`
|
|
3015
|
+
: ""
|
|
3016
|
+
}</span>`
|
|
3017
|
+
: html`<span class="pill pill-warn">daemon down</span><span class="muted" style="margin-left: 8px;">binary on PATH but not serving</span>`
|
|
3018
|
+
: html`<span class="pill pill-err">not installed</span><span class="muted" style="margin-left: 8px;">${o.error ?? "ollama binary not on PATH"}</span>`
|
|
3019
|
+
}
|
|
3020
|
+
</div>
|
|
3021
|
+
<div>
|
|
3022
|
+
<span class="kv-key">model</span>
|
|
3023
|
+
<code>${modelName}</code>
|
|
3024
|
+
${
|
|
3025
|
+
modelPulled
|
|
3026
|
+
? html`<span class="pill pill-ok" style="margin-left: 8px;">pulled</span>`
|
|
3027
|
+
: daemonRunning
|
|
3028
|
+
? html`<span class="pill pill-warn" style="margin-left: 8px;">not pulled</span>`
|
|
3029
|
+
: html`<span class="pill pill-dim" style="margin-left: 8px;">unknown (daemon down)</span>`
|
|
3030
|
+
}
|
|
3031
|
+
</div>
|
|
3032
|
+
<div>
|
|
3033
|
+
<span class="kv-key">index</span>
|
|
3034
|
+
${
|
|
3035
|
+
data.index.exists
|
|
3036
|
+
? html`<span class="muted">present at <code>.reasonix/semantic/</code></span>`
|
|
3037
|
+
: html`<span class="muted">none — run an index to enable <code>semantic_search</code></span>`
|
|
3038
|
+
}
|
|
3039
|
+
</div>
|
|
3040
|
+
</div>
|
|
3041
|
+
|
|
3042
|
+
${
|
|
3043
|
+
!binaryFound
|
|
3044
|
+
? html`
|
|
3045
|
+
<div class="section-title">Install Ollama</div>
|
|
3046
|
+
<div class="card" style="font-size: 13px;">
|
|
3047
|
+
Reasonix doesn't run package managers for you. Install Ollama
|
|
3048
|
+
first, then come back to this panel:
|
|
3049
|
+
<ul style="margin: 10px 0 4px 18px; padding: 0;">
|
|
3050
|
+
<li><strong>macOS / Windows:</strong> download from <a href="https://ollama.com/download" target="_blank" rel="noreferrer">ollama.com/download</a></li>
|
|
3051
|
+
<li><strong>Linux:</strong> <code>curl -fsSL https://ollama.com/install.sh | sh</code></li>
|
|
3052
|
+
</ul>
|
|
3053
|
+
<div class="muted" style="margin-top: 8px;">After install, this panel will offer to start the daemon and pull <code>${modelName}</code> for you. Refresh after installing.</div>
|
|
3054
|
+
</div>
|
|
3055
|
+
`
|
|
3056
|
+
: null
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
${
|
|
3060
|
+
binaryFound && !daemonRunning
|
|
3061
|
+
? html`
|
|
3062
|
+
<div class="section-title">Daemon</div>
|
|
3063
|
+
<div class="card" style="font-size: 13px;">
|
|
3064
|
+
<code>ollama</code> is on your PATH but the HTTP daemon isn't reachable.
|
|
3065
|
+
<div class="row" style="margin-top: 10px;">
|
|
3066
|
+
<button class="primary" disabled=${busy} onClick=${startDaemon}>Start daemon</button>
|
|
3067
|
+
<span class="muted" style="font-size: 12px; align-self: center;">runs <code>ollama serve</code> detached — survives Reasonix exit</span>
|
|
3068
|
+
</div>
|
|
3069
|
+
</div>
|
|
3070
|
+
`
|
|
3071
|
+
: null
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
${
|
|
3075
|
+
daemonRunning && !modelPulled
|
|
3076
|
+
? html`
|
|
3077
|
+
<div class="section-title">Model</div>
|
|
3078
|
+
<div class="card" style="font-size: 13px;">
|
|
3079
|
+
<code>${modelName}</code> isn't installed yet. ${pulling ? "" : "~270 MB download on first pull."}
|
|
3080
|
+
<div class="row" style="margin-top: 10px;">
|
|
3081
|
+
<button
|
|
3082
|
+
class="primary"
|
|
3083
|
+
disabled=${busy || pulling}
|
|
3084
|
+
onClick=${() => pullModel(modelName)}
|
|
3085
|
+
>${pulling ? "pulling…" : `Pull ${modelName}`}</button>
|
|
3086
|
+
</div>
|
|
3087
|
+
${
|
|
3088
|
+
pull
|
|
3089
|
+
? html`
|
|
3090
|
+
<div class="kv" style="margin-top: 10px;">
|
|
3091
|
+
<div>
|
|
3092
|
+
<span class="kv-key">status</span>
|
|
3093
|
+
<span class=${`pill ${pull.status === "done" ? "pill-ok" : pull.status === "error" ? "pill-err" : "pill-active"}`}>${pull.status}</span>
|
|
3094
|
+
<span class="muted" style="margin-left: 8px;">${((Date.now() - pull.startedAt) / 1000).toFixed(1)}s</span>
|
|
3095
|
+
</div>
|
|
3096
|
+
${
|
|
3097
|
+
pull.lastLine
|
|
3098
|
+
? html`<div><span class="kv-key">last</span><code style="font-size: 11.5px;">${pull.lastLine}</code></div>`
|
|
3099
|
+
: null
|
|
3100
|
+
}
|
|
3101
|
+
</div>
|
|
3102
|
+
`
|
|
3103
|
+
: null
|
|
3104
|
+
}
|
|
3105
|
+
</div>
|
|
3106
|
+
`
|
|
3107
|
+
: null
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
<div class="section-title">Job</div>
|
|
3111
|
+
${job ? html`<${SemanticJobView} job=${job} running=${running} />` : html`<div class="muted">No job has run in this dashboard yet.</div>`}
|
|
3112
|
+
|
|
3113
|
+
<div class="row" style="margin-top: 14px;">
|
|
3114
|
+
<button class="primary" disabled=${busy || running || !ready} onClick=${() => start(false)}>Index (incremental)</button>
|
|
3115
|
+
<button disabled=${busy || running || !ready} onClick=${() => start(true)}>Rebuild (wipe + full)</button>
|
|
3116
|
+
<button disabled=${busy || !running} onClick=${stop}>Stop</button>
|
|
3117
|
+
</div>
|
|
3118
|
+
</div>
|
|
3119
|
+
`;
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
function SemanticJobView({ job, running }) {
|
|
3123
|
+
const phaseLabel =
|
|
3124
|
+
{
|
|
3125
|
+
scan: "scanning files",
|
|
3126
|
+
embed: "embedding chunks",
|
|
3127
|
+
write: "writing index",
|
|
3128
|
+
done: "done",
|
|
3129
|
+
error: "error",
|
|
3130
|
+
}[job.phase] ?? job.phase;
|
|
3131
|
+
const total = job.chunksTotal ?? 0;
|
|
3132
|
+
const doneN = job.chunksDone ?? 0;
|
|
3133
|
+
const ratio = total > 0 ? Math.min(1, doneN / total) : 0;
|
|
3134
|
+
const elapsed = ((Date.now() - job.startedAt) / 1000).toFixed(1);
|
|
3135
|
+
|
|
3136
|
+
return html`
|
|
3137
|
+
<div class="kv">
|
|
3138
|
+
<div><span class="kv-key">phase</span>
|
|
3139
|
+
<span class=${`pill ${job.phase === "error" ? "pill-err" : running ? "pill-active" : "pill-dim"}`}>${phaseLabel}</span>
|
|
3140
|
+
${job.aborted ? html`<span class="pill pill-warn" style="margin-left: 6px;">stopping</span>` : null}
|
|
3141
|
+
<span class="muted" style="margin-left: 8px;">${elapsed}s</span>
|
|
3142
|
+
</div>
|
|
3143
|
+
${
|
|
3144
|
+
job.filesScanned !== null && job.filesScanned !== undefined
|
|
3145
|
+
? html`<div><span class="kv-key">files</span>scanned ${job.filesScanned}${
|
|
3146
|
+
job.filesChanged != null ? ` · changed ${job.filesChanged}` : ""
|
|
3147
|
+
}${job.filesSkipped ? ` · skipped ${job.filesSkipped}` : ""}</div>`
|
|
3148
|
+
: null
|
|
3149
|
+
}
|
|
3150
|
+
${
|
|
3151
|
+
total > 0
|
|
3152
|
+
? html`
|
|
3153
|
+
<div>
|
|
3154
|
+
<span class="kv-key">chunks</span>${doneN} / ${total} (${(ratio * 100).toFixed(0)}%)
|
|
3155
|
+
</div>
|
|
3156
|
+
<div class="bar" style="margin-top: 4px;">
|
|
3157
|
+
<div class="fill" style=${`width: ${(ratio * 100).toFixed(1)}%; background: var(--primary);`}></div>
|
|
3158
|
+
</div>
|
|
3159
|
+
`
|
|
3160
|
+
: null
|
|
3161
|
+
}
|
|
3162
|
+
${
|
|
3163
|
+
job.error
|
|
3164
|
+
? html`<div><span class="kv-key">error</span><span class="err">${job.error}</span></div>`
|
|
3165
|
+
: null
|
|
3166
|
+
}
|
|
3167
|
+
${
|
|
3168
|
+
job.result
|
|
3169
|
+
? html`<div><span class="kv-key">result</span>added ${job.result.chunksAdded} · removed ${job.result.chunksRemoved}${
|
|
3170
|
+
job.result.chunksSkipped ? ` · failed ${job.result.chunksSkipped}` : ""
|
|
3171
|
+
} · ${(job.result.durationMs / 1000).toFixed(1)}s</div>`
|
|
3172
|
+
: null
|
|
3173
|
+
}
|
|
3174
|
+
</div>
|
|
3175
|
+
`;
|
|
3176
|
+
}
|
|
3177
|
+
|
|
2725
3178
|
function McpPanel() {
|
|
2726
3179
|
const [data, setData] = useState(null);
|
|
2727
3180
|
const [specs, setSpecs] = useState(null);
|
|
@@ -3518,6 +3971,14 @@ const TABS = [
|
|
|
3518
3971
|
ready: true,
|
|
3519
3972
|
badge: null,
|
|
3520
3973
|
},
|
|
3974
|
+
{
|
|
3975
|
+
id: "semantic",
|
|
3976
|
+
name: "Semantic",
|
|
3977
|
+
glyph: "≈",
|
|
3978
|
+
panel: () => html`<${SemanticPanel} />`,
|
|
3979
|
+
ready: true,
|
|
3980
|
+
badge: null,
|
|
3981
|
+
},
|
|
3521
3982
|
{
|
|
3522
3983
|
id: "mcp",
|
|
3523
3984
|
name: "MCP",
|