@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.
- package/CHANGELOG.md +12 -1
- package/README.md +1 -1
- package/dist/ai/engines-page.d.ts +11 -4
- package/dist/ai/engines-page.js +5 -4
- package/dist/ai/index.d.ts +1201 -10
- package/dist/ai/index.js +297 -8
- package/dist/{ai-ui-C7ENTWWU.js → ai-ui-QFYFN73D.js} +3165 -138
- package/dist/ai-ui-QFYFN73D.js.map +1 -0
- package/dist/{chunk-QDVY7WW6.js → chunk-APPBNQEY.js} +53 -2
- package/dist/chunk-APPBNQEY.js.map +1 -0
- package/dist/{chunk-7A5HEPLK.js → chunk-DWXGWVA3.js} +850 -12
- package/dist/chunk-DWXGWVA3.js.map +1 -0
- package/dist/{chunk-JZWKEBCI.js → chunk-IWCXWUWW.js} +3260 -201
- package/dist/chunk-IWCXWUWW.js.map +1 -0
- package/dist/{chunk-LXZG7IKN.js → chunk-N4N54LK2.js} +2 -2
- package/dist/chunk-N4N54LK2.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +399 -38
- package/dist/index.js.map +1 -1
- package/dist/schema-DVfssxQX.d.ts +100 -0
- package/dist/styles.css +943 -4
- package/dist/{types-CsVNVSNY.d.ts → types-2TZErbJr.d.ts} +25 -3
- package/dist/{types-CgEeIUhi.d.ts → types-DO8UBybZ.d.ts} +316 -7
- package/package.json +1 -1
- package/dist/ai-ui-C7ENTWWU.js.map +0 -1
- package/dist/chunk-7A5HEPLK.js.map +0 -1
- package/dist/chunk-JZWKEBCI.js.map +0 -1
- package/dist/chunk-LXZG7IKN.js.map +0 -1
- package/dist/chunk-QDVY7WW6.js.map +0 -1
- package/dist/schema-DYzxkuSb.d.ts +0 -46
|
@@ -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-
|
|
39
|
-
import "./chunk-
|
|
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
|
-
|
|
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-
|
|
127
|
+
} from "./chunk-IWCXWUWW.js";
|
|
101
128
|
import {
|
|
129
|
+
DEFAULT_CONTEXT_TOKENS,
|
|
102
130
|
hostOfUrl
|
|
103
|
-
} from "./chunk-
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
var
|
|
1116
|
-
var
|
|
1117
|
-
var
|
|
1118
|
-
var
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
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
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
-
|
|
1594
|
+
if (completionTokens !== void 0) {
|
|
1595
|
+
parts.push(`${completionTokens} out`);
|
|
1596
|
+
}
|
|
1597
|
+
return parts.join(" \xB7 ");
|
|
1159
1598
|
}
|
|
1160
|
-
function
|
|
1161
|
-
|
|
1162
|
-
|
|
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
|
-
|
|
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
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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
|
-
${
|
|
1836
|
-
|
|
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 =
|
|
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-
|
|
6397
|
+
//# sourceMappingURL=ai-ui-QFYFN73D.js.map
|