@mikedown/core 0.3.0 → 0.4.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.
@@ -1,22 +1,24 @@
1
1
  import {
2
2
  aiHostTier
3
- } from "./chunk-LXZG7IKN.js";
3
+ } from "./chunk-N4N54LK2.js";
4
4
  import {
5
5
  AI_CLOUD_PRIVACY,
6
- AI_ENGINE_KINDS,
7
6
  AI_ENGINE_PRESETS,
8
- AI_FALLBACK_MODES,
9
7
  AI_HOST_PRESET_FAMILY,
10
8
  AI_HOST_PRIVACY,
11
9
  AI_LOCAL_DETECT_TIMEOUT_MS,
12
10
  AI_PRESET_FAMILIES,
13
11
  AI_TASKS,
14
- DEFAULT_CONTEXT_TOKENS,
12
+ DOCS_NO_HOME_TEXT,
15
13
  aiConsentHost,
16
14
  aiConsentStepNote,
17
15
  aiErrorCopy,
18
16
  aiProviderPolicy,
17
+ checkDocRoot,
18
+ clampDocsHops,
19
+ clampDocsSourceShare,
19
20
  detectCapabilities,
21
+ docRootKey,
20
22
  formatModelSize,
21
23
  generateAiId,
22
24
  getAiTask,
@@ -28,6 +30,8 @@ import {
28
30
  missingCapabilities,
29
31
  modelMoeFastHint,
30
32
  modelSizeHint,
33
+ normalizeDocHomeFile,
34
+ parseAiSettings,
31
35
  presetForEngine,
32
36
  recommendSetupPath,
33
37
  resolveRoute,
@@ -38,14 +42,22 @@ import {
38
42
  vscodeLmAvailability,
39
43
  whatIsSent,
40
44
  withThinkingLevel
41
- } from "./chunk-JZWKEBCI.js";
45
+ } from "./chunk-IWCXWUWW.js";
42
46
  import {
47
+ AI_DOCS_MAX_HOPS_MAX,
48
+ AI_DOCS_MAX_HOPS_MIN,
49
+ AI_DOCS_NAVIGATE_BY,
50
+ AI_DOCS_SOURCE_SHARE_MAX,
51
+ AI_DOCS_SOURCE_SHARE_MIN,
52
+ AI_ENGINE_KINDS,
53
+ AI_FALLBACK_MODES,
54
+ DEFAULT_CONTEXT_TOKENS,
43
55
  DEFAULT_LOCAL_NETWORKS,
44
56
  detectLocality,
45
57
  engineLocality,
46
58
  hostOfUrl,
47
59
  localityMismatch
48
- } from "./chunk-QDVY7WW6.js";
60
+ } from "./chunk-APPBNQEY.js";
49
61
 
50
62
  // src/ai-settings-model.ts
