reasonix 0.31.0 → 0.32.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.
@@ -19637,16 +19637,34 @@ var en = {
19637
19637
  pullModel: "Pull {model}",
19638
19638
  indexStatus: "index status",
19639
19639
  builtStatus: "\u25CF built",
19640
+ incompatibleStatus: "\u25CF incompatible",
19640
19641
  chunks: "chunks",
19641
19642
  files: "files",
19642
19643
  dim: "dim",
19643
19644
  size: "size",
19644
19645
  lastBuild: "last build",
19646
+ builtWith: "built with",
19647
+ currentTarget: "current target",
19648
+ incompatibleHint: "This on-disk index was built for a different provider or model. Run Rebuild to replace it.",
19645
19649
  runIndexHint: "Run an index to enable semantic_search.",
19646
19650
  reIndex: "Re-index",
19647
19651
  build: "Build",
19648
19652
  rebuild: "Rebuild",
19649
19653
  stop: "Stop",
19654
+ provider: "provider",
19655
+ providerType: "service type",
19656
+ openaiCompat: "openai-compatible",
19657
+ apiUrl: "API URL",
19658
+ apiKey: "API key",
19659
+ apiKeyStoredNote: "API key is stored in ~/.reasonix/config.json \u2014 do not share that file.",
19660
+ customRequestBody: "custom request body",
19661
+ invalidCustomRequestBody: "Custom request body must be valid JSON: {error}",
19662
+ customRequestBodyMustBeObject: "Custom request body must be a JSON object.",
19663
+ saveBeforeIndex: "Save semantic settings before starting an index.",
19664
+ extraBody: "extra body",
19665
+ keepExistingKey: "leave blank to keep existing key",
19666
+ remoteProvider: "Remote embedding provider",
19667
+ remoteProviderDesc: "Configure the full OpenAI-compatible embeddings URL here. Reasonix will send requests exactly to the URL you provide.",
19650
19668
  ollama: "ollama",
19651
19669
  binary: "binary",
19652
19670
  found: "found",
@@ -19674,11 +19692,14 @@ var en = {
19674
19692
  nothingSkipped: "nothing skipped \u2014 all walked files would be indexed.",
19675
19693
  firstIncluded: "first {count} included file(s)",
19676
19694
  job: "Job",
19695
+ phaseSetup: "preparing",
19677
19696
  phaseScan: "scanning files",
19678
19697
  phaseEmbed: "embedding chunks",
19679
19698
  phaseWrite: "writing index",
19680
19699
  phaseDone: "done",
19681
19700
  phaseError: "error",
19701
+ phaseCancelled: "cancelled",
19702
+ setupFailed: "setup failed",
19682
19703
  stopping: "stopping",
19683
19704
  scanned: "scanned {count}",
19684
19705
  changed: "changed {count}",
@@ -20190,16 +20211,33 @@ var zhCN = {
20190
20211
  pullModel: "\u62C9\u53D6 {model}",
20191
20212
  indexStatus: "\u7D22\u5F15\u72B6\u6001",
20192
20213
  builtStatus: "\u25CF \u5DF2\u6784\u5EFA",
20214
+ incompatibleStatus: "\u25CF \u4E0D\u517C\u5BB9",
20193
20215
  chunks: "\u5206\u5757",
20194
20216
  files: "\u6587\u4EF6",
20195
20217
  dim: "\u7EF4\u5EA6",
20196
20218
  size: "\u5927\u5C0F",
20197
20219
  lastBuild: "\u4E0A\u6B21\u6784\u5EFA",
20220
+ builtWith: "\u6784\u5EFA\u6765\u6E90",
20221
+ currentTarget: "\u5F53\u524D\u76EE\u6807",
20222
+ incompatibleHint: "\u78C1\u76D8\u4E0A\u7684\u8FD9\u4E2A\u7D22\u5F15\u662F\u4E3A\u4E0D\u540C\u7684 provider \u6216 model \u6784\u5EFA\u7684\u3002\u8FD0\u884C\u201C\u5B8C\u5168\u91CD\u5EFA\u201D\u5373\u53EF\u66FF\u6362\u3002",
20198
20223
  runIndexHint: "\u8FD0\u884C\u7D22\u5F15\u4EE5\u542F\u7528 semantic_search\u3002",
20199
20224
  reIndex: "\u91CD\u5EFA\u7D22\u5F15",
20200
20225
  build: "\u6784\u5EFA",
20201
20226
  rebuild: "\u5B8C\u5168\u91CD\u5EFA",
20202
20227
  stop: "\u505C\u6B62",
20228
+ provider: "\u63D0\u4F9B\u65B9",
20229
+ providerType: "\u670D\u52A1\u7C7B\u578B",
20230
+ openaiCompat: "OpenAI-Compatible",
20231
+ apiUrl: "API URL",
20232
+ apiKey: "API Key",
20233
+ customRequestBody: "\u81EA\u5B9A\u4E49\u8BF7\u6C42\u4F53",
20234
+ invalidCustomRequestBody: "\u81EA\u5B9A\u4E49\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F\u5408\u6CD5 JSON\uFF1A{error}",
20235
+ customRequestBodyMustBeObject: "\u81EA\u5B9A\u4E49\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F JSON \u5BF9\u8C61\u3002",
20236
+ saveBeforeIndex: "\u8BF7\u5148\u4FDD\u5B58\u8BED\u4E49\u8BBE\u7F6E\uFF0C\u518D\u542F\u52A8\u7D22\u5F15\u3002",
20237
+ extraBody: "\u6269\u5C55\u8BF7\u6C42\u4F53",
20238
+ keepExistingKey: "\u7559\u7A7A\u5219\u4FDD\u7559\u73B0\u6709 Key",
20239
+ remoteProvider: "\u8FDC\u7A0B\u5411\u91CF\u670D\u52A1",
20240
+ remoteProviderDesc: "\u5728\u8FD9\u91CC\u914D\u7F6E OpenAI-Compatible embeddings \u7684\u5B8C\u6574 URL\u3002Reasonix \u4F1A\u4E25\u683C\u4F7F\u7528\u4F60\u63D0\u4F9B\u7684 URL \u53D1\u8D77\u8BF7\u6C42\u3002",
20203
20241
  ollama: "Ollama",
20204
20242
  binary: "\u4E8C\u8FDB\u5236",
20205
20243
  found: "\u5DF2\u627E\u5230",
@@ -20227,11 +20265,14 @@ var zhCN = {
20227
20265
  nothingSkipped: "\u65E0\u8DF3\u8FC7 \u2014 \u6240\u6709\u904D\u5386\u7684\u6587\u4EF6\u90FD\u5C06\u88AB\u7D22\u5F15\u3002",
20228
20266
  firstIncluded: "\u524D {count} \u4E2A\u5305\u542B\u7684\u6587\u4EF6",
20229
20267
  job: "\u4EFB\u52A1",
20268
+ phaseSetup: "\u521D\u59CB\u5316\u4E2D",
20230
20269
  phaseScan: "\u626B\u63CF\u6587\u4EF6",
20231
20270
  phaseEmbed: "\u5D4C\u5165\u5206\u5757",
20232
20271
  phaseWrite: "\u5199\u5165\u7D22\u5F15",
20233
20272
  phaseDone: "\u5B8C\u6210",
20234
20273
  phaseError: "\u9519\u8BEF",
20274
+ phaseCancelled: "\u5DF2\u505C\u6B62",
20275
+ setupFailed: "\u521D\u59CB\u5316\u5931\u8D25",
20235
20276
  stopping: "\u505C\u6B62\u4E2D",
20236
20277
  scanned: "\u5DF2\u626B\u63CF {count}",
20237
20278
  changed: "\u5DF2\u53D8\u66F4 {count}",
@@ -25387,13 +25428,20 @@ function PlansPanel() {
25387
25428
  function SemanticPanel() {
25388
25429
  useLang();
25389
25430
  const [data, setData] = d2(null);
25431
+ const [draft, setDraft] = d2(null);
25432
+ const [draftDirty, setDraftDirty] = d2(false);
25433
+ const draftDirtyRef = A2(false);
25390
25434
  const [error, setError] = d2(null);
25391
25435
  const [busy, setBusy] = d2(false);
25392
25436
  const [info, setInfo] = d2(null);
25393
25437
  const load = q2(async () => {
25394
25438
  try {
25395
- const r3 = await api("/semantic");
25396
- setData(r3);
25439
+ const [semantic, config] = await Promise.all([
25440
+ api("/semantic"),
25441
+ api("/semantic/config")
25442
+ ]);
25443
+ setData(semantic);
25444
+ setDraft((current) => current && draftDirtyRef.current ? current : toConfigDraft(config));
25397
25445
  } catch (err) {
25398
25446
  setError(err.message);
25399
25447
  }
@@ -25401,7 +25449,7 @@ function SemanticPanel() {
25401
25449
  y2(() => {
25402
25450
  load();
25403
25451
  const phase2 = data?.job?.phase;
25404
- const running2 = phase2 === "scan" || phase2 === "embed" || phase2 === "write";
25452
+ const running2 = isActiveSemanticPhase(phase2);
25405
25453
  const pulling2 = data?.pull?.status === "pulling";
25406
25454
  const ms = running2 || pulling2 ? 1200 : 5e3;
25407
25455
  const id = setInterval(load, ms);
@@ -25409,10 +25457,18 @@ function SemanticPanel() {
25409
25457
  }, [load, data?.job?.phase, data?.pull?.status]);
25410
25458
  const start = q2(
25411
25459
  async (rebuild) => {
25460
+ if (!draft) return;
25412
25461
  setBusy(true);
25413
25462
  setError(null);
25414
25463
  setInfo(null);
25415
25464
  try {
25465
+ const validation = validateSemanticDraft(draft);
25466
+ if (draftDirty) {
25467
+ throw new Error(t4("semantic.saveBeforeIndex"));
25468
+ }
25469
+ if (validation.error) {
25470
+ throw new Error(validation.error);
25471
+ }
25416
25472
  await api("/semantic/start", { method: "POST", body: { rebuild: !!rebuild } });
25417
25473
  setInfo(rebuild ? t4("semantic.rebuildStarted") : t4("semantic.incrementalStarted"));
25418
25474
  await load();
@@ -25422,7 +25478,7 @@ function SemanticPanel() {
25422
25478
  setBusy(false);
25423
25479
  }
25424
25480
  },
25425
- [load]
25481
+ [draft, draftDirty, load]
25426
25482
  );
25427
25483
  const stop = q2(async () => {
25428
25484
  setBusy(true);
@@ -25442,10 +25498,11 @@ function SemanticPanel() {
25442
25498
  setError(null);
25443
25499
  setInfo(t4("semantic.startingDaemon"));
25444
25500
  try {
25445
- const r3 = await api("/semantic/ollama/start", { method: "POST", body: {} });
25446
- setInfo(
25447
- r3.ready ? t4("semantic.daemonUp") : t4("semantic.daemonTimeout")
25448
- );
25501
+ const r3 = await api("/semantic/ollama/start", {
25502
+ method: "POST",
25503
+ body: {}
25504
+ });
25505
+ setInfo(r3.ready ? t4("semantic.daemonUp") : t4("semantic.daemonTimeout"));
25449
25506
  await load();
25450
25507
  } catch (err) {
25451
25508
  setError(err.message);
@@ -25469,10 +25526,44 @@ function SemanticPanel() {
25469
25526
  },
25470
25527
  [load]
25471
25528
  );
25472
- if (!data && !error)
25529
+ const saveProviderConfig = q2(async () => {
25530
+ if (!draft) return;
25531
+ setBusy(true);
25532
+ setError(null);
25533
+ setInfo(null);
25534
+ try {
25535
+ const extraBody = semanticValidation.extraBody;
25536
+ await api("/semantic/config", {
25537
+ method: "POST",
25538
+ body: {
25539
+ provider: draft.provider,
25540
+ ollama: {
25541
+ baseUrl: draft.ollama.baseUrl,
25542
+ model: draft.ollama.model
25543
+ },
25544
+ openaiCompat: {
25545
+ baseUrl: draft.openaiCompat.baseUrl,
25546
+ apiKey: draft.openaiCompat.apiKey,
25547
+ model: draft.openaiCompat.model,
25548
+ extraBody
25549
+ }
25550
+ }
25551
+ });
25552
+ setDraftDirty(false);
25553
+ draftDirtyRef.current = false;
25554
+ setInfo(t4("semantic.savedConfig", { count: 1 }));
25555
+ await load();
25556
+ } catch (err) {
25557
+ setError(err.message);
25558
+ } finally {
25559
+ setBusy(false);
25560
+ }
25561
+ }, [draft, load]);
25562
+ if (!data && !error) {
25473
25563
  return html4`<div class="card" style="color:var(--fg-3)">${t4("common.loading")}</div>`;
25564
+ }
25474
25565
  if (error && !data) return html4`<div class="card accent-err">${error}</div>`;
25475
- if (!data) return null;
25566
+ if (!data || !draft) return null;
25476
25567
  if (!data.attached) {
25477
25568
  return html4`
25478
25569
  <div class="card" style="color:var(--fg-3)">
@@ -25483,35 +25574,168 @@ function SemanticPanel() {
25483
25574
  }
25484
25575
  const job = data.job;
25485
25576
  const phase = job?.phase;
25486
- const running = phase === "scan" || phase === "embed" || phase === "write";
25577
+ const running = isActiveSemanticPhase(phase);
25487
25578
  const pull = data.pull;
25488
25579
  const pulling = pull?.status === "pulling";
25489
- const o3 = data.ollama ?? {};
25490
- const binaryFound = o3.binaryFound === true;
25491
- const daemonRunning = o3.daemonRunning === true;
25492
- const modelPulled = o3.modelPulled === true;
25493
- const modelName = o3.modelName ?? "nomic-embed-text";
25494
- const installedModels = o3.installedModels ?? [];
25495
- const ready = binaryFound && daemonRunning && modelPulled;
25580
+ const provider = data.providerStatus?.kind ?? draft.provider;
25581
+ const ready = data.providerStatus?.ready === true;
25582
+ const isOllama = provider === "ollama";
25583
+ const ollama = data.providerStatus?.kind === "ollama" ? data.providerStatus : null;
25584
+ const remote = data.providerStatus?.kind === "openai-compat" ? data.providerStatus : null;
25585
+ const binaryFound = ollama?.binaryFound === true;
25586
+ const daemonRunning = ollama?.daemonRunning === true;
25587
+ const modelPulled = ollama?.modelPulled === true;
25588
+ const modelName = isOllama ? ollama?.modelName ?? draft.ollama.model ?? "nomic-embed-text" : draft.openaiCompat.model;
25496
25589
  const sectionH3 = (text) => html4`
25497
25590
  <h3 style="margin:18px 0 8px;font-family:var(--font-mono);font-size:11px;color:var(--fg-3);text-transform:uppercase;letter-spacing:.1em">${text}</h3>
25498
25591
  `;
25499
25592
  const idx = data.index;
25593
+ const indexReady = idx?.exists === true && idx.compatible !== false;
25594
+ const indexMismatch = idx?.exists === true && idx.compatible === false;
25595
+ const semanticValidation = validateSemanticDraft(draft);
25596
+ const semanticDraftBlocked = draftDirty || semanticValidation.error !== null;
25500
25597
  return html4`
25501
25598
  <div style="display:grid;grid-template-columns:minmax(0,1fr) 280px;gap:14px;align-items:start">
25502
25599
  <div style="display:flex;flex-direction:column;gap:10px;min-width:0">
25503
25600
  <div class="chips">
25504
- <span class=${`chip-f static ${idx?.exists ? "active" : ""}`}>
25505
- ${idx?.exists ? t4("semantic.indexBuilt") : t4("semantic.noIndex")}
25601
+ <span class=${`chip-f static ${indexReady ? "active" : ""}`}>
25602
+ ${indexReady ? t4("semantic.indexBuilt") : t4("semantic.noIndex")}
25506
25603
  </span>
25507
25604
  ${ready ? html4`<span class="chip-f static" style="border-color:var(--c-ok);color:var(--c-ok)">${t4("semantic.ready")}</span>` : html4`<span class="chip-f static" style="border-color:var(--c-warn);color:var(--c-warn)">${t4("semantic.setupNeeded")}</span>`}
25508
25605
  </div>
25509
- ${info ? html4`<div><span class="pill info">${info}</span></div>` : null}
25510
25606
  ${error ? html4`<div class="card accent-err">${error}</div>` : null}
25511
25607
 
25512
- ${idx?.exists ? html4`<${SemanticSearchSection} />` : null}
25608
+ <div class="card">
25609
+ <div class="card-h"><span class="title">${t4("semantic.provider")}</span></div>
25610
+ <div class="form-row">
25611
+ <span class="lbl">${t4("semantic.providerType")}</span>
25612
+ <select
25613
+ class="input mono"
25614
+ value=${draft.provider}
25615
+ onInput=${(e3) => {
25616
+ draftDirtyRef.current = true;
25617
+ setDraftDirty(true);
25618
+ setDraft({
25619
+ ...draft,
25620
+ provider: e3.target.value
25621
+ });
25622
+ }}
25623
+ >
25624
+ <option value="ollama">Ollama</option>
25625
+ <option value="openai-compat">OpenAI-Compatible</option>
25626
+ </select>
25627
+ </div>
25628
+ ${draft.provider === "ollama" ? html4`
25629
+ <div class="form-row">
25630
+ <span class="lbl">${t4("semantic.model")}</span>
25631
+ <input
25632
+ class="input mono"
25633
+ type="text"
25634
+ value=${draft.ollama.model}
25635
+ onInput=${(e3) => {
25636
+ draftDirtyRef.current = true;
25637
+ setDraftDirty(true);
25638
+ setDraft({
25639
+ ...draft,
25640
+ ollama: { ...draft.ollama, model: e3.target.value }
25641
+ });
25642
+ }}
25643
+ />
25644
+ </div>
25645
+ ` : html4`
25646
+ <div class="form-row">
25647
+ <span class="lbl">${t4("semantic.apiUrl")}</span>
25648
+ <input
25649
+ class="input mono"
25650
+ type="text"
25651
+ placeholder="https://api.openai.com/v1/embeddings"
25652
+ value=${draft.openaiCompat.baseUrl}
25653
+ onInput=${(e3) => {
25654
+ draftDirtyRef.current = true;
25655
+ setDraftDirty(true);
25656
+ setDraft({
25657
+ ...draft,
25658
+ openaiCompat: {
25659
+ ...draft.openaiCompat,
25660
+ baseUrl: e3.target.value
25661
+ }
25662
+ });
25663
+ }}
25664
+ />
25665
+ </div>
25666
+ <div class="form-row">
25667
+ <span class="lbl">${t4("semantic.apiKey")}</span>
25668
+ <input
25669
+ class="input mono"
25670
+ type="password"
25671
+ placeholder=${draft.openaiCompat.apiKeySet ? t4("semantic.keepExistingKey") : "sk-..."}
25672
+ value=${draft.openaiCompat.apiKey}
25673
+ onInput=${(e3) => {
25674
+ draftDirtyRef.current = true;
25675
+ setDraftDirty(true);
25676
+ setDraft({
25677
+ ...draft,
25678
+ openaiCompat: {
25679
+ ...draft.openaiCompat,
25680
+ apiKey: e3.target.value
25681
+ }
25682
+ });
25683
+ }}
25684
+ />
25685
+ <div style="color:var(--fg-3);font-size:12px">${t4("semantic.apiKeyStoredNote")}</div>
25686
+ </div>
25687
+ <div class="form-row">
25688
+ <span class="lbl">${t4("semantic.model")}</span>
25689
+ <input
25690
+ class="input mono"
25691
+ type="text"
25692
+ value=${draft.openaiCompat.model}
25693
+ onInput=${(e3) => {
25694
+ draftDirtyRef.current = true;
25695
+ setDraftDirty(true);
25696
+ setDraft({
25697
+ ...draft,
25698
+ openaiCompat: {
25699
+ ...draft.openaiCompat,
25700
+ model: e3.target.value
25701
+ }
25702
+ });
25703
+ }}
25704
+ />
25705
+ </div>
25706
+ <details style="margin-top:10px">
25707
+ <summary style="cursor:pointer;color:var(--fg-2);font-size:12px">${t4("semantic.customRequestBody")}</summary>
25708
+ <div class="form-row" style="margin-top:10px">
25709
+ <span class="lbl">${t4("semantic.customRequestBody")}</span>
25710
+ <textarea
25711
+ class="input mono"
25712
+ rows="6"
25713
+ value=${draft.openaiCompat.extraBodyText}
25714
+ onInput=${(e3) => {
25715
+ draftDirtyRef.current = true;
25716
+ setDraftDirty(true);
25717
+ setDraft({
25718
+ ...draft,
25719
+ openaiCompat: {
25720
+ ...draft.openaiCompat,
25721
+ extraBodyText: e3.target.value
25722
+ }
25723
+ });
25724
+ }}
25725
+ ></textarea>
25726
+ </div>
25727
+ </details>
25728
+ ${semanticValidation.error ? html4`<div style="color:var(--c-err);font-size:12px;margin-top:-2px">${semanticValidation.error}</div>` : null}
25729
+ `}
25730
+ <div style="display:flex;gap:6px;margin-top:10px">
25731
+ <button class="btn primary" disabled=${busy || semanticValidation.error !== null} onClick=${saveProviderConfig}>${t4("common.save")}</button>
25732
+ </div>
25733
+ </div>
25734
+ ${info ? html4`<div><span class="pill info">${info}</span></div>` : null}
25735
+
25736
+ ${indexReady ? html4`<${SemanticSearchSection} />` : null}
25513
25737
 
25514
- ${!binaryFound ? html4`
25738
+ ${isOllama && !binaryFound ? html4`
25515
25739
  <div class="card">
25516
25740
  <div class="card-h"><span class="title">${t4("semantic.installOllama")}</span></div>
25517
25741
  <div class="card-b" style="font-size:13px">
@@ -25524,7 +25748,7 @@ function SemanticPanel() {
25524
25748
  </div>
25525
25749
  </div>
25526
25750
  ` : null}
25527
- ${binaryFound && !daemonRunning ? html4`
25751
+ ${isOllama && binaryFound && !daemonRunning ? html4`
25528
25752
  <div class="card">
25529
25753
  <div class="card-h"><span class="title">${t4("semantic.daemon")}</span></div>
25530
25754
  <div class="card-b" style="font-size:13px">
@@ -25536,7 +25760,7 @@ function SemanticPanel() {
25536
25760
  </div>
25537
25761
  </div>
25538
25762
  ` : null}
25539
- ${daemonRunning && !modelPulled ? html4`
25763
+ ${isOllama && daemonRunning && !modelPulled ? html4`
25540
25764
  <div class="card">
25541
25765
  <div class="card-h"><span class="title">${t4("semantic.model")}</span></div>
25542
25766
  <div class="card-b" style="font-size:13px">
@@ -25556,6 +25780,14 @@ function SemanticPanel() {
25556
25780
  </div>
25557
25781
  </div>
25558
25782
  ` : null}
25783
+ ${!isOllama ? html4`
25784
+ <div class="card">
25785
+ <div class="card-h"><span class="title">${t4("semantic.remoteProvider")}</span></div>
25786
+ <div class="card-b" style="font-size:13px;color:var(--fg-2)">
25787
+ ${t4("semantic.remoteProviderDesc")}
25788
+ </div>
25789
+ </div>
25790
+ ` : null}
25559
25791
 
25560
25792
  ${job ? html4`
25561
25793
  ${sectionH3(t4("semantic.job"))}
@@ -25568,42 +25800,42 @@ function SemanticPanel() {
25568
25800
  <div class="card-h">
25569
25801
  <span class="title">${t4("semantic.indexStatus")}</span>
25570
25802
  <span class="meta">
25571
- ${idx?.exists ? html4`<span class="pill ok">${t4("semantic.builtStatus")}</span>` : html4`<span class="pill">${t4("system.none")}</span>`}
25803
+ ${idx?.exists ? idx.compatible === false ? html4`<span class="pill warn">${t4("semantic.incompatibleStatus")}</span>` : html4`<span class="pill ok">${t4("semantic.builtStatus")}</span>` : html4`<span class="pill">${t4("system.none")}</span>`}
25572
25804
  </span>
25573
25805
  </div>
25574
25806
  ${idx?.exists ? html4`
25807
+ <div class="rail-kv"><span class="k">${t4("semantic.provider")}</span><span class="v">${idx.builtWith?.provider ?? idx.provider ?? provider}</span></div>
25575
25808
  <div class="rail-kv"><span class="k">${t4("semantic.chunks")}</span><span class="v">${fmtNum(idx.chunks)}</span></div>
25576
25809
  <div class="rail-kv"><span class="k">${t4("semantic.files")}</span><span class="v">${fmtNum(idx.files)}</span></div>
25577
- <div class="rail-kv"><span class="k">${t4("semantic.model")}</span><span class="v" style="font-size:11px">${idx.model ?? modelName}</span></div>
25810
+ <div class="rail-kv"><span class="k">${t4("semantic.model")}</span><span class="v" style="font-size:11px">${idx.builtWith?.model ?? idx.model ?? modelName}</span></div>
25578
25811
  <div class="rail-kv"><span class="k">${t4("semantic.dim")}</span><span class="v">${fmtNum(idx.dim)}</span></div>
25579
25812
  <div class="rail-kv"><span class="k">${t4("semantic.size")}</span><span class="v">${fmtBytes(idx.sizeBytes)}</span></div>
25580
25813
  <div class="rail-kv"><span class="k">${t4("semantic.lastBuild")}</span><span class="v">${fmtRelativeTime(idx.lastBuiltMs ?? null)}</span></div>
25581
- ` : html4`
25582
- <div style="color:var(--fg-3);font-size:12.5px;padding:6px 0">
25583
- ${t4("semantic.runIndexHint")}
25584
- </div>
25585
- `}
25814
+ ${idx.compatible === false ? html4`
25815
+ <div class="rail-kv"><span class="k">${t4("semantic.builtWith")}</span><span class="v" style="font-size:11px">${idx.builtWith?.provider} · ${idx.builtWith?.model}</span></div>
25816
+ <div class="rail-kv"><span class="k">${t4("semantic.currentTarget")}</span><span class="v" style="font-size:11px">${idx.current?.provider} · ${idx.current?.model}</span></div>
25817
+ <div style="color:var(--c-warn);font-size:12px;padding-top:8px">${t4("semantic.incompatibleHint")}</div>
25818
+ ` : null}
25819
+ ` : html4`<div style="color:var(--fg-3);font-size:12.5px;padding:6px 0">${t4("semantic.runIndexHint")}</div>`}
25586
25820
  <div style="display:flex;gap:6px;margin-top:10px;flex-wrap:wrap">
25587
- <button class="primary" disabled=${busy || running || !ready} onClick=${() => start(false)}>${idx?.exists ? t4("semantic.reIndex") : t4("semantic.build")}</button>
25588
- ${idx?.exists ? html4`<button disabled=${busy || running || !ready} onClick=${() => start(true)}>${t4("semantic.rebuild")}</button>` : null}
25821
+ <button class="primary" disabled=${busy || running || !ready || semanticDraftBlocked} onClick=${() => start(false)}>${indexReady ? t4("semantic.reIndex") : t4("semantic.build")}</button>
25822
+ ${idx?.exists ? html4`<button disabled=${busy || running || !ready || semanticDraftBlocked} onClick=${() => start(true)}>${t4("semantic.rebuild")}</button>` : null}
25589
25823
  ${running ? html4`<button onClick=${stop} style="border-color:var(--c-err);color:var(--c-err)">${t4("semantic.stop")}</button>` : null}
25590
25824
  </div>
25591
25825
  </div>
25592
25826
 
25593
25827
  <div class="card">
25594
- <div class="card-h"><span class="title">${t4("semantic.ollama")}</span></div>
25595
- <div class="rail-kv">
25596
- <span class="k">${t4("semantic.binary")}</span>
25597
- <span class="v">${binaryFound ? html4`<span class="pill ok">${t4("semantic.found")}</span>` : html4`<span class="pill err">${t4("semantic.missing")}</span>`}</span>
25598
- </div>
25599
- <div class="rail-kv">
25600
- <span class="k">${t4("semantic.daemonStatus")}</span>
25601
- <span class="v">${daemonRunning ? html4`<span class="pill ok">${t4("semantic.up")}</span>` : html4`<span class="pill warn">${t4("semantic.down")}</span>`}</span>
25602
- </div>
25603
- <div class="rail-kv">
25604
- <span class="k">${t4("semantic.model")}</span>
25605
- <span class="v">${modelPulled ? html4`<span class="pill ok">${t4("semantic.pulled")}</span>` : html4`<span class="pill warn">${t4("semantic.missing")}</span>`}</span>
25606
- </div>
25828
+ <div class="card-h"><span class="title">${isOllama ? t4("semantic.ollama") : t4("semantic.openaiCompat")}</span></div>
25829
+ ${isOllama ? html4`
25830
+ <div class="rail-kv"><span class="k">${t4("semantic.binary")}</span><span class="v">${binaryFound ? html4`<span class="pill ok">${t4("semantic.found")}</span>` : html4`<span class="pill err">${t4("semantic.missing")}</span>`}</span></div>
25831
+ <div class="rail-kv"><span class="k">${t4("semantic.daemonStatus")}</span><span class="v">${daemonRunning ? html4`<span class="pill ok">${t4("semantic.up")}</span>` : html4`<span class="pill warn">${t4("semantic.down")}</span>`}</span></div>
25832
+ <div class="rail-kv"><span class="k">${t4("semantic.model")}</span><span class="v">${modelPulled ? html4`<span class="pill ok">${t4("semantic.pulled")}</span>` : html4`<span class="pill warn">${t4("semantic.missing")}</span>`}</span></div>
25833
+ ` : html4`
25834
+ <div class="rail-kv"><span class="k">${t4("semantic.apiUrl")}</span><span class="v" style="font-size:11px;max-width:160px;overflow-wrap:anywhere;word-break:break-word;text-align:right">${remote?.baseUrl ?? draft.openaiCompat.baseUrl}</span></div>
25835
+ <div class="rail-kv"><span class="k">${t4("semantic.apiKey")}</span><span class="v">${remote?.apiKeySet ? html4`<span class="pill ok">${t4("semantic.found")}</span>` : html4`<span class="pill warn">${t4("semantic.missing")}</span>`}</span></div>
25836
+ <div class="rail-kv"><span class="k">${t4("semantic.model")}</span><span class="v" style="font-size:11px">${remote?.model ?? draft.openaiCompat.model}</span></div>
25837
+ <div class="rail-kv"><span class="k">${t4("semantic.extraBody")}</span><span class="v">${fmtNum(remote?.extraBodyKeys.length ?? 0)}</span></div>
25838
+ `}
25607
25839
  </div>
25608
25840
 
25609
25841
  <${SemanticExcludesCard} />
@@ -25611,6 +25843,44 @@ function SemanticPanel() {
25611
25843
  </div>
25612
25844
  `;
25613
25845
  }
25846
+ function toConfigDraft(config) {
25847
+ return {
25848
+ provider: config.provider,
25849
+ ollama: {
25850
+ baseUrl: config.ollama.baseUrl,
25851
+ model: config.ollama.model
25852
+ },
25853
+ openaiCompat: {
25854
+ baseUrl: config.openaiCompat.baseUrl,
25855
+ apiKey: "",
25856
+ model: config.openaiCompat.model,
25857
+ extraBodyText: JSON.stringify(config.openaiCompat.extraBody ?? {}, null, 2),
25858
+ apiKeySet: config.openaiCompat.apiKeySet
25859
+ }
25860
+ };
25861
+ }
25862
+ function validateSemanticDraft(draft) {
25863
+ if (draft.provider !== "openai-compat") {
25864
+ return { extraBody: {}, error: null };
25865
+ }
25866
+ const raw = draft.openaiCompat.extraBodyText.trim();
25867
+ if (!raw) {
25868
+ return { extraBody: {}, error: null };
25869
+ }
25870
+ let parsed;
25871
+ try {
25872
+ parsed = JSON.parse(raw);
25873
+ } catch (err) {
25874
+ return {
25875
+ extraBody: {},
25876
+ error: t4("semantic.invalidCustomRequestBody", { error: err.message })
25877
+ };
25878
+ }
25879
+ if (!isPlainObject(parsed)) {
25880
+ return { extraBody: {}, error: t4("semantic.customRequestBodyMustBeObject") };
25881
+ }
25882
+ return { extraBody: parsed, error: null };
25883
+ }
25614
25884
  function SemanticSearchSection() {
25615
25885
  useLang();
25616
25886
  const [query2, setQuery] = d2("");
@@ -25783,11 +26053,7 @@ function SemanticExcludesCard() {
25783
26053
  <div class="card-h">
25784
26054
  <span class="title">${t4("semantic.indexConfig")}</span>
25785
26055
  <span class="meta">
25786
- <a
25787
- class="mono"
25788
- style="color:var(--c-brand);text-decoration:none;font-size:11px;cursor:pointer"
25789
- onClick=${reset}
25790
- >${t4("semantic.reset")}</a>
26056
+ <a class="mono" style="color:var(--c-brand);text-decoration:none;font-size:11px;cursor:pointer" onClick=${reset}>${t4("semantic.reset")}</a>
25791
26057
  </span>
25792
26058
  </div>
25793
26059
  ${info ? html4`<div style="margin-bottom:8px"><span class="pill ok">${info}</span></div>` : null}
@@ -25819,11 +26085,7 @@ function SemanticExcludesCard() {
25819
26085
  placeholder="**/*.test.ts"
25820
26086
  />
25821
26087
 
25822
- <div
25823
- class="checkbox-row"
25824
- style="margin-top:8px;cursor:pointer"
25825
- onClick=${() => setDraft({ ...draft, respectGitignore: !draft.respectGitignore })}
25826
- >
26088
+ <div class="checkbox-row" style="margin-top:8px;cursor:pointer" onClick=${() => setDraft({ ...draft, respectGitignore: !draft.respectGitignore })}>
25827
26089
  <span class=${`box ${draft.respectGitignore ? "on" : ""}`}>${draft.respectGitignore ? "\u2713" : ""}</span>
25828
26090
  <span>${t4("semantic.respectGitignore")}</span>
25829
26091
  </div>
@@ -25843,9 +26105,7 @@ function SemanticExcludesCard() {
25843
26105
  </div>
25844
26106
 
25845
26107
  <div style="display:flex;gap:6px;margin-top:10px">
25846
- <button class="btn ghost" style="flex:1" disabled=${busy} onClick=${runPreview}>
25847
- <span class="g">⊕</span><span>${t4("semantic.preview")}</span>
25848
- </button>
26108
+ <button class="btn ghost" style="flex:1" disabled=${busy} onClick=${runPreview}><span class="g">⊕</span><span>${t4("semantic.preview")}</span></button>
25849
26109
  <button class="btn primary" style="flex:1" disabled=${busy} onClick=${save}>${t4("common.save")}</button>
25850
26110
  </div>
25851
26111
 
@@ -25870,19 +26130,17 @@ function ExcludesPreview({ preview }) {
25870
26130
  ].filter((k3) => (buckets[k3] || 0) > 0);
25871
26131
  return html4`
25872
26132
  <div class="excludes-preview">
25873
- <div class="summary">
25874
- ${t4("semantic.previewSummary", { included: preview.filesIncluded, skipped: totalSkipped })}
25875
- </div>
26133
+ <div class="summary">${t4("semantic.previewSummary", { included: preview.filesIncluded, skipped: totalSkipped })}</div>
25876
26134
  ${reasons.length === 0 ? html4`<div style="color:var(--fg-3)">${t4("semantic.nothingSkipped")}</div>` : reasons.map(
25877
26135
  (r3) => html4`
25878
- <details>
25879
- <summary><strong>${r3}: ${buckets[r3]}</strong></summary>
25880
- <ul>
25881
- ${(samples[r3] || []).map((p3) => html4`<li><code>${p3}</code></li>`)}
25882
- ${(buckets[r3] || 0) > (samples[r3] || []).length ? html4`<li style="color:var(--fg-3)">…${(buckets[r3] || 0) - (samples[r3] || []).length} more</li>` : null}
25883
- </ul>
25884
- </details>
25885
- `
26136
+ <details>
26137
+ <summary><strong>${r3}: ${buckets[r3]}</strong></summary>
26138
+ <ul>
26139
+ ${(samples[r3] || []).map((p3) => html4`<li><code>${p3}</code></li>`)}
26140
+ ${(buckets[r3] || 0) > (samples[r3] || []).length ? html4`<li style="color:var(--fg-3)">…${(buckets[r3] || 0) - (samples[r3] || []).length} more</li>` : null}
26141
+ </ul>
26142
+ </details>
26143
+ `
25886
26144
  )}
25887
26145
  ${preview.sampleIncluded?.length ? html4`
25888
26146
  <details>
@@ -25915,10 +26173,7 @@ function ChipFormRow({
25915
26173
  };
25916
26174
  return html4`
25917
26175
  <div class="form-row">
25918
- <span class="lbl">
25919
- ${label}
25920
- ${sub ? html4`<span style="color:var(--fg-3);font-weight:400;text-transform:none;letter-spacing:0"> · ${sub}</span>` : null}
25921
- </span>
26176
+ <span class="lbl">${label}${sub ? html4`<span style="color:var(--fg-3);font-weight:400;text-transform:none;letter-spacing:0"> · ${sub}</span>` : null}</span>
25922
26177
  <div style="display:flex;flex-wrap:wrap;gap:4px">
25923
26178
  ${value.map(
25924
26179
  (e3) => html4`
@@ -25949,22 +26204,27 @@ function ChipFormRow({
25949
26204
  function SemanticJobView({ job, running }) {
25950
26205
  useLang();
25951
26206
  const phaseLabel = {
26207
+ setup: t4("semantic.phaseSetup"),
25952
26208
  scan: t4("semantic.phaseScan"),
25953
26209
  embed: t4("semantic.phaseEmbed"),
25954
26210
  write: t4("semantic.phaseWrite"),
25955
26211
  done: t4("semantic.phaseDone"),
25956
- error: t4("semantic.phaseError")
26212
+ error: t4("semantic.phaseError"),
26213
+ cancelled: t4("semantic.phaseCancelled")
25957
26214
  }[job.phase] ?? job.phase;
25958
26215
  const total = job.chunksTotal ?? 0;
25959
26216
  const doneN = job.chunksDone ?? 0;
25960
26217
  const ratio = total > 0 ? Math.min(1, doneN / total) : 0;
25961
- const elapsed = ((Date.now() - job.startedAt) / 1e3).toFixed(1);
26218
+ const elapsedBase = job.finishedAt ?? Date.now();
26219
+ const elapsedSeconds = (elapsedBase - job.startedAt) / 1e3;
26220
+ const elapsed = elapsedSeconds < 0.1 ? "<0.1s" : `${elapsedSeconds.toFixed(1)}s`;
26221
+ const phaseSummary = job.phase === "error" && job.lastPhase === "setup" ? t4("semantic.setupFailed") : phaseLabel;
25962
26222
  return html4`
25963
26223
  <div class="kv">
25964
26224
  <div><span class="kv-key">phase</span>
25965
- <span class=${`pill ${job.phase === "error" ? "pill-err" : running ? "pill-active" : "pill-dim"}`}>${phaseLabel}</span>
25966
- ${job.aborted ? html4`<span class="pill warn" style="margin-left: 6px;">${t4("semantic.stopping")}</span>` : null}
25967
- <span style="color:var(--fg-3);margin-left:8px">${elapsed}s</span>
26225
+ <span class=${`pill ${job.phase === "error" ? "pill-err" : job.phase === "cancelled" ? "warn" : running ? "pill-active" : "pill-dim"}`}>${phaseSummary}</span>
26226
+ ${job.aborted && running ? html4`<span class="pill warn" style="margin-left: 6px;">${t4("semantic.stopping")}</span>` : null}
26227
+ <span style="color:var(--fg-3);margin-left:8px">${elapsed}</span>
25968
26228
  </div>
25969
26229
  ${job.filesScanned !== null && job.filesScanned !== void 0 ? html4`<div><span class="kv-key">${t4("semantic.files")}</span>${t4("semantic.scanned", { count: job.filesScanned })}${job.filesChanged != null ? ` \xB7 ${t4("semantic.changed", { count: job.filesChanged })}` : ""}${job.filesSkipped ? ` \xB7 ${t4("semantic.skipped", { count: job.filesSkipped })}` : ""}</div>` : null}
25970
26230
  ${total > 0 ? html4`
@@ -25998,6 +26258,14 @@ function SkipBucketsView({ buckets }) {
25998
26258
  const parts = order.filter(([k3]) => (buckets[k3] || 0) > 0).map(([k3, label]) => `${label}: ${buckets[k3]}`);
25999
26259
  return html4`<div><span class="kv-key">${t4("semantic.skipped")}</span>${t4("semantic.skippedFiles", { total, details: parts.join(", ") })}</div>`;
26000
26260
  }
26261
+ function isActiveSemanticPhase(phase) {
26262
+ return phase === "setup" || phase === "scan" || phase === "embed" || phase === "write";
26263
+ }
26264
+ function isPlainObject(value) {
26265
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
26266
+ const proto = Object.getPrototypeOf(value);
26267
+ return proto === Object.prototype || proto === null;
26268
+ }
26001
26269
 
26002
26270
  // dashboard/src/panels/sessions.ts
26003
26271
  function SessionsPanel() {