reasonix 0.16.1 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dashboard/app.css CHANGED
@@ -665,7 +665,8 @@ button:disabled {
665
665
  }
666
666
 
667
667
  input[type="text"],
668
- input[type="search"] {
668
+ input[type="search"],
669
+ input[type="number"] {
669
670
  background: var(--bg-2);
670
671
  border: 1px solid var(--border);
671
672
  color: var(--fg-0);
@@ -678,6 +679,7 @@ input[type="search"] {
678
679
 
679
680
  input[type="text"]:focus,
680
681
  input[type="search"]:focus,
682
+ input[type="number"]:focus,
681
683
  input[type="password"]:focus {
682
684
  border-color: var(--primary);
683
685
  outline: none;
@@ -712,7 +714,15 @@ select:focus {
712
714
  textarea {
713
715
  background: var(--bg-2);
714
716
  color: var(--fg-0);
717
+ border: 1px solid var(--border);
718
+ border-radius: var(--radius-sm);
719
+ padding: 8px 10px;
715
720
  font-family: var(--mono);
721
+ font-size: 12px;
722
+ line-height: 1.5;
723
+ resize: vertical;
724
+ width: 100%;
725
+ box-sizing: border-box;
716
726
  }
717
727
  textarea:focus {
718
728
  border-color: var(--primary);
@@ -2303,19 +2313,25 @@ textarea:focus {
2303
2313
  }
2304
2314
 
2305
2315
  /* Split view — left half source (CodeMirror), right half preview. */
2306
- .editor-split {
2316
+ .editor-stage {
2307
2317
  flex: 1;
2308
2318
  display: flex;
2309
2319
  min-height: 0;
2310
2320
  overflow: hidden;
2311
2321
  }
2312
- .editor-split-pane {
2322
+ .editor-stage > .editor-host,
2323
+ .editor-stage > .editor-md-preview {
2313
2324
  flex: 1;
2314
2325
  min-width: 0;
2315
- border: none;
2316
- border-radius: 0;
2326
+ min-height: 0;
2327
+ }
2328
+ .editor-stage[data-mode="edit"] > .editor-md-preview {
2329
+ display: none;
2330
+ }
2331
+ .editor-stage[data-mode="preview"] > .editor-host {
2332
+ display: none;
2317
2333
  }
2318
- .editor-split .editor-split-pane + .editor-split-pane {
2334
+ .editor-stage[data-mode="split"] > .editor-host + .editor-md-preview {
2319
2335
  border-left: 1px solid #181a1f;
2320
2336
  }
2321
2337
 
@@ -2538,3 +2554,140 @@ code,
2538
2554
  border: 1px dashed var(--border);
2539
2555
  border-radius: var(--radius-md);
2540
2556
  }
2557
+
2558
+ /* ---------- Excludes settings (Semantic tab) ---------- */
2559
+
2560
+ .excludes-toggle {
2561
+ margin: 24px 0 12px;
2562
+ display: flex;
2563
+ align-items: baseline;
2564
+ gap: 10px;
2565
+ cursor: pointer;
2566
+ user-select: none;
2567
+ }
2568
+ .excludes-toggle:hover {
2569
+ color: var(--primary);
2570
+ }
2571
+ .excludes-toggle .caret {
2572
+ font-size: 11px;
2573
+ color: var(--fg-3);
2574
+ width: 10px;
2575
+ display: inline-block;
2576
+ }
2577
+ .excludes-toggle .label {
2578
+ font-size: 11px;
2579
+ letter-spacing: 0.1em;
2580
+ text-transform: uppercase;
2581
+ color: var(--fg-3);
2582
+ }
2583
+ .excludes-toggle:hover .label,
2584
+ .excludes-toggle:hover .caret {
2585
+ color: var(--primary);
2586
+ }
2587
+ .excludes-toggle .hint {
2588
+ font-size: 12px;
2589
+ color: var(--fg-3);
2590
+ font-weight: normal;
2591
+ }
2592
+
2593
+ .excludes-card {
2594
+ font-size: 13px;
2595
+ display: flex;
2596
+ flex-direction: column;
2597
+ gap: 12px;
2598
+ }
2599
+ .excludes-card .lead {
2600
+ color: var(--fg-2);
2601
+ font-size: 12px;
2602
+ line-height: 1.5;
2603
+ }
2604
+ .excludes-grid {
2605
+ display: grid;
2606
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
2607
+ gap: 14px;
2608
+ }
2609
+ .excludes-field label {
2610
+ display: block;
2611
+ font-size: 12px;
2612
+ color: var(--fg-2);
2613
+ margin-bottom: 4px;
2614
+ font-weight: 500;
2615
+ }
2616
+ .excludes-field textarea {
2617
+ min-height: 90px;
2618
+ }
2619
+ .excludes-options {
2620
+ display: flex;
2621
+ flex-wrap: wrap;
2622
+ gap: 18px;
2623
+ align-items: center;
2624
+ padding-top: 4px;
2625
+ font-size: 12px;
2626
+ color: var(--fg-1);
2627
+ }
2628
+ .excludes-options label {
2629
+ display: inline-flex;
2630
+ align-items: center;
2631
+ gap: 6px;
2632
+ }
2633
+ .excludes-options input[type="number"] {
2634
+ width: 110px;
2635
+ }
2636
+ .excludes-actions {
2637
+ display: flex;
2638
+ gap: 10px;
2639
+ flex-wrap: wrap;
2640
+ margin-top: 4px;
2641
+ }
2642
+
2643
+ .excludes-preview {
2644
+ margin-top: 4px;
2645
+ padding-top: 12px;
2646
+ border-top: 1px solid var(--border);
2647
+ font-size: 12px;
2648
+ display: flex;
2649
+ flex-direction: column;
2650
+ gap: 8px;
2651
+ }
2652
+ .excludes-preview .summary {
2653
+ font-size: 13px;
2654
+ color: var(--fg-1);
2655
+ }
2656
+ .excludes-preview details {
2657
+ background: var(--bg-2);
2658
+ border: 1px solid var(--border);
2659
+ border-radius: var(--radius-sm);
2660
+ padding: 6px 10px;
2661
+ }
2662
+ .excludes-preview details[open] {
2663
+ background: var(--bg-3);
2664
+ }
2665
+ .excludes-preview summary {
2666
+ cursor: pointer;
2667
+ color: var(--fg-1);
2668
+ font-weight: 500;
2669
+ }
2670
+ .excludes-preview summary:hover {
2671
+ color: var(--primary);
2672
+ }
2673
+ .excludes-preview ul {
2674
+ margin: 6px 0 0 16px;
2675
+ padding: 0;
2676
+ font-size: 11.5px;
2677
+ color: var(--fg-2);
2678
+ }
2679
+ .excludes-preview li {
2680
+ margin: 2px 0;
2681
+ }
2682
+ .excludes-preview li code {
2683
+ font-size: 11.5px;
2684
+ background: transparent;
2685
+ padding: 0;
2686
+ color: var(--fg-1);
2687
+ }
2688
+
2689
+ .skip-buckets {
2690
+ margin-top: 4px;
2691
+ font-size: 12px;
2692
+ color: var(--fg-2);
2693
+ }
package/dashboard/app.js CHANGED
@@ -3265,10 +3265,212 @@ function SemanticPanel() {
3265
3265
  <button disabled=${busy || running || !ready} onClick=${() => start(true)}>Rebuild (wipe + full)</button>
3266
3266
  <button disabled=${busy || !running} onClick=${stop}>Stop</button>
3267
3267
  </div>
3268
+
3269
+ <${SemanticExcludesCard} />
3270
+ </div>
3271
+ `;
3272
+ }
3273
+
3274
+ function SemanticExcludesCard() {
3275
+ const [data, setData] = useState(null);
3276
+ const [draft, setDraft] = useState(null);
3277
+ const [preview, setPreview] = useState(null);
3278
+ const [busy, setBusy] = useState(false);
3279
+ const [error, setError] = useState(null);
3280
+ const [info, setInfo] = useState(null);
3281
+ const [open, setOpen] = useState(false);
3282
+
3283
+ const load = useCallback(async () => {
3284
+ try {
3285
+ const r = await api("/index-config");
3286
+ setData(r);
3287
+ setDraft(toDraft(r.resolved));
3288
+ } catch (err) {
3289
+ setError(err.message);
3290
+ }
3291
+ }, []);
3292
+
3293
+ useEffect(() => {
3294
+ if (open && !data) load();
3295
+ }, [open, data, load]);
3296
+
3297
+ const reset = useCallback(() => {
3298
+ if (data) setDraft(toDraft(data.defaults));
3299
+ setPreview(null);
3300
+ }, [data]);
3301
+
3302
+ const save = useCallback(async () => {
3303
+ if (!draft) return;
3304
+ setBusy(true);
3305
+ setError(null);
3306
+ setInfo(null);
3307
+ try {
3308
+ const payload = fromDraft(draft);
3309
+ const r = await api("/index-config", { method: "POST", body: payload });
3310
+ setInfo(`saved · ${r.changed.length || 0} fields updated · re-run index to apply`);
3311
+ await load();
3312
+ } catch (err) {
3313
+ setError(err.message);
3314
+ } finally {
3315
+ setBusy(false);
3316
+ }
3317
+ }, [draft, load]);
3318
+
3319
+ const runPreview = useCallback(async () => {
3320
+ if (!draft) return;
3321
+ setBusy(true);
3322
+ setError(null);
3323
+ setInfo("running dry walk against project root…");
3324
+ try {
3325
+ const payload = fromDraft(draft);
3326
+ const r = await api("/index-config/preview", { method: "POST", body: payload });
3327
+ setPreview(r);
3328
+ setInfo(null);
3329
+ } catch (err) {
3330
+ setError(err.message);
3331
+ setInfo(null);
3332
+ } finally {
3333
+ setBusy(false);
3334
+ }
3335
+ }, [draft]);
3336
+
3337
+ return html`
3338
+ <div class="excludes-toggle" onClick=${() => setOpen(!open)}>
3339
+ <span class="caret">${open ? "▼" : "▶"}</span>
3340
+ <span class="label">Excludes</span>
3341
+ <span class="hint">config-driven skip rules applied during indexing</span>
3342
+ </div>
3343
+ ${
3344
+ !open
3345
+ ? null
3346
+ : !draft
3347
+ ? html`<div class="empty">loading…</div>`
3348
+ : html`
3349
+ <div class="card excludes-card">
3350
+ ${info ? html`<div class="notice">${info}</div>` : null}
3351
+ ${error ? html`<div class="notice err">${error}</div>` : null}
3352
+ <div class="lead">
3353
+ One value per line. Dirs / files match by basename. Patterns use picomatch syntax (e.g. <code>**/*.generated.ts</code>, <code>vendor/**</code>, <code>!keep-me</code>).
3354
+ </div>
3355
+ <div class="excludes-grid">
3356
+ <${ExcludesField} label="Exclude dirs" value=${draft.excludeDirs} onChange=${(v) => setDraft({ ...draft, excludeDirs: v })} />
3357
+ <${ExcludesField} label="Exclude files" value=${draft.excludeFiles} onChange=${(v) => setDraft({ ...draft, excludeFiles: v })} />
3358
+ <${ExcludesField} label="Exclude extensions" value=${draft.excludeExts} onChange=${(v) => setDraft({ ...draft, excludeExts: v })} />
3359
+ <${ExcludesField} label="Exclude patterns (glob)" value=${draft.excludePatterns} onChange=${(v) => setDraft({ ...draft, excludePatterns: v })} />
3360
+ </div>
3361
+ <div class="excludes-options">
3362
+ <label>
3363
+ <input type="checkbox" checked=${draft.respectGitignore} onChange=${(e) => setDraft({ ...draft, respectGitignore: e.target.checked })} />
3364
+ Respect <code>.gitignore</code>
3365
+ </label>
3366
+ <label>
3367
+ Max file size
3368
+ <input type="number" min="1024" step="1024" value=${draft.maxFileBytes} onChange=${(e) => setDraft({ ...draft, maxFileBytes: Number(e.target.value) || 0 })} />
3369
+ <span class="muted">bytes</span>
3370
+ </label>
3371
+ </div>
3372
+ <div class="excludes-actions">
3373
+ <button class="primary" disabled=${busy} onClick=${save}>Save</button>
3374
+ <button disabled=${busy} onClick=${runPreview}>Preview (dry-walk)</button>
3375
+ <button disabled=${busy} onClick=${reset}>Reset to defaults</button>
3376
+ </div>
3377
+ ${preview ? html`<${ExcludesPreview} preview=${preview} />` : null}
3378
+ </div>
3379
+ `
3380
+ }
3381
+ `;
3382
+ }
3383
+
3384
+ function ExcludesPreview({ preview }) {
3385
+ const buckets = preview.skipBuckets || {};
3386
+ const samples = preview.skipSamples || {};
3387
+ const totalSkipped = Object.values(buckets).reduce((a, b) => a + (b || 0), 0);
3388
+ const reasons = [
3389
+ "gitignore",
3390
+ "pattern",
3391
+ "defaultDir",
3392
+ "defaultFile",
3393
+ "binaryExt",
3394
+ "binaryContent",
3395
+ "tooLarge",
3396
+ "readError",
3397
+ ].filter((k) => (buckets[k] || 0) > 0);
3398
+ return html`
3399
+ <div class="excludes-preview">
3400
+ <div class="summary">
3401
+ Preview — would index <strong>${preview.filesIncluded}</strong> file(s), skip <strong>${totalSkipped}</strong>
3402
+ </div>
3403
+ ${
3404
+ reasons.length === 0
3405
+ ? html`<div class="muted">nothing skipped — all walked files would be indexed.</div>`
3406
+ : reasons.map(
3407
+ (r) => html`
3408
+ <details>
3409
+ <summary><strong>${r}: ${buckets[r]}</strong></summary>
3410
+ <ul>
3411
+ ${(samples[r] || []).map((p) => html`<li><code>${p}</code></li>`)}
3412
+ ${
3413
+ (buckets[r] || 0) > (samples[r] || []).length
3414
+ ? html`<li class="muted">…${buckets[r] - samples[r].length} more</li>`
3415
+ : null
3416
+ }
3417
+ </ul>
3418
+ </details>
3419
+ `,
3420
+ )
3421
+ }
3422
+ ${
3423
+ preview.sampleIncluded?.length
3424
+ ? html`
3425
+ <details>
3426
+ <summary>first ${preview.sampleIncluded.length} included file(s)</summary>
3427
+ <ul>
3428
+ ${preview.sampleIncluded.map((p) => html`<li><code>${p}</code></li>`)}
3429
+ </ul>
3430
+ </details>
3431
+ `
3432
+ : null
3433
+ }
3268
3434
  </div>
3269
3435
  `;
3270
3436
  }
3271
3437
 
3438
+ function ExcludesField({ label, value, onChange }) {
3439
+ return html`
3440
+ <div class="excludes-field">
3441
+ <label>${label}</label>
3442
+ <textarea rows="5" value=${value} onChange=${(e) => onChange(e.target.value)}></textarea>
3443
+ </div>
3444
+ `;
3445
+ }
3446
+
3447
+ function toDraft(c) {
3448
+ return {
3449
+ excludeDirs: (c.excludeDirs ?? []).join("\n"),
3450
+ excludeFiles: (c.excludeFiles ?? []).join("\n"),
3451
+ excludeExts: (c.excludeExts ?? []).join("\n"),
3452
+ excludePatterns: (c.excludePatterns ?? []).join("\n"),
3453
+ respectGitignore: c.respectGitignore !== false,
3454
+ maxFileBytes: c.maxFileBytes ?? 262144,
3455
+ };
3456
+ }
3457
+
3458
+ function fromDraft(d) {
3459
+ const lines = (s) =>
3460
+ s
3461
+ .split(/\r?\n/)
3462
+ .map((x) => x.trim())
3463
+ .filter((x) => x.length > 0);
3464
+ return {
3465
+ excludeDirs: lines(d.excludeDirs),
3466
+ excludeFiles: lines(d.excludeFiles),
3467
+ excludeExts: lines(d.excludeExts),
3468
+ excludePatterns: lines(d.excludePatterns),
3469
+ respectGitignore: !!d.respectGitignore,
3470
+ maxFileBytes: d.maxFileBytes,
3471
+ };
3472
+ }
3473
+
3272
3474
  function SemanticJobView({ job, running }) {
3273
3475
  const phaseLabel =
3274
3476
  {
@@ -3321,10 +3523,34 @@ function SemanticJobView({ job, running }) {
3321
3523
  } · ${(job.result.durationMs / 1000).toFixed(1)}s</div>`
3322
3524
  : null
3323
3525
  }
3526
+ ${
3527
+ job.result?.skipBuckets
3528
+ ? html`<${SkipBucketsView} buckets=${job.result.skipBuckets} />`
3529
+ : null
3530
+ }
3324
3531
  </div>
3325
3532
  `;
3326
3533
  }
3327
3534
 
3535
+ function SkipBucketsView({ buckets }) {
3536
+ const order = [
3537
+ ["gitignore", "gitignore"],
3538
+ ["pattern", "pattern"],
3539
+ ["defaultDir", "defaultDir"],
3540
+ ["defaultFile", "defaultFile"],
3541
+ ["binaryExt", "binaryExt"],
3542
+ ["binaryContent", "binaryContent"],
3543
+ ["tooLarge", "tooLarge"],
3544
+ ["readError", "readError"],
3545
+ ];
3546
+ const total = order.reduce((a, [k]) => a + (buckets[k] || 0), 0);
3547
+ if (total === 0) return null;
3548
+ const parts = order
3549
+ .filter(([k]) => (buckets[k] || 0) > 0)
3550
+ .map(([k, label]) => `${label}: ${buckets[k]}`);
3551
+ return html`<div><span class="kv-key">skipped</span>${total} files <span class="muted">(${parts.join(", ")})</span></div>`;
3552
+ }
3553
+
3328
3554
  function McpPanel() {
3329
3555
  const [data, setData] = useState(null);
3330
3556
  const [specs, setSpecs] = useState(null);
@@ -3753,7 +3979,13 @@ function EditorPanel({ onClose } = {}) {
3753
3979
  viewRef.current = null;
3754
3980
  }
3755
3981
  };
3756
- }, [cmReady, activeIdx, tabs[activeIdx]?.path, viewMode]);
3982
+ }, [cmReady, activeIdx, tabs[activeIdx]?.path]);
3983
+
3984
+ // Becoming visible after display:none — CM6 measures lazily, force it.
3985
+ useEffect(() => {
3986
+ if (viewMode === "preview") return;
3987
+ viewRef.current?.requestMeasure?.();
3988
+ }, [viewMode]);
3757
3989
 
3758
3990
  const closeTab = useCallback((idx) => {
3759
3991
  const tab = tabsRef.current[idx];
@@ -3994,28 +4226,22 @@ function EditorPanel({ onClose } = {}) {
3994
4226
  ${(() => {
3995
4227
  const isMd = langFromPath(tab.path) === "markdown";
3996
4228
  const mode = isMd ? viewMode : "edit";
3997
- if (mode === "preview") {
3998
- return html`
3999
- <div
4000
- class="editor-host editor-md-preview md"
4001
- dangerouslySetInnerHTML=${{ __html: previewMarked.parse(tab.content ?? "") }}
4002
- ></div>
4003
- `;
4004
- }
4005
- if (mode === "split") {
4006
- return html`
4007
- <div class="editor-split">
4008
- <div ref=${editorContainerRef} class="editor-host editor-split-pane"></div>
4009
- <div
4010
- class="editor-host editor-md-preview md editor-split-pane"
4011
- dangerouslySetInnerHTML=${{
4012
- __html: previewMarked.parse(tab.content ?? ""),
4013
- }}
4014
- ></div>
4015
- </div>
4016
- `;
4017
- }
4018
- return html`<div ref=${editorContainerRef} class="editor-host"></div>`;
4229
+ // Stable DOM across mode toggles CM-managed children + Preact reconciliation don't mix when the host moves.
4230
+ return html`
4231
+ <div class="editor-stage" data-mode=${mode}>
4232
+ <div ref=${editorContainerRef} class="editor-host"></div>
4233
+ ${
4234
+ isMd
4235
+ ? html`<div
4236
+ class="editor-md-preview md"
4237
+ dangerouslySetInnerHTML=${{
4238
+ __html: previewMarked.parse(tab.content ?? ""),
4239
+ }}
4240
+ ></div>`
4241
+ : null
4242
+ }
4243
+ </div>
4244
+ `;
4019
4245
  })()}
4020
4246
  `
4021
4247
  : html`