51
63
  var AI_ENGINE_KIND_LABELS = {
@@ -1149,7 +1161,8 @@ var ICONS = {
1149
1161
  refresh: svg('<path d="M13 8a5 5 0 1 1-1.5-3.55M13 2.5v3h-3"/>', 12),
1150
1162
  search: svg('<circle cx="7" cy="7" r="4.25"/><path d="M10.2 10.2l3.3 3.3"/>', 12),
1151
1163
  back: svg('<path d="M10 3.5L5.5 8l4.5 4.5"/>', 14),
1152
- routing: svg('<path d="M3 4h4.5a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2H13M3 12h3M11 2.5L13 4l-2 1.5M11 10.5l2 1.5-2 1.5"/>', 16)
1164
+ routing: svg('<path d="M3 4h4.5a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2H13M3 12h3M11 2.5L13 4l-2 1.5M11 10.5l2 1.5-2 1.5"/>', 16),
1165
+ book: svg('<path d="M3 13V4a1.5 1.5 0 0 1 1.5-1.5H13v8.5H4.5A1.5 1.5 0 0 0 3 12.5 1.5 1.5 0 0 0 4.5 14H13M6 5.5h4"/>', 16)
1153
1166
  };
1154
1167
  var KIND_MONOGRAMS = {
1155
1168
  lmstudio: "LM",
@@ -3087,6 +3100,771 @@ function createRoutingSection(ctx, deps) {
3087
3100
  return { element: body, render };
3088
3101
  }
3089
3102
 
3103
+ // src/ai-docs-page.ts
3104
+ var DOCS_CLOUD_LABEL = "May be sent to cloud engines";
3105
+ var DOCS_CLOUD_LINE = "Pages from this base may go to the cloud engines you allow.";
3106
+ var DOCS_NOT_CHECKED_TEXT = "Not checked yet";
3107
+ var DOCS_ROOT_MISSING_TEXT = "Root not found";
3108
+ var NAVIGATE_LABELS = { section: "Section", page: "Page" };
3109
+ var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
3110
+ function docsRootChips(root) {
3111
+ if (!root.found) {
3112
+ return [{ text: DOCS_ROOT_MISSING_TEXT, tone: "warn" }];
3113
+ }
3114
+ const chips = [];
3115
+ const hasHome = !!root.home && root.home !== "map" && !!root.homePath;
3116
+ chips.push({ text: hasHome ? `Home: ${root.homePath}` : DOCS_NO_HOME_TEXT, tone: "quiet" });
3117
+ chips.push({ text: plural(root.fileCount, "file"), tone: "quiet" });
3118
+ if (root.oversizeSkipped > 0) {
3119
+ chips.push({ text: `${root.oversizeSkipped} too large, skipped`, tone: "quiet" });
3120
+ }
3121
+ if (root.capReached) {
3122
+ chips.push({ text: "File limit reached", tone: "warn" });
3123
+ }
3124
+ if (root.notTextSkipped) {
3125
+ chips.push({ text: `${root.notTextSkipped} not UTF-8 text, skipped`, tone: "quiet" });
3126
+ }
3127
+ if (root.dirCapReached) {
3128
+ chips.push({ text: "Folder limit reached", tone: "warn" });
3129
+ }
3130
+ return chips;
3131
+ }
3132
+ function docsBaseChips(status) {
3133
+ const chips = [];
3134
+ if (status.brokenLinks) {
3135
+ chips.push({ text: plural(status.brokenLinks, "broken link"), tone: "warn" });
3136
+ }
3137
+ if (status.timedOut) {
3138
+ chips.push({ text: "Stopped early: this folder took too long to read", tone: "warn" });
3139
+ }
3140
+ return chips;
3141
+ }
3142
+ function withDocsOptions(settings, patch) {
3143
+ const next = { ...settings.docs, ...patch };
3144
+ return {
3145
+ ...settings,
3146
+ docs: {
3147
+ ...next,
3148
+ maxHops: clampDocsHops(next.maxHops),
3149
+ sourceShare: clampDocsSourceShare(next.sourceShare),
3150
+ navigateBy: AI_DOCS_NAVIGATE_BY.includes(next.navigateBy) ? next.navigateBy : "section"
3151
+ }
3152
+ };
3153
+ }
3154
+ function checkBaseRoot(raw, inThisBase, others) {
3155
+ const checked = checkDocRoot(raw);
3156
+ if (!checked.ok) {
3157
+ return { ok: false, message: `This path ${checked.reason}.` };
3158
+ }
3159
+ const key = docRootKey(checked.path);
3160
+ if (inThisBase.some((r) => docRootKey(r) === key)) {
3161
+ return { ok: false, message: "This base already has that folder." };
3162
+ }
3163
+ const holder = others.find((b) => b.roots.some((r) => docRootKey(r) === key));
3164
+ if (holder) {
3165
+ return { ok: false, message: `That folder is already in \u201C${holder.name}\u201D.` };
3166
+ }
3167
+ return { ok: true, path: checked.path };
3168
+ }
3169
+ function checkDocBaseForm(values, others, existing) {
3170
+ const errors = {};
3171
+ if (values.roots.length === 0) {
3172
+ errors.roots = "Add at least one folder.";
3173
+ }
3174
+ for (const [i, root] of values.roots.entries()) {
3175
+ const checked = checkBaseRoot(root, values.roots.slice(0, i), others);
3176
+ if (!checked.ok) {
3177
+ errors.roots = `${root}: ${checked.message}`;
3178
+ break;
3179
+ }
3180
+ }
3181
+ const homeText = values.homeFile.trim();
3182
+ const home = homeText ? normalizeDocHomeFile(homeText) : null;
3183
+ if (homeText && !home) {
3184
+ errors.homeFile = "The home file must be a markdown file name or a path inside the folder, such as HOME.md.";
3185
+ }
3186
+ if (errors.roots || errors.homeFile) {
3187
+ return { ok: false, errors };
3188
+ }
3189
+ const base = {
3190
+ ...existing ?? { enabled: true },
3191
+ id: existing?.id ?? "",
3192
+ name: values.name.trim(),
3193
+ roots: values.roots,
3194
+ allowCloud: values.allowCloud
3195
+ };
3196
+ if (home) {
3197
+ base.homeFile = home;
3198
+ } else {
3199
+ delete base.homeFile;
3200
+ }
3201
+ return { ok: true, base };
3202
+ }
3203
+ function revalidateDocBases(bases) {
3204
+ const warnings = [];
3205
+ const taken = bases.map((b) => b.id).filter(Boolean);
3206
+ const parsed = parseAiSettings({ docBases: bases }, {
3207
+ onWarning: (w) => warnings.push(w),
3208
+ generateId: (name) => {
3209
+ const id = uniqueEngineId(name, taken);
3210
+ taken.push(id);
3211
+ return id;
3212
+ }
3213
+ }).docBases;
3214
+ if (warnings.length > 0) {
3215
+ return { ok: false, reason: `Not saved: ${warnings[0].replace(/^docBases\[\d+\](\.\w+(\[\d+\])?)?\s*/, "")}` };
3216
+ }
3217
+ return { ok: true, bases: parsed };
3218
+ }
3219
+ function lastPathSegment(path) {
3220
+ const parts = path.split(/[\\/]+/).filter(Boolean);
3221
+ return parts[parts.length - 1] ?? "";
3222
+ }
3223
+ function sameRootsAndHome(a, b) {
3224
+ return JSON.stringify(a.roots) === JSON.stringify(b.roots) && (a.homeFile ?? "") === (b.homeFile ?? "");
3225
+ }
3226
+ function createDocsBasesSection(ctx, deps) {
3227
+ const { docs } = deps;
3228
+ const element = el("div", "ai-docs");
3229
+ const listView = el("div", "ai-docs-list-view");
3230
+ const formView = el("div", "ai-docs-form-view");
3231
+ formView.hidden = true;
3232
+ let entries = [];
3233
+ let loaded = false;
3234
+ let checking = false;
3235
+ let openedOnce = false;
3236
+ let disposed = false;
3237
+ let problem = null;
3238
+ let confirmRemoveId = null;
3239
+ let form = null;
3240
+ let queue = Promise.resolve();
3241
+ function enqueue(job) {
3242
+ const run = queue.then(job);
3243
+ queue = run.catch(() => {
3244
+ });
3245
+ return run;
3246
+ }
3247
+ function focusFk(key) {
3248
+ Array.from(element.querySelectorAll("[data-fk]")).find((n) => n.dataset.fk === key)?.focus({ preventScroll: true });
3249
+ }
3250
+ function load() {
3251
+ if (checking || disposed) {
3252
+ return;
3253
+ }
3254
+ checking = true;
3255
+ problem = null;
3256
+ renderList();
3257
+ deps.announce("Checking bases\u2026");
3258
+ void enqueue(async () => {
3259
+ if (disposed) {
3260
+ return;
3261
+ }
3262
+ try {
3263
+ const list = await docs.listBases({ refresh: true });
3264
+ if (disposed) {
3265
+ return;
3266
+ }
3267
+ entries = list.map((status) => ({ base: status.base, health: status }));
3268
+ loaded = true;
3269
+ deps.announce(`Checked ${plural(entries.length, "base")}.`);
3270
+ } catch (err) {
3271
+ if (disposed) {
3272
+ return;
3273
+ }
3274
+ problem = `Could not read your bases: ${toAiError(err).message}`;
3275
+ deps.announce(problem);
3276
+ }
3277
+ checking = false;
3278
+ if (confirmRemoveId && !entries.some((e) => e.base.id === confirmRemoveId)) {
3279
+ confirmRemoveId = null;
3280
+ }
3281
+ renderList();
3282
+ deps.onCountChanged();
3283
+ });
3284
+ }
3285
+ function mutate(change) {
3286
+ return enqueue(async () => {
3287
+ if (disposed) {
3288
+ return null;
3289
+ }
3290
+ if (!loaded) {
3291
+ return "Your bases could not be read, so nothing was saved. Choose Refresh and try again.";
3292
+ }
3293
+ const checked = revalidateDocBases(change(entries.map((e) => e.base)));
3294
+ if (!checked.ok) {
3295
+ return checked.reason;
3296
+ }
3297
+ try {
3298
+ await docs.saveBases(checked.bases);
3299
+ } catch (err) {
3300
+ return `Not saved: ${toAiError(err).message}`;
3301
+ }
3302
+ if (disposed) {
3303
+ return null;
3304
+ }
3305
+ const before = new Map(entries.map((e) => [e.base.id, e]));
3306
+ entries = checked.bases.map((base) => {
3307
+ const prev = before.get(base.id);
3308
+ if (!prev?.health || !sameRootsAndHome(prev.base, base)) {
3309
+ return { base, health: null };
3310
+ }
3311
+ return { base, health: { ...prev.health, base, active: base.enabled && prev.health.rootsMissing < base.roots.length } };
3312
+ });
3313
+ problem = null;
3314
+ deps.onCountChanged();
3315
+ return null;
3316
+ });
3317
+ }
3318
+ function chipNode(chip) {
3319
+ const node = el("span", `ai-docs-chip${chip.tone === "warn" ? " ai-docs-chip--warn" : ""}`);
3320
+ if (chip.tone === "warn") {
3321
+ node.innerHTML = ICONS.warn;
3322
+ }
3323
+ node.append(el("span", "", chip.text));
3324
+ return node;
3325
+ }
3326
+ function chipLine(chips) {
3327
+ const line = el("span", "ai-docs-chips");
3328
+ line.append(...chips.map(chipNode));
3329
+ return line;
3330
+ }
3331
+ function renderList() {
3332
+ keepFocus(listView, () => {
3333
+ listView.replaceChildren();
3334
+ listView.append(buildBar());
3335
+ if (problem) {
3336
+ listView.append(note("error", problem, "ai-result"));
3337
+ }
3338
+ if (loaded && entries.length === 0) {
3339
+ const empty = el("div", "ai-summary-empty");
3340
+ empty.append(
3341
+ el("div", "ai-empty-title", "No bases yet"),
3342
+ el("div", "ai-empty-text", "A base is a folder of markdown the AI can read to answer your questions."),
3343
+ el("div", "ai-empty-text", "Add your docs folder, and chat can cite its pages.")
3344
+ );
3345
+ listView.append(empty);
3346
+ } else if (entries.length > 0) {
3347
+ const group = ctx.makeGroup(...entries.map(buildRow));
3348
+ group.setAttribute("role", "list");
3349
+ group.setAttribute("aria-label", "Documentation bases");
3350
+ listView.append(group);
3351
+ }
3352
+ });
3353
+ }
3354
+ function buildBar() {
3355
+ const bar = el("div", "ai-docs-bar");
3356
+ bar.append(el("span", "ai-docs-status", checking ? "Checking bases\u2026" : ""));
3357
+ if (entries.length > 0 || !loaded) {
3358
+ const refresh = button("settings-btn", "Refresh", { html: ICONS.refresh, fk: "docs:refresh", title: "Check the folders again" });
3359
+ if (checking) {
3360
+ refresh.setAttribute("aria-disabled", "true");
3361
+ }
3362
+ refresh.addEventListener("click", () => load());
3363
+ bar.append(refresh);
3364
+ }
3365
+ const add = button("settings-btn settings-btn--secondary", "Add base", { html: ICONS.plus, fk: "docs:add" });
3366
+ if (!loaded) {
3367
+ add.setAttribute("aria-disabled", "true");
3368
+ }
3369
+ add.addEventListener("click", () => {
3370
+ if (loaded) {
3371
+ openForm();
3372
+ }
3373
+ });
3374
+ bar.append(add);
3375
+ return bar;
3376
+ }
3377
+ function buildRow({ base, health }) {
3378
+ const row = el("div", "settings-row ai-docs-base");
3379
+ row.setAttribute("role", "listitem");
3380
+ row.dataset.baseId = base.id;
3381
+ row.classList.toggle("is-disabled", !base.enabled);
3382
+ const head = el("div", "ai-docs-base-head");
3383
+ const title = el("div", "ai-docs-base-title");
3384
+ title.append(el("span", "ai-docs-base-name", base.name));
3385
+ if (base.allowCloud) {
3386
+ title.append(chipNode({ text: "May go to cloud", tone: "quiet" }));
3387
+ }
3388
+ if (!health) {
3389
+ title.append(chipNode({ text: DOCS_NOT_CHECKED_TEXT, tone: "quiet" }));
3390
+ }
3391
+ const actions = el("div", "ai-docs-base-actions");
3392
+ const edit = button("ai-link-btn", "Edit", { ariaLabel: `Edit ${base.name}`, fk: `base:${base.id}:edit` });
3393
+ edit.addEventListener("click", () => openForm(base));
3394
+ const remove = button("ai-icon-btn ai-delete-btn", "", { html: ICONS.trash, ariaLabel: `Remove ${base.name}`, title: "Remove from this list", fk: `base:${base.id}:remove` });
3395
+ remove.addEventListener("click", () => {
3396
+ confirmRemoveId = base.id;
3397
+ renderList();
3398
+ focusFk(`base:${base.id}:confirm-cancel`);
3399
+ });
3400
+ const enabled = switchInput(base.enabled, `Use ${base.name}`, `base:${base.id}:enabled`);
3401
+ enabled.addEventListener("change", () => setEnabled(base, enabled.checked));
3402
+ actions.append(edit, remove, enabled);
3403
+ head.append(title, actions);
3404
+ const roots = el("ul", "ai-docs-roots");
3405
+ for (const path of base.roots) {
3406
+ const item = el("li", "ai-docs-root");
3407
+ const pathNode = el("span", "ai-docs-path ai-mono", path);
3408
+ pathNode.title = path;
3409
+ item.append(pathNode);
3410
+ const status = health?.roots.find((r) => docRootKey(r.root) === docRootKey(path));
3411
+ if (status) {
3412
+ item.append(chipLine(docsRootChips(status)));
3413
+ }
3414
+ roots.append(item);
3415
+ }
3416
+ row.append(head, roots);
3417
+ const baseChips = health ? docsBaseChips(health) : [];
3418
+ if (baseChips.length > 0) {
3419
+ row.append(chipLine(baseChips));
3420
+ }
3421
+ if (confirmRemoveId === base.id) {
3422
+ row.append(buildRemoveConfirm(base));
3423
+ }
3424
+ return row;
3425
+ }
3426
+ function setEnabled(base, on) {
3427
+ void mutate((bases) => bases.map((b) => b.id === base.id ? { ...b, enabled: on } : b)).then((error) => {
3428
+ if (disposed) {
3429
+ return;
3430
+ }
3431
+ problem = error;
3432
+ renderList();
3433
+ deps.announce(error ?? `${base.name} turned ${on ? "on" : "off"}.`);
3434
+ });
3435
+ }
3436
+ function buildRemoveConfirm(base) {
3437
+ const box = el("div", "ai-confirm");
3438
+ box.setAttribute("role", "group");
3439
+ box.setAttribute("aria-label", `Remove ${base.name}?`);
3440
+ const yes = button("settings-btn ai-btn--danger", "Remove", { fk: `base:${base.id}:confirm-remove` });
3441
+ const no = button("settings-btn", "Cancel", { fk: `base:${base.id}:confirm-cancel` });
3442
+ yes.addEventListener("click", () => removeBase(base));
3443
+ no.addEventListener("click", () => closeConfirm());
3444
+ box.append(el("span", "ai-confirm-text", `Remove ${base.name} from this list? The folders and files on disk are not touched.`), yes, no);
3445
+ return box;
3446
+ }
3447
+ function closeConfirm() {
3448
+ const id = confirmRemoveId;
3449
+ confirmRemoveId = null;
3450
+ renderList();
3451
+ if (id) {
3452
+ focusFk(`base:${id}:remove`);
3453
+ }
3454
+ }
3455
+ function removeBase(base) {
3456
+ const index = entries.findIndex((e) => e.base.id === base.id);
3457
+ void mutate((bases) => bases.filter((b) => b.id !== base.id)).then((error) => {
3458
+ if (disposed) {
3459
+ return;
3460
+ }
3461
+ problem = error;
3462
+ confirmRemoveId = null;
3463
+ renderList();
3464
+ if (error) {
3465
+ deps.announce(error);
3466
+ focusFk(`base:${base.id}:remove`);
3467
+ return;
3468
+ }
3469
+ deps.announce(`Removed ${base.name} from this list. No files were touched.`);
3470
+ const next = entries[Math.min(index, entries.length - 1)];
3471
+ focusFk(next ? `base:${next.base.id}:edit` : "docs:add");
3472
+ });
3473
+ }
3474
+ const advancedPanelId = nextId("ai-docs-advanced");
3475
+ const advancedToggle = button("ai-disclosure ai-docs-advanced-toggle", "Advanced", { html: ICONS.chevron, fk: "docs:advanced" });
3476
+ advancedToggle.setAttribute("aria-expanded", "false");
3477
+ advancedToggle.setAttribute("aria-controls", advancedPanelId);
3478
+ const advancedPanel = el("div", "ai-advanced ai-docs-advanced");
3479
+ advancedPanel.id = advancedPanelId;
3480
+ advancedPanel.hidden = true;
3481
+ advancedToggle.addEventListener("click", () => {
3482
+ advancedPanel.hidden = !advancedPanel.hidden;
3483
+ advancedToggle.setAttribute("aria-expanded", String(!advancedPanel.hidden));
3484
+ });
3485
+ function numberRow(label, description, read, write, min, max, fk) {
3486
+ const row = ctx.makeRow(label, description, "inline");
3487
+ const input = el("input", "settings-input settings-input--small ai-docs-number");
3488
+ input.type = "number";
3489
+ input.min = String(min);
3490
+ input.max = String(max);
3491
+ input.step = "1";
3492
+ input.value = String(read());
3493
+ input.dataset.fk = fk;
3494
+ input.setAttribute("aria-label", label);
3495
+ input.addEventListener("change", () => {
3496
+ const value = input.value.trim() === "" ? NaN : Number(input.value);
3497
+ if (Number.isFinite(value)) {
3498
+ write(value);
3499
+ }
3500
+ input.value = String(read());
3501
+ });
3502
+ row.append(input);
3503
+ return row;
3504
+ }
3505
+ function buildAdvanced() {
3506
+ const draft = () => deps.getDraft();
3507
+ const hops = numberRow(
3508
+ "Max hops",
3509
+ `Rounds of picking pages before the answer, ${AI_DOCS_MAX_HOPS_MIN} to ${AI_DOCS_MAX_HOPS_MAX}.`,
3510
+ () => draft().docs.maxHops,
3511
+ (n) => deps.setDraft(withDocsOptions(draft(), { maxHops: n })),
3512
+ AI_DOCS_MAX_HOPS_MIN,
3513
+ AI_DOCS_MAX_HOPS_MAX,
3514
+ "docs:hops"
3515
+ );
3516
+ const share = numberRow(
3517
+ "Source share",
3518
+ `How much of the model\u2019s context the pages may take, ${AI_DOCS_SOURCE_SHARE_MIN} to ${AI_DOCS_SOURCE_SHARE_MAX}%.`,
3519
+ () => draft().docs.sourceShare,
3520
+ (n) => deps.setDraft(withDocsOptions(draft(), { sourceShare: n })),
3521
+ AI_DOCS_SOURCE_SHARE_MIN,
3522
+ AI_DOCS_SOURCE_SHARE_MAX,
3523
+ "docs:share"
3524
+ );
3525
+ const by = ctx.makeSelectRow(
3526
+ "Navigate by",
3527
+ "What the AI asks to read: one section, or a whole page.",
3528
+ draft().docs.navigateBy,
3529
+ AI_DOCS_NAVIGATE_BY.map((value) => ({ value, label: NAVIGATE_LABELS[value] }))
3530
+ );
3531
+ by.select.classList.add("settings-input--small", "ai-docs-select");
3532
+ by.select.dataset.fk = "docs:navigate-by";
3533
+ by.select.setAttribute("aria-label", "Navigate by");
3534
+ by.select.addEventListener("change", () => deps.setDraft(withDocsOptions(draft(), { navigateBy: by.select.value })));
3535
+ advancedPanel.append(ctx.makeGroup(hops, share, by.row), el("div", "settings-desc", "These save when you choose Done."));
3536
+ }
3537
+ const otherBases = (existing) => entries.map((e) => e.base).filter((b) => b.id !== existing?.id);
3538
+ function openForm(existing) {
3539
+ confirmRemoveId = null;
3540
+ const values = {
3541
+ name: existing?.name ?? "",
3542
+ roots: [...existing?.roots ?? []],
3543
+ homeFile: existing?.homeFile ?? "",
3544
+ allowCloud: existing?.allowCloud ?? false
3545
+ };
3546
+ const state = {
3547
+ existing,
3548
+ initial: { ...values, roots: [...values.roots] },
3549
+ values,
3550
+ saving: false,
3551
+ showError: () => {
3552
+ },
3553
+ focusFirst: () => {
3554
+ }
3555
+ };
3556
+ form = state;
3557
+ formView.replaceChildren(buildForm(state));
3558
+ showView();
3559
+ state.focusFirst();
3560
+ }
3561
+ function closeForm(focusKey) {
3562
+ form = null;
3563
+ formView.replaceChildren();
3564
+ showView();
3565
+ renderList();
3566
+ if (focusKey) {
3567
+ focusFk(focusKey);
3568
+ }
3569
+ }
3570
+ function showView() {
3571
+ const editing = !!form;
3572
+ formView.hidden = !editing;
3573
+ listView.hidden = editing;
3574
+ advancedToggle.hidden = editing;
3575
+ if (editing) {
3576
+ advancedPanel.hidden = true;
3577
+ advancedToggle.setAttribute("aria-expanded", "false");
3578
+ }
3579
+ }
3580
+ function labelRow(label, description, layout, control) {
3581
+ const row = ctx.makeRow(label, description, layout);
3582
+ if (control) {
3583
+ control.id || (control.id = nextId("ai-docs-field"));
3584
+ const labelNode = row.querySelector("label");
3585
+ if (labelNode) {
3586
+ labelNode.htmlFor = control.id;
3587
+ }
3588
+ }
3589
+ return row;
3590
+ }
3591
+ function buildForm(state) {
3592
+ const { existing, values } = state;
3593
+ const box = el("div", "ai-docs-form");
3594
+ box.setAttribute("role", "group");
3595
+ const heading = el("div", "ai-subhead", existing ? `Edit ${existing.name}` : "Add a base");
3596
+ heading.id = nextId("ai-docs-form-title");
3597
+ box.setAttribute("aria-labelledby", heading.id);
3598
+ const nameInput = el("input", "settings-input");
3599
+ nameInput.type = "text";
3600
+ nameInput.value = values.name;
3601
+ nameInput.dataset.fk = "form:name";
3602
+ nameInput.spellcheck = false;
3603
+ nameInput.placeholder = "Team docs";
3604
+ nameInput.addEventListener("input", () => {
3605
+ values.name = nameInput.value;
3606
+ });
3607
+ const nameRow = labelRow("Name", "Shown in the chat and when asking before a cloud request.", "inline", nameInput);
3608
+ nameRow.append(nameInput);
3609
+ const rootsError = el("div", "ai-field-error");
3610
+ rootsError.id = nextId("ai-docs-roots-error");
3611
+ rootsError.setAttribute("aria-live", "polite");
3612
+ const rootsList = el("ul", "ai-docs-form-roots");
3613
+ rootsList.setAttribute("aria-label", "Folders in this base");
3614
+ function setRootsError(text) {
3615
+ rootsError.textContent = text;
3616
+ }
3617
+ function renderRoots() {
3618
+ keepFocus(rootsList, () => {
3619
+ rootsList.replaceChildren();
3620
+ for (const root of values.roots) {
3621
+ const item = el("li", "ai-docs-form-root");
3622
+ const pathNode = el("span", "ai-docs-path ai-mono", root);
3623
+ pathNode.title = root;
3624
+ const remove = button("ai-icon-btn", "", { html: ICONS.close, ariaLabel: `Take ${root} out of this base`, title: "Take out of this base", fk: `form:root-remove:${root}` });
3625
+ remove.addEventListener("click", () => {
3626
+ const at = values.roots.indexOf(root);
3627
+ values.roots = values.roots.filter((r) => r !== root);
3628
+ renderRoots();
3629
+ const next = values.roots[Math.min(at, values.roots.length - 1)];
3630
+ focusFk(next ? `form:root-remove:${next}` : firstAddKey);
3631
+ });
3632
+ item.append(pathNode, remove);
3633
+ rootsList.append(item);
3634
+ }
3635
+ });
3636
+ }
3637
+ function addRoot(raw) {
3638
+ const checked = checkBaseRoot(raw, values.roots, otherBases(existing));
3639
+ if (!checked.ok) {
3640
+ setRootsError(checked.message);
3641
+ return false;
3642
+ }
3643
+ setRootsError("");
3644
+ values.roots = [...values.roots, checked.path];
3645
+ if (!values.name.trim()) {
3646
+ values.name = lastPathSegment(checked.path);
3647
+ nameInput.value = values.name;
3648
+ }
3649
+ renderRoots();
3650
+ deps.announce(`Added ${checked.path}.`);
3651
+ return true;
3652
+ }
3653
+ function pickerButton(label, fk, pick) {
3654
+ const b = button("settings-btn settings-btn--secondary", label, { fk });
3655
+ b.addEventListener("click", () => {
3656
+ void pick().then(
3657
+ (path) => {
3658
+ if (form === state && path) {
3659
+ addRoot(path);
3660
+ }
3661
+ },
3662
+ (err) => {
3663
+ if (form === state) {
3664
+ setRootsError(toAiError(err).message);
3665
+ }
3666
+ }
3667
+ );
3668
+ });
3669
+ return b;
3670
+ }
3671
+ const adders = el("div", "ai-docs-form-adders");
3672
+ if (docs.addWorkspaceFolder) {
3673
+ adders.append(pickerButton("Add this workspace folder", "form:workspace", () => docs.addWorkspaceFolder()));
3674
+ }
3675
+ if (docs.pickFolder) {
3676
+ adders.append(pickerButton("Choose folder\u2026", "form:pick", () => docs.pickFolder()));
3677
+ }
3678
+ let pathLine = null;
3679
+ if (!docs.pickFolder) {
3680
+ pathLine = el("div", "ai-docs-form-path");
3681
+ const pathInput = el("input", "settings-input ai-mono");
3682
+ pathInput.type = "text";
3683
+ pathInput.dataset.fk = "form:path";
3684
+ pathInput.spellcheck = false;
3685
+ pathInput.placeholder = "/full/path/to/docs";
3686
+ pathInput.setAttribute("aria-label", "Folder path");
3687
+ pathInput.setAttribute("aria-describedby", rootsError.id);
3688
+ const addPath = button("settings-btn settings-btn--secondary", "Add", { ariaLabel: "Add this path", fk: "form:add-path" });
3689
+ const submitPath = () => {
3690
+ if (addRoot(pathInput.value)) {
3691
+ pathInput.value = "";
3692
+ pathInput.removeAttribute("aria-invalid");
3693
+ } else {
3694
+ pathInput.setAttribute("aria-invalid", "true");
3695
+ }
3696
+ };
3697
+ addPath.addEventListener("click", submitPath);
3698
+ pathInput.addEventListener("input", () => {
3699
+ pathInput.removeAttribute("aria-invalid");
3700
+ setRootsError("");
3701
+ });
3702
+ pathInput.addEventListener("keydown", (e) => {
3703
+ if (e.key !== "Enter") {
3704
+ return;
3705
+ }
3706
+ e.preventDefault();
3707
+ submitPath();
3708
+ });
3709
+ pathLine.append(pathInput, addPath);
3710
+ }
3711
+ const firstAddKey = docs.addWorkspaceFolder ? "form:workspace" : docs.pickFolder ? "form:pick" : "form:path";
3712
+ const rootsRow = ctx.makeRow("Folders", "Every markdown file under these folders can be read. Symlinks are skipped.");
3713
+ rootsRow.append(rootsList);
3714
+ if (adders.childElementCount > 0) {
3715
+ rootsRow.append(adders);
3716
+ }
3717
+ if (pathLine) {
3718
+ rootsRow.append(pathLine);
3719
+ }
3720
+ rootsRow.append(rootsError);
3721
+ renderRoots();
3722
+ const homeInput = el("input", "settings-input ai-mono");
3723
+ homeInput.type = "text";
3724
+ homeInput.value = values.homeFile;
3725
+ homeInput.dataset.fk = "form:home";
3726
+ homeInput.spellcheck = false;
3727
+ homeInput.placeholder = "HOME.md";
3728
+ const homeError = el("div", "ai-field-error");
3729
+ homeError.id = nextId("ai-docs-home-error");
3730
+ homeError.setAttribute("aria-live", "polite");
3731
+ homeInput.setAttribute("aria-describedby", homeError.id);
3732
+ homeInput.addEventListener("input", () => {
3733
+ values.homeFile = homeInput.value;
3734
+ homeError.textContent = "";
3735
+ homeInput.removeAttribute("aria-invalid");
3736
+ });
3737
+ const homeRow = labelRow("Home file", "Optional. The page to start from in each folder. Otherwise INDEX.md, then README.md.", "inline", homeInput);
3738
+ homeRow.append(homeInput, homeError);
3739
+ const cloud = switchInput(values.allowCloud, DOCS_CLOUD_LABEL, "form:cloud");
3740
+ cloud.addEventListener("change", () => {
3741
+ values.allowCloud = cloud.checked;
3742
+ });
3743
+ const cloudRow = labelRow(DOCS_CLOUD_LABEL, DOCS_CLOUD_LINE, "inline", cloud);
3744
+ cloudRow.append(cloud);
3745
+ const formError = el("div", "ai-docs-form-error");
3746
+ formError.setAttribute("aria-live", "polite");
3747
+ state.showError = (text) => formError.replaceChildren(note("error", text, "ai-result"));
3748
+ const save = button("settings-btn settings-btn--primary", existing ? "Save" : "Add base", { fk: "form:save" });
3749
+ const cancel = button("settings-btn", "Cancel", { fk: "form:cancel" });
3750
+ save.addEventListener("click", () => submit());
3751
+ cancel.addEventListener("click", () => back());
3752
+ const actions = el("div", "ai-docs-form-actions");
3753
+ actions.append(save, cancel);
3754
+ function submit() {
3755
+ if (state.saving) {
3756
+ return;
3757
+ }
3758
+ formError.replaceChildren();
3759
+ const checked = checkDocBaseForm(values, otherBases(existing), existing);
3760
+ setRootsError("");
3761
+ homeError.textContent = "";
3762
+ homeInput.removeAttribute("aria-invalid");
3763
+ if (!checked.ok) {
3764
+ setRootsError(checked.errors.roots ?? "");
3765
+ homeError.textContent = checked.errors.homeFile ?? "";
3766
+ if (checked.errors.homeFile) {
3767
+ homeInput.setAttribute("aria-invalid", "true");
3768
+ }
3769
+ deps.announce(checked.errors.roots ?? checked.errors.homeFile ?? "");
3770
+ focusFk(checked.errors.roots ? firstAddKey : "form:home");
3771
+ return;
3772
+ }
3773
+ state.saving = true;
3774
+ save.setAttribute("aria-disabled", "true");
3775
+ const base = checked.base;
3776
+ void mutate((bases) => existing ? bases.map((b) => b.id === existing.id ? base : b) : [...bases, base]).then((error) => {
3777
+ if (disposed) {
3778
+ return;
3779
+ }
3780
+ if (form !== state) {
3781
+ problem = error;
3782
+ renderList();
3783
+ deps.announce(error ?? `Saved ${base.name || lastPathSegment(base.roots[0])}.`);
3784
+ return;
3785
+ }
3786
+ state.saving = false;
3787
+ save.removeAttribute("aria-disabled");
3788
+ if (error) {
3789
+ state.showError(error);
3790
+ deps.announce(error);
3791
+ save.focus();
3792
+ return;
3793
+ }
3794
+ const name = base.name || lastPathSegment(base.roots[0]);
3795
+ deps.announce(existing ? `Saved ${name}.` : `Added ${name}. Choose Refresh to check it.`);
3796
+ const saved = existing?.id ?? entries[entries.length - 1]?.base.id;
3797
+ closeForm(saved ? `base:${saved}:edit` : "docs:add");
3798
+ });
3799
+ }
3800
+ for (const input of [nameInput, homeInput]) {
3801
+ input.addEventListener("keydown", (e) => {
3802
+ if (e.key !== "Enter") {
3803
+ return;
3804
+ }
3805
+ e.preventDefault();
3806
+ submit();
3807
+ });
3808
+ }
3809
+ state.focusFirst = () => {
3810
+ if (existing || values.roots.length > 0) {
3811
+ nameInput.focus({ preventScroll: true });
3812
+ } else {
3813
+ focusFk(firstAddKey);
3814
+ }
3815
+ };
3816
+ box.append(heading, ctx.makeGroup(nameRow, rootsRow, homeRow, cloudRow), formError, actions);
3817
+ return box;
3818
+ }
3819
+ function formDirty() {
3820
+ return !!form && JSON.stringify(form.values) !== JSON.stringify(form.initial);
3821
+ }
3822
+ function back() {
3823
+ if (form) {
3824
+ const id = form.existing?.id;
3825
+ closeForm(id ? `base:${id}:edit` : "docs:add");
3826
+ return true;
3827
+ }
3828
+ if (confirmRemoveId) {
3829
+ closeConfirm();
3830
+ return true;
3831
+ }
3832
+ return false;
3833
+ }
3834
+ buildAdvanced();
3835
+ element.append(listView, formView, advancedToggle, advancedPanel);
3836
+ renderList();
3837
+ return {
3838
+ element,
3839
+ opened() {
3840
+ if (openedOnce) {
3841
+ return;
3842
+ }
3843
+ openedOnce = true;
3844
+ load();
3845
+ },
3846
+ back,
3847
+ hasUnsavedForm: formDirty,
3848
+ flagUnsavedForm() {
3849
+ form?.showError("Save this base or cancel it before you choose Done.");
3850
+ deps.announce("Save this base or cancel it before you choose Done.");
3851
+ focusFk("form:save");
3852
+ },
3853
+ focus() {
3854
+ if (form) {
3855
+ form.focusFirst();
3856
+ } else {
3857
+ focusFk("docs:add");
3858
+ }
3859
+ },
3860
+ count: () => loaded ? entries.length : null,
3861
+ dispose() {
3862
+ disposed = true;
3863
+ form = null;
3864
+ }
3865
+ };
3866
+ }
3867
+
3090
3868
  // src/ai-setup-wizard.ts
3091
3869
  var AI_WIZARD_STEPS = 3;
3092
3870
  function wizardStepNumber(step) {
@@ -3787,7 +4565,7 @@ function mountAiEnginesPage(container, ctx, opts) {
3787
4565
  let hardware = knownHardware(ai);
3788
4566
  let selection = initialSelection();
3789
4567
  let narrow = false;
3790
- let view = opts.initial?.engineId || opts.initial?.section === "routing" ? "detail" : "list";
4568
+ let view = opts.initial?.engineId || opts.initial?.section === "routing" || opts.initial?.section === "bases" && opts.docs ? "detail" : "list";
3791
4569
  let confirmDeleteId = null;
3792
4570
  let confirmDiscard = false;
3793
4571
  let finished = false;
@@ -3797,6 +4575,9 @@ function mountAiEnginesPage(container, ctx, opts) {
3797
4575
  if (opts.initial?.section === "routing") {
3798
4576
  return { kind: "routing" };
3799
4577
  }
4578
+ if (opts.initial?.section === "bases" && opts.docs) {
4579
+ return { kind: "bases" };
4580
+ }
3800
4581
  const wanted = opts.initial?.engineId && engines.find((e) => e.id === opts.initial?.engineId);
3801
4582
  if (wanted) {
3802
4583
  return { kind: "engine", id: wanted.id };
@@ -3880,6 +4661,15 @@ function mountAiEnginesPage(container, ctx, opts) {
3880
4661
  },
3881
4662
  statusList: () => session.statusList()
3882
4663
  });
4664
+ const docsSection = opts.docs ? createDocsBasesSection(ctx, {
4665
+ docs: opts.docs,
4666
+ getDraft: () => session.draft,
4667
+ setDraft: (next) => {
4668
+ session.draft = next;
4669
+ },
4670
+ announce,
4671
+ onCountChanged: () => refreshBasesEntry()
4672
+ }) : null;
3883
4673
  async function setKey(id, value) {
3884
4674
  const engine = getEngine(id);
3885
4675
  const error = await session.setKey(id, value);
@@ -3972,7 +4762,7 @@ function mountAiEnginesPage(container, ctx, opts) {
3972
4762
  }
3973
4763
  navEngines.append(navList);
3974
4764
  }
3975
- routingEntry.replaceChildren(buildRoutingEntry(), buildAddEntry());
4765
+ routingEntry.replaceChildren(buildRoutingEntry(), ...docsSection ? [buildBasesEntry()] : [], buildAddEntry());
3976
4766
  });
3977
4767
  }
3978
4768
  function buildNavItem(engine) {
@@ -4046,6 +4836,27 @@ function mountAiEnginesPage(container, ctx, opts) {
4046
4836
  sub.textContent = routingSubline();
4047
4837
  }
4048
4838
  }
4839
+ function basesSubline() {
4840
+ const count = docsSection?.count() ?? session.draft.docBases.length;
4841
+ return count === 0 ? "None yet" : `${count} ${count === 1 ? "base" : "bases"}`;
4842
+ }
4843
+ function buildBasesEntry() {
4844
+ const btn = navButton("nav:bases", selection.kind === "bases");
4845
+ btn.classList.add("ai-nav-btn--bases");
4846
+ const icon = el("span", "ai-nav-icon");
4847
+ icon.innerHTML = ICONS.book;
4848
+ const text = el("span", "ai-nav-text");
4849
+ text.append(el("span", "ai-nav-name", "Documentation bases"), el("span", "ai-nav-sub", basesSubline()));
4850
+ btn.append(icon, text);
4851
+ btn.addEventListener("click", () => select({ kind: "bases" }));
4852
+ return btn;
4853
+ }
4854
+ function refreshBasesEntry() {
4855
+ const sub = routingEntry.querySelector(".ai-nav-btn--bases .ai-nav-sub");
4856
+ if (sub) {
4857
+ sub.textContent = basesSubline();
4858
+ }
4859
+ }
4049
4860
  function buildAddEntry() {
4050
4861
  const btn = navButton("nav:add", selection.kind === "add");
4051
4862
  btn.classList.add("ai-nav-btn--add");
@@ -4119,6 +4930,11 @@ function mountAiEnginesPage(container, ctx, opts) {
4119
4930
  } else if (selection.kind === "routing") {
4120
4931
  routing.render();
4121
4932
  detailBody.append(routing.element);
4933
+ } else if (selection.kind === "bases") {
4934
+ if (docsSection) {
4935
+ detailBody.append(docsSection.element);
4936
+ docsSection.opened();
4937
+ }
4122
4938
  } else {
4123
4939
  detailBody.append(buildPresetCards());
4124
4940
  if (canDetect && !detectedOnce) {
@@ -4150,6 +4966,14 @@ function mountAiEnginesPage(container, ctx, opts) {
4150
4966
  detailHead.append(icon, main);
4151
4967
  return;
4152
4968
  }
4969
+ if (selection.kind === "bases") {
4970
+ const icon = el("span", "ai-detail-icon");
4971
+ icon.innerHTML = ICONS.book;
4972
+ const main = el("div", "ai-detail-main");
4973
+ main.append(detailTitle("Documentation bases"), el("div", "ai-detail-sub", "Folders of markdown the AI can answer from. A change to a base saves right away; nothing on disk is ever changed."));
4974
+ detailHead.append(icon, main);
4975
+ return;
4976
+ }
4153
4977
  if (selection.kind === "add") {
4154
4978
  const icon = el("span", "ai-detail-icon");
4155
4979
  icon.innerHTML = ICONS.plus;
@@ -4578,6 +5402,9 @@ function mountAiEnginesPage(container, ctx, opts) {
4578
5402
  wizard.back();
4579
5403
  return;
4580
5404
  }
5405
+ if (selection.kind === "bases" && docsSection?.back()) {
5406
+ return;
5407
+ }
4581
5408
  if (narrow && view === "detail") {
4582
5409
  view = "list";
4583
5410
  applyLayout();
@@ -4614,6 +5441,13 @@ function mountAiEnginesPage(container, ctx, opts) {
4614
5441
  requirePick(unpicked.id);
4615
5442
  return;
4616
5443
  }
5444
+ if (docsSection?.hasUnsavedForm()) {
5445
+ if (selection.kind !== "bases") {
5446
+ select({ kind: "bases" });
5447
+ }
5448
+ docsSection.flagUnsavedForm();
5449
+ return;
5450
+ }
4617
5451
  finished = true;
4618
5452
  const patch = session.collect();
4619
5453
  const saved = session.commit();
@@ -4655,7 +5489,7 @@ function mountAiEnginesPage(container, ctx, opts) {
4655
5489
  box.setAttribute("aria-describedby", textId);
4656
5490
  const head = el("div", "ai-page-confirm-title", "Discard changes?");
4657
5491
  head.id = headId;
4658
- const text = el("div", "ai-page-confirm-text", "Your changes to engines and tasks have not been saved. Engines added here and their keys are removed.");
5492
+ const text = el("div", "ai-page-confirm-text", docsSection ? "Your changes to engines, tasks and base options have not been saved. Engines added here and their keys are removed. Bases you saved stay saved." : "Your changes to engines and tasks have not been saved. Engines added here and their keys are removed.");
4659
5493
  text.id = textId;
4660
5494
  const keep = button("settings-btn", "Keep editing", { fk: "page:keep" });
4661
5495
  keep.addEventListener("click", () => hideDiscard());
@@ -4705,7 +5539,7 @@ function mountAiEnginesPage(container, ctx, opts) {
4705
5539
  applyLayout();
4706
5540
  }
4707
5541
  function focusSelectedEntry() {
4708
- const fk = selection.kind === "engine" ? `nav:eng:${selection.id}` : selection.kind === "routing" ? "nav:routing" : "nav:add";
5542
+ const fk = selection.kind === "engine" ? `nav:eng:${selection.id}` : selection.kind === "routing" ? "nav:routing" : selection.kind === "bases" ? "nav:bases" : "nav:add";
4709
5543
  focusKey(fk);
4710
5544
  }
4711
5545
  let resizeObserver = null;
@@ -4855,6 +5689,7 @@ function mountAiEnginesPage(container, ctx, opts) {
4855
5689
  unsubscribe?.();
4856
5690
  resizeObserver?.disconnect();
4857
5691
  form.dispose();
5692
+ docsSection?.dispose();
4858
5693
  container.replaceChildren();
4859
5694
  },
4860
5695
  focus() {
@@ -4866,6 +5701,8 @@ function mountAiEnginesPage(container, ctx, opts) {
4866
5701
  focusDetailTitle();
4867
5702
  } else if (!narrow && selection.kind === "routing") {
4868
5703
  focusKey("route:default");
5704
+ } else if (!narrow && selection.kind === "bases") {
5705
+ docsSection?.focus();
4869
5706
  } else if (!narrow && selection.kind === "add" && session.draft.engines.length === 0) {
4870
5707
  detailBody.querySelector(".ai-card")?.focus({ preventScroll: true });
4871
5708
  } else {
@@ -4905,6 +5742,7 @@ export {
4905
5742
  bestLocalServer,
4906
5743
  setUpLocalEngine,
4907
5744
  scanForSetup,
5745
+ createDocsBasesSection,
4908
5746
  mountAiEnginesPage
4909
5747
  };
4910
- //# sourceMappingURL=chunk-7A5HEPLK.js.map
5748
+ //# sourceMappingURL=chunk-DWXGWVA3.js.map