@firstpick/pi-package-webui 0.5.3 → 0.5.5
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/README.md +1 -1
- package/bin/pi-webui.mjs +12 -3
- package/package.json +3 -2
- package/public/app.js +849 -45
- package/public/index.html +2 -2
- package/public/styles.css +307 -1
- package/tests/http-endpoints-harness.test.mjs +15 -1
- package/tests/mobile-static.test.mjs +38 -5
package/public/app.js
CHANGED
|
@@ -397,7 +397,12 @@ let footerThinkingPickerOpen = false;
|
|
|
397
397
|
let footerAutoCompactionToggleInFlight = false;
|
|
398
398
|
let footerBranchPickerOpen = false;
|
|
399
399
|
let footerBranchPickerState = { loading: false, error: "", branches: [], current: "", root: "", switching: "", tabId: null };
|
|
400
|
+
let footerBranchCreateDraft = { type: "", name: "" };
|
|
400
401
|
let footerBranchPickerRequestSerial = 0;
|
|
402
|
+
let footerScopedModelDragKey = "";
|
|
403
|
+
let footerScopedModelLastDragOverKey = "";
|
|
404
|
+
let footerScopedModelPointerDrag = null;
|
|
405
|
+
let footerScopedModelSuppressClickUntil = 0;
|
|
401
406
|
let publishMenuOpen = false;
|
|
402
407
|
let maxVisualViewportHeight = 0;
|
|
403
408
|
let abortRequestInFlight = false;
|
|
@@ -437,6 +442,8 @@ const SKILL_USAGE_STORAGE_KEY = "pi-webui-skill-usage-v1";
|
|
|
437
442
|
const TERMINAL_TABS_LAYOUT_STORAGE_KEY = "pi-webui-terminal-tabs-layout";
|
|
438
443
|
const TERMINAL_CUSTOM_GROUPS_STORAGE_KEY = "pi-webui-terminal-custom-groups-v1";
|
|
439
444
|
const TERMINAL_TAB_DRAG_MIME = "application/x-pi-terminal-tab-id";
|
|
445
|
+
const FOOTER_SCOPED_MODEL_ORDER_STORAGE_KEY = "pi-webui-footer-scoped-model-order-v1";
|
|
446
|
+
const FOOTER_SCOPED_MODEL_POINTER_DRAG_THRESHOLD_PX = 6;
|
|
440
447
|
const TOOL_OUTPUT_EXPANDED_STORAGE_KEY = "pi-webui-tool-output-expanded";
|
|
441
448
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
442
449
|
const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background";
|
|
@@ -516,6 +523,9 @@ const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
|
|
|
516
523
|
const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
|
|
517
524
|
const TOOL_LIVE_UPDATE_THROTTLE_MS = 80;
|
|
518
525
|
const UNEXPOSED_THINKING_TEXT = "No thinking content was exposed by the provider.";
|
|
526
|
+
const THINKING_FORMAT_OPEN_TAG_REGEX = /^<think\b[^>]*>/i;
|
|
527
|
+
const THINKING_FORMAT_CLOSE_TAG_REGEX = /<\/think\s*>/i;
|
|
528
|
+
const CHANNEL_THINKING_FORMAT_OPEN_TAG_REGEX = /^<\|([a-z][\w-]*)>/i;
|
|
519
529
|
const TODO_PROGRESS_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)\]\s+.+$/;
|
|
520
530
|
const TODO_PROGRESS_PARTIAL_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)?\]?\s*.*$/;
|
|
521
531
|
const CHAT_SCROLL_KEYS = new Set(["ArrowDown", "ArrowUp", "End", "Home", "PageDown", "PageUp", " "]);
|
|
@@ -531,6 +541,7 @@ const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
|
531
541
|
const sidePanelOverlayMedia = window.matchMedia?.(SIDE_PANEL_OVERLAY_QUERY) || mobileViewMedia;
|
|
532
542
|
const statusEntries = new Map();
|
|
533
543
|
const widgets = new Map();
|
|
544
|
+
const widgetsByTab = new Map();
|
|
534
545
|
const todoProgressWidgetExpandedByTab = new Map();
|
|
535
546
|
const releaseNpmOutputExpandedByTab = new Map();
|
|
536
547
|
const appRunnerDataByTab = new Map();
|
|
@@ -884,6 +895,7 @@ const GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP = [
|
|
|
884
895
|
"5. Return here to commit short, long, or typed input on that branch.",
|
|
885
896
|
"6. Push and Create PR will push upstream, run /pr, let you review, then run gh pr create.",
|
|
886
897
|
].join("\n");
|
|
898
|
+
const GIT_BRANCH_TYPE_SUGGESTIONS = ["feat", "fix", "change", "perf", "test", "chore", "refactor", "docs", "style", "build", "ci", "revert"];
|
|
887
899
|
const GIT_FOOTER_STATUS_SETUP_TOOLTIP = [
|
|
888
900
|
"git-footer-status-setup:",
|
|
889
901
|
"Store the GitHub username used when the Web UI initializes a no-repo directory.",
|
|
@@ -915,6 +927,12 @@ const GIT_INIT_STACK_TOOLTIP = [
|
|
|
915
927
|
"Choose a known stack or type one. The value is saved in this browser.",
|
|
916
928
|
"If left blank, Pi will inspect the codebase and fall back to sane default .gitignore patterns.",
|
|
917
929
|
].join("\n");
|
|
930
|
+
const MERMAID_MODULE_URL = "/vendor/mermaid/mermaid.esm.min.mjs";
|
|
931
|
+
const MERMAID_LANGUAGES = new Set(["mermaid", "mmd"]);
|
|
932
|
+
const MERMAID_MAX_TEXT_SIZE = 100_000;
|
|
933
|
+
let mermaidModulePromise = null;
|
|
934
|
+
let mermaidThemeSignature = "";
|
|
935
|
+
let mermaidRenderSequence = 0;
|
|
918
936
|
|
|
919
937
|
function make(tag, className, text) {
|
|
920
938
|
const node = document.createElement(tag);
|
|
@@ -4445,6 +4463,7 @@ function syncTabMetadata(nextTabs = []) {
|
|
|
4445
4463
|
actionFeedbackByTab.delete(tabId);
|
|
4446
4464
|
skillUsageByTab.delete(tabId);
|
|
4447
4465
|
tabMessagesCache.delete(tabId);
|
|
4466
|
+
widgetsByTab.delete(tabId);
|
|
4448
4467
|
clearGitWorkflowForTab(tabId);
|
|
4449
4468
|
}
|
|
4450
4469
|
}
|
|
@@ -4707,6 +4726,55 @@ function cancelPendingDialogs() {
|
|
|
4707
4726
|
if (elements.dialog.open) elements.dialog.close();
|
|
4708
4727
|
}
|
|
4709
4728
|
|
|
4729
|
+
function widgetCacheForTab(tabId = activeTabId, { create = true } = {}) {
|
|
4730
|
+
if (!tabId) return null;
|
|
4731
|
+
let cache = widgetsByTab.get(tabId);
|
|
4732
|
+
if (!cache && create) {
|
|
4733
|
+
cache = new Map();
|
|
4734
|
+
widgetsByTab.set(tabId, cache);
|
|
4735
|
+
}
|
|
4736
|
+
return cache || null;
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4739
|
+
function cacheWidgetsForTab(tabId = activeTabId) {
|
|
4740
|
+
if (!tabId) return;
|
|
4741
|
+
if (widgets.size === 0) {
|
|
4742
|
+
widgetsByTab.delete(tabId);
|
|
4743
|
+
return;
|
|
4744
|
+
}
|
|
4745
|
+
widgetsByTab.set(tabId, new Map(widgets));
|
|
4746
|
+
}
|
|
4747
|
+
|
|
4748
|
+
function restoreWidgetsForActiveTab() {
|
|
4749
|
+
widgets.clear();
|
|
4750
|
+
const cache = widgetCacheForTab(activeTabId, { create: false });
|
|
4751
|
+
if (!cache) return;
|
|
4752
|
+
for (const [key, value] of cache) widgets.set(key, value);
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4755
|
+
function setWidgetForTab(tabId, widgetKey, request) {
|
|
4756
|
+
if (!widgetKey) return;
|
|
4757
|
+
const targetTabId = tabId || activeTabId;
|
|
4758
|
+
const cache = widgetCacheForTab(targetTabId);
|
|
4759
|
+
const hasLines = Array.isArray(request?.widgetLines);
|
|
4760
|
+
|
|
4761
|
+
if (cache) {
|
|
4762
|
+
if (hasLines) cache.set(widgetKey, request);
|
|
4763
|
+
else cache.delete(widgetKey);
|
|
4764
|
+
if (cache.size === 0) widgetsByTab.delete(targetTabId);
|
|
4765
|
+
}
|
|
4766
|
+
|
|
4767
|
+
if (targetTabId === activeTabId) {
|
|
4768
|
+
if (hasLines) widgets.set(widgetKey, request);
|
|
4769
|
+
else widgets.delete(widgetKey);
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
|
|
4773
|
+
function clearWidgetsForTab(tabId = activeTabId) {
|
|
4774
|
+
if (tabId) widgetsByTab.delete(tabId);
|
|
4775
|
+
if (!tabId || tabId === activeTabId) widgets.clear();
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4710
4778
|
function resetActiveTabUi() {
|
|
4711
4779
|
clearRefreshTimers();
|
|
4712
4780
|
clearLiveToolRenderQueue();
|
|
@@ -4724,7 +4792,7 @@ function resetActiveTabUi() {
|
|
|
4724
4792
|
latestMessagesSessionKey = "";
|
|
4725
4793
|
clearRunIndicatorActivity({ render: false });
|
|
4726
4794
|
statusEntries.clear();
|
|
4727
|
-
|
|
4795
|
+
restoreWidgetsForActiveTab();
|
|
4728
4796
|
transientMessages = [];
|
|
4729
4797
|
liveToolRuns.clear();
|
|
4730
4798
|
liveToolCards.clear();
|
|
@@ -5105,6 +5173,7 @@ async function switchTab(tabId) {
|
|
|
5105
5173
|
footerBranchPickerRequestSerial += 1;
|
|
5106
5174
|
saveActiveDraft();
|
|
5107
5175
|
cacheMessagesForTab(activeTabId);
|
|
5176
|
+
cacheWidgetsForTab(activeTabId);
|
|
5108
5177
|
const tabContext = setActiveTabId(tabId, { remember: true });
|
|
5109
5178
|
resetActiveTabUi();
|
|
5110
5179
|
renderTabs();
|
|
@@ -7688,15 +7757,71 @@ function confirmFooterGitBranchAction(branch, { create = false, requireConfirm =
|
|
|
7688
7757
|
return window.confirm(message);
|
|
7689
7758
|
}
|
|
7690
7759
|
|
|
7691
|
-
function
|
|
7692
|
-
|
|
7693
|
-
if (value === null) return "";
|
|
7694
|
-
return cleanStatusText(value);
|
|
7760
|
+
function footerBranchCreateType(value = footerBranchCreateDraft.type) {
|
|
7761
|
+
return slugifyGitBranchPart(value);
|
|
7695
7762
|
}
|
|
7696
7763
|
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7764
|
+
function slugifyGitBranchName(value) {
|
|
7765
|
+
return cleanStatusText(value)
|
|
7766
|
+
.split("/")
|
|
7767
|
+
.map((part) => slugifyGitBranchPart(part))
|
|
7768
|
+
.filter(Boolean)
|
|
7769
|
+
.join("/");
|
|
7770
|
+
}
|
|
7771
|
+
|
|
7772
|
+
function footerBranchCreateName() {
|
|
7773
|
+
const type = footerBranchCreateType();
|
|
7774
|
+
const rawName = cleanStatusText(footerBranchCreateDraft.name);
|
|
7775
|
+
if (!type || !rawName) return "";
|
|
7776
|
+
return slugifyGitBranchName(`${type}/${rawName}`);
|
|
7777
|
+
}
|
|
7778
|
+
|
|
7779
|
+
function footerBranchCreatePreviewName() {
|
|
7780
|
+
const type = footerBranchCreateType() || "<type>";
|
|
7781
|
+
const rawName = cleanStatusText(footerBranchCreateDraft.name);
|
|
7782
|
+
const name = rawName ? slugifyGitBranchName(rawName) : "<branch-name>";
|
|
7783
|
+
return `${type}/${name}`;
|
|
7784
|
+
}
|
|
7785
|
+
|
|
7786
|
+
function updateFooterBranchCreateDraft(patch = {}) {
|
|
7787
|
+
const has = (key) => Object.prototype.hasOwnProperty.call(patch, key);
|
|
7788
|
+
footerBranchCreateDraft = {
|
|
7789
|
+
type: has("type") ? footerBranchCreateType(patch.type) : footerBranchCreateType(),
|
|
7790
|
+
name: has("name") ? String(patch.name || "") : footerBranchCreateDraft.name,
|
|
7791
|
+
};
|
|
7792
|
+
}
|
|
7793
|
+
|
|
7794
|
+
function quoteGitBranchForDisplay(branch) {
|
|
7795
|
+
return `'${String(branch || "").replace(/'/g, `'\\''`)}'`;
|
|
7796
|
+
}
|
|
7797
|
+
|
|
7798
|
+
function gitSwitchCreateCommandDisplay(branch) {
|
|
7799
|
+
return `git switch -c ${quoteGitBranchForDisplay(branch)}`;
|
|
7800
|
+
}
|
|
7801
|
+
|
|
7802
|
+
function footerBranchCreateTooltip(branchName = footerBranchCreateName()) {
|
|
7803
|
+
const command = gitSwitchCreateCommandDisplay(branchName || footerBranchCreatePreviewName());
|
|
7804
|
+
return [
|
|
7805
|
+
"Create new branch",
|
|
7806
|
+
"A branch is a safe workspace for your changes.",
|
|
7807
|
+
"",
|
|
7808
|
+
`This will run: ${command}`,
|
|
7809
|
+
"",
|
|
7810
|
+
"What happens:",
|
|
7811
|
+
"• creates the branch from the current code",
|
|
7812
|
+
"• switches this tab to that branch",
|
|
7813
|
+
"• does not commit, push, or delete anything",
|
|
7814
|
+
"",
|
|
7815
|
+
"Tip: use short lowercase words, e.g. fix/login-button.",
|
|
7816
|
+
].join("\n");
|
|
7817
|
+
}
|
|
7818
|
+
|
|
7819
|
+
async function createFooterGitBranch(branch = footerBranchCreateName()) {
|
|
7820
|
+
const branchName = cleanStatusText(branch);
|
|
7821
|
+
if (!branchName) {
|
|
7822
|
+
addEvent("Enter a branch name before creating a new git branch.", "warn");
|
|
7823
|
+
return;
|
|
7824
|
+
}
|
|
7700
7825
|
const tabContext = activeTabContext();
|
|
7701
7826
|
if (!confirmFooterGitBranchAction(branchName, { create: true, requireConfirm: true, tabContext })) return;
|
|
7702
7827
|
await applyFooterGitBranch(branchName, { create: true, tabContext, skipConfirm: true });
|
|
@@ -7717,6 +7842,7 @@ async function applyFooterGitBranch(branch, { create = false, tabContext = activ
|
|
|
7717
7842
|
footerBranchPickerOpen = false;
|
|
7718
7843
|
footerBranchPickerRequestSerial += 1;
|
|
7719
7844
|
footerBranchPickerState = { ...footerBranchPickerState, loading: false, switching: "", current: switchedBranch };
|
|
7845
|
+
if (create) updateFooterBranchCreateDraft({ name: "" });
|
|
7720
7846
|
applyOptimisticGitFooterBranch(switchedBranch, tabContext);
|
|
7721
7847
|
addEvent(response.data?.created ? `Created and switched to git branch ${switchedBranch}.` : response.data?.switched === false ? `Already on git branch ${switchedBranch}.` : `Switched git branch to ${switchedBranch}.`, "info");
|
|
7722
7848
|
requestGitFooterWebuiPayload(tabContext, { force: true });
|
|
@@ -7734,6 +7860,124 @@ async function applyFooterGitBranch(branch, { create = false, tabContext = activ
|
|
|
7734
7860
|
}
|
|
7735
7861
|
}
|
|
7736
7862
|
|
|
7863
|
+
function renderFooterBranchCreateForm(state = footerBranchPickerState) {
|
|
7864
|
+
const form = make("form", "footer-branch-create-form");
|
|
7865
|
+
form.setAttribute("aria-label", "Create new git branch");
|
|
7866
|
+
|
|
7867
|
+
const header = make("div", "footer-branch-create-header");
|
|
7868
|
+
header.append(make("strong", "footer-branch-create-title", "Create new branch"));
|
|
7869
|
+
|
|
7870
|
+
const fields = make("div", "footer-branch-create-fields");
|
|
7871
|
+
const typeField = make("div", "footer-branch-create-type-field");
|
|
7872
|
+
const typeInput = make("input", "footer-branch-create-dropdown-inputfield");
|
|
7873
|
+
typeInput.type = "text";
|
|
7874
|
+
typeInput.value = footerBranchCreateType();
|
|
7875
|
+
typeInput.placeholder = "type";
|
|
7876
|
+
typeInput.setAttribute("aria-label", "Branch type suggestion or custom prefix");
|
|
7877
|
+
typeInput.setAttribute("aria-haspopup", "listbox");
|
|
7878
|
+
typeInput.autocomplete = "off";
|
|
7879
|
+
typeInput.autocapitalize = "none";
|
|
7880
|
+
typeInput.spellcheck = false;
|
|
7881
|
+
|
|
7882
|
+
const typeSuggestions = make("div", "footer-branch-type-suggestions");
|
|
7883
|
+
typeSuggestions.setAttribute("role", "listbox");
|
|
7884
|
+
typeSuggestions.hidden = true;
|
|
7885
|
+
const renderTypeSuggestions = () => {
|
|
7886
|
+
const filter = slugifyGitBranchPart(typeInput.value);
|
|
7887
|
+
const matches = GIT_BRANCH_TYPE_SUGGESTIONS.filter((type) => !filter || type.includes(filter));
|
|
7888
|
+
const shown = matches.length ? matches : GIT_BRANCH_TYPE_SUGGESTIONS;
|
|
7889
|
+
typeSuggestions.replaceChildren(...shown.map((type) => {
|
|
7890
|
+
const button = make("button", `footer-branch-type-suggestion${footerBranchCreateType(typeInput.value) === type ? " active" : ""}`, type);
|
|
7891
|
+
button.type = "button";
|
|
7892
|
+
button.setAttribute("role", "option");
|
|
7893
|
+
button.setAttribute("aria-selected", footerBranchCreateType(typeInput.value) === type ? "true" : "false");
|
|
7894
|
+
button.addEventListener("pointerdown", (event) => event.preventDefault());
|
|
7895
|
+
button.addEventListener("click", () => {
|
|
7896
|
+
typeInput.value = type;
|
|
7897
|
+
updateFooterBranchCreateDraft({ type });
|
|
7898
|
+
updatePreview();
|
|
7899
|
+
typeSuggestions.hidden = true;
|
|
7900
|
+
nameInput.focus();
|
|
7901
|
+
});
|
|
7902
|
+
return button;
|
|
7903
|
+
}));
|
|
7904
|
+
};
|
|
7905
|
+
|
|
7906
|
+
const slash = make("span", "footer-branch-create-slash", "/");
|
|
7907
|
+
|
|
7908
|
+
const nameInput = make("input", "footer-branch-create-input-field");
|
|
7909
|
+
nameInput.type = "text";
|
|
7910
|
+
nameInput.value = footerBranchCreateDraft.name;
|
|
7911
|
+
nameInput.placeholder = "short-feature-name";
|
|
7912
|
+
nameInput.autocomplete = "off";
|
|
7913
|
+
nameInput.autocapitalize = "none";
|
|
7914
|
+
nameInput.spellcheck = false;
|
|
7915
|
+
nameInput.setAttribute("aria-label", "New branch name");
|
|
7916
|
+
|
|
7917
|
+
const submitButton = make("button", "footer-branch-create-submit", state.switching ? "Creating…" : "Create new branch");
|
|
7918
|
+
submitButton.type = "submit";
|
|
7919
|
+
|
|
7920
|
+
const preview = make("div", "footer-branch-create-preview");
|
|
7921
|
+
const updatePreview = () => {
|
|
7922
|
+
const branchName = footerBranchCreateName();
|
|
7923
|
+
preview.textContent = gitSwitchCreateCommandDisplay(branchName || footerBranchCreatePreviewName());
|
|
7924
|
+
const submitDisabled = Boolean(state.switching) || !branchName;
|
|
7925
|
+
submitButton.disabled = false;
|
|
7926
|
+
submitButton.classList.toggle("footer-branch-create-submit-disabled", submitDisabled);
|
|
7927
|
+
submitButton.setAttribute("aria-disabled", submitDisabled ? "true" : "false");
|
|
7928
|
+
submitButton.dataset.tooltip = footerBranchCreateTooltip(branchName);
|
|
7929
|
+
submitButton.setAttribute("aria-label", branchName ? `Create and switch to ${branchName}` : "Create new branch: enter both a branch type and name first");
|
|
7930
|
+
submitButton.removeAttribute("title");
|
|
7931
|
+
};
|
|
7932
|
+
|
|
7933
|
+
typeInput.addEventListener("focus", () => {
|
|
7934
|
+
renderTypeSuggestions();
|
|
7935
|
+
typeSuggestions.hidden = false;
|
|
7936
|
+
});
|
|
7937
|
+
typeInput.addEventListener("blur", () => {
|
|
7938
|
+
setTimeout(() => { typeSuggestions.hidden = true; }, 120);
|
|
7939
|
+
});
|
|
7940
|
+
typeInput.addEventListener("input", () => {
|
|
7941
|
+
updateFooterBranchCreateDraft({ type: typeInput.value });
|
|
7942
|
+
updatePreview();
|
|
7943
|
+
renderTypeSuggestions();
|
|
7944
|
+
typeSuggestions.hidden = false;
|
|
7945
|
+
});
|
|
7946
|
+
typeInput.addEventListener("keydown", (event) => {
|
|
7947
|
+
if (event.key === "Escape") {
|
|
7948
|
+
typeSuggestions.hidden = true;
|
|
7949
|
+
return;
|
|
7950
|
+
}
|
|
7951
|
+
if (event.key !== "/" && event.key !== "Enter") return;
|
|
7952
|
+
event.preventDefault();
|
|
7953
|
+
typeSuggestions.hidden = true;
|
|
7954
|
+
nameInput.focus();
|
|
7955
|
+
nameInput.select();
|
|
7956
|
+
});
|
|
7957
|
+
nameInput.addEventListener("input", () => {
|
|
7958
|
+
updateFooterBranchCreateDraft({ name: nameInput.value });
|
|
7959
|
+
updatePreview();
|
|
7960
|
+
});
|
|
7961
|
+
nameInput.addEventListener("keydown", (event) => {
|
|
7962
|
+
if (event.key !== "Enter") return;
|
|
7963
|
+
event.preventDefault();
|
|
7964
|
+
form.requestSubmit();
|
|
7965
|
+
});
|
|
7966
|
+
form.addEventListener("submit", (event) => {
|
|
7967
|
+
event.preventDefault();
|
|
7968
|
+
if (state.switching) return;
|
|
7969
|
+
const branchName = footerBranchCreateName();
|
|
7970
|
+
createFooterGitBranch(branchName).catch((error) => addEvent(error.message || String(error), "error"));
|
|
7971
|
+
});
|
|
7972
|
+
|
|
7973
|
+
updatePreview();
|
|
7974
|
+
renderTypeSuggestions();
|
|
7975
|
+
typeField.append(typeInput, typeSuggestions);
|
|
7976
|
+
fields.append(typeField, slash, nameInput, submitButton);
|
|
7977
|
+
form.append(header, fields, preview);
|
|
7978
|
+
return form;
|
|
7979
|
+
}
|
|
7980
|
+
|
|
7737
7981
|
function renderFooterBranchPicker() {
|
|
7738
7982
|
const picker = make("div", "footer-model-picker footer-branch-picker");
|
|
7739
7983
|
picker.setAttribute("role", "listbox");
|
|
@@ -7750,7 +7994,7 @@ function renderFooterBranchPicker() {
|
|
|
7750
7994
|
return picker;
|
|
7751
7995
|
}
|
|
7752
7996
|
if (state.loading && state.branches.length === 0) {
|
|
7753
|
-
picker.append(make("div", "footer-model-picker-empty muted", "Loading local branches…"));
|
|
7997
|
+
picker.append(make("div", "footer-model-picker-empty muted", "Loading existing local branches… New branch creation is available."), renderFooterBranchCreateForm(state));
|
|
7754
7998
|
return picker;
|
|
7755
7999
|
}
|
|
7756
8000
|
|
|
@@ -7758,16 +8002,11 @@ function renderFooterBranchPicker() {
|
|
|
7758
8002
|
if (!state.loading && !hasOtherBranches) {
|
|
7759
8003
|
const empty = make("div", "footer-model-picker-empty muted");
|
|
7760
8004
|
empty.append(make("strong", undefined, "No other local branches available."), make("span", undefined, " Create a branch from the current HEAD to continue."));
|
|
7761
|
-
|
|
7762
|
-
createButton.type = "button";
|
|
7763
|
-
createButton.append(
|
|
7764
|
-
make("span", "footer-model-option-main", "Create new branch"),
|
|
7765
|
-
make("span", "footer-model-option-name", "prompts for a name, confirms, then runs git switch -c"),
|
|
7766
|
-
);
|
|
7767
|
-
createButton.addEventListener("click", () => createFooterGitBranch().catch((error) => addEvent(error.message || String(error), "error")));
|
|
7768
|
-
picker.append(empty, createButton);
|
|
8005
|
+
picker.append(empty);
|
|
7769
8006
|
}
|
|
7770
8007
|
|
|
8008
|
+
picker.append(renderFooterBranchCreateForm(state));
|
|
8009
|
+
|
|
7771
8010
|
for (const branch of state.branches) {
|
|
7772
8011
|
const selected = branch.current || (!!state.current && branch.name === state.current);
|
|
7773
8012
|
const disabled = selected || state.loading || !!state.switching;
|
|
@@ -7787,6 +8026,162 @@ function renderFooterBranchPicker() {
|
|
|
7787
8026
|
return picker;
|
|
7788
8027
|
}
|
|
7789
8028
|
|
|
8029
|
+
function footerScopedModelKey(model) {
|
|
8030
|
+
return model?.provider && model?.id ? `${model.provider}/${model.id}` : "";
|
|
8031
|
+
}
|
|
8032
|
+
|
|
8033
|
+
function readFooterScopedModelOrder() {
|
|
8034
|
+
try {
|
|
8035
|
+
const parsed = JSON.parse(localStorage.getItem(FOOTER_SCOPED_MODEL_ORDER_STORAGE_KEY) || "[]");
|
|
8036
|
+
return Array.isArray(parsed) ? parsed.filter((key) => typeof key === "string" && key.trim()) : [];
|
|
8037
|
+
} catch {
|
|
8038
|
+
return [];
|
|
8039
|
+
}
|
|
8040
|
+
}
|
|
8041
|
+
|
|
8042
|
+
function writeFooterScopedModelOrder(order) {
|
|
8043
|
+
try {
|
|
8044
|
+
localStorage.setItem(FOOTER_SCOPED_MODEL_ORDER_STORAGE_KEY, JSON.stringify([...new Set(order.filter(Boolean))]));
|
|
8045
|
+
} catch {}
|
|
8046
|
+
}
|
|
8047
|
+
|
|
8048
|
+
function orderedFooterScopedModels() {
|
|
8049
|
+
const order = readFooterScopedModelOrder();
|
|
8050
|
+
if (!order.length) return footerScopedModels;
|
|
8051
|
+
const rank = new Map(order.map((key, index) => [key, index]));
|
|
8052
|
+
return [...footerScopedModels].sort((a, b) => {
|
|
8053
|
+
const aRank = rank.has(footerScopedModelKey(a)) ? rank.get(footerScopedModelKey(a)) : Number.MAX_SAFE_INTEGER;
|
|
8054
|
+
const bRank = rank.has(footerScopedModelKey(b)) ? rank.get(footerScopedModelKey(b)) : Number.MAX_SAFE_INTEGER;
|
|
8055
|
+
return aRank - bRank;
|
|
8056
|
+
});
|
|
8057
|
+
}
|
|
8058
|
+
|
|
8059
|
+
function commitFooterScopedModelOrder(order, { render = true, focusKey = "" } = {}) {
|
|
8060
|
+
writeFooterScopedModelOrder(order);
|
|
8061
|
+
footerScopedModels = orderedFooterScopedModels();
|
|
8062
|
+
if (render) renderFooter();
|
|
8063
|
+
if (focusKey) {
|
|
8064
|
+
const movedButton = document.querySelector(`[data-footer-model-key="${CSS.escape(focusKey)}"]`);
|
|
8065
|
+
if (movedButton) movedButton.focus();
|
|
8066
|
+
}
|
|
8067
|
+
}
|
|
8068
|
+
|
|
8069
|
+
function reorderFooterScopedModel(fromKey, toKey, { focus = true } = {}) {
|
|
8070
|
+
if (!fromKey || !toKey || fromKey === toKey) return false;
|
|
8071
|
+
const models = orderedFooterScopedModels();
|
|
8072
|
+
const fromIndex = models.findIndex((model) => footerScopedModelKey(model) === fromKey);
|
|
8073
|
+
const toIndex = models.findIndex((model) => footerScopedModelKey(model) === toKey);
|
|
8074
|
+
if (fromIndex < 0 || toIndex < 0) return false;
|
|
8075
|
+
const [moved] = models.splice(fromIndex, 1);
|
|
8076
|
+
models.splice(toIndex, 0, moved);
|
|
8077
|
+
commitFooterScopedModelOrder(models.map(footerScopedModelKey), { focusKey: focus ? fromKey : "" });
|
|
8078
|
+
return true;
|
|
8079
|
+
}
|
|
8080
|
+
|
|
8081
|
+
function moveFooterScopedModelByOffset(modelKey, offset) {
|
|
8082
|
+
const models = orderedFooterScopedModels();
|
|
8083
|
+
const fromIndex = models.findIndex((model) => footerScopedModelKey(model) === modelKey);
|
|
8084
|
+
const toIndex = fromIndex + offset;
|
|
8085
|
+
if (fromIndex < 0 || toIndex < 0 || toIndex >= models.length) return false;
|
|
8086
|
+
const [moved] = models.splice(fromIndex, 1);
|
|
8087
|
+
models.splice(toIndex, 0, moved);
|
|
8088
|
+
commitFooterScopedModelOrder(models.map(footerScopedModelKey), { focusKey: modelKey });
|
|
8089
|
+
return true;
|
|
8090
|
+
}
|
|
8091
|
+
|
|
8092
|
+
function footerScopedModelButtons() {
|
|
8093
|
+
return [...document.querySelectorAll(".footer-model-picker .footer-model-option[data-footer-model-key]")];
|
|
8094
|
+
}
|
|
8095
|
+
|
|
8096
|
+
function footerScopedModelButtonFromPoint(clientX, clientY) {
|
|
8097
|
+
return document.elementFromPoint(clientX, clientY)?.closest?.(".footer-model-option[data-footer-model-key]") || null;
|
|
8098
|
+
}
|
|
8099
|
+
|
|
8100
|
+
function clearFooterScopedModelDragMarkers() {
|
|
8101
|
+
for (const button of footerScopedModelButtons()) {
|
|
8102
|
+
button.classList.remove("drag-over", "drag-over-before", "drag-over-after");
|
|
8103
|
+
}
|
|
8104
|
+
}
|
|
8105
|
+
|
|
8106
|
+
function commitVisibleFooterScopedModelOrder({ render = false, focusKey = "" } = {}) {
|
|
8107
|
+
const order = footerScopedModelButtons().map((button) => button.dataset.footerModelKey).filter(Boolean);
|
|
8108
|
+
if (!order.length) return;
|
|
8109
|
+
commitFooterScopedModelOrder(order, { render, focusKey });
|
|
8110
|
+
}
|
|
8111
|
+
|
|
8112
|
+
function moveVisibleFooterScopedModel(fromKey, targetButton, clientY) {
|
|
8113
|
+
if (!fromKey || !targetButton) return false;
|
|
8114
|
+
const sourceButton = document.querySelector(`[data-footer-model-key="${CSS.escape(fromKey)}"]`);
|
|
8115
|
+
if (!sourceButton || sourceButton === targetButton) return false;
|
|
8116
|
+
const parent = sourceButton.parentElement;
|
|
8117
|
+
if (!parent || targetButton.parentElement !== parent) return false;
|
|
8118
|
+
const targetKey = targetButton.dataset.footerModelKey || "";
|
|
8119
|
+
const rect = targetButton.getBoundingClientRect();
|
|
8120
|
+
const insertBefore = clientY < rect.top + rect.height / 2;
|
|
8121
|
+
|
|
8122
|
+
clearFooterScopedModelDragMarkers();
|
|
8123
|
+
targetButton.classList.add("drag-over", insertBefore ? "drag-over-before" : "drag-over-after");
|
|
8124
|
+
footerScopedModelLastDragOverKey = `${targetKey}:${insertBefore ? "before" : "after"}`;
|
|
8125
|
+
|
|
8126
|
+
if (insertBefore) parent.insertBefore(sourceButton, targetButton);
|
|
8127
|
+
else parent.insertBefore(sourceButton, targetButton.nextSibling);
|
|
8128
|
+
commitVisibleFooterScopedModelOrder({ render: false });
|
|
8129
|
+
return true;
|
|
8130
|
+
}
|
|
8131
|
+
|
|
8132
|
+
function beginFooterScopedModelPointerDrag(event, modelKey) {
|
|
8133
|
+
if (event.button !== 0 || !modelKey) return;
|
|
8134
|
+
footerScopedModelPointerDrag = { modelKey, pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, active: false };
|
|
8135
|
+
window.addEventListener("pointermove", updateFooterScopedModelPointerDrag, { capture: true });
|
|
8136
|
+
window.addEventListener("pointerup", endFooterScopedModelPointerDrag, { capture: true });
|
|
8137
|
+
window.addEventListener("pointercancel", endFooterScopedModelPointerDrag, { capture: true });
|
|
8138
|
+
}
|
|
8139
|
+
|
|
8140
|
+
function updateFooterScopedModelPointerDrag(event) {
|
|
8141
|
+
const drag = footerScopedModelPointerDrag;
|
|
8142
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
8143
|
+
const distance = Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY);
|
|
8144
|
+
if (!drag.active && distance < FOOTER_SCOPED_MODEL_POINTER_DRAG_THRESHOLD_PX) return;
|
|
8145
|
+
event.preventDefault();
|
|
8146
|
+
if (!drag.active) {
|
|
8147
|
+
drag.active = true;
|
|
8148
|
+
footerScopedModelDragKey = drag.modelKey;
|
|
8149
|
+
footerScopedModelLastDragOverKey = "";
|
|
8150
|
+
clearTimeout(pointerActivationTimeout);
|
|
8151
|
+
pointerActivationTimeout = null;
|
|
8152
|
+
activePointerActivation = null;
|
|
8153
|
+
deferredUiRenderCallbacks.delete("footer");
|
|
8154
|
+
const sourceButton = document.querySelector(`[data-footer-model-key="${CSS.escape(drag.modelKey)}"]`);
|
|
8155
|
+
if (sourceButton) sourceButton.classList.add("dragging");
|
|
8156
|
+
}
|
|
8157
|
+
const targetButton = footerScopedModelButtonFromPoint(event.clientX, event.clientY);
|
|
8158
|
+
if (!targetButton || targetButton.dataset.footerModelKey === drag.modelKey) return;
|
|
8159
|
+
const rect = targetButton.getBoundingClientRect();
|
|
8160
|
+
const markerKey = `${targetButton.dataset.footerModelKey}:${event.clientY < rect.top + rect.height / 2 ? "before" : "after"}`;
|
|
8161
|
+
if (markerKey === footerScopedModelLastDragOverKey) return;
|
|
8162
|
+
moveVisibleFooterScopedModel(drag.modelKey, targetButton, event.clientY);
|
|
8163
|
+
}
|
|
8164
|
+
|
|
8165
|
+
function endFooterScopedModelPointerDrag(event) {
|
|
8166
|
+
const drag = footerScopedModelPointerDrag;
|
|
8167
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
8168
|
+
window.removeEventListener("pointermove", updateFooterScopedModelPointerDrag, { capture: true });
|
|
8169
|
+
window.removeEventListener("pointerup", endFooterScopedModelPointerDrag, { capture: true });
|
|
8170
|
+
window.removeEventListener("pointercancel", endFooterScopedModelPointerDrag, { capture: true });
|
|
8171
|
+
const wasActive = drag.active;
|
|
8172
|
+
const sourceButton = document.querySelector(`[data-footer-model-key="${CSS.escape(drag.modelKey)}"]`);
|
|
8173
|
+
footerScopedModelPointerDrag = null;
|
|
8174
|
+
footerScopedModelDragKey = "";
|
|
8175
|
+
footerScopedModelLastDragOverKey = "";
|
|
8176
|
+
clearFooterScopedModelDragMarkers();
|
|
8177
|
+
if (sourceButton) sourceButton.classList.remove("dragging");
|
|
8178
|
+
if (wasActive) {
|
|
8179
|
+
footerScopedModelSuppressClickUntil = Date.now() + 250;
|
|
8180
|
+
event.preventDefault();
|
|
8181
|
+
commitVisibleFooterScopedModelOrder({ render: false, focusKey: drag.modelKey });
|
|
8182
|
+
}
|
|
8183
|
+
}
|
|
8184
|
+
|
|
7790
8185
|
async function applyFooterModel(model) {
|
|
7791
8186
|
if (!model?.provider || !model?.id) return;
|
|
7792
8187
|
const tabContext = activeTabContext();
|
|
@@ -7812,7 +8207,7 @@ function renderFooterModelPicker() {
|
|
|
7812
8207
|
picker.setAttribute("role", "listbox");
|
|
7813
8208
|
picker.setAttribute("aria-label", "Scoped models");
|
|
7814
8209
|
picker.append(make("div", "footer-model-picker-title", "Scoped models"));
|
|
7815
|
-
picker.append(make("div", "footer-model-picker-source",
|
|
8210
|
+
picker.append(make("div", "footer-model-picker-source", "Drag models to reorder · Alt+↑/↓ moves focused model"));
|
|
7816
8211
|
if (footerScopedModels.length === 0) {
|
|
7817
8212
|
const empty = make("div", "footer-model-picker-empty muted");
|
|
7818
8213
|
empty.append(
|
|
@@ -7823,10 +8218,16 @@ function renderFooterModelPicker() {
|
|
|
7823
8218
|
return picker;
|
|
7824
8219
|
}
|
|
7825
8220
|
const current = currentState?.model;
|
|
8221
|
+
footerScopedModels = orderedFooterScopedModels();
|
|
7826
8222
|
for (const model of footerScopedModels) {
|
|
7827
8223
|
const selected = current?.provider === model.provider && current?.id === model.id;
|
|
7828
|
-
const
|
|
8224
|
+
const modelKey = footerScopedModelKey(model);
|
|
8225
|
+
const dragging = footerScopedModelDragKey === modelKey;
|
|
8226
|
+
const dragOver = footerScopedModelDragKey && footerScopedModelLastDragOverKey === modelKey;
|
|
8227
|
+
const button = make("button", `footer-model-option${selected ? " active" : ""}${dragging ? " dragging" : ""}${dragOver ? " drag-over" : ""}`);
|
|
7829
8228
|
button.type = "button";
|
|
8229
|
+
button.draggable = false;
|
|
8230
|
+
button.dataset.footerModelKey = modelKey;
|
|
7830
8231
|
button.setAttribute("role", "option");
|
|
7831
8232
|
button.setAttribute("aria-selected", selected ? "true" : "false");
|
|
7832
8233
|
button.title = `${model.provider}/${model.id}${model.name ? ` · ${model.name}` : ""}`;
|
|
@@ -7834,7 +8235,19 @@ function renderFooterModelPicker() {
|
|
|
7834
8235
|
make("span", "footer-model-option-main", `${model.provider}/${model.id}`),
|
|
7835
8236
|
make("span", "footer-model-option-name", model.name || ""),
|
|
7836
8237
|
);
|
|
7837
|
-
button.addEventListener("click", () =>
|
|
8238
|
+
button.addEventListener("click", (event) => {
|
|
8239
|
+
if (Date.now() < footerScopedModelSuppressClickUntil) {
|
|
8240
|
+
event.preventDefault();
|
|
8241
|
+
return;
|
|
8242
|
+
}
|
|
8243
|
+
applyFooterModel(model);
|
|
8244
|
+
});
|
|
8245
|
+
button.addEventListener("keydown", (event) => {
|
|
8246
|
+
if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
|
|
8247
|
+
event.preventDefault();
|
|
8248
|
+
moveFooterScopedModelByOffset(modelKey, event.key === "ArrowUp" ? -1 : 1);
|
|
8249
|
+
});
|
|
8250
|
+
button.addEventListener("pointerdown", (event) => beginFooterScopedModelPointerDrag(event, modelKey));
|
|
7838
8251
|
picker.append(button);
|
|
7839
8252
|
}
|
|
7840
8253
|
return picker;
|
|
@@ -8650,8 +9063,79 @@ function stripTodoProgressLines(text, { streaming = false } = {}) {
|
|
|
8650
9063
|
return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
8651
9064
|
}
|
|
8652
9065
|
|
|
9066
|
+
function parseTodoProgressItemLine(line) {
|
|
9067
|
+
const match = String(line || "").match(/^\s*(?:(?:[-*]|\d+[.)])\s*)?\[( |x|X|-)\]\s+(.+)$/);
|
|
9068
|
+
if (!match) return null;
|
|
9069
|
+
const mark = match[1].toLowerCase();
|
|
9070
|
+
return { status: mark === "x" ? "done" : mark === "-" ? "partial" : "todo", text: match[2].trim() };
|
|
9071
|
+
}
|
|
9072
|
+
|
|
9073
|
+
function todoProgressStatusLabel(status) {
|
|
9074
|
+
if (status === "done") return "[x]";
|
|
9075
|
+
if (status === "partial") return "[-]";
|
|
9076
|
+
return "[ ]";
|
|
9077
|
+
}
|
|
9078
|
+
|
|
9079
|
+
function liveTodoProgressWidgetLinesFromText(text) {
|
|
9080
|
+
if (!isOptionalFeatureEnabled("todoProgressWidget")) return null;
|
|
9081
|
+
const raw = String(text || "");
|
|
9082
|
+
if (!raw.trim()) return null;
|
|
9083
|
+
|
|
9084
|
+
let inFence = false;
|
|
9085
|
+
let goal = "";
|
|
9086
|
+
let current = [];
|
|
9087
|
+
const blocks = [];
|
|
9088
|
+
const flush = () => {
|
|
9089
|
+
if (current.length) blocks.push(current);
|
|
9090
|
+
current = [];
|
|
9091
|
+
};
|
|
9092
|
+
|
|
9093
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
9094
|
+
if (/^\s*```/.test(line)) {
|
|
9095
|
+
inFence = !inFence;
|
|
9096
|
+
flush();
|
|
9097
|
+
continue;
|
|
9098
|
+
}
|
|
9099
|
+
if (inFence) continue;
|
|
9100
|
+
|
|
9101
|
+
const clean = stripAnsi(line).trim();
|
|
9102
|
+
const goalMatch = clean.match(/^Goal\s*[::]\s*(.+)$/i);
|
|
9103
|
+
if (goalMatch?.[1]?.trim()) goal = goalMatch[1].trim();
|
|
9104
|
+
|
|
9105
|
+
const item = parseTodoProgressItemLine(clean);
|
|
9106
|
+
if (item) {
|
|
9107
|
+
current.push(item);
|
|
9108
|
+
continue;
|
|
9109
|
+
}
|
|
9110
|
+
flush();
|
|
9111
|
+
}
|
|
9112
|
+
flush();
|
|
9113
|
+
|
|
9114
|
+
const items = blocks.at(-1) || [];
|
|
9115
|
+
if (!items.length) return null;
|
|
9116
|
+
|
|
9117
|
+
const done = items.filter((item) => item.status === "done").length;
|
|
9118
|
+
const partial = items.filter((item) => item.status === "partial").length;
|
|
9119
|
+
const lines = [];
|
|
9120
|
+
if (goal) lines.push(`Goal: ${goal}`);
|
|
9121
|
+
lines.push(`Todo ${done}/${items.length} done${partial ? `, ${partial} partial` : ""}`);
|
|
9122
|
+
for (const item of items) lines.push(`${todoProgressStatusLabel(item.status)} ${item.text}`);
|
|
9123
|
+
return lines;
|
|
9124
|
+
}
|
|
9125
|
+
|
|
9126
|
+
function syncLiveTodoProgressWidgetFromText(text, tabId = activeTabId) {
|
|
9127
|
+
const lines = liveTodoProgressWidgetLinesFromText(text);
|
|
9128
|
+
if (!lines) return false;
|
|
9129
|
+
setWidgetForTab(tabId, "todo-progress", { method: "setWidget", widgetKey: "todo-progress", widgetLines: lines, tabId, live: true });
|
|
9130
|
+
updateOptionalFeatureAvailability();
|
|
9131
|
+
if (tabId === activeTabId) renderWidgets();
|
|
9132
|
+
return true;
|
|
9133
|
+
}
|
|
9134
|
+
|
|
8653
9135
|
function parseTodoProgressWidget(lines) {
|
|
8654
9136
|
const cleanLines = lines.map(stripAnsi).map((line) => line.trim()).filter(Boolean);
|
|
9137
|
+
const goalLine = cleanLines.find((line) => /^Goal\s*[::]/i.test(line));
|
|
9138
|
+
const goal = goalLine ? goalLine.replace(/^Goal\s*[::]\s*/i, "").trim() : "";
|
|
8655
9139
|
const headerIndex = cleanLines.findIndex((line) => /^Todo\s+\d+\/\d+\s+done/i.test(line));
|
|
8656
9140
|
if (headerIndex === -1) return null;
|
|
8657
9141
|
|
|
@@ -8662,16 +9146,16 @@ function parseTodoProgressWidget(lines) {
|
|
|
8662
9146
|
const items = [];
|
|
8663
9147
|
let footer = "";
|
|
8664
9148
|
for (const line of cleanLines.slice(headerIndex + 1)) {
|
|
8665
|
-
const item = line
|
|
9149
|
+
const item = parseTodoProgressItemLine(line);
|
|
8666
9150
|
if (item) {
|
|
8667
|
-
|
|
8668
|
-
items.push({ status: mark === "x" ? "done" : mark === "-" ? "partial" : "todo", text: item[2].trim() });
|
|
9151
|
+
items.push(item);
|
|
8669
9152
|
} else if (/^Scroll\s+/i.test(line)) {
|
|
8670
9153
|
footer = line;
|
|
8671
9154
|
}
|
|
8672
9155
|
}
|
|
8673
9156
|
|
|
8674
9157
|
return {
|
|
9158
|
+
goal,
|
|
8675
9159
|
done: Number.parseInt(match[1], 10) || 0,
|
|
8676
9160
|
total: Number.parseInt(match[2], 10) || items.length,
|
|
8677
9161
|
partial: Number.parseInt(match[3] || "0", 10) || 0,
|
|
@@ -8706,6 +9190,7 @@ function renderTodoProgressWidget(_key, lines) {
|
|
|
8706
9190
|
const fill = make("span", "todo-widget-progress-fill");
|
|
8707
9191
|
fill.style.width = `${percent}%`;
|
|
8708
9192
|
progress.append(fill);
|
|
9193
|
+
if (todo.goal) summary.append(make("div", "todo-widget-goal", `Goal: ${todo.goal}`));
|
|
8709
9194
|
summary.append(header, progress);
|
|
8710
9195
|
|
|
8711
9196
|
const body = make("div", "todo-widget-body");
|
|
@@ -12885,14 +13370,201 @@ function appendMarkdownParagraph(parent, lines) {
|
|
|
12885
13370
|
parent.append(paragraph);
|
|
12886
13371
|
}
|
|
12887
13372
|
|
|
12888
|
-
function
|
|
13373
|
+
function setMarkdownCodeCopyButtonState(button, copied) {
|
|
13374
|
+
clearTimeout(button._markdownCodeCopyResetTimer);
|
|
13375
|
+
const label = button._markdownCodeCopyDefaultLabel || "Copy";
|
|
13376
|
+
button.classList.toggle("copied", copied);
|
|
13377
|
+
button.textContent = copied ? "Copied" : label;
|
|
13378
|
+
button.title = copied ? "Copied code block" : `${label} code block`;
|
|
13379
|
+
button.setAttribute("aria-label", button.title);
|
|
13380
|
+
if (copied) {
|
|
13381
|
+
button._markdownCodeCopyResetTimer = setTimeout(() => setMarkdownCodeCopyButtonState(button, false), 1400);
|
|
13382
|
+
}
|
|
13383
|
+
}
|
|
13384
|
+
|
|
13385
|
+
async function copyMarkdownCodeBlock(button) {
|
|
13386
|
+
const wrapper = button.closest(".markdown-code-block");
|
|
13387
|
+
const codeNode = wrapper?.querySelector(":scope > pre.markdown-code > code, :scope > details.markdown-mermaid-source pre.markdown-code > code");
|
|
13388
|
+
const text = codeNode?.textContent || "";
|
|
13389
|
+
if (!text) {
|
|
13390
|
+
addEvent("code block has no text to copy", "warn");
|
|
13391
|
+
return;
|
|
13392
|
+
}
|
|
13393
|
+
button.disabled = true;
|
|
13394
|
+
try {
|
|
13395
|
+
await copyText(text);
|
|
13396
|
+
setMarkdownCodeCopyButtonState(button, true);
|
|
13397
|
+
} catch (error) {
|
|
13398
|
+
addEvent(`code block copy failed: ${error.message || String(error)}`, "warn");
|
|
13399
|
+
} finally {
|
|
13400
|
+
button.disabled = false;
|
|
13401
|
+
}
|
|
13402
|
+
}
|
|
13403
|
+
|
|
13404
|
+
function attachMarkdownCodeCopyButton(wrapper, label = "Copy") {
|
|
13405
|
+
if (!wrapper) return null;
|
|
13406
|
+
const existing = wrapper.querySelector(":scope > .markdown-code-copy-button");
|
|
13407
|
+
if (existing) return existing;
|
|
13408
|
+
const button = make("button", "markdown-code-copy-button", label);
|
|
13409
|
+
button.type = "button";
|
|
13410
|
+
button._markdownCodeCopyDefaultLabel = label;
|
|
13411
|
+
setMarkdownCodeCopyButtonState(button, false);
|
|
13412
|
+
button.addEventListener("click", (event) => {
|
|
13413
|
+
event.preventDefault();
|
|
13414
|
+
event.stopPropagation();
|
|
13415
|
+
copyMarkdownCodeBlock(button);
|
|
13416
|
+
});
|
|
13417
|
+
wrapper.classList.add("has-code-copy-action");
|
|
13418
|
+
wrapper.append(button);
|
|
13419
|
+
return button;
|
|
13420
|
+
}
|
|
13421
|
+
|
|
13422
|
+
function normalizedMarkdownLanguage(language) {
|
|
13423
|
+
return String(language || "").trim().toLowerCase();
|
|
13424
|
+
}
|
|
13425
|
+
|
|
13426
|
+
function isMermaidLanguage(language) {
|
|
13427
|
+
return MERMAID_LANGUAGES.has(normalizedMarkdownLanguage(language));
|
|
13428
|
+
}
|
|
13429
|
+
|
|
13430
|
+
function mermaidCssVar(styles, name, fallback) {
|
|
13431
|
+
return styles.getPropertyValue(name).trim() || fallback;
|
|
13432
|
+
}
|
|
13433
|
+
|
|
13434
|
+
function mermaidConfig() {
|
|
13435
|
+
const styles = getComputedStyle(document.documentElement);
|
|
13436
|
+
const text = mermaidCssVar(styles, "--ctp-text", "#cdd6f4");
|
|
13437
|
+
const subtext = mermaidCssVar(styles, "--ctp-subtext", "#bac2de");
|
|
13438
|
+
const surface = mermaidCssVar(styles, "--ctp-surface", "#313244");
|
|
13439
|
+
const base = mermaidCssVar(styles, "--ctp-base", "#1e1e2e");
|
|
13440
|
+
const crust = mermaidCssVar(styles, "--ctp-crust", "#11111b");
|
|
13441
|
+
const mauve = mermaidCssVar(styles, "--ctp-mauve", "#cba6f7");
|
|
13442
|
+
const blue = mermaidCssVar(styles, "--ctp-blue", "#89b4fa");
|
|
13443
|
+
const teal = mermaidCssVar(styles, "--ctp-teal", "#94e2d5");
|
|
13444
|
+
const yellow = mermaidCssVar(styles, "--ctp-yellow", "#f9e2af");
|
|
13445
|
+
const red = mermaidCssVar(styles, "--ctp-red", "#f38ba8");
|
|
13446
|
+
return {
|
|
13447
|
+
startOnLoad: false,
|
|
13448
|
+
securityLevel: "strict",
|
|
13449
|
+
logLevel: "error",
|
|
13450
|
+
maxTextSize: MERMAID_MAX_TEXT_SIZE,
|
|
13451
|
+
theme: "base",
|
|
13452
|
+
flowchart: { htmlLabels: false },
|
|
13453
|
+
themeVariables: {
|
|
13454
|
+
darkMode: true,
|
|
13455
|
+
background: "transparent",
|
|
13456
|
+
mainBkg: base,
|
|
13457
|
+
secondBkg: surface,
|
|
13458
|
+
primaryColor: surface,
|
|
13459
|
+
primaryTextColor: text,
|
|
13460
|
+
primaryBorderColor: mauve,
|
|
13461
|
+
secondaryColor: base,
|
|
13462
|
+
secondaryTextColor: text,
|
|
13463
|
+
secondaryBorderColor: blue,
|
|
13464
|
+
tertiaryColor: crust,
|
|
13465
|
+
tertiaryTextColor: text,
|
|
13466
|
+
tertiaryBorderColor: teal,
|
|
13467
|
+
lineColor: subtext,
|
|
13468
|
+
textColor: text,
|
|
13469
|
+
titleColor: teal,
|
|
13470
|
+
nodeTextColor: text,
|
|
13471
|
+
clusterBkg: crust,
|
|
13472
|
+
clusterBorder: mauve,
|
|
13473
|
+
edgeLabelBackground: base,
|
|
13474
|
+
noteBkgColor: crust,
|
|
13475
|
+
noteTextColor: text,
|
|
13476
|
+
noteBorderColor: yellow,
|
|
13477
|
+
errorBkgColor: crust,
|
|
13478
|
+
errorTextColor: red,
|
|
13479
|
+
fontFamily: 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
13480
|
+
},
|
|
13481
|
+
};
|
|
13482
|
+
}
|
|
13483
|
+
|
|
13484
|
+
function initializeMermaid(mermaid) {
|
|
13485
|
+
const config = mermaidConfig();
|
|
13486
|
+
const signature = JSON.stringify(config);
|
|
13487
|
+
if (signature !== mermaidThemeSignature) {
|
|
13488
|
+
mermaid.initialize(config);
|
|
13489
|
+
mermaidThemeSignature = signature;
|
|
13490
|
+
}
|
|
13491
|
+
}
|
|
13492
|
+
|
|
13493
|
+
async function loadMermaid() {
|
|
13494
|
+
if (!mermaidModulePromise) {
|
|
13495
|
+
mermaidModulePromise = import(MERMAID_MODULE_URL)
|
|
13496
|
+
.then((module) => module.default || module)
|
|
13497
|
+
.catch((error) => {
|
|
13498
|
+
mermaidModulePromise = null;
|
|
13499
|
+
throw error;
|
|
13500
|
+
});
|
|
13501
|
+
}
|
|
13502
|
+
const mermaid = await mermaidModulePromise;
|
|
13503
|
+
initializeMermaid(mermaid);
|
|
13504
|
+
return mermaid;
|
|
13505
|
+
}
|
|
13506
|
+
|
|
13507
|
+
function mermaidRenderErrorMessage(error) {
|
|
13508
|
+
return String(error?.str || error?.message || error || "Unknown Mermaid render error").trim();
|
|
13509
|
+
}
|
|
13510
|
+
|
|
13511
|
+
async function renderMermaidDiagram(diagram, status, source) {
|
|
13512
|
+
const token = `${Date.now().toString(36)}-${++mermaidRenderSequence}`;
|
|
13513
|
+
diagram.dataset.mermaidRenderToken = token;
|
|
13514
|
+
try {
|
|
13515
|
+
if (source.length > MERMAID_MAX_TEXT_SIZE) throw new Error(`Mermaid diagram is too large (${source.length} characters, max ${MERMAID_MAX_TEXT_SIZE}).`);
|
|
13516
|
+
const mermaid = await loadMermaid();
|
|
13517
|
+
const id = `mermaid-${token.replace(/[^a-z0-9_-]/gi, "-")}`;
|
|
13518
|
+
const { svg, bindFunctions } = await mermaid.render(id, source);
|
|
13519
|
+
if (!diagram.isConnected || diagram.dataset.mermaidRenderToken !== token) return;
|
|
13520
|
+
diagram.innerHTML = svg;
|
|
13521
|
+
bindFunctions?.(diagram);
|
|
13522
|
+
diagram.classList.add("rendered");
|
|
13523
|
+
status.textContent = "";
|
|
13524
|
+
status.hidden = true;
|
|
13525
|
+
} catch (error) {
|
|
13526
|
+
if (!diagram.isConnected || diagram.dataset.mermaidRenderToken !== token) return;
|
|
13527
|
+
diagram.classList.add("render-error");
|
|
13528
|
+
status.hidden = false;
|
|
13529
|
+
status.classList.add("error");
|
|
13530
|
+
status.textContent = `Mermaid render failed: ${mermaidRenderErrorMessage(error)}`;
|
|
13531
|
+
}
|
|
13532
|
+
}
|
|
13533
|
+
|
|
13534
|
+
function appendMarkdownMermaidBlock(parent, code) {
|
|
13535
|
+
const source = String(code || "").replace(/\n+$/g, "");
|
|
13536
|
+
const wrapper = make("div", "markdown-code-block markdown-mermaid-block");
|
|
13537
|
+
wrapper.append(make("div", "markdown-code-language", "mermaid"));
|
|
13538
|
+
const diagram = make("div", "markdown-mermaid-diagram");
|
|
13539
|
+
diagram.setAttribute("role", "img");
|
|
13540
|
+
diagram.setAttribute("aria-label", "Mermaid diagram");
|
|
13541
|
+
const status = make("div", "markdown-mermaid-status muted", "Rendering Mermaid diagram…");
|
|
13542
|
+
const sourceDetails = make("details", "markdown-mermaid-source");
|
|
13543
|
+
sourceDetails.append(make("summary", undefined, "Mermaid source"));
|
|
13544
|
+
const pre = make("pre", "code-block markdown-code");
|
|
13545
|
+
const codeNode = make("code", "language-mermaid");
|
|
13546
|
+
codeNode.textContent = source;
|
|
13547
|
+
pre.append(codeNode);
|
|
13548
|
+
sourceDetails.append(pre);
|
|
13549
|
+
wrapper.append(diagram, status, sourceDetails);
|
|
13550
|
+
attachMarkdownCodeCopyButton(wrapper, "Copy source");
|
|
13551
|
+
parent.append(wrapper);
|
|
13552
|
+
queueMicrotask(() => renderMermaidDiagram(diagram, status, source));
|
|
13553
|
+
}
|
|
13554
|
+
|
|
13555
|
+
function appendMarkdownCodeBlock(parent, code, language = "", { closed = true } = {}) {
|
|
13556
|
+
if (closed && isMermaidLanguage(language)) {
|
|
13557
|
+
appendMarkdownMermaidBlock(parent, code);
|
|
13558
|
+
return;
|
|
13559
|
+
}
|
|
12889
13560
|
const wrapper = make("div", "markdown-code-block");
|
|
12890
13561
|
if (language) wrapper.append(make("div", "markdown-code-language", language));
|
|
12891
13562
|
const pre = make("pre", "code-block markdown-code");
|
|
12892
13563
|
const codeNode = make("code", language ? `language-${language.replace(/[^a-z0-9_-]/gi, "")}` : "");
|
|
12893
|
-
codeNode.textContent = code.replace(/\n+$/g, "");
|
|
13564
|
+
codeNode.textContent = String(code || "").replace(/\n+$/g, "");
|
|
12894
13565
|
pre.append(codeNode);
|
|
12895
13566
|
wrapper.append(pre);
|
|
13567
|
+
attachMarkdownCodeCopyButton(wrapper);
|
|
12896
13568
|
parent.append(wrapper);
|
|
12897
13569
|
}
|
|
12898
13570
|
|
|
@@ -12990,8 +13662,9 @@ function renderMarkdownInto(parent, text) {
|
|
|
12990
13662
|
codeLines.push(lines[index]);
|
|
12991
13663
|
index += 1;
|
|
12992
13664
|
}
|
|
12993
|
-
|
|
12994
|
-
|
|
13665
|
+
const closed = index < lines.length;
|
|
13666
|
+
if (closed) index += 1;
|
|
13667
|
+
appendMarkdownCodeBlock(parent, codeLines.join("\n"), language, { closed });
|
|
12995
13668
|
continue;
|
|
12996
13669
|
}
|
|
12997
13670
|
if (markdownTableSeparator(lines[index + 1]) && line.includes("|")) {
|
|
@@ -13446,6 +14119,89 @@ function visibleThinkingText(text) {
|
|
|
13446
14119
|
return value;
|
|
13447
14120
|
}
|
|
13448
14121
|
|
|
14122
|
+
function escapeRegExp(value) {
|
|
14123
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14124
|
+
}
|
|
14125
|
+
|
|
14126
|
+
function isPartialThinkingFormatOpenTag(text) {
|
|
14127
|
+
const value = String(text || "").trimStart().toLowerCase();
|
|
14128
|
+
if (!value) return false;
|
|
14129
|
+
if ("<think>".startsWith(value)) return true;
|
|
14130
|
+
if (value === "<|" || /^<\|[a-z][\w-]*$/i.test(value)) return true;
|
|
14131
|
+
return /^<think\b[^>]*$/i.test(value);
|
|
14132
|
+
}
|
|
14133
|
+
|
|
14134
|
+
function stripPartialThinkingFormatClose(text, closeTag = "</think>") {
|
|
14135
|
+
const value = String(text || "");
|
|
14136
|
+
const lower = value.toLowerCase();
|
|
14137
|
+
const expected = String(closeTag || "").toLowerCase();
|
|
14138
|
+
const start = lower.lastIndexOf("<");
|
|
14139
|
+
if (start < 0) return value;
|
|
14140
|
+
const partial = lower.slice(start).trimEnd();
|
|
14141
|
+
return expected.startsWith(partial) ? value.slice(0, start) : value;
|
|
14142
|
+
}
|
|
14143
|
+
|
|
14144
|
+
function stripThinkingFormatOutputSeparator(text) {
|
|
14145
|
+
return String(text || "").replace(/^(?:[ \t]*\r?\n)+/, "").replace(/^[ \t]+/, "");
|
|
14146
|
+
}
|
|
14147
|
+
|
|
14148
|
+
function joinedThinkingFormatParts(parts) {
|
|
14149
|
+
return parts.map((part) => String(part || "")).filter((part) => part.length > 0).join("\n\n");
|
|
14150
|
+
}
|
|
14151
|
+
|
|
14152
|
+
function thinkingFormatOpenMatch(text) {
|
|
14153
|
+
const value = String(text || "");
|
|
14154
|
+
const think = THINKING_FORMAT_OPEN_TAG_REGEX.exec(value);
|
|
14155
|
+
if (think) return { raw: think[0], closeRegex: THINKING_FORMAT_CLOSE_TAG_REGEX, closeTag: "</think>" };
|
|
14156
|
+
const channel = CHANNEL_THINKING_FORMAT_OPEN_TAG_REGEX.exec(value);
|
|
14157
|
+
if (!channel) return null;
|
|
14158
|
+
const name = channel[1];
|
|
14159
|
+
return { raw: channel[0], closeRegex: new RegExp(`<${escapeRegExp(name)}\\|>`, "i"), closeTag: `<${name}|>` };
|
|
14160
|
+
}
|
|
14161
|
+
|
|
14162
|
+
function splitThinkingFormatText(text, { streaming = false } = {}) {
|
|
14163
|
+
let rest = String(text ?? "").trimStart();
|
|
14164
|
+
if (!rest) return null;
|
|
14165
|
+
if (!thinkingFormatOpenMatch(rest)) {
|
|
14166
|
+
return streaming && isPartialThinkingFormatOpenTag(rest)
|
|
14167
|
+
? { hasThinkingFormat: true, thinkingText: "", finalText: "", complete: false }
|
|
14168
|
+
: null;
|
|
14169
|
+
}
|
|
14170
|
+
|
|
14171
|
+
const thinkingParts = [];
|
|
14172
|
+
let open = thinkingFormatOpenMatch(rest);
|
|
14173
|
+
while (open) {
|
|
14174
|
+
const afterOpen = rest.slice(open.raw.length);
|
|
14175
|
+
const close = open.closeRegex.exec(afterOpen);
|
|
14176
|
+
if (!close) {
|
|
14177
|
+
thinkingParts.push(streaming ? stripPartialThinkingFormatClose(afterOpen, open.closeTag) : afterOpen);
|
|
14178
|
+
return { hasThinkingFormat: true, thinkingText: joinedThinkingFormatParts(thinkingParts), finalText: "", complete: false };
|
|
14179
|
+
}
|
|
14180
|
+
|
|
14181
|
+
thinkingParts.push(afterOpen.slice(0, close.index));
|
|
14182
|
+
rest = afterOpen.slice(close.index + close[0].length);
|
|
14183
|
+
const next = rest.trimStart();
|
|
14184
|
+
open = thinkingFormatOpenMatch(next);
|
|
14185
|
+
if (open) {
|
|
14186
|
+
rest = next;
|
|
14187
|
+
continue;
|
|
14188
|
+
}
|
|
14189
|
+
break;
|
|
14190
|
+
}
|
|
14191
|
+
|
|
14192
|
+
return {
|
|
14193
|
+
hasThinkingFormat: true,
|
|
14194
|
+
thinkingText: joinedThinkingFormatParts(thinkingParts),
|
|
14195
|
+
finalText: stripThinkingFormatOutputSeparator(rest),
|
|
14196
|
+
complete: true,
|
|
14197
|
+
};
|
|
14198
|
+
}
|
|
14199
|
+
|
|
14200
|
+
function appendThinkingFormatDisplayMessages(displayMessages, base, parsed) {
|
|
14201
|
+
const thinking = visibleThinkingText(parsed?.thinkingText || "");
|
|
14202
|
+
if (thinking) displayMessages.push({ ...base, role: "thinking", title: "thinking", content: thinking, thinking });
|
|
14203
|
+
}
|
|
14204
|
+
|
|
13449
14205
|
function isAssistantToolCallPart(part) {
|
|
13450
14206
|
return !!(part && typeof part === "object" && (part.type === "toolCall" || part.toolCall));
|
|
13451
14207
|
}
|
|
@@ -13495,6 +14251,13 @@ function assistantDisplayMessages(message) {
|
|
|
13495
14251
|
const base = { timestamp: message.timestamp };
|
|
13496
14252
|
const content = message.content;
|
|
13497
14253
|
if (typeof content === "string") {
|
|
14254
|
+
const parsed = splitThinkingFormatText(content);
|
|
14255
|
+
if (parsed?.hasThinkingFormat) {
|
|
14256
|
+
const displayMessages = [];
|
|
14257
|
+
appendThinkingFormatDisplayMessages(displayMessages, base, parsed);
|
|
14258
|
+
if (parsed.finalText.trim()) displayMessages.push({ ...message, title: "final output", content: parsed.finalText });
|
|
14259
|
+
return displayMessages;
|
|
14260
|
+
}
|
|
13498
14261
|
return content.trim() ? [{ ...message, title: "final output" }] : [];
|
|
13499
14262
|
}
|
|
13500
14263
|
if (!Array.isArray(content)) {
|
|
@@ -13518,6 +14281,16 @@ function assistantDisplayMessages(message) {
|
|
|
13518
14281
|
displayMessages.push({ ...base, role: "toolCall", title: `tool call: ${toolName}`, toolName, toolCallId, arguments: args, content: args });
|
|
13519
14282
|
continue;
|
|
13520
14283
|
}
|
|
14284
|
+
const primitiveText = part !== undefined && part !== null && typeof part !== "object" ? String(part) : "";
|
|
14285
|
+
const textForThinkingFormat = primitiveText || (part && typeof part === "object" && (part.type === "text" || typeof part.text === "string") ? assistantTextPartText(part) || part.text : "");
|
|
14286
|
+
if (textForThinkingFormat) {
|
|
14287
|
+
const parsed = splitThinkingFormatText(textForThinkingFormat);
|
|
14288
|
+
if (parsed?.hasThinkingFormat) {
|
|
14289
|
+
appendThinkingFormatDisplayMessages(displayMessages, base, parsed);
|
|
14290
|
+
if (parsed.finalText.trim() && !assistantHasToolCallAfter(content, index)) finalParts.push(part && typeof part === "object" ? { ...part, type: "text", text: parsed.finalText } : { type: "text", text: parsed.finalText });
|
|
14291
|
+
continue;
|
|
14292
|
+
}
|
|
14293
|
+
}
|
|
13521
14294
|
const finalPart = assistantFinalOutputPart(part);
|
|
13522
14295
|
if (finalPart) {
|
|
13523
14296
|
if (!assistantHasToolCallAfter(content, index)) finalParts.push(finalPart);
|
|
@@ -16683,6 +17456,12 @@ function removeStreamBubble() {
|
|
|
16683
17456
|
renderRunIndicator({ scroll: false });
|
|
16684
17457
|
}
|
|
16685
17458
|
|
|
17459
|
+
function streamRenderableAssistantText() {
|
|
17460
|
+
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
17461
|
+
const parsed = splitThinkingFormatText(assistantText, { streaming: true });
|
|
17462
|
+
return parsed?.hasThinkingFormat ? stripTodoProgressLines(parsed.finalText, { streaming: true }) : assistantText;
|
|
17463
|
+
}
|
|
17464
|
+
|
|
16686
17465
|
function scheduleStreamBubbleHide() {
|
|
16687
17466
|
if (!streamBubble) return;
|
|
16688
17467
|
const visibleForMs = streamBubbleVisibleSince ? performance.now() - streamBubbleVisibleSince : STREAM_OUTPUT_MIN_VISIBLE_MS;
|
|
@@ -16690,16 +17469,27 @@ function scheduleStreamBubbleHide() {
|
|
|
16690
17469
|
clearTimeout(streamBubbleHideTimer);
|
|
16691
17470
|
streamBubbleHideTimer = setTimeout(() => {
|
|
16692
17471
|
streamBubbleHideTimer = null;
|
|
16693
|
-
if (
|
|
17472
|
+
if (streamRenderableAssistantText() || !streamBubble) return;
|
|
16694
17473
|
removeStreamBubble();
|
|
16695
17474
|
}, delayMs);
|
|
16696
17475
|
}
|
|
16697
17476
|
|
|
17477
|
+
function syncStreamingThinkingFormat(assistantText) {
|
|
17478
|
+
const parsed = splitThinkingFormatText(assistantText, { streaming: true });
|
|
17479
|
+
if (!parsed?.hasThinkingFormat) return null;
|
|
17480
|
+
const thinking = visibleThinkingText(parsed.thinkingText);
|
|
17481
|
+
if (thinking) setStreamingThinkingText(thinking);
|
|
17482
|
+
if (parsed.complete && streamThinkingBubble) streamThinkingBubble.classList.add("complete");
|
|
17483
|
+
return parsed;
|
|
17484
|
+
}
|
|
17485
|
+
|
|
16698
17486
|
function renderStreamingAssistantText() {
|
|
16699
17487
|
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
16700
|
-
|
|
17488
|
+
const thinkingFormat = syncStreamingThinkingFormat(assistantText);
|
|
17489
|
+
const finalText = thinkingFormat?.hasThinkingFormat ? stripTodoProgressLines(thinkingFormat.finalText, { streaming: true }) : assistantText;
|
|
17490
|
+
if (finalText) {
|
|
16701
17491
|
ensureStreamBubble();
|
|
16702
|
-
renderStreamingMarkdown(streamText,
|
|
17492
|
+
renderStreamingMarkdown(streamText, finalText);
|
|
16703
17493
|
} else {
|
|
16704
17494
|
scheduleStreamBubbleHide();
|
|
16705
17495
|
}
|
|
@@ -16793,26 +17583,39 @@ function assistantStreamingMessage(event) {
|
|
|
16793
17583
|
return partial?.role === "assistant" ? partial : null;
|
|
16794
17584
|
}
|
|
16795
17585
|
|
|
16796
|
-
function assistantTextFromMessage(message) {
|
|
17586
|
+
function assistantTextFromMessage(message, { streaming = false } = {}) {
|
|
17587
|
+
void streaming;
|
|
16797
17588
|
const content = message?.content;
|
|
16798
17589
|
if (typeof content === "string") return content;
|
|
16799
17590
|
if (!Array.isArray(content)) return null;
|
|
16800
17591
|
const parts = [];
|
|
16801
17592
|
for (let index = 0; index < content.length; index += 1) {
|
|
16802
17593
|
const part = content[index];
|
|
16803
|
-
const text = assistantTextPartText(part);
|
|
17594
|
+
const text = assistantTextPartText(part) || (part && typeof part === "object" && typeof part.text === "string" ? part.text : part !== undefined && part !== null && typeof part !== "object" ? String(part) : "");
|
|
16804
17595
|
if (text && !assistantHasToolCallAfter(content, index)) parts.push(text);
|
|
16805
17596
|
}
|
|
16806
17597
|
return parts.length ? parts.join("\n\n") : "";
|
|
16807
17598
|
}
|
|
16808
17599
|
|
|
16809
|
-
function assistantThinkingTextFromMessage(message) {
|
|
17600
|
+
function assistantThinkingTextFromMessage(message, { streaming = false } = {}) {
|
|
16810
17601
|
const content = message?.content;
|
|
17602
|
+
if (typeof content === "string") {
|
|
17603
|
+
const parsed = splitThinkingFormatText(content, { streaming });
|
|
17604
|
+
return parsed?.hasThinkingFormat ? visibleThinkingText(parsed.thinkingText) : null;
|
|
17605
|
+
}
|
|
16811
17606
|
if (!Array.isArray(content)) return null;
|
|
16812
|
-
const parts =
|
|
16813
|
-
|
|
16814
|
-
|
|
16815
|
-
|
|
17607
|
+
const parts = [];
|
|
17608
|
+
for (const part of content) {
|
|
17609
|
+
if (part && typeof part === "object" && (part.type === "thinking" || typeof part.thinking === "string")) {
|
|
17610
|
+
const thinking = visibleThinkingText(assistantThinkingText(part));
|
|
17611
|
+
if (thinking.trim()) parts.push(thinking);
|
|
17612
|
+
continue;
|
|
17613
|
+
}
|
|
17614
|
+
const text = assistantTextPartText(part) || (part && typeof part === "object" && typeof part.text === "string" ? part.text : part !== undefined && part !== null && typeof part !== "object" ? String(part) : "");
|
|
17615
|
+
const parsed = splitThinkingFormatText(text, { streaming });
|
|
17616
|
+
const thinking = parsed?.hasThinkingFormat ? visibleThinkingText(parsed.thinkingText) : "";
|
|
17617
|
+
if (thinking.trim()) parts.push(thinking);
|
|
17618
|
+
}
|
|
16816
17619
|
return parts.length ? parts.join("\n\n") : "";
|
|
16817
17620
|
}
|
|
16818
17621
|
|
|
@@ -16826,7 +17629,7 @@ function setStreamingThinkingText(text) {
|
|
|
16826
17629
|
|
|
16827
17630
|
function syncStreamingThinkingFromMessage(event, { placeholder = "" } = {}) {
|
|
16828
17631
|
if (!thinkingOutputVisible) return true;
|
|
16829
|
-
const text = assistantThinkingTextFromMessage(assistantStreamingMessage(event));
|
|
17632
|
+
const text = assistantThinkingTextFromMessage(assistantStreamingMessage(event), { streaming: true });
|
|
16830
17633
|
if (text === null) return false;
|
|
16831
17634
|
return setStreamingThinkingText(text || placeholder);
|
|
16832
17635
|
}
|
|
@@ -16848,16 +17651,17 @@ function handleMessageUpdate(event) {
|
|
|
16848
17651
|
}
|
|
16849
17652
|
scrollChatToBottom();
|
|
16850
17653
|
} else if (update.type === "thinking_end") {
|
|
16851
|
-
const finalThinking = assistantThinkingTextFromMessage(assistantStreamingMessage(event)) || thinkingDeltaText(update);
|
|
17654
|
+
const finalThinking = assistantThinkingTextFromMessage(assistantStreamingMessage(event), { streaming: true }) || thinkingDeltaText(update);
|
|
16852
17655
|
if (finalThinking) setStreamingThinkingText(finalThinking);
|
|
16853
17656
|
streamThinkingBubble?.classList.add("complete");
|
|
16854
17657
|
setRunIndicatorActivity("Finished thinking; waiting for the next output or action…", { scroll: false });
|
|
16855
17658
|
} else if (update.type === "text_delta" || update.type === "text_end") {
|
|
16856
17659
|
const delta = update.type === "text_delta" ? update.delta || "" : "";
|
|
16857
|
-
const partialText = assistantTextFromMessage(assistantStreamingMessage(event));
|
|
17660
|
+
const partialText = assistantTextFromMessage(assistantStreamingMessage(event), { streaming: true });
|
|
16858
17661
|
if (typeof partialText === "string") streamRawText = partialText;
|
|
16859
17662
|
else if (update.type === "text_end" && typeof update.content === "string") streamRawText = update.content;
|
|
16860
17663
|
else streamRawText += delta;
|
|
17664
|
+
syncLiveTodoProgressWidgetFromText(streamRawText, event.tabId || activeTabId);
|
|
16861
17665
|
setRunIndicatorActivity("Writing response…", { scroll: false });
|
|
16862
17666
|
if (streamToolCallSeen || streamBubble) renderStreamingAssistantText();
|
|
16863
17667
|
else scheduleStreamingAssistantTextRender();
|
|
@@ -17171,6 +17975,7 @@ async function refreshModels(tabContext = activeTabContext()) {
|
|
|
17171
17975
|
if (!isCurrentTabContext(tabContext)) return;
|
|
17172
17976
|
availableModels = models;
|
|
17173
17977
|
footerScopedModels = scopedModels;
|
|
17978
|
+
footerScopedModels = orderedFooterScopedModels();
|
|
17174
17979
|
footerScopedModelPatterns = scopedModelPatterns;
|
|
17175
17980
|
footerScopedModelSource = scopedModelSource;
|
|
17176
17981
|
if (scopedModelError) addEvent(`failed to load scoped models: ${scopedModelError.message}`, "warn");
|
|
@@ -18472,18 +19277,17 @@ function handleExtensionUiRequest(request) {
|
|
|
18472
19277
|
}
|
|
18473
19278
|
case "setWidget": {
|
|
18474
19279
|
const widgetKey = request.widgetKey || request.id;
|
|
19280
|
+
const requestTabId = request.tabId || activeTabId;
|
|
18475
19281
|
if (widgetKey === "pi-remote-webui") {
|
|
18476
|
-
|
|
19282
|
+
setWidgetForTab(requestTabId, widgetKey, { ...request, widgetLines: undefined });
|
|
18477
19283
|
if (Array.isArray(request.widgetLines)) {
|
|
18478
19284
|
mirrorRemoteWebuiWidgetToTranscript(widgetKey, request.widgetLines, request);
|
|
18479
19285
|
showRemoteWebuiQrPopup(widgetKey, request.widgetLines, request);
|
|
18480
19286
|
} else {
|
|
18481
19287
|
closeRemoteWebuiQrPopup();
|
|
18482
19288
|
}
|
|
18483
|
-
} else if (Array.isArray(request.widgetLines)) {
|
|
18484
|
-
widgets.set(widgetKey, request);
|
|
18485
19289
|
} else {
|
|
18486
|
-
|
|
19290
|
+
setWidgetForTab(requestTabId, widgetKey, request);
|
|
18487
19291
|
}
|
|
18488
19292
|
updateOptionalFeatureAvailability();
|
|
18489
19293
|
renderWidgets();
|
|
@@ -18660,7 +19464,7 @@ function handleEvent(event) {
|
|
|
18660
19464
|
addTransientMessage({ role: "native", title: "/reload", content: `${event.tabTitle || "terminal"} reloaded. Keybindings, extensions, skills, prompts, and themes were refreshed by restarting the RPC tab${event.sessionFile ? ` and resuming ${event.sessionFile}` : ""}.`, level: "info" });
|
|
18661
19465
|
clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
18662
19466
|
statusEntries.clear();
|
|
18663
|
-
|
|
19467
|
+
clearWidgetsForTab(event.tabId || activeTabId);
|
|
18664
19468
|
latestBtwWidgetPayload = null;
|
|
18665
19469
|
btwWidgetDismissedId = "";
|
|
18666
19470
|
btwWidgetComposerOpen = false;
|