@firstpick/pi-package-webui 0.5.4 → 0.5.6
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 +55 -3
- package/package.json +4 -3
- package/public/app.js +1155 -100
- 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 +49 -14
- package/tests/streaming-ui-coupling.test.mjs +175 -0
package/public/app.js
CHANGED
|
@@ -257,10 +257,20 @@ let tabSeenCompletionSerials = new Map();
|
|
|
257
257
|
let streamBubble = null;
|
|
258
258
|
let streamText = null;
|
|
259
259
|
let streamRawText = "";
|
|
260
|
+
let streamThinkingRawText = "";
|
|
261
|
+
let streamDerivedTextCache = { rawText: null, assistantText: "", thinkingFormat: null, finalText: "" };
|
|
260
262
|
let streamBubbleVisibleSince = 0;
|
|
261
263
|
let streamBubbleHideTimer = null;
|
|
262
264
|
let streamTextRenderTimer = null;
|
|
265
|
+
let streamTextRenderFrame = null;
|
|
263
266
|
let streamToolCallSeen = false;
|
|
267
|
+
let streamToolCallBubble = null;
|
|
268
|
+
let streamToolCallText = null;
|
|
269
|
+
let streamToolCallRawArguments = "";
|
|
270
|
+
let streamToolCallName = "";
|
|
271
|
+
let streamToolCallId = "";
|
|
272
|
+
let streamToolCallContentIndex = null;
|
|
273
|
+
let streamToolCallComplete = false;
|
|
264
274
|
let streamThinkingBubble = null;
|
|
265
275
|
let streamThinking = null;
|
|
266
276
|
let streamMessageActive = false;
|
|
@@ -268,6 +278,8 @@ let runIndicatorBubble = null;
|
|
|
268
278
|
let runIndicatorText = null;
|
|
269
279
|
let runIndicatorMeta = null;
|
|
270
280
|
let runIndicatorTimer = null;
|
|
281
|
+
let runIndicatorRenderFrame = null;
|
|
282
|
+
let runIndicatorRenderScroll = false;
|
|
271
283
|
let runIndicatorGraceCheckTimer = null;
|
|
272
284
|
let runIndicatorLastStateCheckAt = 0;
|
|
273
285
|
let runIndicatorLocallyActive = false;
|
|
@@ -277,6 +289,7 @@ let refreshMessagesTimer = null;
|
|
|
277
289
|
let refreshStateTimer = null;
|
|
278
290
|
let refreshFooterTimer = null;
|
|
279
291
|
let refreshTabsTimer = null;
|
|
292
|
+
let tabsRenderFrame = null;
|
|
280
293
|
let foregroundReconcileTimer = null;
|
|
281
294
|
let eventSource = null;
|
|
282
295
|
let activeDialog = null;
|
|
@@ -366,6 +379,7 @@ let terminalTabsLayout = "top";
|
|
|
366
379
|
let webuiSettings = {};
|
|
367
380
|
let busyPromptBehavior = "followUp";
|
|
368
381
|
let composerModeRenderSignature = "";
|
|
382
|
+
let composerModeButtonsFrame = null;
|
|
369
383
|
let autocompleteMaxVisible = 12;
|
|
370
384
|
let doubleEscapeAction = "none";
|
|
371
385
|
let treeFilterMode = "default";
|
|
@@ -389,6 +403,11 @@ const contextUsageUnknownAfterCompactionByTab = new Map();
|
|
|
389
403
|
let autoFollowChat = true;
|
|
390
404
|
let chatFollowFrame = null;
|
|
391
405
|
let chatFollowSettleTimer = null;
|
|
406
|
+
let chatFollowNeedsSettle = false;
|
|
407
|
+
let liveWidgetRenderFrame = null;
|
|
408
|
+
let liveTodoProgressSyncFrame = null;
|
|
409
|
+
let liveTodoProgressPendingText = "";
|
|
410
|
+
let liveTodoProgressPendingTabId = null;
|
|
392
411
|
let lastChatProgrammaticScrollAt = 0;
|
|
393
412
|
let chatUserScrollIntentUntil = 0;
|
|
394
413
|
let mobileFooterExpanded = false;
|
|
@@ -397,7 +416,12 @@ let footerThinkingPickerOpen = false;
|
|
|
397
416
|
let footerAutoCompactionToggleInFlight = false;
|
|
398
417
|
let footerBranchPickerOpen = false;
|
|
399
418
|
let footerBranchPickerState = { loading: false, error: "", branches: [], current: "", root: "", switching: "", tabId: null };
|
|
419
|
+
let footerBranchCreateDraft = { type: "", name: "" };
|
|
400
420
|
let footerBranchPickerRequestSerial = 0;
|
|
421
|
+
let footerScopedModelDragKey = "";
|
|
422
|
+
let footerScopedModelLastDragOverKey = "";
|
|
423
|
+
let footerScopedModelPointerDrag = null;
|
|
424
|
+
let footerScopedModelSuppressClickUntil = 0;
|
|
401
425
|
let publishMenuOpen = false;
|
|
402
426
|
let maxVisualViewportHeight = 0;
|
|
403
427
|
let abortRequestInFlight = false;
|
|
@@ -437,6 +461,8 @@ const SKILL_USAGE_STORAGE_KEY = "pi-webui-skill-usage-v1";
|
|
|
437
461
|
const TERMINAL_TABS_LAYOUT_STORAGE_KEY = "pi-webui-terminal-tabs-layout";
|
|
438
462
|
const TERMINAL_CUSTOM_GROUPS_STORAGE_KEY = "pi-webui-terminal-custom-groups-v1";
|
|
439
463
|
const TERMINAL_TAB_DRAG_MIME = "application/x-pi-terminal-tab-id";
|
|
464
|
+
const FOOTER_SCOPED_MODEL_ORDER_STORAGE_KEY = "pi-webui-footer-scoped-model-order-v1";
|
|
465
|
+
const FOOTER_SCOPED_MODEL_POINTER_DRAG_THRESHOLD_PX = 6;
|
|
440
466
|
const TOOL_OUTPUT_EXPANDED_STORAGE_KEY = "pi-webui-tool-output-expanded";
|
|
441
467
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
442
468
|
const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background";
|
|
@@ -534,7 +560,10 @@ const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
|
534
560
|
const sidePanelOverlayMedia = window.matchMedia?.(SIDE_PANEL_OVERLAY_QUERY) || mobileViewMedia;
|
|
535
561
|
const statusEntries = new Map();
|
|
536
562
|
const widgets = new Map();
|
|
563
|
+
const widgetsByTab = new Map();
|
|
537
564
|
const todoProgressWidgetExpandedByTab = new Map();
|
|
565
|
+
const todoProgressGoalByTab = new Map();
|
|
566
|
+
const todoProgressSignatureByTab = new Map();
|
|
538
567
|
const releaseNpmOutputExpandedByTab = new Map();
|
|
539
568
|
const appRunnerDataByTab = new Map();
|
|
540
569
|
const appRunnerInputDraftByRun = new Map();
|
|
@@ -887,6 +916,7 @@ const GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP = [
|
|
|
887
916
|
"5. Return here to commit short, long, or typed input on that branch.",
|
|
888
917
|
"6. Push and Create PR will push upstream, run /pr, let you review, then run gh pr create.",
|
|
889
918
|
].join("\n");
|
|
919
|
+
const GIT_BRANCH_TYPE_SUGGESTIONS = ["feat", "fix", "change", "perf", "test", "chore", "refactor", "docs", "style", "build", "ci", "revert"];
|
|
890
920
|
const GIT_FOOTER_STATUS_SETUP_TOOLTIP = [
|
|
891
921
|
"git-footer-status-setup:",
|
|
892
922
|
"Store the GitHub username used when the Web UI initializes a no-repo directory.",
|
|
@@ -918,6 +948,12 @@ const GIT_INIT_STACK_TOOLTIP = [
|
|
|
918
948
|
"Choose a known stack or type one. The value is saved in this browser.",
|
|
919
949
|
"If left blank, Pi will inspect the codebase and fall back to sane default .gitignore patterns.",
|
|
920
950
|
].join("\n");
|
|
951
|
+
const MERMAID_MODULE_URL = "/vendor/mermaid/mermaid.esm.min.mjs";
|
|
952
|
+
const MERMAID_LANGUAGES = new Set(["mermaid", "mmd"]);
|
|
953
|
+
const MERMAID_MAX_TEXT_SIZE = 100_000;
|
|
954
|
+
let mermaidModulePromise = null;
|
|
955
|
+
let mermaidThemeSignature = "";
|
|
956
|
+
let mermaidRenderSequence = 0;
|
|
921
957
|
|
|
922
958
|
function make(tag, className, text) {
|
|
923
959
|
const node = document.createElement(tag);
|
|
@@ -1868,6 +1904,17 @@ function trackSkillsFromMessages(messages = latestMessages, tabId = activeTabId)
|
|
|
1868
1904
|
for (const message of messages || []) trackSkillsFromMessage(tabId, message);
|
|
1869
1905
|
}
|
|
1870
1906
|
|
|
1907
|
+
function assistantMessageUpdateType(event) {
|
|
1908
|
+
return event?.assistantMessageEvent?.type || "";
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
function eventMayAffectSkillUsage(event) {
|
|
1912
|
+
const type = event?.type || "";
|
|
1913
|
+
return ["tool_execution_start", "tool_execution_update", "tool_execution_end"].includes(type)
|
|
1914
|
+
|| (type === "message_update" && assistantMessageUpdateType(event) === "toolcall_start")
|
|
1915
|
+
|| (type === "response" && event.command === "new_session");
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1871
1918
|
function trackSkillsFromEvent(event) {
|
|
1872
1919
|
const tabId = event?.tabId || activeTabId;
|
|
1873
1920
|
if (!tabId || !event) return;
|
|
@@ -1875,11 +1922,9 @@ function trackSkillsFromEvent(event) {
|
|
|
1875
1922
|
trackSkillsFromToolInvocation(tabId, event.toolName, event.args, { sourcePrefix: `event:${event.type}` });
|
|
1876
1923
|
return;
|
|
1877
1924
|
}
|
|
1878
|
-
if (event
|
|
1925
|
+
if (assistantMessageUpdateType(event) === "toolcall_start") {
|
|
1879
1926
|
const update = event.assistantMessageEvent || {};
|
|
1880
|
-
|
|
1881
|
-
trackSkillsFromToolInvocation(tabId, update.name || update.toolName || update.toolCall?.name, update.arguments || update.args || update.toolCall?.arguments || {}, { sourcePrefix: "event:message_update" });
|
|
1882
|
-
}
|
|
1927
|
+
trackSkillsFromToolInvocation(tabId, update.name || update.toolName || update.toolCall?.name, update.arguments || update.args || update.toolCall?.arguments || {}, { sourcePrefix: "event:message_update" });
|
|
1883
1928
|
return;
|
|
1884
1929
|
}
|
|
1885
1930
|
if (event.type === "response" && event.command === "new_session") {
|
|
@@ -2164,6 +2209,16 @@ function updateComposerModeButtons() {
|
|
|
2164
2209
|
document.body.classList.toggle("pi-run-active", runActive || abortAvailable);
|
|
2165
2210
|
}
|
|
2166
2211
|
|
|
2212
|
+
function scheduleComposerModeButtonsUpdate() {
|
|
2213
|
+
if (composerModeButtonsFrame !== null) return;
|
|
2214
|
+
const flush = () => {
|
|
2215
|
+
composerModeButtonsFrame = null;
|
|
2216
|
+
updateComposerModeButtons();
|
|
2217
|
+
};
|
|
2218
|
+
if (typeof requestAnimationFrame === "function") composerModeButtonsFrame = requestAnimationFrame(flush);
|
|
2219
|
+
else composerModeButtonsFrame = setTimeout(flush, 0);
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2167
2222
|
function isFooterPickerOpen() {
|
|
2168
2223
|
return footerModelPickerOpen || footerThinkingPickerOpen || footerBranchPickerOpen;
|
|
2169
2224
|
}
|
|
@@ -2501,7 +2556,7 @@ function messageCopyText(message, body = null) {
|
|
|
2501
2556
|
return sections.join("\n\n").trimEnd() || messageCopyFallbackText(body);
|
|
2502
2557
|
}
|
|
2503
2558
|
if (message.role === "thinking") return visibleThinkingText(message.thinking || textFromContent(message.content)).trimEnd() || messageCopyFallbackText(body);
|
|
2504
|
-
if (message.role === "toolCall") return
|
|
2559
|
+
if (message.role === "toolCall") return toolCallDisplayText(message).trimEnd() || messageCopyFallbackText(body);
|
|
2505
2560
|
if (message.role === "assistantEvent") {
|
|
2506
2561
|
return (typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? {}, null, 2)).trimEnd();
|
|
2507
2562
|
}
|
|
@@ -4448,6 +4503,7 @@ function syncTabMetadata(nextTabs = []) {
|
|
|
4448
4503
|
actionFeedbackByTab.delete(tabId);
|
|
4449
4504
|
skillUsageByTab.delete(tabId);
|
|
4450
4505
|
tabMessagesCache.delete(tabId);
|
|
4506
|
+
widgetsByTab.delete(tabId);
|
|
4451
4507
|
clearGitWorkflowForTab(tabId);
|
|
4452
4508
|
}
|
|
4453
4509
|
}
|
|
@@ -4528,7 +4584,7 @@ function markTabWorkingLocally(tabId = activeTabId) {
|
|
|
4528
4584
|
const previous = activityForTab(tab);
|
|
4529
4585
|
const next = normalizeTabActivity({ ...previous, status: "working", isWorking: true });
|
|
4530
4586
|
tabActivities.set(tabId, next);
|
|
4531
|
-
if (tabActivityStateChanged(previous, next))
|
|
4587
|
+
if (tabActivityStateChanged(previous, next)) scheduleTabsRender();
|
|
4532
4588
|
return true;
|
|
4533
4589
|
}
|
|
4534
4590
|
|
|
@@ -4538,7 +4594,7 @@ function markTabIdleLocally(tabId = activeTabId) {
|
|
|
4538
4594
|
const previous = activityForTab(tab);
|
|
4539
4595
|
const next = normalizeTabActivity({ ...previous, status: "idle", isWorking: false });
|
|
4540
4596
|
tabActivities.set(tabId, next);
|
|
4541
|
-
if (tabActivityStateChanged(previous, next))
|
|
4597
|
+
if (tabActivityStateChanged(previous, next)) scheduleTabsRender();
|
|
4542
4598
|
return true;
|
|
4543
4599
|
}
|
|
4544
4600
|
|
|
@@ -4554,7 +4610,7 @@ function markTabDoneLocally(tabId = activeTabId) {
|
|
|
4554
4610
|
lastCompletedAt: new Date().toISOString(),
|
|
4555
4611
|
});
|
|
4556
4612
|
tabActivities.set(tabId, next);
|
|
4557
|
-
if (tabActivityStateChanged(previous, next))
|
|
4613
|
+
if (tabActivityStateChanged(previous, next)) scheduleTabsRender();
|
|
4558
4614
|
return true;
|
|
4559
4615
|
}
|
|
4560
4616
|
|
|
@@ -4594,7 +4650,7 @@ function markTabOutputSeen(tabId = activeTabId, { force = false } = {}) {
|
|
|
4594
4650
|
const previousSerial = tabSeenCompletionSerials.get(tabId) ?? 0;
|
|
4595
4651
|
if (previousSerial >= completionSerial) return false;
|
|
4596
4652
|
tabSeenCompletionSerials.set(tabId, completionSerial);
|
|
4597
|
-
|
|
4653
|
+
scheduleTabsRender();
|
|
4598
4654
|
return true;
|
|
4599
4655
|
}
|
|
4600
4656
|
|
|
@@ -4614,7 +4670,14 @@ function ingestEventTabActivity(event) {
|
|
|
4614
4670
|
const next = setTabActivity(event.tabId, event.tabActivity);
|
|
4615
4671
|
changed = tabActivityStateChanged(previous, next) || changed;
|
|
4616
4672
|
}
|
|
4617
|
-
if (changed)
|
|
4673
|
+
if (changed) scheduleTabsRender();
|
|
4674
|
+
}
|
|
4675
|
+
|
|
4676
|
+
function eventHasTabActivityPayload(event) {
|
|
4677
|
+
return !!(
|
|
4678
|
+
event?.tabId &&
|
|
4679
|
+
(event.tabTitle || event.tabActivity || Object.prototype.hasOwnProperty.call(event, "pendingExtensionUiRequestCount"))
|
|
4680
|
+
);
|
|
4618
4681
|
}
|
|
4619
4682
|
|
|
4620
4683
|
function trackAutoRetryStateFromEvent(event) {
|
|
@@ -4710,6 +4773,96 @@ function cancelPendingDialogs() {
|
|
|
4710
4773
|
if (elements.dialog.open) elements.dialog.close();
|
|
4711
4774
|
}
|
|
4712
4775
|
|
|
4776
|
+
function widgetCacheForTab(tabId = activeTabId, { create = true } = {}) {
|
|
4777
|
+
if (!tabId) return null;
|
|
4778
|
+
let cache = widgetsByTab.get(tabId);
|
|
4779
|
+
if (!cache && create) {
|
|
4780
|
+
cache = new Map();
|
|
4781
|
+
widgetsByTab.set(tabId, cache);
|
|
4782
|
+
}
|
|
4783
|
+
return cache || null;
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
function cacheWidgetsForTab(tabId = activeTabId) {
|
|
4787
|
+
if (!tabId) return;
|
|
4788
|
+
if (widgets.size === 0) {
|
|
4789
|
+
widgetsByTab.delete(tabId);
|
|
4790
|
+
return;
|
|
4791
|
+
}
|
|
4792
|
+
widgetsByTab.set(tabId, new Map(widgets));
|
|
4793
|
+
}
|
|
4794
|
+
|
|
4795
|
+
function restoreWidgetsForActiveTab() {
|
|
4796
|
+
widgets.clear();
|
|
4797
|
+
const cache = widgetCacheForTab(activeTabId, { create: false });
|
|
4798
|
+
if (!cache) return;
|
|
4799
|
+
for (const [key, value] of cache) widgets.set(key, value);
|
|
4800
|
+
}
|
|
4801
|
+
|
|
4802
|
+
function todoProgressSignatureFromLines(lines = []) {
|
|
4803
|
+
const parsed = parseTodoProgressWidget(lines);
|
|
4804
|
+
if (!parsed) return JSON.stringify(lines.map(stripAnsi));
|
|
4805
|
+
return JSON.stringify({ goal: parsed.goal, done: parsed.done, total: parsed.total, partial: parsed.partial, items: parsed.items, footer: parsed.footer });
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
function rememberTodoProgressGoalForTab(tabId, widgetKey, request) {
|
|
4809
|
+
if (widgetKey !== "todo-progress" || !tabId) return;
|
|
4810
|
+
if (!Array.isArray(request?.widgetLines)) {
|
|
4811
|
+
todoProgressGoalByTab.delete(tabId);
|
|
4812
|
+
todoProgressSignatureByTab.delete(tabId);
|
|
4813
|
+
return;
|
|
4814
|
+
}
|
|
4815
|
+
const parsed = parseTodoProgressWidget(request.widgetLines);
|
|
4816
|
+
if (parsed?.goal) todoProgressGoalByTab.set(tabId, parsed.goal);
|
|
4817
|
+
todoProgressSignatureByTab.set(tabId, todoProgressSignatureFromLines(request.widgetLines));
|
|
4818
|
+
}
|
|
4819
|
+
|
|
4820
|
+
function widgetRequestEquivalent(a, b) {
|
|
4821
|
+
const aHasLines = Array.isArray(a?.widgetLines);
|
|
4822
|
+
const bHasLines = Array.isArray(b?.widgetLines);
|
|
4823
|
+
if (aHasLines !== bHasLines) return false;
|
|
4824
|
+
if (!aHasLines) return true;
|
|
4825
|
+
if (a.widgetLines.length !== b.widgetLines.length) return false;
|
|
4826
|
+
return a.widgetLines.every((line, index) => String(line) === String(b.widgetLines[index]));
|
|
4827
|
+
}
|
|
4828
|
+
|
|
4829
|
+
function setWidgetForTab(tabId, widgetKey, request) {
|
|
4830
|
+
if (!widgetKey) return false;
|
|
4831
|
+
const targetTabId = tabId || activeTabId;
|
|
4832
|
+
const cache = widgetCacheForTab(targetTabId);
|
|
4833
|
+
const hasLines = Array.isArray(request?.widgetLines);
|
|
4834
|
+
const current = cache?.get(widgetKey) || (targetTabId === activeTabId ? widgets.get(widgetKey) : undefined);
|
|
4835
|
+
if (widgetRequestEquivalent(current, request)) return false;
|
|
4836
|
+
|
|
4837
|
+
rememberTodoProgressGoalForTab(targetTabId, widgetKey, request);
|
|
4838
|
+
|
|
4839
|
+
if (cache) {
|
|
4840
|
+
if (hasLines) cache.set(widgetKey, request);
|
|
4841
|
+
else cache.delete(widgetKey);
|
|
4842
|
+
if (cache.size === 0) widgetsByTab.delete(targetTabId);
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4845
|
+
if (targetTabId === activeTabId) {
|
|
4846
|
+
if (hasLines) widgets.set(widgetKey, request);
|
|
4847
|
+
else widgets.delete(widgetKey);
|
|
4848
|
+
}
|
|
4849
|
+
|
|
4850
|
+
return true;
|
|
4851
|
+
}
|
|
4852
|
+
|
|
4853
|
+
function clearWidgetsForTab(tabId = activeTabId) {
|
|
4854
|
+
if (tabId) {
|
|
4855
|
+
widgetsByTab.delete(tabId);
|
|
4856
|
+
todoProgressGoalByTab.delete(tabId);
|
|
4857
|
+
todoProgressSignatureByTab.delete(tabId);
|
|
4858
|
+
}
|
|
4859
|
+
if (!tabId || tabId === activeTabId) widgets.clear();
|
|
4860
|
+
if (!tabId) {
|
|
4861
|
+
todoProgressGoalByTab.clear();
|
|
4862
|
+
todoProgressSignatureByTab.clear();
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
|
|
4713
4866
|
function resetActiveTabUi() {
|
|
4714
4867
|
clearRefreshTimers();
|
|
4715
4868
|
clearLiveToolRenderQueue();
|
|
@@ -4727,7 +4880,7 @@ function resetActiveTabUi() {
|
|
|
4727
4880
|
latestMessagesSessionKey = "";
|
|
4728
4881
|
clearRunIndicatorActivity({ render: false });
|
|
4729
4882
|
statusEntries.clear();
|
|
4730
|
-
|
|
4883
|
+
restoreWidgetsForActiveTab();
|
|
4731
4884
|
transientMessages = [];
|
|
4732
4885
|
liveToolRuns.clear();
|
|
4733
4886
|
liveToolCards.clear();
|
|
@@ -5050,6 +5203,16 @@ function moveNewTabMenuFocus(delta) {
|
|
|
5050
5203
|
items[nextIndex].focus({ preventScroll: true });
|
|
5051
5204
|
}
|
|
5052
5205
|
|
|
5206
|
+
function scheduleTabsRender() {
|
|
5207
|
+
if (tabsRenderFrame !== null) return;
|
|
5208
|
+
const flush = () => {
|
|
5209
|
+
tabsRenderFrame = null;
|
|
5210
|
+
renderTabs();
|
|
5211
|
+
};
|
|
5212
|
+
if (typeof requestAnimationFrame === "function") tabsRenderFrame = requestAnimationFrame(flush);
|
|
5213
|
+
else tabsRenderFrame = setTimeout(flush, 0);
|
|
5214
|
+
}
|
|
5215
|
+
|
|
5053
5216
|
function renderTabs() {
|
|
5054
5217
|
if (deferUiRenderDuringPointerActivation("tabs", renderTabs)) return;
|
|
5055
5218
|
const active = activeTab();
|
|
@@ -5108,6 +5271,7 @@ async function switchTab(tabId) {
|
|
|
5108
5271
|
footerBranchPickerRequestSerial += 1;
|
|
5109
5272
|
saveActiveDraft();
|
|
5110
5273
|
cacheMessagesForTab(activeTabId);
|
|
5274
|
+
cacheWidgetsForTab(activeTabId);
|
|
5111
5275
|
const tabContext = setActiveTabId(tabId, { remember: true });
|
|
5112
5276
|
resetActiveTabUi();
|
|
5113
5277
|
renderTabs();
|
|
@@ -7691,15 +7855,71 @@ function confirmFooterGitBranchAction(branch, { create = false, requireConfirm =
|
|
|
7691
7855
|
return window.confirm(message);
|
|
7692
7856
|
}
|
|
7693
7857
|
|
|
7694
|
-
function
|
|
7695
|
-
|
|
7696
|
-
if (value === null) return "";
|
|
7697
|
-
return cleanStatusText(value);
|
|
7858
|
+
function footerBranchCreateType(value = footerBranchCreateDraft.type) {
|
|
7859
|
+
return slugifyGitBranchPart(value);
|
|
7698
7860
|
}
|
|
7699
7861
|
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7862
|
+
function slugifyGitBranchName(value) {
|
|
7863
|
+
return cleanStatusText(value)
|
|
7864
|
+
.split("/")
|
|
7865
|
+
.map((part) => slugifyGitBranchPart(part))
|
|
7866
|
+
.filter(Boolean)
|
|
7867
|
+
.join("/");
|
|
7868
|
+
}
|
|
7869
|
+
|
|
7870
|
+
function footerBranchCreateName() {
|
|
7871
|
+
const type = footerBranchCreateType();
|
|
7872
|
+
const rawName = cleanStatusText(footerBranchCreateDraft.name);
|
|
7873
|
+
if (!type || !rawName) return "";
|
|
7874
|
+
return slugifyGitBranchName(`${type}/${rawName}`);
|
|
7875
|
+
}
|
|
7876
|
+
|
|
7877
|
+
function footerBranchCreatePreviewName() {
|
|
7878
|
+
const type = footerBranchCreateType() || "<type>";
|
|
7879
|
+
const rawName = cleanStatusText(footerBranchCreateDraft.name);
|
|
7880
|
+
const name = rawName ? slugifyGitBranchName(rawName) : "<branch-name>";
|
|
7881
|
+
return `${type}/${name}`;
|
|
7882
|
+
}
|
|
7883
|
+
|
|
7884
|
+
function updateFooterBranchCreateDraft(patch = {}) {
|
|
7885
|
+
const has = (key) => Object.prototype.hasOwnProperty.call(patch, key);
|
|
7886
|
+
footerBranchCreateDraft = {
|
|
7887
|
+
type: has("type") ? footerBranchCreateType(patch.type) : footerBranchCreateType(),
|
|
7888
|
+
name: has("name") ? String(patch.name || "") : footerBranchCreateDraft.name,
|
|
7889
|
+
};
|
|
7890
|
+
}
|
|
7891
|
+
|
|
7892
|
+
function quoteGitBranchForDisplay(branch) {
|
|
7893
|
+
return `'${String(branch || "").replace(/'/g, `'\\''`)}'`;
|
|
7894
|
+
}
|
|
7895
|
+
|
|
7896
|
+
function gitSwitchCreateCommandDisplay(branch) {
|
|
7897
|
+
return `git switch -c ${quoteGitBranchForDisplay(branch)}`;
|
|
7898
|
+
}
|
|
7899
|
+
|
|
7900
|
+
function footerBranchCreateTooltip(branchName = footerBranchCreateName()) {
|
|
7901
|
+
const command = gitSwitchCreateCommandDisplay(branchName || footerBranchCreatePreviewName());
|
|
7902
|
+
return [
|
|
7903
|
+
"Create new branch",
|
|
7904
|
+
"A branch is a safe workspace for your changes.",
|
|
7905
|
+
"",
|
|
7906
|
+
`This will run: ${command}`,
|
|
7907
|
+
"",
|
|
7908
|
+
"What happens:",
|
|
7909
|
+
"• creates the branch from the current code",
|
|
7910
|
+
"• switches this tab to that branch",
|
|
7911
|
+
"• does not commit, push, or delete anything",
|
|
7912
|
+
"",
|
|
7913
|
+
"Tip: use short lowercase words, e.g. fix/login-button.",
|
|
7914
|
+
].join("\n");
|
|
7915
|
+
}
|
|
7916
|
+
|
|
7917
|
+
async function createFooterGitBranch(branch = footerBranchCreateName()) {
|
|
7918
|
+
const branchName = cleanStatusText(branch);
|
|
7919
|
+
if (!branchName) {
|
|
7920
|
+
addEvent("Enter a branch name before creating a new git branch.", "warn");
|
|
7921
|
+
return;
|
|
7922
|
+
}
|
|
7703
7923
|
const tabContext = activeTabContext();
|
|
7704
7924
|
if (!confirmFooterGitBranchAction(branchName, { create: true, requireConfirm: true, tabContext })) return;
|
|
7705
7925
|
await applyFooterGitBranch(branchName, { create: true, tabContext, skipConfirm: true });
|
|
@@ -7720,6 +7940,7 @@ async function applyFooterGitBranch(branch, { create = false, tabContext = activ
|
|
|
7720
7940
|
footerBranchPickerOpen = false;
|
|
7721
7941
|
footerBranchPickerRequestSerial += 1;
|
|
7722
7942
|
footerBranchPickerState = { ...footerBranchPickerState, loading: false, switching: "", current: switchedBranch };
|
|
7943
|
+
if (create) updateFooterBranchCreateDraft({ name: "" });
|
|
7723
7944
|
applyOptimisticGitFooterBranch(switchedBranch, tabContext);
|
|
7724
7945
|
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
7946
|
requestGitFooterWebuiPayload(tabContext, { force: true });
|
|
@@ -7737,6 +7958,124 @@ async function applyFooterGitBranch(branch, { create = false, tabContext = activ
|
|
|
7737
7958
|
}
|
|
7738
7959
|
}
|
|
7739
7960
|
|
|
7961
|
+
function renderFooterBranchCreateForm(state = footerBranchPickerState) {
|
|
7962
|
+
const form = make("form", "footer-branch-create-form");
|
|
7963
|
+
form.setAttribute("aria-label", "Create new git branch");
|
|
7964
|
+
|
|
7965
|
+
const header = make("div", "footer-branch-create-header");
|
|
7966
|
+
header.append(make("strong", "footer-branch-create-title", "Create new branch"));
|
|
7967
|
+
|
|
7968
|
+
const fields = make("div", "footer-branch-create-fields");
|
|
7969
|
+
const typeField = make("div", "footer-branch-create-type-field");
|
|
7970
|
+
const typeInput = make("input", "footer-branch-create-dropdown-inputfield");
|
|
7971
|
+
typeInput.type = "text";
|
|
7972
|
+
typeInput.value = footerBranchCreateType();
|
|
7973
|
+
typeInput.placeholder = "type";
|
|
7974
|
+
typeInput.setAttribute("aria-label", "Branch type suggestion or custom prefix");
|
|
7975
|
+
typeInput.setAttribute("aria-haspopup", "listbox");
|
|
7976
|
+
typeInput.autocomplete = "off";
|
|
7977
|
+
typeInput.autocapitalize = "none";
|
|
7978
|
+
typeInput.spellcheck = false;
|
|
7979
|
+
|
|
7980
|
+
const typeSuggestions = make("div", "footer-branch-type-suggestions");
|
|
7981
|
+
typeSuggestions.setAttribute("role", "listbox");
|
|
7982
|
+
typeSuggestions.hidden = true;
|
|
7983
|
+
const renderTypeSuggestions = () => {
|
|
7984
|
+
const filter = slugifyGitBranchPart(typeInput.value);
|
|
7985
|
+
const matches = GIT_BRANCH_TYPE_SUGGESTIONS.filter((type) => !filter || type.includes(filter));
|
|
7986
|
+
const shown = matches.length ? matches : GIT_BRANCH_TYPE_SUGGESTIONS;
|
|
7987
|
+
typeSuggestions.replaceChildren(...shown.map((type) => {
|
|
7988
|
+
const button = make("button", `footer-branch-type-suggestion${footerBranchCreateType(typeInput.value) === type ? " active" : ""}`, type);
|
|
7989
|
+
button.type = "button";
|
|
7990
|
+
button.setAttribute("role", "option");
|
|
7991
|
+
button.setAttribute("aria-selected", footerBranchCreateType(typeInput.value) === type ? "true" : "false");
|
|
7992
|
+
button.addEventListener("pointerdown", (event) => event.preventDefault());
|
|
7993
|
+
button.addEventListener("click", () => {
|
|
7994
|
+
typeInput.value = type;
|
|
7995
|
+
updateFooterBranchCreateDraft({ type });
|
|
7996
|
+
updatePreview();
|
|
7997
|
+
typeSuggestions.hidden = true;
|
|
7998
|
+
nameInput.focus();
|
|
7999
|
+
});
|
|
8000
|
+
return button;
|
|
8001
|
+
}));
|
|
8002
|
+
};
|
|
8003
|
+
|
|
8004
|
+
const slash = make("span", "footer-branch-create-slash", "/");
|
|
8005
|
+
|
|
8006
|
+
const nameInput = make("input", "footer-branch-create-input-field");
|
|
8007
|
+
nameInput.type = "text";
|
|
8008
|
+
nameInput.value = footerBranchCreateDraft.name;
|
|
8009
|
+
nameInput.placeholder = "short-feature-name";
|
|
8010
|
+
nameInput.autocomplete = "off";
|
|
8011
|
+
nameInput.autocapitalize = "none";
|
|
8012
|
+
nameInput.spellcheck = false;
|
|
8013
|
+
nameInput.setAttribute("aria-label", "New branch name");
|
|
8014
|
+
|
|
8015
|
+
const submitButton = make("button", "footer-branch-create-submit", state.switching ? "Creating…" : "Create new branch");
|
|
8016
|
+
submitButton.type = "submit";
|
|
8017
|
+
|
|
8018
|
+
const preview = make("div", "footer-branch-create-preview");
|
|
8019
|
+
const updatePreview = () => {
|
|
8020
|
+
const branchName = footerBranchCreateName();
|
|
8021
|
+
preview.textContent = gitSwitchCreateCommandDisplay(branchName || footerBranchCreatePreviewName());
|
|
8022
|
+
const submitDisabled = Boolean(state.switching) || !branchName;
|
|
8023
|
+
submitButton.disabled = false;
|
|
8024
|
+
submitButton.classList.toggle("footer-branch-create-submit-disabled", submitDisabled);
|
|
8025
|
+
submitButton.setAttribute("aria-disabled", submitDisabled ? "true" : "false");
|
|
8026
|
+
submitButton.dataset.tooltip = footerBranchCreateTooltip(branchName);
|
|
8027
|
+
submitButton.setAttribute("aria-label", branchName ? `Create and switch to ${branchName}` : "Create new branch: enter both a branch type and name first");
|
|
8028
|
+
submitButton.removeAttribute("title");
|
|
8029
|
+
};
|
|
8030
|
+
|
|
8031
|
+
typeInput.addEventListener("focus", () => {
|
|
8032
|
+
renderTypeSuggestions();
|
|
8033
|
+
typeSuggestions.hidden = false;
|
|
8034
|
+
});
|
|
8035
|
+
typeInput.addEventListener("blur", () => {
|
|
8036
|
+
setTimeout(() => { typeSuggestions.hidden = true; }, 120);
|
|
8037
|
+
});
|
|
8038
|
+
typeInput.addEventListener("input", () => {
|
|
8039
|
+
updateFooterBranchCreateDraft({ type: typeInput.value });
|
|
8040
|
+
updatePreview();
|
|
8041
|
+
renderTypeSuggestions();
|
|
8042
|
+
typeSuggestions.hidden = false;
|
|
8043
|
+
});
|
|
8044
|
+
typeInput.addEventListener("keydown", (event) => {
|
|
8045
|
+
if (event.key === "Escape") {
|
|
8046
|
+
typeSuggestions.hidden = true;
|
|
8047
|
+
return;
|
|
8048
|
+
}
|
|
8049
|
+
if (event.key !== "/" && event.key !== "Enter") return;
|
|
8050
|
+
event.preventDefault();
|
|
8051
|
+
typeSuggestions.hidden = true;
|
|
8052
|
+
nameInput.focus();
|
|
8053
|
+
nameInput.select();
|
|
8054
|
+
});
|
|
8055
|
+
nameInput.addEventListener("input", () => {
|
|
8056
|
+
updateFooterBranchCreateDraft({ name: nameInput.value });
|
|
8057
|
+
updatePreview();
|
|
8058
|
+
});
|
|
8059
|
+
nameInput.addEventListener("keydown", (event) => {
|
|
8060
|
+
if (event.key !== "Enter") return;
|
|
8061
|
+
event.preventDefault();
|
|
8062
|
+
form.requestSubmit();
|
|
8063
|
+
});
|
|
8064
|
+
form.addEventListener("submit", (event) => {
|
|
8065
|
+
event.preventDefault();
|
|
8066
|
+
if (state.switching) return;
|
|
8067
|
+
const branchName = footerBranchCreateName();
|
|
8068
|
+
createFooterGitBranch(branchName).catch((error) => addEvent(error.message || String(error), "error"));
|
|
8069
|
+
});
|
|
8070
|
+
|
|
8071
|
+
updatePreview();
|
|
8072
|
+
renderTypeSuggestions();
|
|
8073
|
+
typeField.append(typeInput, typeSuggestions);
|
|
8074
|
+
fields.append(typeField, slash, nameInput, submitButton);
|
|
8075
|
+
form.append(header, fields, preview);
|
|
8076
|
+
return form;
|
|
8077
|
+
}
|
|
8078
|
+
|
|
7740
8079
|
function renderFooterBranchPicker() {
|
|
7741
8080
|
const picker = make("div", "footer-model-picker footer-branch-picker");
|
|
7742
8081
|
picker.setAttribute("role", "listbox");
|
|
@@ -7753,7 +8092,7 @@ function renderFooterBranchPicker() {
|
|
|
7753
8092
|
return picker;
|
|
7754
8093
|
}
|
|
7755
8094
|
if (state.loading && state.branches.length === 0) {
|
|
7756
|
-
picker.append(make("div", "footer-model-picker-empty muted", "Loading local branches…"));
|
|
8095
|
+
picker.append(make("div", "footer-model-picker-empty muted", "Loading existing local branches… New branch creation is available."), renderFooterBranchCreateForm(state));
|
|
7757
8096
|
return picker;
|
|
7758
8097
|
}
|
|
7759
8098
|
|
|
@@ -7761,16 +8100,11 @@ function renderFooterBranchPicker() {
|
|
|
7761
8100
|
if (!state.loading && !hasOtherBranches) {
|
|
7762
8101
|
const empty = make("div", "footer-model-picker-empty muted");
|
|
7763
8102
|
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);
|
|
8103
|
+
picker.append(empty);
|
|
7772
8104
|
}
|
|
7773
8105
|
|
|
8106
|
+
picker.append(renderFooterBranchCreateForm(state));
|
|
8107
|
+
|
|
7774
8108
|
for (const branch of state.branches) {
|
|
7775
8109
|
const selected = branch.current || (!!state.current && branch.name === state.current);
|
|
7776
8110
|
const disabled = selected || state.loading || !!state.switching;
|
|
@@ -7790,6 +8124,162 @@ function renderFooterBranchPicker() {
|
|
|
7790
8124
|
return picker;
|
|
7791
8125
|
}
|
|
7792
8126
|
|
|
8127
|
+
function footerScopedModelKey(model) {
|
|
8128
|
+
return model?.provider && model?.id ? `${model.provider}/${model.id}` : "";
|
|
8129
|
+
}
|
|
8130
|
+
|
|
8131
|
+
function readFooterScopedModelOrder() {
|
|
8132
|
+
try {
|
|
8133
|
+
const parsed = JSON.parse(localStorage.getItem(FOOTER_SCOPED_MODEL_ORDER_STORAGE_KEY) || "[]");
|
|
8134
|
+
return Array.isArray(parsed) ? parsed.filter((key) => typeof key === "string" && key.trim()) : [];
|
|
8135
|
+
} catch {
|
|
8136
|
+
return [];
|
|
8137
|
+
}
|
|
8138
|
+
}
|
|
8139
|
+
|
|
8140
|
+
function writeFooterScopedModelOrder(order) {
|
|
8141
|
+
try {
|
|
8142
|
+
localStorage.setItem(FOOTER_SCOPED_MODEL_ORDER_STORAGE_KEY, JSON.stringify([...new Set(order.filter(Boolean))]));
|
|
8143
|
+
} catch {}
|
|
8144
|
+
}
|
|
8145
|
+
|
|
8146
|
+
function orderedFooterScopedModels() {
|
|
8147
|
+
const order = readFooterScopedModelOrder();
|
|
8148
|
+
if (!order.length) return footerScopedModels;
|
|
8149
|
+
const rank = new Map(order.map((key, index) => [key, index]));
|
|
8150
|
+
return [...footerScopedModels].sort((a, b) => {
|
|
8151
|
+
const aRank = rank.has(footerScopedModelKey(a)) ? rank.get(footerScopedModelKey(a)) : Number.MAX_SAFE_INTEGER;
|
|
8152
|
+
const bRank = rank.has(footerScopedModelKey(b)) ? rank.get(footerScopedModelKey(b)) : Number.MAX_SAFE_INTEGER;
|
|
8153
|
+
return aRank - bRank;
|
|
8154
|
+
});
|
|
8155
|
+
}
|
|
8156
|
+
|
|
8157
|
+
function commitFooterScopedModelOrder(order, { render = true, focusKey = "" } = {}) {
|
|
8158
|
+
writeFooterScopedModelOrder(order);
|
|
8159
|
+
footerScopedModels = orderedFooterScopedModels();
|
|
8160
|
+
if (render) renderFooter();
|
|
8161
|
+
if (focusKey) {
|
|
8162
|
+
const movedButton = document.querySelector(`[data-footer-model-key="${CSS.escape(focusKey)}"]`);
|
|
8163
|
+
if (movedButton) movedButton.focus();
|
|
8164
|
+
}
|
|
8165
|
+
}
|
|
8166
|
+
|
|
8167
|
+
function reorderFooterScopedModel(fromKey, toKey, { focus = true } = {}) {
|
|
8168
|
+
if (!fromKey || !toKey || fromKey === toKey) return false;
|
|
8169
|
+
const models = orderedFooterScopedModels();
|
|
8170
|
+
const fromIndex = models.findIndex((model) => footerScopedModelKey(model) === fromKey);
|
|
8171
|
+
const toIndex = models.findIndex((model) => footerScopedModelKey(model) === toKey);
|
|
8172
|
+
if (fromIndex < 0 || toIndex < 0) return false;
|
|
8173
|
+
const [moved] = models.splice(fromIndex, 1);
|
|
8174
|
+
models.splice(toIndex, 0, moved);
|
|
8175
|
+
commitFooterScopedModelOrder(models.map(footerScopedModelKey), { focusKey: focus ? fromKey : "" });
|
|
8176
|
+
return true;
|
|
8177
|
+
}
|
|
8178
|
+
|
|
8179
|
+
function moveFooterScopedModelByOffset(modelKey, offset) {
|
|
8180
|
+
const models = orderedFooterScopedModels();
|
|
8181
|
+
const fromIndex = models.findIndex((model) => footerScopedModelKey(model) === modelKey);
|
|
8182
|
+
const toIndex = fromIndex + offset;
|
|
8183
|
+
if (fromIndex < 0 || toIndex < 0 || toIndex >= models.length) return false;
|
|
8184
|
+
const [moved] = models.splice(fromIndex, 1);
|
|
8185
|
+
models.splice(toIndex, 0, moved);
|
|
8186
|
+
commitFooterScopedModelOrder(models.map(footerScopedModelKey), { focusKey: modelKey });
|
|
8187
|
+
return true;
|
|
8188
|
+
}
|
|
8189
|
+
|
|
8190
|
+
function footerScopedModelButtons() {
|
|
8191
|
+
return [...document.querySelectorAll(".footer-model-picker .footer-model-option[data-footer-model-key]")];
|
|
8192
|
+
}
|
|
8193
|
+
|
|
8194
|
+
function footerScopedModelButtonFromPoint(clientX, clientY) {
|
|
8195
|
+
return document.elementFromPoint(clientX, clientY)?.closest?.(".footer-model-option[data-footer-model-key]") || null;
|
|
8196
|
+
}
|
|
8197
|
+
|
|
8198
|
+
function clearFooterScopedModelDragMarkers() {
|
|
8199
|
+
for (const button of footerScopedModelButtons()) {
|
|
8200
|
+
button.classList.remove("drag-over", "drag-over-before", "drag-over-after");
|
|
8201
|
+
}
|
|
8202
|
+
}
|
|
8203
|
+
|
|
8204
|
+
function commitVisibleFooterScopedModelOrder({ render = false, focusKey = "" } = {}) {
|
|
8205
|
+
const order = footerScopedModelButtons().map((button) => button.dataset.footerModelKey).filter(Boolean);
|
|
8206
|
+
if (!order.length) return;
|
|
8207
|
+
commitFooterScopedModelOrder(order, { render, focusKey });
|
|
8208
|
+
}
|
|
8209
|
+
|
|
8210
|
+
function moveVisibleFooterScopedModel(fromKey, targetButton, clientY) {
|
|
8211
|
+
if (!fromKey || !targetButton) return false;
|
|
8212
|
+
const sourceButton = document.querySelector(`[data-footer-model-key="${CSS.escape(fromKey)}"]`);
|
|
8213
|
+
if (!sourceButton || sourceButton === targetButton) return false;
|
|
8214
|
+
const parent = sourceButton.parentElement;
|
|
8215
|
+
if (!parent || targetButton.parentElement !== parent) return false;
|
|
8216
|
+
const targetKey = targetButton.dataset.footerModelKey || "";
|
|
8217
|
+
const rect = targetButton.getBoundingClientRect();
|
|
8218
|
+
const insertBefore = clientY < rect.top + rect.height / 2;
|
|
8219
|
+
|
|
8220
|
+
clearFooterScopedModelDragMarkers();
|
|
8221
|
+
targetButton.classList.add("drag-over", insertBefore ? "drag-over-before" : "drag-over-after");
|
|
8222
|
+
footerScopedModelLastDragOverKey = `${targetKey}:${insertBefore ? "before" : "after"}`;
|
|
8223
|
+
|
|
8224
|
+
if (insertBefore) parent.insertBefore(sourceButton, targetButton);
|
|
8225
|
+
else parent.insertBefore(sourceButton, targetButton.nextSibling);
|
|
8226
|
+
commitVisibleFooterScopedModelOrder({ render: false });
|
|
8227
|
+
return true;
|
|
8228
|
+
}
|
|
8229
|
+
|
|
8230
|
+
function beginFooterScopedModelPointerDrag(event, modelKey) {
|
|
8231
|
+
if (event.button !== 0 || !modelKey) return;
|
|
8232
|
+
footerScopedModelPointerDrag = { modelKey, pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, active: false };
|
|
8233
|
+
window.addEventListener("pointermove", updateFooterScopedModelPointerDrag, { capture: true });
|
|
8234
|
+
window.addEventListener("pointerup", endFooterScopedModelPointerDrag, { capture: true });
|
|
8235
|
+
window.addEventListener("pointercancel", endFooterScopedModelPointerDrag, { capture: true });
|
|
8236
|
+
}
|
|
8237
|
+
|
|
8238
|
+
function updateFooterScopedModelPointerDrag(event) {
|
|
8239
|
+
const drag = footerScopedModelPointerDrag;
|
|
8240
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
8241
|
+
const distance = Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY);
|
|
8242
|
+
if (!drag.active && distance < FOOTER_SCOPED_MODEL_POINTER_DRAG_THRESHOLD_PX) return;
|
|
8243
|
+
event.preventDefault();
|
|
8244
|
+
if (!drag.active) {
|
|
8245
|
+
drag.active = true;
|
|
8246
|
+
footerScopedModelDragKey = drag.modelKey;
|
|
8247
|
+
footerScopedModelLastDragOverKey = "";
|
|
8248
|
+
clearTimeout(pointerActivationTimeout);
|
|
8249
|
+
pointerActivationTimeout = null;
|
|
8250
|
+
activePointerActivation = null;
|
|
8251
|
+
deferredUiRenderCallbacks.delete("footer");
|
|
8252
|
+
const sourceButton = document.querySelector(`[data-footer-model-key="${CSS.escape(drag.modelKey)}"]`);
|
|
8253
|
+
if (sourceButton) sourceButton.classList.add("dragging");
|
|
8254
|
+
}
|
|
8255
|
+
const targetButton = footerScopedModelButtonFromPoint(event.clientX, event.clientY);
|
|
8256
|
+
if (!targetButton || targetButton.dataset.footerModelKey === drag.modelKey) return;
|
|
8257
|
+
const rect = targetButton.getBoundingClientRect();
|
|
8258
|
+
const markerKey = `${targetButton.dataset.footerModelKey}:${event.clientY < rect.top + rect.height / 2 ? "before" : "after"}`;
|
|
8259
|
+
if (markerKey === footerScopedModelLastDragOverKey) return;
|
|
8260
|
+
moveVisibleFooterScopedModel(drag.modelKey, targetButton, event.clientY);
|
|
8261
|
+
}
|
|
8262
|
+
|
|
8263
|
+
function endFooterScopedModelPointerDrag(event) {
|
|
8264
|
+
const drag = footerScopedModelPointerDrag;
|
|
8265
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
8266
|
+
window.removeEventListener("pointermove", updateFooterScopedModelPointerDrag, { capture: true });
|
|
8267
|
+
window.removeEventListener("pointerup", endFooterScopedModelPointerDrag, { capture: true });
|
|
8268
|
+
window.removeEventListener("pointercancel", endFooterScopedModelPointerDrag, { capture: true });
|
|
8269
|
+
const wasActive = drag.active;
|
|
8270
|
+
const sourceButton = document.querySelector(`[data-footer-model-key="${CSS.escape(drag.modelKey)}"]`);
|
|
8271
|
+
footerScopedModelPointerDrag = null;
|
|
8272
|
+
footerScopedModelDragKey = "";
|
|
8273
|
+
footerScopedModelLastDragOverKey = "";
|
|
8274
|
+
clearFooterScopedModelDragMarkers();
|
|
8275
|
+
if (sourceButton) sourceButton.classList.remove("dragging");
|
|
8276
|
+
if (wasActive) {
|
|
8277
|
+
footerScopedModelSuppressClickUntil = Date.now() + 250;
|
|
8278
|
+
event.preventDefault();
|
|
8279
|
+
commitVisibleFooterScopedModelOrder({ render: false, focusKey: drag.modelKey });
|
|
8280
|
+
}
|
|
8281
|
+
}
|
|
8282
|
+
|
|
7793
8283
|
async function applyFooterModel(model) {
|
|
7794
8284
|
if (!model?.provider || !model?.id) return;
|
|
7795
8285
|
const tabContext = activeTabContext();
|
|
@@ -7815,7 +8305,7 @@ function renderFooterModelPicker() {
|
|
|
7815
8305
|
picker.setAttribute("role", "listbox");
|
|
7816
8306
|
picker.setAttribute("aria-label", "Scoped models");
|
|
7817
8307
|
picker.append(make("div", "footer-model-picker-title", "Scoped models"));
|
|
7818
|
-
picker.append(make("div", "footer-model-picker-source",
|
|
8308
|
+
picker.append(make("div", "footer-model-picker-source", "Drag models to reorder · Alt+↑/↓ moves focused model"));
|
|
7819
8309
|
if (footerScopedModels.length === 0) {
|
|
7820
8310
|
const empty = make("div", "footer-model-picker-empty muted");
|
|
7821
8311
|
empty.append(
|
|
@@ -7826,10 +8316,16 @@ function renderFooterModelPicker() {
|
|
|
7826
8316
|
return picker;
|
|
7827
8317
|
}
|
|
7828
8318
|
const current = currentState?.model;
|
|
8319
|
+
footerScopedModels = orderedFooterScopedModels();
|
|
7829
8320
|
for (const model of footerScopedModels) {
|
|
7830
8321
|
const selected = current?.provider === model.provider && current?.id === model.id;
|
|
7831
|
-
const
|
|
8322
|
+
const modelKey = footerScopedModelKey(model);
|
|
8323
|
+
const dragging = footerScopedModelDragKey === modelKey;
|
|
8324
|
+
const dragOver = footerScopedModelDragKey && footerScopedModelLastDragOverKey === modelKey;
|
|
8325
|
+
const button = make("button", `footer-model-option${selected ? " active" : ""}${dragging ? " dragging" : ""}${dragOver ? " drag-over" : ""}`);
|
|
7832
8326
|
button.type = "button";
|
|
8327
|
+
button.draggable = false;
|
|
8328
|
+
button.dataset.footerModelKey = modelKey;
|
|
7833
8329
|
button.setAttribute("role", "option");
|
|
7834
8330
|
button.setAttribute("aria-selected", selected ? "true" : "false");
|
|
7835
8331
|
button.title = `${model.provider}/${model.id}${model.name ? ` · ${model.name}` : ""}`;
|
|
@@ -7837,7 +8333,19 @@ function renderFooterModelPicker() {
|
|
|
7837
8333
|
make("span", "footer-model-option-main", `${model.provider}/${model.id}`),
|
|
7838
8334
|
make("span", "footer-model-option-name", model.name || ""),
|
|
7839
8335
|
);
|
|
7840
|
-
button.addEventListener("click", () =>
|
|
8336
|
+
button.addEventListener("click", (event) => {
|
|
8337
|
+
if (Date.now() < footerScopedModelSuppressClickUntil) {
|
|
8338
|
+
event.preventDefault();
|
|
8339
|
+
return;
|
|
8340
|
+
}
|
|
8341
|
+
applyFooterModel(model);
|
|
8342
|
+
});
|
|
8343
|
+
button.addEventListener("keydown", (event) => {
|
|
8344
|
+
if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
|
|
8345
|
+
event.preventDefault();
|
|
8346
|
+
moveFooterScopedModelByOffset(modelKey, event.key === "ArrowUp" ? -1 : 1);
|
|
8347
|
+
});
|
|
8348
|
+
button.addEventListener("pointerdown", (event) => beginFooterScopedModelPointerDrag(event, modelKey));
|
|
7841
8349
|
picker.append(button);
|
|
7842
8350
|
}
|
|
7843
8351
|
return picker;
|
|
@@ -8630,13 +9138,27 @@ function renderReleaseDialogMessage(parent, text) {
|
|
|
8630
9138
|
}
|
|
8631
9139
|
}
|
|
8632
9140
|
|
|
9141
|
+
function textLines(raw) {
|
|
9142
|
+
const value = String(raw || "");
|
|
9143
|
+
const lines = [];
|
|
9144
|
+
let start = 0;
|
|
9145
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
9146
|
+
if (value[index] !== "\n") continue;
|
|
9147
|
+
const end = index > start && value[index - 1] === "\r" ? index - 1 : index;
|
|
9148
|
+
lines.push(value.slice(start, end));
|
|
9149
|
+
start = index + 1;
|
|
9150
|
+
}
|
|
9151
|
+
if (start <= value.length) lines.push(value.slice(start));
|
|
9152
|
+
return lines;
|
|
9153
|
+
}
|
|
9154
|
+
|
|
8633
9155
|
function stripTodoProgressLines(text, { streaming = false } = {}) {
|
|
8634
9156
|
if (!isOptionalFeatureEnabled("todoProgressWidget")) return String(text || "");
|
|
8635
9157
|
let inFence = false;
|
|
8636
9158
|
const kept = [];
|
|
8637
9159
|
const raw = String(text || "");
|
|
8638
9160
|
const hasTrailingNewline = /\r?\n$/.test(raw);
|
|
8639
|
-
const lines = raw
|
|
9161
|
+
const lines = textLines(raw);
|
|
8640
9162
|
|
|
8641
9163
|
lines.forEach((line, index) => {
|
|
8642
9164
|
const isUnfinishedTail = streaming && !hasTrailingNewline && index === lines.length - 1;
|
|
@@ -8653,8 +9175,118 @@ function stripTodoProgressLines(text, { streaming = false } = {}) {
|
|
|
8653
9175
|
return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
8654
9176
|
}
|
|
8655
9177
|
|
|
9178
|
+
function parseTodoProgressItemLine(line) {
|
|
9179
|
+
const match = String(line || "").match(/^\s*(?:(?:[-*]|\d+[.)])\s*)?\[( |x|X|-)\]\s+(.+)$/);
|
|
9180
|
+
if (!match) return null;
|
|
9181
|
+
const mark = match[1].toLowerCase();
|
|
9182
|
+
return { status: mark === "x" ? "done" : mark === "-" ? "partial" : "todo", text: match[2].trim() };
|
|
9183
|
+
}
|
|
9184
|
+
|
|
9185
|
+
function todoProgressStatusLabel(status) {
|
|
9186
|
+
if (status === "done") return "[x]";
|
|
9187
|
+
if (status === "partial") return "[-]";
|
|
9188
|
+
return "[ ]";
|
|
9189
|
+
}
|
|
9190
|
+
|
|
9191
|
+
function liveTodoProgressWidgetLinesFromText(text, tabId = activeTabId) {
|
|
9192
|
+
if (!isOptionalFeatureEnabled("todoProgressWidget")) return null;
|
|
9193
|
+
const raw = String(text || "");
|
|
9194
|
+
if (!raw.trim()) return null;
|
|
9195
|
+
|
|
9196
|
+
let inFence = false;
|
|
9197
|
+
let goal = "";
|
|
9198
|
+
let current = [];
|
|
9199
|
+
const blocks = [];
|
|
9200
|
+
const flush = () => {
|
|
9201
|
+
if (current.length) blocks.push(current);
|
|
9202
|
+
current = [];
|
|
9203
|
+
};
|
|
9204
|
+
|
|
9205
|
+
for (const line of textLines(raw)) {
|
|
9206
|
+
if (/^\s*```/.test(line)) {
|
|
9207
|
+
inFence = !inFence;
|
|
9208
|
+
flush();
|
|
9209
|
+
continue;
|
|
9210
|
+
}
|
|
9211
|
+
if (inFence) continue;
|
|
9212
|
+
|
|
9213
|
+
const clean = stripAnsi(line).trim();
|
|
9214
|
+
const goalMatch = clean.match(/^Goal\s*[::]\s*(.+)$/i);
|
|
9215
|
+
if (goalMatch?.[1]?.trim()) goal = goalMatch[1].trim();
|
|
9216
|
+
|
|
9217
|
+
const item = parseTodoProgressItemLine(clean);
|
|
9218
|
+
if (item) {
|
|
9219
|
+
current.push(item);
|
|
9220
|
+
continue;
|
|
9221
|
+
}
|
|
9222
|
+
flush();
|
|
9223
|
+
}
|
|
9224
|
+
flush();
|
|
9225
|
+
|
|
9226
|
+
const items = blocks.at(-1) || [];
|
|
9227
|
+
if (!items.length) return null;
|
|
9228
|
+
|
|
9229
|
+
if (goal && tabId) todoProgressGoalByTab.set(tabId, goal);
|
|
9230
|
+
const displayGoal = goal || (tabId ? todoProgressGoalByTab.get(tabId) : "") || "";
|
|
9231
|
+
const done = items.filter((item) => item.status === "done").length;
|
|
9232
|
+
const partial = items.filter((item) => item.status === "partial").length;
|
|
9233
|
+
const lines = [];
|
|
9234
|
+
if (displayGoal) lines.push(`Goal: ${displayGoal}`);
|
|
9235
|
+
lines.push(`Todo ${done}/${items.length} done${partial ? `, ${partial} partial` : ""}`);
|
|
9236
|
+
for (const item of items) lines.push(`${todoProgressStatusLabel(item.status)} ${item.text}`);
|
|
9237
|
+
return lines;
|
|
9238
|
+
}
|
|
9239
|
+
|
|
9240
|
+
// Coalesce live widget rebuilds to one per animation frame. The streaming
|
|
9241
|
+
// output handler calls into here on every text token, but the widget area
|
|
9242
|
+
// must not be torn down/rebuilt per token (that is UI reacting to the agent
|
|
9243
|
+
// output stream). One render per frame keeps streaming transcript-local.
|
|
9244
|
+
function scheduleLiveWidgetRender() {
|
|
9245
|
+
if (liveWidgetRenderFrame !== null) return;
|
|
9246
|
+
const flush = () => {
|
|
9247
|
+
liveWidgetRenderFrame = null;
|
|
9248
|
+
renderWidgets();
|
|
9249
|
+
};
|
|
9250
|
+
if (typeof requestAnimationFrame === "function") liveWidgetRenderFrame = requestAnimationFrame(flush);
|
|
9251
|
+
else liveWidgetRenderFrame = setTimeout(flush, 0);
|
|
9252
|
+
}
|
|
9253
|
+
|
|
9254
|
+
function scheduleLiveTodoProgressWidgetSync(text, tabId = activeTabId) {
|
|
9255
|
+
liveTodoProgressPendingText = String(text || "");
|
|
9256
|
+
liveTodoProgressPendingTabId = tabId || activeTabId;
|
|
9257
|
+
if (liveTodoProgressSyncFrame !== null) return;
|
|
9258
|
+
const flush = () => {
|
|
9259
|
+
const pendingText = liveTodoProgressPendingText;
|
|
9260
|
+
const pendingTabId = liveTodoProgressPendingTabId || activeTabId;
|
|
9261
|
+
liveTodoProgressSyncFrame = null;
|
|
9262
|
+
liveTodoProgressPendingText = "";
|
|
9263
|
+
liveTodoProgressPendingTabId = null;
|
|
9264
|
+
syncLiveTodoProgressWidgetFromText(pendingText, pendingTabId);
|
|
9265
|
+
};
|
|
9266
|
+
if (typeof requestAnimationFrame === "function") liveTodoProgressSyncFrame = requestAnimationFrame(flush);
|
|
9267
|
+
else liveTodoProgressSyncFrame = setTimeout(flush, 0);
|
|
9268
|
+
}
|
|
9269
|
+
|
|
9270
|
+
function syncLiveTodoProgressWidgetFromText(text, tabId = activeTabId) {
|
|
9271
|
+
const lines = liveTodoProgressWidgetLinesFromText(text, tabId);
|
|
9272
|
+
if (!lines) return false;
|
|
9273
|
+
const signature = todoProgressSignatureFromLines(lines);
|
|
9274
|
+
if (tabId && todoProgressSignatureByTab.get(tabId) === signature) return false;
|
|
9275
|
+
// liveTodoProgressWidgetLinesFromText already short-circuits unless the
|
|
9276
|
+
// feature is enabled, so detection is settled here. Do NOT run
|
|
9277
|
+
// updateOptionalFeatureAvailability() per token: it triggers git-footer
|
|
9278
|
+
// payload reconciliation and a full optional-feature control rebuild.
|
|
9279
|
+
// Availability is reconciled on command/state refreshes and RPC widget
|
|
9280
|
+
// updates instead, keeping streaming output decoupled from chrome UI.
|
|
9281
|
+
const changed = setWidgetForTab(tabId, "todo-progress", { method: "setWidget", widgetKey: "todo-progress", widgetLines: lines, tabId, live: true });
|
|
9282
|
+
if (changed && tabId === activeTabId) scheduleLiveWidgetRender();
|
|
9283
|
+
return changed;
|
|
9284
|
+
}
|
|
9285
|
+
|
|
8656
9286
|
function parseTodoProgressWidget(lines) {
|
|
8657
9287
|
const cleanLines = lines.map(stripAnsi).map((line) => line.trim()).filter(Boolean);
|
|
9288
|
+
const goalLine = cleanLines.find((line) => /^Goal\s*[::]/i.test(line));
|
|
9289
|
+
const goal = goalLine ? goalLine.replace(/^Goal\s*[::]\s*/i, "").trim() : "";
|
|
8658
9290
|
const headerIndex = cleanLines.findIndex((line) => /^Todo\s+\d+\/\d+\s+done/i.test(line));
|
|
8659
9291
|
if (headerIndex === -1) return null;
|
|
8660
9292
|
|
|
@@ -8665,16 +9297,16 @@ function parseTodoProgressWidget(lines) {
|
|
|
8665
9297
|
const items = [];
|
|
8666
9298
|
let footer = "";
|
|
8667
9299
|
for (const line of cleanLines.slice(headerIndex + 1)) {
|
|
8668
|
-
const item = line
|
|
9300
|
+
const item = parseTodoProgressItemLine(line);
|
|
8669
9301
|
if (item) {
|
|
8670
|
-
|
|
8671
|
-
items.push({ status: mark === "x" ? "done" : mark === "-" ? "partial" : "todo", text: item[2].trim() });
|
|
9302
|
+
items.push(item);
|
|
8672
9303
|
} else if (/^Scroll\s+/i.test(line)) {
|
|
8673
9304
|
footer = line;
|
|
8674
9305
|
}
|
|
8675
9306
|
}
|
|
8676
9307
|
|
|
8677
9308
|
return {
|
|
9309
|
+
goal,
|
|
8678
9310
|
done: Number.parseInt(match[1], 10) || 0,
|
|
8679
9311
|
total: Number.parseInt(match[2], 10) || items.length,
|
|
8680
9312
|
partial: Number.parseInt(match[3] || "0", 10) || 0,
|
|
@@ -8709,6 +9341,7 @@ function renderTodoProgressWidget(_key, lines) {
|
|
|
8709
9341
|
const fill = make("span", "todo-widget-progress-fill");
|
|
8710
9342
|
fill.style.width = `${percent}%`;
|
|
8711
9343
|
progress.append(fill);
|
|
9344
|
+
if (todo.goal) summary.append(make("div", "todo-widget-goal", `Goal: ${todo.goal}`));
|
|
8712
9345
|
summary.append(header, progress);
|
|
8713
9346
|
|
|
8714
9347
|
const body = make("div", "todo-widget-body");
|
|
@@ -12888,14 +13521,201 @@ function appendMarkdownParagraph(parent, lines) {
|
|
|
12888
13521
|
parent.append(paragraph);
|
|
12889
13522
|
}
|
|
12890
13523
|
|
|
12891
|
-
function
|
|
13524
|
+
function setMarkdownCodeCopyButtonState(button, copied) {
|
|
13525
|
+
clearTimeout(button._markdownCodeCopyResetTimer);
|
|
13526
|
+
const label = button._markdownCodeCopyDefaultLabel || "Copy";
|
|
13527
|
+
button.classList.toggle("copied", copied);
|
|
13528
|
+
button.textContent = copied ? "Copied" : label;
|
|
13529
|
+
button.title = copied ? "Copied code block" : `${label} code block`;
|
|
13530
|
+
button.setAttribute("aria-label", button.title);
|
|
13531
|
+
if (copied) {
|
|
13532
|
+
button._markdownCodeCopyResetTimer = setTimeout(() => setMarkdownCodeCopyButtonState(button, false), 1400);
|
|
13533
|
+
}
|
|
13534
|
+
}
|
|
13535
|
+
|
|
13536
|
+
async function copyMarkdownCodeBlock(button) {
|
|
13537
|
+
const wrapper = button.closest(".markdown-code-block");
|
|
13538
|
+
const codeNode = wrapper?.querySelector(":scope > pre.markdown-code > code, :scope > details.markdown-mermaid-source pre.markdown-code > code");
|
|
13539
|
+
const text = codeNode?.textContent || "";
|
|
13540
|
+
if (!text) {
|
|
13541
|
+
addEvent("code block has no text to copy", "warn");
|
|
13542
|
+
return;
|
|
13543
|
+
}
|
|
13544
|
+
button.disabled = true;
|
|
13545
|
+
try {
|
|
13546
|
+
await copyText(text);
|
|
13547
|
+
setMarkdownCodeCopyButtonState(button, true);
|
|
13548
|
+
} catch (error) {
|
|
13549
|
+
addEvent(`code block copy failed: ${error.message || String(error)}`, "warn");
|
|
13550
|
+
} finally {
|
|
13551
|
+
button.disabled = false;
|
|
13552
|
+
}
|
|
13553
|
+
}
|
|
13554
|
+
|
|
13555
|
+
function attachMarkdownCodeCopyButton(wrapper, label = "Copy") {
|
|
13556
|
+
if (!wrapper) return null;
|
|
13557
|
+
const existing = wrapper.querySelector(":scope > .markdown-code-copy-button");
|
|
13558
|
+
if (existing) return existing;
|
|
13559
|
+
const button = make("button", "markdown-code-copy-button", label);
|
|
13560
|
+
button.type = "button";
|
|
13561
|
+
button._markdownCodeCopyDefaultLabel = label;
|
|
13562
|
+
setMarkdownCodeCopyButtonState(button, false);
|
|
13563
|
+
button.addEventListener("click", (event) => {
|
|
13564
|
+
event.preventDefault();
|
|
13565
|
+
event.stopPropagation();
|
|
13566
|
+
copyMarkdownCodeBlock(button);
|
|
13567
|
+
});
|
|
13568
|
+
wrapper.classList.add("has-code-copy-action");
|
|
13569
|
+
wrapper.append(button);
|
|
13570
|
+
return button;
|
|
13571
|
+
}
|
|
13572
|
+
|
|
13573
|
+
function normalizedMarkdownLanguage(language) {
|
|
13574
|
+
return String(language || "").trim().toLowerCase();
|
|
13575
|
+
}
|
|
13576
|
+
|
|
13577
|
+
function isMermaidLanguage(language) {
|
|
13578
|
+
return MERMAID_LANGUAGES.has(normalizedMarkdownLanguage(language));
|
|
13579
|
+
}
|
|
13580
|
+
|
|
13581
|
+
function mermaidCssVar(styles, name, fallback) {
|
|
13582
|
+
return styles.getPropertyValue(name).trim() || fallback;
|
|
13583
|
+
}
|
|
13584
|
+
|
|
13585
|
+
function mermaidConfig() {
|
|
13586
|
+
const styles = getComputedStyle(document.documentElement);
|
|
13587
|
+
const text = mermaidCssVar(styles, "--ctp-text", "#cdd6f4");
|
|
13588
|
+
const subtext = mermaidCssVar(styles, "--ctp-subtext", "#bac2de");
|
|
13589
|
+
const surface = mermaidCssVar(styles, "--ctp-surface", "#313244");
|
|
13590
|
+
const base = mermaidCssVar(styles, "--ctp-base", "#1e1e2e");
|
|
13591
|
+
const crust = mermaidCssVar(styles, "--ctp-crust", "#11111b");
|
|
13592
|
+
const mauve = mermaidCssVar(styles, "--ctp-mauve", "#cba6f7");
|
|
13593
|
+
const blue = mermaidCssVar(styles, "--ctp-blue", "#89b4fa");
|
|
13594
|
+
const teal = mermaidCssVar(styles, "--ctp-teal", "#94e2d5");
|
|
13595
|
+
const yellow = mermaidCssVar(styles, "--ctp-yellow", "#f9e2af");
|
|
13596
|
+
const red = mermaidCssVar(styles, "--ctp-red", "#f38ba8");
|
|
13597
|
+
return {
|
|
13598
|
+
startOnLoad: false,
|
|
13599
|
+
securityLevel: "strict",
|
|
13600
|
+
logLevel: "error",
|
|
13601
|
+
maxTextSize: MERMAID_MAX_TEXT_SIZE,
|
|
13602
|
+
theme: "base",
|
|
13603
|
+
flowchart: { htmlLabels: false },
|
|
13604
|
+
themeVariables: {
|
|
13605
|
+
darkMode: true,
|
|
13606
|
+
background: "transparent",
|
|
13607
|
+
mainBkg: base,
|
|
13608
|
+
secondBkg: surface,
|
|
13609
|
+
primaryColor: surface,
|
|
13610
|
+
primaryTextColor: text,
|
|
13611
|
+
primaryBorderColor: mauve,
|
|
13612
|
+
secondaryColor: base,
|
|
13613
|
+
secondaryTextColor: text,
|
|
13614
|
+
secondaryBorderColor: blue,
|
|
13615
|
+
tertiaryColor: crust,
|
|
13616
|
+
tertiaryTextColor: text,
|
|
13617
|
+
tertiaryBorderColor: teal,
|
|
13618
|
+
lineColor: subtext,
|
|
13619
|
+
textColor: text,
|
|
13620
|
+
titleColor: teal,
|
|
13621
|
+
nodeTextColor: text,
|
|
13622
|
+
clusterBkg: crust,
|
|
13623
|
+
clusterBorder: mauve,
|
|
13624
|
+
edgeLabelBackground: base,
|
|
13625
|
+
noteBkgColor: crust,
|
|
13626
|
+
noteTextColor: text,
|
|
13627
|
+
noteBorderColor: yellow,
|
|
13628
|
+
errorBkgColor: crust,
|
|
13629
|
+
errorTextColor: red,
|
|
13630
|
+
fontFamily: 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
13631
|
+
},
|
|
13632
|
+
};
|
|
13633
|
+
}
|
|
13634
|
+
|
|
13635
|
+
function initializeMermaid(mermaid) {
|
|
13636
|
+
const config = mermaidConfig();
|
|
13637
|
+
const signature = JSON.stringify(config);
|
|
13638
|
+
if (signature !== mermaidThemeSignature) {
|
|
13639
|
+
mermaid.initialize(config);
|
|
13640
|
+
mermaidThemeSignature = signature;
|
|
13641
|
+
}
|
|
13642
|
+
}
|
|
13643
|
+
|
|
13644
|
+
async function loadMermaid() {
|
|
13645
|
+
if (!mermaidModulePromise) {
|
|
13646
|
+
mermaidModulePromise = import(MERMAID_MODULE_URL)
|
|
13647
|
+
.then((module) => module.default || module)
|
|
13648
|
+
.catch((error) => {
|
|
13649
|
+
mermaidModulePromise = null;
|
|
13650
|
+
throw error;
|
|
13651
|
+
});
|
|
13652
|
+
}
|
|
13653
|
+
const mermaid = await mermaidModulePromise;
|
|
13654
|
+
initializeMermaid(mermaid);
|
|
13655
|
+
return mermaid;
|
|
13656
|
+
}
|
|
13657
|
+
|
|
13658
|
+
function mermaidRenderErrorMessage(error) {
|
|
13659
|
+
return String(error?.str || error?.message || error || "Unknown Mermaid render error").trim();
|
|
13660
|
+
}
|
|
13661
|
+
|
|
13662
|
+
async function renderMermaidDiagram(diagram, status, source) {
|
|
13663
|
+
const token = `${Date.now().toString(36)}-${++mermaidRenderSequence}`;
|
|
13664
|
+
diagram.dataset.mermaidRenderToken = token;
|
|
13665
|
+
try {
|
|
13666
|
+
if (source.length > MERMAID_MAX_TEXT_SIZE) throw new Error(`Mermaid diagram is too large (${source.length} characters, max ${MERMAID_MAX_TEXT_SIZE}).`);
|
|
13667
|
+
const mermaid = await loadMermaid();
|
|
13668
|
+
const id = `mermaid-${token.replace(/[^a-z0-9_-]/gi, "-")}`;
|
|
13669
|
+
const { svg, bindFunctions } = await mermaid.render(id, source);
|
|
13670
|
+
if (!diagram.isConnected || diagram.dataset.mermaidRenderToken !== token) return;
|
|
13671
|
+
diagram.innerHTML = svg;
|
|
13672
|
+
bindFunctions?.(diagram);
|
|
13673
|
+
diagram.classList.add("rendered");
|
|
13674
|
+
status.textContent = "";
|
|
13675
|
+
status.hidden = true;
|
|
13676
|
+
} catch (error) {
|
|
13677
|
+
if (!diagram.isConnected || diagram.dataset.mermaidRenderToken !== token) return;
|
|
13678
|
+
diagram.classList.add("render-error");
|
|
13679
|
+
status.hidden = false;
|
|
13680
|
+
status.classList.add("error");
|
|
13681
|
+
status.textContent = `Mermaid render failed: ${mermaidRenderErrorMessage(error)}`;
|
|
13682
|
+
}
|
|
13683
|
+
}
|
|
13684
|
+
|
|
13685
|
+
function appendMarkdownMermaidBlock(parent, code) {
|
|
13686
|
+
const source = String(code || "").replace(/\n+$/g, "");
|
|
13687
|
+
const wrapper = make("div", "markdown-code-block markdown-mermaid-block");
|
|
13688
|
+
wrapper.append(make("div", "markdown-code-language", "mermaid"));
|
|
13689
|
+
const diagram = make("div", "markdown-mermaid-diagram");
|
|
13690
|
+
diagram.setAttribute("role", "img");
|
|
13691
|
+
diagram.setAttribute("aria-label", "Mermaid diagram");
|
|
13692
|
+
const status = make("div", "markdown-mermaid-status muted", "Rendering Mermaid diagram…");
|
|
13693
|
+
const sourceDetails = make("details", "markdown-mermaid-source");
|
|
13694
|
+
sourceDetails.append(make("summary", undefined, "Mermaid source"));
|
|
13695
|
+
const pre = make("pre", "code-block markdown-code");
|
|
13696
|
+
const codeNode = make("code", "language-mermaid");
|
|
13697
|
+
codeNode.textContent = source;
|
|
13698
|
+
pre.append(codeNode);
|
|
13699
|
+
sourceDetails.append(pre);
|
|
13700
|
+
wrapper.append(diagram, status, sourceDetails);
|
|
13701
|
+
attachMarkdownCodeCopyButton(wrapper, "Copy source");
|
|
13702
|
+
parent.append(wrapper);
|
|
13703
|
+
queueMicrotask(() => renderMermaidDiagram(diagram, status, source));
|
|
13704
|
+
}
|
|
13705
|
+
|
|
13706
|
+
function appendMarkdownCodeBlock(parent, code, language = "", { closed = true } = {}) {
|
|
13707
|
+
if (closed && isMermaidLanguage(language)) {
|
|
13708
|
+
appendMarkdownMermaidBlock(parent, code);
|
|
13709
|
+
return;
|
|
13710
|
+
}
|
|
12892
13711
|
const wrapper = make("div", "markdown-code-block");
|
|
12893
13712
|
if (language) wrapper.append(make("div", "markdown-code-language", language));
|
|
12894
13713
|
const pre = make("pre", "code-block markdown-code");
|
|
12895
13714
|
const codeNode = make("code", language ? `language-${language.replace(/[^a-z0-9_-]/gi, "")}` : "");
|
|
12896
|
-
codeNode.textContent = code.replace(/\n+$/g, "");
|
|
13715
|
+
codeNode.textContent = String(code || "").replace(/\n+$/g, "");
|
|
12897
13716
|
pre.append(codeNode);
|
|
12898
13717
|
wrapper.append(pre);
|
|
13718
|
+
attachMarkdownCodeCopyButton(wrapper);
|
|
12899
13719
|
parent.append(wrapper);
|
|
12900
13720
|
}
|
|
12901
13721
|
|
|
@@ -12993,8 +13813,9 @@ function renderMarkdownInto(parent, text) {
|
|
|
12993
13813
|
codeLines.push(lines[index]);
|
|
12994
13814
|
index += 1;
|
|
12995
13815
|
}
|
|
12996
|
-
|
|
12997
|
-
|
|
13816
|
+
const closed = index < lines.length;
|
|
13817
|
+
if (closed) index += 1;
|
|
13818
|
+
appendMarkdownCodeBlock(parent, codeLines.join("\n"), language, { closed });
|
|
12998
13819
|
continue;
|
|
12999
13820
|
}
|
|
13000
13821
|
if (markdownTableSeparator(lines[index + 1]) && line.includes("|")) {
|
|
@@ -13098,16 +13919,21 @@ function streamingMarkdownStableBoundary(text) {
|
|
|
13098
13919
|
return boundary;
|
|
13099
13920
|
}
|
|
13100
13921
|
|
|
13922
|
+
function clearStreamingMarkdownBlock(block) {
|
|
13923
|
+
while (block.firstChild) block.firstChild.remove();
|
|
13924
|
+
}
|
|
13925
|
+
|
|
13101
13926
|
function renderStreamingMarkdown(block, text) {
|
|
13102
13927
|
let state = streamMarkdownState;
|
|
13103
13928
|
if (!state || state.block !== block) {
|
|
13104
|
-
block
|
|
13929
|
+
clearStreamingMarkdownBlock(block);
|
|
13105
13930
|
state = streamMarkdownState = { block, stableText: "", tailNodes: [] };
|
|
13106
13931
|
}
|
|
13107
13932
|
if (!text.startsWith(state.stableText)) {
|
|
13108
|
-
//
|
|
13109
|
-
//
|
|
13110
|
-
|
|
13933
|
+
// Derived streaming text should be append-only; if a provider still sends a
|
|
13934
|
+
// retroactive rewrite, reset the streaming renderer without replaceChildren
|
|
13935
|
+
// so this path cannot tear down external chrome or widget nodes.
|
|
13936
|
+
clearStreamingMarkdownBlock(block);
|
|
13111
13937
|
state.stableText = "";
|
|
13112
13938
|
state.tailNodes = [];
|
|
13113
13939
|
}
|
|
@@ -13548,6 +14374,30 @@ function assistantToolCallArguments(part) {
|
|
|
13548
14374
|
return part?.arguments || part?.args || part?.input || part?.toolCall?.arguments || {};
|
|
13549
14375
|
}
|
|
13550
14376
|
|
|
14377
|
+
function toolCallArgumentsText(value, { includeEmptyObject = true } = {}) {
|
|
14378
|
+
if (typeof value === "string") return value;
|
|
14379
|
+
if (value === undefined || value === null) return includeEmptyObject ? "{}" : "";
|
|
14380
|
+
if (typeof value === "object") {
|
|
14381
|
+
if (!includeEmptyObject && !Array.isArray(value) && Object.keys(value).length === 0) return "";
|
|
14382
|
+
try {
|
|
14383
|
+
return JSON.stringify(value, null, 2);
|
|
14384
|
+
} catch {
|
|
14385
|
+
return String(value);
|
|
14386
|
+
}
|
|
14387
|
+
}
|
|
14388
|
+
return String(value);
|
|
14389
|
+
}
|
|
14390
|
+
|
|
14391
|
+
function toolCallDisplayText(message, options = {}) {
|
|
14392
|
+
if (typeof message?.rawArguments === "string" && message.rawArguments.length > 0) return message.rawArguments;
|
|
14393
|
+
return toolCallArgumentsText(message?.arguments ?? message?.content ?? {}, options);
|
|
14394
|
+
}
|
|
14395
|
+
|
|
14396
|
+
function renderToolCallMessageBody(body, message, { placeholder = "{}" } = {}) {
|
|
14397
|
+
const text = toolCallDisplayText(message).trimEnd() || placeholder;
|
|
14398
|
+
appendText(body, text, "code-block tool-call-arguments");
|
|
14399
|
+
}
|
|
14400
|
+
|
|
13551
14401
|
function assistantTextPartText(part) {
|
|
13552
14402
|
if (!part || typeof part !== "object" || part.type !== "text") return "";
|
|
13553
14403
|
if (typeof part.text === "string") return part.text;
|
|
@@ -14582,7 +15432,7 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
14582
15432
|
const thinkingText = visibleThinkingText(message.thinking || textFromContent(message.content));
|
|
14583
15433
|
if (thinkingOutputVisible && thinkingText) appendText(body, thinkingText, "thinking-text");
|
|
14584
15434
|
} else if (message.role === "toolCall") {
|
|
14585
|
-
|
|
15435
|
+
renderToolCallMessageBody(body, message);
|
|
14586
15436
|
} else if (message.role === "assistantEvent") {
|
|
14587
15437
|
appendText(body, typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? {}, null, 2), "code-block");
|
|
14588
15438
|
} else {
|
|
@@ -14779,6 +15629,19 @@ function renderRunIndicator({ scroll = false } = {}) {
|
|
|
14779
15629
|
if (shouldFollow) scrollChatToBottom();
|
|
14780
15630
|
}
|
|
14781
15631
|
|
|
15632
|
+
function scheduleRunIndicatorRender({ scroll = false } = {}) {
|
|
15633
|
+
runIndicatorRenderScroll = runIndicatorRenderScroll || scroll;
|
|
15634
|
+
if (runIndicatorRenderFrame !== null) return;
|
|
15635
|
+
const flush = () => {
|
|
15636
|
+
const shouldScroll = runIndicatorRenderScroll;
|
|
15637
|
+
runIndicatorRenderFrame = null;
|
|
15638
|
+
runIndicatorRenderScroll = false;
|
|
15639
|
+
renderRunIndicator({ scroll: shouldScroll });
|
|
15640
|
+
};
|
|
15641
|
+
if (typeof requestAnimationFrame === "function") runIndicatorRenderFrame = requestAnimationFrame(flush);
|
|
15642
|
+
else runIndicatorRenderFrame = setTimeout(flush, 0);
|
|
15643
|
+
}
|
|
15644
|
+
|
|
14782
15645
|
function setRunIndicatorActivity(activity, { active = true, scroll = true } = {}) {
|
|
14783
15646
|
const wasLocallyActive = runIndicatorLocallyActive;
|
|
14784
15647
|
const previousActivity = runIndicatorActivity;
|
|
@@ -14789,9 +15652,9 @@ function setRunIndicatorActivity(activity, { active = true, scroll = true } = {}
|
|
|
14789
15652
|
}
|
|
14790
15653
|
runIndicatorActivity = activity || runIndicatorActivity || "Waiting for output or action…";
|
|
14791
15654
|
const needsRender = scroll || !hadRunIndicatorBubble || wasLocallyActive !== runIndicatorLocallyActive || previousActivity !== runIndicatorActivity;
|
|
14792
|
-
if (needsRender)
|
|
15655
|
+
if (needsRender) scheduleRunIndicatorRender({ scroll });
|
|
14793
15656
|
else if (runIndicatorIsActive()) startRunIndicatorTicker();
|
|
14794
|
-
|
|
15657
|
+
scheduleComposerModeButtonsUpdate();
|
|
14795
15658
|
if (active) scheduleRunIndicatorGraceCheck();
|
|
14796
15659
|
}
|
|
14797
15660
|
|
|
@@ -15217,11 +16080,15 @@ function applyChatFollowScroll() {
|
|
|
15217
16080
|
updateStickyUserPromptButton();
|
|
15218
16081
|
}
|
|
15219
16082
|
|
|
15220
|
-
function scheduleChatFollowScroll() {
|
|
16083
|
+
function scheduleChatFollowScroll({ settle = true } = {}) {
|
|
15221
16084
|
if (chatFollowFrame === null) chatFollowFrame = requestAnimationFrame(applyChatFollowScroll);
|
|
16085
|
+
if (!settle) return;
|
|
16086
|
+
chatFollowNeedsSettle = true;
|
|
15222
16087
|
clearTimeout(chatFollowSettleTimer);
|
|
15223
16088
|
chatFollowSettleTimer = setTimeout(() => {
|
|
15224
16089
|
chatFollowSettleTimer = null;
|
|
16090
|
+
if (!chatFollowNeedsSettle) return;
|
|
16091
|
+
chatFollowNeedsSettle = false;
|
|
15225
16092
|
applyChatFollowScroll();
|
|
15226
16093
|
}, CHAT_FOLLOW_SETTLE_DELAY_MS);
|
|
15227
16094
|
}
|
|
@@ -15230,16 +16097,7 @@ function scrollChatToBottom({ force = false } = {}) {
|
|
|
15230
16097
|
if (deferChatFollowScrollDuringPointerActivation({ force })) return;
|
|
15231
16098
|
if (deferChatFollowScrollDuringInteractiveDropdown({ force })) return;
|
|
15232
16099
|
if (force) autoFollowChat = true;
|
|
15233
|
-
if (!autoFollowChat) {
|
|
15234
|
-
updateJumpToLatestButton();
|
|
15235
|
-
updateStickyUserPromptButton();
|
|
15236
|
-
return;
|
|
15237
|
-
}
|
|
15238
|
-
lastChatProgrammaticScrollAt = performance.now();
|
|
15239
|
-
setChatScrollTopInstant(elements.chat.scrollHeight);
|
|
15240
16100
|
scheduleChatFollowScroll();
|
|
15241
|
-
updateJumpToLatestButton();
|
|
15242
|
-
updateStickyUserPromptButton();
|
|
15243
16101
|
}
|
|
15244
16102
|
|
|
15245
16103
|
function syncAutoFollowFromChatScroll() {
|
|
@@ -16774,6 +17632,8 @@ function cancelStreamBubbleHide() {
|
|
|
16774
17632
|
function cancelStreamingAssistantTextRender() {
|
|
16775
17633
|
clearTimeout(streamTextRenderTimer);
|
|
16776
17634
|
streamTextRenderTimer = null;
|
|
17635
|
+
if (streamTextRenderFrame !== null && typeof cancelAnimationFrame === "function") cancelAnimationFrame(streamTextRenderFrame);
|
|
17636
|
+
streamTextRenderFrame = null;
|
|
16777
17637
|
}
|
|
16778
17638
|
|
|
16779
17639
|
function removeStreamBubble() {
|
|
@@ -16786,10 +17646,54 @@ function removeStreamBubble() {
|
|
|
16786
17646
|
renderRunIndicator({ scroll: false });
|
|
16787
17647
|
}
|
|
16788
17648
|
|
|
16789
|
-
function
|
|
17649
|
+
function resetStreamDerivedTextCache() {
|
|
17650
|
+
streamDerivedTextCache = { rawText: null, assistantText: "", thinkingFormat: null, finalText: "" };
|
|
17651
|
+
}
|
|
17652
|
+
|
|
17653
|
+
function setStreamRawText(text) {
|
|
17654
|
+
const nextText = String(text || "");
|
|
17655
|
+
if (nextText === streamRawText) return false;
|
|
17656
|
+
streamRawText = nextText;
|
|
17657
|
+
resetStreamDerivedTextCache();
|
|
17658
|
+
return true;
|
|
17659
|
+
}
|
|
17660
|
+
|
|
17661
|
+
function appendStreamRawText(delta) {
|
|
17662
|
+
const text = String(delta || "");
|
|
17663
|
+
if (!text) return false;
|
|
17664
|
+
streamRawText += text;
|
|
17665
|
+
resetStreamDerivedTextCache();
|
|
17666
|
+
return true;
|
|
17667
|
+
}
|
|
17668
|
+
|
|
17669
|
+
function streamingAssistantTextFallback(event) {
|
|
17670
|
+
// Fallback for legacy/summary events that do not carry a delta. The hot
|
|
17671
|
+
// text_delta path appends update.delta and avoids this accumulated-message scan.
|
|
17672
|
+
return assistantTextFromMessage(assistantStreamingMessage(event), { streaming: true });
|
|
17673
|
+
}
|
|
17674
|
+
|
|
17675
|
+
function syncStreamRawTextFromUpdate(event, update) {
|
|
17676
|
+
if (update.type === "text_delta") {
|
|
17677
|
+
const delta = update.delta ?? update.text ?? update.content ?? "";
|
|
17678
|
+
if (appendStreamRawText(delta)) return true;
|
|
17679
|
+
}
|
|
17680
|
+
if (typeof update.content === "string") return setStreamRawText(update.content);
|
|
17681
|
+
if (typeof update.text === "string") return setStreamRawText(update.text);
|
|
17682
|
+
const partialText = streamingAssistantTextFallback(event);
|
|
17683
|
+
return typeof partialText === "string" ? setStreamRawText(partialText) : false;
|
|
17684
|
+
}
|
|
17685
|
+
|
|
17686
|
+
function streamDerivedText() {
|
|
17687
|
+
if (streamDerivedTextCache.rawText === streamRawText) return streamDerivedTextCache;
|
|
16790
17688
|
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
16791
|
-
const
|
|
16792
|
-
|
|
17689
|
+
const thinkingFormat = splitThinkingFormatText(assistantText, { streaming: true });
|
|
17690
|
+
const finalText = thinkingFormat?.hasThinkingFormat ? stripTodoProgressLines(thinkingFormat.finalText, { streaming: true }) : assistantText;
|
|
17691
|
+
streamDerivedTextCache = { rawText: streamRawText, assistantText, thinkingFormat, finalText };
|
|
17692
|
+
return streamDerivedTextCache;
|
|
17693
|
+
}
|
|
17694
|
+
|
|
17695
|
+
function streamRenderableAssistantText() {
|
|
17696
|
+
return streamDerivedText().finalText;
|
|
16793
17697
|
}
|
|
16794
17698
|
|
|
16795
17699
|
function scheduleStreamBubbleHide() {
|
|
@@ -16804,8 +17708,8 @@ function scheduleStreamBubbleHide() {
|
|
|
16804
17708
|
}, delayMs);
|
|
16805
17709
|
}
|
|
16806
17710
|
|
|
16807
|
-
function syncStreamingThinkingFormat(
|
|
16808
|
-
const parsed =
|
|
17711
|
+
function syncStreamingThinkingFormat() {
|
|
17712
|
+
const parsed = streamDerivedText().thinkingFormat;
|
|
16809
17713
|
if (!parsed?.hasThinkingFormat) return null;
|
|
16810
17714
|
const thinking = visibleThinkingText(parsed.thinkingText);
|
|
16811
17715
|
if (thinking) setStreamingThinkingText(thinking);
|
|
@@ -16814,9 +17718,8 @@ function syncStreamingThinkingFormat(assistantText) {
|
|
|
16814
17718
|
}
|
|
16815
17719
|
|
|
16816
17720
|
function renderStreamingAssistantText() {
|
|
16817
|
-
const
|
|
16818
|
-
const
|
|
16819
|
-
const finalText = thinkingFormat?.hasThinkingFormat ? stripTodoProgressLines(thinkingFormat.finalText, { streaming: true }) : assistantText;
|
|
17721
|
+
const thinkingFormat = syncStreamingThinkingFormat();
|
|
17722
|
+
const finalText = thinkingFormat?.hasThinkingFormat ? streamDerivedText().finalText : streamRenderableAssistantText();
|
|
16820
17723
|
if (finalText) {
|
|
16821
17724
|
ensureStreamBubble();
|
|
16822
17725
|
renderStreamingMarkdown(streamText, finalText);
|
|
@@ -16825,19 +17728,72 @@ function renderStreamingAssistantText() {
|
|
|
16825
17728
|
}
|
|
16826
17729
|
}
|
|
16827
17730
|
|
|
16828
|
-
function scheduleStreamingAssistantTextRender() {
|
|
16829
|
-
if (streamTextRenderTimer) return;
|
|
16830
|
-
|
|
17731
|
+
function scheduleStreamingAssistantTextRender({ immediate = false } = {}) {
|
|
17732
|
+
if (streamTextRenderTimer || streamTextRenderFrame !== null) return;
|
|
17733
|
+
const flush = () => {
|
|
16831
17734
|
streamTextRenderTimer = null;
|
|
17735
|
+
streamTextRenderFrame = null;
|
|
16832
17736
|
renderStreamingAssistantText();
|
|
16833
|
-
|
|
17737
|
+
scheduleChatFollowScroll();
|
|
17738
|
+
};
|
|
17739
|
+
if (immediate && typeof requestAnimationFrame === "function") streamTextRenderFrame = requestAnimationFrame(flush);
|
|
17740
|
+
else streamTextRenderTimer = setTimeout(flush, immediate ? 0 : STREAM_OUTPUT_TOOLCALL_GUARD_MS);
|
|
16834
17741
|
}
|
|
16835
17742
|
|
|
16836
17743
|
function suppressStreamingAssistantTextBeforeToolCall() {
|
|
16837
|
-
|
|
17744
|
+
setStreamRawText("");
|
|
16838
17745
|
removeStreamBubble();
|
|
16839
17746
|
}
|
|
16840
17747
|
|
|
17748
|
+
function resetStreamingToolCallState({ remove = true } = {}) {
|
|
17749
|
+
if (remove) streamToolCallBubble?.remove();
|
|
17750
|
+
streamToolCallBubble = null;
|
|
17751
|
+
streamToolCallText = null;
|
|
17752
|
+
streamToolCallRawArguments = "";
|
|
17753
|
+
streamToolCallName = "";
|
|
17754
|
+
streamToolCallId = "";
|
|
17755
|
+
streamToolCallContentIndex = null;
|
|
17756
|
+
streamToolCallComplete = false;
|
|
17757
|
+
}
|
|
17758
|
+
|
|
17759
|
+
function streamingToolCallTitle() {
|
|
17760
|
+
const name = streamToolCallName || "tool";
|
|
17761
|
+
return `tool call: ${name}${streamToolCallComplete ? " (ready)" : " (building)"}`;
|
|
17762
|
+
}
|
|
17763
|
+
|
|
17764
|
+
function streamingToolCallMessage() {
|
|
17765
|
+
return {
|
|
17766
|
+
role: "toolCall",
|
|
17767
|
+
title: streamingToolCallTitle(),
|
|
17768
|
+
timestamp: Date.now(),
|
|
17769
|
+
toolName: streamToolCallName || "tool",
|
|
17770
|
+
toolCallId: streamToolCallId,
|
|
17771
|
+
rawArguments: streamToolCallRawArguments,
|
|
17772
|
+
content: streamToolCallRawArguments,
|
|
17773
|
+
};
|
|
17774
|
+
}
|
|
17775
|
+
|
|
17776
|
+
function renderStreamingToolCallCard({ scroll = false } = {}) {
|
|
17777
|
+
const message = streamingToolCallMessage();
|
|
17778
|
+
const displayText = streamToolCallRawArguments || (streamToolCallComplete ? "{}" : "(waiting for argument stream…)");
|
|
17779
|
+
if (!streamToolCallBubble?.parentElement || !streamToolCallText) {
|
|
17780
|
+
const created = appendMessage(message, { streaming: true });
|
|
17781
|
+
streamToolCallBubble = created.bubble;
|
|
17782
|
+
streamToolCallText = created.body.querySelector(".tool-call-arguments") || created.body.querySelector(".code-block");
|
|
17783
|
+
}
|
|
17784
|
+
streamToolCallBubble._copyMessage = message;
|
|
17785
|
+
const role = streamToolCallBubble.querySelector(":scope > .message-header .message-role");
|
|
17786
|
+
if (role && role.textContent !== message.title) role.textContent = message.title;
|
|
17787
|
+
if (streamToolCallText && streamToolCallText.textContent !== displayText) streamToolCallText.textContent = displayText;
|
|
17788
|
+
renderRunIndicator({ scroll: false });
|
|
17789
|
+
if (scroll) scrollChatToBottom();
|
|
17790
|
+
}
|
|
17791
|
+
|
|
17792
|
+
function removeStreamingToolCallCard() {
|
|
17793
|
+
resetStreamingToolCallState({ remove: true });
|
|
17794
|
+
renderRunIndicator({ scroll: false });
|
|
17795
|
+
}
|
|
17796
|
+
|
|
16841
17797
|
function ensureStreamBubble() {
|
|
16842
17798
|
cancelStreamBubbleHide();
|
|
16843
17799
|
if (streamBubble?.parentElement === elements.chat) return;
|
|
@@ -16872,16 +17828,19 @@ function resetStreamBubble() {
|
|
|
16872
17828
|
streamBubble = null;
|
|
16873
17829
|
streamText = null;
|
|
16874
17830
|
streamRawText = "";
|
|
17831
|
+
streamThinkingRawText = "";
|
|
17832
|
+
resetStreamDerivedTextCache();
|
|
16875
17833
|
streamMarkdownState = null;
|
|
16876
17834
|
streamBubbleVisibleSince = 0;
|
|
16877
17835
|
streamToolCallSeen = false;
|
|
17836
|
+
resetStreamingToolCallState({ remove: true });
|
|
16878
17837
|
streamThinkingBubble = null;
|
|
16879
17838
|
streamThinking = null;
|
|
16880
17839
|
streamMessageActive = false;
|
|
16881
17840
|
}
|
|
16882
17841
|
|
|
16883
17842
|
function liveStreamRenderActive() {
|
|
16884
|
-
return streamMessageActive && currentState?.isStreaming === true && Boolean(streamBubble || streamThinkingBubble || streamRawText);
|
|
17843
|
+
return streamMessageActive && currentState?.isStreaming === true && Boolean(streamBubble || streamThinkingBubble || streamToolCallBubble || streamRawText || streamToolCallRawArguments);
|
|
16885
17844
|
}
|
|
16886
17845
|
|
|
16887
17846
|
/**
|
|
@@ -16892,15 +17851,19 @@ function liveStreamRenderActive() {
|
|
|
16892
17851
|
function restoreStreamRenderAfterChatRebuild() {
|
|
16893
17852
|
const thinkingText = streamThinking?.textContent || "";
|
|
16894
17853
|
const thinkingComplete = streamThinkingBubble?.classList.contains("complete") === true;
|
|
17854
|
+
const toolCallWasVisible = !!(streamToolCallBubble?.parentElement === elements.chat || streamToolCallRawArguments || streamToolCallName);
|
|
16895
17855
|
streamBubble = null;
|
|
16896
17856
|
streamText = null;
|
|
16897
17857
|
streamThinkingBubble = null;
|
|
16898
17858
|
streamThinking = null;
|
|
17859
|
+
streamToolCallBubble = null;
|
|
17860
|
+
streamToolCallText = null;
|
|
16899
17861
|
streamBubbleVisibleSince = 0;
|
|
16900
17862
|
if (thinkingText && setStreamingThinkingText(thinkingText) && thinkingComplete) {
|
|
16901
17863
|
streamThinkingBubble?.classList.add("complete");
|
|
16902
17864
|
}
|
|
16903
|
-
if (
|
|
17865
|
+
if (toolCallWasVisible) renderStreamingToolCallCard();
|
|
17866
|
+
if (streamRenderableAssistantText()) renderStreamingAssistantText();
|
|
16904
17867
|
}
|
|
16905
17868
|
|
|
16906
17869
|
function thinkingDeltaText(update) {
|
|
@@ -16913,6 +17876,56 @@ function assistantStreamingMessage(event) {
|
|
|
16913
17876
|
return partial?.role === "assistant" ? partial : null;
|
|
16914
17877
|
}
|
|
16915
17878
|
|
|
17879
|
+
function assistantToolCallPartFromUpdate(event, update = event?.assistantMessageEvent || {}) {
|
|
17880
|
+
if (isAssistantToolCallPart(update.toolCall)) return update.toolCall;
|
|
17881
|
+
const message = assistantStreamingMessage(event);
|
|
17882
|
+
const content = message?.content;
|
|
17883
|
+
if (!Array.isArray(content)) return null;
|
|
17884
|
+
const contentIndex = Number(update.contentIndex);
|
|
17885
|
+
if (Number.isInteger(contentIndex) && isAssistantToolCallPart(content[contentIndex])) return content[contentIndex];
|
|
17886
|
+
for (let index = content.length - 1; index >= 0; index -= 1) {
|
|
17887
|
+
if (isAssistantToolCallPart(content[index])) return content[index];
|
|
17888
|
+
}
|
|
17889
|
+
return null;
|
|
17890
|
+
}
|
|
17891
|
+
|
|
17892
|
+
function streamToolCallNameFromUpdate(update, part) {
|
|
17893
|
+
const rawName = update.name || update.toolName || update.toolCall?.name || assistantToolCallName(part);
|
|
17894
|
+
const name = runIndicatorToolName(rawName);
|
|
17895
|
+
return name === "unknown" ? "tool" : name;
|
|
17896
|
+
}
|
|
17897
|
+
|
|
17898
|
+
function streamingToolCallContentIndexFromUpdate(update) {
|
|
17899
|
+
const contentIndex = Number(update.contentIndex);
|
|
17900
|
+
return Number.isInteger(contentIndex) ? contentIndex : null;
|
|
17901
|
+
}
|
|
17902
|
+
|
|
17903
|
+
function updateStreamingToolCallFromEvent(event, { reset = false, appendDelta = false, complete = false, scroll = false } = {}) {
|
|
17904
|
+
const update = event.assistantMessageEvent || {};
|
|
17905
|
+
const part = assistantToolCallPartFromUpdate(event, update);
|
|
17906
|
+
const contentIndex = streamingToolCallContentIndexFromUpdate(update);
|
|
17907
|
+
if (reset || (contentIndex !== null && streamToolCallContentIndex !== null && contentIndex !== streamToolCallContentIndex)) {
|
|
17908
|
+
resetStreamingToolCallState({ remove: true });
|
|
17909
|
+
}
|
|
17910
|
+
streamToolCallContentIndex = contentIndex ?? streamToolCallContentIndex;
|
|
17911
|
+
streamToolCallName = streamToolCallNameFromUpdate(update, part) || streamToolCallName || "tool";
|
|
17912
|
+
streamToolCallId = assistantToolCallId(update.toolCall || part) || streamToolCallId;
|
|
17913
|
+
streamToolCallComplete = !!complete;
|
|
17914
|
+
|
|
17915
|
+
const partArgumentText = toolCallArgumentsText(assistantToolCallArguments(update.toolCall || part), { includeEmptyObject: false });
|
|
17916
|
+
if (reset) streamToolCallRawArguments = partArgumentText || "";
|
|
17917
|
+
if (appendDelta && update.delta !== undefined && update.delta !== null) streamToolCallRawArguments += typeof update.delta === "string" ? update.delta : String(update.delta);
|
|
17918
|
+
if (!streamToolCallRawArguments && partArgumentText) streamToolCallRawArguments = partArgumentText;
|
|
17919
|
+
if (complete) {
|
|
17920
|
+
const finalArgumentText = toolCallArgumentsText(assistantToolCallArguments(update.toolCall || part), { includeEmptyObject: true });
|
|
17921
|
+
if (finalArgumentText && (finalArgumentText !== "{}" || !streamToolCallRawArguments)) streamToolCallRawArguments = finalArgumentText;
|
|
17922
|
+
if (!streamToolCallRawArguments) streamToolCallRawArguments = "{}";
|
|
17923
|
+
}
|
|
17924
|
+
|
|
17925
|
+
renderStreamingToolCallCard({ scroll });
|
|
17926
|
+
return streamToolCallName || "tool";
|
|
17927
|
+
}
|
|
17928
|
+
|
|
16916
17929
|
function assistantTextFromMessage(message, { streaming = false } = {}) {
|
|
16917
17930
|
void streaming;
|
|
16918
17931
|
const content = message?.content;
|
|
@@ -16957,57 +17970,98 @@ function setStreamingThinkingText(text) {
|
|
|
16957
17970
|
return true;
|
|
16958
17971
|
}
|
|
16959
17972
|
|
|
16960
|
-
function
|
|
17973
|
+
function streamingThinkingTextFallback(event) {
|
|
17974
|
+
return assistantThinkingTextFromMessage(assistantStreamingMessage(event), { streaming: true });
|
|
17975
|
+
}
|
|
17976
|
+
|
|
17977
|
+
function setStreamThinkingRawText(text) {
|
|
17978
|
+
const thinking = visibleThinkingText(text);
|
|
17979
|
+
if (thinking === streamThinkingRawText) return false;
|
|
17980
|
+
streamThinkingRawText = thinking;
|
|
17981
|
+
return true;
|
|
17982
|
+
}
|
|
17983
|
+
|
|
17984
|
+
function appendStreamThinkingText(delta) {
|
|
17985
|
+
const thinking = visibleThinkingText(delta);
|
|
17986
|
+
if (!thinking) return false;
|
|
17987
|
+
streamThinkingRawText += thinking;
|
|
17988
|
+
return true;
|
|
17989
|
+
}
|
|
17990
|
+
|
|
17991
|
+
function syncStreamingThinkingFromUpdate(event, update, { placeholder = "" } = {}) {
|
|
16961
17992
|
if (!thinkingOutputVisible) return true;
|
|
16962
|
-
const
|
|
16963
|
-
if (
|
|
16964
|
-
|
|
17993
|
+
const delta = thinkingDeltaText(update);
|
|
17994
|
+
if (update.type === "thinking_delta" && delta) {
|
|
17995
|
+
appendStreamThinkingText(delta);
|
|
17996
|
+
return setStreamingThinkingText(streamThinkingRawText || placeholder);
|
|
17997
|
+
}
|
|
17998
|
+
if (update.type === "thinking_start") {
|
|
17999
|
+
if (delta) setStreamThinkingRawText(delta);
|
|
18000
|
+
return streamThinkingRawText ? setStreamingThinkingText(streamThinkingRawText || placeholder) : false;
|
|
18001
|
+
}
|
|
18002
|
+
if (update.type === "thinking_end") {
|
|
18003
|
+
if (delta) setStreamThinkingRawText(delta);
|
|
18004
|
+
else {
|
|
18005
|
+
const fallback = streamingThinkingTextFallback(event);
|
|
18006
|
+
if (fallback !== null) setStreamThinkingRawText(fallback);
|
|
18007
|
+
}
|
|
18008
|
+
return setStreamingThinkingText(streamThinkingRawText || placeholder);
|
|
18009
|
+
}
|
|
18010
|
+
const fallback = streamingThinkingTextFallback(event);
|
|
18011
|
+
if (fallback === null) return false;
|
|
18012
|
+
setStreamThinkingRawText(fallback);
|
|
18013
|
+
return setStreamingThinkingText(streamThinkingRawText || placeholder);
|
|
16965
18014
|
}
|
|
16966
18015
|
|
|
16967
18016
|
function handleMessageUpdate(event) {
|
|
16968
18017
|
const update = event.assistantMessageEvent || {};
|
|
16969
18018
|
if (update.type === "thinking_start") {
|
|
16970
18019
|
setRunIndicatorActivity("Thinking…", { scroll: false });
|
|
16971
|
-
|
|
16972
|
-
|
|
18020
|
+
syncStreamingThinkingFromUpdate(event, update);
|
|
18021
|
+
scheduleChatFollowScroll();
|
|
16973
18022
|
} else if (update.type === "thinking_delta") {
|
|
16974
18023
|
const delta = thinkingDeltaText(update);
|
|
16975
18024
|
setRunIndicatorActivity("Thinking…", { scroll: false });
|
|
16976
|
-
const synced =
|
|
18025
|
+
const synced = syncStreamingThinkingFromUpdate(event, update);
|
|
16977
18026
|
if (thinkingOutputVisible && delta && (!synced || !streamThinking?.textContent)) {
|
|
16978
18027
|
showStreamingThinking("");
|
|
16979
18028
|
if (streamThinking?.textContent === "Thinking…") streamThinking.textContent = "";
|
|
16980
18029
|
if (streamThinking) streamThinking.textContent += delta;
|
|
16981
18030
|
}
|
|
16982
|
-
|
|
18031
|
+
scheduleChatFollowScroll();
|
|
16983
18032
|
} else if (update.type === "thinking_end") {
|
|
16984
|
-
|
|
16985
|
-
if (finalThinking) setStreamingThinkingText(finalThinking);
|
|
16986
|
-
streamThinkingBubble?.classList.add("complete");
|
|
18033
|
+
if (syncStreamingThinkingFromUpdate(event, update)) streamThinkingBubble?.classList.add("complete");
|
|
16987
18034
|
setRunIndicatorActivity("Finished thinking; waiting for the next output or action…", { scroll: false });
|
|
16988
18035
|
} else if (update.type === "text_delta" || update.type === "text_end") {
|
|
16989
|
-
|
|
16990
|
-
|
|
16991
|
-
if (typeof partialText === "string") streamRawText = partialText;
|
|
16992
|
-
else if (update.type === "text_end" && typeof update.content === "string") streamRawText = update.content;
|
|
16993
|
-
else streamRawText += delta;
|
|
18036
|
+
syncStreamRawTextFromUpdate(event, update);
|
|
18037
|
+
scheduleLiveTodoProgressWidgetSync(streamRawText, event.tabId || activeTabId);
|
|
16994
18038
|
setRunIndicatorActivity("Writing response…", { scroll: false });
|
|
16995
|
-
|
|
16996
|
-
else scheduleStreamingAssistantTextRender();
|
|
18039
|
+
scheduleStreamingAssistantTextRender({ immediate: !!(streamToolCallSeen || streamBubble) });
|
|
16997
18040
|
// Streaming output must stay transcript-local. Full footer/status
|
|
16998
18041
|
// reconciliation happens on message/state refreshes, not per token.
|
|
16999
|
-
|
|
18042
|
+
scheduleChatFollowScroll();
|
|
17000
18043
|
} else if (update.type === "toolcall_start") {
|
|
17001
18044
|
streamToolCallSeen = true;
|
|
17002
18045
|
suppressStreamingAssistantTextBeforeToolCall();
|
|
17003
|
-
const name =
|
|
17004
|
-
setRunIndicatorActivity(`
|
|
18046
|
+
const name = updateStreamingToolCallFromEvent(event, { reset: true, scroll: true });
|
|
18047
|
+
setRunIndicatorActivity(`Building tool call: ${name}…`, { scroll: false });
|
|
17005
18048
|
addEvent(`tool call started in assistant message`, "info");
|
|
18049
|
+
} else if (update.type === "toolcall_delta") {
|
|
18050
|
+
streamToolCallSeen = true;
|
|
18051
|
+
suppressStreamingAssistantTextBeforeToolCall();
|
|
18052
|
+
const name = updateStreamingToolCallFromEvent(event, { appendDelta: true });
|
|
18053
|
+
setRunIndicatorActivity(`Building tool call: ${name}…`, { scroll: false });
|
|
18054
|
+
scheduleChatFollowScroll();
|
|
18055
|
+
} else if (update.type === "toolcall_end") {
|
|
18056
|
+
streamToolCallSeen = true;
|
|
18057
|
+
suppressStreamingAssistantTextBeforeToolCall();
|
|
18058
|
+
const name = updateStreamingToolCallFromEvent(event, { complete: true, scroll: true });
|
|
18059
|
+
setRunIndicatorActivity(`Tool call ready: ${name}; waiting to run…`, { scroll: false });
|
|
17006
18060
|
} else if (update.type === "error") {
|
|
17007
18061
|
setRunIndicatorActivity("Assistant stream reported an error…");
|
|
17008
18062
|
appendMessage({ role: "error", title: "assistant error", timestamp: Date.now(), content: update.reason || update.errorMessage || "assistant error", level: "error" }, { streaming: true });
|
|
17009
18063
|
renderRunIndicator({ scroll: false });
|
|
17010
|
-
|
|
18064
|
+
scheduleChatFollowScroll();
|
|
17011
18065
|
}
|
|
17012
18066
|
}
|
|
17013
18067
|
|
|
@@ -17304,6 +18358,7 @@ async function refreshModels(tabContext = activeTabContext()) {
|
|
|
17304
18358
|
if (!isCurrentTabContext(tabContext)) return;
|
|
17305
18359
|
availableModels = models;
|
|
17306
18360
|
footerScopedModels = scopedModels;
|
|
18361
|
+
footerScopedModels = orderedFooterScopedModels();
|
|
17307
18362
|
footerScopedModelPatterns = scopedModelPatterns;
|
|
17308
18363
|
footerScopedModelSource = scopedModelSource;
|
|
17309
18364
|
if (scopedModelError) addEvent(`failed to load scoped models: ${scopedModelError.message}`, "warn");
|
|
@@ -18605,18 +19660,17 @@ function handleExtensionUiRequest(request) {
|
|
|
18605
19660
|
}
|
|
18606
19661
|
case "setWidget": {
|
|
18607
19662
|
const widgetKey = request.widgetKey || request.id;
|
|
19663
|
+
const requestTabId = request.tabId || activeTabId;
|
|
18608
19664
|
if (widgetKey === "pi-remote-webui") {
|
|
18609
|
-
|
|
19665
|
+
setWidgetForTab(requestTabId, widgetKey, { ...request, widgetLines: undefined });
|
|
18610
19666
|
if (Array.isArray(request.widgetLines)) {
|
|
18611
19667
|
mirrorRemoteWebuiWidgetToTranscript(widgetKey, request.widgetLines, request);
|
|
18612
19668
|
showRemoteWebuiQrPopup(widgetKey, request.widgetLines, request);
|
|
18613
19669
|
} else {
|
|
18614
19670
|
closeRemoteWebuiQrPopup();
|
|
18615
19671
|
}
|
|
18616
|
-
} else if (Array.isArray(request.widgetLines)) {
|
|
18617
|
-
widgets.set(widgetKey, request);
|
|
18618
19672
|
} else {
|
|
18619
|
-
|
|
19673
|
+
if (!setWidgetForTab(requestTabId, widgetKey, request)) return;
|
|
18620
19674
|
}
|
|
18621
19675
|
updateOptionalFeatureAvailability();
|
|
18622
19676
|
renderWidgets();
|
|
@@ -18753,9 +19807,9 @@ function handleInactiveTabEvent(event) {
|
|
|
18753
19807
|
}
|
|
18754
19808
|
|
|
18755
19809
|
function handleEvent(event) {
|
|
18756
|
-
ingestEventTabActivity(event);
|
|
18757
|
-
trackAutoRetryStateFromEvent(event);
|
|
18758
|
-
trackSkillsFromEvent(event);
|
|
19810
|
+
if (eventHasTabActivityPayload(event)) ingestEventTabActivity(event);
|
|
19811
|
+
if (event?.type === "auto_retry_start" || event?.type === "auto_retry_end") trackAutoRetryStateFromEvent(event);
|
|
19812
|
+
if (eventMayAffectSkillUsage(event)) trackSkillsFromEvent(event);
|
|
18759
19813
|
if (!eventTargetsActiveTab(event)) {
|
|
18760
19814
|
handleInactiveTabEvent(event);
|
|
18761
19815
|
return;
|
|
@@ -18793,7 +19847,7 @@ function handleEvent(event) {
|
|
|
18793
19847
|
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
19848
|
clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
18795
19849
|
statusEntries.clear();
|
|
18796
|
-
|
|
19850
|
+
clearWidgetsForTab(event.tabId || activeTabId);
|
|
18797
19851
|
latestBtwWidgetPayload = null;
|
|
18798
19852
|
btwWidgetDismissedId = "";
|
|
18799
19853
|
btwWidgetComposerOpen = false;
|
|
@@ -18924,6 +19978,7 @@ function handleEvent(event) {
|
|
|
18924
19978
|
case "tool_execution_start":
|
|
18925
19979
|
streamToolCallSeen = true;
|
|
18926
19980
|
suppressStreamingAssistantTextBeforeToolCall();
|
|
19981
|
+
removeStreamingToolCallCard();
|
|
18927
19982
|
handleToolExecutionStart(event);
|
|
18928
19983
|
setRunIndicatorActivity(`Running tool: ${runIndicatorToolName(event.toolName)}…`);
|
|
18929
19984
|
addEvent(`tool ${event.toolName} started`);
|