@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.
@@ -13,6 +13,7 @@ import {
13
13
  bestLocalServer,
14
14
  button,
15
15
  createAiSettingsSession,
16
+ createDocsBasesSection,
16
17
  detectLocalServers,
17
18
  el,
18
19
  engineDisplayName,
@@ -35,8 +36,8 @@ import {
35
36
  switchInput,
36
37
  taskRouteNote,
37
38
  withSavedAiKeys
38
- } from "./chunk-7A5HEPLK.js";
39
- import "./chunk-LXZG7IKN.js";
39
+ } from "./chunk-DWXGWVA3.js";
40
+ import "./chunk-N4N54LK2.js";
40
41
  import {
41
42
  AI_AUDIENCES,
42
43
  AI_ENGINE_PRESETS,
@@ -44,8 +45,15 @@ import {
44
45
  AI_NO_CONSENTS_TEXT,
45
46
  AI_REWRITE_VARIANTS,
46
47
  AI_TASKS,
48
+ ANSWERED_WITHOUT_SOURCES,
47
49
  CHARS_PER_TOKEN,
48
- DEFAULT_CONTEXT_TOKENS,
50
+ GHOST_DELAY_MS_DEFAULT,
51
+ GHOST_DELAY_MS_MAX,
52
+ GHOST_DELAY_MS_MIN,
53
+ GHOST_MAX_TOKENS_DEFAULT,
54
+ GHOST_MAX_TOKENS_MAX,
55
+ GHOST_MAX_TOKENS_MIN,
56
+ NO_SOURCES_CITED,
49
57
  PROPERTY_VOCABULARY_MAX,
50
58
  REWRITE_CONTEXT_CHARS,
51
59
  aiConsentHost,
@@ -58,28 +66,46 @@ import {
58
66
  aiResultStatsText,
59
67
  aiResultStatsTitle,
60
68
  budgetDocument,
69
+ buildContinueMessages,
61
70
  buildExplainMessages,
62
71
  buildPropertiesMessages,
63
72
  buildRewriteMessages,
64
73
  buildSummaryMessages,
65
74
  buildTitleMessages,
66
75
  checkMarkdownStructure,
76
+ continueMaxTokens,
67
77
  coveragePercent,
78
+ createDocsChat,
79
+ docsActiveBases,
80
+ docsCitationLabel,
81
+ docsDocumentLocation,
82
+ docsFileHref,
83
+ docsHeadingAt,
84
+ docsSectionFile,
85
+ docsSectionSlug,
68
86
  explainMaxTokens,
69
87
  finalizeRewriteText,
70
88
  finalizeSummary,
71
89
  getAiAudience,
90
+ getAiFeature,
72
91
  getAiRewriteVariant,
73
92
  getAiTask,
93
+ ghostAcceptWord,
94
+ ghostContext,
95
+ ghostPrefixMatch,
96
+ ghostTextOptions,
74
97
  headingsOutline,
98
+ insertUrlIsVerbatim,
75
99
  isHostEngineKind,
76
100
  isRouteUsable,
77
101
  jsonRepairMessages,
78
102
  makeAiError,
79
103
  nearestThinkingLevel,
80
104
  parseAiSettings,
105
+ parseCitations,
81
106
  parsePropertySuggestions,
82
107
  parseTitles,
108
+ prepareInsertMarkdown,
83
109
  propertiesMaxTokens,
84
110
  renderHeadingOutline,
85
111
  resolveAiFeature,
@@ -95,12 +121,15 @@ import {
95
121
  thinkingMaxTokens,
96
122
  titleMaxTokens,
97
123
  toAiError,
124
+ trimGhostSuggestionDetailed,
98
125
  whatIsSent,
99
126
  withThinkingLevel
100
- } from "./chunk-JZWKEBCI.js";
127
+ } from "./chunk-IWCXWUWW.js";
101
128
  import {
129
+ DEFAULT_CONTEXT_TOKENS,
102
130
  hostOfUrl
103
- } from "./chunk-QDVY7WW6.js";
131
+ } from "./chunk-APPBNQEY.js";
132
+ import "./chunk-TSP7BAV3.js";
104
133
 
105
134
  // src/ai-prefs.ts
106
135
  var LANGUAGES_KEY = "mikedown.ai.recentLanguages";
@@ -252,6 +281,23 @@ function buildAiSettingsSection(ctx, opts) {
252
281
  toolbarField.input.addEventListener("change", () => {
253
282
  session.draft = { ...session.draft, showToolbarButton: toolbarField.input.checked };
254
283
  });
284
+ const activityField = ctx.makeCheckboxRow(
285
+ "AI Activity",
286
+ "Shows exactly what each AI request sent and received. Kept in memory for this window only; turning it off clears it.",
287
+ session.draft.activityPanel
288
+ );
289
+ activityField.row.classList.add("ai-activity-setting");
290
+ activityField.input.dataset.fk = "ai-activity-panel";
291
+ const activityOpen = button("ai-link-btn ai-activity-open", "Open", { fk: "ai-activity:open", ariaLabel: "Open AI Activity" });
292
+ activityOpen.hidden = !session.draft.activityPanel;
293
+ activityOpen.addEventListener("click", () => opts.openActivityPanel?.());
294
+ if (opts.openActivityPanel) {
295
+ activityField.row.insertBefore(activityOpen, activityField.input);
296
+ }
297
+ activityField.input.addEventListener("change", () => {
298
+ session.draft = { ...session.draft, activityPanel: activityField.input.checked };
299
+ activityOpen.hidden = !activityField.input.checked;
300
+ });
255
301
  const cloudField = ctx.makeCheckboxRow(
256
302
  "Allow cloud engines",
257
303
  "Lets tasks use engines outside this network, such as Anthropic or OpenAI. Local engines never need it.",
@@ -342,6 +388,11 @@ function buildAiSettingsSection(ctx, opts) {
342
388
  }
343
389
  });
344
390
  }
