@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/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
- widgets.clear();
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 promptFooterGitBranchName() {
7695
- const value = window.prompt("New git branch name:", "");
7696
- if (value === null) return "";
7697
- return cleanStatusText(value);
7760
+ function footerBranchCreateType(value = footerBranchCreateDraft.type) {
7761
+ return slugifyGitBranchPart(value);
7698
7762
  }
7699
7763
 
7700
- async function createFooterGitBranch() {
7701
- const branchName = promptFooterGitBranchName();
7702
- if (!branchName) return;
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
- const createButton = make("button", "footer-model-option footer-branch-create-option");
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", footerScopedModelSource === "none" ? "No saved scope" : `Source: ${footerScopedModelSource}${footerScopedModelPatterns.length ? ` · ${footerScopedModelPatterns.join(", ")}` : ""}`));
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 button = make("button", `footer-model-option${selected ? " active" : ""}`);
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", () => applyFooterModel(model));
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.match(/^\[( |x|X|-)\]\s+(.+)$/);
9149
+ const item = parseTodoProgressItemLine(line);
8669
9150
  if (item) {
8670
- const mark = item[1].toLowerCase();
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 appendMarkdownCodeBlock(parent, code, language = "") {
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
- if (index < lines.length) index += 1;
12997
- appendMarkdownCodeBlock(parent, codeLines.join("\n"), language);
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
- widgets.delete(widgetKey);
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
- widgets.delete(widgetKey);
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
- widgets.clear();
19467
+ clearWidgetsForTab(event.tabId || activeTabId);
18797
19468
  latestBtwWidgetPayload = null;
18798
19469
  btwWidgetDismissedId = "";
18799
19470
  btwWidgetComposerOpen = false;