@firstpick/pi-package-webui 0.5.4 → 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 +703 -32
- 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 +28 -3
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";
|
|
@@ -534,6 +541,7 @@ const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
|
534
541
|
const sidePanelOverlayMedia = window.matchMedia?.(SIDE_PANEL_OVERLAY_QUERY) || mobileViewMedia;
|
|
535
542
|
const statusEntries = new Map();
|
|
536
543
|
const widgets = new Map();
|
|
544
|
+
const widgetsByTab = new Map();
|
|
537
545
|
const todoProgressWidgetExpandedByTab = new Map();
|
|
538
546
|
const releaseNpmOutputExpandedByTab = new Map();
|
|
539
547
|
const appRunnerDataByTab = new Map();
|
|
@@ -887,6 +895,7 @@ const GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP = [
|
|
|
887
895
|
"5. Return here to commit short, long, or typed input on that branch.",
|
|
888
896
|
"6. Push and Create PR will push upstream, run /pr, let you review, then run gh pr create.",
|
|
889
897
|
].join("\n");
|
|
898
|
+
const GIT_BRANCH_TYPE_SUGGESTIONS = ["feat", "fix", "change", "perf", "test", "chore", "refactor", "docs", "style", "build", "ci", "revert"];
|
|
890
899
|
const GIT_FOOTER_STATUS_SETUP_TOOLTIP = [
|
|
891
900
|
"git-footer-status-setup:",
|
|
892
901
|
"Store the GitHub username used when the Web UI initializes a no-repo directory.",
|
|
@@ -918,6 +927,12 @@ const GIT_INIT_STACK_TOOLTIP = [
|
|
|
918
927
|
"Choose a known stack or type one. The value is saved in this browser.",
|
|
919
928
|
"If left blank, Pi will inspect the codebase and fall back to sane default .gitignore patterns.",
|
|
920
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;
|
|
921
936
|
|
|
922
937
|
function make(tag, className, text) {
|
|
923
938
|
const node = document.createElement(tag);
|
|
@@ -4448,6 +4463,7 @@ function syncTabMetadata(nextTabs = []) {
|
|
|
4448
4463
|
actionFeedbackByTab.delete(tabId);
|
|
4449
4464
|
skillUsageByTab.delete(tabId);
|
|
4450
4465
|
tabMessagesCache.delete(tabId);
|
|
4466
|
+
widgetsByTab.delete(tabId);
|
|
4451
4467
|
clearGitWorkflowForTab(tabId);
|
|
4452
4468
|
}
|
|
4453
4469
|
}
|
|
@@ -4710,6 +4726,55 @@ function cancelPendingDialogs() {
|
|
|
4710
4726
|
if (elements.dialog.open) elements.dialog.close();
|
|
4711
4727
|
}
|
|
4712
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
|
+
|
|
4713
4778
|
function resetActiveTabUi() {
|
|
4714
4779
|
clearRefreshTimers();
|
|
4715
4780
|
clearLiveToolRenderQueue();
|
|
@@ -4727,7 +4792,7 @@ function resetActiveTabUi() {
|
|
|
4727
4792
|
latestMessagesSessionKey = "";
|
|
4728
4793
|
clearRunIndicatorActivity({ render: false });
|
|
4729
4794
|
statusEntries.clear();
|
|
4730
|
-
|
|
4795
|
+
restoreWidgetsForActiveTab();
|
|
4731
4796
|
transientMessages = [];
|
|
4732
4797
|
liveToolRuns.clear();
|
|
4733
4798
|
liveToolCards.clear();
|
|
@@ -5108,6 +5173,7 @@ async function switchTab(tabId) {
|
|
|
5108
5173
|
footerBranchPickerRequestSerial += 1;
|
|
5109
5174
|
saveActiveDraft();
|
|
5110
5175
|
cacheMessagesForTab(activeTabId);
|
|
5176
|
+
cacheWidgetsForTab(activeTabId);
|
|
5111
5177
|
const tabContext = setActiveTabId(tabId, { remember: true });
|
|
5112
5178
|
resetActiveTabUi();
|
|
5113
5179
|
renderTabs();
|
|
@@ -7691,15 +7757,71 @@ function confirmFooterGitBranchAction(branch, { create = false, requireConfirm =
|
|
|
7691
7757
|
return window.confirm(message);
|
|
7692
7758
|
}
|
|
7693
7759
|
|
|
7694
|
-
function
|
|
7695
|
-
|
|
7696
|
-
if (value === null) return "";
|
|
7697
|
-
return cleanStatusText(value);
|
|
7760
|
+
function footerBranchCreateType(value = footerBranchCreateDraft.type) {
|
|
7761
|
+
return slugifyGitBranchPart(value);
|
|
7698
7762
|
}
|
|
7699
7763
|
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
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
|
+
}
|
|
7703
7825
|
const tabContext = activeTabContext();
|
|
7704
7826
|
if (!confirmFooterGitBranchAction(branchName, { create: true, requireConfirm: true, tabContext })) return;
|
|
7705
7827
|
await applyFooterGitBranch(branchName, { create: true, tabContext, skipConfirm: true });
|
|
@@ -7720,6 +7842,7 @@ async function applyFooterGitBranch(branch, { create = false, tabContext = activ
|
|
|
7720
7842
|
footerBranchPickerOpen = false;
|
|
7721
7843
|
footerBranchPickerRequestSerial += 1;
|
|
7722
7844
|
footerBranchPickerState = { ...footerBranchPickerState, loading: false, switching: "", current: switchedBranch };
|
|
7845
|
+
if (create) updateFooterBranchCreateDraft({ name: "" });
|
|
7723
7846
|
applyOptimisticGitFooterBranch(switchedBranch, tabContext);
|
|
7724
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");
|
|
7725
7848
|
requestGitFooterWebuiPayload(tabContext, { force: true });
|
|
@@ -7737,6 +7860,124 @@ async function applyFooterGitBranch(branch, { create = false, tabContext = activ
|
|
|
7737
7860
|
}
|
|
7738
7861
|
}
|
|
7739
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
|
+
|
|
7740
7981
|
function renderFooterBranchPicker() {
|
|
7741
7982
|
const picker = make("div", "footer-model-picker footer-branch-picker");
|
|
7742
7983
|
picker.setAttribute("role", "listbox");
|
|
@@ -7753,7 +7994,7 @@ function renderFooterBranchPicker() {
|
|
|
7753
7994
|
return picker;
|
|
7754
7995
|
}
|
|
7755
7996
|
if (state.loading && state.branches.length === 0) {
|
|
7756
|
-
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));
|
|
7757
7998
|
return picker;
|
|
7758
7999
|
}
|
|
7759
8000
|
|
|
@@ -7761,16 +8002,11 @@ function renderFooterBranchPicker() {
|
|
|
7761
8002
|
if (!state.loading && !hasOtherBranches) {
|
|
7762
8003
|
const empty = make("div", "footer-model-picker-empty muted");
|
|
7763
8004
|
empty.append(make("strong", undefined, "No other local branches available."), make("span", undefined, " Create a branch from the current HEAD to continue."));
|
|
7764
|
-
|
|
7765
|
-
createButton.type = "button";
|
|
7766
|
-
createButton.append(
|
|
7767
|
-
make("span", "footer-model-option-main", "Create new branch"),
|
|
7768
|
-
make("span", "footer-model-option-name", "prompts for a name, confirms, then runs git switch -c"),
|
|
7769
|
-
);
|
|
7770
|
-
createButton.addEventListener("click", () => createFooterGitBranch().catch((error) => addEvent(error.message || String(error), "error")));
|
|
7771
|
-
picker.append(empty, createButton);
|
|
8005
|
+
picker.append(empty);
|
|
7772
8006
|
}
|
|
7773
8007
|
|
|
8008
|
+
picker.append(renderFooterBranchCreateForm(state));
|
|
8009
|
+
|
|
7774
8010
|
for (const branch of state.branches) {
|
|
7775
8011
|
const selected = branch.current || (!!state.current && branch.name === state.current);
|
|
7776
8012
|
const disabled = selected || state.loading || !!state.switching;
|
|
@@ -7790,6 +8026,162 @@ function renderFooterBranchPicker() {
|
|
|
7790
8026
|
return picker;
|
|
7791
8027
|
}
|
|
7792
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
|
+
|
|
7793
8185
|
async function applyFooterModel(model) {
|
|
7794
8186
|
if (!model?.provider || !model?.id) return;
|
|
7795
8187
|
const tabContext = activeTabContext();
|
|
@@ -7815,7 +8207,7 @@ function renderFooterModelPicker() {
|
|
|
7815
8207
|
picker.setAttribute("role", "listbox");
|
|
7816
8208
|
picker.setAttribute("aria-label", "Scoped models");
|
|
7817
8209
|
picker.append(make("div", "footer-model-picker-title", "Scoped models"));
|
|
7818
|
-
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"));
|
|
7819
8211
|
if (footerScopedModels.length === 0) {
|
|
7820
8212
|
const empty = make("div", "footer-model-picker-empty muted");
|
|
7821
8213
|
empty.append(
|
|
@@ -7826,10 +8218,16 @@ function renderFooterModelPicker() {
|
|
|
7826
8218
|
return picker;
|
|
7827
8219
|
}
|
|
7828
8220
|
const current = currentState?.model;
|
|
8221
|
+
footerScopedModels = orderedFooterScopedModels();
|
|
7829
8222
|
for (const model of footerScopedModels) {
|
|
7830
8223
|
const selected = current?.provider === model.provider && current?.id === model.id;
|
|
7831
|
-
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" : ""}`);
|
|
7832
8228
|
button.type = "button";
|
|
8229
|
+
button.draggable = false;
|
|
8230
|
+
button.dataset.footerModelKey = modelKey;
|
|
7833
8231
|
button.setAttribute("role", "option");
|
|
7834
8232
|
button.setAttribute("aria-selected", selected ? "true" : "false");
|
|
7835
8233
|
button.title = `${model.provider}/${model.id}${model.name ? ` · ${model.name}` : ""}`;
|
|
@@ -7837,7 +8235,19 @@ function renderFooterModelPicker() {
|
|
|
7837
8235
|
make("span", "footer-model-option-main", `${model.provider}/${model.id}`),
|
|
7838
8236
|
make("span", "footer-model-option-name", model.name || ""),
|
|
7839
8237
|
);
|
|
7840
|
-
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));
|
|
7841
8251
|
picker.append(button);
|
|
7842
8252
|
}
|
|
7843
8253
|
return picker;
|
|
@@ -8653,8 +9063,79 @@ function stripTodoProgressLines(text, { streaming = false } = {}) {
|
|
|
8653
9063
|
return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
8654
9064
|
}
|
|
8655
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
|
+
|
|
8656
9135
|
function parseTodoProgressWidget(lines) {
|
|
8657
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() : "";
|
|
8658
9139
|
const headerIndex = cleanLines.findIndex((line) => /^Todo\s+\d+\/\d+\s+done/i.test(line));
|
|
8659
9140
|
if (headerIndex === -1) return null;
|
|
8660
9141
|
|
|
@@ -8665,16 +9146,16 @@ function parseTodoProgressWidget(lines) {
|
|
|
8665
9146
|
const items = [];
|
|
8666
9147
|
let footer = "";
|
|
8667
9148
|
for (const line of cleanLines.slice(headerIndex + 1)) {
|
|
8668
|
-
const item = line
|
|
9149
|
+
const item = parseTodoProgressItemLine(line);
|
|
8669
9150
|
if (item) {
|
|
8670
|
-
|
|
8671
|
-
items.push({ status: mark === "x" ? "done" : mark === "-" ? "partial" : "todo", text: item[2].trim() });
|
|
9151
|
+
items.push(item);
|
|
8672
9152
|
} else if (/^Scroll\s+/i.test(line)) {
|
|
8673
9153
|
footer = line;
|
|
8674
9154
|
}
|
|
8675
9155
|
}
|
|
8676
9156
|
|
|
8677
9157
|
return {
|
|
9158
|
+
goal,
|
|
8678
9159
|
done: Number.parseInt(match[1], 10) || 0,
|
|
8679
9160
|
total: Number.parseInt(match[2], 10) || items.length,
|
|
8680
9161
|
partial: Number.parseInt(match[3] || "0", 10) || 0,
|
|
@@ -8709,6 +9190,7 @@ function renderTodoProgressWidget(_key, lines) {
|
|
|
8709
9190
|
const fill = make("span", "todo-widget-progress-fill");
|
|
8710
9191
|
fill.style.width = `${percent}%`;
|
|
8711
9192
|
progress.append(fill);
|
|
9193
|
+
if (todo.goal) summary.append(make("div", "todo-widget-goal", `Goal: ${todo.goal}`));
|
|
8712
9194
|
summary.append(header, progress);
|
|
8713
9195
|
|
|
8714
9196
|
const body = make("div", "todo-widget-body");
|
|
@@ -12888,14 +13370,201 @@ function appendMarkdownParagraph(parent, lines) {
|
|
|
12888
13370
|
parent.append(paragraph);
|
|
12889
13371
|
}
|
|
12890
13372
|
|
|
12891
|
-
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
|
+
}
|
|
12892
13560
|
const wrapper = make("div", "markdown-code-block");
|
|
12893
13561
|
if (language) wrapper.append(make("div", "markdown-code-language", language));
|
|
12894
13562
|
const pre = make("pre", "code-block markdown-code");
|
|
12895
13563
|
const codeNode = make("code", language ? `language-${language.replace(/[^a-z0-9_-]/gi, "")}` : "");
|
|
12896
|
-
codeNode.textContent = code.replace(/\n+$/g, "");
|
|
13564
|
+
codeNode.textContent = String(code || "").replace(/\n+$/g, "");
|
|
12897
13565
|
pre.append(codeNode);
|
|
12898
13566
|
wrapper.append(pre);
|
|
13567
|
+
attachMarkdownCodeCopyButton(wrapper);
|
|
12899
13568
|
parent.append(wrapper);
|
|
12900
13569
|
}
|
|
12901
13570
|
|
|
@@ -12993,8 +13662,9 @@ function renderMarkdownInto(parent, text) {
|
|
|
12993
13662
|
codeLines.push(lines[index]);
|
|
12994
13663
|
index += 1;
|
|
12995
13664
|
}
|
|
12996
|
-
|
|
12997
|
-
|
|
13665
|
+
const closed = index < lines.length;
|
|
13666
|
+
if (closed) index += 1;
|
|
13667
|
+
appendMarkdownCodeBlock(parent, codeLines.join("\n"), language, { closed });
|
|
12998
13668
|
continue;
|
|
12999
13669
|
}
|
|
13000
13670
|
if (markdownTableSeparator(lines[index + 1]) && line.includes("|")) {
|
|
@@ -16991,6 +17661,7 @@ function handleMessageUpdate(event) {
|
|
|
16991
17661
|
if (typeof partialText === "string") streamRawText = partialText;
|
|
16992
17662
|
else if (update.type === "text_end" && typeof update.content === "string") streamRawText = update.content;
|
|
16993
17663
|
else streamRawText += delta;
|
|
17664
|
+
syncLiveTodoProgressWidgetFromText(streamRawText, event.tabId || activeTabId);
|
|
16994
17665
|
setRunIndicatorActivity("Writing response…", { scroll: false });
|
|
16995
17666
|
if (streamToolCallSeen || streamBubble) renderStreamingAssistantText();
|
|
16996
17667
|
else scheduleStreamingAssistantTextRender();
|
|
@@ -17304,6 +17975,7 @@ async function refreshModels(tabContext = activeTabContext()) {
|
|
|
17304
17975
|
if (!isCurrentTabContext(tabContext)) return;
|
|
17305
17976
|
availableModels = models;
|
|
17306
17977
|
footerScopedModels = scopedModels;
|
|
17978
|
+
footerScopedModels = orderedFooterScopedModels();
|
|
17307
17979
|
footerScopedModelPatterns = scopedModelPatterns;
|
|
17308
17980
|
footerScopedModelSource = scopedModelSource;
|
|
17309
17981
|
if (scopedModelError) addEvent(`failed to load scoped models: ${scopedModelError.message}`, "warn");
|
|
@@ -18605,18 +19277,17 @@ function handleExtensionUiRequest(request) {
|
|
|
18605
19277
|
}
|
|
18606
19278
|
case "setWidget": {
|
|
18607
19279
|
const widgetKey = request.widgetKey || request.id;
|
|
19280
|
+
const requestTabId = request.tabId || activeTabId;
|
|
18608
19281
|
if (widgetKey === "pi-remote-webui") {
|
|
18609
|
-
|
|
19282
|
+
setWidgetForTab(requestTabId, widgetKey, { ...request, widgetLines: undefined });
|
|
18610
19283
|
if (Array.isArray(request.widgetLines)) {
|
|
18611
19284
|
mirrorRemoteWebuiWidgetToTranscript(widgetKey, request.widgetLines, request);
|
|
18612
19285
|
showRemoteWebuiQrPopup(widgetKey, request.widgetLines, request);
|
|
18613
19286
|
} else {
|
|
18614
19287
|
closeRemoteWebuiQrPopup();
|
|
18615
19288
|
}
|
|
18616
|
-
} else if (Array.isArray(request.widgetLines)) {
|
|
18617
|
-
widgets.set(widgetKey, request);
|
|
18618
19289
|
} else {
|
|
18619
|
-
|
|
19290
|
+
setWidgetForTab(requestTabId, widgetKey, request);
|
|
18620
19291
|
}
|
|
18621
19292
|
updateOptionalFeatureAvailability();
|
|
18622
19293
|
renderWidgets();
|
|
@@ -18793,7 +19464,7 @@ function handleEvent(event) {
|
|
|
18793
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" });
|
|
18794
19465
|
clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
18795
19466
|
statusEntries.clear();
|
|
18796
|
-
|
|
19467
|
+
clearWidgetsForTab(event.tabId || activeTabId);
|
|
18797
19468
|
latestBtwWidgetPayload = null;
|
|
18798
19469
|
btwWidgetDismissedId = "";
|
|
18799
19470
|
btwWidgetComposerOpen = false;
|