391
+ const basesRow = ctx.makeRow("Documentation bases", "Folders of markdown the AI can answer from.", "inline");
392
+ basesRow.classList.add("ai-section");
393
+ const basesOpen = button("settings-btn settings-btn--secondary", "Manage bases\u2026", { fk: "manage-bases" });
394
+ basesOpen.addEventListener("click", () => openPage({ section: "bases" }));
395
+ basesRow.append(basesOpen);
345
396
  function renderEngines() {
346
397
  keepFocus(enginesBody, () => {
347
398
  enginesBody.replaceChildren();
@@ -413,7 +464,7 @@ function buildAiSettingsSection(ctx, opts) {
413
464
  }
414
465
  return line;
415
466
  }
416
- const featuresRow = ctx.makeRow("Features", "Turn each feature on or off.");
467
+ const featuresRow = ctx.makeRow("Features", "Turn each feature on or off. A feature that is off disappears from the menus, and its commands and shortcuts say so instead of running.");
417
468
  featuresRow.classList.add("ai-section");
418
469
  const featureRows = [];
419
470
  const taskRows = [];
@@ -428,6 +479,7 @@ function buildAiSettingsSection(ctx, opts) {
428
479
  lbl.id = labelId;
429
480
  text.append(lbl, el("span", "settings-desc", def.description));
430
481
  const controls = el("div", "ai-feature-controls");
482
+ let options = null;
431
483
  if (planned) {
432
484
  controls.append(el("span", "ai-pill ai-pill--soon", "Coming later"));
433
485
  row.setAttribute("aria-disabled", "true");
@@ -443,9 +495,15 @@ function buildAiSettingsSection(ctx, opts) {
443
495
  session.draft = { ...draft, features: { ...draft.features, [def.id]: { ...draft.features[def.id] ?? {}, enabled: toggle.checked } } };
444
496
  });
445
497
  controls.append(toggle);
498
+ if (def.id === "ghostText") {
499
+ options = buildGhostOptions(toggle, text);
500
+ }
446
501
  }
447
502
  row.append(text, controls);
448
503
  featureRows.push(row);
504
+ if (options) {
505
+ featureRows.push(options);
506
+ }
449
507
  }
450
508
  featuresRow.append(ctx.makeGroup(...featureRows));
451
509
  if (taskRows.length > 0) {
@@ -491,6 +549,105 @@ function buildAiSettingsSection(ctx, opts) {
491
549
  row.append(lbl, select);
492
550
  return row;
493
551
  }
552
+ function buildGhostOptions(enabled, rowText) {
553
+ const saved = ghostTextOptions(session.draft.features);
554
+ const patch = (change) => {
555
+ const draft = session.draft;
556
+ const entry = { ...draft.features.ghostText ?? {} };
557
+ change(entry);
558
+ session.draft = { ...draft, features: { ...draft.features, ghostText: entry } };
559
+ };
560
+ const numberRow = (label, hint, key, value, range) => {
561
+ const row = ctx.makeRow(label, hint, "inline");
562
+ const input = el("input", "settings-input settings-input--small settings-input--short");
563
+ input.type = "number";
564
+ input.min = String(range.min);
565
+ input.max = String(range.max);
566
+ input.step = String(range.step);
567
+ input.value = String(value);
568
+ input.dataset.fk = `feature:ghostText:${key}`;
569
+ input.setAttribute("aria-label", label);
570
+ input.addEventListener("change", () => {
571
+ const typed = Number(input.value);
572
+ if (input.value.trim() === "" || !Number.isFinite(typed)) {
573
+ patch((entry) => {
574
+ delete entry[key];
575
+ });
576
+ input.value = String(range.dflt);
577
+ return;
578
+ }
579
+ const next = Math.min(range.max, Math.max(range.min, Math.round(typed)));
580
+ patch((entry) => {
581
+ entry[key] = next;
582
+ });
583
+ input.value = String(next);
584
+ });
585
+ row.append(input);
586
+ return row;
587
+ };
588
+ const delay = numberRow("Delay (ms)", "How long you pause before a suggestion is requested.", "delayMs", saved.delayMs, { min: GHOST_DELAY_MS_MIN, max: GHOST_DELAY_MS_MAX, step: 50, dflt: GHOST_DELAY_MS_DEFAULT });
589
+ const tokens = numberRow("Max tokens", "The longest suggestion a model may write.", "maxTokens", saved.maxTokens, { min: GHOST_MAX_TOKENS_MIN, max: GHOST_MAX_TOKENS_MAX, step: 1, dflt: GHOST_MAX_TOKENS_DEFAULT });
590
+ const triggerRow = ctx.makeRow("Trigger", "Automatic suggests as you pause. Manual shortcut suggests only when you press the shortcut.", "inline");
591
+ const trigger = el("select", "settings-input settings-input--small");
592
+ trigger.dataset.fk = "feature:ghostText:trigger";
593
+ trigger.setAttribute("aria-label", "Trigger");
594
+ for (const [value, label] of [["auto", "Automatic"], ["manual", "Manual shortcut"]]) {
595
+ const o = el("option", "", label);
596
+ o.value = value;
597
+ trigger.append(o);
598
+ }
599
+ trigger.value = saved.trigger;
600
+ trigger.addEventListener("change", () => {
601
+ patch((entry) => {
602
+ entry.trigger = trigger.value === "manual" ? "manual" : "auto";
603
+ });
604
+ });
605
+ triggerRow.append(trigger);
606
+ const panelId = nextId("ai-ghost-options");
607
+ const toggle = button("ai-disclosure ai-ghost-options-toggle", "Options", { html: ICONS.chevron, fk: "feature:ghostText:options", ariaLabel: "Inline suggestions options" });
608
+ toggle.setAttribute("aria-expanded", "false");
609
+ toggle.setAttribute("aria-controls", panelId);
610
+ const panel = el("div", "ai-advanced ai-ghost-options");
611
+ panel.id = panelId;
612
+ panel.hidden = true;
613
+ toggle.addEventListener("click", () => {
614
+ panel.hidden = !panel.hidden;
615
+ toggle.setAttribute("aria-expanded", String(!panel.hidden));
616
+ });
617
+ const group = ctx.makeGroup(delay, tokens, triggerRow);
618
+ panel.append(group);
619
+ let line = null;
620
+ let localOnly = null;
621
+ const syncVisible = () => {
622
+ box.hidden = !enabled.checked;
623
+ if (line) {
624
+ line.hidden = !enabled.checked || !!localOnly?.checked;
625
+ }
626
+ };
627
+ if (tier === 2) {
628
+ const localRow = ctx.makeRow("Local engines only", "Never sends suggestions to a cloud engine, even when one is set for this task.", "inline");
629
+ localOnly = switchInput(saved.localOnly, "Local engines only", "feature:ghostText:localOnly");
630
+ const onlyLocal = localOnly;
631
+ onlyLocal.addEventListener("change", () => {
632
+ patch((entry) => {
633
+ entry.localOnly = onlyLocal.checked;
634
+ });
635
+ syncVisible();
636
+ });
637
+ localRow.append(onlyLocal);
638
+ group.append(localRow);
639
+ line = privacyLine(() => openPage());
640
+ if (!canManage) {
641
+ line.querySelector("button")?.remove();
642
+ }
643
+ rowText.append(line);
644
+ }
645
+ const box = el("div", "settings-row ai-ghost-options-box");
646
+ box.append(toggle, panel);
647
+ enabled.addEventListener("change", syncVisible);
648
+ syncVisible();
649
+ return box;
650
+ }
494
651
  function buildTaskDisclosure(taskRowsIn) {
495
652
  const panelId = nextId("ai-feature-tasks");
496
653
  const toggle = button("ai-disclosure ai-feature-tasks-toggle", "Advanced: task per feature", { html: ICONS.chevron, fk: "features:advanced" });
@@ -562,6 +719,9 @@ function buildAiSettingsSection(ctx, opts) {
562
719
  if (canManage) {
563
720
  rows.push(enginesRow);
564
721
  renderEngines();
722
+ if (opts.docsAvailable) {
723
+ rows.push(basesRow);
724
+ }
565
725
  }
566
726
  rows.push(cloudField.row);
567
727
  if (listConsents && forgetConsent) {
@@ -569,7 +729,7 @@ function buildAiSettingsSection(ctx, opts) {
569
729
  void refreshConsents();
570
730
  }
571
731
  }
572
- rows.push(toolbarField.row, featuresRow);
732
+ rows.push(toolbarField.row, activityField.row, featuresRow);
573
733
  if (tier === 2) {
574
734
  rows.push(localNetsRow);
575
735
  }
@@ -666,7 +826,7 @@ function aiToolbarMenuEntries(input) {
666
826
  entries.push({ type: "separator" });
667
827
  }
668
828
  };
669
- const shared = sharedDisabledReason([input.rewrite, input.explain, input.translate, input.summarize, input.titles, input.properties]);
829
+ const shared = sharedDisabledReason([input.rewrite, input.explain, input.translate, input.continuation, input.summarize, input.titles, input.properties, input.chat]);
670
830
  if (shared) {
671
831
  const { label, detail } = aiMenuStatusLine(shared);
672
832
  entries.push({ type: "status", id: "ai-status", label, ...detail ? { detail } : {} });
@@ -684,7 +844,7 @@ function aiToolbarMenuEntries(input) {
684
844
  id,
685
845
  label,
686
846
  icon: icon2 ?? input.icons.rewrite,
687
- hint: state.enabled || state.hint === shared ? void 0 : state.hint,
847
+ hint: state.hint === shared ? void 0 : state.hint,
688
848
  isDisabled: () => !state.enabled,
689
849
  action: run
690
850
  });
@@ -692,15 +852,39 @@ function aiToolbarMenuEntries(input) {
692
852
  add("ai-rewrite", "Rewrite selection", input.icons.rewrite, input.rewrite, () => input.onRewrite());
693
853
  add("ai-explain", "Explain selection", input.icons.explain, input.explain, () => input.onExplain?.());
694
854
  add("ai-translate", "Translate\u2026", input.icons.translate, input.translate, () => input.onTranslate?.());
855
+ add("ai-continuation", "Suggest continuation", input.icons.continuation, input.continuation, () => input.onSuggestContinuation?.());
856
+ if (input.inlineSuggestions) {
857
+ const { on } = input.inlineSuggestions;
858
+ entries.push({
859
+ type: "action",
860
+ id: "ai-inline-suggestions",
861
+ label: "Inline suggestions",
862
+ icon: input.icons.continuation ?? input.icons.rewrite,
863
+ hint: on ? "On" : "Off",
864
+ isActive: () => on,
865
+ action: () => input.onToggleInlineSuggestions?.(!on)
866
+ });
867
+ }
695
868
  separate();
696
869
  add("ai-summarize", "Summarize document\u2026", input.icons.summarize, input.summarize, () => input.onSummarize?.());
697
870
  add("ai-titles", "Suggest titles\u2026", input.icons.titles, input.titles, () => input.onSuggestTitles?.());
698
871
  add("ai-properties", "Suggest properties", input.icons.properties, input.properties, () => input.onSuggestProperties?.());
872
+ separate();
873
+ add("ai-chat", "Chat\u2026", input.icons.chat, input.chat, () => input.onChat?.());
699
874
  if (input.thinking && input.thinking.choices.length > 0) {
700
875
  separate();
701
876
  entries.push(thinkingRowEntry(input.thinking));
702
877
  }
703
878
  separate();
879
+ if (input.onActivity) {
880
+ entries.push({
881
+ type: "action",
882
+ id: "ai-activity",
883
+ label: "AI Activity\u2026",
884
+ icon: input.icons.activity ?? input.icons.manage,
885
+ action: () => input.onActivity?.()
886
+ });
887
+ }
704
888
  entries.push({
705
889
  type: "action",
706
890
  id: "ai-manage",
@@ -919,6 +1103,87 @@ function position(panel, anchor) {
919
1103
  panel.style.top = `${Math.max(8, Math.min(top, vh - p.height - 8))}px`;
920
1104
  }
921
1105
 
1106
+ // src/ai-activity.ts
1107
+ var AI_ACTIVITY_LIMIT = 50;
1108
+ function aiActivityStatus(entry) {
1109
+ if (entry.error) {
1110
+ return entry.error.code === "cancelled" ? "cancelled" : "error";
1111
+ }
1112
+ if (entry.endedAt === null) {
1113
+ return "streaming";
1114
+ }
1115
+ return entry.route?.viaFallback ? "fallback" : "ok";
1116
+ }
1117
+ function createAiActivityLog() {
1118
+ let enabled = false;
1119
+ let list = [];
1120
+ const listeners = /* @__PURE__ */ new Set();
1121
+ const notify = () => {
1122
+ for (const fn of [...listeners]) {
1123
+ try {
1124
+ fn();
1125
+ } catch {
1126
+ }
1127
+ }
1128
+ };
1129
+ return {
1130
+ get enabled() {
1131
+ return enabled;
1132
+ },
1133
+ setEnabled(on) {
1134
+ if (enabled === on) {
1135
+ return;
1136
+ }
1137
+ enabled = on;
1138
+ if (!on && list.length > 0) {
1139
+ list = [];
1140
+ notify();
1141
+ }
1142
+ },
1143
+ entries: () => [...list],
1144
+ get: (id) => list.find((e) => e.id === id),
1145
+ record(entry) {
1146
+ if (!enabled) {
1147
+ return;
1148
+ }
1149
+ list = [...list, entry].slice(-AI_ACTIVITY_LIMIT);
1150
+ notify();
1151
+ },
1152
+ update(id, patch) {
1153
+ const i = list.findIndex((e) => e.id === id);
1154
+ if (i < 0) {
1155
+ return;
1156
+ }
1157
+ const current = list[i];
1158
+ const next = { ...current, ...typeof patch === "function" ? patch(current) : patch, id };
1159
+ list = [...list.slice(0, i), next, ...list.slice(i + 1)];
1160
+ notify();
1161
+ },
1162
+ clear() {
1163
+ if (list.length === 0) {
1164
+ return;
1165
+ }
1166
+ list = [];
1167
+ notify();
1168
+ },
1169
+ subscribe(listener) {
1170
+ listeners.add(listener);
1171
+ return () => {
1172
+ listeners.delete(listener);
1173
+ };
1174
+ }
1175
+ };
1176
+ }
1177
+ function aiActivityResponse(result) {
1178
+ return {
1179
+ text: result.text,
1180
+ ...result.usage ? { usage: result.usage } : {},
1181
+ ...result.finishReason ? { finishReason: result.finishReason } : {},
1182
+ ...result.timing ? { timing: result.timing } : {},
1183
+ served: result.served
1184
+ };
1185
+ }
1186
+
922
1187
  // src/ai-request.ts
923
1188
  function aiRunThinkingOn(thinking) {
924
1189
  return !!thinking && (thinking.sent ?? thinking.configured) !== "off";
@@ -983,6 +1248,39 @@ function nextRequestId(feature) {
983
1248
  requestCounter += 1;
984
1249
  return `ai-${feature}-${Date.now().toString(36)}-${requestCounter}`;
985
1250
  }
1251
+ var turnCounter = 0;
1252
+ function nextTurnId() {
1253
+ turnCounter += 1;
1254
+ return `turn-${Date.now().toString(36)}-${turnCounter}`;
1255
+ }
1256
+ function askConsent(ai, engineId, about, turnId) {
1257
+ if (turnId && ai.requestTurnConsent) {
1258
+ return ai.requestTurnConsent(engineId, { ...about, turnId });
1259
+ }
1260
+ return ai.requestCloudConsent ? ai.requestCloudConsent(engineId, about) : true;
1261
+ }
1262
+ function endAiTurn(ctx, turnId) {
1263
+ try {
1264
+ ctx.ai.endTurn?.(turnId);
1265
+ } catch {
1266
+ }
1267
+ }
1268
+ function activityRoute(settings, route) {
1269
+ const engine = settings.engines.find((e) => e.id === route.engineId);
1270
+ return { engineId: route.engineId, modelId: route.modelId, label: engine?.name ?? route.engineId, isLocal: route.isLocal, viaFallback: route.viaFallback };
1271
+ }
1272
+ function openActivity(ctx, entry) {
1273
+ try {
1274
+ ctx.activity?.record(entry);
1275
+ } catch {
1276
+ }
1277
+ }
1278
+ function patchActivity(ctx, id, patch) {
1279
+ try {
1280
+ ctx.activity?.update(id, patch);
1281
+ } catch {
1282
+ }
1283
+ }
986
1284
  function routeUnusableError(r) {
987
1285
  const map = {
988
1286
  cloudBlocked: "cloudBlocked",
@@ -1009,11 +1307,83 @@ function withFailures(statuses, failed) {
1009
1307
  }
1010
1308
  return out;
1011
1309
  }
1310
+ function pinnedSettings(settings, req, pin) {
1311
+ const task = req.task ?? resolveAiFeature(settings.features, req.feature).task;
1312
+ return {
1313
+ ...settings,
1314
+ routing: {
1315
+ ...settings.routing,
1316
+ fallback: "none",
1317
+ tasks: { ...settings.routing.tasks, [task]: { engineId: pin.engineId, modelId: pin.modelId } }
1318
+ }
1319
+ };
1320
+ }
1321
+ async function resolveAiTurnRoute(ctx, req, signal) {
1322
+ if (!isTier2(ctx.ai)) {
1323
+ return { blocked: req.allowed ? !req.allowed(null) : false, route: null, pin: null };
1324
+ }
1325
+ const refused = {};
1326
+ const once = {};
1327
+ const turnId = nextTurnId();
1328
+ try {
1329
+ for (let pass = 0; pass < 12; pass++) {
1330
+ if (signal.aborted) {
1331
+ throw makeAiError("cancelled");
1332
+ }
1333
+ const consent = { ...ctx.consent, ...once, ...refused };
1334
+ const resolved = resolveRoute(ctx.settings(), ctx.statuses(), consent, { feature: req.feature, ...req.task ? { task: req.task } : {} });
1335
+ if (!isRouteUsable(resolved)) {
1336
+ throw routeUnusableError(resolved);
1337
+ }
1338
+ if (req.allowed && !req.allowed(resolved)) {
1339
+ endAiTurn(ctx, turnId);
1340
+ return { blocked: true, route: resolved, pin: null };
1341
+ }
1342
+ if (!resolved.needsConsent) {
1343
+ return {
1344
+ blocked: false,
1345
+ route: resolved,
1346
+ pin: {
1347
+ engineId: resolved.engineId,
1348
+ modelId: resolved.modelId,
1349
+ viaFallback: resolved.viaFallback,
1350
+ isLocal: resolved.isLocal,
1351
+ turnId,
1352
+ ...resolved.reason ? { reason: resolved.reason } : {},
1353
+ ...once[resolved.engineId] ? { consented: true } : {}
1354
+ }
1355
+ };
1356
+ }
1357
+ const reply = await askConsent(ctx.ai, resolved.engineId, { feature: req.feature, ...req.sends ? { sends: req.sends } : {}, ...req.baseIds && req.baseIds.length > 0 ? { baseIds: req.baseIds } : {} }, turnId);
1358
+ if (signal.aborted) {
1359
+ throw makeAiError("cancelled");
1360
+ }
1361
+ if (reply === true) {
1362
+ ctx.consent[resolved.engineId] = true;
1363
+ } else if (reply === "once") {
1364
+ once[resolved.engineId] = true;
1365
+ } else {
1366
+ refused[resolved.engineId] = false;
1367
+ if (reply === "never") {
1368
+ ctx.consent[resolved.engineId] = false;
1369
+ }
1370
+ }
1371
+ }
1372
+ throw makeAiError("unknown", "No engine answered.");
1373
+ } catch (err) {
1374
+ endAiTurn(ctx, turnId);
1375
+ throw err;
1376
+ }
1377
+ }
1012
1378
  async function runAiCompletion(ctx, req, events, signal) {
1013
1379
  const tier2 = isTier2(ctx.ai);
1014
1380
  const failed = /* @__PURE__ */ new Map();
1015
1381
  const refused = {};
1016
1382
  const once = {};
1383
+ const pin = tier2 ? req.pin : void 0;
1384
+ if (pin?.consented) {
1385
+ once[pin.engineId] = true;
1386
+ }
1017
1387
  let retried = false;
1018
1388
  for (let pass = 0; pass < 12; pass++) {
1019
1389
  if (signal.aborted) {
@@ -1022,12 +1392,19 @@ async function runAiCompletion(ctx, req, events, signal) {
1022
1392
  let route = null;
1023
1393
  if (tier2) {
1024
1394
  const consent = { ...ctx.consent, ...once, ...refused };
1025
- const resolved = resolveRoute(ctx.settings(), withFailures(ctx.statuses(), failed), consent, { feature: req.feature, ...req.task ? { task: req.task } : {} });
1395
+ const settings = pin ? pinnedSettings(ctx.settings(), req, pin) : ctx.settings();
1396
+ const resolved = resolveRoute(settings, withFailures(ctx.statuses(), failed), consent, { feature: req.feature, ...req.task ? { task: req.task } : {}, ...req.localOnly ? { localOnly: true } : {} });
1026
1397
  if (!isRouteUsable(resolved)) {
1027
1398
  throw routeUnusableError(resolved);
1028
1399
  }
1400
+ if (pin && pin.isLocal !== void 0 && resolved.isLocal !== pin.isLocal) {
1401
+ const name = ctx.settings().engines.find((e) => e.id === pin.engineId)?.name || pin.engineId;
1402
+ const moved = { ...makeAiError("unknown", `${name} moved between local and cloud during this turn. Ask again.`), engineId: pin.engineId };
1403
+ throw moved;
1404
+ }
1029
1405
  if (resolved.needsConsent) {
1030
- const reply = ctx.ai.requestCloudConsent ? await ctx.ai.requestCloudConsent(resolved.engineId, { feature: req.feature, ...req.sends ? { sends: req.sends } : {} }) : true;
1406
+ const reply = await askConsent(ctx.ai, resolved.engineId, { feature: req.feature, ...req.sends ? { sends: req.sends } : {}, ...req.baseIds && req.baseIds.length > 0 ? { baseIds: req.baseIds } : {} }, pin?.turnId);
1407
+ events.onConsent?.(reply);
1031
1408
  if (signal.aborted) {
1032
1409
  throw makeAiError("cancelled");
1033
1410
  }
@@ -1035,6 +1412,9 @@ async function runAiCompletion(ctx, req, events, signal) {
1035
1412
  ctx.consent[resolved.engineId] = true;
1036
1413
  } else if (reply === "once") {
1037
1414
  once[resolved.engineId] = true;
1415
+ if (pin) {
1416
+ pin.consented = true;
1417
+ }
1038
1418
  } else {
1039
1419
  refused[resolved.engineId] = false;
1040
1420
  if (reply === "never") {
@@ -1043,36 +1423,62 @@ async function runAiCompletion(ctx, req, events, signal) {
1043
1423
  }
1044
1424
  continue;
1045
1425
  }
1046
- route = resolved;
1426
+ route = pin ? { ...resolved, viaFallback: pin.viaFallback ?? false, ...pin.reason ? { reason: pin.reason } : {} } : resolved;
1047
1427
  }
1048
1428
  const thinking = route ? routeThinking(ctx.settings(), route, req) : null;
1049
1429
  const maxTokens = requestMaxTokens(req, thinking);
1050
1430
  events.onRoute?.(route, thinking?.info ?? null);
1051
1431
  let text = "";
1052
1432
  let gotToken = false;
1433
+ const request = {
1434
+ requestId: nextRequestId(req.feature),
1435
+ feature: req.feature,
1436
+ task: route?.task ?? req.task ?? resolveAiFeature(ctx.settings().features, req.feature).task,
1437
+ ...route ? { route: { engineId: route.engineId, modelId: route.modelId } } : {},
1438
+ messages: req.messages,
1439
+ ...req.format ? { format: req.format } : {},
1440
+ ...maxTokens !== void 0 ? { maxTokens } : {},
1441
+ ...req.temperature !== void 0 ? { temperature: req.temperature } : {},
1442
+ ...thinking?.overrides ? { requestOverrides: thinking.overrides } : {},
1443
+ ...pin?.turnId && route ? { turn: { id: pin.turnId, ...req.sends ? { sends: req.sends } : {}, ...req.baseIds && req.baseIds.length > 0 ? { baseIds: req.baseIds } : {}, isLocal: route.isLocal } } : {}
1444
+ };
1445
+ const entryId = request.requestId;
1446
+ openActivity(ctx, {
1447
+ id: entryId,
1448
+ startedAt: Date.now(),
1449
+ endedAt: null,
1450
+ feature: request.feature,
1451
+ ...req.job ? { job: req.job } : {},
1452
+ ...req.docs ? { docs: req.docs } : {},
1453
+ task: request.task,
1454
+ route: route ? activityRoute(ctx.settings(), route) : null,
1455
+ thinking: thinking?.info ?? null,
1456
+ request,
1457
+ response: { text: "" },
1458
+ error: null,
1459
+ attempt: retried ? 2 : 1
1460
+ });
1053
1461
  try {
1054
- const result = await ctx.ai.complete({
1055
- requestId: nextRequestId(req.feature),
1056
- feature: req.feature,
1057
- task: route?.task ?? req.task ?? resolveAiFeature(ctx.settings().features, req.feature).task,
1058
- ...route ? { route: { engineId: route.engineId, modelId: route.modelId } } : {},
1059
- messages: req.messages,
1060
- ...req.format ? { format: req.format } : {},
1061
- ...maxTokens !== void 0 ? { maxTokens } : {},
1062
- ...req.temperature !== void 0 ? { temperature: req.temperature } : {},
1063
- ...thinking?.overrides ? { requestOverrides: thinking.overrides } : {}
1064
- }, (delta) => {
1462
+ const result = await ctx.ai.complete(request, (delta) => {
1065
1463
  if (signal.aborted) {
1066
1464
  return;
1067
1465
  }
1068
1466
  gotToken = true;
1069
1467
  text += delta;
1468
+ patchActivity(ctx, entryId, { response: { text } });
1070
1469
  events.onToken?.(delta, text);
1071
1470
  }, signal);
1072
- return { result, route, viaFallback: route?.viaFallback ?? false, thinking: thinking?.info ?? null };
1471
+ patchActivity(ctx, entryId, { endedAt: Date.now(), response: aiActivityResponse(result) });
1472
+ return { result, route, viaFallback: route?.viaFallback ?? false, thinking: thinking?.info ?? null, requestId: entryId };
1073
1473
  } catch (err) {
1074
1474
  const e = signal.aborted ? makeAiError("cancelled") : toAiError(err);
1075
- if (e.code === "unreachable" && !gotToken && !retried && route) {
1475
+ const retrying = e.code === "unreachable" && !gotToken && !retried && !!route && !pin;
1476
+ patchActivity(ctx, entryId, {
1477
+ endedAt: Date.now(),
1478
+ error: route && !e.engineId && e.code !== "cancelled" ? { ...e, engineId: route.engineId } : e,
1479
+ ...retrying ? { retried: true } : {}
1480
+ });
1481
+ if (retrying && route) {
1076
1482
  retried = true;
1077
1483
  failed.set(route.engineId, e.message);
1078
1484
  continue;
@@ -1082,6 +1488,19 @@ async function runAiCompletion(ctx, req, events, signal) {
1082
1488
  }
1083
1489
  throw makeAiError("unknown", "No engine answered.");
1084
1490
  }
1491
+ function aiFeatureOffNotice(ai, settings, feature) {
1492
+ if (!ai) {
1493
+ return null;
1494
+ }
1495
+ if (!settings.enabled) {
1496
+ return "AI is turned off. Turn it on in the AI settings.";
1497
+ }
1498
+ const resolved = resolveAiFeature(settings.features, feature);
1499
+ if (resolved.enabled) {
1500
+ return null;
1501
+ }
1502
+ return `${resolved.definition?.label ?? feature} is turned off in the AI settings.`;
1503
+ }
1085
1504
  function aiFeatureAvailability(ai, settings, statuses, consent, feature) {
1086
1505
  if (!ai || !settings.enabled) {
1087
1506
  return { state: "hidden" };
@@ -1102,112 +1521,1725 @@ function aiFeatureAvailability(ai, settings, statuses, consent, feature) {
1102
1521
  return isRouteUsable(r) ? { state: "enabled" } : { state: "disabled", reason: r.reason };
1103
1522
  }
1104
1523
 
1105
- // src/ai-rewrite.ts
1106
- import { elementFromString, posToDOMRect } from "@tiptap/core";
1107
- import { TextSelection } from "@tiptap/pm/state";
1108
- import { DOMParser as PmDOMParser, Fragment } from "@tiptap/pm/model";
1109
- import { closeHistory } from "@tiptap/pm/history";
1110
- import { EditorView as CmEditorView } from "@codemirror/view";
1111
- import { isolateHistory } from "@codemirror/commands";
1112
-
1113
- // src/ai-markdown-preview.ts
1114
- var FENCE = /^ {0,3}(`{3,}|~{3,})/;
1115
- var HEADING = /^ {0,3}(#{1,6})[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/;
1116
- var ITEM = /^([ \t]*)([-*+]|\d{1,9}[.)])[ \t]+(.*)$/;
1117
- var QUOTE = /^ {0,3}>[ \t]?(.*)$/;
1118
- var RULE = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/;
1119
- var isSpace = (c) => c === void 0 || /\s/.test(c);
1120
- var isWord = (c) => c !== void 0 && /[\p{L}\p{N}]/u.test(c);
1121
- function make(tag, className) {
1122
- const node = document.createElement(tag);
1123
- if (className) {
1124
- node.className = className;
1524
+ // src/ai-activity-panel.ts
1525
+ var AI_ACTIVITY_BANNER = "This record stays in memory: nothing here is stored or sent anywhere, and turning AI Activity off clears it.";
1526
+ var AI_ACTIVITY_EMPTY = {
1527
+ title: "No AI requests yet",
1528
+ text: "Each request an AI feature makes from this window shows up here, with the exact messages sent and the raw response that came back."
1529
+ };
1530
+ var AI_ACTIVITY_OFF = {
1531
+ title: "AI Activity is off",
1532
+ text: "Requests are not recorded, and earlier ones were cleared. To record them, turn on AI Activity in the AI tab of Settings and save."
1533
+ };
1534
+ var RENDER_THROTTLE_MS = 100;
1535
+ var CONFIRM_MS = 1600;
1536
+ var NARROW_PX = 720;
1537
+ var STATUS_LABELS = {
1538
+ streaming: "Streaming",
1539
+ ok: "OK",
1540
+ error: "Failed",
1541
+ cancelled: "Cancelled",
1542
+ fallback: "Fallback"
1543
+ };
1544
+ var pad = (n) => String(n).padStart(2, "0");
1545
+ function activityExportFileName(now = /* @__PURE__ */ new Date()) {
1546
+ const day = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
1547
+ return `mikedown-ai-activity-${day}-${pad(now.getHours())}${pad(now.getMinutes())}.txt`;
1548
+ }
1549
+ function clockTime(ms) {
1550
+ const d = new Date(ms);
1551
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
1552
+ }
1553
+ function formatDuration(ms) {
1554
+ if (ms < 1e3) {
1555
+ return `${Math.round(ms)} ms`;
1125
1556
  }
1126
- return node;
1557
+ if (ms < 6e4) {
1558
+ return `${(ms / 1e3).toFixed(1)} s`;
1559
+ }
1560
+ const seconds = Math.round(ms / 1e3);
1561
+ return `${Math.floor(seconds / 60)} min ${seconds % 60} s`;
1127
1562
  }
1128
- function findCloser(s, delim, from) {
1129
- for (let j = s.indexOf(delim, from); j !== -1; j = s.indexOf(delim, j + 1)) {
1130
- if (s[j - 1] === "\\" || isSpace(s[j - 1])) {
1131
- continue;
1132
- }
1133
- if (delim.length === 1 && (s[j + 1] === delim || s[j - 1] === delim)) {
1134
- continue;
1135
- }
1136
- if (delim === "_" && isWord(s[j + 1])) {
1137
- continue;
1138
- }
1139
- return j;
1563
+ function entryDuration(entry) {
1564
+ return entry.endedAt === null ? null : entry.endedAt - entry.startedAt;
1565
+ }
1566
+ var JOB_LABELS = { explain: "Explain", translate: "Translate", navigate: "Chat: navigate", answer: "Chat: answer", ghostText: "Inline suggestions" };
1567
+ function featureLabel(entry) {
1568
+ return (entry.job ? JOB_LABELS[entry.job] : void 0) ?? getAiFeature(entry.feature)?.label ?? entry.feature;
1569
+ }
1570
+ function routeOf(entry) {
1571
+ if (entry.route) {
1572
+ return { engine: entry.route.label, model: entry.route.modelId, isLocal: entry.route.isLocal };
1140
1573
  }
1141
- return -1;
1574
+ const served = entry.response.served;
1575
+ if (served) {
1576
+ return { engine: served.engineName, model: served.modelId, isLocal: served.isLocal };
1577
+ }
1578
+ return { engine: null, model: null, isLocal: null };
1142
1579
  }
1143
- function linkEnd(s, close) {
1144
- if (s[close + 1] !== "(") {
1145
- return -1;
1580
+ var NO_ROUTE_LABEL = "Chosen by the app";
1581
+ function routeLabel2(entry) {
1582
+ const { engine, model } = routeOf(entry);
1583
+ if (!engine) {
1584
+ return NO_ROUTE_LABEL;
1146
1585
  }
1147
- let depth = 0;
1148
- for (let k = close + 1; k < s.length; k++) {
1149
- if (s[k] === "\n") {
1150
- return -1;
1151
- }
1152
- if (s[k] === "(") {
1153
- depth++;
1154
- } else if (s[k] === ")" && --depth === 0) {
1155
- return k + 1;
1156
- }
1586
+ return model ? `${engine} \xB7 ${model}` : engine;
1587
+ }
1588
+ function tokensLabel(entry) {
1589
+ const { promptTokens: promptTokens2, completionTokens } = entry.response.usage ?? {};
1590
+ const parts = [];
1591
+ if (promptTokens2 !== void 0) {
1592
+ parts.push(`${promptTokens2} in`);
1157
1593
  }
1158
- return -1;
1594
+ if (completionTokens !== void 0) {
1595
+ parts.push(`${completionTokens} out`);
1596
+ }
1597
+ return parts.join(" \xB7 ");
1159
1598
  }
1160
- function matchingBracket(s, open) {
1161
- let depth = 0;
1162
- for (let k = open; k < s.length; k++) {
1163
- if (s[k] === "\\") {
1164
- k++;
1165
- } else if (s[k] === "[") {
1166
- depth++;
1167
- } else if (s[k] === "]" && --depth === 0) {
1168
- return k;
1169
- }
1599
+ function thinkingLabel(thinking) {
1600
+ if (!thinking) {
1601
+ return null;
1170
1602
  }
1171
- return -1;
1603
+ const level = thinking.sent ?? thinking.configured;
1604
+ const name = level ? level.charAt(0).toUpperCase() + level.slice(1) : "Model default";
1605
+ return thinking.auto ? `${name} (this task's default)` : name;
1172
1606
  }
1173
- function appendInline(parent, s) {
1174
- let buf = "";
1175
- const flush = () => {
1176
- if (buf) {
1177
- parent.appendChild(document.createTextNode(buf));
1178
- buf = "";
1179
- }
1607
+ function messageText(message) {
1608
+ if (typeof message.content === "string") {
1609
+ return message.content;
1610
+ }
1611
+ return message.content.map((part) => part.type === "text" ? part.text : `[image: ${part.mime}, ${part.bytes.length} bytes]`).join("\n");
1612
+ }
1613
+ var plural = (n, word) => `${n.toLocaleString()} ${word}${n === 1 ? "" : "s"}`;
1614
+ function docsDetailRows(entry) {
1615
+ const { docs } = entry;
1616
+ if (!docs) {
1617
+ return [];
1618
+ }
1619
+ const ids = (list, count = list.length) => {
1620
+ const more = count - list.length;
1621
+ return more > 0 ? `${list.join(", ")}${list.length > 0 ? ", " : ""}and ${more} more` : list.join(", ");
1180
1622
  };
1181
- const wrap = (tag, inner, className) => {
1182
- flush();
1183
- const node = make(tag, className);
1184
- appendInline(node, inner);
1185
- parent.appendChild(node);
1623
+ const rows = [];
1624
+ if (docs.hop !== void 0) {
1625
+ rows.push({ label: "Hop", value: String(docs.hop) });
1626
+ }
1627
+ rows.push({ label: "Pages", value: docs.pages.length > 0 ? ids(docs.pages) : "None", raw: true });
1628
+ if (docs.droppedCount > 0) {
1629
+ rows.push({ label: "Dropped ids", value: ids(docs.droppedIds, docs.droppedCount), raw: true });
1630
+ }
1631
+ if (docs.leftOutIds && docs.leftOutIds.length > 0) {
1632
+ rows.push({ label: "Left out (key)", value: ids(docs.leftOutIds), raw: true });
1633
+ }
1634
+ if (docs.cutIds && docs.cutIds.length > 0) {
1635
+ rows.push({ label: "Did not fit", value: ids(docs.cutIds), raw: true });
1636
+ }
1637
+ return rows;
1638
+ }
1639
+ function exportEntry(entry, index, total) {
1640
+ const { request, response, error } = entry;
1641
+ const { engine, model, isLocal } = routeOf(entry);
1642
+ const duration = entryDuration(entry);
1643
+ const thinking = thinkingLabel(entry.thinking);
1644
+ const head = [
1645
+ ["Time", new Date(entry.startedAt).toISOString()],
1646
+ ["Feature", entry.feature],
1647
+ ["Job", entry.job && JOB_LABELS[entry.job] ? entry.job : null],
1648
+ ["Task", entry.task],
1649
+ ["Status", aiActivityStatus(entry)],
1650
+ ["Engine", engine ? `${engine}${isLocal === null ? "" : isLocal ? " (local)" : " (cloud)"}` : "chosen by the app"],
1651
+ ["Model", model],
1652
+ ["Via fallback", entry.route?.viaFallback ? "yes" : null],
1653
+ ["Attempt", entry.attempt],
1654
+ ...docsDetailRows(entry).map(({ label, value }) => [label, value]),
1655
+ ["Thinking", thinking],
1656
+ ["Duration", duration === null ? null : `${duration} ms`],
1657
+ ["Format", request.format],
1658
+ ["Max tokens", request.maxTokens],
1659
+ ["Temperature", request.temperature],
1660
+ ["Request overrides", request.requestOverrides ? JSON.stringify(request.requestOverrides) : null]
1661
+ ];
1662
+ const result = [
1663
+ ["Prompt tokens", response.usage?.promptTokens],
1664
+ ["Completion tokens", response.usage?.completionTokens],
1665
+ ["Finish reason", response.finishReason],
1666
+ ["Total time", response.timing ? `${response.timing.totalMs} ms` : null],
1667
+ ["First token", response.timing?.firstTokenMs !== void 0 ? `${response.timing.firstTokenMs} ms` : null],
1668
+ ["Served by", response.served ? `${response.served.engineName} \xB7 ${response.served.modelId} (${response.served.isLocal ? "local" : "cloud"})` : null],
1669
+ ["Error", error ? `${error.code}: ${error.message}` : null],
1670
+ ["Retried", entry.retried ? "yes, on another engine (the next request)" : null]
1671
+ ];
1672
+ const lines = (rows) => rows.filter(([, v]) => v !== null && v !== void 0).map(([k, v]) => `${k}: ${v}`);
1673
+ const metadata = lines(result);
1674
+ const blocks = [
1675
+ [`=== Request ${index + 1} of ${total} ===`, ...lines(head)].join("\n"),
1676
+ "--- Sent ---",
1677
+ ...request.messages.map((m) => `[${m.role}]
1678
+ ${messageText(m)}`),
1679
+ "--- Received ---",
1680
+ response.text,
1681
+ ...metadata.length > 0 ? [["--- Result ---", ...metadata].join("\n")] : []
1682
+ ];
1683
+ return blocks.join("\n\n");
1684
+ }
1685
+ function formatActivityExport(entries) {
1686
+ if (entries.length === 0) {
1687
+ return "";
1688
+ }
1689
+ return `${entries.map((entry, i) => exportEntry(entry, i, entries.length)).join("\n\n")}
1690
+ `;
1691
+ }
1692
+ function setText(node, text) {
1693
+ if (node.textContent !== text) {
1694
+ node.textContent = text;
1695
+ }
1696
+ }
1697
+ function setAttr(node, name, value) {
1698
+ if (node.getAttribute(name) !== value) {
1699
+ node.setAttribute(name, value);
1700
+ }
1701
+ }
1702
+ function localityPill(isLocal, extraClass = "") {
1703
+ const pill = el("span", `ai-pill ai-pill--${isLocal ? "local" : "cloud"}${extraClass ? ` ${extraClass}` : ""}`);
1704
+ pill.innerHTML = isLocal ? ICONS.local : ICONS.cloud;
1705
+ pill.append(el("span", "", isLocal ? "local" : "cloud"));
1706
+ return pill;
1707
+ }
1708
+ function actionButton(action, label) {
1709
+ const b = button("settings-btn ai-activity-action", label, { fk: `activity:${action}` });
1710
+ b.dataset.action = action;
1711
+ return b;
1712
+ }
1713
+ function metaList(rows) {
1714
+ const list = el("dl", "ai-activity-meta");
1715
+ for (const row of rows) {
1716
+ const value = el("dd", row.raw ? "is-raw" : "", row.value);
1717
+ value.dataset.meta = row.label;
1718
+ list.append(el("dt", "", row.label), value);
1719
+ }
1720
+ return list;
1721
+ }
1722
+ function inlineStatus() {
1723
+ const element = el("span", "ai-activity-inline-status");
1724
+ element.setAttribute("role", "status");
1725
+ let timer;
1726
+ const clear = () => {
1727
+ window.clearTimeout(timer);
1728
+ element.textContent = "";
1729
+ delete element.dataset.level;
1186
1730
  };
1187
- let i = 0;
1188
- while (i < s.length) {
1189
- const c = s[i];
1190
- if (c === "\\" && i + 1 < s.length && /[\\`*_{}[\]()#+\-.!~>|]/.test(s[i + 1])) {
1191
- buf += s[i + 1];
1192
- i += 2;
1193
- continue;
1731
+ return {
1732
+ element,
1733
+ clear,
1734
+ show(text, level, ms) {
1735
+ clear();
1736
+ element.textContent = text;
1737
+ element.dataset.level = level;
1738
+ if (ms) {
1739
+ timer = window.setTimeout(clear, ms);
1740
+ }
1194
1741
  }
1195
- if (c === "`") {
1196
- let n = 1;
1197
- while (s[i + n] === "`") {
1198
- n++;
1742
+ };
1743
+ }
1744
+ function mountAiActivityPanel(container, ctx, opts) {
1745
+ const { log } = opts;
1746
+ const canSave = typeof opts.saveTextFile === "function";
1747
+ let selectedId = opts.selectId !== void 0 && log.get(opts.selectId) ? opts.selectId : null;
1748
+ let detail = null;
1749
+ let mounted = false;
1750
+ let destroyed = false;
1751
+ let narrow = false;
1752
+ let view = selectedId ? "detail" : "list";
1753
+ let renderTimer;
1754
+ let lastRender = 0;
1755
+ let savingAll = false;
1756
+ const collapsed = { sent: false, received: false };
1757
+ const root = el("div", "ai-page ai-activity");
1758
+ root.tabIndex = -1;
1759
+ const header = el("div", "ai-page-header");
1760
+ const backBtn = button("ai-page-back", "Back", { html: ICONS.back, fk: "activity:back" });
1761
+ backBtn.addEventListener("click", () => back());
1762
+ const title = el("h2", "ai-page-title", "AI Activity");
1763
+ title.id = nextId("ai-activity-title");
1764
+ const titleLine = el("div", "ai-page-title-line");
1765
+ titleLine.append(title, experimentalBadge());
1766
+ const headerActions = el("div", "ai-activity-header-actions");
1767
+ const headerStatus = inlineStatus();
1768
+ const saveAllBtn = actionButton("save-all", "Save all");
1769
+ saveAllBtn.hidden = !canSave;
1770
+ saveAllBtn.addEventListener("click", () => {
1771
+ void saveAll();
1772
+ });
1773
+ const clearBtn = actionButton("clear", "Clear all");
1774
+ clearBtn.addEventListener("click", () => log.clear());
1775
+ headerActions.append(headerStatus.element, saveAllBtn, clearBtn);
1776
+ header.append(backBtn, titleLine, headerActions);
1777
+ const banner = el("p", "ai-activity-banner", AI_ACTIVITY_BANNER);
1778
+ const body = el("div", "ai-activity-body");
1779
+ const master = el("div", "ai-activity-master");
1780
+ const listHead = el("div", "ai-activity-list-head");
1781
+ const listId = nextId("ai-activity-list-label");
1782
+ const listLabel = el("span", "ai-activity-list-label", "Requests");
1783
+ listLabel.id = listId;
1784
+ const listCount = el("span", "ai-activity-list-count");
1785
+ listHead.append(listLabel, listCount);
1786
+ const list = el("div", "ai-activity-list");
1787
+ list.setAttribute("role", "listbox");
1788
+ list.setAttribute("aria-labelledby", listId);
1789
+ master.append(listHead, list);
1790
+ const detailPane = el("section", "ai-activity-detail");
1791
+ detailPane.tabIndex = 0;
1792
+ detailPane.setAttribute("aria-label", "Selected request");
1793
+ body.append(master, detailPane);
1794
+ const empty = el("div", "ai-activity-empty");
1795
+ const emptyTitle = el("div", "ai-activity-empty-title");
1796
+ const emptyText = el("p", "ai-activity-empty-text");
1797
+ empty.append(emptyTitle, emptyText);
1798
+ root.append(header, banner, body, empty);
1799
+ const rows = /* @__PURE__ */ new Map();
1800
+ function createRow(entry) {
1801
+ const row = el("div", "ai-activity-row");
1802
+ row.setAttribute("role", "option");
1803
+ row.dataset.entryId = entry.id;
1804
+ row.tabIndex = -1;
1805
+ const dot = el("span", "ai-activity-dot");
1806
+ dot.setAttribute("aria-hidden", "true");
1807
+ const head = el("span", "ai-activity-row-head");
1808
+ const feature = el("span", "ai-activity-row-feature");
1809
+ const statusWord = el("span", "ai-activity-row-status");
1810
+ head.append(feature, statusWord);
1811
+ const when = el("span", "ai-activity-row-when");
1812
+ const duration = el("span", "ai-activity-row-duration");
1813
+ const time = el("span", "ai-activity-row-time");
1814
+ when.append(duration, time);
1815
+ const sub = el("span", "ai-activity-row-sub");
1816
+ const engine = el("span", "ai-activity-row-engine");
1817
+ const locality = el("span", "ai-activity-row-locality");
1818
+ sub.append(engine, locality);
1819
+ const tokens = el("span", "ai-activity-row-tokens");
1820
+ row.append(dot, head, when, sub, tokens);
1821
+ const refs = { row, dot, feature, statusWord, time, engine, locality, duration, tokens, entry: null, shownLocal: null };
1822
+ updateRow(refs, entry);
1823
+ return refs;
1824
+ }
1825
+ function updateRow(refs, entry) {
1826
+ if (refs.entry === entry) {
1827
+ return;
1828
+ }
1829
+ refs.entry = entry;
1830
+ const status = aiActivityStatus(entry);
1831
+ const statusText = entry.retried ? "Failed, retried" : STATUS_LABELS[status];
1832
+ const route = routeLabel2(entry);
1833
+ const { isLocal } = routeOf(entry);
1834
+ const duration = entryDuration(entry);
1835
+ if (refs.row.dataset.status !== status) {
1836
+ refs.row.dataset.status = status;
1837
+ }
1838
+ setText(refs.feature, featureLabel(entry));
1839
+ setText(refs.statusWord, status === "ok" ? "" : statusText);
1840
+ setText(refs.time, clockTime(entry.startedAt));
1841
+ setText(refs.engine, route);
1842
+ setAttr(refs.engine, "title", route);
1843
+ if (refs.shownLocal !== isLocal) {
1844
+ refs.shownLocal = isLocal;
1845
+ refs.locality.replaceChildren(...isLocal === null ? [] : [localityPill(isLocal)]);
1846
+ }
1847
+ setText(refs.duration, duration === null ? "" : formatDuration(duration));
1848
+ setText(refs.tokens, tokensLabel(entry));
1849
+ setAttr(refs.row, "aria-label", `${featureLabel(entry)}, ${statusText}, ${route}, ${clockTime(entry.startedAt)}`);
1850
+ }
1851
+ function renderList(entries) {
1852
+ const live = new Set(entries.map((e) => e.id));
1853
+ for (const [id, refs] of rows) {
1854
+ if (!live.has(id)) {
1855
+ refs.row.remove();
1856
+ rows.delete(id);
1199
1857
  }
1200
- const run = "`".repeat(n);
1201
- const end = s.indexOf(run, i + n);
1202
- if (end !== -1 && s[end + n] !== "`") {
1203
- flush();
1204
- const code = make("code");
1205
- code.textContent = s.slice(i + n, end).replace(/^ (.*) $/, "$1");
1206
- parent.appendChild(code);
1207
- i = end + n;
1858
+ }
1859
+ const before = list.scrollTop;
1860
+ const added = [];
1861
+ let cursor = list.firstElementChild;
1862
+ for (let i = entries.length - 1; i >= 0; i--) {
1863
+ const entry = entries[i];
1864
+ let refs = rows.get(entry.id);
1865
+ if (!refs) {
1866
+ refs = createRow(entry);
1867
+ rows.set(entry.id, refs);
1868
+ list.insertBefore(refs.row, cursor);
1869
+ added.push(refs.row);
1208
1870
  continue;
1209
1871
  }
1210
- buf += run;
1872
+ updateRow(refs, entry);
1873
+ if (refs.row === cursor) {
1874
+ cursor = cursor.nextElementSibling;
1875
+ } else {
1876
+ list.insertBefore(refs.row, cursor);
1877
+ }
1878
+ }
1879
+ if (before > 0 && added.length > 0) {
1880
+ list.scrollTop = before + added.reduce((sum, row) => sum + row.offsetHeight, 0);
1881
+ }
1882
+ setText(listCount, entries.length === 0 ? "" : `${entries.length} of the last ${AI_ACTIVITY_LIMIT}`);
1883
+ }
1884
+ function markSelected() {
1885
+ for (const [id, refs] of rows) {
1886
+ const on = id === selectedId;
1887
+ setAttr(refs.row, "aria-selected", String(on));
1888
+ refs.row.tabIndex = on ? 0 : -1;
1889
+ }
1890
+ }
1891
+ function selectedRow() {
1892
+ return selectedId ? rows.get(selectedId)?.row ?? null : null;
1893
+ }
1894
+ function select(id, o = {}) {
1895
+ flush();
1896
+ if (!log.get(id)) {
1897
+ return;
1898
+ }
1899
+ selectedId = id;
1900
+ markSelected();
1901
+ renderDetail(log.get(id) ?? null);
1902
+ const row = selectedRow();
1903
+ if (row) {
1904
+ row.scrollIntoView?.({ block: "nearest" });
1905
+ if (o.focus) {
1906
+ row.focus({ preventScroll: true });
1907
+ }
1908
+ }
1909
+ }
1910
+ function openDetail(o) {
1911
+ if (!selectedId) {
1912
+ return;
1913
+ }
1914
+ if (narrow) {
1915
+ view = "detail";
1916
+ applyLayout();
1917
+ }
1918
+ if (o.focus || narrow) {
1919
+ detailPane.focus({ preventScroll: true });
1920
+ }
1921
+ }
1922
+ list.addEventListener("click", (e) => {
1923
+ const row = e.target.closest(".ai-activity-row");
1924
+ const id = row?.dataset.entryId;
1925
+ if (!id) {
1926
+ return;
1927
+ }
1928
+ select(id, { focus: true });
1929
+ openDetail({ focus: false });
1930
+ });
1931
+ list.addEventListener("keydown", (e) => {
1932
+ if (e.altKey || e.ctrlKey || e.metaKey) {
1933
+ return;
1934
+ }
1935
+ const ids = Array.from(list.children, (n) => n.dataset.entryId ?? "");
1936
+ if (ids.length === 0) {
1937
+ return;
1938
+ }
1939
+ const at = selectedId ? ids.indexOf(selectedId) : -1;
1940
+ let next;
1941
+ if (e.key === "ArrowDown") {
1942
+ next = ids[Math.min(at + 1, ids.length - 1)];
1943
+ } else if (e.key === "ArrowUp") {
1944
+ next = ids[Math.max(at - 1, 0)];
1945
+ } else if (e.key === "Home") {
1946
+ next = ids[0];
1947
+ } else if (e.key === "End") {
1948
+ next = ids[ids.length - 1];
1949
+ } else if (e.key === "Enter") {
1950
+ e.preventDefault();
1951
+ openDetail({ focus: true });
1952
+ return;
1953
+ } else {
1954
+ return;
1955
+ }
1956
+ e.preventDefault();
1957
+ if (next) {
1958
+ select(next, { focus: true });
1959
+ }
1960
+ });
1961
+ function buildSection(key, label) {
1962
+ const section = el("section", "ai-activity-section");
1963
+ section.dataset.section = key;
1964
+ const head = el("div", "ai-activity-section-head");
1965
+ const toggle = button("ai-disclosure ai-activity-section-toggle", label, { html: ICONS.chevron, fk: `activity:section:${key}` });
1966
+ const summary = el("span", "ai-activity-section-summary");
1967
+ head.append(toggle, summary);
1968
+ const content = el("div", "ai-activity-section-body");
1969
+ content.id = nextId(`ai-activity-${key}`);
1970
+ toggle.setAttribute("aria-controls", content.id);
1971
+ const apply = () => {
1972
+ content.hidden = collapsed[key];
1973
+ toggle.setAttribute("aria-expanded", String(!collapsed[key]));
1974
+ };
1975
+ apply();
1976
+ toggle.addEventListener("click", () => {
1977
+ collapsed[key] = !collapsed[key];
1978
+ apply();
1979
+ });
1980
+ section.append(head, content);
1981
+ return { section, summary, content };
1982
+ }
1983
+ function renderRoute(node, entry) {
1984
+ const { engine, model, isLocal } = routeOf(entry);
1985
+ const parts = [el("span", "ai-activity-route-engine", engine ?? NO_ROUTE_LABEL)];
1986
+ if (model) {
1987
+ parts.push(el("span", "ai-activity-route-model", model));
1988
+ }
1989
+ if (isLocal !== null) {
1990
+ parts.push(localityPill(isLocal));
1991
+ }
1992
+ node.replaceChildren(...parts);
1993
+ }
1994
+ function detailNotes(entry) {
1995
+ const out = [];
1996
+ if (entry.error) {
1997
+ out.push(note(entry.error.code === "cancelled" ? "info" : "error", entry.error.message));
1998
+ }
1999
+ if (entry.retried) {
2000
+ out.push(note("info", "This engine could not be reached, so the request went to another engine. That attempt is the next request in the list."));
2001
+ }
2002
+ if (!entry.error && entry.route?.viaFallback) {
2003
+ out.push(note("info", "The preferred engine was passed over, so a fallback engine handled this request."));
2004
+ }
2005
+ return out;
2006
+ }
2007
+ function requestRows(entry) {
2008
+ const { format, maxTokens, temperature, requestOverrides } = entry.request;
2009
+ const out = [];
2010
+ if (format) {
2011
+ out.push({ label: "Format", value: format });
2012
+ }
2013
+ if (maxTokens !== void 0) {
2014
+ out.push({ label: "Max tokens", value: maxTokens.toLocaleString() });
2015
+ }
2016
+ if (temperature !== void 0) {
2017
+ out.push({ label: "Temperature", value: String(temperature) });
2018
+ }
2019
+ if (requestOverrides) {
2020
+ out.push({ label: "Overrides", value: JSON.stringify(requestOverrides), raw: true });
2021
+ }
2022
+ return out;
2023
+ }
2024
+ function resultRows(entry) {
2025
+ if (entry.endedAt === null) {
2026
+ return [];
2027
+ }
2028
+ const { usage, finishReason, timing, served } = entry.response;
2029
+ const out = [];
2030
+ const usageParts = [];
2031
+ if (usage?.promptTokens !== void 0) {
2032
+ usageParts.push(`${usage.promptTokens.toLocaleString()} prompt`);
2033
+ }
2034
+ if (usage?.completionTokens !== void 0) {
2035
+ usageParts.push(`${usage.completionTokens.toLocaleString()} completion`);
2036
+ }
2037
+ if (usageParts.length > 0) {
2038
+ out.push({ label: "Usage", value: `${usageParts.join(" \xB7 ")} tokens` });
2039
+ }
2040
+ if (finishReason) {
2041
+ out.push({ label: "Finish reason", value: finishReason });
2042
+ }
2043
+ const total = timing?.totalMs ?? entryDuration(entry);
2044
+ if (total !== null) {
2045
+ const first = timing?.firstTokenMs;
2046
+ out.push({ label: "Timing", value: first === void 0 ? formatDuration(total) : `${formatDuration(total)} total \xB7 first token at ${formatDuration(first)}` });
2047
+ }
2048
+ if (served) {
2049
+ out.push({ label: "Served by", value: `${served.engineName} \xB7 ${served.modelId} (${served.isLocal ? "local" : "cloud"})` });
2050
+ }
2051
+ if (entry.error) {
2052
+ out.push({ label: "Error code", value: entry.error.code });
2053
+ }
2054
+ return out;
2055
+ }
2056
+ function receivedSummaryText(entry) {
2057
+ const size = plural(entry.response.text.length, "character");
2058
+ if (entry.endedAt === null) {
2059
+ return entry.response.text ? `Streaming \xB7 ${size}` : "Waiting for the first token";
2060
+ }
2061
+ return entry.response.text ? size : "Nothing received";
2062
+ }
2063
+ function buildDetail(entry) {
2064
+ const head = el("div", "ai-activity-detail-head");
2065
+ const titleRow = el("div", "ai-activity-detail-title-row");
2066
+ const detailTitle = el("h3", "ai-activity-detail-title", featureLabel(entry));
2067
+ const status = el("span", "ai-activity-status");
2068
+ const dot = el("span", "ai-activity-dot");
2069
+ dot.setAttribute("aria-hidden", "true");
2070
+ const statusWord = el("span", "ai-activity-status-word");
2071
+ status.append(dot, statusWord);
2072
+ const actions = el("div", "ai-activity-detail-actions");
2073
+ const copyStatus = inlineStatus();
2074
+ const copyBtn = actionButton("copy", "Copy");
2075
+ copyBtn.addEventListener("click", () => copySelected(copyStatus));
2076
+ actions.append(copyStatus.element, copyBtn);
2077
+ if (canSave) {
2078
+ const saveBtn = actionButton("save", "Save this request");
2079
+ saveBtn.addEventListener("click", () => {
2080
+ void saveSelected(saveBtn, copyStatus);
2081
+ });
2082
+ actions.append(saveBtn);
2083
+ }
2084
+ titleRow.append(detailTitle, status, actions);
2085
+ const route = el("div", "ai-activity-route");
2086
+ renderRoute(route, entry);
2087
+ const facts = [`Task: ${entry.task}`, `Started ${clockTime(entry.startedAt)}`];
2088
+ const thinking = thinkingLabel(entry.thinking);
2089
+ if (thinking) {
2090
+ facts.unshift(`Thinking: ${thinking}`);
2091
+ }
2092
+ if (entry.attempt === 2) {
2093
+ facts.push("Second attempt");
2094
+ }
2095
+ head.append(titleRow, route, el("div", "ai-activity-thinking", facts.join(" \xB7 ")));
2096
+ const notes = el("div", "ai-activity-notes");
2097
+ const docs = el("div", "ai-activity-docs");
2098
+ const sent = buildSection("sent", "Sent");
2099
+ const totalChars = entry.request.messages.reduce((sum, m) => sum + messageText(m).length, 0);
2100
+ sent.summary.textContent = `${plural(entry.request.messages.length, "message")} \xB7 ${plural(totalChars, "character")}`;
2101
+ for (const message of entry.request.messages) {
2102
+ const text = messageText(message);
2103
+ const block = el("div", "ai-activity-block ai-activity-message");
2104
+ block.dataset.role = message.role;
2105
+ const caption = el("div", "ai-activity-block-caption");
2106
+ caption.append(el("span", "ai-activity-message-role", message.role), el("span", "ai-activity-block-size", plural(text.length, "character")));
2107
+ block.append(caption, el("pre", "ai-activity-text ai-activity-message-text", text));
2108
+ sent.content.append(block);
2109
+ }
2110
+ const requestMeta = requestRows(entry);
2111
+ if (requestMeta.length > 0) {
2112
+ sent.content.append(metaList(requestMeta));
2113
+ }
2114
+ const received = buildSection("received", "Received");
2115
+ const receivedBlock = el("div", "ai-activity-block ai-activity-received");
2116
+ const receivedPre = el("pre", "ai-activity-text ai-activity-received-text");
2117
+ const receivedText = document.createTextNode("");
2118
+ receivedPre.append(receivedText);
2119
+ receivedBlock.append(receivedPre);
2120
+ const receivedMeta = el("div", "ai-activity-received-meta");
2121
+ received.content.append(receivedBlock, receivedMeta);
2122
+ detailPane.replaceChildren();
2123
+ const inner = el("div", "ai-activity-detail-inner");
2124
+ inner.append(head, notes, docs, sent.section, received.section);
2125
+ detailPane.append(inner);
2126
+ detailPane.scrollTop = 0;
2127
+ const refs = {
2128
+ entry,
2129
+ status,
2130
+ statusWord,
2131
+ route,
2132
+ notes,
2133
+ docs,
2134
+ receivedSummary: received.summary,
2135
+ receivedBlock,
2136
+ receivedText,
2137
+ receivedMeta,
2138
+ copyStatus
2139
+ };
2140
+ syncDetail(refs, entry, true);
2141
+ return refs;
2142
+ }
2143
+ function syncDetail(refs, entry, first = false) {
2144
+ const previous = refs.entry;
2145
+ refs.entry = entry;
2146
+ const text = entry.response.text;
2147
+ const current = refs.receivedText.data;
2148
+ if (text !== current) {
2149
+ if (text.startsWith(current)) {
2150
+ refs.receivedText.appendData(text.slice(current.length));
2151
+ } else {
2152
+ refs.receivedText.data = text;
2153
+ }
2154
+ }
2155
+ refs.receivedBlock.hidden = text === "";
2156
+ setText(refs.receivedSummary, receivedSummaryText(entry));
2157
+ if (first || previous.docs !== entry.docs) {
2158
+ const rows2 = docsDetailRows(entry);
2159
+ refs.docs.replaceChildren(...rows2.length > 0 ? [metaList(rows2)] : []);
2160
+ }
2161
+ const ended = first || previous.endedAt !== entry.endedAt || previous.error !== entry.error || previous.retried !== entry.retried || previous.response.served !== entry.response.served;
2162
+ if (!ended) {
2163
+ return;
2164
+ }
2165
+ const status = aiActivityStatus(entry);
2166
+ refs.status.dataset.status = status;
2167
+ setText(refs.statusWord, entry.retried ? "Failed, retried" : STATUS_LABELS[status]);
2168
+ renderRoute(refs.route, entry);
2169
+ refs.notes.replaceChildren(...detailNotes(entry));
2170
+ const meta = resultRows(entry);
2171
+ refs.receivedMeta.replaceChildren(...meta.length > 0 ? [metaList(meta)] : []);
2172
+ }
2173
+ function renderDetail(entry) {
2174
+ if (!entry) {
2175
+ detail?.copyStatus.clear();
2176
+ detail = null;
2177
+ detailPane.replaceChildren();
2178
+ return;
2179
+ }
2180
+ if (detail && detail.entry.id === entry.id) {
2181
+ if (detail.entry !== entry) {
2182
+ syncDetail(detail, entry);
2183
+ }
2184
+ return;
2185
+ }
2186
+ detail?.copyStatus.clear();
2187
+ detail = buildDetail(entry);
2188
+ }
2189
+ function copySelected(status) {
2190
+ const entry = selectedId ? log.get(selectedId) : void 0;
2191
+ if (!entry) {
2192
+ return;
2193
+ }
2194
+ let written;
2195
+ try {
2196
+ written = navigator.clipboard.writeText(formatActivityExport([entry]));
2197
+ } catch {
2198
+ status.show("Could not copy", "error", CONFIRM_MS * 2);
2199
+ return;
2200
+ }
2201
+ written.then(
2202
+ () => {
2203
+ if (!destroyed) {
2204
+ status.show("Copied", "ok", CONFIRM_MS);
2205
+ }
2206
+ },
2207
+ () => {
2208
+ if (!destroyed) {
2209
+ status.show("Could not copy", "error", CONFIRM_MS * 2);
2210
+ }
2211
+ }
2212
+ );
2213
+ }
2214
+ async function saveEntries(entries, status) {
2215
+ status.clear();
2216
+ try {
2217
+ await opts.saveTextFile?.(activityExportFileName(), formatActivityExport(entries));
2218
+ } catch (err) {
2219
+ if (destroyed) {
2220
+ return;
2221
+ }
2222
+ const reason = err instanceof Error && err.message ? `: ${err.message}` : "";
2223
+ status.show(`Could not save${reason}`, "error");
2224
+ }
2225
+ }
2226
+ async function saveSelected(btn, status) {
2227
+ const entry = selectedId ? log.get(selectedId) : void 0;
2228
+ if (!entry) {
2229
+ return;
2230
+ }
2231
+ btn.disabled = true;
2232
+ await saveEntries([entry], status);
2233
+ btn.disabled = false;
2234
+ }
2235
+ async function saveAll() {
2236
+ const entries = log.entries();
2237
+ if (entries.length === 0) {
2238
+ return;
2239
+ }
2240
+ savingAll = true;
2241
+ saveAllBtn.disabled = true;
2242
+ await saveEntries(entries, headerStatus);
2243
+ savingAll = false;
2244
+ if (!destroyed) {
2245
+ saveAllBtn.disabled = log.entries().length === 0;
2246
+ }
2247
+ }
2248
+ function flush() {
2249
+ if (renderTimer !== void 0) {
2250
+ render();
2251
+ }
2252
+ }
2253
+ function render() {
2254
+ window.clearTimeout(renderTimer);
2255
+ renderTimer = void 0;
2256
+ lastRender = Date.now();
2257
+ const entries = log.entries();
2258
+ const isEmpty = entries.length === 0;
2259
+ root.dataset.recording = log.enabled ? "on" : "off";
2260
+ body.hidden = isEmpty;
2261
+ empty.hidden = !isEmpty;
2262
+ if (isEmpty) {
2263
+ const copy = log.enabled ? AI_ACTIVITY_EMPTY : AI_ACTIVITY_OFF;
2264
+ setText(emptyTitle, copy.title);
2265
+ setText(emptyText, copy.text);
2266
+ view = "list";
2267
+ applyLayout();
2268
+ }
2269
+ clearBtn.disabled = isEmpty;
2270
+ if (!savingAll) {
2271
+ saveAllBtn.disabled = isEmpty;
2272
+ }
2273
+ renderList(entries);
2274
+ if (selectedId && !log.get(selectedId)) {
2275
+ selectedId = isEmpty ? null : entries[0].id;
2276
+ } else if (!selectedId && !isEmpty) {
2277
+ selectedId = entries[entries.length - 1].id;
2278
+ }
2279
+ markSelected();
2280
+ renderDetail(selectedId ? log.get(selectedId) ?? null : null);
2281
+ keepFocusInside();
2282
+ }
2283
+ function keepFocusInside() {
2284
+ if (!mounted) {
2285
+ return;
2286
+ }
2287
+ const active = document.activeElement;
2288
+ if (active === document.body || active instanceof HTMLButtonElement && active.disabled && root.contains(active)) {
2289
+ focus();
2290
+ }
2291
+ }
2292
+ function onLogChange() {
2293
+ if (destroyed) {
2294
+ return;
2295
+ }
2296
+ const entries = log.entries();
2297
+ const newest = entries[entries.length - 1];
2298
+ const wait = RENDER_THROTTLE_MS - (Date.now() - lastRender);
2299
+ if (wait <= 0 || !newest || !rows.has(newest.id)) {
2300
+ render();
2301
+ } else if (renderTimer === void 0) {
2302
+ renderTimer = window.setTimeout(render, wait);
2303
+ }
2304
+ }
2305
+ function focus() {
2306
+ (selectedRow() ?? backBtn).focus({ preventScroll: true });
2307
+ }
2308
+ function back() {
2309
+ if (narrow && view === "detail") {
2310
+ view = "list";
2311
+ applyLayout();
2312
+ focus();
2313
+ return;
2314
+ }
2315
+ ctx.close();
2316
+ }
2317
+ root.addEventListener("keydown", (e) => {
2318
+ if (e.key !== "Escape" || e.defaultPrevented) {
2319
+ return;
2320
+ }
2321
+ e.preventDefault();
2322
+ e.stopPropagation();
2323
+ back();
2324
+ });
2325
+ function applyLayout() {
2326
+ root.classList.toggle("is-narrow", narrow);
2327
+ root.dataset.view = narrow ? view : "both";
2328
+ backBtn.setAttribute("aria-label", narrow && view === "detail" ? "Back to the list" : "Back");
2329
+ }
2330
+ function measure(width) {
2331
+ const next = width > 0 && width < NARROW_PX;
2332
+ if (next === narrow) {
2333
+ return;
2334
+ }
2335
+ narrow = next;
2336
+ applyLayout();
2337
+ }
2338
+ let resizeObserver = null;
2339
+ if (typeof ResizeObserver === "function") {
2340
+ resizeObserver = new ResizeObserver((entries) => {
2341
+ measure(entries[0]?.contentRect.width ?? 0);
2342
+ });
2343
+ resizeObserver.observe(root);
2344
+ }
2345
+ container.append(root);
2346
+ applyLayout();
2347
+ render();
2348
+ mounted = true;
2349
+ measure(root.getBoundingClientRect().width);
2350
+ const unsubscribe = log.subscribe(onLogChange);
2351
+ return {
2352
+ destroy() {
2353
+ if (destroyed) {
2354
+ return;
2355
+ }
2356
+ destroyed = true;
2357
+ unsubscribe();
2358
+ window.clearTimeout(renderTimer);
2359
+ renderTimer = void 0;
2360
+ resizeObserver?.disconnect();
2361
+ headerStatus.clear();
2362
+ detail?.copyStatus.clear();
2363
+ root.remove();
2364
+ },
2365
+ focus,
2366
+ refresh() {
2367
+ if (!destroyed) {
2368
+ render();
2369
+ }
2370
+ }
2371
+ };
2372
+ }
2373
+
2374
+ // src/ai-docs-run.ts
2375
+ function withSupportedFormat(ctx, req) {
2376
+ if (req.format !== "json" || !req.pin) {
2377
+ return req;
2378
+ }
2379
+ const { pin } = req;
2380
+ const model = ctx.settings().engines.find((e) => e.id === pin.engineId)?.models.find((m) => m.id === pin.modelId);
2381
+ if (model?.capabilities.json !== false) {
2382
+ return req;
2383
+ }
2384
+ const { format: _format, ...rest } = req;
2385
+ return rest;
2386
+ }
2387
+ function aiDocsLoopDeps(ctx) {
2388
+ return {
2389
+ resolveRoute: (req, signal) => resolveAiTurnRoute(ctx, req, signal),
2390
+ run: (req, events, signal) => runAiCompletion(ctx, withSupportedFormat(ctx, req), events, signal),
2391
+ contextTokens(route) {
2392
+ if (!route) {
2393
+ return null;
2394
+ }
2395
+ const engine = ctx.settings().engines.find((e) => e.id === route.engineId);
2396
+ return engine?.models.find((m) => m.id === route.modelId)?.capabilities.contextTokens ?? null;
2397
+ },
2398
+ annotate(requestId, detail) {
2399
+ try {
2400
+ ctx.activity?.update(requestId, { docs: detail });
2401
+ } catch {
2402
+ }
2403
+ },
2404
+ endTurn(pin) {
2405
+ if (pin.turnId) {
2406
+ endAiTurn(ctx, pin.turnId);
2407
+ }
2408
+ }
2409
+ };
2410
+ }
2411
+ function createAiDocsChat(ctx) {
2412
+ return createDocsChat(aiDocsLoopDeps(ctx));
2413
+ }
2414
+
2415
+ // src/ai-ghost.ts
2416
+ var GHOST_BACKOFF_MAX_MS = 5e3;
2417
+ var SILENT_REASONS = /* @__PURE__ */ new Set(["cloudBlocked", "consentDenied", "capability", "featureDisabled"]);
2418
+ function classifyGhostError(error) {
2419
+ if (error.code !== "unknown") {
2420
+ return error.code;
2421
+ }
2422
+ if (/\bis turned off\.?$/i.test(error.message) || /^No engine and model is chosen/i.test(error.message)) {
2423
+ return "featureDisabled";
2424
+ }
2425
+ if (/\blacks\b/.test(error.message)) {
2426
+ return "capability";
2427
+ }
2428
+ return "unknown";
2429
+ }
2430
+ var originCounter = 0;
2431
+ function createGhostController(deps) {
2432
+ const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
2433
+ const clearTimer = deps.clearTimer ?? ((h) => clearTimeout(h));
2434
+ const now = deps.now ?? (() => Date.now());
2435
+ let destroyed = false;
2436
+ let latest = null;
2437
+ let timer = null;
2438
+ let controller = null;
2439
+ let seq = 0;
2440
+ let busy = false;
2441
+ let visible = null;
2442
+ let origin = null;
2443
+ let backoffMs = 0;
2444
+ const reported = /* @__PURE__ */ new Set();
2445
+ let silenced = false;
2446
+ let idleStopped = false;
2447
+ function setBusy(next) {
2448
+ if (busy === next) {
2449
+ return;
2450
+ }
2451
+ busy = next;
2452
+ try {
2453
+ deps.onBusy?.(next);
2454
+ } catch {
2455
+ }
2456
+ }
2457
+ function show(text) {
2458
+ const next = text === null || text === "" ? null : text;
2459
+ if (next === visible) {
2460
+ return;
2461
+ }
2462
+ visible = next;
2463
+ if (destroyed) {
2464
+ return;
2465
+ }
2466
+ deps.onSuggestion(next);
2467
+ }
2468
+ function cancelTimer() {
2469
+ if (timer !== null) {
2470
+ clearTimer(timer);
2471
+ timer = null;
2472
+ }
2473
+ }
2474
+ function stopStream() {
2475
+ seq += 1;
2476
+ const c = controller;
2477
+ controller = null;
2478
+ if (c && !c.signal.aborted) {
2479
+ c.abort();
2480
+ }
2481
+ setBusy(false);
2482
+ }
2483
+ function clearAll() {
2484
+ cancelTimer();
2485
+ stopStream();
2486
+ origin = null;
2487
+ show(null);
2488
+ }
2489
+ function delay() {
2490
+ return Math.max(deps.options().delayMs, backoffMs);
2491
+ }
2492
+ function bumpBackoff() {
2493
+ backoffMs = Math.min(GHOST_BACKOFF_MAX_MS, Math.max(deps.options().delayMs, backoffMs) * 2);
2494
+ }
2495
+ function activityRoute2(route) {
2496
+ let label = route.engineId;
2497
+ try {
2498
+ label = deps.routeLabel?.(route) ?? label;
2499
+ } catch {
2500
+ }
2501
+ return { engineId: route.engineId, modelId: route.modelId, label, isLocal: route.isLocal, viaFallback: route.viaFallback };
2502
+ }
2503
+ function servedOf(o) {
2504
+ if (o.served) {
2505
+ return o.served;
2506
+ }
2507
+ if (!o.route) {
2508
+ return null;
2509
+ }
2510
+ return { engineId: o.route.engineId, engineName: activityRoute2(o.route).label, modelId: o.route.modelId, isLocal: o.route.isLocal };
2511
+ }
2512
+ function record(entry) {
2513
+ try {
2514
+ deps.record?.(entry);
2515
+ } catch {
2516
+ }
2517
+ }
2518
+ function entryFor(o, response, error) {
2519
+ return {
2520
+ id: o.id,
2521
+ startedAt: o.startedAt,
2522
+ endedAt: o.endedAt ?? now(),
2523
+ feature: "ghostText",
2524
+ job: "ghostText",
2525
+ task: o.request.task,
2526
+ route: o.route ? activityRoute2(o.route) : null,
2527
+ thinking: o.thinking,
2528
+ request: o.request,
2529
+ response: { text: response, ...o.served ? { served: o.served } : {} },
2530
+ error,
2531
+ attempt: 1
2532
+ };
2533
+ }
2534
+ function markAccepted() {
2535
+ const o = origin;
2536
+ if (!o || o.recorded) {
2537
+ return;
2538
+ }
2539
+ o.recorded = true;
2540
+ record(entryFor(o, visible ?? o.raw, null));
2541
+ const served = servedOf(o);
2542
+ if (served) {
2543
+ try {
2544
+ deps.onServed?.(served, o.viaFallback);
2545
+ } catch {
2546
+ }
2547
+ }
2548
+ }
2549
+ function handleError(err, o) {
2550
+ const error = toError(err);
2551
+ if (error.code === "cancelled") {
2552
+ return;
2553
+ }
2554
+ const reason = classifyGhostError(error);
2555
+ const silent = SILENT_REASONS.has(reason);
2556
+ show(null);
2557
+ if (reason === "consentDenied") {
2558
+ silenced = true;
2559
+ }
2560
+ if (silent) {
2561
+ if (!reported.has(reason)) {
2562
+ reported.add(reason);
2563
+ report({ reason, silent: true, error });
2564
+ }
2565
+ return;
2566
+ }
2567
+ bumpBackoff();
2568
+ o.endedAt = now();
2569
+ record(entryFor(o, o.raw, error));
2570
+ report({ reason, silent: false, error });
2571
+ }
2572
+ function report(info) {
2573
+ if (destroyed) {
2574
+ return;
2575
+ }
2576
+ try {
2577
+ deps.onError?.(info);
2578
+ } catch {
2579
+ }
2580
+ }
2581
+ function fire(input) {
2582
+ if (destroyed || silenced || !input.eligible) {
2583
+ return;
2584
+ }
2585
+ cancelTimer();
2586
+ stopStream();
2587
+ show(null);
2588
+ origin = null;
2589
+ const options = deps.options();
2590
+ let snapshot;
2591
+ try {
2592
+ snapshot = input.read();
2593
+ } catch {
2594
+ return;
2595
+ }
2596
+ const maxTokens = continueMaxTokens(options.maxTokens);
2597
+ const ctx = ghostContext(snapshot.markdown, snapshot.caretOffset, deps.contextTokens(), { outputTokens: maxTokens });
2598
+ if (ctx.before.trim() === "") {
2599
+ return;
2600
+ }
2601
+ const messages = buildContinueMessages({ before: ctx.before, after: ctx.after, ...input.blockKind ? { blockKind: input.blockKind } : {} });
2602
+ const req = {
2603
+ feature: "ghostText",
2604
+ task: "continue",
2605
+ sends: "cursor",
2606
+ messages,
2607
+ maxTokens,
2608
+ temperature: 0.3,
2609
+ thinking: "off",
2610
+ job: "ghostText",
2611
+ localOnly: options.localOnly
2612
+ };
2613
+ const ac = new AbortController();
2614
+ controller = ac;
2615
+ const mySeq = ++seq;
2616
+ originCounter += 1;
2617
+ const o = {
2618
+ id: `ai-ghostText-${now().toString(36)}-g${originCounter}`,
2619
+ startedAt: now(),
2620
+ endedAt: null,
2621
+ request: { requestId: "", feature: req.feature, task: req.task ?? "continue", messages, maxTokens, temperature: 0.3 },
2622
+ route: null,
2623
+ thinking: null,
2624
+ served: null,
2625
+ viaFallback: false,
2626
+ raw: "",
2627
+ recorded: false
2628
+ };
2629
+ o.request.requestId = o.id;
2630
+ origin = o;
2631
+ setBusy(true);
2632
+ const events = {
2633
+ onRoute(route, thinking) {
2634
+ if (mySeq !== seq) {
2635
+ return;
2636
+ }
2637
+ o.route = route;
2638
+ o.thinking = thinking;
2639
+ o.viaFallback = route?.viaFallback ?? false;
2640
+ },
2641
+ onToken(_delta, text) {
2642
+ if (mySeq !== seq) {
2643
+ return;
2644
+ }
2645
+ o.raw = text;
2646
+ const r = trimGhostSuggestionDetailed(text, { atBlockEnd: input.atBlockEnd, before: ctx.before });
2647
+ show(r.text);
2648
+ if (r.stopped) {
2649
+ o.endedAt = now();
2650
+ succeeded();
2651
+ stopStream();
2652
+ }
2653
+ },
2654
+ // Not tied to `seq`: an answer given after this request was aborted counts too.
2655
+ onConsent(reply) {
2656
+ if (reply === "once") {
2657
+ idleStopped = true;
2658
+ }
2659
+ }
2660
+ };
2661
+ let pending;
2662
+ try {
2663
+ pending = deps.run(req, events, ac.signal);
2664
+ } catch (err) {
2665
+ stopStream();
2666
+ handleError(err, o);
2667
+ return;
2668
+ }
2669
+ pending.then(
2670
+ (outcome) => {
2671
+ if (mySeq !== seq) {
2672
+ return;
2673
+ }
2674
+ o.endedAt = now();
2675
+ o.served = outcome.result.served ?? null;
2676
+ o.viaFallback = outcome.viaFallback;
2677
+ if (outcome.route) {
2678
+ o.route = outcome.route;
2679
+ }
2680
+ if (outcome.thinking) {
2681
+ o.thinking = outcome.thinking;
2682
+ }
2683
+ if (outcome.requestId) {
2684
+ o.id = outcome.requestId;
2685
+ o.request.requestId = outcome.requestId;
2686
+ }
2687
+ o.raw = outcome.result.text || o.raw;
2688
+ const r = trimGhostSuggestionDetailed(o.raw, { atBlockEnd: input.atBlockEnd, before: ctx.before });
2689
+ show(r.text);
2690
+ succeeded();
2691
+ controller = null;
2692
+ setBusy(false);
2693
+ },
2694
+ (err) => {
2695
+ if (mySeq !== seq) {
2696
+ return;
2697
+ }
2698
+ controller = null;
2699
+ setBusy(false);
2700
+ handleError(err, o);
2701
+ }
2702
+ );
2703
+ }
2704
+ function succeeded() {
2705
+ backoffMs = 0;
2706
+ reported.clear();
2707
+ }
2708
+ function startTimer(input) {
2709
+ timer = setTimer(() => {
2710
+ timer = null;
2711
+ if (!idleStopped) {
2712
+ fire(input);
2713
+ }
2714
+ }, delay());
2715
+ }
2716
+ return {
2717
+ noteChange(input) {
2718
+ if (destroyed) {
2719
+ return;
2720
+ }
2721
+ latest = input;
2722
+ clearAll();
2723
+ if (!input.eligible || silenced || idleStopped) {
2724
+ return;
2725
+ }
2726
+ if (deps.options().trigger === "manual") {
2727
+ return;
2728
+ }
2729
+ startTimer(input);
2730
+ },
2731
+ noteSelectionMove() {
2732
+ if (destroyed) {
2733
+ return;
2734
+ }
2735
+ clearAll();
2736
+ },
2737
+ triggerNow(input) {
2738
+ if (destroyed) {
2739
+ return;
2740
+ }
2741
+ if (input) {
2742
+ latest = input;
2743
+ }
2744
+ const target = latest;
2745
+ clearAll();
2746
+ if (!target || !target.eligible) {
2747
+ return;
2748
+ }
2749
+ fire(target);
2750
+ },
2751
+ accept() {
2752
+ const text = visible;
2753
+ if (text === null) {
2754
+ return null;
2755
+ }
2756
+ markAccepted();
2757
+ cancelTimer();
2758
+ stopStream();
2759
+ origin = null;
2760
+ show(null);
2761
+ return text;
2762
+ },
2763
+ acceptWord() {
2764
+ const text = visible;
2765
+ if (text === null) {
2766
+ return null;
2767
+ }
2768
+ const word = ghostAcceptWord(text);
2769
+ markAccepted();
2770
+ stopStream();
2771
+ show(text.slice(word.length));
2772
+ return word;
2773
+ },
2774
+ dismiss() {
2775
+ if (destroyed) {
2776
+ return;
2777
+ }
2778
+ clearAll();
2779
+ },
2780
+ typed(char) {
2781
+ const text = visible;
2782
+ if (text === null || char === "") {
2783
+ return false;
2784
+ }
2785
+ const rest = ghostPrefixMatch(text, char);
2786
+ if (rest === null) {
2787
+ return false;
2788
+ }
2789
+ stopStream();
2790
+ if (rest === "") {
2791
+ origin = null;
2792
+ }
2793
+ show(rest);
2794
+ return true;
2795
+ },
2796
+ destroy() {
2797
+ if (destroyed) {
2798
+ return;
2799
+ }
2800
+ cancelTimer();
2801
+ stopStream();
2802
+ destroyed = true;
2803
+ latest = null;
2804
+ origin = null;
2805
+ visible = null;
2806
+ }
2807
+ };
2808
+ }
2809
+ function toError(err) {
2810
+ if (typeof err === "object" && err !== null && typeof err.code === "string" && typeof err.message === "string") {
2811
+ return err;
2812
+ }
2813
+ if (typeof err === "object" && err !== null && err.name === "AbortError") {
2814
+ return makeAiError("cancelled");
2815
+ }
2816
+ return makeAiError("unknown", err instanceof Error ? err.message : void 0);
2817
+ }
2818
+
2819
+ // src/ai-ghost-plugin.ts
2820
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
2821
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
2822
+ import { closeHistory, isHistoryTransaction } from "@tiptap/pm/history";
2823
+ var aiGhostKey = new PluginKey("aiGhost");
2824
+ var GHOST_KEY_WINDOW_MS = 1e3;
2825
+ var CARET_MARK = "\uE000\uF8FF\uE000";
2826
+ var ALLOWED_CONTAINERS = /* @__PURE__ */ new Set(["doc", "blockquote", "callout", "bulletList", "orderedList", "listItem", "taskList", "taskItem"]);
2827
+ var EMPTY = { text: null, pos: 0, shownAt: 0, change: null, decorations: DecorationSet.empty };
2828
+ function classify(tr, meta) {
2829
+ if (meta?.own) {
2830
+ return "own";
2831
+ }
2832
+ if (!tr.docChanged) {
2833
+ return null;
2834
+ }
2835
+ const notTyping = isHistoryTransaction(tr) || !!tr.getMeta("paste") || !!tr.getMeta("uiEvent") || !!tr.getMeta("preventUpdate") || tr.getMeta("addToHistory") === false;
2836
+ return notTyping ? "other" : "typing";
2837
+ }
2838
+ function ghostWidget(state, text, pos, shownAt) {
2839
+ const toDOM = (view) => {
2840
+ const span = view.dom.ownerDocument.createElement("span");
2841
+ span.className = "mikedown-ghost";
2842
+ span.setAttribute("aria-hidden", "true");
2843
+ span.setAttribute("spellcheck", "false");
2844
+ span.style.animationDelay = `${shownAt - Date.now()}ms`;
2845
+ span.textContent = text;
2846
+ return span;
2847
+ };
2848
+ const marks = state.storedMarks ?? state.selection.$head.marks();
2849
+ return DecorationSet.create(state.doc, [Decoration.widget(pos, toDOM, { side: 1, marks })]);
2850
+ }
2851
+ function applyGhost(tr, prev, oldState, newState) {
2852
+ const meta = tr.getMeta(aiGhostKey);
2853
+ const root = tr.getMeta("appendedTransaction");
2854
+ const change = (root ? prev.change : null) ?? classify(tr, meta);
2855
+ let text = prev.text;
2856
+ if (meta && "text" in meta) {
2857
+ text = meta.text ?? null;
2858
+ } else if (change !== "own" && (tr.docChanged || !oldState.selection.eq(newState.selection))) {
2859
+ text = null;
2860
+ }
2861
+ if (text === null) {
2862
+ return prev.text === null && prev.change === change ? prev : { ...EMPTY, change };
2863
+ }
2864
+ const pos = newState.selection.head;
2865
+ if (text === prev.text && pos === prev.pos && !tr.docChanged) {
2866
+ return prev.change === change ? prev : { ...prev, change };
2867
+ }
2868
+ const shownAt = prev.text === null ? Date.now() : prev.shownAt;
2869
+ return { text, pos, shownAt, change, decorations: ghostWidget(newState, text, pos, shownAt) };
2870
+ }
2871
+ function visibleLine(text) {
2872
+ if (!text) {
2873
+ return null;
2874
+ }
2875
+ const newline = text.indexOf("\n");
2876
+ return (newline < 0 ? text : text.slice(0, newline).replace(/[ \t]+$/, "")) || null;
2877
+ }
2878
+ function readAtCaret(state, serialize) {
2879
+ const marked = serialize(state.tr.insertText(CARET_MARK, state.selection.head).doc);
2880
+ const caretOffset = marked.indexOf(CARET_MARK);
2881
+ if (caretOffset < 0) {
2882
+ return { markdown: "", caretOffset: 0 };
2883
+ }
2884
+ return { markdown: marked.slice(0, caretOffset) + marked.slice(caretOffset + CARET_MARK.length), caretOffset };
2885
+ }
2886
+ function blockKindAt($head) {
2887
+ const name = $head.parent.type.name;
2888
+ if (name !== "paragraph" && name !== "heading") {
2889
+ return null;
2890
+ }
2891
+ let kind = name === "heading" ? "heading" : "paragraph";
2892
+ let inner = kind !== "paragraph";
2893
+ for (let depth = $head.depth - 1; depth >= 0; depth--) {
2894
+ const container = $head.node(depth).type.name;
2895
+ if (!ALLOWED_CONTAINERS.has(container)) {
2896
+ return null;
2897
+ }
2898
+ if (inner) {
2899
+ continue;
2900
+ }
2901
+ if (container === "listItem" || container === "taskItem") {
2902
+ kind = "listItem";
2903
+ inner = true;
2904
+ } else if (container === "blockquote" || container === "callout") {
2905
+ kind = "blockquote";
2906
+ inner = true;
2907
+ }
2908
+ }
2909
+ return kind;
2910
+ }
2911
+ function probe(view, options, manual) {
2912
+ const { state } = view;
2913
+ const { selection } = state;
2914
+ const { $head } = selection;
2915
+ const input = { read: () => readAtCaret(state, options.serialize), atBlockEnd: false, eligible: false };
2916
+ if (!selection.empty || !view.editable || view.composing) {
2917
+ return input;
2918
+ }
2919
+ if (options.enabled?.() === false || options.popupOpen?.()) {
2920
+ return input;
2921
+ }
2922
+ const blockKind = blockKindAt($head);
2923
+ if (!blockKind || (state.storedMarks ?? $head.marks()).some((mark) => mark.type.name === "code")) {
2924
+ return input;
2925
+ }
2926
+ const block = $head.parent;
2927
+ if (block.textBetween($head.parentOffset, block.content.size, "\n", "\uFFFC").trim() !== "") {
2928
+ return input;
2929
+ }
2930
+ if (!manual && (block.content.size === 0 || !view.hasFocus())) {
2931
+ return input;
2932
+ }
2933
+ return { ...input, atBlockEnd: $head.parentOffset === block.content.size, eligible: true, blockKind };
2934
+ }
2935
+ function insertOwn(view, text, keep) {
2936
+ const meta = keep ? { own: true } : { own: true, text: null };
2937
+ view.dispatch(closeHistory(view.state.tr).insertText(text).setMeta(aiGhostKey, meta).scrollIntoView());
2938
+ view.dispatch(closeHistory(view.state.tr).setMeta(aiGhostKey, { own: true }));
2939
+ }
2940
+ function insertTyped(view, char) {
2941
+ const { from, to } = view.state.selection;
2942
+ const plain = () => view.state.tr.insertText(char).setMeta(aiGhostKey, { own: true }).scrollIntoView();
2943
+ if (!view.someProp("handleTextInput", (handle) => handle(view, from, to, char, plain))) {
2944
+ view.dispatch(plain());
2945
+ }
2946
+ }
2947
+ function ghostKey(view, event, controller) {
2948
+ const mod = event.metaKey || event.ctrlKey;
2949
+ if (event.key === "Tab" && !mod && !event.shiftKey && !event.altKey) {
2950
+ const text = visibleLine(controller.accept());
2951
+ setGhostSuggestion(view, null);
2952
+ if (!text) {
2953
+ return false;
2954
+ }
2955
+ insertOwn(view, text, false);
2956
+ return true;
2957
+ }
2958
+ if (event.key === "ArrowRight" && mod && !event.shiftKey && !event.altKey) {
2959
+ const word = visibleLine(controller.acceptWord());
2960
+ if (!word) {
2961
+ setGhostSuggestion(view, null);
2962
+ return false;
2963
+ }
2964
+ insertOwn(view, word, true);
2965
+ return true;
2966
+ }
2967
+ if (event.key === "Escape" && !mod && !event.shiftKey && !event.altKey) {
2968
+ controller.dismiss();
2969
+ setGhostSuggestion(view, null);
2970
+ return true;
2971
+ }
2972
+ const printable = [...event.key].length === 1 && !event.metaKey && !(event.ctrlKey && !event.altKey);
2973
+ if (printable && controller.typed(event.key)) {
2974
+ insertTyped(view, event.key);
2975
+ return true;
2976
+ }
2977
+ return false;
2978
+ }
2979
+ function createGhostPlugin(options) {
2980
+ const { controller } = options;
2981
+ let lastKeyAt = 0;
2982
+ const spec = {
2983
+ key: aiGhostKey,
2984
+ state: {
2985
+ init: () => EMPTY,
2986
+ apply: applyGhost
2987
+ },
2988
+ view: () => ({
2989
+ update(view, prevState) {
2990
+ const { change } = aiGhostKey.getState(view.state) ?? EMPTY;
2991
+ if (change === "own") {
2992
+ return;
2993
+ }
2994
+ if (change === null) {
2995
+ if (!prevState.selection.eq(view.state.selection)) {
2996
+ controller.noteSelectionMove();
2997
+ }
2998
+ return;
2999
+ }
3000
+ const input = probe(view, options, false);
3001
+ const keystroke = change === "typing" && Date.now() - lastKeyAt < GHOST_KEY_WINDOW_MS;
3002
+ controller.noteChange(keystroke ? input : { ...input, eligible: false });
3003
+ }
3004
+ }),
3005
+ props: {
3006
+ decorations: (state) => aiGhostKey.getState(state)?.decorations,
3007
+ handleDOMEvents: {
3008
+ keydown(view, event) {
3009
+ lastKeyAt = Date.now();
3010
+ if (!aiGhostKey.getState(view.state)?.text) {
3011
+ return false;
3012
+ }
3013
+ if (event.isComposing || event.keyCode === 229 || view.composing) {
3014
+ return false;
3015
+ }
3016
+ if (options.popupOpen?.()) {
3017
+ controller.dismiss();
3018
+ setGhostSuggestion(view, null);
3019
+ return false;
3020
+ }
3021
+ if (!ghostKey(view, event, controller)) {
3022
+ return false;
3023
+ }
3024
+ event.preventDefault();
3025
+ return true;
3026
+ },
3027
+ // Do not sit under an IME's own preview.
3028
+ compositionstart() {
3029
+ controller.dismiss();
3030
+ return false;
3031
+ },
3032
+ compositionend() {
3033
+ lastKeyAt = Date.now();
3034
+ return false;
3035
+ },
3036
+ // Tab would go elsewhere, and nobody is waiting for a request once focus has left.
3037
+ blur() {
3038
+ controller.dismiss();
3039
+ return false;
3040
+ }
3041
+ }
3042
+ },
3043
+ triggerNow(view) {
3044
+ controller.triggerNow(probe(view, options, true));
3045
+ }
3046
+ };
3047
+ return new Plugin(spec);
3048
+ }
3049
+ function setGhostSuggestion(view, text) {
3050
+ if (view.isDestroyed) {
3051
+ return;
3052
+ }
3053
+ const ghost = aiGhostKey.getState(view.state);
3054
+ if (!ghost) {
3055
+ return;
3056
+ }
3057
+ const next = visibleLine(text);
3058
+ if (next === ghost.text) {
3059
+ return;
3060
+ }
3061
+ if (next !== null && (!view.state.selection.empty || view.composing)) {
3062
+ return;
3063
+ }
3064
+ view.dispatch(view.state.tr.setMeta(aiGhostKey, { text: next }).setMeta("addToHistory", false));
3065
+ }
3066
+ function triggerGhostNow(view) {
3067
+ aiGhostKey.get(view.state)?.spec?.triggerNow(view);
3068
+ }
3069
+
3070
+ // src/ai-ghost-session.ts
3071
+ function ghostFeatureOn(settings) {
3072
+ return settings.enabled && resolveAiFeature(settings.features, "ghostText").enabled;
3073
+ }
3074
+ function withGhostFeature(settings, enabled) {
3075
+ const features = { ...settings.features, ghostText: { ...settings.features.ghostText ?? {}, enabled } };
3076
+ return {
3077
+ settings: { ...settings, features },
3078
+ patch: { "ai.features": JSON.parse(JSON.stringify(features)) }
3079
+ };
3080
+ }
3081
+ function ghostAvailability(ai, settings, statuses, consent) {
3082
+ const plain = aiFeatureAvailability(ai, settings, statuses, consent, "ghostText");
3083
+ if (plain.state === "hidden" || typeof ai?.listEngines !== "function" || !ghostTextOptions(settings.features).localOnly) {
3084
+ return plain;
3085
+ }
3086
+ const route = resolveRoute(settings, statuses, consent, { feature: "ghostText", localOnly: true });
3087
+ return isRouteUsable(route) ? { state: "enabled" } : { state: "disabled", reason: route.reason };
3088
+ }
3089
+ function createGhostSession(deps) {
3090
+ const { editor } = deps;
3091
+ let reason = null;
3092
+ const controller = createGhostController({
3093
+ run: (req, events, signal) => deps.run(req, events, signal),
3094
+ options: () => ghostTextOptions(deps.settings().features),
3095
+ contextTokens() {
3096
+ const settings = deps.settings();
3097
+ const { localOnly } = ghostTextOptions(settings.features);
3098
+ const route = resolveRoute(settings, deps.statuses(), deps.consent, { feature: "ghostText", localOnly });
3099
+ if (!isRouteUsable(route)) {
3100
+ return DEFAULT_CONTEXT_TOKENS;
3101
+ }
3102
+ const model = settings.engines.find((e) => e.id === route.engineId)?.models.find((m) => m.id === route.modelId);
3103
+ return model?.capabilities.contextTokens ?? DEFAULT_CONTEXT_TOKENS;
3104
+ },
3105
+ onSuggestion(text) {
3106
+ if (text) {
3107
+ reason = null;
3108
+ }
3109
+ setGhostSuggestion(editor.view, text);
3110
+ },
3111
+ onBusy: (busy) => deps.onBusy(busy),
3112
+ onServed: (served, viaFallback) => deps.onServed(served, viaFallback),
3113
+ onError(info) {
3114
+ reason = info.error.message;
3115
+ },
3116
+ record: (entry) => deps.record(entry),
3117
+ routeLabel(route) {
3118
+ const engine = deps.settings().engines.find((e) => e.id === route.engineId);
3119
+ return engine ? engineDisplayName(engine) : route.engineId;
3120
+ }
3121
+ });
3122
+ editor.registerPlugin(createGhostPlugin({
3123
+ controller,
3124
+ serialize: (doc) => editor.storage.markdown.serializer.serialize(doc),
3125
+ popupOpen: () => deps.popupOpen(),
3126
+ enabled: () => deps.enabled()
3127
+ }));
3128
+ return {
3129
+ dismiss() {
3130
+ controller.dismiss();
3131
+ setGhostSuggestion(editor.view, null);
3132
+ },
3133
+ trigger() {
3134
+ editor.view.focus();
3135
+ triggerGhostNow(editor.view);
3136
+ },
3137
+ reason: () => reason,
3138
+ destroy() {
3139
+ controller.destroy();
3140
+ editor.unregisterPlugin(aiGhostKey);
3141
+ }
3142
+ };
3143
+ }
3144
+
3145
+ // src/ai-markdown-preview.ts
3146
+ var FENCE = /^ {0,3}(`{3,}|~{3,})/;
3147
+ var HEADING = /^ {0,3}(#{1,6})[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/;
3148
+ var ITEM = /^([ \t]*)([-*+]|\d{1,9}[.)])[ \t]+(.*)$/;
3149
+ var QUOTE = /^ {0,3}>[ \t]?(.*)$/;
3150
+ var RULE = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/;
3151
+ var isSpace = (c) => c === void 0 || /\s/.test(c);
3152
+ var isWord = (c) => c !== void 0 && /[\p{L}\p{N}]/u.test(c);
3153
+ function make(tag, className) {
3154
+ const node = document.createElement(tag);
3155
+ if (className) {
3156
+ node.className = className;
3157
+ }
3158
+ return node;
3159
+ }
3160
+ function findCloser(s, delim, from) {
3161
+ for (let j = s.indexOf(delim, from); j !== -1; j = s.indexOf(delim, j + 1)) {
3162
+ if (s[j - 1] === "\\" || isSpace(s[j - 1])) {
3163
+ continue;
3164
+ }
3165
+ if (delim.length === 1 && (s[j + 1] === delim || s[j - 1] === delim)) {
3166
+ continue;
3167
+ }
3168
+ if (delim === "_" && isWord(s[j + 1])) {
3169
+ continue;
3170
+ }
3171
+ return j;
3172
+ }
3173
+ return -1;
3174
+ }
3175
+ function linkEnd(s, close) {
3176
+ if (s[close + 1] !== "(") {
3177
+ return -1;
3178
+ }
3179
+ let depth = 0;
3180
+ for (let k = close + 1; k < s.length; k++) {
3181
+ if (s[k] === "\n") {
3182
+ return -1;
3183
+ }
3184
+ if (s[k] === "(") {
3185
+ depth++;
3186
+ } else if (s[k] === ")" && --depth === 0) {
3187
+ return k + 1;
3188
+ }
3189
+ }
3190
+ return -1;
3191
+ }
3192
+ function matchingBracket(s, open) {
3193
+ let depth = 0;
3194
+ for (let k = open; k < s.length; k++) {
3195
+ if (s[k] === "\\") {
3196
+ k++;
3197
+ } else if (s[k] === "[") {
3198
+ depth++;
3199
+ } else if (s[k] === "]" && --depth === 0) {
3200
+ return k;
3201
+ }
3202
+ }
3203
+ return -1;
3204
+ }
3205
+ function appendInline(parent, s) {
3206
+ let buf = "";
3207
+ const flush = () => {
3208
+ if (buf) {
3209
+ parent.appendChild(document.createTextNode(buf));
3210
+ buf = "";
3211
+ }
3212
+ };
3213
+ const wrap = (tag, inner, className) => {
3214
+ flush();
3215
+ const node = make(tag, className);
3216
+ appendInline(node, inner);
3217
+ parent.appendChild(node);
3218
+ };
3219
+ let i = 0;
3220
+ while (i < s.length) {
3221
+ const c = s[i];
3222
+ if (c === "\\" && i + 1 < s.length && /[\\`*_{}[\]()#+\-.!~>|]/.test(s[i + 1])) {
3223
+ buf += s[i + 1];
3224
+ i += 2;
3225
+ continue;
3226
+ }
3227
+ if (c === "`") {
3228
+ let n = 1;
3229
+ while (s[i + n] === "`") {
3230
+ n++;
3231
+ }
3232
+ const run = "`".repeat(n);
3233
+ const end = s.indexOf(run, i + n);
3234
+ if (end !== -1 && s[end + n] !== "`") {
3235
+ flush();
3236
+ const code = make("code");
3237
+ code.textContent = s.slice(i + n, end).replace(/^ (.*) $/, "$1");
3238
+ parent.appendChild(code);
3239
+ i = end + n;
3240
+ continue;
3241
+ }
3242
+ buf += run;
1211
3243
  i += n;
1212
3244
  continue;
1213
3245
  }
@@ -1368,6 +3400,959 @@ ${lines[i].trim()}`));
1368
3400
  into.replaceChildren(...out);
1369
3401
  }
1370
3402
 
3403
+ // src/ai-chat-panel.ts
3404
+ var AI_CHAT_SECRET_NOTICE = "Your question looks like it holds a key, so nothing was sent. Take the key out and ask again.";
3405
+ var AI_CHAT_BUSY_NOTICE = "The last request is still finishing. Try again in a moment.";
3406
+ var AI_CHAT_NO_BASES = "No documentation base is active. Turn one on or add one, or ask about this document instead.";
3407
+ var STREAM_THROTTLE_MS = 80;
3408
+ var CONFIRM_MS2 = 1600;
3409
+ var CHIP_OPEN = "\uE000";
3410
+ var CHIP_CLOSE = "\uE001";
3411
+ var CHIP_RE = /(\d+)/;
3412
+ var ICON_SEND = svgIcon('<path d="M8 13V3M3.5 7.5L8 3l4.5 4.5"/>');
3413
+ var ICON_STOP = svgIcon('<rect x="4" y="4" width="8" height="8" rx="1.5"/>');
3414
+ var ICON_COPY = svgIcon('<rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 5.5v-2a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2"/>');
3415
+ var ICON_SAVE = svgIcon('<path d="M8 2.5v8M4.5 7.5L8 11l3.5-3.5M3 13.5h10"/>');
3416
+ function svgIcon(body) {
3417
+ return `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
3418
+ }
3419
+ var plural2 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
3420
+ function chatProgressLine(progress, scope) {
3421
+ const hop = progress.hop !== void 0 ? ` hop ${progress.hop} of ${progress.maxHops}` : "";
3422
+ switch (progress.phase) {
3423
+ case "navigate":
3424
+ return progress.pages > 0 ? `Reading ${plural2(progress.pages, "page")}...${hop}` : `Looking through the ${scope === "document" ? "outline" : "index"}...${hop}`;
3425
+ case "keyword":
3426
+ return "Searching for matching sections...";
3427
+ case "answer":
3428
+ return progress.pages > 0 ? `Answering from ${plural2(progress.pages, "section")}...` : "Answering...";
3429
+ }
3430
+ }
3431
+ function chatLocalOnlyTitle(bases) {
3432
+ const names = bases.map((b) => b.name);
3433
+ if (names.length === 1) {
3434
+ return `Base ${names[0]} is local-only`;
3435
+ }
3436
+ return `Bases ${names.slice(0, -1).join(", ")} and ${names[names.length - 1]} are local-only`;
3437
+ }
3438
+ var pad2 = (n) => String(n).padStart(2, "0");
3439
+ function chatExportFileName(now = /* @__PURE__ */ new Date()) {
3440
+ const day = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}`;
3441
+ return `mikedown-ai-chat-${day}-${pad2(now.getHours())}${pad2(now.getMinutes())}.txt`;
3442
+ }
3443
+ function formatChatExport(turns) {
3444
+ if (turns.length === 0) {
3445
+ return "";
3446
+ }
3447
+ const blocks = turns.map((turn, i) => {
3448
+ const head = [
3449
+ `=== Turn ${i + 1} of ${turns.length} ===`,
3450
+ `Time: ${new Date(turn.startedAt).toISOString()}`,
3451
+ `Scope: ${turn.scope === "bases" ? "Active bases" : "This document"}`,
3452
+ `Status: ${turn.status}`,
3453
+ ...turn.engine ? [`Engine: ${turn.engine}${turn.isLocal === void 0 ? "" : turn.isLocal ? " (local)" : " (cloud)"}`] : []
3454
+ ].join("\n");
3455
+ return [
3456
+ head,
3457
+ "--- Question ---",
3458
+ turn.question ?? "(not kept: it looked like it held a key)",
3459
+ ...turn.answer !== void 0 ? ["--- Answer ---", turn.answer] : [],
3460
+ ...turn.notes && turn.notes.length > 0 ? ["--- Notes ---", turn.notes.join("\n")] : [],
3461
+ ...turn.sources && turn.sources.length > 0 ? ["--- Sources ---", turn.sources.join("\n")] : []
3462
+ ].join("\n\n");
3463
+ });
3464
+ return `${blocks.join("\n\n")}
3465
+ `;
3466
+ }
3467
+ function errorText(error, route, engineName) {
3468
+ if (error.code === "unknown" && error.message) {
3469
+ return error.message;
3470
+ }
3471
+ return aiErrorCopy(error.code, route ? { engine: engineName(route.engineId), model: route.modelId } : {}).message;
3472
+ }
3473
+ function answerNotes(turn, answer) {
3474
+ const notes = [];
3475
+ if (answer.coverage) {
3476
+ notes.push(answer.coverage);
3477
+ }
3478
+ if (answer.withoutSources && !answer.coverage.includes(ANSWERED_WITHOUT_SOURCES)) {
3479
+ notes.push(ANSWERED_WITHOUT_SOURCES);
3480
+ }
3481
+ if (answer.noSources && turn.scope === "bases") {
3482
+ notes.push(NO_SOURCES_CITED);
3483
+ }
3484
+ return notes;
3485
+ }
3486
+ function answerParts(turn, answer) {
3487
+ return turn.scope === "bases" ? answer.citations.parts : parseCitations(answer.answer, []).parts;
3488
+ }
3489
+ function exportTurn(turn, engineName) {
3490
+ const base = { startedAt: turn.startedAt, scope: turn.scope, question: turn.question };
3491
+ const route = turn.route ? { engine: `${engineName(turn.route.engineId)} \xB7 ${turn.route.modelId}`, isLocal: turn.route.isLocal } : {};
3492
+ const outcome = turn.outcome;
3493
+ if (!outcome) {
3494
+ return { ...base, ...route, status: "running", ...turn.partial ? { answer: turn.partial } : {} };
3495
+ }
3496
+ switch (outcome.kind) {
3497
+ case "answered":
3498
+ return {
3499
+ ...base,
3500
+ ...route,
3501
+ status: "answered",
3502
+ answer: answerParts(turn, outcome).map((p) => p.type === "text" ? p.text : `[${p.ns.join(", ")}]`).join(""),
3503
+ notes: answerNotes(turn, outcome),
3504
+ sources: turn.scope === "bases" ? outcome.sent.filter((s) => outcome.citations.cited.includes(s.n)).map((s) => `[${s.n}] ${docsCitationLabel(s.section)}`) : []
3505
+ };
3506
+ case "cancelled":
3507
+ return { ...base, ...route, status: "cancelled", ...turn.partial ? { answer: turn.partial } : {} };
3508
+ case "error":
3509
+ return { ...base, ...route, status: "failed", notes: [errorText(outcome.error, turn.route, engineName)] };
3510
+ case "baseLocalOnly":
3511
+ return { ...base, status: "not sent", notes: [chatLocalOnlyTitle(outcome.bases)] };
3512
+ case "noActiveBases":
3513
+ return { ...base, status: "not sent", notes: [AI_CHAT_NO_BASES] };
3514
+ case "questionHasSecret":
3515
+ return { ...base, status: "not sent", notes: [AI_CHAT_SECRET_NOTICE] };
3516
+ case "busy":
3517
+ return { ...base, status: "not sent", notes: [AI_CHAT_BUSY_NOTICE] };
3518
+ }
3519
+ }
3520
+ function inlineStatus2() {
3521
+ const element = el("span", "ai-chat-status");
3522
+ element.setAttribute("role", "status");
3523
+ let timer;
3524
+ return {
3525
+ element,
3526
+ show(text, level = "ok") {
3527
+ window.clearTimeout(timer);
3528
+ element.textContent = text;
3529
+ element.dataset.level = level;
3530
+ timer = window.setTimeout(() => {
3531
+ element.textContent = "";
3532
+ }, level === "ok" ? CONFIRM_MS2 : CONFIRM_MS2 * 3);
3533
+ },
3534
+ dispose() {
3535
+ window.clearTimeout(timer);
3536
+ }
3537
+ };
3538
+ }
3539
+ function mountAiChatPanel(container, opts) {
3540
+ const engineName = (id) => opts.engineName?.(id) ?? id;
3541
+ const turns = [];
3542
+ let statuses = [];
3543
+ let scope = "document";
3544
+ let scopeChosen = false;
3545
+ let current = null;
3546
+ let controller = null;
3547
+ let destroyed = false;
3548
+ let streamTimer;
3549
+ const statusDisposers = /* @__PURE__ */ new Set();
3550
+ const makeStatus = () => {
3551
+ const status = inlineStatus2();
3552
+ statusDisposers.add(() => status.dispose());
3553
+ return status;
3554
+ };
3555
+ const root = el("section", "ai-chat");
3556
+ const titleId = nextId("ai-chat-title");
3557
+ root.setAttribute("aria-labelledby", titleId);
3558
+ const header = el("div", "ai-chat-header");
3559
+ const title = el("h2", "ai-chat-title", "Chat");
3560
+ title.id = titleId;
3561
+ const headerStatus = makeStatus();
3562
+ const copyAllBtn = button("ai-chat-icon", "", { html: ICON_COPY, ariaLabel: "Copy conversation", title: "Copy conversation", fk: "chat:copy-all" });
3563
+ const saveBtn = button("ai-chat-icon", "", { html: ICON_SAVE, ariaLabel: "Save conversation", title: "Save conversation", fk: "chat:save" });
3564
+ const closeBtn = button("ai-chat-icon", "", { html: ICONS.close, ariaLabel: "Close chat (clears this conversation)", title: "Close (clears this conversation)", fk: "chat:close" });
3565
+ header.append(title, experimentalBadge(), el("span", "ai-chat-spacer"), headerStatus.element, copyAllBtn, ...opts.saveTextFile ? [saveBtn] : [], closeBtn);
3566
+ const log = el("div", "ai-chat-log");
3567
+ log.setAttribute("role", "log");
3568
+ log.setAttribute("aria-live", "polite");
3569
+ log.tabIndex = 0;
3570
+ log.setAttribute("aria-label", "Conversation");
3571
+ const empty = el("div", "ai-chat-empty");
3572
+ const emptyTitle = el("div", "ai-chat-empty-title");
3573
+ const emptyText = el("p", "ai-chat-empty-text");
3574
+ empty.append(emptyTitle, emptyText);
3575
+ log.append(empty);
3576
+ const composer = el("div", "ai-chat-composer");
3577
+ const offNote = el("div", "ai-chat-off");
3578
+ const picker = el("div", "ai-chat-scope");
3579
+ picker.setAttribute("role", "radiogroup");
3580
+ picker.setAttribute("aria-label", "What to ask about");
3581
+ const radioName = nextId("ai-chat-scope");
3582
+ const radios = /* @__PURE__ */ new Map();
3583
+ for (const [value, label] of [["document", "This document"], ["bases", "Active bases"]]) {
3584
+ const option = el("label", "ai-chat-seg");
3585
+ const radio = el("input");
3586
+ radio.type = "radio";
3587
+ radio.name = radioName;
3588
+ radio.value = value;
3589
+ radio.addEventListener("change", () => {
3590
+ if (!radio.checked) {
3591
+ return;
3592
+ }
3593
+ scopeChosen = true;
3594
+ setScope(value);
3595
+ });
3596
+ radios.set(value, radio);
3597
+ option.append(radio, el("span", "", label));
3598
+ picker.append(option);
3599
+ }
3600
+ picker.hidden = !opts.docs;
3601
+ const basesLine = el("div", "ai-chat-bases");
3602
+ const box = el("div", "ai-chat-box");
3603
+ const input = el("textarea", "ai-chat-input");
3604
+ input.rows = 1;
3605
+ input.setAttribute("aria-label", "Your question");
3606
+ const sendBtn = button("ai-chat-send", "", { html: ICON_SEND, ariaLabel: "Send", title: "Send (Enter)", fk: "chat:send" });
3607
+ const cancelBtn = button("ai-chat-send ai-chat-send--cancel", "", { html: ICON_STOP, ariaLabel: "Cancel", title: "Cancel (Esc)", fk: "chat:cancel" });
3608
+ cancelBtn.hidden = true;
3609
+ box.append(input, sendBtn, cancelBtn);
3610
+ composer.append(offNote, picker, basesLine, box);
3611
+ root.append(header, log, composer);
3612
+ function activeBases() {
3613
+ return docsActiveBases(statuses);
3614
+ }
3615
+ function setScope(next) {
3616
+ scope = next;
3617
+ const radio = radios.get(next);
3618
+ if (radio) {
3619
+ radio.checked = true;
3620
+ }
3621
+ renderComposerState();
3622
+ }
3623
+ function renderBasesLine() {
3624
+ basesLine.replaceChildren();
3625
+ basesLine.hidden = scope !== "bases";
3626
+ if (scope !== "bases") {
3627
+ return;
3628
+ }
3629
+ const active = activeBases();
3630
+ if (active.length > 0) {
3631
+ basesLine.textContent = active.map((s) => s.base.name).join(", ");
3632
+ basesLine.title = basesLine.textContent;
3633
+ return;
3634
+ }
3635
+ basesLine.title = "";
3636
+ basesLine.append(el("span", "", "No base is active. "));
3637
+ if (opts.openBases) {
3638
+ const manage = button("ai-chat-link", "Manage bases\u2026", { fk: "chat:manage-bases" });
3639
+ manage.addEventListener("click", () => opts.openBases?.());
3640
+ basesLine.append(manage);
3641
+ }
3642
+ }
3643
+ function renderComposerState() {
3644
+ const off = opts.featureOffNotice();
3645
+ offNote.replaceChildren();
3646
+ offNote.hidden = !off;
3647
+ if (off) {
3648
+ offNote.append(note("info", off));
3649
+ }
3650
+ picker.hidden = !opts.docs || !!off;
3651
+ renderBasesLine();
3652
+ if (off) {
3653
+ basesLine.hidden = true;
3654
+ }
3655
+ const busy = current !== null;
3656
+ input.disabled = !!off;
3657
+ input.placeholder = off ? "" : scope === "bases" ? "Ask your documentation\u2026" : "Ask about this document\u2026";
3658
+ sendBtn.hidden = busy;
3659
+ cancelBtn.hidden = !busy;
3660
+ sendBtn.disabled = !!off || input.value.trim() === "";
3661
+ for (const radio of radios.values()) {
3662
+ radio.disabled = busy;
3663
+ }
3664
+ emptyTitle.textContent = scope === "bases" ? "Ask your documentation" : "Ask about this document";
3665
+ emptyText.textContent = scope === "bases" ? "Answers come from the active bases, with citations you can open." : "Answers come from the text in this editor, as it is right now.";
3666
+ empty.hidden = turns.length > 0;
3667
+ copyAllBtn.hidden = turns.length === 0;
3668
+ saveBtn.hidden = turns.length === 0;
3669
+ }
3670
+ async function loadStatuses() {
3671
+ if (!opts.docs) {
3672
+ return;
3673
+ }
3674
+ try {
3675
+ statuses = await opts.docs.listBases();
3676
+ } catch {
3677
+ statuses = [];
3678
+ }
3679
+ if (destroyed) {
3680
+ return;
3681
+ }
3682
+ if (!scopeChosen && current === null) {
3683
+ setScope(activeBases().length > 0 ? "bases" : "document");
3684
+ }
3685
+ for (const turn of turns) {
3686
+ updateChipTitles(turn);
3687
+ }
3688
+ renderComposerState();
3689
+ }
3690
+ function nearBottom() {
3691
+ return log.scrollHeight - log.scrollTop - log.clientHeight < 48;
3692
+ }
3693
+ function scrollDown(was) {
3694
+ if (was) {
3695
+ log.scrollTop = log.scrollHeight;
3696
+ }
3697
+ }
3698
+ function chipTitle(sent) {
3699
+ const file = docsSectionFile(sent.section, sent.baseId, statuses);
3700
+ if (!file) {
3701
+ return sent.section.relPath;
3702
+ }
3703
+ return `${file.baseName}: ${file.root.replace(/[\\/]+$/, "")}/${file.relPath}`;
3704
+ }
3705
+ function updateChipTitles(turn) {
3706
+ for (const { node, sent } of turn.chips) {
3707
+ node.title = chipTitle(sent);
3708
+ }
3709
+ }
3710
+ function makeChip(turn, sent, status) {
3711
+ const label = docsCitationLabel(sent.section);
3712
+ const content = [el("span", "ai-chat-chip-n", String(sent.n)), el("span", "ai-chat-chip-label", label)];
3713
+ let node;
3714
+ if (opts.openFile) {
3715
+ node = button("ai-chat-chip", "", {});
3716
+ node.setAttribute("aria-label", `Open source ${sent.n}: ${label}`);
3717
+ node.addEventListener("click", () => {
3718
+ void openCitation(sent, status);
3719
+ });
3720
+ } else {
3721
+ node = el("span", "ai-chat-chip");
3722
+ }
3723
+ node.append(...content);
3724
+ node.title = chipTitle(sent);
3725
+ turn.chips.push({ node, sent });
3726
+ return node;
3727
+ }
3728
+ async function openCitation(sent, status) {
3729
+ let file = docsSectionFile(sent.section, sent.baseId, statuses);
3730
+ if (!file) {
3731
+ await loadStatuses();
3732
+ if (destroyed) {
3733
+ return;
3734
+ }
3735
+ file = docsSectionFile(sent.section, sent.baseId, statuses);
3736
+ }
3737
+ if (!file || !opts.openFile) {
3738
+ status.show(`Could not open ${sent.section.relPath}: its base is no longer set up.`, "error");
3739
+ return;
3740
+ }
3741
+ const slug = docsSectionSlug(sent.section);
3742
+ opts.openFile(docsFileHref(file.root, file.relPath), slug ? { section: slug } : void 0);
3743
+ }
3744
+ function renderAnswerInto(into, turn, answer, status) {
3745
+ const bySent = new Map(answer.sent.map((s) => [s.n, s]));
3746
+ const marks = [];
3747
+ let markdown = "";
3748
+ for (const part of answerParts(turn, answer)) {
3749
+ if (part.type === "text") {
3750
+ markdown += part.text.replace(/[]/g, "");
3751
+ continue;
3752
+ }
3753
+ part.ns.forEach((n) => {
3754
+ const sent = bySent.get(n);
3755
+ if (!sent) {
3756
+ return;
3757
+ }
3758
+ markdown += `${CHIP_OPEN}${marks.length}${CHIP_CLOSE}`;
3759
+ marks.push(sent);
3760
+ });
3761
+ }
3762
+ renderPreviewMarkdown(into, markdown.trim());
3763
+ const walker = document.createTreeWalker(into, NodeFilter.SHOW_TEXT);
3764
+ const texts = [];
3765
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
3766
+ if (CHIP_RE.test(node.data)) {
3767
+ texts.push(node);
3768
+ }
3769
+ }
3770
+ for (const text of texts) {
3771
+ const pieces = text.data.split(CHIP_RE);
3772
+ const out = [];
3773
+ pieces.forEach((piece, i) => {
3774
+ if (i % 2 === 1) {
3775
+ out.push(makeChip(turn, marks[Number(piece)], status));
3776
+ } else if (piece) {
3777
+ out.push(document.createTextNode(piece));
3778
+ }
3779
+ });
3780
+ text.replaceWith(...out);
3781
+ }
3782
+ }
3783
+ function renderNotice(body, level, text, actions = [], titleText) {
3784
+ const card = el("div", "ai-chat-notice");
3785
+ if (titleText) {
3786
+ card.append(el("div", "ai-chat-notice-title", titleText));
3787
+ }
3788
+ card.append(note(level, text));
3789
+ if (actions.length > 0) {
3790
+ const row = el("div", "ai-chat-actions");
3791
+ for (const action of actions) {
3792
+ const b = button("ai-chat-action", action.label, { fk: action.fk });
3793
+ b.addEventListener("click", () => action.run());
3794
+ row.append(b);
3795
+ }
3796
+ card.append(row);
3797
+ }
3798
+ body.append(card);
3799
+ }
3800
+ function renderAnswered(turn, answer) {
3801
+ const { body } = turn;
3802
+ const status = makeStatus();
3803
+ const text = el("div", "ai-chat-answer ai-rw-out");
3804
+ renderAnswerInto(text, turn, answer, status);
3805
+ body.append(text);
3806
+ const notes = answerNotes(turn, answer);
3807
+ if (notes.length > 0) {
3808
+ const box2 = el("div", "ai-chat-notes");
3809
+ for (const line of notes) {
3810
+ box2.append(el("div", "ai-chat-note", line));
3811
+ }
3812
+ body.append(box2);
3813
+ }
3814
+ const row = el("div", "ai-chat-actions");
3815
+ if (opts.canInsert()) {
3816
+ const insert = button("ai-chat-action", "Insert at cursor", { fk: "chat:insert" });
3817
+ insert.addEventListener("click", () => {
3818
+ const sources = turn.scope === "bases" ? answer.sent : [];
3819
+ const location = docsDocumentLocation(opts.documentFolder?.() ?? null, statuses, answer.sent.map((s) => s.baseId));
3820
+ const { markdown, policy } = prepareInsertMarkdown(answer.answer, sources, location);
3821
+ if (opts.insertAtCursor(markdown, policy)) {
3822
+ status.show("Inserted");
3823
+ } else {
3824
+ status.show("Could not insert here.", "error");
3825
+ }
3826
+ });
3827
+ row.append(insert);
3828
+ }
3829
+ const copy = button("ai-chat-action", "Copy", { fk: "chat:copy" });
3830
+ copy.addEventListener("click", () => {
3831
+ const one = exportTurn(turn, engineName);
3832
+ const sources = one.sources && one.sources.length > 0 ? `
3833
+
3834
+ ${one.sources.join("\n")}` : "";
3835
+ void copyText(`${one.answer ?? ""}${sources}`, status);
3836
+ });
3837
+ row.append(copy);
3838
+ const requestId = answer.run.requestId;
3839
+ if (requestId && opts.openActivity && opts.activityOn?.()) {
3840
+ const activity = button("ai-chat-link", "Activity", { fk: "chat:activity", title: "View this request in AI Activity" });
3841
+ activity.addEventListener("click", () => opts.openActivity?.(requestId));
3842
+ row.append(activity);
3843
+ }
3844
+ row.append(status.element, el("span", "ai-chat-spacer"));
3845
+ if (turn.route) {
3846
+ const pill = el("span", "ai-chat-route");
3847
+ pill.title = `${engineName(turn.route.engineId)} \xB7 ${turn.route.modelId}${turn.route.isLocal ? " (local)" : " (cloud)"}`;
3848
+ const icon2 = el("span", "ai-chat-route-icon");
3849
+ icon2.innerHTML = turn.route.isLocal ? ICONS.local : ICONS.cloud;
3850
+ pill.append(icon2, el("span", "ai-chat-route-name", turn.route.modelId));
3851
+ row.append(pill);
3852
+ }
3853
+ body.append(row);
3854
+ }
3855
+ function renderBody(turn) {
3856
+ const { body } = turn;
3857
+ body.replaceChildren();
3858
+ turn.chips = [];
3859
+ turn.live = null;
3860
+ if (turn.question === null) {
3861
+ turn.node.querySelector(".ai-chat-question")?.remove();
3862
+ }
3863
+ if (turn.running) {
3864
+ if (turn.partial) {
3865
+ turn.live = el("div", "ai-chat-answer ai-rw-out");
3866
+ renderPreviewMarkdown(turn.live, turn.partial);
3867
+ body.append(turn.live);
3868
+ }
3869
+ const line = el("div", "ai-chat-progress");
3870
+ line.append(el("span", "ai-chat-dot"), el("span", "", turn.progress));
3871
+ body.append(line);
3872
+ return;
3873
+ }
3874
+ const outcome = turn.outcome;
3875
+ if (!outcome) {
3876
+ return;
3877
+ }
3878
+ switch (outcome.kind) {
3879
+ case "answered":
3880
+ renderAnswered(turn, outcome);
3881
+ return;
3882
+ case "baseLocalOnly": {
3883
+ const onCloud = outcome.route ? `${engineName(outcome.route.engineId)} is a cloud engine, so` : "This route may not stay on this machine, so";
3884
+ const actions = [
3885
+ ...opts.openRouting ? [{ label: "Pick a local engine", fk: "chat:pick-local", run: () => opts.openRouting?.() }] : [],
3886
+ ...opts.openBases ? [{ label: "Open the base's switch", fk: "chat:base-switch", run: () => opts.openBases?.() }] : []
3887
+ ];
3888
+ renderNotice(body, "warning", `${onCloud} nothing was sent.`, actions, chatLocalOnlyTitle(outcome.bases));
3889
+ return;
3890
+ }
3891
+ case "noActiveBases":
3892
+ renderNotice(body, "info", AI_CHAT_NO_BASES, opts.openBases ? [{ label: "Manage bases\u2026", fk: "chat:manage-bases", run: () => opts.openBases?.() }] : []);
3893
+ return;
3894
+ case "questionHasSecret":
3895
+ renderNotice(body, "warning", AI_CHAT_SECRET_NOTICE);
3896
+ return;
3897
+ case "busy":
3898
+ renderNotice(body, "info", AI_CHAT_BUSY_NOTICE);
3899
+ return;
3900
+ case "cancelled":
3901
+ if (turn.partial) {
3902
+ const text = el("div", "ai-chat-answer ai-rw-out is-cancelled");
3903
+ renderPreviewMarkdown(text, turn.partial);
3904
+ body.append(text);
3905
+ }
3906
+ body.append(el("div", "ai-chat-note", "Cancelled"));
3907
+ return;
3908
+ case "error": {
3909
+ const actions = opts.openRouting ? [{ label: "Open engines", fk: "chat:engines", run: () => opts.openRouting?.() }] : [];
3910
+ renderNotice(body, "error", errorText(outcome.error, turn.route, engineName), actions);
3911
+ return;
3912
+ }
3913
+ }
3914
+ }
3915
+ function addTurn(question) {
3916
+ const node = el("article", "ai-chat-turn");
3917
+ const body = el("div", "ai-chat-reply");
3918
+ node.append(el("div", "ai-chat-question", question), body);
3919
+ const turn = {
3920
+ scope,
3921
+ startedAt: Date.now(),
3922
+ question,
3923
+ running: true,
3924
+ partial: "",
3925
+ progress: "Starting...",
3926
+ route: null,
3927
+ outcome: null,
3928
+ node,
3929
+ body,
3930
+ live: null,
3931
+ chips: []
3932
+ };
3933
+ turns.push(turn);
3934
+ log.append(node);
3935
+ renderBody(turn);
3936
+ return turn;
3937
+ }
3938
+ function flushStream(turn) {
3939
+ window.clearTimeout(streamTimer);
3940
+ streamTimer = void 0;
3941
+ if (!turn.running || turn !== current) {
3942
+ return;
3943
+ }
3944
+ const was = nearBottom();
3945
+ if (turn.live) {
3946
+ renderPreviewMarkdown(turn.live, turn.partial);
3947
+ } else {
3948
+ renderBody(turn);
3949
+ }
3950
+ scrollDown(was);
3951
+ }
3952
+ function finish(turn, outcome) {
3953
+ window.clearTimeout(streamTimer);
3954
+ streamTimer = void 0;
3955
+ const was = nearBottom();
3956
+ turn.running = false;
3957
+ turn.outcome = outcome;
3958
+ if (outcome.kind === "questionHasSecret") {
3959
+ if (input.value === "" && turn.question) {
3960
+ input.value = turn.question;
3961
+ }
3962
+ turn.question = null;
3963
+ }
3964
+ if (outcome.kind === "busy" && input.value === "" && turn.question) {
3965
+ input.value = turn.question;
3966
+ }
3967
+ if (outcome.kind === "answered") {
3968
+ turn.route = outcome.run.route ?? turn.route;
3969
+ turn.node.classList.add("ai-chat-turn--arrived");
3970
+ }
3971
+ renderBody(turn);
3972
+ updateChipTitles(turn);
3973
+ current = null;
3974
+ controller = null;
3975
+ if (outcome.kind === "noActiveBases") {
3976
+ void loadStatuses();
3977
+ }
3978
+ if (document.activeElement === cancelBtn) {
3979
+ input.focus();
3980
+ }
3981
+ renderComposerState();
3982
+ resizeInput();
3983
+ scrollDown(was);
3984
+ }
3985
+ function send() {
3986
+ const question = input.value.trim();
3987
+ if (question === "" || current !== null || opts.featureOffNotice()) {
3988
+ return;
3989
+ }
3990
+ const useBases = scope === "bases" && !!opts.docs;
3991
+ const turn = addTurn(question);
3992
+ current = turn;
3993
+ input.value = "";
3994
+ resizeInput();
3995
+ const abort = new AbortController();
3996
+ controller = abort;
3997
+ renderComposerState();
3998
+ log.scrollTop = log.scrollHeight;
3999
+ if (useBases) {
4000
+ void loadStatuses();
4001
+ }
4002
+ const options = opts.docsOptions?.();
4003
+ const docs = opts.docs;
4004
+ const request = {
4005
+ question,
4006
+ scope: useBases && docs ? { kind: "bases", docs, ...options ? { options } : {} } : { kind: "document", text: opts.documentText(), ...options ? { options } : {} },
4007
+ ...opts.thinking !== void 0 ? { thinking: opts.thinking } : {}
4008
+ };
4009
+ const live = () => !destroyed && current === turn;
4010
+ opts.chat.run(request, {
4011
+ onRoute(route) {
4012
+ if (live()) {
4013
+ turn.route = route;
4014
+ }
4015
+ },
4016
+ onProgress(progress) {
4017
+ if (!live()) {
4018
+ return;
4019
+ }
4020
+ turn.progress = chatProgressLine(progress, turn.scope);
4021
+ const was = nearBottom();
4022
+ renderBody(turn);
4023
+ scrollDown(was);
4024
+ },
4025
+ onToken(_delta, text) {
4026
+ if (!live()) {
4027
+ return;
4028
+ }
4029
+ turn.partial = rewriteVisibleText(text).trimStart();
4030
+ if (streamTimer === void 0) {
4031
+ streamTimer = window.setTimeout(() => flushStream(turn), STREAM_THROTTLE_MS);
4032
+ }
4033
+ }
4034
+ }, abort.signal).then(
4035
+ (outcome) => {
4036
+ if (live()) {
4037
+ finish(turn, outcome);
4038
+ }
4039
+ },
4040
+ (err) => {
4041
+ if (live()) {
4042
+ finish(turn, { kind: "error", error: toAiError(err) });
4043
+ }
4044
+ }
4045
+ );
4046
+ }
4047
+ function cancel() {
4048
+ if (!current) {
4049
+ return;
4050
+ }
4051
+ controller?.abort();
4052
+ finish(current, { kind: "cancelled" });
4053
+ }
4054
+ async function copyText(text, status) {
4055
+ try {
4056
+ await navigator.clipboard.writeText(text);
4057
+ status.show("Copied");
4058
+ } catch {
4059
+ status.show("Could not copy.", "error");
4060
+ }
4061
+ }
4062
+ const exportAll = () => formatChatExport(turns.map((turn) => exportTurn(turn, engineName)));
4063
+ copyAllBtn.addEventListener("click", () => {
4064
+ void copyText(exportAll(), headerStatus);
4065
+ });
4066
+ saveBtn.addEventListener("click", () => {
4067
+ Promise.resolve(opts.saveTextFile?.(chatExportFileName(), exportAll())).catch(() => {
4068
+ if (!destroyed) {
4069
+ headerStatus.show("Could not save.", "error");
4070
+ }
4071
+ });
4072
+ });
4073
+ function resizeInput() {
4074
+ input.style.height = "auto";
4075
+ if (input.scrollHeight > 0) {
4076
+ input.style.height = `${input.scrollHeight}px`;
4077
+ }
4078
+ }
4079
+ input.addEventListener("input", () => {
4080
+ resizeInput();
4081
+ sendBtn.disabled = !!opts.featureOffNotice() || input.value.trim() === "";
4082
+ });
4083
+ input.addEventListener("keydown", (e) => {
4084
+ if (e.key !== "Enter" || e.shiftKey || e.isComposing) {
4085
+ return;
4086
+ }
4087
+ e.preventDefault();
4088
+ send();
4089
+ });
4090
+ sendBtn.addEventListener("click", () => send());
4091
+ cancelBtn.addEventListener("click", () => cancel());
4092
+ closeBtn.addEventListener("click", () => opts.onClose());
4093
+ root.addEventListener("keydown", (e) => {
4094
+ if (e.key !== "Escape") {
4095
+ return;
4096
+ }
4097
+ e.stopPropagation();
4098
+ if (current) {
4099
+ cancel();
4100
+ } else {
4101
+ opts.onEscape?.();
4102
+ }
4103
+ });
4104
+ container.append(root);
4105
+ setScope("document");
4106
+ void loadStatuses();
4107
+ return {
4108
+ destroy() {
4109
+ if (destroyed) {
4110
+ return;
4111
+ }
4112
+ destroyed = true;
4113
+ controller?.abort();
4114
+ controller = null;
4115
+ current = null;
4116
+ window.clearTimeout(streamTimer);
4117
+ for (const dispose of statusDisposers) {
4118
+ dispose();
4119
+ }
4120
+ statusDisposers.clear();
4121
+ turns.length = 0;
4122
+ container.replaceChildren();
4123
+ },
4124
+ focus() {
4125
+ if (input.disabled) {
4126
+ closeBtn.focus();
4127
+ } else {
4128
+ input.focus();
4129
+ }
4130
+ },
4131
+ refresh() {
4132
+ if (destroyed) {
4133
+ return;
4134
+ }
4135
+ void loadStatuses();
4136
+ for (const turn of turns) {
4137
+ if (!turn.running) {
4138
+ renderBody(turn);
4139
+ }
4140
+ }
4141
+ renderComposerState();
4142
+ }
4143
+ };
4144
+ }
4145
+
4146
+ // src/ai-rewrite.ts
4147
+ import { elementFromString, posToDOMRect } from "@tiptap/core";
4148
+ import { TextSelection } from "@tiptap/pm/state";
4149
+ import { DOMParser as PmDOMParser, Fragment as Fragment2 } from "@tiptap/pm/model";
4150
+ import { closeHistory as closeHistory2 } from "@tiptap/pm/history";
4151
+ import { EditorView as CmEditorView } from "@codemirror/view";
4152
+ import { isolateHistory } from "@codemirror/commands";
4153
+
4154
+ // src/ai-insert-guard.ts
4155
+ import { Fragment } from "@tiptap/pm/model";
4156
+ var INERT_NODES = /* @__PURE__ */ new Set([
4157
+ "blockquote",
4158
+ "bulletList",
4159
+ "callout",
4160
+ "codeBlock",
4161
+ "emoji",
4162
+ "footnoteDef",
4163
+ "footnoteRef",
4164
+ "hardBreak",
4165
+ "heading",
4166
+ "horizontalRule",
4167
+ "listItem",
4168
+ "mathBlock",
4169
+ "mathInline",
4170
+ "orderedList",
4171
+ "paragraph",
4172
+ "table",
4173
+ "tableCell",
4174
+ "tableHeader",
4175
+ "tableRow",
4176
+ "taskItem",
4177
+ "taskList",
4178
+ "text"
4179
+ ]);
4180
+ var INERT_MARKS = /* @__PURE__ */ new Set(["bold", "code", "highlight", "italic", "strike", "underline"]);
4181
+ function insertUrlCheck(policy, md) {
4182
+ const citations = new Set(md ? policy.citationHrefs.map((h) => md.normalizeLink(h)) : policy.citationHrefs);
4183
+ const verbatim = (url, kind) => insertUrlIsVerbatim(policy, url, kind) || !!md && insertUrlIsVerbatim(policy, md.normalizeLinkText(url), kind);
4184
+ return {
4185
+ link: (href) => citations.has(href) || verbatim(href, "link"),
4186
+ image: (src) => verbatim(src, "image")
4187
+ };
4188
+ }
4189
+ function plainText(node) {
4190
+ if (node.type.name === "image") {
4191
+ return [node.attrs.alt, node.attrs.src].filter(Boolean).join(" ");
4192
+ }
4193
+ const attrs = node.attrs;
4194
+ return (node.textContent || String(attrs.alias ?? attrs.target ?? attrs.body ?? "")).replace(/\s+/g, " ").trim();
4195
+ }
4196
+ var InvalidContent = class extends Error {
4197
+ };
4198
+ function guardInsertFragment(fragment, check) {
4199
+ const dropped = [];
4200
+ const keepMarks = (marks) => {
4201
+ const kept = marks.filter((mark) => {
4202
+ if (INERT_MARKS.has(mark.type.name)) {
4203
+ return true;
4204
+ }
4205
+ if (mark.type.name === "link") {
4206
+ const href = String(mark.attrs.href ?? "");
4207
+ if (check.link(href)) {
4208
+ return true;
4209
+ }
4210
+ dropped.push(href);
4211
+ return false;
4212
+ }
4213
+ dropped.push(`mark:${mark.type.name}`);
4214
+ return false;
4215
+ });
4216
+ return kept.length === marks.length ? marks : kept;
4217
+ };
4218
+ const asPlain = (node, marks) => {
4219
+ const { schema } = node.type;
4220
+ const text = plainText(node);
4221
+ if (node.isInline) {
4222
+ return text ? [schema.text(text, marks)] : [];
4223
+ }
4224
+ return text ? [schema.nodes.paragraph.create(null, schema.text(text))] : [];
4225
+ };
4226
+ const walkNode = (node) => {
4227
+ const name = node.type.name;
4228
+ const marks = keepMarks(node.marks);
4229
+ if (name === "image") {
4230
+ const src = String(node.attrs.src ?? "");
4231
+ if (check.image(src)) {
4232
+ return [marks === node.marks ? node : node.mark(marks)];
4233
+ }
4234
+ dropped.push(src);
4235
+ return asPlain(node, marks);
4236
+ }
4237
+ if (!INERT_NODES.has(name)) {
4238
+ dropped.push(`node:${name}:${JSON.stringify(node.attrs)}`);
4239
+ return asPlain(node, marks);
4240
+ }
4241
+ if (node.isText) {
4242
+ return [marks === node.marks ? node : node.mark(marks)];
4243
+ }
4244
+ const kids = walkFragment(node.content);
4245
+ if (kids !== node.content && !node.type.validContent(kids)) {
4246
+ throw new InvalidContent();
4247
+ }
4248
+ if (kids === node.content && marks === node.marks) {
4249
+ return [node];
4250
+ }
4251
+ return [node.type.create(node.attrs, kids, marks)];
4252
+ };
4253
+ const walkFragment = (content) => {
4254
+ const out = [];
4255
+ let same = true;
4256
+ content.forEach((child) => {
4257
+ const next = walkNode(child);
4258
+ if (next.length !== 1 || next[0] !== child) {
4259
+ same = false;
4260
+ }
4261
+ out.push(...next);
4262
+ });
4263
+ return same ? content : Fragment.fromArray(out);
4264
+ };
4265
+ try {
4266
+ return { fragment: walkFragment(fragment), dropped };
4267
+ } catch (err) {
4268
+ if (err instanceof InvalidContent) {
4269
+ return null;
4270
+ }
4271
+ throw err;
4272
+ }
4273
+ }
4274
+ function offenders(markdown, inspector) {
4275
+ const { check, md } = inspector;
4276
+ if (!md) {
4277
+ return null;
4278
+ }
4279
+ const guarded = guardInsertFragment(inspector.fragment(markdown), check);
4280
+ if (!guarded) {
4281
+ return null;
4282
+ }
4283
+ const found = new Set(guarded.dropped);
4284
+ const env = {};
4285
+ const visit = (tokens) => {
4286
+ for (const token of tokens) {
4287
+ if (token.type === "link_open") {
4288
+ const href = token.attrGet("href") ?? "";
4289
+ if (!check.link(href)) {
4290
+ found.add(href);
4291
+ }
4292
+ } else if (token.type === "image") {
4293
+ const src = token.attrGet("src") ?? "";
4294
+ if (!check.image(src)) {
4295
+ found.add(src);
4296
+ }
4297
+ }
4298
+ if (token.children) {
4299
+ visit(token.children);
4300
+ }
4301
+ }
4302
+ };
4303
+ visit(md.parse(markdown, env));
4304
+ for (const def of Object.values(env.references ?? {})) {
4305
+ const href = def.href ?? "";
4306
+ if (!check.link(href) && !check.image(href)) {
4307
+ found.add(href);
4308
+ }
4309
+ }
4310
+ return found;
4311
+ }
4312
+ function escapeLinkSyntax(text) {
4313
+ return text.replace(/[\\[\]!<`]/g, "\\$&");
4314
+ }
4315
+ function escapeAllPunctuation(text) {
4316
+ return text.replace(/[!-/:-@[-`{-~]/g, "\\$&");
4317
+ }
4318
+ function escapeAffectedParts(markdown, clean) {
4319
+ return markdown.split(/(\n[ \t]*\n)/).map((part, i) => i % 2 === 1 || clean(part) ? part : escapeLinkSyntax(part)).join("");
4320
+ }
4321
+ function guardSourceMarkdown(markdown, inspector, placed) {
4322
+ const cleanAlone = (text) => offenders(text, inspector)?.size === 0;
4323
+ let existing = null;
4324
+ const cleanInPlace = (text) => {
4325
+ if (!placed) {
4326
+ return true;
4327
+ }
4328
+ const { before, after } = placed(text);
4329
+ existing ?? (existing = offenders(before, inspector));
4330
+ const now = offenders(after, inspector);
4331
+ if (!existing || !now) {
4332
+ return false;
4333
+ }
4334
+ for (const url of now) {
4335
+ if (!existing.has(url)) {
4336
+ return false;
4337
+ }
4338
+ }
4339
+ return true;
4340
+ };
4341
+ const candidates = [
4342
+ () => markdown,
4343
+ () => escapeAffectedParts(markdown, cleanAlone),
4344
+ () => escapeLinkSyntax(markdown),
4345
+ () => escapeAllPunctuation(markdown)
4346
+ ];
4347
+ for (const candidate of candidates) {
4348
+ const text = candidate();
4349
+ if (cleanAlone(text) && cleanInPlace(text)) {
4350
+ return text;
4351
+ }
4352
+ }
4353
+ return null;
4354
+ }
4355
+
1371
4356
  // src/ai-rewrite.ts
1372
4357
  var nextTargetId = 0;
1373
4358
  function serializeFragment(editor, fragment) {
@@ -1386,7 +4371,7 @@ function serializeRange(editor, doc, from, to) {
1386
4371
  const parent = doc.resolve(from).parent;
1387
4372
  const paragraph = editor.state.schema.nodes.paragraph;
1388
4373
  if (content.firstChild?.isInline && !parent.type.spec.code && paragraph) {
1389
- return serializeFragment(editor, Fragment.from(paragraph.create(null, content)));
4374
+ return serializeFragment(editor, Fragment2.from(paragraph.create(null, content)));
1390
4375
  }
1391
4376
  return serializeFragment(editor, content);
1392
4377
  }
@@ -1397,6 +4382,10 @@ function parseBlocks(editor, markdown) {
1397
4382
  const html = markdownStorage(editor)?.parser?.parse(markdown) ?? markdown;
1398
4383
  return PmDOMParser.fromSchema(editor.state.schema).parse(elementFromString(html)).content;
1399
4384
  }
4385
+ function sourceInspector(editor, policy) {
4386
+ const md = markdownStorage(editor)?.parser?.md;
4387
+ return { check: insertUrlCheck(policy, md), fragment: (markdown) => parseBlocks(editor, markdown), md };
4388
+ }
1400
4389
  function blockquoteMarkdown(text) {
1401
4390
  return text.trim().split("\n").map((line) => line.trim() ? `> ${line}` : ">").join("\n");
1402
4391
  }
@@ -1486,7 +4475,7 @@ function richRewriteTarget(editor) {
1486
4475
  from2 = $a.before();
1487
4476
  to2 = $b.after();
1488
4477
  }
1489
- const tr = closeHistory(current.tr).replaceRange(from2, to2, slice);
4478
+ const tr = closeHistory2(current.tr).replaceRange(from2, to2, slice);
1490
4479
  tr.setMeta(aiRewriteHighlightKey, { range: null });
1491
4480
  const end = tr.mapping.map(to2, 1);
1492
4481
  try {
@@ -1508,7 +4497,7 @@ function richRewriteTarget(editor) {
1508
4497
  }
1509
4498
  const $to2 = current.doc.resolve(r.to);
1510
4499
  const pos = $to2.depth > 0 ? $to2.after(1) : r.to;
1511
- editor.view.dispatch(closeHistory(current.tr).insert(pos, content).scrollIntoView());
4500
+ editor.view.dispatch(closeHistory2(current.tr).insert(pos, content).scrollIntoView());
1512
4501
  return true;
1513
4502
  },
1514
4503
  release() {
@@ -1643,7 +4632,7 @@ function richDocumentTarget(editor, opts = {}) {
1643
4632
  if (!content.size) {
1644
4633
  return false;
1645
4634
  }
1646
- const tr = closeHistory(current.tr).replaceWith(0, current.doc.content.size, content);
4635
+ const tr = closeHistory2(current.tr).replaceWith(0, current.doc.content.size, content);
1647
4636
  tr.setMeta(aiRewriteHighlightKey, { range: null });
1648
4637
  tr.setSelection(TextSelection.atStart(tr.doc));
1649
4638
  editor.view.dispatch(tr.scrollIntoView());
@@ -1734,17 +4723,24 @@ function cursorInsertTarget(editor, slash) {
1734
4723
  return null;
1735
4724
  }
1736
4725
  },
1737
- accept(markdown) {
4726
+ accept(markdown, policy) {
1738
4727
  const r = range();
1739
4728
  if (!r || editor.isDestroyed) {
1740
4729
  return false;
1741
4730
  }
1742
- const content = parseBlocks(editor, markdown);
4731
+ let content = parseBlocks(editor, markdown);
4732
+ if (policy) {
4733
+ const guarded = guardInsertFragment(content, insertUrlCheck(policy, markdownStorage(editor)?.parser?.md));
4734
+ if (!guarded) {
4735
+ return false;
4736
+ }
4737
+ content = guarded.fragment;
4738
+ }
1743
4739
  if (!content.size) {
1744
4740
  return false;
1745
4741
  }
1746
4742
  const current = editor.state;
1747
- const tr = closeHistory(current.tr);
4743
+ const tr = closeHistory2(current.tr);
1748
4744
  let at = r.from;
1749
4745
  if (r.to > r.from) {
1750
4746
  const $q = current.doc.resolve(r.from);
@@ -1754,6 +4750,9 @@ function cursorInsertTarget(editor, slash) {
1754
4750
  tr.delete(at, r.to);
1755
4751
  }
1756
4752
  tr.setMeta(aiRewriteHighlightKey, { range: null });
4753
+ if (policy) {
4754
+ tr.setMeta("preventAutolink", true);
4755
+ }
1757
4756
  const $at = tr.doc.resolve(Math.min(at, tr.doc.content.size));
1758
4757
  let from = $at.pos;
1759
4758
  let to = $at.pos;
@@ -1786,7 +4785,7 @@ function cursorInsertTarget(editor, slash) {
1786
4785
  }
1787
4786
  };
1788
4787
  }
1789
- function sourceCursorTarget(view, bodyStart) {
4788
+ function sourceCursorTarget(view, bodyStart, parserEditor) {
1790
4789
  const id = ++nextTargetId;
1791
4790
  const head = view.state.selection.main.head;
1792
4791
  view.dispatch({ effects: setCmRewriteRange.of({ from: head, to: head, id, anchor: true }) });
@@ -1811,9 +4810,9 @@ function sourceCursorTarget(view, bodyStart) {
1811
4810
  const c = r ? view.coordsAtPos(r.from) : null;
1812
4811
  return c ? rectUnion(c, c) : null;
1813
4812
  },
1814
- accept(markdown) {
4813
+ accept(markdown, policy) {
1815
4814
  const r = range();
1816
- const md = markdown.trim();
4815
+ let md = markdown.trim();
1817
4816
  if (!r || !md) {
1818
4817
  return false;
1819
4818
  }
@@ -1823,18 +4822,31 @@ function sourceCursorTarget(view, bodyStart) {
1823
4822
  const prev = line.number > 1 ? doc.line(line.number - 1) : null;
1824
4823
  const next = line.number < doc.lines ? doc.line(line.number + 1) : null;
1825
4824
  const textAfter = !!next && !!next.text.trim();
1826
- let changes;
1827
- let caret;
1828
- if (!line.text.trim()) {
1829
- const lead = prev && prev.text.trim() && line.from >= start ? "\n" : "";
1830
- changes = { from: line.from, to: line.to, insert: `${lead}${md}${textAfter ? "\n" : ""}` };
1831
- caret = line.from + lead.length + md.length;
1832
- } else {
1833
- changes = { from: line.to, insert: `
4825
+ const place = (text) => {
4826
+ if (!line.text.trim()) {
4827
+ const lead = prev && prev.text.trim() && line.from >= start ? "\n" : "";
4828
+ return { changes: { from: line.from, to: line.to, insert: `${lead}${text}${textAfter ? "\n" : ""}` }, caret: line.from + lead.length + text.length };
4829
+ }
4830
+ return { changes: { from: line.to, insert: `
1834
4831
 
1835
- ${md}${textAfter ? "\n" : ""}` };
1836
- caret = line.to + 2 + md.length;
4832
+ ${text}${textAfter ? "\n" : ""}` }, caret: line.to + 2 + text.length };
4833
+ };
4834
+ if (policy) {
4835
+ if (!parserEditor || parserEditor.isDestroyed) {
4836
+ return false;
4837
+ }
4838
+ const before = doc.toString();
4839
+ const guarded = guardSourceMarkdown(md, sourceInspector(parserEditor, policy), (text) => {
4840
+ const { changes: changes2 } = place(text);
4841
+ const after = before.slice(0, changes2.from) + changes2.insert + before.slice(changes2.to ?? changes2.from);
4842
+ return { before: before.slice(start), after: after.slice(bodyStart ? bodyStart(after) : 0) };
4843
+ });
4844
+ if (guarded === null) {
4845
+ return false;
4846
+ }
4847
+ md = guarded;
1837
4848
  }
4849
+ const { changes, caret } = place(md);
1838
4850
  view.dispatch({
1839
4851
  changes,
1840
4852
  selection: { anchor: caret },
@@ -1890,7 +4902,7 @@ function applyRichTitle(editor, effect, title, remove) {
1890
4902
  if (!heading) {
1891
4903
  return false;
1892
4904
  }
1893
- const tr = closeHistory(editor.state.tr);
4905
+ const tr = closeHistory2(editor.state.tr);
1894
4906
  if (remove && remove.to > remove.from) {
1895
4907
  tr.delete(remove.from, remove.to);
1896
4908
  }
@@ -3305,14 +6317,18 @@ ${untranslated.trimStart()}`) : t.accept(finalText);
3305
6317
  };
3306
6318
  }
3307
6319
  export {
6320
+ AI_ACTIVITY_LIMIT,
3308
6321
  AI_ENGINE_PRESETS,
3309
6322
  AI_TURN_ON_PATCH,
3310
6323
  CHARS_PER_TOKEN,
3311
6324
  DEFAULT_CONTEXT_TOKENS,
3312
6325
  PROPERTY_VOCABULARY_MAX,
6326
+ activityExportFileName,
6327
+ aiActivityStatus,
3313
6328
  aiConsentHost,
3314
6329
  aiErrorCopy,
3315
6330
  aiFeatureAvailability,
6331
+ aiFeatureOffNotice,
3316
6332
  aiIndicatorState,
3317
6333
  aiLengthLimitCopy,
3318
6334
  aiNextRouteChanged,
@@ -3326,11 +6342,19 @@ export {
3326
6342
  budgetDocument,
3327
6343
  buildAiSettingsSection,
3328
6344
  buildPropertiesMessages,
6345
+ createAiActivityLog,
6346
+ createAiDocsChat,
3329
6347
  createAiRewritePopover,
6348
+ createDocsBasesSection,
6349
+ createGhostSession,
3330
6350
  cursorInsertTarget,
3331
6351
  detectLocalServers,
6352
+ docsHeadingAt,
3332
6353
  engineDisplayName,
3333
6354
  experimentalBadge,
6355
+ formatActivityExport,
6356
+ ghostAvailability,
6357
+ ghostFeatureOn,
3334
6358
  headingsOutline,
3335
6359
  hideAiFirstRunPanel,
3336
6360
  hostOfUrl,
@@ -3340,6 +6364,8 @@ export {
3340
6364
  jsonRepairMessages,
3341
6365
  loadThinkingLevel,
3342
6366
  makeAiError,
6367
+ mountAiActivityPanel,
6368
+ mountAiChatPanel,
3343
6369
  mountAiEnginesPage,
3344
6370
  parseAiSettings,
3345
6371
  parsePropertySuggestions,
@@ -3364,7 +6390,8 @@ export {
3364
6390
  thinkingLevelFor,
3365
6391
  toAiError,
3366
6392
  whatIsSent,
6393
+ withGhostFeature,
3367
6394
  withSavedAiKeys,
3368
6395
  yamlHasTitle
3369
6396
  };
3370
- //# sourceMappingURL=ai-ui-C7ENTWWU.js.map
6397
+ //# sourceMappingURL=ai-ui-QFYFN73D.js.map