@firstpick/pi-package-webui 0.3.9 → 0.4.1
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 +16 -11
- package/bin/pi-webui.mjs +841 -39
- package/package.json +11 -22
- package/public/app.js +2629 -193
- package/public/index.html +78 -4
- package/public/service-worker.js +1 -1
- package/public/styles.css +931 -4
- package/tests/fixtures/fake-pi.mjs +10 -1
- package/tests/http-endpoints-harness.test.mjs +140 -2
- package/tests/mobile-static.test.mjs +96 -31
package/public/app.js
CHANGED
|
@@ -11,7 +11,11 @@ const elements = {
|
|
|
11
11
|
newTabCurrentDirectoryButton: $("#newTabCurrentDirectoryButton"),
|
|
12
12
|
newTabChooseDirectoryButton: $("#newTabChooseDirectoryButton"),
|
|
13
13
|
closeAllTabsButton: $("#closeAllTabsButton"),
|
|
14
|
+
commandPaletteButton: $("#commandPaletteButton"),
|
|
15
|
+
workspaceDashboardToggleButton: $("#workspaceDashboardToggleButton"),
|
|
16
|
+
workspaceDashboard: $("#workspaceDashboard"),
|
|
14
17
|
statusBar: $("#statusBar"),
|
|
18
|
+
contextMeterBar: $("#contextMeterBar"),
|
|
15
19
|
serverOfflinePanel: $("#serverOfflinePanel"),
|
|
16
20
|
serverRestartPanel: $("#serverRestartPanel"),
|
|
17
21
|
serverRestartMessage: $("#serverRestartMessage"),
|
|
@@ -28,6 +32,12 @@ const elements = {
|
|
|
28
32
|
widgetArea: $("#widgetArea"),
|
|
29
33
|
stickyUserPromptButton: $("#stickyUserPromptButton"),
|
|
30
34
|
chat: $("#chat"),
|
|
35
|
+
chatSearchBar: $("#chatSearchBar"),
|
|
36
|
+
chatSearchInput: $("#chatSearchInput"),
|
|
37
|
+
chatSearchCount: $("#chatSearchCount"),
|
|
38
|
+
chatSearchPrevButton: $("#chatSearchPrevButton"),
|
|
39
|
+
chatSearchNextButton: $("#chatSearchNextButton"),
|
|
40
|
+
chatSearchCloseButton: $("#chatSearchCloseButton"),
|
|
31
41
|
feedbackTray: $("#feedbackTray"),
|
|
32
42
|
feedbackTraySummary: $("#feedbackTraySummary"),
|
|
33
43
|
sendFeedbackButton: $("#sendFeedbackButton"),
|
|
@@ -72,6 +82,7 @@ const elements = {
|
|
|
72
82
|
appRunnerMenuPanel: $("#appRunnerMenuPanel"),
|
|
73
83
|
optionsMenuButton: $("#optionsMenuButton"),
|
|
74
84
|
optionsMenu: $("#optionsMenu"),
|
|
85
|
+
optionsCommandPaletteButton: $("#optionsCommandPaletteButton"),
|
|
75
86
|
optionsResumeButton: $("#optionsResumeButton"),
|
|
76
87
|
optionsReloadButton: $("#optionsReloadButton"),
|
|
77
88
|
optionsNameButton: $("#optionsNameButton"),
|
|
@@ -80,7 +91,9 @@ const elements = {
|
|
|
80
91
|
optionsExportButton: $("#optionsExportButton"),
|
|
81
92
|
optionsForkButton: $("#optionsForkButton"),
|
|
82
93
|
optionsTreeButton: $("#optionsTreeButton"),
|
|
94
|
+
optionsStatsButton: $("#optionsStatsButton"),
|
|
83
95
|
gitWorkflowPanel: $("#gitWorkflowPanel"),
|
|
96
|
+
gitWorkflowKicker: $("#gitWorkflowKicker"),
|
|
84
97
|
gitWorkflowTitle: $("#gitWorkflowTitle"),
|
|
85
98
|
gitWorkflowHint: $("#gitWorkflowHint"),
|
|
86
99
|
gitWorkflowSteps: $("#gitWorkflowSteps"),
|
|
@@ -178,6 +191,17 @@ const elements = {
|
|
|
178
191
|
pathPickerError: $("#pathPickerError"),
|
|
179
192
|
pathPickerCancelButton: $("#pathPickerCancelButton"),
|
|
180
193
|
pathPickerChooseButton: $("#pathPickerChooseButton"),
|
|
194
|
+
commandPaletteDialog: $("#commandPaletteDialog"),
|
|
195
|
+
commandPaletteInput: $("#commandPaletteInput"),
|
|
196
|
+
commandPaletteList: $("#commandPaletteList"),
|
|
197
|
+
commandPaletteHint: $("#commandPaletteHint"),
|
|
198
|
+
editRetryDialog: $("#editRetryDialog"),
|
|
199
|
+
editRetryMessage: $("#editRetryMessage"),
|
|
200
|
+
editRetryText: $("#editRetryText"),
|
|
201
|
+
editRetryStatus: $("#editRetryStatus"),
|
|
202
|
+
editRetryCancelButton: $("#editRetryCancelButton"),
|
|
203
|
+
editRetryForkButton: $("#editRetryForkButton"),
|
|
204
|
+
editRetrySendButton: $("#editRetrySendButton"),
|
|
181
205
|
nativeCommandDialog: $("#nativeCommandDialog"),
|
|
182
206
|
nativeCommandTitle: $("#nativeCommandTitle"),
|
|
183
207
|
nativeCommandMessage: $("#nativeCommandMessage"),
|
|
@@ -188,6 +212,15 @@ const elements = {
|
|
|
188
212
|
appRunnerInfoDialog: $("#appRunnerInfoDialog"),
|
|
189
213
|
appRunnerInfoBody: $("#appRunnerInfoBody"),
|
|
190
214
|
appRunnerInfoCloseButton: $("#appRunnerInfoCloseButton"),
|
|
215
|
+
statsOverlayDialog: $("#statsOverlayDialog"),
|
|
216
|
+
statsOverlaySubtitle: $("#statsOverlaySubtitle"),
|
|
217
|
+
statsOverlayScope: $("#statsOverlayScope"),
|
|
218
|
+
statsOverlayCustomDays: $("#statsOverlayCustomDays"),
|
|
219
|
+
statsOverlayRefreshButton: $("#statsOverlayRefreshButton"),
|
|
220
|
+
statsOverlayStatus: $("#statsOverlayStatus"),
|
|
221
|
+
statsOverlayTabs: $("#statsOverlayTabs"),
|
|
222
|
+
statsOverlayBody: $("#statsOverlayBody"),
|
|
223
|
+
statsOverlayCloseButton: $("#statsOverlayCloseButton"),
|
|
191
224
|
};
|
|
192
225
|
|
|
193
226
|
let currentState = null;
|
|
@@ -257,6 +290,13 @@ let pathSuggestActiveQuery = null;
|
|
|
257
290
|
let pathSuggestRequestSerial = 0;
|
|
258
291
|
let pathSuggestAbortController = null;
|
|
259
292
|
let latestStats = null;
|
|
293
|
+
let statsOverlayActiveTab = "overview";
|
|
294
|
+
let statsOverlayLoading = false;
|
|
295
|
+
let statsOverlayError = "";
|
|
296
|
+
let statsOverlayLastScope = "14";
|
|
297
|
+
let statsOverlayCalibrationMessage = "";
|
|
298
|
+
let statsOverlayCalibrationBusy = "";
|
|
299
|
+
let latestStatsOverlayPayload = null;
|
|
260
300
|
let latestWorkspace = null;
|
|
261
301
|
let latestNetwork = null;
|
|
262
302
|
let webuiVersion = "";
|
|
@@ -328,6 +368,10 @@ let userBashQueuesByTab = new Map();
|
|
|
328
368
|
let latestQueuedMessagesByTab = new Map();
|
|
329
369
|
let loadedPromptList = null;
|
|
330
370
|
let promptListRunning = false;
|
|
371
|
+
let workspaceDashboardCollapsed = false;
|
|
372
|
+
let commandPaletteIndex = 0;
|
|
373
|
+
let commandPaletteItems = [];
|
|
374
|
+
let activeEditRetry = null;
|
|
331
375
|
let abortLongPressTimer = null;
|
|
332
376
|
let abortLongPressHandled = false;
|
|
333
377
|
const dialogQueue = [];
|
|
@@ -356,10 +400,16 @@ const GIT_FOOTER_WEBUI_STATUS_KEY = "git-footer-webui";
|
|
|
356
400
|
const GIT_FOOTER_WEBUI_PAYLOAD_TYPE = "firstpick.git-footer-status.footer";
|
|
357
401
|
const GIT_FOOTER_WEBUI_PAYLOAD_VERSION = 1;
|
|
358
402
|
const GIT_FOOTER_WEBUI_PAYLOAD_CACHE_KEY = "pi-webui-git-footer-webui-payload-cache";
|
|
403
|
+
const GIT_FOOTER_STATUS_SETUP_STORAGE_KEY = "pi-webui-git-footer-status-setup";
|
|
404
|
+
const GIT_INIT_STACK_STORAGE_KEY = "pi-webui-git-init-stack";
|
|
405
|
+
const STATS_WEBUI_STATUS_KEY = "stats-webui";
|
|
406
|
+
const STATS_WEBUI_PAYLOAD_TYPE = "firstpick.pi-extension-stats.overlay";
|
|
407
|
+
const STATS_WEBUI_PAYLOAD_VERSION = 1;
|
|
359
408
|
const GIT_CHANGES_RENDER_ROW_LIMIT = 4000;
|
|
360
409
|
const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
|
|
361
410
|
const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history";
|
|
362
411
|
const PROMPT_LIST_STORAGE_KEY = "pi-webui-prompt-lists";
|
|
412
|
+
const WORKSPACE_DASHBOARD_STORAGE_KEY = "pi-webui-workspace-dashboard-collapsed";
|
|
363
413
|
const PROMPT_HISTORY_LIMIT_PER_TAB = 50;
|
|
364
414
|
const ATTACHMENT_MAX_FILES = 12;
|
|
365
415
|
const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
@@ -493,10 +543,10 @@ const OPTIONAL_FEATURES = [
|
|
|
493
543
|
},
|
|
494
544
|
{
|
|
495
545
|
id: "statsCommand",
|
|
496
|
-
label: "Stats
|
|
546
|
+
label: "Stats dashboard",
|
|
497
547
|
packageName: "@firstpick/pi-extension-stats",
|
|
498
548
|
capabilityLabel: "/stats",
|
|
499
|
-
description: "Token and cost usage analytics commands.",
|
|
549
|
+
description: "Token and cost usage analytics commands plus the browser dashboard overlay.",
|
|
500
550
|
},
|
|
501
551
|
{
|
|
502
552
|
id: "themeBundle",
|
|
@@ -521,6 +571,7 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
|
521
571
|
["todo-progress-status", "todoProgressWidget"],
|
|
522
572
|
]);
|
|
523
573
|
const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate", "webui-helper"]);
|
|
574
|
+
HIDDEN_COMMAND_NAMES.add("stats-webui");
|
|
524
575
|
const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "name", "resume", "tree", "login", "logout", "scoped-models", "tools", "skills"]);
|
|
525
576
|
const SETTINGS_THINKING_OPTIONS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
526
577
|
const SETTINGS_TRANSPORT_OPTIONS = ["sse", "websocket", "websocket-cached", "auto"];
|
|
@@ -541,13 +592,21 @@ const SETTINGS_IMAGE_WIDTH_OPTIONS = ["60", "80", "120"];
|
|
|
541
592
|
const SETTINGS_EDITOR_PADDING_OPTIONS = ["0", "1", "2", "3"];
|
|
542
593
|
const SETTINGS_AUTOCOMPLETE_OPTIONS = ["3", "5", "7", "10", "15", "20"];
|
|
543
594
|
const optionalFeatureInstallInProgress = new Set();
|
|
595
|
+
const optionalFeaturePackageStatuses = new Map();
|
|
596
|
+
const optionalFeatureInstallMessages = new Map();
|
|
544
597
|
const gitFooterPayloadRefreshInFlightByTab = new Set();
|
|
545
598
|
|
|
546
599
|
function createGitWorkflowActionsDone(patch = {}) {
|
|
547
600
|
return {
|
|
601
|
+
init: false,
|
|
602
|
+
stack: false,
|
|
603
|
+
readme: false,
|
|
604
|
+
gitignore: false,
|
|
548
605
|
stage: false,
|
|
549
606
|
message: false,
|
|
550
607
|
commit: false,
|
|
608
|
+
branch: false,
|
|
609
|
+
remote: false,
|
|
551
610
|
push: false,
|
|
552
611
|
...patch,
|
|
553
612
|
};
|
|
@@ -561,16 +620,41 @@ function gitWorkflowActionDonePatch(workflow, process) {
|
|
|
561
620
|
return { actionsDone: createGitWorkflowActionsDone({ ...workflow?.actionsDone, [process]: true }) };
|
|
562
621
|
}
|
|
563
622
|
|
|
623
|
+
function resetGitWorkflowManualCommitDefaultPatch() {
|
|
624
|
+
return {
|
|
625
|
+
manualCommitMessageDefault: "",
|
|
626
|
+
manualCommitMessageDefaultReason: "",
|
|
627
|
+
manualCommitMessageDefaultPath: "",
|
|
628
|
+
manualCommitMessageDefaultAction: "",
|
|
629
|
+
manualCommitMessageDefaultRequestedAt: 0,
|
|
630
|
+
manualCommitMessageDefaultLoading: false,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function gitWorkflowManualCommitInputMessage(workflow) {
|
|
635
|
+
return String(workflow?.manualCommitMessage || "").trim() || String(workflow?.manualCommitMessageDefault || "").trim();
|
|
636
|
+
}
|
|
637
|
+
|
|
564
638
|
function createGitWorkflowState() {
|
|
565
639
|
return {
|
|
566
640
|
active: false,
|
|
641
|
+
mode: "standard",
|
|
567
642
|
step: "idle",
|
|
568
643
|
process: "stage",
|
|
569
644
|
busy: false,
|
|
570
645
|
runId: 0,
|
|
571
646
|
output: "",
|
|
572
647
|
error: "",
|
|
648
|
+
githubUsername: "",
|
|
649
|
+
repoName: "",
|
|
650
|
+
remoteUrl: "",
|
|
651
|
+
stack: "",
|
|
652
|
+
readmeRequestedAt: 0,
|
|
653
|
+
gitignoreRequestedAt: 0,
|
|
654
|
+
initFilesStatus: null,
|
|
573
655
|
message: null,
|
|
656
|
+
manualCommitMessage: "",
|
|
657
|
+
...resetGitWorkflowManualCommitDefaultPatch(),
|
|
574
658
|
messageRequestedAt: 0,
|
|
575
659
|
branchName: "",
|
|
576
660
|
branchNameRequestedAt: 0,
|
|
@@ -629,6 +713,16 @@ const GIT_WORKFLOW_PROCESSES = [
|
|
|
629
713
|
{ value: "push", label: "Push" },
|
|
630
714
|
];
|
|
631
715
|
const GIT_WORKFLOW_PROCESS_VALUES = new Set(GIT_WORKFLOW_PROCESSES.map((process) => process.value));
|
|
716
|
+
const GIT_INIT_WORKFLOW_PROCESSES = [
|
|
717
|
+
{ value: "init", label: "Init" },
|
|
718
|
+
{ value: "stack", label: "Stack" },
|
|
719
|
+
{ value: "readme", label: "Files" },
|
|
720
|
+
{ value: "commit", label: "Commit" },
|
|
721
|
+
{ value: "branch", label: "Main" },
|
|
722
|
+
{ value: "remote", label: "Remote" },
|
|
723
|
+
{ value: "push", label: "Push" },
|
|
724
|
+
];
|
|
725
|
+
const GIT_INIT_WORKFLOW_PROCESS_VALUES = new Set(GIT_INIT_WORKFLOW_PROCESSES.map((process) => process.value));
|
|
632
726
|
const ACTION_FEEDBACK_REACTIONS = {
|
|
633
727
|
up: { icon: "👍", label: "Good job", title: "Good job!" },
|
|
634
728
|
down: { icon: "👎", label: "Avoid this", title: "Avoid this" },
|
|
@@ -650,13 +744,32 @@ const GIT_WORKFLOW_ACTIVE_INDEX = {
|
|
|
650
744
|
prCreating: 3,
|
|
651
745
|
done: 4,
|
|
652
746
|
};
|
|
747
|
+
const GIT_INIT_WORKFLOW_ACTIVE_INDEX = {
|
|
748
|
+
initSetup: 0,
|
|
749
|
+
initRepo: 0,
|
|
750
|
+
initializingRepo: 0,
|
|
751
|
+
initStack: 1,
|
|
752
|
+
readme: 2,
|
|
753
|
+
readmeCreating: 2,
|
|
754
|
+
readmeGenerating: 2,
|
|
755
|
+
gitignoreGenerating: 2,
|
|
756
|
+
initialCommit: 3,
|
|
757
|
+
initialCommitting: 3,
|
|
758
|
+
mainBranch: 4,
|
|
759
|
+
mainBranching: 4,
|
|
760
|
+
remote: 5,
|
|
761
|
+
remoteAdding: 5,
|
|
762
|
+
initialPush: 6,
|
|
763
|
+
initialPushing: 6,
|
|
764
|
+
done: 7,
|
|
765
|
+
};
|
|
653
766
|
const GIT_WORKFLOW_CREATE_PR_TOOLTIP = [
|
|
654
767
|
"Create PR branch:",
|
|
655
768
|
"1. Ask Pi to generate a type/feature-name branch from staged changes.",
|
|
656
769
|
"2. Read dev/COMMIT/staged-branch-name.txt.",
|
|
657
770
|
"3. Let you confirm or edit the generated branch name.",
|
|
658
771
|
"4. Run git switch -c <branch>.",
|
|
659
|
-
"5. Return here to commit short or
|
|
772
|
+
"5. Return here to commit short, long, or typed input on that branch.",
|
|
660
773
|
"6. Push and Create PR will push upstream, run /pr, let you review, then run gh pr create.",
|
|
661
774
|
].join("\n");
|
|
662
775
|
const GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP = [
|
|
@@ -665,9 +778,40 @@ const GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP = [
|
|
|
665
778
|
"2. Prefill a branch from the commit message if possible.",
|
|
666
779
|
"3. Let you type or edit the type/feature-name branch name.",
|
|
667
780
|
"4. Run git switch -c <branch>.",
|
|
668
|
-
"5. Return here to commit short or
|
|
781
|
+
"5. Return here to commit short, long, or typed input on that branch.",
|
|
669
782
|
"6. Push and Create PR will push upstream, run /pr, let you review, then run gh pr create.",
|
|
670
783
|
].join("\n");
|
|
784
|
+
const GIT_FOOTER_STATUS_SETUP_TOOLTIP = [
|
|
785
|
+
"git-footer-status-setup:",
|
|
786
|
+
"Store the GitHub username used when the Web UI initializes a no-repo directory.",
|
|
787
|
+
"The remote URL is https://github.com/USERNAME/REPO_NAME.git.",
|
|
788
|
+
"The repository name is asked per initialization and defaults to the current folder name.",
|
|
789
|
+
].join("\n");
|
|
790
|
+
const GIT_INIT_REMOTE_TOOLTIP = [
|
|
791
|
+
"Add origin remote:",
|
|
792
|
+
"1. Confirm the GitHub username from git-footer-status-setup.",
|
|
793
|
+
"2. Ask for the repository name if needed.",
|
|
794
|
+
"3. Run git remote add origin https://github.com/USERNAME/REPO_NAME.git.",
|
|
795
|
+
].join("\n");
|
|
796
|
+
const GIT_INIT_STACK_OPTIONS = [
|
|
797
|
+
{ value: "", label: "Auto-detect from codebase" },
|
|
798
|
+
{ value: "Node.js / TypeScript", label: "Node.js / TypeScript" },
|
|
799
|
+
{ value: "React / Vite", label: "React / Vite" },
|
|
800
|
+
{ value: "Next.js", label: "Next.js" },
|
|
801
|
+
{ value: "Python", label: "Python" },
|
|
802
|
+
{ value: "Django", label: "Django" },
|
|
803
|
+
{ value: "FastAPI", label: "FastAPI" },
|
|
804
|
+
{ value: "Rust", label: "Rust" },
|
|
805
|
+
{ value: "Go", label: "Go" },
|
|
806
|
+
{ value: "Java / Gradle", label: "Java / Gradle" },
|
|
807
|
+
{ value: "Docker", label: "Docker" },
|
|
808
|
+
{ value: "Custom", label: "Custom…" },
|
|
809
|
+
];
|
|
810
|
+
const GIT_INIT_STACK_TOOLTIP = [
|
|
811
|
+
"Repository stack:",
|
|
812
|
+
"Choose a known stack or type one. The value is saved in this browser.",
|
|
813
|
+
"If left blank, Pi will inspect the codebase and fall back to sane default .gitignore patterns.",
|
|
814
|
+
].join("\n");
|
|
671
815
|
|
|
672
816
|
function make(tag, className, text) {
|
|
673
817
|
const node = document.createElement(tag);
|
|
@@ -1617,6 +1761,41 @@ function restoreSidePanelState() {
|
|
|
1617
1761
|
setSidePanelCollapsed(stored ?? false, { persist: stored !== null });
|
|
1618
1762
|
}
|
|
1619
1763
|
|
|
1764
|
+
function readStoredWorkspaceDashboardCollapsed() {
|
|
1765
|
+
try {
|
|
1766
|
+
const stored = localStorage.getItem(WORKSPACE_DASHBOARD_STORAGE_KEY);
|
|
1767
|
+
return stored === null ? true : stored === "1";
|
|
1768
|
+
} catch {
|
|
1769
|
+
return true;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function persistWorkspaceDashboardCollapsed(collapsed) {
|
|
1774
|
+
try {
|
|
1775
|
+
localStorage.setItem(WORKSPACE_DASHBOARD_STORAGE_KEY, collapsed ? "1" : "0");
|
|
1776
|
+
} catch {
|
|
1777
|
+
// Ignore storage failures; this is only a browser preference.
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function setWorkspaceDashboardCollapsed(collapsed, { persist = true } = {}) {
|
|
1782
|
+
workspaceDashboardCollapsed = !!collapsed;
|
|
1783
|
+
if (elements.workspaceDashboard) elements.workspaceDashboard.hidden = workspaceDashboardCollapsed;
|
|
1784
|
+
if (elements.workspaceDashboardToggleButton) {
|
|
1785
|
+
elements.workspaceDashboardToggleButton.setAttribute("aria-expanded", workspaceDashboardCollapsed ? "false" : "true");
|
|
1786
|
+
const tooltip = workspaceDashboardCollapsed ? "Show workspace overview" : "Hide workspace overview";
|
|
1787
|
+
const tooltipDetail = `${tooltip}:\n• Shows current tab, cwd, model, context, session, and queue.\n• Opens common workspace/session actions from one place.`;
|
|
1788
|
+
elements.workspaceDashboardToggleButton.title = tooltip;
|
|
1789
|
+
elements.workspaceDashboardToggleButton.setAttribute("aria-label", tooltip);
|
|
1790
|
+
elements.workspaceDashboardToggleButton.setAttribute("data-tooltip", tooltipDetail);
|
|
1791
|
+
}
|
|
1792
|
+
if (persist) persistWorkspaceDashboardCollapsed(workspaceDashboardCollapsed);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function restoreWorkspaceDashboardState() {
|
|
1796
|
+
setWorkspaceDashboardCollapsed(readStoredWorkspaceDashboardCollapsed(), { persist: false });
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1620
1799
|
function bindMobileViewChanges() {
|
|
1621
1800
|
if (!mobileViewMedia) return;
|
|
1622
1801
|
const syncForViewport = (event) => {
|
|
@@ -1866,6 +2045,142 @@ function attachMessageCopyButton(bubble, message, body) {
|
|
|
1866
2045
|
return button;
|
|
1867
2046
|
}
|
|
1868
2047
|
|
|
2048
|
+
function userMessageEditText(message) {
|
|
2049
|
+
return textFromContent(message?.content).trim();
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
function messageEntryId(message) {
|
|
2053
|
+
for (const key of ["entryId", "id", "sessionEntryId", "messageId"]) {
|
|
2054
|
+
const value = message?.[key];
|
|
2055
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2056
|
+
}
|
|
2057
|
+
return "";
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function userMessageOrdinalAtIndex(messageIndex) {
|
|
2061
|
+
if (!Number.isInteger(messageIndex) || messageIndex < 0) return -1;
|
|
2062
|
+
let ordinal = -1;
|
|
2063
|
+
for (let index = 0; index <= messageIndex && index < latestMessages.length; index += 1) {
|
|
2064
|
+
if (latestMessages[index]?.role === "user") ordinal += 1;
|
|
2065
|
+
}
|
|
2066
|
+
return ordinal;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
async function resolveForkMessageForEdit(message, messageIndex, tabId = activeTabId) {
|
|
2070
|
+
const directEntryId = messageEntryId(message);
|
|
2071
|
+
const text = userMessageEditText(message);
|
|
2072
|
+
if (directEntryId) return { entryId: directEntryId, text };
|
|
2073
|
+
const response = await api("/api/fork-messages", { tabId });
|
|
2074
|
+
const forkMessages = Array.isArray(response.data?.messages) ? response.data.messages : [];
|
|
2075
|
+
const ordinal = userMessageOrdinalAtIndex(messageIndex);
|
|
2076
|
+
const ordinalMatch = ordinal >= 0 ? forkMessages[ordinal] : null;
|
|
2077
|
+
if (ordinalMatch?.entryId && userMessageEditText({ content: ordinalMatch.text }) === text) return ordinalMatch;
|
|
2078
|
+
const exactMatches = forkMessages.filter((item) => item?.entryId && String(item.text || "").trim() === text);
|
|
2079
|
+
if (exactMatches.length === 1) return exactMatches[0];
|
|
2080
|
+
if (exactMatches.length > 1 && ordinalMatch?.entryId) return ordinalMatch;
|
|
2081
|
+
throw new Error("Could not map this transcript message to a fork point. Use /fork for the full selector.");
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
function setEditRetryStatus(message = "", level = "info") {
|
|
2085
|
+
if (!elements.editRetryStatus) return;
|
|
2086
|
+
elements.editRetryStatus.textContent = message;
|
|
2087
|
+
elements.editRetryStatus.hidden = !message;
|
|
2088
|
+
elements.editRetryStatus.className = `edit-retry-status ${level} ${message ? "" : "muted"}`.trim();
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function setEditRetryBusy(busy, label = "Working…") {
|
|
2092
|
+
for (const button of [elements.editRetryForkButton, elements.editRetrySendButton, elements.editRetryCancelButton].filter(Boolean)) button.disabled = !!busy;
|
|
2093
|
+
if (elements.editRetrySendButton) elements.editRetrySendButton.textContent = busy ? label : "Fork & run";
|
|
2094
|
+
if (elements.editRetryForkButton) elements.editRetryForkButton.textContent = busy ? "Forking…" : "Fork only";
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function openEditRetryDialog(message, messageIndex = -1) {
|
|
2098
|
+
const text = userMessageEditText(message);
|
|
2099
|
+
if (!text) {
|
|
2100
|
+
addEvent("user message has no editable text", "warn");
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
activeEditRetry = { message, messageIndex, tabId: activeTabId };
|
|
2104
|
+
if (elements.editRetryMessage) elements.editRetryMessage.textContent = `Fork from user message #${messageIndex >= 0 ? messageIndex + 1 : "?"}, edit it, then run or leave the edited prompt in the composer.`;
|
|
2105
|
+
if (elements.editRetryText) {
|
|
2106
|
+
elements.editRetryText.value = text;
|
|
2107
|
+
elements.editRetryText.style.height = "auto";
|
|
2108
|
+
}
|
|
2109
|
+
setEditRetryStatus();
|
|
2110
|
+
setEditRetryBusy(false);
|
|
2111
|
+
if (!elements.editRetryDialog.open) elements.editRetryDialog.showModal();
|
|
2112
|
+
queueMicrotask(() => {
|
|
2113
|
+
elements.editRetryText?.focus();
|
|
2114
|
+
elements.editRetryText?.select();
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
function closeEditRetryDialog() {
|
|
2119
|
+
activeEditRetry = null;
|
|
2120
|
+
if (elements.editRetryDialog?.open) elements.editRetryDialog.close();
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
async function submitEditRetry({ send = false } = {}) {
|
|
2124
|
+
if (!activeEditRetry) return;
|
|
2125
|
+
const editedText = String(elements.editRetryText?.value || "").trim();
|
|
2126
|
+
if (!editedText) {
|
|
2127
|
+
setEditRetryStatus("Prompt cannot be empty.", "error");
|
|
2128
|
+
elements.editRetryText?.focus();
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
const { message, messageIndex, tabId } = activeEditRetry;
|
|
2132
|
+
const tabContext = activeTabContext(tabId || activeTabId);
|
|
2133
|
+
setEditRetryBusy(true, "Forking…");
|
|
2134
|
+
setEditRetryStatus("Resolving fork point…");
|
|
2135
|
+
try {
|
|
2136
|
+
const forkMessage = await resolveForkMessageForEdit(message, messageIndex, tabContext.tabId);
|
|
2137
|
+
setEditRetryStatus("Forking session…");
|
|
2138
|
+
const result = await api("/api/fork", { method: "POST", body: { entryId: forkMessage.entryId }, tabId: tabContext.tabId });
|
|
2139
|
+
applyResponseTab(result);
|
|
2140
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
2141
|
+
closeEditRetryDialog();
|
|
2142
|
+
await refreshAll(tabContext);
|
|
2143
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
2144
|
+
if (send) {
|
|
2145
|
+
addEvent("forked session; sending edited prompt", "info");
|
|
2146
|
+
await sendPrompt("prompt", editedText, { targetTabId: tabContext.tabId, throwOnError: true });
|
|
2147
|
+
} else {
|
|
2148
|
+
elements.promptInput.value = editedText;
|
|
2149
|
+
resizePromptInput();
|
|
2150
|
+
focusPromptInput({ defer: true });
|
|
2151
|
+
addEvent("forked session; edited prompt restored in composer", "info");
|
|
2152
|
+
}
|
|
2153
|
+
} catch (error) {
|
|
2154
|
+
setEditRetryStatus(error.message || String(error), "error");
|
|
2155
|
+
if (send) {
|
|
2156
|
+
elements.promptInput.value = editedText;
|
|
2157
|
+
resizePromptInput();
|
|
2158
|
+
}
|
|
2159
|
+
} finally {
|
|
2160
|
+
setEditRetryBusy(false);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
function attachMessageEditRetryButton(bubble, message, messageIndex, { streaming = false, transient = false } = {}) {
|
|
2165
|
+
if (!bubble || streaming || transient || message?.role !== "user") return null;
|
|
2166
|
+
const text = userMessageEditText(message);
|
|
2167
|
+
if (!text) return null;
|
|
2168
|
+
const existing = bubble.querySelector(":scope > .message-edit-retry-button");
|
|
2169
|
+
if (existing) return existing;
|
|
2170
|
+
const button = make("button", "message-edit-retry-button", "↺");
|
|
2171
|
+
button.type = "button";
|
|
2172
|
+
button.title = "Edit this prompt and retry from here";
|
|
2173
|
+
button.setAttribute("aria-label", button.title);
|
|
2174
|
+
button.addEventListener("click", (event) => {
|
|
2175
|
+
event.preventDefault();
|
|
2176
|
+
event.stopPropagation();
|
|
2177
|
+
openEditRetryDialog(message, messageIndex);
|
|
2178
|
+
});
|
|
2179
|
+
bubble.classList.add("has-edit-retry-action");
|
|
2180
|
+
bubble.append(button);
|
|
2181
|
+
return button;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
1869
2184
|
function safeHttpUrl(value, base = window.location.href) {
|
|
1870
2185
|
const text = String(value || "").trim();
|
|
1871
2186
|
if (!text) return "";
|
|
@@ -2072,12 +2387,12 @@ function renderUpdateNotification(status = latestUpdateStatus, { force = false }
|
|
|
2072
2387
|
if (elements.updateNotificationTitle) elements.updateNotificationTitle.textContent = items.length === 1 ? `${items[0]} available` : "Pi updates available";
|
|
2073
2388
|
if (elements.updateNotificationMessage) {
|
|
2074
2389
|
elements.updateNotificationMessage.textContent = canRunUpdate
|
|
2075
|
-
? "Run
|
|
2390
|
+
? "Run Pi and Web UI package updates now, then restart this Web UI server automatically."
|
|
2076
2391
|
: "Updates are available. Direct Web UI updates are only enabled from localhost on the host machine.";
|
|
2077
2392
|
}
|
|
2078
2393
|
const details = [
|
|
2079
2394
|
items.join(" · "),
|
|
2080
|
-
latestUpdateStatus.webuiDev && latestUpdateStatus.webui?.updateAvailable ? "The current Web UI is a dev checkout;
|
|
2395
|
+
latestUpdateStatus.webuiDev && latestUpdateStatus.webui?.updateAvailable ? "The current Web UI is a dev checkout; update also refreshes this checkout's Web UI/Pi package dependencies when possible." : "",
|
|
2081
2396
|
latestUpdateStatus.packages?.note || "",
|
|
2082
2397
|
].filter(Boolean).join(" ");
|
|
2083
2398
|
if (elements.updateNotificationDetail) elements.updateNotificationDetail.textContent = details;
|
|
@@ -2103,14 +2418,14 @@ function scheduleUpdateStatusRefresh() {
|
|
|
2103
2418
|
clearTimeout(updateStatusRefreshTimer);
|
|
2104
2419
|
updateStatusRefreshTimer = setTimeout(() => {
|
|
2105
2420
|
updateStatusRefreshTimer = null;
|
|
2106
|
-
refreshUpdateStatus({ force: true }).catch((error) => addEvent(`Pi update check failed: ${error.message || String(error)}`, "warn"));
|
|
2421
|
+
refreshUpdateStatus({ force: true }).catch((error) => addEvent(`Pi/Web UI update check failed: ${error.message || String(error)}`, "warn"));
|
|
2107
2422
|
scheduleUpdateStatusRefresh();
|
|
2108
2423
|
}, UPDATE_STATUS_REFRESH_MS);
|
|
2109
2424
|
}
|
|
2110
2425
|
|
|
2111
2426
|
function initializeUpdateNotifications() {
|
|
2112
2427
|
setTimeout(() => {
|
|
2113
|
-
refreshUpdateStatus().catch((error) => addEvent(`Pi update check failed: ${error.message || String(error)}`, "warn"));
|
|
2428
|
+
refreshUpdateStatus().catch((error) => addEvent(`Pi/Web UI update check failed: ${error.message || String(error)}`, "warn"));
|
|
2114
2429
|
scheduleUpdateStatusRefresh();
|
|
2115
2430
|
}, UPDATE_STATUS_INITIAL_DELAY_MS);
|
|
2116
2431
|
}
|
|
@@ -2119,13 +2434,13 @@ function piUpdateConfirmationText() {
|
|
|
2119
2434
|
const items = updateNotificationItems();
|
|
2120
2435
|
const workingWarning = hasWorkingTab() ? "\n\nOne or more Pi tabs look busy or blocked. Finish or abort in-flight work before updating if you need to preserve it." : "";
|
|
2121
2436
|
const versionText = items.length ? `\n\nDetected update: ${items.join(" · ")}.` : "";
|
|
2122
|
-
return `Run
|
|
2437
|
+
return `Run Pi/Web UI package updates now?${versionText}\n\nThis will run \"pi update\" plus detected local and global Web UI/Pi package-manager updates on the Web UI host. After it finishes, Pi Web UI will restart itself. Browser clients will briefly disconnect, and managed Pi tabs/RPC processes will be restarted from saved session state when possible.${workingWarning}`;
|
|
2123
2438
|
}
|
|
2124
2439
|
|
|
2125
2440
|
async function runPiUpdateAndRestart() {
|
|
2126
2441
|
if (updateRequestInProgress) return;
|
|
2127
2442
|
if (latestUpdateStatus?.canRunUpdate === false) {
|
|
2128
|
-
addEvent("Pi
|
|
2443
|
+
addEvent("Pi/Web UI package updates can only be started from localhost on the Web UI host", "warn");
|
|
2129
2444
|
renderUpdateNotification(latestUpdateStatus, { force: true });
|
|
2130
2445
|
return;
|
|
2131
2446
|
}
|
|
@@ -2134,11 +2449,11 @@ async function runPiUpdateAndRestart() {
|
|
|
2134
2449
|
updateRequestInProgress = true;
|
|
2135
2450
|
hideUpdateNotification();
|
|
2136
2451
|
setServerActionBusy("Updating…");
|
|
2137
|
-
setServerActionStatus("Running
|
|
2138
|
-
setServerRestartOverlay(true, "Running
|
|
2452
|
+
setServerActionStatus("Running Pi/Web UI package updates. The server will restart after the update completes…", "warn");
|
|
2453
|
+
setServerRestartOverlay(true, "Running Pi/Web UI package updates. The server will restart after the update completes…");
|
|
2139
2454
|
try {
|
|
2140
2455
|
await api("/api/update", { method: "POST", scoped: false });
|
|
2141
|
-
addEvent("Pi
|
|
2456
|
+
addEvent("Pi/Web UI package updates completed; Pi Web UI server restart requested", "warn");
|
|
2142
2457
|
} catch (error) {
|
|
2143
2458
|
if (!error?.backendOffline) {
|
|
2144
2459
|
updateRequestInProgress = false;
|
|
@@ -3034,7 +3349,7 @@ function renderOptionalFeatureDependentDisplays() {
|
|
|
3034
3349
|
streamBubble = null;
|
|
3035
3350
|
streamText = null;
|
|
3036
3351
|
streamBubbleVisibleSince = 0;
|
|
3037
|
-
renderAllMessages({ preserveScroll: true });
|
|
3352
|
+
renderAllMessages({ preserveScroll: true, forceRebuild: true });
|
|
3038
3353
|
if (streamRawText) renderStreamingAssistantText();
|
|
3039
3354
|
}
|
|
3040
3355
|
|
|
@@ -3617,7 +3932,7 @@ function restoreActiveDraft() {
|
|
|
3617
3932
|
|
|
3618
3933
|
function focusPromptInput({ defer = false } = {}) {
|
|
3619
3934
|
const focus = () => {
|
|
3620
|
-
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.gitChangesDialog?.open || elements.nativeCommandDialog.open || elements.appRunnerInfoDialog?.open || elements.promptListDialog?.open || elements.attachmentTextDialog?.open || elements.skillEditorDialog?.open || document.visibilityState === "hidden") return;
|
|
3935
|
+
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.gitChangesDialog?.open || elements.commandPaletteDialog?.open || elements.editRetryDialog?.open || elements.nativeCommandDialog.open || elements.appRunnerInfoDialog?.open || elements.promptListDialog?.open || elements.attachmentTextDialog?.open || elements.skillEditorDialog?.open || document.visibilityState === "hidden") return;
|
|
3621
3936
|
try {
|
|
3622
3937
|
elements.promptInput.focus({ preventScroll: true });
|
|
3623
3938
|
} catch {
|
|
@@ -3661,6 +3976,7 @@ function resetActiveTabUi() {
|
|
|
3661
3976
|
eventSource = null;
|
|
3662
3977
|
currentState = null;
|
|
3663
3978
|
latestStats = null;
|
|
3979
|
+
latestStatsOverlayPayload = null;
|
|
3664
3980
|
latestWorkspace = null;
|
|
3665
3981
|
latestMessages = [];
|
|
3666
3982
|
clearRunIndicatorActivity({ render: false });
|
|
@@ -3956,6 +4272,9 @@ function renderTabs() {
|
|
|
3956
4272
|
updateTerminalTabGroupOpenState();
|
|
3957
4273
|
setMobileTabsExpanded(mobileTabsExpanded);
|
|
3958
4274
|
updateDocumentTitle();
|
|
4275
|
+
renderWorkspaceDashboard();
|
|
4276
|
+
renderContextMeter();
|
|
4277
|
+
if (elements.commandPaletteDialog?.open) renderCommandPalette();
|
|
3959
4278
|
syncTabPolling();
|
|
3960
4279
|
}
|
|
3961
4280
|
|
|
@@ -5239,7 +5558,10 @@ function renderGitFooterPayloadMeta(chip, tab) {
|
|
|
5239
5558
|
if (chip.key === "cwd" && tab) {
|
|
5240
5559
|
options.onClick = changeActiveTabCwd;
|
|
5241
5560
|
action = `Click to change the working directory for ${tab.title}.`;
|
|
5242
|
-
} else if (chip.key === "git" && chip.value
|
|
5561
|
+
} else if (chip.key === "git" && cleanFooterPayloadText(chip.value, "").toLowerCase() === "no repo") {
|
|
5562
|
+
options.onClick = () => startGitInitWorkflow();
|
|
5563
|
+
action = "No Git repository detected. Click to initialize a repo, create README.md, add origin, and push main.";
|
|
5564
|
+
} else if (chip.key === "git") {
|
|
5243
5565
|
options.onClick = () => setFooterBranchPickerOpen(!footerBranchPickerOpen);
|
|
5244
5566
|
action = "Click to switch to another local branch.";
|
|
5245
5567
|
} else if (chip.key === "changes") {
|
|
@@ -5257,7 +5579,7 @@ function renderGitFooterPayloadMeta(chip, tab) {
|
|
|
5257
5579
|
options.tooltipAlign = gitFooterTooltipAlign(chip);
|
|
5258
5580
|
const node = footerMeta(chip.label, chip.value, footerMetaClassForPayload(chip), options);
|
|
5259
5581
|
applyFooterChangedFilesDropdown(node, chip);
|
|
5260
|
-
if (chip.key === "git" && options.onClick) {
|
|
5582
|
+
if (chip.key === "git" && options.onClick && cleanFooterPayloadText(chip.value, "").toLowerCase() !== "no repo") {
|
|
5261
5583
|
node.setAttribute("aria-haspopup", "listbox");
|
|
5262
5584
|
node.setAttribute("aria-expanded", footerBranchPickerOpen ? "true" : "false");
|
|
5263
5585
|
}
|
|
@@ -5770,6 +6092,194 @@ function renderMinimalFooter() {
|
|
|
5770
6092
|
updateFooterModelPickerPosition();
|
|
5771
6093
|
}
|
|
5772
6094
|
|
|
6095
|
+
function contextUsageSnapshot() {
|
|
6096
|
+
const usage = latestStats?.contextUsage || currentState?.contextUsage || null;
|
|
6097
|
+
const contextWindow = contextWindowFromSources(usage, currentState?.model?.contextWindow);
|
|
6098
|
+
if (!contextWindow) return null;
|
|
6099
|
+
const rawPercent = Number(usage?.percent);
|
|
6100
|
+
const unknown = contextUsageUnknownAfterCompaction() || !Number.isFinite(rawPercent);
|
|
6101
|
+
const rawTokens = Number(usage?.tokens);
|
|
6102
|
+
return {
|
|
6103
|
+
tokens: Number.isFinite(rawTokens) && rawTokens >= 0 ? rawTokens : null,
|
|
6104
|
+
contextWindow,
|
|
6105
|
+
percent: unknown ? null : Math.max(0, Math.min(100, rawPercent)),
|
|
6106
|
+
unknown,
|
|
6107
|
+
autoCompactionEnabled: footerAutoCompactionEnabled(),
|
|
6108
|
+
};
|
|
6109
|
+
}
|
|
6110
|
+
|
|
6111
|
+
function contextUsageDisplay(snapshot = contextUsageSnapshot()) {
|
|
6112
|
+
if (!snapshot) return "Context unknown";
|
|
6113
|
+
const windowText = formatFooterTokenCount(snapshot.contextWindow);
|
|
6114
|
+
if (typeof snapshot.percent === "number") return `${snapshot.percent.toFixed(1)}% of ${windowText}`;
|
|
6115
|
+
return `?/${windowText}`;
|
|
6116
|
+
}
|
|
6117
|
+
|
|
6118
|
+
function contextUsageDetail(snapshot = contextUsageSnapshot()) {
|
|
6119
|
+
if (!snapshot) return "Waiting for model context-window data.";
|
|
6120
|
+
const tokenText = snapshot.tokens === null ? "tokens unknown" : `${formatFooterTokenCount(snapshot.tokens)} tokens`;
|
|
6121
|
+
const autoText = snapshot.autoCompactionEnabled ? "auto-compaction on" : "auto-compaction off";
|
|
6122
|
+
return `${tokenText} · ${formatFooterTokenCount(snapshot.contextWindow)} window · ${autoText}`;
|
|
6123
|
+
}
|
|
6124
|
+
|
|
6125
|
+
function appendContextMeterFill(meter, snapshot) {
|
|
6126
|
+
const fill = make("span", "context-meter-fill");
|
|
6127
|
+
const percent = typeof snapshot?.percent === "number" ? snapshot.percent : 0;
|
|
6128
|
+
fill.style.width = `${Math.max(0, Math.min(100, percent)).toFixed(1)}%`;
|
|
6129
|
+
if (typeof snapshot?.percent === "number") {
|
|
6130
|
+
const activeColor = contextUsageActiveColor(snapshot.percent);
|
|
6131
|
+
fill.style.setProperty("--context-active-color", activeColor.color);
|
|
6132
|
+
fill.style.setProperty("--context-active-glow", activeColor.glow);
|
|
6133
|
+
}
|
|
6134
|
+
meter.append(fill);
|
|
6135
|
+
}
|
|
6136
|
+
|
|
6137
|
+
async function requestManualCompaction({ triggerButton = null } = {}) {
|
|
6138
|
+
const tabContext = activeTabContext();
|
|
6139
|
+
if (!tabContext.tabId) return;
|
|
6140
|
+
const buttons = [...new Set([elements.compactButton, triggerButton].filter(Boolean))];
|
|
6141
|
+
try {
|
|
6142
|
+
for (const button of buttons) {
|
|
6143
|
+
button.disabled = true;
|
|
6144
|
+
button.textContent = "Compacting…";
|
|
6145
|
+
}
|
|
6146
|
+
setRunIndicatorActivity("Requesting context compaction…");
|
|
6147
|
+
scrollChatToBottom({ force: true });
|
|
6148
|
+
markContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
6149
|
+
renderFooter();
|
|
6150
|
+
renderContextMeter();
|
|
6151
|
+
renderWorkspaceDashboard();
|
|
6152
|
+
addEvent("manual compaction requested");
|
|
6153
|
+
await api("/api/compact", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
6154
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
6155
|
+
scheduleRefreshState(120, tabContext);
|
|
6156
|
+
scheduleRefreshMessages(600, tabContext);
|
|
6157
|
+
scheduleRefreshFooter(600, tabContext);
|
|
6158
|
+
} catch (error) {
|
|
6159
|
+
if (isCurrentTabContext(tabContext)) {
|
|
6160
|
+
clearContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
6161
|
+
clearRunIndicatorActivity();
|
|
6162
|
+
renderFooter();
|
|
6163
|
+
renderContextMeter();
|
|
6164
|
+
renderWorkspaceDashboard();
|
|
6165
|
+
addEvent(error.message, "error");
|
|
6166
|
+
}
|
|
6167
|
+
} finally {
|
|
6168
|
+
if (isCurrentTabContext(tabContext)) {
|
|
6169
|
+
for (const button of buttons) {
|
|
6170
|
+
button.disabled = !!currentState?.isCompacting;
|
|
6171
|
+
button.textContent = button === elements.compactButton && currentState?.isCompacting ? "Compacting…" : button === elements.compactButton ? "Compact" : "Compact now";
|
|
6172
|
+
}
|
|
6173
|
+
renderContextMeter();
|
|
6174
|
+
renderWorkspaceDashboard();
|
|
6175
|
+
}
|
|
6176
|
+
}
|
|
6177
|
+
}
|
|
6178
|
+
|
|
6179
|
+
function renderContextMeter() {
|
|
6180
|
+
const root = elements.contextMeterBar;
|
|
6181
|
+
if (!root) return;
|
|
6182
|
+
const tab = activeTab();
|
|
6183
|
+
if (!tab) {
|
|
6184
|
+
root.hidden = true;
|
|
6185
|
+
root.replaceChildren();
|
|
6186
|
+
return;
|
|
6187
|
+
}
|
|
6188
|
+
root.hidden = false;
|
|
6189
|
+
const snapshot = contextUsageSnapshot();
|
|
6190
|
+
if (!snapshot || typeof snapshot.percent !== "number" || snapshot.percent <= 50) {
|
|
6191
|
+
root.hidden = true;
|
|
6192
|
+
root.replaceChildren();
|
|
6193
|
+
return;
|
|
6194
|
+
}
|
|
6195
|
+
const meter = make("div", `context-meter${snapshot?.unknown ? " unknown" : ""}`);
|
|
6196
|
+
appendContextMeterFill(meter, snapshot);
|
|
6197
|
+
|
|
6198
|
+
const summary = make("div", "context-meter-summary");
|
|
6199
|
+
summary.append(
|
|
6200
|
+
make("strong", undefined, contextUsageDisplay(snapshot)),
|
|
6201
|
+
make("span", "muted", contextUsageDetail(snapshot)),
|
|
6202
|
+
);
|
|
6203
|
+
|
|
6204
|
+
const actions = make("div", "context-meter-actions");
|
|
6205
|
+
const compact = make("button", "context-meter-compact", currentState?.isCompacting ? "Compacting…" : "Compact now");
|
|
6206
|
+
compact.type = "button";
|
|
6207
|
+
compact.disabled = !!currentState?.isCompacting;
|
|
6208
|
+
compact.title = "Manually compact this tab's conversation context";
|
|
6209
|
+
compact.addEventListener("click", () => requestManualCompaction({ triggerButton: compact }));
|
|
6210
|
+
const auto = make("button", "context-meter-auto", footerAutoCompactionEnabled() ? "Auto on" : "Auto off");
|
|
6211
|
+
auto.type = "button";
|
|
6212
|
+
auto.setAttribute("aria-pressed", footerAutoCompactionEnabled() ? "true" : "false");
|
|
6213
|
+
auto.disabled = footerAutoCompactionToggleInFlight;
|
|
6214
|
+
auto.title = footerAutoCompactionToggleInFlight ? "Updating auto-compaction…" : footerAutoCompactionToggleAction();
|
|
6215
|
+
auto.addEventListener("click", () => toggleFooterAutoCompaction());
|
|
6216
|
+
actions.append(compact, auto);
|
|
6217
|
+
|
|
6218
|
+
root.replaceChildren(summary, meter, actions);
|
|
6219
|
+
}
|
|
6220
|
+
|
|
6221
|
+
function dashboardMetric(label, value, detail = "") {
|
|
6222
|
+
const item = make("div", "workspace-dashboard-metric");
|
|
6223
|
+
item.append(make("span", "workspace-dashboard-metric-label", label), make("strong", undefined, value || "—"));
|
|
6224
|
+
if (detail) item.append(make("span", "workspace-dashboard-metric-detail", detail));
|
|
6225
|
+
return item;
|
|
6226
|
+
}
|
|
6227
|
+
|
|
6228
|
+
function dashboardAction(label, handler, className = "") {
|
|
6229
|
+
const button = make("button", `workspace-dashboard-action ${className}`.trim(), label);
|
|
6230
|
+
button.type = "button";
|
|
6231
|
+
button.addEventListener("click", handler);
|
|
6232
|
+
return button;
|
|
6233
|
+
}
|
|
6234
|
+
|
|
6235
|
+
function renderWorkspaceDashboard() {
|
|
6236
|
+
const root = elements.workspaceDashboard;
|
|
6237
|
+
if (!root) return;
|
|
6238
|
+
const tab = activeTab();
|
|
6239
|
+
const snapshot = contextUsageSnapshot();
|
|
6240
|
+
const workspaceLabel = latestWorkspace?.displayCwd || (tab?.cwd ? normalizeDisplayPath(tab.cwd) : "Choose or create a tab to start");
|
|
6241
|
+
const queueCount = Number(currentState?.pendingMessageCount || 0) || 0;
|
|
6242
|
+
root.replaceChildren();
|
|
6243
|
+
|
|
6244
|
+
const header = make("div", "workspace-dashboard-header");
|
|
6245
|
+
const title = make("div", "workspace-dashboard-title");
|
|
6246
|
+
title.append(make("span", "workspace-dashboard-kicker", "Workspace"), make("h2", undefined, tab?.title || "Pi Web UI"), make("p", "muted", workspaceLabel));
|
|
6247
|
+
const actions = make("div", "workspace-dashboard-actions");
|
|
6248
|
+
actions.append(
|
|
6249
|
+
dashboardAction("Command palette", () => openCommandPalette(), "primary"),
|
|
6250
|
+
dashboardAction("New tab", () => createTerminalTab()),
|
|
6251
|
+
dashboardAction("Resume", () => runNativeCommandMenu("/resume")),
|
|
6252
|
+
dashboardAction("Model", () => runNativeCommandMenu("/model")),
|
|
6253
|
+
dashboardAction("Settings", () => runNativeCommandMenu("/settings")),
|
|
6254
|
+
);
|
|
6255
|
+
header.append(title, actions);
|
|
6256
|
+
|
|
6257
|
+
const metrics = make("div", "workspace-dashboard-metrics");
|
|
6258
|
+
metrics.append(
|
|
6259
|
+
dashboardMetric("Model", currentState?.model ? shortModelLabel(currentState.model) : "loading…", currentState?.thinkingLevel ? `thinking ${currentState.thinkingLevel}` : ""),
|
|
6260
|
+
dashboardMetric("Context", contextUsageDisplay(snapshot), contextUsageDetail(snapshot)),
|
|
6261
|
+
dashboardMetric("Session", currentState?.sessionName || currentState?.sessionId || "loading…", currentState?.sessionFile || "in-memory"),
|
|
6262
|
+
dashboardMetric("Queue", `${queueCount}`, queueCount === 1 ? "pending message" : "pending messages"),
|
|
6263
|
+
);
|
|
6264
|
+
|
|
6265
|
+
const tabsPanel = make("div", "workspace-dashboard-tabs");
|
|
6266
|
+
tabsPanel.append(make("span", "workspace-dashboard-tabs-title", `Open tabs (${tabs.length})`));
|
|
6267
|
+
const tabList = make("div", "workspace-dashboard-tab-list");
|
|
6268
|
+
for (const item of tabs.slice(0, 8)) {
|
|
6269
|
+
const indicator = tabIndicator(item);
|
|
6270
|
+
const button = make("button", `workspace-dashboard-tab activity-${indicator.state}${item.id === activeTabId ? " active" : ""}`);
|
|
6271
|
+
button.type = "button";
|
|
6272
|
+
button.title = `${item.title} · ${indicator.label}`;
|
|
6273
|
+
button.append(make("span", "workspace-dashboard-tab-dot", indicator.glyph), make("span", undefined, item.title));
|
|
6274
|
+
button.addEventListener("click", () => switchTab(item.id));
|
|
6275
|
+
tabList.append(button);
|
|
6276
|
+
}
|
|
6277
|
+
if (tabs.length > 8) tabList.append(make("span", "workspace-dashboard-tab-more", `+${tabs.length - 8} more`));
|
|
6278
|
+
tabsPanel.append(tabList);
|
|
6279
|
+
|
|
6280
|
+
root.append(header, metrics, tabsPanel);
|
|
6281
|
+
}
|
|
6282
|
+
|
|
5773
6283
|
function setFooterModelPickerOpen(open) {
|
|
5774
6284
|
footerModelPickerOpen = !!open;
|
|
5775
6285
|
if (footerModelPickerOpen) {
|
|
@@ -6783,6 +7293,8 @@ function renderStatus() {
|
|
|
6783
7293
|
elements.compactButton.textContent = state?.isCompacting ? "Compacting…" : "Compact";
|
|
6784
7294
|
syncModelSelectToState();
|
|
6785
7295
|
renderFooter();
|
|
7296
|
+
renderContextMeter();
|
|
7297
|
+
renderWorkspaceDashboard();
|
|
6786
7298
|
renderFeedbackTray();
|
|
6787
7299
|
}
|
|
6788
7300
|
|
|
@@ -7726,130 +8238,851 @@ function renderAppRunnerWidget() {
|
|
|
7726
8238
|
return node;
|
|
7727
8239
|
}
|
|
7728
8240
|
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
if (releaseAurLog) elements.widgetArea.append(releaseAurLog);
|
|
7739
|
-
const appRunnerWidget = renderAppRunnerWidget();
|
|
7740
|
-
if (appRunnerWidget) elements.widgetArea.append(appRunnerWidget);
|
|
7741
|
-
|
|
7742
|
-
for (const [key, value] of widgets) {
|
|
7743
|
-
const widgetFeatureId = optionalFeatureWidgetFeatureId(key);
|
|
7744
|
-
if (widgetFeatureId && !isOptionalFeatureEnabled(widgetFeatureId)) continue;
|
|
7745
|
-
if (widgetFeatureId && key !== "todo-progress") continue;
|
|
7746
|
-
const lines = Array.isArray(value.widgetLines) ? value.widgetLines : [];
|
|
7747
|
-
const specialized = key === "todo-progress" && isOptionalFeatureEnabled("todoProgressWidget") ? renderTodoProgressWidget(key, lines) : null;
|
|
7748
|
-
if (specialized) {
|
|
7749
|
-
elements.widgetArea.append(specialized);
|
|
7750
|
-
continue;
|
|
7751
|
-
}
|
|
8241
|
+
const STATS_OVERLAY_TABS = [
|
|
8242
|
+
{ id: "overview", label: "Overview" },
|
|
8243
|
+
{ id: "daily", label: "Daily" },
|
|
8244
|
+
{ id: "models", label: "Models" },
|
|
8245
|
+
{ id: "sessions", label: "Sessions" },
|
|
8246
|
+
{ id: "cost-cache", label: "Cost & cache" },
|
|
8247
|
+
{ id: "prompt", label: "Prompt/context" },
|
|
8248
|
+
{ id: "raw", label: "Command outputs" },
|
|
8249
|
+
];
|
|
7752
8250
|
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
elements.widgetArea.append(node);
|
|
7757
|
-
}
|
|
8251
|
+
function statsNumber(value, fallback = 0) {
|
|
8252
|
+
const number = Number(value);
|
|
8253
|
+
return Number.isFinite(number) ? number : fallback;
|
|
7758
8254
|
}
|
|
7759
8255
|
|
|
7760
|
-
function
|
|
7761
|
-
const
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
if (
|
|
7766
|
-
if (
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
}
|
|
7770
|
-
return workflow;
|
|
8256
|
+
function formatStatsTokens(value) {
|
|
8257
|
+
const number = statsNumber(value);
|
|
8258
|
+
const abs = Math.abs(number);
|
|
8259
|
+
const sign = number < 0 ? "-" : "";
|
|
8260
|
+
if (abs >= 1_000_000_000) return `${sign}${(abs / 1_000_000_000).toFixed(2)}B`;
|
|
8261
|
+
if (abs >= 1_000_000) return `${sign}${(abs / 1_000_000).toFixed(2)}M`;
|
|
8262
|
+
if (abs >= 10_000) return `${sign}${Math.round(abs / 1000)}k`;
|
|
8263
|
+
if (abs >= 1000) return `${sign}${(abs / 1000).toFixed(1)}k`;
|
|
8264
|
+
return `${number.toLocaleString()}`;
|
|
7771
8265
|
}
|
|
7772
8266
|
|
|
7773
|
-
function
|
|
7774
|
-
const
|
|
7775
|
-
|
|
8267
|
+
function formatStatsCost(value) {
|
|
8268
|
+
const cost = statsNumber(value);
|
|
8269
|
+
if (cost <= 0) return "$0.000";
|
|
8270
|
+
if (cost < 0.01) return `$${cost.toFixed(4)}`;
|
|
8271
|
+
if (cost < 10) return `$${cost.toFixed(3)}`;
|
|
8272
|
+
return `$${cost.toFixed(2)}`;
|
|
7776
8273
|
}
|
|
7777
8274
|
|
|
7778
|
-
function
|
|
7779
|
-
|
|
7780
|
-
if (!workflow) return;
|
|
7781
|
-
const next = `${workflow.output || ""}${workflow.output ? "\n" : ""}${text}`;
|
|
7782
|
-
setGitWorkflow({ output: next.slice(-60000) }, { tabId });
|
|
8275
|
+
function formatStatsPercent(value) {
|
|
8276
|
+
return `${statsNumber(value).toFixed(1)}%`;
|
|
7783
8277
|
}
|
|
7784
8278
|
|
|
7785
|
-
function
|
|
7786
|
-
if (!
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
8279
|
+
function parseStatsWebuiPayloadRaw(raw) {
|
|
8280
|
+
if (!raw) return null;
|
|
8281
|
+
try {
|
|
8282
|
+
const parsed = JSON.parse(raw);
|
|
8283
|
+
if (!parsed || parsed.type !== STATS_WEBUI_PAYLOAD_TYPE || parsed.version !== STATS_WEBUI_PAYLOAD_VERSION) return null;
|
|
8284
|
+
return parsed;
|
|
8285
|
+
} catch {
|
|
8286
|
+
return null;
|
|
7792
8287
|
}
|
|
7793
|
-
return lines.join("\n");
|
|
7794
8288
|
}
|
|
7795
8289
|
|
|
7796
|
-
function
|
|
7797
|
-
if (
|
|
7798
|
-
return
|
|
8290
|
+
function currentStatsOverlayPayload() {
|
|
8291
|
+
if (isOptionalFeatureDisabled("statsCommand")) return null;
|
|
8292
|
+
return parseStatsWebuiPayloadRaw(statusEntries.get(STATS_WEBUI_STATUS_KEY)) || latestStatsOverlayPayload;
|
|
7799
8293
|
}
|
|
7800
8294
|
|
|
7801
|
-
function
|
|
7802
|
-
return
|
|
8295
|
+
function statsScopeDaysFromPayload(payload) {
|
|
8296
|
+
return payload?.scope?.mode === "range" && statsNumber(payload.scope.days) > 0 ? String(payload.scope.days) : "";
|
|
7803
8297
|
}
|
|
7804
8298
|
|
|
7805
|
-
function
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
7811
|
-
.replace(/^-+|-+$/g, "")
|
|
7812
|
-
.slice(0, 48);
|
|
8299
|
+
function statsScopeValueFromPayload(payload) {
|
|
8300
|
+
if (payload?.scope?.mode === "all") return "all";
|
|
8301
|
+
const days = statsScopeDaysFromPayload(payload);
|
|
8302
|
+
if (!days) return statsOverlayLastScope;
|
|
8303
|
+
return ["14", "30", "90"].includes(days) ? days : "custom";
|
|
7813
8304
|
}
|
|
7814
8305
|
|
|
7815
|
-
function
|
|
7816
|
-
const
|
|
7817
|
-
|
|
7818
|
-
const
|
|
7819
|
-
|
|
7820
|
-
return
|
|
8306
|
+
function statsCustomDaysValue() {
|
|
8307
|
+
const fromInput = Number.parseInt(elements.statsOverlayCustomDays?.value || "", 10);
|
|
8308
|
+
if (Number.isFinite(fromInput) && fromInput > 0) return Math.max(1, Math.min(3650, fromInput));
|
|
8309
|
+
const fromPayload = Number.parseInt(statsScopeDaysFromPayload(currentStatsOverlayPayload()), 10);
|
|
8310
|
+
if (Number.isFinite(fromPayload) && fromPayload > 0) return Math.max(1, Math.min(3650, fromPayload));
|
|
8311
|
+
return 14;
|
|
7821
8312
|
}
|
|
7822
8313
|
|
|
7823
|
-
function
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
return
|
|
8314
|
+
function statsScopeCommandArg() {
|
|
8315
|
+
const value = elements.statsOverlayScope?.value || statsOverlayLastScope || "14";
|
|
8316
|
+
if (value === "all") return "all";
|
|
8317
|
+
const days = value === "custom" ? statsCustomDaysValue() : Math.max(1, Math.min(3650, Number.parseInt(value, 10) || 14));
|
|
8318
|
+
return String(days);
|
|
7828
8319
|
}
|
|
7829
8320
|
|
|
7830
|
-
function
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
8321
|
+
function syncStatsScopeControls(payload = currentStatsOverlayPayload()) {
|
|
8322
|
+
if (!elements.statsOverlayScope) return;
|
|
8323
|
+
const nextValue = payload ? statsScopeValueFromPayload(payload) : statsOverlayLastScope;
|
|
8324
|
+
elements.statsOverlayScope.value = ["14", "30", "90", "all", "custom"].includes(nextValue) ? nextValue : "custom";
|
|
8325
|
+
const custom = elements.statsOverlayScope.value === "custom";
|
|
8326
|
+
if (elements.statsOverlayCustomDays) {
|
|
8327
|
+
const payloadDays = statsScopeDaysFromPayload(payload);
|
|
8328
|
+
if (payloadDays && !["14", "30", "90"].includes(payloadDays)) elements.statsOverlayCustomDays.value = payloadDays;
|
|
8329
|
+
else if (!elements.statsOverlayCustomDays.value) elements.statsOverlayCustomDays.value = "14";
|
|
8330
|
+
elements.statsOverlayCustomDays.hidden = !custom;
|
|
7838
8331
|
}
|
|
7839
|
-
button.addEventListener("click", handler);
|
|
7840
|
-
elements.gitWorkflowActions.append(button);
|
|
7841
|
-
return button;
|
|
7842
8332
|
}
|
|
7843
8333
|
|
|
7844
|
-
function
|
|
7845
|
-
if (
|
|
7846
|
-
|
|
7847
|
-
|
|
8334
|
+
function statsPromptEstimateSourceLabel(estimate = {}) {
|
|
8335
|
+
if (estimate.source === "export-html") return "export-backed";
|
|
8336
|
+
if (estimate.source === "fallback") return "live fallback";
|
|
8337
|
+
return estimate.source || "estimate";
|
|
7848
8338
|
}
|
|
7849
8339
|
|
|
7850
|
-
function
|
|
7851
|
-
const
|
|
7852
|
-
|
|
8340
|
+
function statsMetricCard(label, value, detail = "", tone = "") {
|
|
8341
|
+
const node = make("div", `stats-overlay-card ${tone}`.trim());
|
|
8342
|
+
node.append(make("span", "stats-overlay-card-label", label), make("strong", undefined, value));
|
|
8343
|
+
if (detail) node.append(make("span", "stats-overlay-card-detail", detail));
|
|
8344
|
+
return node;
|
|
8345
|
+
}
|
|
8346
|
+
|
|
8347
|
+
function statsLineBlock(lines = []) {
|
|
8348
|
+
const pre = make("pre", "stats-overlay-lines");
|
|
8349
|
+
pre.textContent = (Array.isArray(lines) ? lines : []).map(stripAnsi).join("\n") || "No data.";
|
|
8350
|
+
return pre;
|
|
8351
|
+
}
|
|
8352
|
+
|
|
8353
|
+
function renderStatsTable(headers, rows, emptyText = "No data.") {
|
|
8354
|
+
if (!rows.length) return make("p", "stats-overlay-empty muted", emptyText);
|
|
8355
|
+
const wrapper = make("div", "stats-overlay-table-wrap");
|
|
8356
|
+
const table = make("table", "stats-overlay-table");
|
|
8357
|
+
const thead = make("thead");
|
|
8358
|
+
const headRow = make("tr");
|
|
8359
|
+
for (const header of headers) headRow.append(make("th", undefined, header));
|
|
8360
|
+
thead.append(headRow);
|
|
8361
|
+
const tbody = make("tbody");
|
|
8362
|
+
for (const row of rows) {
|
|
8363
|
+
const tr = make("tr");
|
|
8364
|
+
for (const cell of row) tr.append(make("td", undefined, cell));
|
|
8365
|
+
tbody.append(tr);
|
|
8366
|
+
}
|
|
8367
|
+
table.append(thead, tbody);
|
|
8368
|
+
wrapper.append(table);
|
|
8369
|
+
return wrapper;
|
|
8370
|
+
}
|
|
8371
|
+
|
|
8372
|
+
function renderStatsBarRows(daily = []) {
|
|
8373
|
+
const rows = daily.filter((row) => statsNumber(row.total) > 0 || statsNumber(row.cost) > 0);
|
|
8374
|
+
if (!rows.length) return make("p", "stats-overlay-empty muted", "No non-zero usage in this range.");
|
|
8375
|
+
const maxTokens = Math.max(1, ...rows.map((row) => statsNumber(row.total)));
|
|
8376
|
+
const list = make("div", "stats-overlay-bars");
|
|
8377
|
+
for (const row of rows) {
|
|
8378
|
+
const tokenRatio = Math.max(0.015, statsNumber(row.total) / maxTokens);
|
|
8379
|
+
const item = make("div", "stats-overlay-bar-row");
|
|
8380
|
+
const bar = make("span", "stats-overlay-bar");
|
|
8381
|
+
const fill = make("span", "stats-overlay-bar-fill");
|
|
8382
|
+
fill.style.width = `${Math.min(100, tokenRatio * 100)}%`;
|
|
8383
|
+
bar.append(fill);
|
|
8384
|
+
item.append(
|
|
8385
|
+
make("span", "stats-overlay-bar-day", row.day || "—"),
|
|
8386
|
+
bar,
|
|
8387
|
+
make("span", "stats-overlay-bar-value", `${formatStatsTokens(row.total)} tok`),
|
|
8388
|
+
make("span", "stats-overlay-bar-cost", formatStatsCost(row.cost)),
|
|
8389
|
+
);
|
|
8390
|
+
list.append(item);
|
|
8391
|
+
}
|
|
8392
|
+
return list;
|
|
8393
|
+
}
|
|
8394
|
+
|
|
8395
|
+
function renderStatsOverview(payload) {
|
|
8396
|
+
const node = make("div", "stats-overlay-pane stats-overlay-overview");
|
|
8397
|
+
const totals = payload?.totals || {};
|
|
8398
|
+
const summary = payload?.summary || {};
|
|
8399
|
+
const highest = summary.highestDay;
|
|
8400
|
+
const cards = make("div", "stats-overlay-cards");
|
|
8401
|
+
cards.append(
|
|
8402
|
+
statsMetricCard("Total tokens", formatStatsTokens(totals.total), `↑${formatStatsTokens(totals.input)} ↓${formatStatsTokens(totals.output)}`, "tone-blue"),
|
|
8403
|
+
statsMetricCard("Cost", formatStatsCost(totals.cost), `projected 30d ${formatStatsCost(summary.projected30DayCost)}`, "tone-green"),
|
|
8404
|
+
statsMetricCard("Messages", String(statsNumber(totals.messages)), `${payload?.sessionCount ?? 0} sessions`, "tone-mauve"),
|
|
8405
|
+
statsMetricCard("PI initial prompt", `~${formatStatsTokens(payload?.promptEstimate?.total)} tok`, `${statsPromptEstimateSourceLabel(payload?.promptEstimate)} · ${payload?.promptEstimate?.confidence || "estimate"}`, "tone-yellow"),
|
|
8406
|
+
statsMetricCard("Cache hit", formatStatsPercent(summary.cacheHitRate), `reads ${formatStatsTokens(totals.cacheRead)} · writes ${formatStatsTokens(totals.cacheWrite)}`, "tone-teal"),
|
|
8407
|
+
statsMetricCard("Active days", `${payload?.activeDayCount ?? 0}/${payload?.dayCount ?? 0}`, highest ? `peak ${highest.day} · ${formatStatsCost(highest.cost)}` : "no peak yet", "tone-pink"),
|
|
8408
|
+
);
|
|
8409
|
+
node.append(cards, make("h3", undefined, "Daily usage"), renderStatsBarRows(payload?.daily || []));
|
|
8410
|
+
return node;
|
|
8411
|
+
}
|
|
8412
|
+
|
|
8413
|
+
function renderStatsDaily(payload) {
|
|
8414
|
+
const node = make("div", "stats-overlay-pane");
|
|
8415
|
+
node.append(make("h3", undefined, "Daily token and cost trend"), renderStatsBarRows(payload?.daily || []));
|
|
8416
|
+
node.append(renderStatsTable(
|
|
8417
|
+
["Day", "Tokens", "Cost", "Input", "Output", "Cache R/W", "Msgs"],
|
|
8418
|
+
(payload?.daily || []).map((row) => [
|
|
8419
|
+
row.day || "—",
|
|
8420
|
+
formatStatsTokens(row.total),
|
|
8421
|
+
formatStatsCost(row.cost),
|
|
8422
|
+
formatStatsTokens(row.input),
|
|
8423
|
+
formatStatsTokens(row.output),
|
|
8424
|
+
`${formatStatsTokens(row.cacheRead)} / ${formatStatsTokens(row.cacheWrite)}`,
|
|
8425
|
+
String(statsNumber(row.messages)),
|
|
8426
|
+
]),
|
|
8427
|
+
));
|
|
8428
|
+
return node;
|
|
8429
|
+
}
|
|
8430
|
+
|
|
8431
|
+
function renderStatsModels(payload) {
|
|
8432
|
+
return renderStatsTable(
|
|
8433
|
+
["Model", "Tokens", "Token %", "Cost", "Spend %", "$/1M", "Avg out", "Msgs"],
|
|
8434
|
+
(payload?.models || []).map((model) => [
|
|
8435
|
+
model.model || "unknown",
|
|
8436
|
+
formatStatsTokens(model.tokens),
|
|
8437
|
+
formatStatsPercent(model.percent),
|
|
8438
|
+
formatStatsCost(model.cost),
|
|
8439
|
+
formatStatsPercent(model.costPercent),
|
|
8440
|
+
formatStatsCost(model.avgCostPerMillion),
|
|
8441
|
+
formatStatsTokens(Math.round(statsNumber(model.avgOutputTokens))),
|
|
8442
|
+
String(statsNumber(model.messages)),
|
|
8443
|
+
]),
|
|
8444
|
+
"No model usage in this range.",
|
|
8445
|
+
);
|
|
8446
|
+
}
|
|
8447
|
+
|
|
8448
|
+
function renderStatsSessions(payload) {
|
|
8449
|
+
return renderStatsTable(
|
|
8450
|
+
["Day", "Session", "Cost", "Tokens", "Model"],
|
|
8451
|
+
(payload?.expensiveSessions || []).map((session) => [
|
|
8452
|
+
session.day || "—",
|
|
8453
|
+
session.displayName || session.sessionId || "unknown",
|
|
8454
|
+
formatStatsCost(session.cost),
|
|
8455
|
+
formatStatsTokens(session.tokens),
|
|
8456
|
+
session.model || "unknown",
|
|
8457
|
+
]),
|
|
8458
|
+
"No session usage in this range.",
|
|
8459
|
+
);
|
|
8460
|
+
}
|
|
8461
|
+
|
|
8462
|
+
function renderStatsCostCache(payload) {
|
|
8463
|
+
const node = make("div", "stats-overlay-pane");
|
|
8464
|
+
const totals = payload?.totals || {};
|
|
8465
|
+
const summary = payload?.summary || {};
|
|
8466
|
+
const cards = make("div", "stats-overlay-cards compact");
|
|
8467
|
+
cards.append(
|
|
8468
|
+
statsMetricCard("Avg/day", formatStatsCost(summary.calendarAvgCost), "calendar average", "tone-green"),
|
|
8469
|
+
statsMetricCard("Active avg", formatStatsCost(summary.activeAvgCost), "per active day", "tone-teal"),
|
|
8470
|
+
statsMetricCard("Non-cache", formatStatsTokens(summary.nonCacheTokens), `${formatStatsTokens(totals.total)} total`, "tone-blue"),
|
|
8471
|
+
statsMetricCard("Cache hit", formatStatsPercent(summary.cacheHitRate), `${formatStatsTokens(totals.cacheRead)} read tokens`, "tone-yellow"),
|
|
8472
|
+
);
|
|
8473
|
+
node.append(cards, make("h3", undefined, "Cost trend"), statsLineBlock(payload?.lines?.costTrend), make("h3", undefined, "Cache efficiency"), statsLineBlock(payload?.lines?.cache));
|
|
8474
|
+
return node;
|
|
8475
|
+
}
|
|
8476
|
+
|
|
8477
|
+
function statsCalibrationButton(label, mode, className = "") {
|
|
8478
|
+
const button = make("button", className, statsOverlayCalibrationBusy === mode ? "Running…" : label);
|
|
8479
|
+
button.type = "button";
|
|
8480
|
+
button.disabled = statsOverlayLoading || !!statsOverlayCalibrationBusy;
|
|
8481
|
+
button.addEventListener("click", () => runStatsCalibration(mode));
|
|
8482
|
+
return button;
|
|
8483
|
+
}
|
|
8484
|
+
|
|
8485
|
+
function renderStatsCalibrationPanel(payload) {
|
|
8486
|
+
const estimate = payload?.promptEstimate || {};
|
|
8487
|
+
const panel = make("section", "stats-overlay-calibration-panel");
|
|
8488
|
+
const text = make("div", "stats-overlay-calibration-copy");
|
|
8489
|
+
text.append(
|
|
8490
|
+
make("strong", undefined, "Calibration"),
|
|
8491
|
+
make("span", undefined, `${statsNumber(estimate.calibrationSamples)} sample${statsNumber(estimate.calibrationSamples) === 1 ? "" : "s"} · scale ×${statsNumber(estimate.calibrationMultiplier, 1).toFixed(2)} · ${statsPromptEstimateSourceLabel(estimate)}`),
|
|
8492
|
+
);
|
|
8493
|
+
if (estimate.warning) text.append(make("span", "warning", estimate.warning));
|
|
8494
|
+
if (statsOverlayCalibrationMessage) text.append(make("span", "muted", statsOverlayCalibrationMessage));
|
|
8495
|
+
const actions = make("div", "stats-overlay-calibration-actions");
|
|
8496
|
+
actions.append(
|
|
8497
|
+
statsCalibrationButton("Calibrate current", "current"),
|
|
8498
|
+
statsCalibrationButton("Start probe", "probe", "primary"),
|
|
8499
|
+
);
|
|
8500
|
+
panel.append(text, actions);
|
|
8501
|
+
return panel;
|
|
8502
|
+
}
|
|
8503
|
+
|
|
8504
|
+
function renderStatsPrompt(payload) {
|
|
8505
|
+
const node = make("div", "stats-overlay-pane");
|
|
8506
|
+
const cards = make("div", "stats-overlay-cards compact");
|
|
8507
|
+
cards.append(
|
|
8508
|
+
statsMetricCard("PI estimate", `~${formatStatsTokens(payload?.promptEstimate?.total)} tok`, `${statsPromptEstimateSourceLabel(payload?.promptEstimate)} · ${payload?.promptEstimate?.confidence || "estimate"}`, "tone-yellow"),
|
|
8509
|
+
statsMetricCard("Prompt chars", statsNumber(payload?.promptEstimate?.systemPromptChars).toLocaleString(), `${statsNumber(payload?.promptEstimate?.activeToolSchemas)} active tool schemas`, "tone-blue"),
|
|
8510
|
+
statsMetricCard("Calibration", `×${statsNumber(payload?.promptEstimate?.calibrationMultiplier, 1).toFixed(2)}`, `${statsNumber(payload?.promptEstimate?.calibrationSamples)} samples`, "tone-teal"),
|
|
8511
|
+
statsMetricCard("Attempts", String(statsNumber(payload?.promptEstimate?.attempts)), payload?.promptEstimate?.settled ? "settled" : "live fallback", "tone-mauve"),
|
|
8512
|
+
);
|
|
8513
|
+
node.append(
|
|
8514
|
+
cards,
|
|
8515
|
+
renderStatsCalibrationPanel(payload),
|
|
8516
|
+
make("h3", undefined, "PI prompt estimate"),
|
|
8517
|
+
statsLineBlock(payload?.lines?.promptInjection),
|
|
8518
|
+
make("h3", undefined, "Detailed prompt snapshot"),
|
|
8519
|
+
statsLineBlock(payload?.lines?.promptDetailed),
|
|
8520
|
+
make("h3", undefined, "Current context token breakdown"),
|
|
8521
|
+
statsLineBlock(payload?.lines?.tokenBreakdown),
|
|
8522
|
+
);
|
|
8523
|
+
return node;
|
|
8524
|
+
}
|
|
8525
|
+
|
|
8526
|
+
function statsCommandOutputSection(title, command, description, lines = []) {
|
|
8527
|
+
const section = make("section", "stats-overlay-command-section");
|
|
8528
|
+
const header = make("div", "stats-overlay-command-header");
|
|
8529
|
+
const text = make("div", "stats-overlay-command-title");
|
|
8530
|
+
text.append(make("h3", undefined, title), make("p", "muted", description));
|
|
8531
|
+
header.append(text, make("code", "stats-overlay-command-pill", command));
|
|
8532
|
+
section.append(header, statsLineBlock(lines));
|
|
8533
|
+
return section;
|
|
8534
|
+
}
|
|
8535
|
+
|
|
8536
|
+
function renderStatsRaw(payload) {
|
|
8537
|
+
const node = make("div", "stats-overlay-pane");
|
|
8538
|
+
node.append(
|
|
8539
|
+
statsCommandOutputSection("Daily usage graph", "/stats-last [days|all]", "Non-zero daily token/cost graph for the selected range.", payload?.lines?.graph),
|
|
8540
|
+
statsCommandOutputSection("Model comparison", "/stats-model-compare [days|all]", "Token share, spend share, average cost, and average output by model.", payload?.lines?.modelComparison),
|
|
8541
|
+
statsCommandOutputSection("Most expensive sessions", "/stats-most-expense [days|all]", "Highest-cost sessions in the selected range.", payload?.lines?.expensiveSessions),
|
|
8542
|
+
statsCommandOutputSection("Cost trend", "/stats-cost-trend [days|all]", "Daily averages, 30-day projection, highest day, and latest active day.", payload?.lines?.costTrend),
|
|
8543
|
+
statsCommandOutputSection("Cache efficiency", "/stats-cache [days|all]", "Cache hit rate, cache read/write tokens, estimated savings, and token mix.", payload?.lines?.cache),
|
|
8544
|
+
statsCommandOutputSection("PI prompt breakdown", "/stats-pi detailed", "Export-backed initial prompt estimate with detailed prompt snapshot sections.", [...(payload?.lines?.promptInjection || []), "", ...(payload?.lines?.promptDetailed || [])]),
|
|
8545
|
+
);
|
|
8546
|
+
return node;
|
|
8547
|
+
}
|
|
8548
|
+
|
|
8549
|
+
function renderStatsOverlayPane(payload) {
|
|
8550
|
+
if (!payload) return make("p", "stats-overlay-empty muted", statsOverlayLoading ? "Loading stats…" : "No stats payload loaded yet.");
|
|
8551
|
+
switch (statsOverlayActiveTab) {
|
|
8552
|
+
case "daily": return renderStatsDaily(payload);
|
|
8553
|
+
case "models": return renderStatsModels(payload);
|
|
8554
|
+
case "sessions": return renderStatsSessions(payload);
|
|
8555
|
+
case "cost-cache": return renderStatsCostCache(payload);
|
|
8556
|
+
case "prompt": return renderStatsPrompt(payload);
|
|
8557
|
+
case "raw": return renderStatsRaw(payload);
|
|
8558
|
+
default: return renderStatsOverview(payload);
|
|
8559
|
+
}
|
|
8560
|
+
}
|
|
8561
|
+
|
|
8562
|
+
function renderStatsOverlay() {
|
|
8563
|
+
const payload = currentStatsOverlayPayload();
|
|
8564
|
+
if (!elements.statsOverlayDialog) return;
|
|
8565
|
+
|
|
8566
|
+
if (payload) statsOverlayLastScope = statsScopeValueFromPayload(payload);
|
|
8567
|
+
syncStatsScopeControls(statsOverlayLoading ? null : payload);
|
|
8568
|
+
|
|
8569
|
+
const generated = payload?.generatedAt ? new Date(payload.generatedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }) : "not loaded";
|
|
8570
|
+
elements.statsOverlaySubtitle.textContent = payload
|
|
8571
|
+
? `${payload.scopeLabel || "stats"} · ${payload.sessionCount ?? 0} sessions · updated ${generated}`
|
|
8572
|
+
: "Run stats to load the browser dashboard.";
|
|
8573
|
+
|
|
8574
|
+
elements.statsOverlayStatus.textContent = statsOverlayError || (statsOverlayLoading ? "Loading stats from the Pi stats extension…" : statsOverlayCalibrationMessage || (payload ? "" : "No stats payload loaded yet."));
|
|
8575
|
+
elements.statsOverlayStatus.hidden = !elements.statsOverlayStatus.textContent;
|
|
8576
|
+
elements.statsOverlayStatus.classList.toggle("error", !!statsOverlayError);
|
|
8577
|
+
|
|
8578
|
+
elements.statsOverlayTabs.replaceChildren();
|
|
8579
|
+
for (const tab of STATS_OVERLAY_TABS) {
|
|
8580
|
+
const button = make("button", tab.id === statsOverlayActiveTab ? "active" : "", tab.label);
|
|
8581
|
+
button.type = "button";
|
|
8582
|
+
button.setAttribute("role", "tab");
|
|
8583
|
+
button.setAttribute("aria-selected", tab.id === statsOverlayActiveTab ? "true" : "false");
|
|
8584
|
+
button.addEventListener("click", () => {
|
|
8585
|
+
statsOverlayActiveTab = tab.id;
|
|
8586
|
+
renderStatsOverlay();
|
|
8587
|
+
});
|
|
8588
|
+
elements.statsOverlayTabs.append(button);
|
|
8589
|
+
}
|
|
8590
|
+
|
|
8591
|
+
elements.statsOverlayRefreshButton.disabled = statsOverlayLoading;
|
|
8592
|
+
elements.statsOverlayBody.replaceChildren(renderStatsOverlayPane(payload));
|
|
8593
|
+
}
|
|
8594
|
+
|
|
8595
|
+
function scheduleStatsRefreshAfterCalibration(tabContext, delays = [1200]) {
|
|
8596
|
+
for (const delayMs of delays) {
|
|
8597
|
+
setTimeout(() => {
|
|
8598
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
8599
|
+
requestStatsOverlayRefresh();
|
|
8600
|
+
}, delayMs);
|
|
8601
|
+
}
|
|
8602
|
+
}
|
|
8603
|
+
|
|
8604
|
+
async function runStatsCalibration(mode) {
|
|
8605
|
+
const tabContext = activeTabContext();
|
|
8606
|
+
if (!tabContext.tabId) return;
|
|
8607
|
+
const commandName = resolveAvailableCommandName("calibrate", { rpcOnly: true });
|
|
8608
|
+
if (!commandName) {
|
|
8609
|
+
statsOverlayError = "Calibration command unavailable: /calibrate is not loaded in this Pi tab.";
|
|
8610
|
+
renderStatsOverlay();
|
|
8611
|
+
return;
|
|
8612
|
+
}
|
|
8613
|
+
if (mode === "probe" && !confirm("Start an isolated calibration probe? This sends one tiny model request and may incur provider token usage.")) return;
|
|
8614
|
+
|
|
8615
|
+
const command = mode === "current" ? `/${commandName} current` : `/${commandName}`;
|
|
8616
|
+
statsOverlayCalibrationBusy = mode;
|
|
8617
|
+
statsOverlayCalibrationMessage = mode === "current"
|
|
8618
|
+
? "Calibrating from the current session…"
|
|
8619
|
+
: "Starting isolated calibration probe…";
|
|
8620
|
+
statsOverlayError = "";
|
|
8621
|
+
renderStatsOverlay();
|
|
8622
|
+
try {
|
|
8623
|
+
await sendPrompt("prompt", command, { targetTabId: tabContext.tabId, throwOnError: true });
|
|
8624
|
+
statsOverlayCalibrationMessage = mode === "current"
|
|
8625
|
+
? "Calibration command finished; refreshing stats…"
|
|
8626
|
+
: "Probe started; stats will refresh after the probe response is recorded…";
|
|
8627
|
+
statsOverlayCalibrationBusy = "";
|
|
8628
|
+
renderStatsOverlay();
|
|
8629
|
+
scheduleStatsRefreshAfterCalibration(tabContext, mode === "probe" ? [5000, 14000] : [1000]);
|
|
8630
|
+
} catch (error) {
|
|
8631
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
8632
|
+
statsOverlayCalibrationBusy = "";
|
|
8633
|
+
statsOverlayCalibrationMessage = "";
|
|
8634
|
+
statsOverlayError = error.message || String(error);
|
|
8635
|
+
renderStatsOverlay();
|
|
8636
|
+
}
|
|
8637
|
+
}
|
|
8638
|
+
|
|
8639
|
+
async function requestStatsOverlayRefresh() {
|
|
8640
|
+
const tabContext = activeTabContext();
|
|
8641
|
+
if (!tabContext.tabId) return;
|
|
8642
|
+
const statsWebuiCommand = resolveAvailableCommandName("stats-webui", { rpcOnly: true });
|
|
8643
|
+
const fallbackStatsCommand = resolveAvailableCommandName("stats", { rpcOnly: true });
|
|
8644
|
+
const scopeArg = statsScopeCommandArg();
|
|
8645
|
+
const command = statsWebuiCommand
|
|
8646
|
+
? `/${statsWebuiCommand}${scopeArg ? ` ${scopeArg}` : ""}`
|
|
8647
|
+
: fallbackStatsCommand
|
|
8648
|
+
? `/${fallbackStatsCommand}${scopeArg ? ` ${scopeArg}` : ""} --webui`
|
|
8649
|
+
: "";
|
|
8650
|
+
if (!command) {
|
|
8651
|
+
statsOverlayError = "Stats command unavailable: enable/install @firstpick/pi-extension-stats in Optional features.";
|
|
8652
|
+
statsOverlayLoading = false;
|
|
8653
|
+
renderStatsOverlay();
|
|
8654
|
+
return;
|
|
8655
|
+
}
|
|
8656
|
+
|
|
8657
|
+
statsOverlayLastScope = scopeArg;
|
|
8658
|
+
statsOverlayLoading = true;
|
|
8659
|
+
statsOverlayError = "";
|
|
8660
|
+
renderStatsOverlay();
|
|
8661
|
+
try {
|
|
8662
|
+
await sendPrompt("prompt", command, { targetTabId: tabContext.tabId, throwOnError: true });
|
|
8663
|
+
setTimeout(() => {
|
|
8664
|
+
if (!isCurrentTabContext(tabContext) || !statsOverlayLoading) return;
|
|
8665
|
+
statsOverlayLoading = false;
|
|
8666
|
+
if (!currentStatsOverlayPayload()) statsOverlayError = "Stats command returned without a WebUI payload. Try /reload, then open Stats again.";
|
|
8667
|
+
renderStatsOverlay();
|
|
8668
|
+
}, 2500);
|
|
8669
|
+
} catch (error) {
|
|
8670
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
8671
|
+
statsOverlayLoading = false;
|
|
8672
|
+
statsOverlayError = error.message || String(error);
|
|
8673
|
+
renderStatsOverlay();
|
|
8674
|
+
}
|
|
8675
|
+
}
|
|
8676
|
+
|
|
8677
|
+
function openStatsOverlay({ refresh = true } = {}) {
|
|
8678
|
+
setComposerActionsOpen(false);
|
|
8679
|
+
setPublishMenuOpen(false);
|
|
8680
|
+
setNativeCommandMenuOpen(false);
|
|
8681
|
+
setAppRunnerMenuOpen(false);
|
|
8682
|
+
setOptionsMenuOpen(false);
|
|
8683
|
+
statsOverlayError = "";
|
|
8684
|
+
if (!elements.statsOverlayDialog.open) elements.statsOverlayDialog.showModal();
|
|
8685
|
+
renderStatsOverlay();
|
|
8686
|
+
if (refresh || !currentStatsOverlayPayload()) requestStatsOverlayRefresh();
|
|
8687
|
+
}
|
|
8688
|
+
|
|
8689
|
+
function handleStatsWebuiStatus(statusText) {
|
|
8690
|
+
const payload = parseStatsWebuiPayloadRaw(statusText);
|
|
8691
|
+
if (!payload) {
|
|
8692
|
+
if (elements.statsOverlayDialog?.open) {
|
|
8693
|
+
statsOverlayLoading = false;
|
|
8694
|
+
renderStatsOverlay();
|
|
8695
|
+
}
|
|
8696
|
+
return;
|
|
8697
|
+
}
|
|
8698
|
+
latestStatsOverlayPayload = payload;
|
|
8699
|
+
statsOverlayLoading = false;
|
|
8700
|
+
statsOverlayError = "";
|
|
8701
|
+
statsOverlayCalibrationMessage = "";
|
|
8702
|
+
statsOverlayLastScope = statsScopeValueFromPayload(payload);
|
|
8703
|
+
if (payload.open && !elements.statsOverlayDialog?.open) elements.statsOverlayDialog?.showModal();
|
|
8704
|
+
if (payload.open || elements.statsOverlayDialog?.open) renderStatsOverlay();
|
|
8705
|
+
}
|
|
8706
|
+
|
|
8707
|
+
function renderWidgets() {
|
|
8708
|
+
elements.widgetArea.replaceChildren();
|
|
8709
|
+
const releaseOutput = renderReleaseNpmOutputWidget();
|
|
8710
|
+
if (releaseOutput) elements.widgetArea.append(releaseOutput);
|
|
8711
|
+
const releaseLog = renderReleaseNpmLogWidget();
|
|
8712
|
+
if (releaseLog) elements.widgetArea.append(releaseLog);
|
|
8713
|
+
const releaseAurOutput = renderReleaseAurOutputWidget();
|
|
8714
|
+
if (releaseAurOutput) elements.widgetArea.append(releaseAurOutput);
|
|
8715
|
+
const releaseAurLog = renderReleaseAurLogWidget();
|
|
8716
|
+
if (releaseAurLog) elements.widgetArea.append(releaseAurLog);
|
|
8717
|
+
const appRunnerWidget = renderAppRunnerWidget();
|
|
8718
|
+
if (appRunnerWidget) elements.widgetArea.append(appRunnerWidget);
|
|
8719
|
+
|
|
8720
|
+
for (const [key, value] of widgets) {
|
|
8721
|
+
const widgetFeatureId = optionalFeatureWidgetFeatureId(key);
|
|
8722
|
+
if (widgetFeatureId && !isOptionalFeatureEnabled(widgetFeatureId)) continue;
|
|
8723
|
+
if (widgetFeatureId && key !== "todo-progress") continue;
|
|
8724
|
+
const lines = Array.isArray(value.widgetLines) ? value.widgetLines : [];
|
|
8725
|
+
const specialized = key === "todo-progress" && isOptionalFeatureEnabled("todoProgressWidget") ? renderTodoProgressWidget(key, lines) : null;
|
|
8726
|
+
if (specialized) {
|
|
8727
|
+
elements.widgetArea.append(specialized);
|
|
8728
|
+
continue;
|
|
8729
|
+
}
|
|
8730
|
+
|
|
8731
|
+
const node = make("div", "widget");
|
|
8732
|
+
const cleanLines = lines.map(stripAnsi);
|
|
8733
|
+
node.textContent = `${key}\n${cleanLines.join("\n")}`;
|
|
8734
|
+
elements.widgetArea.append(node);
|
|
8735
|
+
}
|
|
8736
|
+
}
|
|
8737
|
+
|
|
8738
|
+
function setGitWorkflow(patch, { tabId = activeTabId } = {}) {
|
|
8739
|
+
const workflow = gitWorkflowForTab(tabId);
|
|
8740
|
+
if (!workflow) return null;
|
|
8741
|
+
Object.assign(workflow, patch);
|
|
8742
|
+
workflow.actionsDone = createGitWorkflowActionsDone(workflow.actionsDone);
|
|
8743
|
+
if (patch.step && !("process" in patch)) workflow.process = gitWorkflowProcessForStep(workflow.step, workflow.process);
|
|
8744
|
+
if (tabId === activeTabId) {
|
|
8745
|
+
gitWorkflow = workflow;
|
|
8746
|
+
renderGitWorkflow();
|
|
8747
|
+
}
|
|
8748
|
+
return workflow;
|
|
8749
|
+
}
|
|
8750
|
+
|
|
8751
|
+
function isCurrentGitWorkflowRun(runId, tabId = activeTabId) {
|
|
8752
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
8753
|
+
return !!workflow?.active && workflow.runId === runId;
|
|
8754
|
+
}
|
|
8755
|
+
|
|
8756
|
+
function appendGitWorkflowOutput(text, { tabId = activeTabId } = {}) {
|
|
8757
|
+
const workflow = gitWorkflowForTab(tabId);
|
|
8758
|
+
if (!workflow) return;
|
|
8759
|
+
const next = `${workflow.output || ""}${workflow.output ? "\n" : ""}${text}`;
|
|
8760
|
+
setGitWorkflow({ output: next.slice(-60000) }, { tabId });
|
|
8761
|
+
}
|
|
8762
|
+
|
|
8763
|
+
function formatGitCommandResult(result) {
|
|
8764
|
+
if (!result) return "";
|
|
8765
|
+
const lines = [`$ ${result.command || "git"}`];
|
|
8766
|
+
if (result.stdout?.trim()) lines.push("", result.stdout.trimEnd());
|
|
8767
|
+
if (result.stderr?.trim()) lines.push("", result.stderr.trimEnd());
|
|
8768
|
+
if (result.exitCode !== 0 || result.signal || result.timedOut || result.cancelled) {
|
|
8769
|
+
lines.push("", `[exit: ${result.exitCode ?? result.signal ?? "unknown"}${result.timedOut ? ", timed out" : ""}${result.cancelled ? ", cancelled" : ""}]`);
|
|
8770
|
+
}
|
|
8771
|
+
return lines.join("\n");
|
|
8772
|
+
}
|
|
8773
|
+
|
|
8774
|
+
function formatCommitMessagePreview(message) {
|
|
8775
|
+
if (!message) return "No commit message loaded yet.";
|
|
8776
|
+
return [`=== SHORT ===`, message.short || "(empty)", "", "=== LONG ===", message.long || "(empty)"].join("\n");
|
|
8777
|
+
}
|
|
8778
|
+
|
|
8779
|
+
function formatInputCommitMessagePreview(message) {
|
|
8780
|
+
return [`=== INPUT ===`, String(message || "").trim() || "(empty)"].join("\n");
|
|
8781
|
+
}
|
|
8782
|
+
|
|
8783
|
+
function gitWorkflowMessageTitle(message) {
|
|
8784
|
+
return String(message?.short || message?.long || "").split("\n").find((line) => line.trim())?.trim() || "Pull request";
|
|
8785
|
+
}
|
|
8786
|
+
|
|
8787
|
+
function slugifyGitBranchPart(value) {
|
|
8788
|
+
return String(value || "")
|
|
8789
|
+
.normalize("NFKD")
|
|
8790
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
8791
|
+
.toLowerCase()
|
|
8792
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
8793
|
+
.replace(/^-+|-+$/g, "")
|
|
8794
|
+
.slice(0, 48);
|
|
8795
|
+
}
|
|
8796
|
+
|
|
8797
|
+
function defaultGitPrBranchName(message = gitWorkflow.message) {
|
|
8798
|
+
const title = gitWorkflowMessageTitle(message);
|
|
8799
|
+
const match = title.match(/^([a-z][a-z0-9-]*)(?:\([^)]*\))?:\s*(.+)$/i);
|
|
8800
|
+
const type = slugifyGitBranchPart(match?.[1] || "feat") || "feat";
|
|
8801
|
+
const summary = slugifyGitBranchPart(match?.[2] || title) || "feature";
|
|
8802
|
+
return `${type}/${summary}`;
|
|
8803
|
+
}
|
|
8804
|
+
|
|
8805
|
+
function formatGitPrPreview(pr) {
|
|
8806
|
+
if (!pr) return "No PR description loaded yet.";
|
|
8807
|
+
const header = [`=== PR DESCRIPTION ===`, `Branch: ${pr.branch || gitWorkflow.prBranch || "current branch"}`];
|
|
8808
|
+
if (pr.path) header.push(`File: ${pr.path}`);
|
|
8809
|
+
return [...header, "", pr.body || "(empty)"].join("\n");
|
|
8810
|
+
}
|
|
8811
|
+
|
|
8812
|
+
function readGitFooterStatusSetup() {
|
|
8813
|
+
try {
|
|
8814
|
+
const parsed = JSON.parse(localStorage.getItem(GIT_FOOTER_STATUS_SETUP_STORAGE_KEY) || "{}");
|
|
8815
|
+
const githubUsername = typeof parsed?.githubUsername === "string" ? parsed.githubUsername.trim() : "";
|
|
8816
|
+
return { githubUsername };
|
|
8817
|
+
} catch {
|
|
8818
|
+
return { githubUsername: "" };
|
|
8819
|
+
}
|
|
8820
|
+
}
|
|
8821
|
+
|
|
8822
|
+
function cleanGitHubUsernameInput(value) {
|
|
8823
|
+
const username = String(value || "").trim().replace(/^@+/, "");
|
|
8824
|
+
if (!username) throw new Error("GitHub username is required.");
|
|
8825
|
+
if (username.length > 39 || !/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(username) || username.includes("--")) {
|
|
8826
|
+
throw new Error("GitHub username must be 1-39 letters/numbers/hyphens, without leading/trailing or repeated hyphens.");
|
|
8827
|
+
}
|
|
8828
|
+
return username;
|
|
8829
|
+
}
|
|
8830
|
+
|
|
8831
|
+
function cleanGitHubRepoNameInput(value) {
|
|
8832
|
+
let repoName = String(value || "").trim();
|
|
8833
|
+
const githubUrlMatch = repoName.match(/github\.com[:/][^/\s]+\/([^/\s]+?)(?:\.git)?\/?$/i);
|
|
8834
|
+
if (githubUrlMatch) repoName = githubUrlMatch[1];
|
|
8835
|
+
if (repoName.includes("/")) repoName = repoName.split("/").filter(Boolean).pop() || "";
|
|
8836
|
+
repoName = repoName.replace(/\.git$/i, "");
|
|
8837
|
+
if (!repoName) throw new Error("GitHub repository name is required.");
|
|
8838
|
+
if (repoName.length > 100 || repoName === "." || repoName === ".." || !/^[A-Za-z0-9._-]+$/.test(repoName)) {
|
|
8839
|
+
throw new Error("GitHub repository name may only contain letters, numbers, dots, underscores, and hyphens.");
|
|
8840
|
+
}
|
|
8841
|
+
return repoName;
|
|
8842
|
+
}
|
|
8843
|
+
|
|
8844
|
+
function writeGitFooterStatusSetup(setup) {
|
|
8845
|
+
try {
|
|
8846
|
+
localStorage.setItem(GIT_FOOTER_STATUS_SETUP_STORAGE_KEY, JSON.stringify({ githubUsername: setup.githubUsername }));
|
|
8847
|
+
} catch {
|
|
8848
|
+
addEvent("Could not persist git-footer-status-setup in browser storage; it will be asked again next time.", "warn");
|
|
8849
|
+
}
|
|
8850
|
+
}
|
|
8851
|
+
|
|
8852
|
+
function cleanGitInitStack(value) {
|
|
8853
|
+
return String(value || "").replace(/\s+/g, " ").trim().slice(0, 160);
|
|
8854
|
+
}
|
|
8855
|
+
|
|
8856
|
+
function readStoredGitInitStack() {
|
|
8857
|
+
try {
|
|
8858
|
+
return cleanGitInitStack(localStorage.getItem(GIT_INIT_STACK_STORAGE_KEY) || "");
|
|
8859
|
+
} catch {
|
|
8860
|
+
return "";
|
|
8861
|
+
}
|
|
8862
|
+
}
|
|
8863
|
+
|
|
8864
|
+
function writeStoredGitInitStack(stack) {
|
|
8865
|
+
const cleanStack = cleanGitInitStack(stack);
|
|
8866
|
+
try {
|
|
8867
|
+
if (cleanStack) localStorage.setItem(GIT_INIT_STACK_STORAGE_KEY, cleanStack);
|
|
8868
|
+
else localStorage.removeItem(GIT_INIT_STACK_STORAGE_KEY);
|
|
8869
|
+
} catch {
|
|
8870
|
+
addEvent("Could not persist the repository stack in browser storage.", "warn");
|
|
8871
|
+
}
|
|
8872
|
+
return cleanStack;
|
|
8873
|
+
}
|
|
8874
|
+
|
|
8875
|
+
function gitInitStackDisplay(stack = "") {
|
|
8876
|
+
const cleanStack = cleanGitInitStack(stack);
|
|
8877
|
+
return cleanStack || "Auto-detect from codebase";
|
|
8878
|
+
}
|
|
8879
|
+
|
|
8880
|
+
function gitInitFilesStatusSummary(status) {
|
|
8881
|
+
if (!status) return "README.md and .gitignore status: not checked yet.";
|
|
8882
|
+
const readme = status.readmeExists ? "README.md exists; it will be staged without overwriting." : "README.md is missing; it will be created.";
|
|
8883
|
+
const gitignore = status.gitignoreExists ? ".gitignore exists; it will be staged without overwriting." : ".gitignore is missing; Pi will be prompted to generate it.";
|
|
8884
|
+
const detected = status.detectedStack ? `Detected stack hint: ${status.detectedStack}` : "Detected stack hint: none yet.";
|
|
8885
|
+
return [readme, gitignore, detected].join("\n");
|
|
8886
|
+
}
|
|
8887
|
+
|
|
8888
|
+
function configureGitFooterStatusSetup({ force = true } = {}) {
|
|
8889
|
+
const current = readGitFooterStatusSetup();
|
|
8890
|
+
if (current.githubUsername && !force) return current;
|
|
8891
|
+
const value = window.prompt("git-footer-status-setup: GitHub username for origin remotes", current.githubUsername || "");
|
|
8892
|
+
if (value === null) return null;
|
|
8893
|
+
try {
|
|
8894
|
+
const setup = { githubUsername: cleanGitHubUsernameInput(value) };
|
|
8895
|
+
writeGitFooterStatusSetup(setup);
|
|
8896
|
+
addEvent(`git-footer-status-setup saved GitHub username ${setup.githubUsername}`, "success");
|
|
8897
|
+
return setup;
|
|
8898
|
+
} catch (error) {
|
|
8899
|
+
addEvent(error.message || String(error), "error");
|
|
8900
|
+
return null;
|
|
8901
|
+
}
|
|
8902
|
+
}
|
|
8903
|
+
|
|
8904
|
+
function ensureGitFooterStatusSetup() {
|
|
8905
|
+
const setup = readGitFooterStatusSetup();
|
|
8906
|
+
if (setup.githubUsername) return setup;
|
|
8907
|
+
return configureGitFooterStatusSetup({ force: true });
|
|
8908
|
+
}
|
|
8909
|
+
|
|
8910
|
+
function defaultGitInitRepoName(tab = activeTab()) {
|
|
8911
|
+
const cwd = latestWorkspace?.cwd || tab?.cwd || latestWorkspace?.displayCwd || "";
|
|
8912
|
+
const lastPart = String(cwd).split(/[\\/]+/).filter(Boolean).pop() || "new-repo";
|
|
8913
|
+
try {
|
|
8914
|
+
return cleanGitHubRepoNameInput(lastPart);
|
|
8915
|
+
} catch {
|
|
8916
|
+
return "new-repo";
|
|
8917
|
+
}
|
|
8918
|
+
}
|
|
8919
|
+
|
|
8920
|
+
function gitInitRemoteUrl(username, repoName) {
|
|
8921
|
+
return `https://github.com/${username}/${repoName}.git`;
|
|
8922
|
+
}
|
|
8923
|
+
|
|
8924
|
+
function promptGitInitRepoName(workflow = gitWorkflow) {
|
|
8925
|
+
const fallback = workflow?.repoName || defaultGitInitRepoName();
|
|
8926
|
+
const value = window.prompt("GitHub repository name for origin remote", fallback);
|
|
8927
|
+
if (value === null) return null;
|
|
8928
|
+
try {
|
|
8929
|
+
return cleanGitHubRepoNameInput(value);
|
|
8930
|
+
} catch (error) {
|
|
8931
|
+
addEvent(error.message || String(error), "error");
|
|
8932
|
+
return null;
|
|
8933
|
+
}
|
|
8934
|
+
}
|
|
8935
|
+
|
|
8936
|
+
function ensureGitInitRemoteDetails(tabId = activeTabId) {
|
|
8937
|
+
const workflow = gitWorkflowForTab(tabId, { create: false }) || gitWorkflow;
|
|
8938
|
+
const setup = ensureGitFooterStatusSetup();
|
|
8939
|
+
if (!setup?.githubUsername) return null;
|
|
8940
|
+
const repoName = workflow.repoName ? cleanGitHubRepoNameInput(workflow.repoName) : promptGitInitRepoName(workflow);
|
|
8941
|
+
if (!repoName) return null;
|
|
8942
|
+
const remoteUrl = gitInitRemoteUrl(setup.githubUsername, repoName);
|
|
8943
|
+
setGitWorkflow({ githubUsername: setup.githubUsername, repoName, remoteUrl }, { tabId });
|
|
8944
|
+
return { username: setup.githubUsername, repoName, remoteUrl };
|
|
8945
|
+
}
|
|
8946
|
+
|
|
8947
|
+
function gitInitWorkflowSetupSummary(workflow = gitWorkflow) {
|
|
8948
|
+
const username = workflow.githubUsername || readGitFooterStatusSetup().githubUsername || "not set";
|
|
8949
|
+
const repoName = workflow.repoName || defaultGitInitRepoName();
|
|
8950
|
+
const remoteUrl = username !== "not set" ? gitInitRemoteUrl(username, repoName) : "not configured";
|
|
8951
|
+
return [`GitHub username: ${username}`, `Repository name: ${repoName}`, `Stack: ${gitInitStackDisplay(workflow.stack)}`, `Origin URL: ${remoteUrl}`].join("\n");
|
|
8952
|
+
}
|
|
8953
|
+
|
|
8954
|
+
function addGitWorkflowAction(label, handler, className = "", disabled = gitWorkflow.busy, tooltip = "") {
|
|
8955
|
+
const button = make("button", className, label);
|
|
8956
|
+
button.type = "button";
|
|
8957
|
+
button.disabled = disabled;
|
|
8958
|
+
if (tooltip) {
|
|
8959
|
+
button.title = tooltip;
|
|
8960
|
+
button.dataset.tooltip = tooltip;
|
|
8961
|
+
button.setAttribute("aria-label", `${label}. ${tooltip.replace(/\s+/g, " ")}`);
|
|
8962
|
+
}
|
|
8963
|
+
button.addEventListener("click", handler);
|
|
8964
|
+
elements.gitWorkflowActions.append(button);
|
|
8965
|
+
return button;
|
|
8966
|
+
}
|
|
8967
|
+
|
|
8968
|
+
function renderGitInitStackInput() {
|
|
8969
|
+
const tabId = gitWorkflowActionTabId();
|
|
8970
|
+
const workflow = gitWorkflowForTab(tabId, { create: false }) || gitWorkflow;
|
|
8971
|
+
const storedStack = workflow?.stack || readStoredGitInitStack();
|
|
8972
|
+
const row = make("div", "git-workflow-message-input-row git-workflow-stack-input-row");
|
|
8973
|
+
|
|
8974
|
+
const selectField = make("label", "git-workflow-message-input-field");
|
|
8975
|
+
selectField.setAttribute("for", "gitWorkflowStackSelect");
|
|
8976
|
+
selectField.append(make("span", "git-workflow-message-input-label", "Stack preset"));
|
|
8977
|
+
const select = make("select", "git-workflow-message-input");
|
|
8978
|
+
select.id = "gitWorkflowStackSelect";
|
|
8979
|
+
for (const option of GIT_INIT_STACK_OPTIONS) {
|
|
8980
|
+
const node = make("option", undefined, option.label);
|
|
8981
|
+
node.value = option.value;
|
|
8982
|
+
select.append(node);
|
|
8983
|
+
}
|
|
8984
|
+
const matching = GIT_INIT_STACK_OPTIONS.some((option) => option.value === storedStack);
|
|
8985
|
+
select.value = matching ? storedStack : storedStack ? "Custom" : "";
|
|
8986
|
+
selectField.append(select);
|
|
8987
|
+
|
|
8988
|
+
const inputField = make("label", "git-workflow-message-input-field");
|
|
8989
|
+
inputField.setAttribute("for", "gitWorkflowStackInput");
|
|
8990
|
+
inputField.append(make("span", "git-workflow-message-input-label", "Stack input"));
|
|
8991
|
+
const input = make("input", "git-workflow-message-input");
|
|
8992
|
+
input.id = "gitWorkflowStackInput";
|
|
8993
|
+
input.type = "text";
|
|
8994
|
+
input.value = storedStack;
|
|
8995
|
+
input.placeholder = "e.g. Node.js + Vite + React, Python + FastAPI, Rust CLI";
|
|
8996
|
+
input.autocomplete = "off";
|
|
8997
|
+
input.spellcheck = false;
|
|
8998
|
+
inputField.append(input);
|
|
8999
|
+
|
|
9000
|
+
const saveButton = make("button", "git-workflow-message-input-commit", "Save stack");
|
|
9001
|
+
saveButton.type = "button";
|
|
9002
|
+
saveButton.title = GIT_INIT_STACK_TOOLTIP;
|
|
9003
|
+
saveButton.dataset.tooltip = GIT_INIT_STACK_TOOLTIP;
|
|
9004
|
+
const saveStack = () => {
|
|
9005
|
+
const stack = writeStoredGitInitStack(input.value);
|
|
9006
|
+
const current = gitWorkflowForTab(tabId, { create: false });
|
|
9007
|
+
if (!current) return;
|
|
9008
|
+
setGitWorkflow({ step: "readme", stack, ...gitWorkflowActionDonePatch(current, "stack"), output: `Stack saved: ${gitInitStackDisplay(stack)}\n\nNext: check README.md and .gitignore before staging. ${stack ? "Pi will use this stack for .gitignore." : "Pi will inspect the codebase and fall back to sane defaults for .gitignore."}` }, { tabId });
|
|
9009
|
+
};
|
|
9010
|
+
select.addEventListener("change", () => {
|
|
9011
|
+
if (select.value === "Custom") {
|
|
9012
|
+
input.focus();
|
|
9013
|
+
input.select();
|
|
9014
|
+
} else {
|
|
9015
|
+
input.value = select.value;
|
|
9016
|
+
}
|
|
9017
|
+
});
|
|
9018
|
+
input.addEventListener("keydown", (event) => {
|
|
9019
|
+
if (event.key !== "Enter") return;
|
|
9020
|
+
event.preventDefault();
|
|
9021
|
+
saveStack();
|
|
9022
|
+
});
|
|
9023
|
+
saveButton.addEventListener("click", saveStack);
|
|
9024
|
+
row.append(selectField, inputField, saveButton);
|
|
9025
|
+
elements.gitWorkflowActions.append(row);
|
|
9026
|
+
}
|
|
9027
|
+
|
|
9028
|
+
function renderGitWorkflowManualCommitInput() {
|
|
9029
|
+
const tabId = gitWorkflowActionTabId();
|
|
9030
|
+
const workflow = gitWorkflowForTab(tabId, { create: false }) || gitWorkflow;
|
|
9031
|
+
const defaultCommitMessage = String(workflow?.manualCommitMessageDefault || "").trim();
|
|
9032
|
+
const row = make("div", "git-workflow-message-input-row");
|
|
9033
|
+
const field = make("label", "git-workflow-message-input-field");
|
|
9034
|
+
field.setAttribute("for", "gitWorkflowManualCommitMessage");
|
|
9035
|
+
field.append(make("span", "git-workflow-message-input-label", "Input commit message"));
|
|
9036
|
+
|
|
9037
|
+
const input = make("input", "git-workflow-message-input");
|
|
9038
|
+
input.id = "gitWorkflowManualCommitMessage";
|
|
9039
|
+
input.type = "text";
|
|
9040
|
+
input.value = workflow?.manualCommitMessage || "";
|
|
9041
|
+
input.placeholder = defaultCommitMessage || "Type a commit message to use instead of short/long";
|
|
9042
|
+
input.autocomplete = "off";
|
|
9043
|
+
input.spellcheck = true;
|
|
9044
|
+
|
|
9045
|
+
const commitButton = make("button", "git-workflow-message-input-commit", "Commit input");
|
|
9046
|
+
commitButton.type = "button";
|
|
9047
|
+
const updateCommitState = () => {
|
|
9048
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
9049
|
+
const message = String(input.value || "").trim() || String(currentWorkflow?.manualCommitMessageDefault || "").trim();
|
|
9050
|
+
commitButton.disabled = !currentWorkflow || !!currentWorkflow.busy || !message;
|
|
9051
|
+
if (message && !String(input.value || "").trim()) commitButton.title = `Use default commit message: ${message}`;
|
|
9052
|
+
else commitButton.removeAttribute("title");
|
|
9053
|
+
};
|
|
9054
|
+
input.addEventListener("input", () => {
|
|
9055
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
9056
|
+
if (currentWorkflow) currentWorkflow.manualCommitMessage = input.value;
|
|
9057
|
+
updateCommitState();
|
|
9058
|
+
});
|
|
9059
|
+
input.addEventListener("keydown", (event) => {
|
|
9060
|
+
if (event.key !== "Enter") return;
|
|
9061
|
+
event.preventDefault();
|
|
9062
|
+
if (!commitButton.disabled) commitGitWorkflow("input", tabId);
|
|
9063
|
+
});
|
|
9064
|
+
commitButton.addEventListener("click", () => {
|
|
9065
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
9066
|
+
if (currentWorkflow) currentWorkflow.manualCommitMessage = input.value;
|
|
9067
|
+
commitGitWorkflow("input", tabId);
|
|
9068
|
+
});
|
|
9069
|
+
updateCommitState();
|
|
9070
|
+
loadGitWorkflowDefaultCommitMessage({ runId: workflow?.runId, tabId });
|
|
9071
|
+
|
|
9072
|
+
field.append(input);
|
|
9073
|
+
row.append(field, commitButton);
|
|
9074
|
+
elements.gitWorkflowActions.append(row);
|
|
9075
|
+
}
|
|
9076
|
+
|
|
9077
|
+
function setGitPrDialogStatus(message = "", level = "muted") {
|
|
9078
|
+
if (!elements.gitPrStatus) return;
|
|
9079
|
+
elements.gitPrStatus.textContent = message;
|
|
9080
|
+
elements.gitPrStatus.className = `git-pr-status ${level || "muted"}`;
|
|
9081
|
+
}
|
|
9082
|
+
|
|
9083
|
+
function resolveGitPrDialog(value) {
|
|
9084
|
+
const resolve = activeGitPrDialogResolve;
|
|
9085
|
+
activeGitPrDialogResolve = null;
|
|
7853
9086
|
if (elements.gitPrDialog?.open) elements.gitPrDialog.close();
|
|
7854
9087
|
if (resolve) resolve(value);
|
|
7855
9088
|
}
|
|
@@ -7869,6 +9102,29 @@ function openGitPrReviewDialog(pr, { title = "" } = {}) {
|
|
|
7869
9102
|
|
|
7870
9103
|
function gitWorkflowProcessForStep(step = gitWorkflow.step, fallback = gitWorkflow.process || "stage") {
|
|
7871
9104
|
switch (step) {
|
|
9105
|
+
case "initSetup":
|
|
9106
|
+
case "initRepo":
|
|
9107
|
+
case "initializingRepo":
|
|
9108
|
+
return "init";
|
|
9109
|
+
case "initStack":
|
|
9110
|
+
return "stack";
|
|
9111
|
+
case "readme":
|
|
9112
|
+
case "readmeCreating":
|
|
9113
|
+
case "readmeGenerating":
|
|
9114
|
+
case "gitignoreGenerating":
|
|
9115
|
+
return "readme";
|
|
9116
|
+
case "initialCommit":
|
|
9117
|
+
case "initialCommitting":
|
|
9118
|
+
return "commit";
|
|
9119
|
+
case "mainBranch":
|
|
9120
|
+
case "mainBranching":
|
|
9121
|
+
return "branch";
|
|
9122
|
+
case "remote":
|
|
9123
|
+
case "remoteAdding":
|
|
9124
|
+
return "remote";
|
|
9125
|
+
case "initialPush":
|
|
9126
|
+
case "initialPushing":
|
|
9127
|
+
return "push";
|
|
7872
9128
|
case "generate":
|
|
7873
9129
|
case "generating":
|
|
7874
9130
|
return "message";
|
|
@@ -7889,26 +9145,89 @@ function gitWorkflowProcessForStep(step = gitWorkflow.step, fallback = gitWorkfl
|
|
|
7889
9145
|
return "stage";
|
|
7890
9146
|
case "cancelled":
|
|
7891
9147
|
case "error":
|
|
7892
|
-
return GIT_WORKFLOW_PROCESS_VALUES.has(fallback) ? fallback : "stage";
|
|
9148
|
+
return GIT_WORKFLOW_PROCESS_VALUES.has(fallback) || GIT_INIT_WORKFLOW_PROCESS_VALUES.has(fallback) ? fallback : "stage";
|
|
7893
9149
|
default:
|
|
7894
9150
|
return "stage";
|
|
7895
9151
|
}
|
|
7896
9152
|
}
|
|
7897
9153
|
|
|
9154
|
+
function selectGitInitWorkflowProcess(processValue, tabId, workflow) {
|
|
9155
|
+
const process = GIT_INIT_WORKFLOW_PROCESS_VALUES.has(processValue) ? processValue : "init";
|
|
9156
|
+
workflow.runId += 1;
|
|
9157
|
+
const username = workflow.githubUsername || readGitFooterStatusSetup().githubUsername || "";
|
|
9158
|
+
const repoName = workflow.repoName || defaultGitInitRepoName();
|
|
9159
|
+
const remoteUrl = username ? gitInitRemoteUrl(username, repoName) : "";
|
|
9160
|
+
const stack = workflow.stack || readStoredGitInitStack();
|
|
9161
|
+
const base = {
|
|
9162
|
+
mode: "initRepo",
|
|
9163
|
+
active: true,
|
|
9164
|
+
process,
|
|
9165
|
+
busy: false,
|
|
9166
|
+
error: "",
|
|
9167
|
+
githubUsername: username,
|
|
9168
|
+
repoName,
|
|
9169
|
+
remoteUrl,
|
|
9170
|
+
stack,
|
|
9171
|
+
readmeRequestedAt: 0,
|
|
9172
|
+
gitignoreRequestedAt: 0,
|
|
9173
|
+
initFilesStatus: null,
|
|
9174
|
+
message: null,
|
|
9175
|
+
manualCommitMessage: "",
|
|
9176
|
+
...resetGitWorkflowManualCommitDefaultPatch(),
|
|
9177
|
+
messageRequestedAt: 0,
|
|
9178
|
+
branchName: "",
|
|
9179
|
+
branchNameRequestedAt: 0,
|
|
9180
|
+
prMode: false,
|
|
9181
|
+
prBranch: "",
|
|
9182
|
+
pr: null,
|
|
9183
|
+
prRequestedAt: 0,
|
|
9184
|
+
};
|
|
9185
|
+
|
|
9186
|
+
if (process === "init") {
|
|
9187
|
+
setGitWorkflow({ ...base, step: username ? "initRepo" : "initSetup", output: username ? `Ready to initialize a Git repository.\n\n${gitInitWorkflowSetupSummary(base)}` : "No GitHub username stored yet. Run git-footer-status-setup first." }, { tabId });
|
|
9188
|
+
return;
|
|
9189
|
+
}
|
|
9190
|
+
if (process === "stack") {
|
|
9191
|
+
setGitWorkflow({ ...base, step: "initStack", output: `Choose a repository stack before README/.gitignore preparation.\n\nCurrent stack: ${gitInitStackDisplay(stack)}` }, { tabId });
|
|
9192
|
+
return;
|
|
9193
|
+
}
|
|
9194
|
+
if (process === "readme") {
|
|
9195
|
+
setGitWorkflow({ ...base, step: "readme", output: "Ready to check README.md and .gitignore before staging them." }, { tabId });
|
|
9196
|
+
return;
|
|
9197
|
+
}
|
|
9198
|
+
if (process === "commit") {
|
|
9199
|
+
setGitWorkflow({ ...base, step: "initialCommit", output: "Ready to create the initial commit with message: Initial commit." }, { tabId });
|
|
9200
|
+
return;
|
|
9201
|
+
}
|
|
9202
|
+
if (process === "branch") {
|
|
9203
|
+
setGitWorkflow({ ...base, step: "mainBranch", output: "Ready to rename the current branch to main with git branch -M main." }, { tabId });
|
|
9204
|
+
return;
|
|
9205
|
+
}
|
|
9206
|
+
if (process === "remote") {
|
|
9207
|
+
setGitWorkflow({ ...base, step: "remote", output: `Ready to add origin.\n\n${gitInitWorkflowSetupSummary(base)}` }, { tabId });
|
|
9208
|
+
return;
|
|
9209
|
+
}
|
|
9210
|
+
setGitWorkflow({ ...base, step: "initialPush", output: "Ready to run git push -u origin main." }, { tabId });
|
|
9211
|
+
}
|
|
9212
|
+
|
|
7898
9213
|
function selectGitWorkflowProcess(processValue, tabId = gitWorkflowActionTabId()) {
|
|
7899
9214
|
const workflow = gitWorkflowForTab(tabId);
|
|
7900
9215
|
if (!workflow) return;
|
|
9216
|
+
if (workflow.mode === "initRepo") {
|
|
9217
|
+
selectGitInitWorkflowProcess(processValue, tabId, workflow);
|
|
9218
|
+
return;
|
|
9219
|
+
}
|
|
7901
9220
|
const process = GIT_WORKFLOW_PROCESS_VALUES.has(processValue) ? processValue : "stage";
|
|
7902
9221
|
workflow.runId += 1;
|
|
7903
9222
|
const runId = workflow.runId;
|
|
7904
|
-
const base = { active: true, process, busy: false, error: "", messageRequestedAt: 0, branchName: "", branchNameRequestedAt: 0, prMode: false, prBranch: "", pr: null, prRequestedAt: 0 };
|
|
9223
|
+
const base = { mode: "standard", active: true, process, busy: false, error: "", githubUsername: "", repoName: "", remoteUrl: "", stack: "", readmeRequestedAt: 0, gitignoreRequestedAt: 0, initFilesStatus: null, manualCommitMessage: "", ...resetGitWorkflowManualCommitDefaultPatch(), messageRequestedAt: 0, branchName: "", branchNameRequestedAt: 0, prMode: false, prBranch: "", pr: null, prRequestedAt: 0 };
|
|
7905
9224
|
|
|
7906
9225
|
if (process === "stage") {
|
|
7907
9226
|
setGitWorkflow({ ...base, step: "add", message: null, output: "Ready to stage all changes with git add ." }, { tabId });
|
|
7908
9227
|
return;
|
|
7909
9228
|
}
|
|
7910
9229
|
if (process === "message") {
|
|
7911
|
-
setGitWorkflow({ ...base, step: "generate", message: null, output: "Ready to generate a commit message from the currently staged changes." }, { tabId });
|
|
9230
|
+
setGitWorkflow({ ...base, step: "generate", message: null, output: "Ready to generate a commit message from the currently staged changes, or type one and commit it directly." }, { tabId });
|
|
7912
9231
|
return;
|
|
7913
9232
|
}
|
|
7914
9233
|
if (process === "commit") {
|
|
@@ -7921,6 +9240,22 @@ function selectGitWorkflowProcess(processValue, tabId = gitWorkflowActionTabId()
|
|
|
7921
9240
|
|
|
7922
9241
|
function gitWorkflowTitle() {
|
|
7923
9242
|
switch (gitWorkflow.step) {
|
|
9243
|
+
case "initSetup": return "Set up GitHub username";
|
|
9244
|
+
case "initRepo": return "Initialize Git repository";
|
|
9245
|
+
case "initializingRepo": return "Running git init";
|
|
9246
|
+
case "initStack": return "Choose repository stack";
|
|
9247
|
+
case "readme": return "Prepare README and .gitignore";
|
|
9248
|
+
case "readmeCreating": return "Preparing README and .gitignore";
|
|
9249
|
+
case "readmeGenerating": return "Waiting for README";
|
|
9250
|
+
case "gitignoreGenerating": return "Waiting for .gitignore";
|
|
9251
|
+
case "initialCommit": return "Create initial commit";
|
|
9252
|
+
case "initialCommitting": return "Committing initial files";
|
|
9253
|
+
case "mainBranch": return "Rename branch to main";
|
|
9254
|
+
case "mainBranching": return "Running git branch -M main";
|
|
9255
|
+
case "remote": return "Add origin remote";
|
|
9256
|
+
case "remoteAdding": return "Adding origin remote";
|
|
9257
|
+
case "initialPush": return "Push main upstream";
|
|
9258
|
+
case "initialPushing": return "Pushing main";
|
|
7924
9259
|
case "add": return "Stage all changes";
|
|
7925
9260
|
case "generate": return "Generate staged commit message";
|
|
7926
9261
|
case "generating": return "Waiting for /git-staged-msg";
|
|
@@ -7933,31 +9268,107 @@ function gitWorkflowTitle() {
|
|
|
7933
9268
|
case "prGenerating": return "Waiting for /pr";
|
|
7934
9269
|
case "prReview": return "Review PR description";
|
|
7935
9270
|
case "prCreating": return "Creating pull request";
|
|
7936
|
-
case "done": return "Git workflow complete";
|
|
7937
|
-
case "cancelled": return "Git workflow cancelled";
|
|
7938
|
-
case "error": return "Git workflow needs attention";
|
|
7939
|
-
default: return "Git workflow";
|
|
9271
|
+
case "done": return gitWorkflow.mode === "initRepo" ? "Git repository setup complete" : "Git workflow complete";
|
|
9272
|
+
case "cancelled": return gitWorkflow.mode === "initRepo" ? "Git repository setup cancelled" : "Git workflow cancelled";
|
|
9273
|
+
case "error": return gitWorkflow.mode === "initRepo" ? "Git repository setup needs attention" : "Git workflow needs attention";
|
|
9274
|
+
default: return gitWorkflow.mode === "initRepo" ? "Git repository setup" : "Git workflow";
|
|
7940
9275
|
}
|
|
7941
9276
|
}
|
|
7942
9277
|
|
|
7943
9278
|
function gitWorkflowHint() {
|
|
7944
9279
|
switch (gitWorkflow.step) {
|
|
9280
|
+
case "initSetup": return "First-time setup: save the GitHub username used in https://github.com/USERNAME/REPO_NAME.git.";
|
|
9281
|
+
case "initRepo": return "Step 1: run git init in the current Pi working directory.";
|
|
9282
|
+
case "initializingRepo": return "Running git init. Cancel will terminate the git command.";
|
|
9283
|
+
case "initStack": return "Step 2: choose a stack preset or type one; leave blank to let Pi infer it from the codebase.";
|
|
9284
|
+
case "readme": return "Step 3: check README.md and .gitignore, prompt Pi for missing files, then stage them.";
|
|
9285
|
+
case "readmeCreating": return "Checking README.md/.gitignore, creating missing files if needed, and staging them.";
|
|
9286
|
+
case "readmeGenerating": return "Pi is filling out README.md from the selected stack and repository contents.";
|
|
9287
|
+
case "gitignoreGenerating": return "Pi is generating .gitignore from the selected stack or by inspecting the codebase.";
|
|
9288
|
+
case "initialCommit": return "Step 4: run git commit -m \"Initial commit\".";
|
|
9289
|
+
case "initialCommitting": return "Creating the initial commit.";
|
|
9290
|
+
case "mainBranch": return "Step 5: run git branch -M main.";
|
|
9291
|
+
case "mainBranching": return "Renaming the current branch to main.";
|
|
9292
|
+
case "remote": return "Step 6: add origin as https://github.com/USERNAME/REPO_NAME.git.";
|
|
9293
|
+
case "remoteAdding": return "Adding the GitHub origin remote.";
|
|
9294
|
+
case "initialPush": return "Step 7: run git push -u origin main.";
|
|
9295
|
+
case "initialPushing": return "Pushing main upstream. Authentication must already be available to git.";
|
|
7945
9296
|
case "add": return "Step 1: run git add . in the current Pi working directory.";
|
|
7946
|
-
case "generate": return "Step 2: run /git-staged-msg,
|
|
9297
|
+
case "generate": return "Step 2: run /git-staged-msg, or type a commit message and use Commit input.";
|
|
7947
9298
|
case "generating": return "Pi is generating dev/COMMIT/staged-commit-short.txt and staged-commit-long.txt.";
|
|
7948
|
-
case "message": return gitWorkflow.prMode ? `Branch ${gitWorkflow.prBranch || "created"}: choose short or
|
|
9299
|
+
case "message": return gitWorkflow.prMode ? `Branch ${gitWorkflow.prBranch || "created"}: choose short, long, or typed input before opening a PR.` : "Step 3/4: preview the native g-msg output, type a commit message if needed, commit here, or create a PR branch first.";
|
|
7949
9300
|
case "branchNaming": return "Pi is generating dev/COMMIT/staged-branch-name.txt. Cancel will request Pi abort.";
|
|
7950
9301
|
case "branching": return "Creating a new branch with git switch -c before committing.";
|
|
7951
|
-
case "committing": return "Running native git commit
|
|
9302
|
+
case "committing": return "Running native git commit with the selected message.";
|
|
7952
9303
|
case "push": return gitWorkflow.prMode ? "Push the PR branch, generate /pr, review the description, then create the pull request." : "Step 5: push the new commit to the configured remote.";
|
|
7953
9304
|
case "pushing": return "Running git push. Cancel will request process termination.";
|
|
7954
9305
|
case "prGenerating": return "Pi is generating dev/PR/<current-branch>.md with /pr.";
|
|
7955
9306
|
case "prReview": return "Review or edit the generated PR description before creating the pull request.";
|
|
7956
9307
|
case "prCreating": return "Running gh pr create with the confirmed description.";
|
|
7957
|
-
case "done": return gitWorkflow.prMode ? "PR workflow finished. Review the output below." : "Push finished. Review the output below.";
|
|
9308
|
+
case "done": return gitWorkflow.mode === "initRepo" ? "Initial repository workflow finished. Review the output below." : gitWorkflow.prMode ? "PR workflow finished. Review the output below." : "Push finished. Review the output below.";
|
|
7958
9309
|
case "cancelled": return "No further workflow steps will run.";
|
|
7959
9310
|
case "error": return gitWorkflow.error || "Fix the issue, then retry or restart.";
|
|
7960
|
-
default: return "Stage changes, generate a commit message, commit, and push.";
|
|
9311
|
+
default: return gitWorkflow.mode === "initRepo" ? "Initialize a repository, create README.md, commit, add origin, and push main." : "Stage changes, generate a commit message, commit, and push.";
|
|
9312
|
+
}
|
|
9313
|
+
}
|
|
9314
|
+
|
|
9315
|
+
function renderGitInitWorkflowActions() {
|
|
9316
|
+
if (gitWorkflow.step === "initSetup") {
|
|
9317
|
+
addGitWorkflowAction("git-footer-status-setup", () => {
|
|
9318
|
+
const setup = configureGitFooterStatusSetup({ force: true });
|
|
9319
|
+
if (!setup?.githubUsername) return;
|
|
9320
|
+
const repoName = gitWorkflow.repoName || defaultGitInitRepoName();
|
|
9321
|
+
const stack = gitWorkflow.stack || readStoredGitInitStack();
|
|
9322
|
+
setGitWorkflow({ step: "initRepo", githubUsername: setup.githubUsername, repoName, stack, remoteUrl: gitInitRemoteUrl(setup.githubUsername, repoName), output: `Ready to initialize a Git repository.\n\n${gitInitWorkflowSetupSummary({ githubUsername: setup.githubUsername, repoName, stack })}` });
|
|
9323
|
+
}, "primary", false, GIT_FOOTER_STATUS_SETUP_TOOLTIP);
|
|
9324
|
+
} else if (gitWorkflow.step === "initRepo") {
|
|
9325
|
+
addGitWorkflowAction("Run git init", () => runGitInitRepository(), "primary", false, "Run git init in the current Pi working directory.");
|
|
9326
|
+
} else if (gitWorkflow.step === "initializingRepo") {
|
|
9327
|
+
addGitWorkflowAction("Running git init…", () => {}, "primary", true);
|
|
9328
|
+
} else if (gitWorkflow.step === "initStack") {
|
|
9329
|
+
renderGitInitStackInput();
|
|
9330
|
+
addGitWorkflowAction("Skip / auto-detect", () => {
|
|
9331
|
+
const workflow = gitWorkflowForTab(gitWorkflowActionTabId(), { create: false }) || gitWorkflow;
|
|
9332
|
+
writeStoredGitInitStack("");
|
|
9333
|
+
setGitWorkflow({ step: "readme", stack: "", ...gitWorkflowActionDonePatch(workflow, "stack"), output: "Stack left blank. Pi will inspect the codebase for .gitignore and fallback to sane defaults.\n\nNext: check README.md and .gitignore before staging." });
|
|
9334
|
+
}, "", false, GIT_INIT_STACK_TOOLTIP);
|
|
9335
|
+
} else if (gitWorkflow.step === "readme") {
|
|
9336
|
+
addGitWorkflowAction("Check and prepare files", () => prepareGitInitFiles(), "primary", false, "Check README.md and .gitignore first; create missing README.md; prompt Pi to generate missing .gitignore.");
|
|
9337
|
+
} else if (gitWorkflow.step === "readmeCreating") {
|
|
9338
|
+
addGitWorkflowAction("Preparing files…", () => {}, "primary", true);
|
|
9339
|
+
} else if (gitWorkflow.step === "readmeGenerating") {
|
|
9340
|
+
addGitWorkflowAction("Waiting for Pi…", () => {}, "primary", true);
|
|
9341
|
+
} else if (gitWorkflow.step === "gitignoreGenerating") {
|
|
9342
|
+
addGitWorkflowAction("Waiting for Pi…", () => {}, "primary", true);
|
|
9343
|
+
} else if (gitWorkflow.step === "initialCommit") {
|
|
9344
|
+
addGitWorkflowAction("Commit initial files", () => commitGitInitialReadme(), "primary", false, "Run git commit -m \"Initial commit\".");
|
|
9345
|
+
} else if (gitWorkflow.step === "initialCommitting") {
|
|
9346
|
+
addGitWorkflowAction("Committing…", () => {}, "primary", true);
|
|
9347
|
+
} else if (gitWorkflow.step === "mainBranch") {
|
|
9348
|
+
addGitWorkflowAction("Run git branch -M main", () => branchGitInitMain(), "primary", false, "Rename the current branch to main.");
|
|
9349
|
+
} else if (gitWorkflow.step === "mainBranching") {
|
|
9350
|
+
addGitWorkflowAction("Renaming branch…", () => {}, "primary", true);
|
|
9351
|
+
} else if (gitWorkflow.step === "remote") {
|
|
9352
|
+
addGitWorkflowAction("Add origin remote", () => addGitInitRemote(), "primary", false, GIT_INIT_REMOTE_TOOLTIP);
|
|
9353
|
+
addGitWorkflowAction("git-footer-status-setup", () => {
|
|
9354
|
+
const setup = configureGitFooterStatusSetup({ force: true });
|
|
9355
|
+
if (!setup?.githubUsername) return;
|
|
9356
|
+
const repoName = gitWorkflow.repoName || defaultGitInitRepoName();
|
|
9357
|
+
const stack = gitWorkflow.stack || readStoredGitInitStack();
|
|
9358
|
+
setGitWorkflow({ githubUsername: setup.githubUsername, repoName, stack, remoteUrl: gitInitRemoteUrl(setup.githubUsername, repoName), output: `Ready to add origin.\n\n${gitInitWorkflowSetupSummary({ githubUsername: setup.githubUsername, repoName, stack })}` });
|
|
9359
|
+
}, "", false, GIT_FOOTER_STATUS_SETUP_TOOLTIP);
|
|
9360
|
+
} else if (gitWorkflow.step === "remoteAdding") {
|
|
9361
|
+
addGitWorkflowAction("Adding origin…", () => {}, "primary", true);
|
|
9362
|
+
} else if (gitWorkflow.step === "initialPush") {
|
|
9363
|
+
addGitWorkflowAction("Run git push -u origin main", () => pushGitInitWorkflow(), "primary", false, "Push main to origin and set upstream tracking.");
|
|
9364
|
+
} else if (gitWorkflow.step === "initialPushing") {
|
|
9365
|
+
addGitWorkflowAction("Pushing main…", () => {}, "primary", true);
|
|
9366
|
+
} else if (gitWorkflow.step === "done") {
|
|
9367
|
+
addGitWorkflowAction("Close", () => setGitWorkflow({ active: false }), "primary", false);
|
|
9368
|
+
addGitWorkflowAction("Initialize another", () => startGitInitWorkflow(), "", false);
|
|
9369
|
+
} else if (["cancelled", "error"].includes(gitWorkflow.step)) {
|
|
9370
|
+
addGitWorkflowAction("Close", () => setGitWorkflow({ active: false }), "primary", false);
|
|
9371
|
+
addGitWorkflowAction("Restart setup", () => startGitInitWorkflow(), "", false);
|
|
7961
9372
|
}
|
|
7962
9373
|
}
|
|
7963
9374
|
|
|
@@ -7965,15 +9376,19 @@ function renderGitWorkflow() {
|
|
|
7965
9376
|
elements.gitWorkflowPanel.hidden = !gitWorkflow.active;
|
|
7966
9377
|
if (!gitWorkflow.active) return;
|
|
7967
9378
|
|
|
9379
|
+
elements.gitWorkflowPanel.dataset.mode = gitWorkflow.mode || "standard";
|
|
9380
|
+
if (elements.gitWorkflowKicker) elements.gitWorkflowKicker.textContent = gitWorkflow.mode === "initRepo" ? "Git repository setup" : "Git workflow";
|
|
7968
9381
|
elements.gitWorkflowTitle.textContent = gitWorkflowTitle();
|
|
7969
9382
|
elements.gitWorkflowHint.textContent = gitWorkflowHint();
|
|
7970
9383
|
elements.gitWorkflowOutput.textContent = gitWorkflow.output || "Ready.";
|
|
7971
9384
|
elements.gitWorkflowSteps.replaceChildren();
|
|
7972
9385
|
elements.gitWorkflowActions.replaceChildren();
|
|
7973
9386
|
|
|
7974
|
-
const
|
|
9387
|
+
const processes = gitWorkflow.mode === "initRepo" ? GIT_INIT_WORKFLOW_PROCESSES : GIT_WORKFLOW_PROCESSES;
|
|
9388
|
+
const activeIndexMap = gitWorkflow.mode === "initRepo" ? GIT_INIT_WORKFLOW_ACTIVE_INDEX : GIT_WORKFLOW_ACTIVE_INDEX;
|
|
9389
|
+
const activeIndex = activeIndexMap[gitWorkflow.step] ?? 0;
|
|
7975
9390
|
const activeProcess = gitWorkflowProcessForStep(gitWorkflow.step, gitWorkflow.process);
|
|
7976
|
-
for (const [index, process] of
|
|
9391
|
+
for (const [index, process] of processes.entries()) {
|
|
7977
9392
|
const item = make("button", "git-workflow-step", process.label);
|
|
7978
9393
|
item.type = "button";
|
|
7979
9394
|
item.dataset.gitWorkflowProcess = process.value;
|
|
@@ -7987,9 +9402,15 @@ function renderGitWorkflow() {
|
|
|
7987
9402
|
elements.gitWorkflowCancelButton.hidden = ["done", "cancelled"].includes(gitWorkflow.step);
|
|
7988
9403
|
elements.gitWorkflowCancelButton.disabled = false;
|
|
7989
9404
|
|
|
9405
|
+
if (gitWorkflow.mode === "initRepo") {
|
|
9406
|
+
renderGitInitWorkflowActions();
|
|
9407
|
+
return;
|
|
9408
|
+
}
|
|
9409
|
+
|
|
7990
9410
|
if (gitWorkflow.step === "add") {
|
|
7991
9411
|
addGitWorkflowAction("Run git add .", () => runGitAdd(), "primary", false);
|
|
7992
9412
|
} else if (gitWorkflow.step === "generate") {
|
|
9413
|
+
renderGitWorkflowManualCommitInput();
|
|
7993
9414
|
addGitWorkflowAction("Run /git-staged-msg", () => runGitMessagePrompt(), "primary", false);
|
|
7994
9415
|
addGitWorkflowAction("Preview current message files", () => loadGitWorkflowMessage({ requireFresh: false }), "", false);
|
|
7995
9416
|
} else if (gitWorkflow.step === "generating") {
|
|
@@ -7999,6 +9420,7 @@ function renderGitWorkflow() {
|
|
|
7999
9420
|
addGitWorkflowAction("Create PR", () => createGitPrBranch(), "primary", false, GIT_WORKFLOW_CREATE_PR_TOOLTIP);
|
|
8000
9421
|
addGitWorkflowAction("Manual branch", () => createGitPrBranchManually(), "", false, GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP);
|
|
8001
9422
|
}
|
|
9423
|
+
renderGitWorkflowManualCommitInput();
|
|
8002
9424
|
addGitWorkflowAction("Commit short", () => commitGitWorkflow("short"), gitWorkflow.prMode ? "primary" : "", false);
|
|
8003
9425
|
addGitWorkflowAction("Commit long", () => commitGitWorkflow("long"), gitWorkflow.prMode ? "primary" : "", false);
|
|
8004
9426
|
addGitWorkflowAction("Regenerate", () => runGitMessagePrompt(), "", false);
|
|
@@ -8029,7 +9451,15 @@ function renderGitWorkflow() {
|
|
|
8029
9451
|
async function gitWorkflowRequest(path, { method = "POST", body = {}, runId, tabId = activeTabId } = {}) {
|
|
8030
9452
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
8031
9453
|
const expectedRunId = runId ?? workflow?.runId;
|
|
8032
|
-
|
|
9454
|
+
let response;
|
|
9455
|
+
try {
|
|
9456
|
+
response = await api(path, method === "GET" ? { method, tabId } : { method, body, tabId });
|
|
9457
|
+
} catch (error) {
|
|
9458
|
+
if (error?.statusCode === 404 && path.startsWith("/api/git-workflow/")) {
|
|
9459
|
+
throw new Error("Git workflow endpoint not found. Restart Pi Web UI so the browser and backend both load the latest git repository setup endpoints.");
|
|
9460
|
+
}
|
|
9461
|
+
throw error;
|
|
9462
|
+
}
|
|
8033
9463
|
if (expectedRunId !== undefined && !isCurrentGitWorkflowRun(expectedRunId, tabId)) return null;
|
|
8034
9464
|
if (!response.ok) {
|
|
8035
9465
|
const detail = response.data ? `\n\n${formatGitCommandResult(response.data)}` : "";
|
|
@@ -8065,12 +9495,62 @@ function startGitWorkflow(tabId = activeTabId) {
|
|
|
8065
9495
|
workflow.runId += 1;
|
|
8066
9496
|
setGitWorkflow({
|
|
8067
9497
|
active: true,
|
|
9498
|
+
mode: "standard",
|
|
8068
9499
|
step: "add",
|
|
8069
9500
|
process: "stage",
|
|
8070
9501
|
busy: false,
|
|
8071
|
-
output: "Ready to stage all changes with git add .\n\nNative mode is used for g-msg/g-short/g-long: dev/COMMIT message files are read directly and git commit is run without fish. After the message is generated, use Create PR to switch to a new branch before committing.",
|
|
9502
|
+
output: "Ready to stage all changes with git add .\n\nNative mode is used for g-msg/g-short/g-long: dev/COMMIT message files are read directly and git commit is run without fish. In the Message stage you can also type a commit message and use Commit input. After the message is generated, use Create PR to switch to a new branch before committing.",
|
|
9503
|
+
error: "",
|
|
9504
|
+
githubUsername: "",
|
|
9505
|
+
repoName: "",
|
|
9506
|
+
remoteUrl: "",
|
|
9507
|
+
stack: "",
|
|
9508
|
+
readmeRequestedAt: 0,
|
|
9509
|
+
gitignoreRequestedAt: 0,
|
|
9510
|
+
initFilesStatus: null,
|
|
9511
|
+
message: null,
|
|
9512
|
+
manualCommitMessage: "",
|
|
9513
|
+
...resetGitWorkflowManualCommitDefaultPatch(),
|
|
9514
|
+
messageRequestedAt: 0,
|
|
9515
|
+
branchName: "",
|
|
9516
|
+
branchNameRequestedAt: 0,
|
|
9517
|
+
actionsDone: createGitWorkflowActionsDone(),
|
|
9518
|
+
prMode: false,
|
|
9519
|
+
prBranch: "",
|
|
9520
|
+
pr: null,
|
|
9521
|
+
prRequestedAt: 0,
|
|
9522
|
+
}, { tabId });
|
|
9523
|
+
}
|
|
9524
|
+
|
|
9525
|
+
function startGitInitWorkflow(tabId = activeTabId) {
|
|
9526
|
+
if (!tabId) return;
|
|
9527
|
+
const workflow = gitWorkflowForTab(tabId);
|
|
9528
|
+
if (workflow.active && !["done", "cancelled", "error"].includes(workflow.step) && !confirm("Restart the active git repository setup workflow?")) return;
|
|
9529
|
+
const setup = readGitFooterStatusSetup().githubUsername ? readGitFooterStatusSetup() : configureGitFooterStatusSetup({ force: true });
|
|
9530
|
+
const githubUsername = setup?.githubUsername || "";
|
|
9531
|
+
const repoName = defaultGitInitRepoName(tabs.find((tab) => tab.id === tabId) || activeTab());
|
|
9532
|
+
const stack = readStoredGitInitStack();
|
|
9533
|
+
workflow.runId += 1;
|
|
9534
|
+
setGitWorkflow({
|
|
9535
|
+
active: true,
|
|
9536
|
+
mode: "initRepo",
|
|
9537
|
+
step: githubUsername ? "initRepo" : "initSetup",
|
|
9538
|
+
process: "init",
|
|
9539
|
+
busy: false,
|
|
9540
|
+
output: githubUsername
|
|
9541
|
+
? `Ready to initialize a Git repository.\n\n${gitInitWorkflowSetupSummary({ githubUsername, repoName, stack })}`
|
|
9542
|
+
: "No GitHub username stored yet. Run git-footer-status-setup to save the username for https://github.com/USERNAME/REPO_NAME.git.",
|
|
8072
9543
|
error: "",
|
|
9544
|
+
githubUsername,
|
|
9545
|
+
repoName,
|
|
9546
|
+
remoteUrl: githubUsername ? gitInitRemoteUrl(githubUsername, repoName) : "",
|
|
9547
|
+
stack,
|
|
9548
|
+
readmeRequestedAt: 0,
|
|
9549
|
+
gitignoreRequestedAt: 0,
|
|
9550
|
+
initFilesStatus: null,
|
|
8073
9551
|
message: null,
|
|
9552
|
+
manualCommitMessage: "",
|
|
9553
|
+
...resetGitWorkflowManualCommitDefaultPatch(),
|
|
8074
9554
|
messageRequestedAt: 0,
|
|
8075
9555
|
branchName: "",
|
|
8076
9556
|
branchNameRequestedAt: 0,
|
|
@@ -8082,11 +9562,269 @@ function startGitWorkflow(tabId = activeTabId) {
|
|
|
8082
9562
|
}, { tabId });
|
|
8083
9563
|
}
|
|
8084
9564
|
|
|
9565
|
+
async function runGitInitRepository(tabId = gitWorkflowActionTabId()) {
|
|
9566
|
+
const tabContext = activeTabContext(tabId);
|
|
9567
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
9568
|
+
if (!workflow) return;
|
|
9569
|
+
const runId = workflow.runId;
|
|
9570
|
+
setGitWorkflow({ step: "initializingRepo", busy: true, error: "", output: "Running git init…" }, { tabId });
|
|
9571
|
+
try {
|
|
9572
|
+
const result = await gitWorkflowRequest("/api/git-workflow/init", { runId, tabId });
|
|
9573
|
+
if (!result) return;
|
|
9574
|
+
const stack = workflow.stack || readStoredGitInitStack();
|
|
9575
|
+
setGitWorkflow({ step: "initStack", busy: false, stack, ...gitWorkflowActionDonePatch(workflow, "init"), output: `${formatGitCommandResult(result)}\n\nRepository initialized. Next: choose the stack used for .gitignore generation.\n\nCurrent stack: ${gitInitStackDisplay(stack)}` }, { tabId });
|
|
9576
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
9577
|
+
} catch (error) {
|
|
9578
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "initRepo", { tabId });
|
|
9579
|
+
}
|
|
9580
|
+
}
|
|
9581
|
+
|
|
9582
|
+
function gitInitFilesResultSummary(result) {
|
|
9583
|
+
const readme = result?.readme?.created
|
|
9584
|
+
? "Created README.md."
|
|
9585
|
+
: result?.readme?.exists
|
|
9586
|
+
? "README.md already existed; staged without overwriting."
|
|
9587
|
+
: "README.md was checked.";
|
|
9588
|
+
const gitignore = result?.gitignore?.created
|
|
9589
|
+
? `Created .gitignore${result.gitignore.source ? ` (${result.gitignore.source})` : ""}.`
|
|
9590
|
+
: result?.gitignore?.exists
|
|
9591
|
+
? ".gitignore already existed; staged without overwriting."
|
|
9592
|
+
: ".gitignore was checked.";
|
|
9593
|
+
return [readme, gitignore].join("\n");
|
|
9594
|
+
}
|
|
9595
|
+
|
|
9596
|
+
function gitInitReadmePromptMessage({ stack = "", status = null, repoName = "" } = {}) {
|
|
9597
|
+
const cleanStack = cleanGitInitStack(stack);
|
|
9598
|
+
return [
|
|
9599
|
+
"Create a useful README.md for this new repository.",
|
|
9600
|
+
"Write or update only README.md in the current repository root. Do not commit, push, or run git add.",
|
|
9601
|
+
repoName ? `Repository name: ${repoName}` : "",
|
|
9602
|
+
cleanStack
|
|
9603
|
+
? `User-provided stack: ${cleanStack}`
|
|
9604
|
+
: "No user stack was provided. Inspect the codebase (package manifests, lockfiles, framework configs, file extensions, build files) and infer the project purpose/stack.",
|
|
9605
|
+
!cleanStack && status?.detectedStack ? `Web UI detected stack hint: ${status.detectedStack}` : "",
|
|
9606
|
+
"Include a clear title, short description, basic setup/install instructions, common development commands, and usage notes that match the detected repository.",
|
|
9607
|
+
"If the repository is mostly empty or unclear, create a concise README with placeholders/TODOs rather than inventing unsupported project details.",
|
|
9608
|
+
].filter(Boolean).join("\n");
|
|
9609
|
+
}
|
|
9610
|
+
|
|
9611
|
+
function gitInitGitignorePromptMessage({ stack = "", status = null, repoName = "" } = {}) {
|
|
9612
|
+
const cleanStack = cleanGitInitStack(stack);
|
|
9613
|
+
return [
|
|
9614
|
+
"Create a practical .gitignore file for this repository initialization.",
|
|
9615
|
+
"Write or update only .gitignore in the current repository root. Do not commit, push, or run git add.",
|
|
9616
|
+
repoName ? `Repository name: ${repoName}` : "",
|
|
9617
|
+
cleanStack
|
|
9618
|
+
? `User-provided stack: ${cleanStack}`
|
|
9619
|
+
: "No user stack was provided. Inspect the codebase (package manifests, lockfiles, framework configs, file extensions, build files) and infer the stack.",
|
|
9620
|
+
!cleanStack && status?.detectedStack ? `Web UI detected stack hint: ${status.detectedStack}` : "",
|
|
9621
|
+
"If the stack is still unclear, use sane defaults for OS/editor files, local env/secrets, dependency folders, build outputs, logs, caches, coverage, and temporary files.",
|
|
9622
|
+
"Keep useful generated/project files trackable; do not ignore source files or lockfiles by default.",
|
|
9623
|
+
].filter(Boolean).join("\n");
|
|
9624
|
+
}
|
|
9625
|
+
|
|
9626
|
+
async function promptGitInitReadme(status, { runId, tabId }) {
|
|
9627
|
+
const tabContext = activeTabContext(tabId);
|
|
9628
|
+
const targetTab = tabs.find((tab) => tab.id === tabId);
|
|
9629
|
+
const targetBusy = tabId === activeTabId ? !!currentState?.isStreaming : activityForTab(targetTab).isWorking;
|
|
9630
|
+
if (targetBusy) {
|
|
9631
|
+
failGitWorkflow(new Error("Pi is currently running. Wait for it to finish or abort before filling out README.md."), "readme", { tabId });
|
|
9632
|
+
return;
|
|
9633
|
+
}
|
|
9634
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
9635
|
+
if (!workflow) return;
|
|
9636
|
+
const requestedAt = Date.now();
|
|
9637
|
+
const repoName = workflow.repoName || defaultGitInitRepoName();
|
|
9638
|
+
const stack = workflow.stack || "";
|
|
9639
|
+
setGitWorkflow({
|
|
9640
|
+
step: "readmeGenerating",
|
|
9641
|
+
busy: true,
|
|
9642
|
+
error: "",
|
|
9643
|
+
readmeRequestedAt: requestedAt,
|
|
9644
|
+
initFilesStatus: status,
|
|
9645
|
+
output: `${gitInitFilesStatusSummary(status)}\n\nSending README.md fill-out request to Pi.\n\nStack: ${gitInitStackDisplay(stack)}\nCancel will request Pi abort.`,
|
|
9646
|
+
}, { tabId });
|
|
9647
|
+
if (isCurrentTabContext(tabContext)) setRunIndicatorActivity("Sending README.md fill-out request to Pi…");
|
|
9648
|
+
try {
|
|
9649
|
+
await api("/api/prompt", { method: "POST", body: { message: gitInitReadmePromptMessage({ stack, status, repoName }) }, tabId });
|
|
9650
|
+
if (!isCurrentGitWorkflowRun(runId, tabId)) return;
|
|
9651
|
+
appendGitWorkflowOutput("README.md request accepted. Waiting for agent_end, then README.md/.gitignore will be checked and staged. If Pi does not create README.md, Web UI will use a minimal fallback README.", { tabId });
|
|
9652
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshState(120, tabContext);
|
|
9653
|
+
setTimeout(() => {
|
|
9654
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
9655
|
+
const targetStillBusy = tabId === activeTabId && currentState?.isStreaming;
|
|
9656
|
+
if (isCurrentGitWorkflowRun(runId, tabId) && currentWorkflow?.step === "readmeGenerating" && !targetStillBusy) {
|
|
9657
|
+
prepareGitInitFiles({ afterReadmePrompt: true, runId, tabId });
|
|
9658
|
+
}
|
|
9659
|
+
}, 2500);
|
|
9660
|
+
} catch (error) {
|
|
9661
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) {
|
|
9662
|
+
if (isCurrentTabContext(tabContext)) clearRunIndicatorActivity();
|
|
9663
|
+
failGitWorkflow(error, "readme", { tabId });
|
|
9664
|
+
}
|
|
9665
|
+
}
|
|
9666
|
+
}
|
|
9667
|
+
|
|
9668
|
+
async function promptGitInitGitignore(status, { runId, tabId }) {
|
|
9669
|
+
const tabContext = activeTabContext(tabId);
|
|
9670
|
+
const targetTab = tabs.find((tab) => tab.id === tabId);
|
|
9671
|
+
const targetBusy = tabId === activeTabId ? !!currentState?.isStreaming : activityForTab(targetTab).isWorking;
|
|
9672
|
+
if (targetBusy) {
|
|
9673
|
+
failGitWorkflow(new Error("Pi is currently running. Wait for it to finish or abort before generating .gitignore."), "readme", { tabId });
|
|
9674
|
+
return;
|
|
9675
|
+
}
|
|
9676
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
9677
|
+
if (!workflow) return;
|
|
9678
|
+
const requestedAt = Date.now();
|
|
9679
|
+
const repoName = workflow.repoName || defaultGitInitRepoName();
|
|
9680
|
+
const stack = workflow.stack || "";
|
|
9681
|
+
setGitWorkflow({
|
|
9682
|
+
step: "gitignoreGenerating",
|
|
9683
|
+
busy: true,
|
|
9684
|
+
error: "",
|
|
9685
|
+
gitignoreRequestedAt: requestedAt,
|
|
9686
|
+
initFilesStatus: status,
|
|
9687
|
+
output: `${gitInitFilesStatusSummary(status)}\n\nSending .gitignore generation request to Pi.\n\nStack: ${gitInitStackDisplay(stack)}\nCancel will request Pi abort.`,
|
|
9688
|
+
}, { tabId });
|
|
9689
|
+
if (isCurrentTabContext(tabContext)) setRunIndicatorActivity("Sending .gitignore generation request to Pi…");
|
|
9690
|
+
try {
|
|
9691
|
+
await api("/api/prompt", { method: "POST", body: { message: gitInitGitignorePromptMessage({ stack, status, repoName }) }, tabId });
|
|
9692
|
+
if (!isCurrentGitWorkflowRun(runId, tabId)) return;
|
|
9693
|
+
appendGitWorkflowOutput(".gitignore request accepted. Waiting for agent_end, then README.md/.gitignore will be staged. If Pi does not create .gitignore, Web UI will use sane fallback patterns.", { tabId });
|
|
9694
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshState(120, tabContext);
|
|
9695
|
+
setTimeout(() => {
|
|
9696
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
9697
|
+
const targetStillBusy = tabId === activeTabId && currentState?.isStreaming;
|
|
9698
|
+
if (isCurrentGitWorkflowRun(runId, tabId) && currentWorkflow?.step === "gitignoreGenerating" && !targetStillBusy) {
|
|
9699
|
+
prepareGitInitFiles({ afterGitignorePrompt: true, runId, tabId });
|
|
9700
|
+
}
|
|
9701
|
+
}, 2500);
|
|
9702
|
+
} catch (error) {
|
|
9703
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) {
|
|
9704
|
+
if (isCurrentTabContext(tabContext)) clearRunIndicatorActivity();
|
|
9705
|
+
failGitWorkflow(error, "readme", { tabId });
|
|
9706
|
+
}
|
|
9707
|
+
}
|
|
9708
|
+
}
|
|
9709
|
+
|
|
9710
|
+
async function prepareGitInitFiles({ afterReadmePrompt = false, afterGitignorePrompt = false, runId, tabId = gitWorkflowActionTabId() } = {}) {
|
|
9711
|
+
const tabContext = activeTabContext(tabId);
|
|
9712
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
9713
|
+
if (!workflow) return;
|
|
9714
|
+
const expectedRunId = runId ?? workflow.runId;
|
|
9715
|
+
const repoName = workflow.repoName || defaultGitInitRepoName();
|
|
9716
|
+
const stack = cleanGitInitStack(workflow.stack || readStoredGitInitStack());
|
|
9717
|
+
setGitWorkflow({ step: "readmeCreating", busy: true, error: "", output: afterReadmePrompt || afterGitignorePrompt ? "Checking generated files, preparing README.md/.gitignore, and staging files…" : "Checking whether README.md and .gitignore already exist…" }, { tabId });
|
|
9718
|
+
try {
|
|
9719
|
+
const status = await gitWorkflowRequest("/api/git-workflow/init-files-status", { method: "GET", runId: expectedRunId, tabId });
|
|
9720
|
+
if (!status) return;
|
|
9721
|
+
setGitWorkflow({ initFilesStatus: status, output: `${gitInitFilesStatusSummary(status)}\n\nStack: ${gitInitStackDisplay(stack)}` }, { tabId });
|
|
9722
|
+
if (!status.readmeExists && !afterReadmePrompt) {
|
|
9723
|
+
await promptGitInitReadme(status, { runId: expectedRunId, tabId });
|
|
9724
|
+
return;
|
|
9725
|
+
}
|
|
9726
|
+
if (!status.gitignoreExists && !afterGitignorePrompt) {
|
|
9727
|
+
await promptGitInitGitignore(status, { runId: expectedRunId, tabId });
|
|
9728
|
+
return;
|
|
9729
|
+
}
|
|
9730
|
+
const result = await gitWorkflowRequest("/api/git-workflow/readme", { body: { repoName, stack }, runId: expectedRunId, tabId });
|
|
9731
|
+
if (!result) return;
|
|
9732
|
+
const current = gitWorkflowForTab(tabId, { create: false }) || workflow;
|
|
9733
|
+
setGitWorkflow({
|
|
9734
|
+
step: "initialCommit",
|
|
9735
|
+
busy: false,
|
|
9736
|
+
repoName,
|
|
9737
|
+
stack,
|
|
9738
|
+
initFilesStatus: status,
|
|
9739
|
+
actionsDone: createGitWorkflowActionsDone({ ...current.actionsDone, readme: true, gitignore: true }),
|
|
9740
|
+
output: `${gitInitFilesStatusSummary(status)}\n\n${formatGitCommandResult(result)}\n\n${gitInitFilesResultSummary(result)}\nNext: commit the initial files.`,
|
|
9741
|
+
}, { tabId });
|
|
9742
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
9743
|
+
} catch (error) {
|
|
9744
|
+
if (isCurrentGitWorkflowRun(expectedRunId, tabId)) failGitWorkflow(error, "readme", { tabId });
|
|
9745
|
+
}
|
|
9746
|
+
}
|
|
9747
|
+
|
|
9748
|
+
async function commitGitInitialReadme(tabId = gitWorkflowActionTabId()) {
|
|
9749
|
+
const tabContext = activeTabContext(tabId);
|
|
9750
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
9751
|
+
if (!workflow) return;
|
|
9752
|
+
const runId = workflow.runId;
|
|
9753
|
+
setGitWorkflow({ step: "initialCommitting", busy: true, error: "", output: "Running git commit -m \"Initial commit\"…" }, { tabId });
|
|
9754
|
+
try {
|
|
9755
|
+
const result = await gitWorkflowRequest("/api/git-workflow/initial-commit", { runId, tabId });
|
|
9756
|
+
if (!result) return;
|
|
9757
|
+
setGitWorkflow({ step: "mainBranch", busy: false, ...gitWorkflowActionDonePatch(workflow, "commit"), output: `${formatGitCommandResult(result)}\n\nInitial commit created. Next: rename the branch to main.` }, { tabId });
|
|
9758
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
9759
|
+
} catch (error) {
|
|
9760
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "initialCommit", { tabId });
|
|
9761
|
+
}
|
|
9762
|
+
}
|
|
9763
|
+
|
|
9764
|
+
async function branchGitInitMain(tabId = gitWorkflowActionTabId()) {
|
|
9765
|
+
const tabContext = activeTabContext(tabId);
|
|
9766
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
9767
|
+
if (!workflow) return;
|
|
9768
|
+
const runId = workflow.runId;
|
|
9769
|
+
setGitWorkflow({ step: "mainBranching", busy: true, error: "", output: "Running git branch -M main…" }, { tabId });
|
|
9770
|
+
try {
|
|
9771
|
+
const result = await gitWorkflowRequest("/api/git-workflow/main-branch", { runId, tabId });
|
|
9772
|
+
if (!result) return;
|
|
9773
|
+
setGitWorkflow({ step: "remote", busy: false, ...gitWorkflowActionDonePatch(workflow, "branch"), output: `${formatGitCommandResult(result)}\n\nBranch is main. Next: add origin.\n\n${gitInitWorkflowSetupSummary(gitWorkflowForTab(tabId, { create: false }) || workflow)}` }, { tabId });
|
|
9774
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
9775
|
+
} catch (error) {
|
|
9776
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "mainBranch", { tabId });
|
|
9777
|
+
}
|
|
9778
|
+
}
|
|
9779
|
+
|
|
9780
|
+
async function addGitInitRemote(tabId = gitWorkflowActionTabId()) {
|
|
9781
|
+
const tabContext = activeTabContext(tabId);
|
|
9782
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
9783
|
+
if (!workflow) return;
|
|
9784
|
+
let details;
|
|
9785
|
+
try {
|
|
9786
|
+
details = ensureGitInitRemoteDetails(tabId);
|
|
9787
|
+
} catch (error) {
|
|
9788
|
+
addEvent(error.message || String(error), "error");
|
|
9789
|
+
return;
|
|
9790
|
+
}
|
|
9791
|
+
if (!details) {
|
|
9792
|
+
setGitWorkflow({ step: "remote", busy: false, output: `Origin setup cancelled.\n\n${gitInitWorkflowSetupSummary(workflow)}` }, { tabId });
|
|
9793
|
+
return;
|
|
9794
|
+
}
|
|
9795
|
+
const runId = workflow.runId;
|
|
9796
|
+
setGitWorkflow({ step: "remoteAdding", busy: true, error: "", githubUsername: details.username, repoName: details.repoName, remoteUrl: details.remoteUrl, output: `Running git remote add origin ${details.remoteUrl}…` }, { tabId });
|
|
9797
|
+
try {
|
|
9798
|
+
const result = await gitWorkflowRequest("/api/git-workflow/remote", { body: details, runId, tabId });
|
|
9799
|
+
if (!result) return;
|
|
9800
|
+
setGitWorkflow({ step: "initialPush", busy: false, ...gitWorkflowActionDonePatch(workflow, "remote"), output: `${formatGitCommandResult(result)}\n\nOrigin added: ${result.remoteUrl || details.remoteUrl}\nNext: git push -u origin main.` }, { tabId });
|
|
9801
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
9802
|
+
} catch (error) {
|
|
9803
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "remote", { tabId });
|
|
9804
|
+
}
|
|
9805
|
+
}
|
|
9806
|
+
|
|
9807
|
+
async function pushGitInitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
9808
|
+
const tabContext = activeTabContext(tabId);
|
|
9809
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
9810
|
+
if (!workflow) return;
|
|
9811
|
+
const runId = workflow.runId;
|
|
9812
|
+
setGitWorkflow({ step: "initialPushing", busy: true, error: "", output: "Running git push -u origin main…" }, { tabId });
|
|
9813
|
+
try {
|
|
9814
|
+
const result = await gitWorkflowRequest("/api/git-workflow/init-push", { runId, tabId });
|
|
9815
|
+
if (!result) return;
|
|
9816
|
+
setGitWorkflow({ step: "done", busy: false, ...gitWorkflowActionDonePatch(workflow, "push"), output: `${formatGitCommandResult(result)}\n\nInitial repository pushed to origin/main.` }, { tabId });
|
|
9817
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
9818
|
+
} catch (error) {
|
|
9819
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "initialPush", { tabId });
|
|
9820
|
+
}
|
|
9821
|
+
}
|
|
9822
|
+
|
|
8085
9823
|
async function cancelGitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
8086
9824
|
const tabContext = activeTabContext(tabId);
|
|
8087
9825
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
8088
9826
|
if (!workflow?.active) return;
|
|
8089
|
-
const shouldAbortPi = workflow.step === "generating" || workflow.step === "branchNaming" || workflow.step === "prGenerating";
|
|
9827
|
+
const shouldAbortPi = workflow.step === "generating" || workflow.step === "branchNaming" || workflow.step === "prGenerating" || workflow.step === "readmeGenerating" || workflow.step === "gitignoreGenerating";
|
|
8090
9828
|
if (activeGitPrDialogResolve) resolveGitPrDialog(null);
|
|
8091
9829
|
workflow.runId += 1;
|
|
8092
9830
|
setGitWorkflow({ step: "cancelled", busy: false, error: "", output: `${workflow.output || ""}${workflow.output ? "\n\n" : ""}Cancelled by user.` }, { tabId });
|
|
@@ -8103,17 +9841,47 @@ async function runGitAdd(tabId = gitWorkflowActionTabId()) {
|
|
|
8103
9841
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
8104
9842
|
if (!workflow) return;
|
|
8105
9843
|
const runId = workflow.runId;
|
|
8106
|
-
setGitWorkflow({ step: "add", busy: true, error: "", output: "Running git add ." }, { tabId });
|
|
9844
|
+
setGitWorkflow({ step: "add", busy: true, error: "", ...resetGitWorkflowManualCommitDefaultPatch(), output: "Running git add ." }, { tabId });
|
|
8107
9845
|
try {
|
|
8108
9846
|
const result = await gitWorkflowRequest("/api/git-workflow/add", { runId, tabId });
|
|
8109
9847
|
if (!result) return;
|
|
8110
|
-
setGitWorkflow({ step: "generate", busy: false, ...gitWorkflowActionDonePatch(workflow, "stage"), output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` }, { tabId });
|
|
9848
|
+
setGitWorkflow({ step: "generate", busy: false, ...resetGitWorkflowManualCommitDefaultPatch(), ...gitWorkflowActionDonePatch(workflow, "stage"), output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` }, { tabId });
|
|
8111
9849
|
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
8112
9850
|
} catch (error) {
|
|
8113
9851
|
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "add", { tabId });
|
|
8114
9852
|
}
|
|
8115
9853
|
}
|
|
8116
9854
|
|
|
9855
|
+
async function loadGitWorkflowDefaultCommitMessage({ runId, tabId = activeTabId } = {}) {
|
|
9856
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
9857
|
+
const expectedRunId = runId ?? workflow?.runId;
|
|
9858
|
+
if (!workflow || workflow.manualCommitMessageDefaultLoading || workflow.manualCommitMessageDefaultRequestedAt) return;
|
|
9859
|
+
workflow.manualCommitMessageDefaultLoading = true;
|
|
9860
|
+
workflow.manualCommitMessageDefaultRequestedAt = Date.now();
|
|
9861
|
+
try {
|
|
9862
|
+
const data = await gitWorkflowRequest("/api/git-workflow/default-commit-message", { method: "GET", runId: expectedRunId, tabId });
|
|
9863
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
9864
|
+
if (!data || !currentWorkflow || !isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
9865
|
+
setGitWorkflow({
|
|
9866
|
+
manualCommitMessageDefault: String(data.message || "").trim(),
|
|
9867
|
+
manualCommitMessageDefaultReason: String(data.reason || ""),
|
|
9868
|
+
manualCommitMessageDefaultPath: String(data.path || ""),
|
|
9869
|
+
manualCommitMessageDefaultAction: String(data.action || ""),
|
|
9870
|
+
manualCommitMessageDefaultLoading: false,
|
|
9871
|
+
}, { tabId });
|
|
9872
|
+
} catch (error) {
|
|
9873
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
9874
|
+
if (!currentWorkflow || !isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
9875
|
+
setGitWorkflow({
|
|
9876
|
+
manualCommitMessageDefault: "",
|
|
9877
|
+
manualCommitMessageDefaultReason: error?.message || String(error),
|
|
9878
|
+
manualCommitMessageDefaultPath: "",
|
|
9879
|
+
manualCommitMessageDefaultAction: "",
|
|
9880
|
+
manualCommitMessageDefaultLoading: false,
|
|
9881
|
+
}, { tabId });
|
|
9882
|
+
}
|
|
9883
|
+
}
|
|
9884
|
+
|
|
8117
9885
|
async function runGitMessagePrompt(tabId = gitWorkflowActionTabId()) {
|
|
8118
9886
|
const tabContext = activeTabContext(tabId);
|
|
8119
9887
|
const targetTab = tabs.find((tab) => tab.id === tabId);
|
|
@@ -8300,7 +10068,7 @@ async function createGitPrBranchWithSuggestion(suggestion, tabId = gitWorkflowAc
|
|
|
8300
10068
|
try {
|
|
8301
10069
|
const result = await gitWorkflowRequest("/api/git-workflow/branch", { body: { branch }, runId, tabId });
|
|
8302
10070
|
if (!result) return;
|
|
8303
|
-
setGitWorkflow({ step: "message", prMode: true, prBranch: result.branch || branch, branchName: result.branch || branch, busy: false, output: `${formatGitCommandResult(result)}\n\nCreated PR branch ${result.branch || branch}. Choose Commit short or Commit
|
|
10071
|
+
setGitWorkflow({ step: "message", prMode: true, prBranch: result.branch || branch, branchName: result.branch || branch, busy: false, output: `${formatGitCommandResult(result)}\n\nCreated PR branch ${result.branch || branch}. Choose Commit short, Commit long, or Commit input to commit on this branch.` }, { tabId });
|
|
8304
10072
|
} catch (error) {
|
|
8305
10073
|
if (isCurrentGitWorkflowRun(runId, tabId)) {
|
|
8306
10074
|
setGitWorkflow({ prMode: false, prBranch: "" }, { tabId });
|
|
@@ -8314,15 +10082,26 @@ async function commitGitWorkflow(variant, tabId = gitWorkflowActionTabId()) {
|
|
|
8314
10082
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
8315
10083
|
if (!workflow) return;
|
|
8316
10084
|
const runId = workflow.runId;
|
|
8317
|
-
|
|
10085
|
+
const failureStep = variant === "input" && workflow.step === "generate" ? "generate" : "message";
|
|
10086
|
+
const inputMessage = variant === "input" ? gitWorkflowManualCommitInputMessage(workflow) : "";
|
|
10087
|
+
if (variant === "input" && !inputMessage) {
|
|
10088
|
+
failGitWorkflow(new Error("Type a commit message, or stage exactly one created/updated/deleted file to use the default."), failureStep, { tabId });
|
|
10089
|
+
return;
|
|
10090
|
+
}
|
|
10091
|
+
const preview = variant === "input" ? formatInputCommitMessagePreview(inputMessage) : formatCommitMessagePreview(workflow.message);
|
|
10092
|
+
setGitWorkflow({ step: "committing", busy: true, error: "", output: `${preview}\n\nRunning native ${variant} commit…` }, { tabId });
|
|
8318
10093
|
try {
|
|
8319
|
-
const
|
|
10094
|
+
const body = variant === "input" ? { variant, message: inputMessage } : { variant };
|
|
10095
|
+
const result = await gitWorkflowRequest("/api/git-workflow/commit", { body, runId, tabId });
|
|
8320
10096
|
if (!result) return;
|
|
8321
10097
|
const nextAction = workflow.prMode ? "Push and Create PR." : "git push.";
|
|
8322
|
-
|
|
10098
|
+
const donePatch = variant === "input"
|
|
10099
|
+
? { actionsDone: createGitWorkflowActionsDone({ ...workflow.actionsDone, message: true, commit: true }) }
|
|
10100
|
+
: gitWorkflowActionDonePatch(workflow, "commit");
|
|
10101
|
+
setGitWorkflow({ step: "push", busy: false, ...donePatch, output: `${formatGitCommandResult(result)}\n\nCommit created. Next: ${nextAction}` }, { tabId });
|
|
8323
10102
|
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
8324
10103
|
} catch (error) {
|
|
8325
|
-
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error,
|
|
10104
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, failureStep, { tabId });
|
|
8326
10105
|
}
|
|
8327
10106
|
}
|
|
8328
10107
|
|
|
@@ -8487,6 +10266,22 @@ function resumeGitWorkflowForActiveTab(tabContext = activeTabContext()) {
|
|
|
8487
10266
|
}
|
|
8488
10267
|
loadGitWorkflowPr({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId: workflowTabId });
|
|
8489
10268
|
}
|
|
10269
|
+
if (workflowTabId === tabContext.tabId && gitWorkflow.active && gitWorkflow.step === "readmeGenerating" && !currentState?.isStreaming) {
|
|
10270
|
+
const retryDelayMs = Math.max(0, 2500 - (Date.now() - (gitWorkflow.readmeRequestedAt || 0)));
|
|
10271
|
+
if (retryDelayMs > 0) {
|
|
10272
|
+
setTimeout(() => resumeGitWorkflowForActiveTab(tabContext), retryDelayMs);
|
|
10273
|
+
return;
|
|
10274
|
+
}
|
|
10275
|
+
prepareGitInitFiles({ afterReadmePrompt: true, runId: gitWorkflow.runId, tabId: workflowTabId });
|
|
10276
|
+
}
|
|
10277
|
+
if (workflowTabId === tabContext.tabId && gitWorkflow.active && gitWorkflow.step === "gitignoreGenerating" && !currentState?.isStreaming) {
|
|
10278
|
+
const retryDelayMs = Math.max(0, 2500 - (Date.now() - (gitWorkflow.gitignoreRequestedAt || 0)));
|
|
10279
|
+
if (retryDelayMs > 0) {
|
|
10280
|
+
setTimeout(() => resumeGitWorkflowForActiveTab(tabContext), retryDelayMs);
|
|
10281
|
+
return;
|
|
10282
|
+
}
|
|
10283
|
+
prepareGitInitFiles({ afterGitignorePrompt: true, runId: gitWorkflow.runId, tabId: workflowTabId });
|
|
10284
|
+
}
|
|
8490
10285
|
}
|
|
8491
10286
|
|
|
8492
10287
|
function normalizeQueuedMessages(event) {
|
|
@@ -9222,6 +11017,64 @@ function renderMarkdown(block, text) {
|
|
|
9222
11017
|
renderMarkdownInto(block, text);
|
|
9223
11018
|
}
|
|
9224
11019
|
|
|
11020
|
+
/**
|
|
11021
|
+
* Incremental renderer for streaming assistant markdown. The block-based
|
|
11022
|
+
* parser in renderMarkdownInto only ever closes a block at a blank line
|
|
11023
|
+
* outside a code fence, so everything before the last such boundary is
|
|
11024
|
+
* stable: it is parsed exactly once and its DOM is never rebuilt. Only the
|
|
11025
|
+
* open tail is re-parsed per streaming tick, keeping per-tick cost flat
|
|
11026
|
+
* instead of O(message length).
|
|
11027
|
+
*/
|
|
11028
|
+
let streamMarkdownState = null;
|
|
11029
|
+
|
|
11030
|
+
function streamingMarkdownStableBoundary(text) {
|
|
11031
|
+
const lines = text.split("\n");
|
|
11032
|
+
let inFence = false;
|
|
11033
|
+
let boundary = 0;
|
|
11034
|
+
let offset = 0;
|
|
11035
|
+
// Exclude the final line: it may still be streaming in.
|
|
11036
|
+
for (let index = 0; index < lines.length - 1; index += 1) {
|
|
11037
|
+
const line = lines[index];
|
|
11038
|
+
if (inFence) {
|
|
11039
|
+
if (/^\s*```\s*$/.test(line)) inFence = false;
|
|
11040
|
+
} else if (/^\s*```\s*[\w.+-]*\s*$/.test(line)) {
|
|
11041
|
+
inFence = true;
|
|
11042
|
+
}
|
|
11043
|
+
offset += line.length + 1;
|
|
11044
|
+
if (!inFence && !line.trim()) boundary = offset;
|
|
11045
|
+
}
|
|
11046
|
+
return boundary;
|
|
11047
|
+
}
|
|
11048
|
+
|
|
11049
|
+
function renderStreamingMarkdown(block, text) {
|
|
11050
|
+
let state = streamMarkdownState;
|
|
11051
|
+
if (!state || state.block !== block) {
|
|
11052
|
+
block.replaceChildren();
|
|
11053
|
+
state = streamMarkdownState = { block, stableText: "", tailNodes: [] };
|
|
11054
|
+
}
|
|
11055
|
+
if (!text.startsWith(state.stableText)) {
|
|
11056
|
+
// Earlier content changed retroactively (e.g. todo-progress stripping);
|
|
11057
|
+
// fall back to a full re-render for correctness.
|
|
11058
|
+
block.replaceChildren();
|
|
11059
|
+
state.stableText = "";
|
|
11060
|
+
state.tailNodes = [];
|
|
11061
|
+
}
|
|
11062
|
+
for (const node of state.tailNodes) node.remove();
|
|
11063
|
+
state.tailNodes = [];
|
|
11064
|
+
const boundary = streamingMarkdownStableBoundary(text);
|
|
11065
|
+
if (boundary > state.stableText.length) {
|
|
11066
|
+
renderMarkdownInto(block, text.slice(state.stableText.length, boundary));
|
|
11067
|
+
state.stableText = text.slice(0, boundary);
|
|
11068
|
+
}
|
|
11069
|
+
const tail = text.slice(state.stableText.length);
|
|
11070
|
+
if (tail.trim()) {
|
|
11071
|
+
const fragment = document.createDocumentFragment();
|
|
11072
|
+
renderMarkdownInto(fragment, tail);
|
|
11073
|
+
state.tailNodes = [...fragment.childNodes];
|
|
11074
|
+
block.append(fragment);
|
|
11075
|
+
}
|
|
11076
|
+
}
|
|
11077
|
+
|
|
9225
11078
|
function appendImage(parent, part) {
|
|
9226
11079
|
const wrapper = make("div", "image-block");
|
|
9227
11080
|
const img = document.createElement("img");
|
|
@@ -9852,6 +11705,7 @@ function stickyUserPromptViewportGap() {
|
|
|
9852
11705
|
|
|
9853
11706
|
function resetChatOutput() {
|
|
9854
11707
|
liveToolCards.clear();
|
|
11708
|
+
renderedTranscriptState = { epoch: "", entries: [] };
|
|
9855
11709
|
const preservedNodes = [];
|
|
9856
11710
|
if (elements.stickyUserPromptButton) preservedNodes.push(elements.stickyUserPromptButton);
|
|
9857
11711
|
if (runIndicatorBubble?.parentElement === elements.chat) preservedNodes.push(runIndicatorBubble);
|
|
@@ -10536,9 +12390,12 @@ function jumpToStickyUserPrompt() {
|
|
|
10536
12390
|
requestAnimationFrame(updateStickyUserPromptButton);
|
|
10537
12391
|
}
|
|
10538
12392
|
|
|
10539
|
-
function appendMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false, reusableToolCards = null } = {}) {
|
|
12393
|
+
function appendMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false, reusableToolCards = null, itemKey = "" } = {}) {
|
|
10540
12394
|
const reused = reuseToolExecutionBubble(reusableToolCards, message, { streaming, messageIndex, transient });
|
|
10541
|
-
if (reused)
|
|
12395
|
+
if (reused) {
|
|
12396
|
+
if (itemKey) reused.bubble.dataset.itemKey = itemKey;
|
|
12397
|
+
return reused;
|
|
12398
|
+
}
|
|
10542
12399
|
const role = String(message.role || "message");
|
|
10543
12400
|
const safeRole = role.replace(/[^a-z0-9_-]/gi, "");
|
|
10544
12401
|
const bubble = make("article", `message ${safeRole}${message.level ? ` ${message.level}` : ""}${streaming ? " streaming" : ""}${animateEntry ? " action-enter" : ""}`);
|
|
@@ -10547,6 +12404,7 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
10547
12404
|
bubble.dataset.messageIndex = String(messageIndex);
|
|
10548
12405
|
if (role === "user") bubble.dataset.userPrompt = "true";
|
|
10549
12406
|
}
|
|
12407
|
+
if (itemKey) bubble.dataset.itemKey = itemKey;
|
|
10550
12408
|
const isCollapsibleOutput = !streaming && (message.role === "toolResult" || message.role === "bashExecution" || message.role === "compactionSummary");
|
|
10551
12409
|
|
|
10552
12410
|
const hideMessageHeader = message.role === "assistant" && !isCollapsibleOutput;
|
|
@@ -10593,14 +12451,15 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
10593
12451
|
bubble.append(header, body);
|
|
10594
12452
|
}
|
|
10595
12453
|
attachMessageCopyButton(bubble, message, body);
|
|
12454
|
+
attachMessageEditRetryButton(bubble, message, messageIndex, { streaming, transient });
|
|
10596
12455
|
if (!streaming && !transient) renderActionFeedbackControls(bubble, message, messageIndex);
|
|
10597
12456
|
appendChatMessageBubble(bubble);
|
|
10598
12457
|
return { bubble, body };
|
|
10599
12458
|
}
|
|
10600
12459
|
|
|
10601
|
-
function appendTranscriptMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false, reusableToolCards = null } = {}) {
|
|
12460
|
+
function appendTranscriptMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false, reusableToolCards = null, itemKey = "" } = {}) {
|
|
10602
12461
|
if (streaming || transient || message?.role !== "assistant") {
|
|
10603
|
-
return appendMessage(message, { streaming, messageIndex, transient, animateEntry, reusableToolCards });
|
|
12462
|
+
return appendMessage(message, { streaming, messageIndex, transient, animateEntry, reusableToolCards, itemKey });
|
|
10604
12463
|
}
|
|
10605
12464
|
|
|
10606
12465
|
let finalOutput = null;
|
|
@@ -10630,6 +12489,7 @@ function appendTranscriptMessage(message, { streaming = false, messageIndex = -1
|
|
|
10630
12489
|
transient: false,
|
|
10631
12490
|
animateEntry: animateEntry && isActionTranscriptMessage(transcriptMessage),
|
|
10632
12491
|
reusableToolCards,
|
|
12492
|
+
itemKey,
|
|
10633
12493
|
});
|
|
10634
12494
|
if (transcriptMessage.role === "assistant") finalOutput = created;
|
|
10635
12495
|
});
|
|
@@ -10901,20 +12761,169 @@ function orderedTranscriptItems() {
|
|
|
10901
12761
|
return items.sort((a, b) => a.timestampMs - b.timestampMs || a.order - b.order);
|
|
10902
12762
|
}
|
|
10903
12763
|
|
|
10904
|
-
|
|
12764
|
+
/**
|
|
12765
|
+
* Keyed transcript reconciliation state. Each transcript item gets a stable
|
|
12766
|
+
* key plus a cheap content signature; renders reuse the longest common
|
|
12767
|
+
* prefix of unchanged items and only rebuild DOM from the first divergence
|
|
12768
|
+
* (typically the last one or two items), instead of rebuilding every bubble.
|
|
12769
|
+
*/
|
|
12770
|
+
let renderedTranscriptState = { epoch: "", entries: [] };
|
|
12771
|
+
|
|
12772
|
+
function transcriptRenderEpoch() {
|
|
12773
|
+
return `${activeTabId || ""}|${thinkingOutputVisible ? 1 : 0}`;
|
|
12774
|
+
}
|
|
12775
|
+
|
|
12776
|
+
function transcriptItemKey(item) {
|
|
12777
|
+
if (!item.transient) return `m:${item.messageIndex}`;
|
|
12778
|
+
if (item.messageIndex >= 0) return `t:${item.messageIndex}`;
|
|
12779
|
+
return `live:${item.message?.toolCallId || `o${item.order}`}`;
|
|
12780
|
+
}
|
|
12781
|
+
|
|
12782
|
+
function safeJsonLength(value) {
|
|
12783
|
+
if (value === undefined || value === null) return 0;
|
|
12784
|
+
try {
|
|
12785
|
+
return JSON.stringify(value).length;
|
|
12786
|
+
} catch {
|
|
12787
|
+
return -1;
|
|
12788
|
+
}
|
|
12789
|
+
}
|
|
12790
|
+
|
|
12791
|
+
function contentSignature(content) {
|
|
12792
|
+
if (content === undefined || content === null) return "";
|
|
12793
|
+
if (typeof content === "string") return `s${content.length}`;
|
|
12794
|
+
if (!Array.isArray(content)) return `o${safeJsonLength(content)}`;
|
|
12795
|
+
let sig = `a${content.length}`;
|
|
12796
|
+
for (const part of content) {
|
|
12797
|
+
if (!part || typeof part !== "object") {
|
|
12798
|
+
sig += ";x";
|
|
12799
|
+
continue;
|
|
12800
|
+
}
|
|
12801
|
+
const text = typeof part.text === "string" ? part.text
|
|
12802
|
+
: typeof part.thinking === "string" ? part.thinking
|
|
12803
|
+
: typeof part.data === "string" ? part.data
|
|
12804
|
+
: typeof part.content === "string" ? part.content : "";
|
|
12805
|
+
sig += `;${part.type || "?"}:${text.length}:${part.toolCallId || part.id || ""}`;
|
|
12806
|
+
}
|
|
12807
|
+
return sig;
|
|
12808
|
+
}
|
|
12809
|
+
|
|
12810
|
+
function toolCallLiveStateSignature(toolCallId) {
|
|
12811
|
+
if (!toolCallId) return "";
|
|
12812
|
+
const id = String(toolCallId);
|
|
12813
|
+
const result = toolResultForCallId(id);
|
|
12814
|
+
const run = liveToolRuns.get(id);
|
|
12815
|
+
let sig = "";
|
|
12816
|
+
if (result) sig += `r:${contentSignature(result.content)}:${result.isError ? "e" : ""}`;
|
|
12817
|
+
if (run) sig += `|l:${run.isPartial ? "p" : ""}${run.isError ? "e" : ""}:${run.endedAt || ""}:${contentSignature(run.result?.content)}:${safeJsonLength(run.arguments)}`;
|
|
12818
|
+
return sig;
|
|
12819
|
+
}
|
|
12820
|
+
|
|
12821
|
+
function actionFeedbackSignature(messageIndex) {
|
|
12822
|
+
const map = actionFeedbackByTab.get(activeTabId);
|
|
12823
|
+
if (!map?.size) return "";
|
|
12824
|
+
let sig = "";
|
|
12825
|
+
for (const entry of map.values()) {
|
|
12826
|
+
if (entry.messageIndex === messageIndex) sig += `${entry.key}=${entry.reaction};`;
|
|
12827
|
+
}
|
|
12828
|
+
return sig;
|
|
12829
|
+
}
|
|
12830
|
+
|
|
12831
|
+
// Cache of the message-object-derived part of an item signature. Session
|
|
12832
|
+
// messages are append-only and treated as immutable, and delta merges keep
|
|
12833
|
+
// previous object identities, so cached entries stay valid until a full
|
|
12834
|
+
// fetch replaces the array (when the cache simply rebuilds).
|
|
12835
|
+
const messageStaticSignatureCache = new WeakMap();
|
|
12836
|
+
|
|
12837
|
+
function messageStaticSignature(message) {
|
|
12838
|
+
const cacheable = message && typeof message === "object";
|
|
12839
|
+
if (cacheable) {
|
|
12840
|
+
const cached = messageStaticSignatureCache.get(message);
|
|
12841
|
+
if (cached !== undefined) return cached;
|
|
12842
|
+
}
|
|
12843
|
+
const sig = [
|
|
12844
|
+
message.role || "",
|
|
12845
|
+
String(message.timestamp || ""),
|
|
12846
|
+
message.level || "",
|
|
12847
|
+
String(message.title || ""),
|
|
12848
|
+
contentSignature(message.content),
|
|
12849
|
+
typeof message.command === "string" ? `c${message.command.length}` : "",
|
|
12850
|
+
typeof message.output === "string" ? `out${message.output.length}` : "",
|
|
12851
|
+
typeof message.summary === "string" ? `sum${message.summary.length}` : "",
|
|
12852
|
+
typeof message.thinking === "string" ? `th${message.thinking.length}` : "",
|
|
12853
|
+
];
|
|
12854
|
+
if (message.role === "toolExecution") {
|
|
12855
|
+
sig.push(
|
|
12856
|
+
message.live ? "live" : "",
|
|
12857
|
+
message.isPartial ? "p" : "",
|
|
12858
|
+
message.isError ? "e" : "",
|
|
12859
|
+
String(message.startedAt || ""),
|
|
12860
|
+
String(message.endedAt || ""),
|
|
12861
|
+
contentSignature(message.result?.content),
|
|
12862
|
+
String(safeJsonLength(message.arguments)),
|
|
12863
|
+
);
|
|
12864
|
+
}
|
|
12865
|
+
const joined = sig.join("|");
|
|
12866
|
+
if (cacheable) messageStaticSignatureCache.set(message, joined);
|
|
12867
|
+
return joined;
|
|
12868
|
+
}
|
|
12869
|
+
|
|
12870
|
+
function transcriptItemSignature(item) {
|
|
12871
|
+
const message = item.message || {};
|
|
12872
|
+
const sig = [messageStaticSignature(message)];
|
|
12873
|
+
if (message.role === "toolExecution") sig.push(toolCallLiveStateSignature(message.toolCallId));
|
|
12874
|
+
if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
12875
|
+
for (const part of message.content) {
|
|
12876
|
+
if (isAssistantToolCallPart(part)) sig.push(toolCallLiveStateSignature(assistantToolCallId(part)));
|
|
12877
|
+
}
|
|
12878
|
+
}
|
|
12879
|
+
if (!item.transient && item.messageIndex >= 0) sig.push(actionFeedbackSignature(item.messageIndex));
|
|
12880
|
+
return sig.join("|");
|
|
12881
|
+
}
|
|
12882
|
+
|
|
12883
|
+
function removeChatBubblesAfterPrefix(keptKeys) {
|
|
12884
|
+
for (const child of [...elements.chat.children]) {
|
|
12885
|
+
if (child === elements.stickyUserPromptButton || child === runIndicatorBubble) continue;
|
|
12886
|
+
const key = child.dataset?.itemKey;
|
|
12887
|
+
if (key && keptKeys.has(key)) continue;
|
|
12888
|
+
child.remove();
|
|
12889
|
+
}
|
|
12890
|
+
}
|
|
12891
|
+
|
|
12892
|
+
function pruneDisconnectedLiveToolCards() {
|
|
12893
|
+
for (const [id, bubble] of liveToolCards) {
|
|
12894
|
+
if (!bubble.isConnected) liveToolCards.delete(id);
|
|
12895
|
+
}
|
|
12896
|
+
}
|
|
12897
|
+
|
|
12898
|
+
function renderAllMessages({ preserveScroll = false, forceRebuild = false } = {}) {
|
|
10905
12899
|
const shouldFollow = !preserveScroll && (autoFollowChat || isChatNearBottom());
|
|
10906
12900
|
const previousScrollTop = elements.chat.scrollTop;
|
|
10907
|
-
const reusableToolCards = captureReusableToolCards();
|
|
10908
|
-
resetChatOutput();
|
|
10909
12901
|
const transcriptItems = orderedTranscriptItems();
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
|
|
12902
|
+
const epoch = transcriptRenderEpoch();
|
|
12903
|
+
const nextEntries = transcriptItems.map((item) => ({ item, key: transcriptItemKey(item), sig: transcriptItemSignature(item) }));
|
|
12904
|
+
let prefixLength = 0;
|
|
12905
|
+
if (!forceRebuild && epoch === renderedTranscriptState.epoch) {
|
|
12906
|
+
const previous = renderedTranscriptState.entries;
|
|
12907
|
+
const limit = Math.min(previous.length, nextEntries.length);
|
|
12908
|
+
while (prefixLength < limit && previous[prefixLength].key === nextEntries[prefixLength].key && previous[prefixLength].sig === nextEntries[prefixLength].sig) {
|
|
12909
|
+
prefixLength += 1;
|
|
12910
|
+
}
|
|
12911
|
+
}
|
|
12912
|
+
const reusableToolCards = captureReusableToolCards();
|
|
12913
|
+
if (prefixLength === 0) resetChatOutput();
|
|
12914
|
+
else removeChatBubblesAfterPrefix(new Set(nextEntries.slice(0, prefixLength).map((entry) => entry.key)));
|
|
12915
|
+
for (let index = prefixLength; index < nextEntries.length; index += 1) {
|
|
12916
|
+
const entry = nextEntries[index];
|
|
12917
|
+
appendTranscriptMessage(entry.item.message, {
|
|
12918
|
+
messageIndex: entry.item.messageIndex,
|
|
12919
|
+
transient: entry.item.transient,
|
|
12920
|
+
animateEntry: shouldAnimateActionEntry(entry.item),
|
|
10915
12921
|
reusableToolCards,
|
|
12922
|
+
itemKey: entry.key,
|
|
10916
12923
|
});
|
|
10917
12924
|
}
|
|
12925
|
+
pruneDisconnectedLiveToolCards();
|
|
12926
|
+
renderedTranscriptState = { epoch, entries: nextEntries.map(({ key, sig }) => ({ key, sig })) };
|
|
10918
12927
|
rememberActionEntries(transcriptItems);
|
|
10919
12928
|
applyToolOutputExpansionToDom();
|
|
10920
12929
|
renderRunIndicator({ scroll: false });
|
|
@@ -11248,6 +13257,30 @@ function resetOptionalFeatureAvailability() {
|
|
|
11248
13257
|
renderOptionalFeatureControls();
|
|
11249
13258
|
}
|
|
11250
13259
|
|
|
13260
|
+
function optionalFeaturePackageStatus(featureId) {
|
|
13261
|
+
return optionalFeaturePackageStatuses.get(featureId) || null;
|
|
13262
|
+
}
|
|
13263
|
+
|
|
13264
|
+
function optionalFeaturePackageVersionLabel(status) {
|
|
13265
|
+
if (!status?.installedVersion) return "";
|
|
13266
|
+
return status.declaredSpec ? `${status.installedVersion} (expects ${status.declaredSpec})` : status.installedVersion;
|
|
13267
|
+
}
|
|
13268
|
+
|
|
13269
|
+
async function refreshOptionalFeaturePackageStatuses({ announce = false } = {}) {
|
|
13270
|
+
try {
|
|
13271
|
+
const response = await api("/api/optional-features", { scoped: false });
|
|
13272
|
+
optionalFeaturePackageStatuses.clear();
|
|
13273
|
+
for (const status of response.data?.features || []) {
|
|
13274
|
+
if (status?.featureId) optionalFeaturePackageStatuses.set(status.featureId, status);
|
|
13275
|
+
}
|
|
13276
|
+
renderOptionalFeatureControls();
|
|
13277
|
+
return true;
|
|
13278
|
+
} catch (error) {
|
|
13279
|
+
if (announce) addEvent(`optional feature package status check failed: ${error.message || String(error)}`, "warn");
|
|
13280
|
+
return false;
|
|
13281
|
+
}
|
|
13282
|
+
}
|
|
13283
|
+
|
|
11251
13284
|
function requestGitFooterWebuiPayload(tabContext = activeTabContext(), { force = false } = {}) {
|
|
11252
13285
|
if (!tabContext.tabId || isOptionalFeatureDisabled("gitFooterStatus")) return;
|
|
11253
13286
|
if (currentState?.isStreaming || currentState?.isCompacting) return;
|
|
@@ -11287,9 +13320,16 @@ function updateOptionalFeatureAvailability() {
|
|
|
11287
13320
|
function optionalFeatureStatus(featureId) {
|
|
11288
13321
|
const detected = isOptionalFeatureDetected(featureId);
|
|
11289
13322
|
const disabled = isOptionalFeatureDisabled(featureId);
|
|
11290
|
-
|
|
11291
|
-
|
|
11292
|
-
|
|
13323
|
+
const packageStatus = optionalFeaturePackageStatus(featureId);
|
|
13324
|
+
const installMessage = optionalFeatureInstallMessages.get(featureId);
|
|
13325
|
+
const versionLabel = optionalFeaturePackageVersionLabel(packageStatus);
|
|
13326
|
+
const versionSuffix = versionLabel ? ` · package ${versionLabel}` : "";
|
|
13327
|
+
if (optionalFeatureInstallInProgress.has(featureId)) return { label: "Installing", className: "updating", detail: installMessage || "npm install is running; waiting for the package manager to finish" };
|
|
13328
|
+
if (packageStatus?.updateAvailable) return { label: "Update available", className: "updating", detail: packageStatus.updateReason || `Installed package is older than the Web UI expects${versionSuffix}` };
|
|
13329
|
+
if (detected && !disabled) return { label: "Enabled", className: "enabled", detail: `Detected and enabled in Web UI${versionSuffix}` };
|
|
13330
|
+
if (detected && disabled) return { label: "Disabled", className: "disabled", detail: `Detected, but disabled in Web UI${versionSuffix}` };
|
|
13331
|
+
if (packageStatus?.installed) return { label: "Installed", className: "installed", detail: `Package is installed but not loaded in the active Pi tab${versionSuffix}` };
|
|
13332
|
+
return { label: "Install needed", className: "missing", detail: installMessage || "Package is not installed or not visible from the Web UI package root" };
|
|
11293
13333
|
}
|
|
11294
13334
|
|
|
11295
13335
|
function optionalFeatureWidgetFeatureId(key) {
|
|
@@ -11308,6 +13348,7 @@ function renderOptionalFeaturePanel() {
|
|
|
11308
13348
|
const detected = isOptionalFeatureDetected(feature.id);
|
|
11309
13349
|
const enabled = isOptionalFeatureEnabled(feature.id);
|
|
11310
13350
|
const installing = optionalFeatureInstallInProgress.has(feature.id);
|
|
13351
|
+
const packageStatus = optionalFeaturePackageStatus(feature.id);
|
|
11311
13352
|
const status = optionalFeatureStatus(feature.id);
|
|
11312
13353
|
const row = make("div", `optional-feature-row ${status.className}`);
|
|
11313
13354
|
|
|
@@ -11319,21 +13360,40 @@ function renderOptionalFeaturePanel() {
|
|
|
11319
13360
|
const packageLine = make("code", "optional-feature-package", feature.packageName);
|
|
11320
13361
|
main.append(title, detail, description, packageLine);
|
|
11321
13362
|
|
|
13363
|
+
const actions = make("div", "optional-feature-actions");
|
|
13364
|
+
if (feature.id === "gitFooterStatus") {
|
|
13365
|
+
const setup = make("button", "optional-feature-action setup", "git-footer-status-setup");
|
|
13366
|
+
setup.type = "button";
|
|
13367
|
+
setup.title = GIT_FOOTER_STATUS_SETUP_TOOLTIP;
|
|
13368
|
+
setup.dataset.tooltip = GIT_FOOTER_STATUS_SETUP_TOOLTIP;
|
|
13369
|
+
setup.disabled = installing;
|
|
13370
|
+
setup.addEventListener("click", () => configureGitFooterStatusSetup({ force: true }));
|
|
13371
|
+
actions.append(setup);
|
|
13372
|
+
}
|
|
13373
|
+
|
|
11322
13374
|
const action = make("button", "optional-feature-action");
|
|
11323
13375
|
action.type = "button";
|
|
11324
13376
|
action.disabled = installing;
|
|
11325
13377
|
if (installing) {
|
|
11326
13378
|
action.textContent = "Installing…";
|
|
13379
|
+
} else if (packageStatus?.updateAvailable) {
|
|
13380
|
+
action.textContent = "Update…";
|
|
13381
|
+
action.classList.add("update");
|
|
13382
|
+
action.addEventListener("click", () => installOptionalFeature(feature.id, { update: true }));
|
|
11327
13383
|
} else if (detected) {
|
|
11328
13384
|
action.textContent = enabled ? "Disable" : "Enable";
|
|
11329
13385
|
action.addEventListener("click", () => setOptionalFeatureDisabled(feature.id, enabled));
|
|
13386
|
+
} else if (packageStatus?.installed) {
|
|
13387
|
+
action.textContent = "Reload";
|
|
13388
|
+
action.addEventListener("click", () => sendPrompt("prompt", "/reload"));
|
|
11330
13389
|
} else {
|
|
11331
13390
|
action.textContent = "Install…";
|
|
11332
13391
|
action.classList.add("install");
|
|
11333
13392
|
action.addEventListener("click", () => installOptionalFeature(feature.id));
|
|
11334
13393
|
}
|
|
13394
|
+
actions.append(action);
|
|
11335
13395
|
|
|
11336
|
-
row.append(main,
|
|
13396
|
+
row.append(main, actions);
|
|
11337
13397
|
elements.optionalFeaturesBox.append(row);
|
|
11338
13398
|
}
|
|
11339
13399
|
}
|
|
@@ -11373,6 +13433,16 @@ function renderOptionalFeatureControls() {
|
|
|
11373
13433
|
);
|
|
11374
13434
|
if (!hasNativeCommandMenu && nativeCommandMenuOpen) setNativeCommandMenuOpen(false);
|
|
11375
13435
|
|
|
13436
|
+
const hasStatsCommand = isOptionalFeatureEnabled("statsCommand");
|
|
13437
|
+
if (elements.optionsStatsButton) {
|
|
13438
|
+
elements.optionsStatsButton.hidden = !hasStatsCommand;
|
|
13439
|
+
setOptionalControlState(
|
|
13440
|
+
elements.optionsStatsButton,
|
|
13441
|
+
hasStatsCommand,
|
|
13442
|
+
optionalFeatureUnavailableMessage("statsCommand"),
|
|
13443
|
+
);
|
|
13444
|
+
}
|
|
13445
|
+
|
|
11376
13446
|
renderOptionalFeaturePanel();
|
|
11377
13447
|
}
|
|
11378
13448
|
|
|
@@ -11382,15 +13452,17 @@ function commandUnavailableMessage(commandName) {
|
|
|
11382
13452
|
return `Command unavailable: /${commandName} is not loaded in the active Pi tab.`;
|
|
11383
13453
|
}
|
|
11384
13454
|
|
|
11385
|
-
async function installOptionalFeature(featureId) {
|
|
13455
|
+
async function installOptionalFeature(featureId, { update = false } = {}) {
|
|
11386
13456
|
const feature = OPTIONAL_FEATURE_BY_ID.get(featureId);
|
|
11387
13457
|
if (!feature || optionalFeatureInstallInProgress.has(featureId)) return;
|
|
11388
13458
|
|
|
13459
|
+
const actionLabel = update ? "Update" : "Install";
|
|
11389
13460
|
const warning = [
|
|
11390
|
-
|
|
13461
|
+
`${actionLabel} optional feature: ${feature.label}?`,
|
|
11391
13462
|
"",
|
|
11392
13463
|
`This will run npm install for ${feature.packageName} in the Web UI package install root.`,
|
|
11393
13464
|
"It can download code from npm and modify the local Pi/Web UI npm installation.",
|
|
13465
|
+
"Progress and failures will be shown in the optional-features row and activity log.",
|
|
11394
13466
|
"If this feature is already installed but disabled in Pi settings, cancel and enable it there instead.",
|
|
11395
13467
|
"",
|
|
11396
13468
|
"Continue?",
|
|
@@ -11398,14 +13470,20 @@ async function installOptionalFeature(featureId) {
|
|
|
11398
13470
|
if (!confirm(warning)) return;
|
|
11399
13471
|
|
|
11400
13472
|
optionalFeatureInstallInProgress.add(featureId);
|
|
13473
|
+
optionalFeatureInstallMessages.set(featureId, `${actionLabel} running via npm; waiting for package-manager output…`);
|
|
11401
13474
|
renderOptionalFeatureControls();
|
|
11402
|
-
addEvent(
|
|
13475
|
+
addEvent(`${update ? "updating" : "installing"} optional feature ${feature.label} (${feature.packageName})…`, "warn");
|
|
11403
13476
|
try {
|
|
11404
13477
|
const response = await api("/api/optional-feature-install", { method: "POST", body: { featureId }, scoped: false });
|
|
11405
13478
|
disabledOptionalFeatures.delete(featureId);
|
|
11406
13479
|
storeDisabledOptionalFeatures();
|
|
11407
|
-
|
|
11408
|
-
|
|
13480
|
+
const command = response.data?.command ? ` · ${response.data.command}` : "";
|
|
13481
|
+
optionalFeatureInstallMessages.set(featureId, `${response.data?.message || `${actionLabel} finished`}${command}`);
|
|
13482
|
+
addEvent(response.data?.message || `${update ? "updated" : "installed"} ${feature.packageName}`, "info");
|
|
13483
|
+
const output = [response.data?.stderr, response.data?.stdout].filter(Boolean).join("\n").trim();
|
|
13484
|
+
if (output) addEvent(`npm output for ${feature.packageName}:\n${output.slice(-4000)}`, "info");
|
|
13485
|
+
await refreshOptionalFeaturePackageStatuses({ announce: true });
|
|
13486
|
+
if (confirm(`${feature.label} ${actionLabel.toLowerCase()} finished. Reload the active Pi tab now to enable newly loaded resources?`)) {
|
|
11409
13487
|
sendPrompt("prompt", "/reload");
|
|
11410
13488
|
} else {
|
|
11411
13489
|
const tabContext = activeTabContext();
|
|
@@ -11413,6 +13491,7 @@ async function installOptionalFeature(featureId) {
|
|
|
11413
13491
|
if (isCurrentTabContext(tabContext)) renderOptionalFeatureControls();
|
|
11414
13492
|
}
|
|
11415
13493
|
} catch (error) {
|
|
13494
|
+
optionalFeatureInstallMessages.set(featureId, `${actionLabel} failed: ${error.message || String(error)}`);
|
|
11416
13495
|
addEvent(error.message || String(error), "error");
|
|
11417
13496
|
} finally {
|
|
11418
13497
|
optionalFeatureInstallInProgress.delete(featureId);
|
|
@@ -12472,7 +14551,7 @@ function renderStreamingAssistantText() {
|
|
|
12472
14551
|
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
12473
14552
|
if (assistantText) {
|
|
12474
14553
|
ensureStreamBubble();
|
|
12475
|
-
|
|
14554
|
+
renderStreamingMarkdown(streamText, assistantText);
|
|
12476
14555
|
} else {
|
|
12477
14556
|
scheduleStreamBubbleHide();
|
|
12478
14557
|
}
|
|
@@ -12525,6 +14604,7 @@ function resetStreamBubble() {
|
|
|
12525
14604
|
streamBubble = null;
|
|
12526
14605
|
streamText = null;
|
|
12527
14606
|
streamRawText = "";
|
|
14607
|
+
streamMarkdownState = null;
|
|
12528
14608
|
streamBubbleVisibleSince = 0;
|
|
12529
14609
|
streamToolCallSeen = false;
|
|
12530
14610
|
streamThinkingBubble = null;
|
|
@@ -12678,6 +14758,8 @@ async function refreshStats(tabContext = activeTabContext()) {
|
|
|
12678
14758
|
if (!isCurrentTabContext(tabContext)) return;
|
|
12679
14759
|
latestStats = response.data || null;
|
|
12680
14760
|
renderFooter();
|
|
14761
|
+
renderContextMeter();
|
|
14762
|
+
renderWorkspaceDashboard();
|
|
12681
14763
|
}
|
|
12682
14764
|
|
|
12683
14765
|
async function refreshWorkspace(tabContext = activeTabContext()) {
|
|
@@ -12703,6 +14785,7 @@ async function refreshWorkspace(tabContext = activeTabContext()) {
|
|
|
12703
14785
|
latestWorkspace = nextWorkspace;
|
|
12704
14786
|
rememberServerStartCwd(nextWorkspace?.cwd);
|
|
12705
14787
|
renderFooter();
|
|
14788
|
+
renderWorkspaceDashboard();
|
|
12706
14789
|
}
|
|
12707
14790
|
|
|
12708
14791
|
function renderNetworkStatus() {
|
|
@@ -12776,11 +14859,53 @@ async function refreshFooterData(tabContext = activeTabContext()) {
|
|
|
12776
14859
|
await Promise.allSettled([refreshStats(tabContext), refreshWorkspace(tabContext)]);
|
|
12777
14860
|
}
|
|
12778
14861
|
|
|
14862
|
+
// Session key of the last applied transcript fetch; deltas are only
|
|
14863
|
+
// attempted while the tab+session is unchanged.
|
|
14864
|
+
let latestMessagesSessionKey = "";
|
|
14865
|
+
|
|
14866
|
+
function messagesLookEqual(a, b) {
|
|
14867
|
+
return !!a && !!b && a.role === b.role && String(a.timestamp || "") === String(b.timestamp || "")
|
|
14868
|
+
&& contentSignature(a.content) === contentSignature(b.content);
|
|
14869
|
+
}
|
|
14870
|
+
|
|
14871
|
+
/**
|
|
14872
|
+
* Merge a /api/messages?since= delta into the previous transcript. Returns
|
|
14873
|
+
* null whenever the delta cannot be applied safely — history shrank
|
|
14874
|
+
* (compaction), counts are inconsistent, or the one-message overlap no
|
|
14875
|
+
* longer matches (fork/resume/retroactive edit) — in which case the caller
|
|
14876
|
+
* falls back to a full fetch. Merged arrays keep previous message object
|
|
14877
|
+
* identities, which keeps the WeakMap signature cache hot.
|
|
14878
|
+
*/
|
|
14879
|
+
function mergeMessagesDelta(previous, data) {
|
|
14880
|
+
if (!data || !Array.isArray(data.messages)) return null;
|
|
14881
|
+
const since = Number(data.since);
|
|
14882
|
+
const totalCount = Number(data.totalCount);
|
|
14883
|
+
if (!Number.isInteger(since) || !Number.isInteger(totalCount)) return null;
|
|
14884
|
+
if (since > previous.length || totalCount < previous.length) return null;
|
|
14885
|
+
if (totalCount !== since + data.messages.length) return null;
|
|
14886
|
+
if (since < previous.length && !messagesLookEqual(previous[since], data.messages[0])) return null;
|
|
14887
|
+
return previous.slice(0, since).concat(data.messages);
|
|
14888
|
+
}
|
|
14889
|
+
|
|
12779
14890
|
async function refreshMessages(tabContext = activeTabContext()) {
|
|
12780
14891
|
if (!tabContext.tabId) return;
|
|
12781
|
-
const
|
|
12782
|
-
|
|
12783
|
-
|
|
14892
|
+
const previousMessages = latestMessages;
|
|
14893
|
+
const sessionKey = `${tabContext.tabId}|${currentState?.sessionId || ""}`;
|
|
14894
|
+
let nextMessages = null;
|
|
14895
|
+
if (previousMessages.length > 1 && sessionKey === latestMessagesSessionKey) {
|
|
14896
|
+
// Delta fetch with a one-message overlap: the last known message is
|
|
14897
|
+
// re-requested so retroactive changes are detected via mergeMessagesDelta.
|
|
14898
|
+
const response = await api(`/api/messages?since=${previousMessages.length - 1}`, { tabId: tabContext.tabId });
|
|
14899
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
14900
|
+
nextMessages = mergeMessagesDelta(previousMessages, response.data);
|
|
14901
|
+
}
|
|
14902
|
+
if (!nextMessages) {
|
|
14903
|
+
const response = await api("/api/messages", { tabId: tabContext.tabId });
|
|
14904
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
14905
|
+
nextMessages = response.data?.messages || [];
|
|
14906
|
+
}
|
|
14907
|
+
latestMessages = nextMessages;
|
|
14908
|
+
latestMessagesSessionKey = sessionKey;
|
|
12784
14909
|
const preserveLiveStream = liveStreamRenderActive();
|
|
12785
14910
|
if (!preserveLiveStream) resetStreamBubble();
|
|
12786
14911
|
renderMessages(latestMessages);
|
|
@@ -12821,6 +14946,7 @@ async function refreshModels(tabContext = activeTabContext()) {
|
|
|
12821
14946
|
syncModelSelectToState();
|
|
12822
14947
|
renderFooter();
|
|
12823
14948
|
renderFeedbackTray();
|
|
14949
|
+
if (elements.commandPaletteDialog?.open) renderCommandPalette();
|
|
12824
14950
|
}
|
|
12825
14951
|
|
|
12826
14952
|
function syncModelSelectToState() {
|
|
@@ -13297,6 +15423,162 @@ async function refreshCommands(tabContext = activeTabContext()) {
|
|
|
13297
15423
|
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
13298
15424
|
updateOptionalFeatureAvailability();
|
|
13299
15425
|
renderCommands();
|
|
15426
|
+
if (elements.commandPaletteDialog?.open) renderCommandPalette();
|
|
15427
|
+
}
|
|
15428
|
+
|
|
15429
|
+
function paletteText(value) {
|
|
15430
|
+
return String(value || "").toLowerCase();
|
|
15431
|
+
}
|
|
15432
|
+
|
|
15433
|
+
function paletteItemMatches(item, query) {
|
|
15434
|
+
const text = [item.label, item.description, item.kind, item.keywords].map(paletteText).join(" ");
|
|
15435
|
+
return query.split(/\s+/).filter(Boolean).every((token) => text.includes(token));
|
|
15436
|
+
}
|
|
15437
|
+
|
|
15438
|
+
function commandPaletteCoreItems() {
|
|
15439
|
+
const items = [
|
|
15440
|
+
{ kind: "Action", label: "New tab", description: "Start an isolated Pi terminal in the current directory", keywords: "workspace session", run: () => createTerminalTab() },
|
|
15441
|
+
{ kind: "Action", label: "Choose directory for new tab", description: "Pick a cwd before starting a tab", keywords: "cwd folder workspace", run: () => createTerminalTabFromChosenDirectory({ triggerButton: elements.commandPaletteButton }) },
|
|
15442
|
+
{ kind: "Action", label: "New session", description: "Start a fresh session in the active tab", keywords: "/new clear", run: () => elements.newSessionButton.click() },
|
|
15443
|
+
{ kind: "Action", label: "Compact context", description: contextUsageDetail(), keywords: "/compact context window tokens", run: () => requestManualCompaction() },
|
|
15444
|
+
{ kind: "Action", label: footerAutoCompactionEnabled() ? "Disable auto-compaction" : "Enable auto-compaction", description: footerAutoCompactionToggleAction(), keywords: "context automatic", run: () => toggleFooterAutoCompaction() },
|
|
15445
|
+
{ kind: "Action", label: workspaceDashboardCollapsed ? "Show workspace dashboard" : "Hide workspace dashboard", description: "Toggle the launch/workspace overview", keywords: "home overview", run: () => setWorkspaceDashboardCollapsed(!workspaceDashboardCollapsed) },
|
|
15446
|
+
{ kind: "Action", label: document.body.classList.contains("side-panel-collapsed") ? "Show side panel" : "Hide side panel", description: "Toggle the Control Deck", keywords: "controls settings", run: () => setSidePanelCollapsed(!document.body.classList.contains("side-panel-collapsed"), { focusPanel: true }) },
|
|
15447
|
+
{ kind: "Action", label: "Change working directory", description: "Restart active tab in another cwd", keywords: "cwd folder workspace", run: () => changeActiveTabCwd() },
|
|
15448
|
+
{ kind: "Action", label: "Search transcript", description: "Open transcript search", keywords: "find", run: () => openChatSearch() },
|
|
15449
|
+
{ kind: "Pi", label: "/model", description: "Select the active model", keywords: "provider llm", run: () => runNativeCommandMenu("/model") },
|
|
15450
|
+
{ kind: "Pi", label: "/resume", description: "Resume a previous session", keywords: "sessions history", run: () => runNativeCommandMenu("/resume") },
|
|
15451
|
+
{ kind: "Pi", label: "/fork", description: "Fork from a previous user message", keywords: "branch edit retry", run: () => runNativeCommandMenu("/fork") },
|
|
15452
|
+
{ kind: "Pi", label: "/tree", description: "Navigate the session tree", keywords: "branch history", run: () => runNativeCommandMenu("/tree") },
|
|
15453
|
+
{ kind: "Pi", label: "/settings", description: "Open settings", keywords: "configuration", run: () => runNativeCommandMenu("/settings") },
|
|
15454
|
+
{ kind: "Pi", label: "/scoped-models", description: "Manage model cycling scope", keywords: "models cycle ctrl p", run: () => runNativeCommandMenu("/scoped-models") },
|
|
15455
|
+
{ kind: "Pi", label: "/tools", description: "Manage active tools", keywords: "capabilities", run: () => runNativeCommandMenu("/tools") },
|
|
15456
|
+
{ kind: "Pi", label: "/skills", description: "Manage active skills", keywords: "system prompt", run: () => runNativeCommandMenu("/skills") },
|
|
15457
|
+
];
|
|
15458
|
+
if (isOptionalFeatureEnabled("statsCommand")) items.push({ kind: "Pi", label: "/stats-webui", description: "Open usage dashboard", keywords: "tokens cost budget", run: () => openStatsOverlay({ refresh: true }) });
|
|
15459
|
+
return items;
|
|
15460
|
+
}
|
|
15461
|
+
|
|
15462
|
+
function commandPaletteTabItems() {
|
|
15463
|
+
return tabs.map((tab) => {
|
|
15464
|
+
const indicator = tabIndicator(tab);
|
|
15465
|
+
return {
|
|
15466
|
+
kind: "Tab",
|
|
15467
|
+
label: tab.id === activeTabId ? `Current tab: ${tab.title}` : `Switch to tab: ${tab.title}`,
|
|
15468
|
+
description: `${indicator.label} · ${normalizeDisplayPath(tab.cwd || "")}`,
|
|
15469
|
+
keywords: `${tab.id} ${tab.cwd || ""}`,
|
|
15470
|
+
run: () => switchTab(tab.id),
|
|
15471
|
+
};
|
|
15472
|
+
});
|
|
15473
|
+
}
|
|
15474
|
+
|
|
15475
|
+
function commandPaletteModelItems() {
|
|
15476
|
+
return availableModels.map((model) => ({
|
|
15477
|
+
kind: "Model",
|
|
15478
|
+
label: `${model.provider}/${model.id}`,
|
|
15479
|
+
description: model.name || (model.contextWindow ? `context ${formatFooterTokenCount(model.contextWindow)}` : "Set active model"),
|
|
15480
|
+
keywords: `${model.provider} ${model.id} ${model.name || ""}`,
|
|
15481
|
+
run: async () => {
|
|
15482
|
+
const tabContext = activeTabContext();
|
|
15483
|
+
const response = await api("/api/model", { method: "POST", body: { provider: model.provider, modelId: model.id }, tabId: tabContext.tabId });
|
|
15484
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
15485
|
+
applyOptimisticModelSelection(response.data || model, tabContext);
|
|
15486
|
+
await refreshState(tabContext);
|
|
15487
|
+
await refreshModels(tabContext);
|
|
15488
|
+
},
|
|
15489
|
+
}));
|
|
15490
|
+
}
|
|
15491
|
+
|
|
15492
|
+
function commandPaletteSlashItems() {
|
|
15493
|
+
return visibleCommands().slice(0, 140).map((command) => ({
|
|
15494
|
+
kind: command.source || "Command",
|
|
15495
|
+
label: `/${command.name}`,
|
|
15496
|
+
description: command.description || "Run slash command",
|
|
15497
|
+
keywords: `${command.location || ""} ${command.path || ""}`,
|
|
15498
|
+
run: () => sendPrompt("prompt", `/${command.name}`),
|
|
15499
|
+
}));
|
|
15500
|
+
}
|
|
15501
|
+
|
|
15502
|
+
function buildCommandPaletteItems() {
|
|
15503
|
+
return [
|
|
15504
|
+
...commandPaletteCoreItems(),
|
|
15505
|
+
...commandPaletteTabItems(),
|
|
15506
|
+
...commandPaletteModelItems(),
|
|
15507
|
+
...commandPaletteSlashItems(),
|
|
15508
|
+
];
|
|
15509
|
+
}
|
|
15510
|
+
|
|
15511
|
+
function filteredCommandPaletteItems() {
|
|
15512
|
+
const query = paletteText(elements.commandPaletteInput?.value || "").trim();
|
|
15513
|
+
const items = buildCommandPaletteItems();
|
|
15514
|
+
return (query ? items.filter((item) => paletteItemMatches(item, query)) : items).slice(0, 80);
|
|
15515
|
+
}
|
|
15516
|
+
|
|
15517
|
+
function setCommandPaletteIndex(index) {
|
|
15518
|
+
const count = commandPaletteItems.length;
|
|
15519
|
+
commandPaletteIndex = count ? (index + count) % count : 0;
|
|
15520
|
+
renderCommandPaletteList();
|
|
15521
|
+
}
|
|
15522
|
+
|
|
15523
|
+
function renderCommandPaletteList() {
|
|
15524
|
+
const list = elements.commandPaletteList;
|
|
15525
|
+
if (!list) return;
|
|
15526
|
+
list.replaceChildren();
|
|
15527
|
+
if (!commandPaletteItems.length) {
|
|
15528
|
+
list.append(make("div", "command-palette-empty muted", "No matching actions."));
|
|
15529
|
+
return;
|
|
15530
|
+
}
|
|
15531
|
+
commandPaletteItems.forEach((item, index) => {
|
|
15532
|
+
const button = make("button", `command-palette-item${index === commandPaletteIndex ? " active" : ""}`);
|
|
15533
|
+
button.type = "button";
|
|
15534
|
+
button.setAttribute("role", "option");
|
|
15535
|
+
button.setAttribute("aria-selected", index === commandPaletteIndex ? "true" : "false");
|
|
15536
|
+
button.addEventListener("click", () => executeCommandPaletteItem(item));
|
|
15537
|
+
button.append(
|
|
15538
|
+
make("span", "command-palette-item-kind", item.kind || "Action"),
|
|
15539
|
+
make("span", "command-palette-item-label", item.label || "Untitled action"),
|
|
15540
|
+
make("span", "command-palette-item-description", item.description || ""),
|
|
15541
|
+
);
|
|
15542
|
+
list.append(button);
|
|
15543
|
+
});
|
|
15544
|
+
const active = list.children[commandPaletteIndex];
|
|
15545
|
+
active?.scrollIntoView({ block: "nearest" });
|
|
15546
|
+
}
|
|
15547
|
+
|
|
15548
|
+
function renderCommandPalette() {
|
|
15549
|
+
commandPaletteItems = filteredCommandPaletteItems();
|
|
15550
|
+
if (commandPaletteIndex >= commandPaletteItems.length) commandPaletteIndex = 0;
|
|
15551
|
+
renderCommandPaletteList();
|
|
15552
|
+
}
|
|
15553
|
+
|
|
15554
|
+
function openCommandPalette(initialQuery = "") {
|
|
15555
|
+
setComposerActionsOpen(false);
|
|
15556
|
+
setPublishMenuOpen(false);
|
|
15557
|
+
setNativeCommandMenuOpen(false);
|
|
15558
|
+
setAppRunnerMenuOpen(false);
|
|
15559
|
+
setOptionsMenuOpen(false);
|
|
15560
|
+
if (elements.commandPaletteInput) elements.commandPaletteInput.value = initialQuery;
|
|
15561
|
+
commandPaletteIndex = 0;
|
|
15562
|
+
renderCommandPalette();
|
|
15563
|
+
if (!elements.commandPaletteDialog.open) elements.commandPaletteDialog.showModal();
|
|
15564
|
+
queueMicrotask(() => {
|
|
15565
|
+
elements.commandPaletteInput?.focus();
|
|
15566
|
+
elements.commandPaletteInput?.select();
|
|
15567
|
+
});
|
|
15568
|
+
}
|
|
15569
|
+
|
|
15570
|
+
function closeCommandPalette() {
|
|
15571
|
+
if (elements.commandPaletteDialog?.open) elements.commandPaletteDialog.close();
|
|
15572
|
+
}
|
|
15573
|
+
|
|
15574
|
+
async function executeCommandPaletteItem(item = commandPaletteItems[commandPaletteIndex]) {
|
|
15575
|
+
if (!item) return;
|
|
15576
|
+
closeCommandPalette();
|
|
15577
|
+
try {
|
|
15578
|
+
await item.run?.();
|
|
15579
|
+
} catch (error) {
|
|
15580
|
+
addEvent(error.message || String(error), "error");
|
|
15581
|
+
}
|
|
13300
15582
|
}
|
|
13301
15583
|
|
|
13302
15584
|
async function refreshAll(tabContext = activeTabContext()) {
|
|
@@ -13871,6 +16153,7 @@ function handleExtensionUiRequest(request) {
|
|
|
13871
16153
|
} else {
|
|
13872
16154
|
statusEntries.delete(statusKey);
|
|
13873
16155
|
}
|
|
16156
|
+
if (statusKey === STATS_WEBUI_STATUS_KEY) handleStatsWebuiStatus(request.statusText);
|
|
13874
16157
|
updateOptionalFeatureAvailability();
|
|
13875
16158
|
renderStatus();
|
|
13876
16159
|
return;
|
|
@@ -14174,7 +16457,9 @@ function handleEvent(event) {
|
|
|
14174
16457
|
handleToolExecutionEnd(event);
|
|
14175
16458
|
setRunIndicatorActivity(`Tool ${runIndicatorToolName(event.toolName)} ${event.isError ? "failed" : "finished"}; waiting for the agent's next step…`);
|
|
14176
16459
|
addEvent(`tool ${event.toolName} ${event.isError ? "failed" : "finished"}`, event.isError ? "error" : "info");
|
|
14177
|
-
|
|
16460
|
+
// No transcript refresh here: the live tool card already shows the
|
|
16461
|
+
// result via renderLiveToolRun, and message_end/agent_end reconcile the
|
|
16462
|
+
// transcript. This avoids one fetch+render per tool call.
|
|
14178
16463
|
scheduleRefreshFooter();
|
|
14179
16464
|
break;
|
|
14180
16465
|
case "compaction_start":
|
|
@@ -14431,6 +16716,8 @@ elements.newTabMenu?.addEventListener("focusout", () => {
|
|
|
14431
16716
|
elements.newTabCurrentDirectoryButton?.addEventListener("click", () => createTerminalTab(currentDirectoryForNewTab(), { triggerButton: elements.newTabCurrentDirectoryButton }));
|
|
14432
16717
|
elements.newTabChooseDirectoryButton?.addEventListener("click", () => createTerminalTabFromChosenDirectory({ triggerButton: elements.newTabChooseDirectoryButton }));
|
|
14433
16718
|
elements.closeAllTabsButton.addEventListener("click", () => closeAllTerminalTabs());
|
|
16719
|
+
elements.commandPaletteButton?.addEventListener("click", () => openCommandPalette());
|
|
16720
|
+
elements.workspaceDashboardToggleButton?.addEventListener("click", () => setWorkspaceDashboardCollapsed(!workspaceDashboardCollapsed));
|
|
14434
16721
|
elements.gitWorkflowButton.addEventListener("click", () => {
|
|
14435
16722
|
setComposerActionsOpen(false);
|
|
14436
16723
|
startGitWorkflow();
|
|
@@ -14556,6 +16843,7 @@ elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/r
|
|
|
14556
16843
|
elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
|
|
14557
16844
|
elements.nativeSkillsButton.addEventListener("click", () => runNativeCommandMenu("/skills"));
|
|
14558
16845
|
elements.nativeToolsButton.addEventListener("click", () => runNativeCommandMenu("/tools"));
|
|
16846
|
+
elements.optionsCommandPaletteButton.addEventListener("click", () => openCommandPalette());
|
|
14559
16847
|
elements.optionsResumeButton.addEventListener("click", () => runNativeCommandMenu("/resume"));
|
|
14560
16848
|
elements.optionsReloadButton.addEventListener("click", () => runNativeCommandMenu("/reload"));
|
|
14561
16849
|
elements.optionsNameButton.addEventListener("click", () => runNativeCommandMenu("/name"));
|
|
@@ -14564,6 +16852,25 @@ elements.optionsSettingsButton.addEventListener("click", () => runNativeCommandM
|
|
|
14564
16852
|
elements.optionsExportButton.addEventListener("click", () => runNativeCommandMenu("/export"));
|
|
14565
16853
|
elements.optionsForkButton.addEventListener("click", () => runNativeCommandMenu("/fork"));
|
|
14566
16854
|
elements.optionsTreeButton.addEventListener("click", () => runNativeCommandMenu("/tree"));
|
|
16855
|
+
elements.optionsStatsButton?.addEventListener("click", () => openStatsOverlay({ refresh: true }));
|
|
16856
|
+
elements.statsOverlayRefreshButton?.addEventListener("click", () => requestStatsOverlayRefresh());
|
|
16857
|
+
elements.statsOverlayScope?.addEventListener("change", () => {
|
|
16858
|
+
const custom = elements.statsOverlayScope?.value === "custom";
|
|
16859
|
+
if (elements.statsOverlayCustomDays) elements.statsOverlayCustomDays.hidden = !custom;
|
|
16860
|
+
if (custom) {
|
|
16861
|
+
elements.statsOverlayCustomDays?.focus();
|
|
16862
|
+
return;
|
|
16863
|
+
}
|
|
16864
|
+
requestStatsOverlayRefresh();
|
|
16865
|
+
});
|
|
16866
|
+
elements.statsOverlayCustomDays?.addEventListener("change", () => requestStatsOverlayRefresh());
|
|
16867
|
+
elements.statsOverlayCustomDays?.addEventListener("keydown", (event) => {
|
|
16868
|
+
if (event.key !== "Enter") return;
|
|
16869
|
+
event.preventDefault();
|
|
16870
|
+
requestStatsOverlayRefresh();
|
|
16871
|
+
});
|
|
16872
|
+
elements.statsOverlayCloseButton?.addEventListener("click", () => elements.statsOverlayDialog?.close());
|
|
16873
|
+
elements.statsOverlayDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
14567
16874
|
elements.gitWorkflowSteps.addEventListener("click", (event) => {
|
|
14568
16875
|
const target = event.target instanceof Element ? event.target : null;
|
|
14569
16876
|
const button = target?.closest("[data-git-workflow-process]");
|
|
@@ -14594,6 +16901,42 @@ elements.nativeCommandDialog.addEventListener("close", () => {
|
|
|
14594
16901
|
elements.nativeCommandSearch.oninput = null;
|
|
14595
16902
|
nativeCommandTabId = null;
|
|
14596
16903
|
});
|
|
16904
|
+
elements.commandPaletteDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
16905
|
+
elements.commandPaletteDialog?.addEventListener("cancel", (event) => {
|
|
16906
|
+
event.preventDefault();
|
|
16907
|
+
closeCommandPalette();
|
|
16908
|
+
});
|
|
16909
|
+
elements.commandPaletteInput?.addEventListener("input", () => {
|
|
16910
|
+
commandPaletteIndex = 0;
|
|
16911
|
+
renderCommandPalette();
|
|
16912
|
+
});
|
|
16913
|
+
elements.commandPaletteInput?.addEventListener("keydown", (event) => {
|
|
16914
|
+
if (event.key === "ArrowDown") {
|
|
16915
|
+
event.preventDefault();
|
|
16916
|
+
setCommandPaletteIndex(commandPaletteIndex + 1);
|
|
16917
|
+
} else if (event.key === "ArrowUp") {
|
|
16918
|
+
event.preventDefault();
|
|
16919
|
+
setCommandPaletteIndex(commandPaletteIndex - 1);
|
|
16920
|
+
} else if (event.key === "Enter") {
|
|
16921
|
+
event.preventDefault();
|
|
16922
|
+
executeCommandPaletteItem();
|
|
16923
|
+
} else if (event.key === "Escape") {
|
|
16924
|
+
event.preventDefault();
|
|
16925
|
+
closeCommandPalette();
|
|
16926
|
+
}
|
|
16927
|
+
});
|
|
16928
|
+
elements.editRetryDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
16929
|
+
elements.editRetryDialog?.addEventListener("cancel", (event) => {
|
|
16930
|
+
event.preventDefault();
|
|
16931
|
+
closeEditRetryDialog();
|
|
16932
|
+
});
|
|
16933
|
+
elements.editRetryDialog?.addEventListener("close", () => {
|
|
16934
|
+
activeEditRetry = null;
|
|
16935
|
+
setEditRetryBusy(false);
|
|
16936
|
+
});
|
|
16937
|
+
elements.editRetryCancelButton?.addEventListener("click", closeEditRetryDialog);
|
|
16938
|
+
elements.editRetryForkButton?.addEventListener("click", () => submitEditRetry({ send: false }));
|
|
16939
|
+
elements.editRetrySendButton?.addEventListener("click", () => submitEditRetry({ send: true }));
|
|
14597
16940
|
|
|
14598
16941
|
function resetAbortLongPressAffordance() {
|
|
14599
16942
|
clearTimeout(abortLongPressTimer);
|
|
@@ -14679,33 +17022,7 @@ elements.newSessionButton.addEventListener("click", async () => {
|
|
|
14679
17022
|
});
|
|
14680
17023
|
elements.compactButton.addEventListener("click", async () => {
|
|
14681
17024
|
setComposerActionsOpen(false);
|
|
14682
|
-
|
|
14683
|
-
try {
|
|
14684
|
-
elements.compactButton.disabled = true;
|
|
14685
|
-
elements.compactButton.textContent = "Compacting…";
|
|
14686
|
-
setRunIndicatorActivity("Requesting context compaction…");
|
|
14687
|
-
scrollChatToBottom({ force: true });
|
|
14688
|
-
markContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
14689
|
-
renderFooter();
|
|
14690
|
-
addEvent("manual compaction requested");
|
|
14691
|
-
await api("/api/compact", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
14692
|
-
if (!isCurrentTabContext(tabContext)) return;
|
|
14693
|
-
scheduleRefreshState(120, tabContext);
|
|
14694
|
-
scheduleRefreshMessages(600, tabContext);
|
|
14695
|
-
scheduleRefreshFooter(600, tabContext);
|
|
14696
|
-
} catch (error) {
|
|
14697
|
-
if (isCurrentTabContext(tabContext)) {
|
|
14698
|
-
clearContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
14699
|
-
clearRunIndicatorActivity();
|
|
14700
|
-
renderFooter();
|
|
14701
|
-
addEvent(error.message, "error");
|
|
14702
|
-
}
|
|
14703
|
-
} finally {
|
|
14704
|
-
if (isCurrentTabContext(tabContext)) {
|
|
14705
|
-
elements.compactButton.disabled = !!currentState?.isCompacting;
|
|
14706
|
-
elements.compactButton.textContent = currentState?.isCompacting ? "Compacting…" : "Compact";
|
|
14707
|
-
}
|
|
14708
|
-
}
|
|
17025
|
+
await requestManualCompaction({ triggerButton: elements.compactButton });
|
|
14709
17026
|
});
|
|
14710
17027
|
elements.setModelButton.addEventListener("click", async () => {
|
|
14711
17028
|
if (!elements.modelSelect.value) return;
|
|
@@ -14850,7 +17167,7 @@ function isTextEntryTarget(target) {
|
|
|
14850
17167
|
|
|
14851
17168
|
function shouldHandleNativeAppShortcut(event) {
|
|
14852
17169
|
if (event.defaultPrevented) return false;
|
|
14853
|
-
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open || elements.nativeCommandDialog?.open || elements.appRunnerInfoDialog?.open) return false;
|
|
17170
|
+
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open || elements.commandPaletteDialog?.open || elements.editRetryDialog?.open || elements.nativeCommandDialog?.open || elements.appRunnerInfoDialog?.open) return false;
|
|
14854
17171
|
return event.target === elements.promptInput || !isTextEntryTarget(event.target);
|
|
14855
17172
|
}
|
|
14856
17173
|
|
|
@@ -14860,6 +17177,11 @@ function handleNativeAppShortcut(event) {
|
|
|
14860
17177
|
const lowerKey = String(key || "").toLowerCase();
|
|
14861
17178
|
const ctrlOrMeta = event.ctrlKey || event.metaKey;
|
|
14862
17179
|
|
|
17180
|
+
if (ctrlOrMeta && !event.altKey && !event.shiftKey && lowerKey === "k") {
|
|
17181
|
+
event.preventDefault();
|
|
17182
|
+
openCommandPalette();
|
|
17183
|
+
return;
|
|
17184
|
+
}
|
|
14863
17185
|
if (ctrlOrMeta && !event.altKey && lowerKey === "l") {
|
|
14864
17186
|
event.preventDefault();
|
|
14865
17187
|
openNativeModelSelector();
|
|
@@ -14900,6 +17222,118 @@ function handleNativeAppShortcut(event) {
|
|
|
14900
17222
|
}
|
|
14901
17223
|
}
|
|
14902
17224
|
|
|
17225
|
+
// --- Transcript search (Ctrl/Cmd+F) ---
|
|
17226
|
+
let chatSearchMatches = [];
|
|
17227
|
+
let chatSearchIndex = -1;
|
|
17228
|
+
let chatSearchTimer = null;
|
|
17229
|
+
|
|
17230
|
+
function chatSearchQueryText() {
|
|
17231
|
+
return (elements.chatSearchInput?.value || "").trim().toLowerCase();
|
|
17232
|
+
}
|
|
17233
|
+
|
|
17234
|
+
function collectChatSearchMatches(query) {
|
|
17235
|
+
if (!query) return [];
|
|
17236
|
+
const matches = [];
|
|
17237
|
+
for (const bubble of elements.chat.querySelectorAll(".message")) {
|
|
17238
|
+
if (bubble === runIndicatorBubble || bubble.classList.contains("runIndicator")) continue;
|
|
17239
|
+
if ((bubble.textContent || "").toLowerCase().includes(query)) matches.push(bubble);
|
|
17240
|
+
}
|
|
17241
|
+
return matches;
|
|
17242
|
+
}
|
|
17243
|
+
|
|
17244
|
+
function clearChatSearchHighlights() {
|
|
17245
|
+
for (const bubble of elements.chat.querySelectorAll(".message.search-current")) bubble.classList.remove("search-current");
|
|
17246
|
+
}
|
|
17247
|
+
|
|
17248
|
+
function updateChatSearchCount() {
|
|
17249
|
+
if (!elements.chatSearchCount) return;
|
|
17250
|
+
const query = chatSearchQueryText();
|
|
17251
|
+
elements.chatSearchCount.textContent = !query ? "" : chatSearchMatches.length === 0 ? "0/0" : `${chatSearchIndex + 1}/${chatSearchMatches.length}`;
|
|
17252
|
+
}
|
|
17253
|
+
|
|
17254
|
+
function focusChatSearchMatch() {
|
|
17255
|
+
const bubble = chatSearchMatches[chatSearchIndex];
|
|
17256
|
+
if (!bubble) return;
|
|
17257
|
+
if (!bubble.isConnected) {
|
|
17258
|
+
runChatSearch({ navigate: false });
|
|
17259
|
+
return;
|
|
17260
|
+
}
|
|
17261
|
+
clearChatSearchHighlights();
|
|
17262
|
+
bubble.classList.add("search-current");
|
|
17263
|
+
const query = chatSearchQueryText();
|
|
17264
|
+
for (const details of bubble.querySelectorAll("details")) {
|
|
17265
|
+
if (!details.open && (details.textContent || "").toLowerCase().includes(query)) details.open = true;
|
|
17266
|
+
}
|
|
17267
|
+
autoFollowChat = false;
|
|
17268
|
+
lastChatProgrammaticScrollAt = performance.now();
|
|
17269
|
+
bubble.scrollIntoView({ block: "center", behavior: "instant" });
|
|
17270
|
+
updateJumpToLatestButton();
|
|
17271
|
+
updateChatSearchCount();
|
|
17272
|
+
}
|
|
17273
|
+
|
|
17274
|
+
function runChatSearch({ navigate = false } = {}) {
|
|
17275
|
+
const query = chatSearchQueryText();
|
|
17276
|
+
clearChatSearchHighlights();
|
|
17277
|
+
chatSearchMatches = collectChatSearchMatches(query);
|
|
17278
|
+
if (chatSearchIndex >= chatSearchMatches.length || chatSearchIndex < 0) chatSearchIndex = chatSearchMatches.length - 1;
|
|
17279
|
+
updateChatSearchCount();
|
|
17280
|
+
if (navigate) focusChatSearchMatch();
|
|
17281
|
+
}
|
|
17282
|
+
|
|
17283
|
+
function stepChatSearch(step) {
|
|
17284
|
+
if (chatSearchMatches.some((bubble) => !bubble.isConnected)) runChatSearch();
|
|
17285
|
+
if (!chatSearchMatches.length) {
|
|
17286
|
+
runChatSearch();
|
|
17287
|
+
if (!chatSearchMatches.length) return;
|
|
17288
|
+
}
|
|
17289
|
+
chatSearchIndex = (chatSearchIndex + step + chatSearchMatches.length) % chatSearchMatches.length;
|
|
17290
|
+
focusChatSearchMatch();
|
|
17291
|
+
}
|
|
17292
|
+
|
|
17293
|
+
function openChatSearch() {
|
|
17294
|
+
if (!elements.chatSearchBar) return;
|
|
17295
|
+
elements.chatSearchBar.hidden = false;
|
|
17296
|
+
elements.chatSearchInput?.focus();
|
|
17297
|
+
elements.chatSearchInput?.select();
|
|
17298
|
+
if (chatSearchQueryText()) runChatSearch();
|
|
17299
|
+
}
|
|
17300
|
+
|
|
17301
|
+
function closeChatSearch() {
|
|
17302
|
+
if (!elements.chatSearchBar || elements.chatSearchBar.hidden) return;
|
|
17303
|
+
elements.chatSearchBar.hidden = true;
|
|
17304
|
+
clearChatSearchHighlights();
|
|
17305
|
+
chatSearchMatches = [];
|
|
17306
|
+
chatSearchIndex = -1;
|
|
17307
|
+
updateChatSearchCount();
|
|
17308
|
+
}
|
|
17309
|
+
|
|
17310
|
+
elements.chatSearchInput?.addEventListener("input", () => {
|
|
17311
|
+
clearTimeout(chatSearchTimer);
|
|
17312
|
+
chatSearchTimer = setTimeout(() => {
|
|
17313
|
+
chatSearchIndex = -1;
|
|
17314
|
+
runChatSearch({ navigate: true });
|
|
17315
|
+
}, 150);
|
|
17316
|
+
});
|
|
17317
|
+
elements.chatSearchInput?.addEventListener("keydown", (event) => {
|
|
17318
|
+
if (event.key === "Enter") {
|
|
17319
|
+
event.preventDefault();
|
|
17320
|
+
stepChatSearch(event.shiftKey ? -1 : 1);
|
|
17321
|
+
} else if (event.key === "Escape") {
|
|
17322
|
+
event.preventDefault();
|
|
17323
|
+
event.stopPropagation();
|
|
17324
|
+
closeChatSearch();
|
|
17325
|
+
}
|
|
17326
|
+
});
|
|
17327
|
+
elements.chatSearchPrevButton?.addEventListener("click", () => stepChatSearch(-1));
|
|
17328
|
+
elements.chatSearchNextButton?.addEventListener("click", () => stepChatSearch(1));
|
|
17329
|
+
elements.chatSearchCloseButton?.addEventListener("click", closeChatSearch);
|
|
17330
|
+
window.addEventListener("keydown", (event) => {
|
|
17331
|
+
if ((event.ctrlKey || event.metaKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "f") {
|
|
17332
|
+
event.preventDefault();
|
|
17333
|
+
openChatSearch();
|
|
17334
|
+
}
|
|
17335
|
+
});
|
|
17336
|
+
|
|
14903
17337
|
window.addEventListener("keydown", handleNativeAppShortcut, { capture: true });
|
|
14904
17338
|
document.addEventListener("visibilitychange", () => {
|
|
14905
17339
|
if (document.visibilityState === "visible") scheduleForegroundReconcile("visibility resume", 0);
|
|
@@ -14909,7 +17343,7 @@ window.addEventListener("focus", () => scheduleForegroundReconcile("window focus
|
|
|
14909
17343
|
window.addEventListener("online", () => scheduleForegroundReconcile("network online", 0));
|
|
14910
17344
|
window.addEventListener("keydown", (event) => {
|
|
14911
17345
|
if (event.key !== "Escape") return;
|
|
14912
|
-
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open) return;
|
|
17346
|
+
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open || elements.commandPaletteDialog?.open || elements.editRetryDialog?.open) return;
|
|
14913
17347
|
if (publishMenuOpen) {
|
|
14914
17348
|
setPublishMenuOpen(false);
|
|
14915
17349
|
return;
|
|
@@ -15100,6 +17534,7 @@ restoreStoredSkillUsage();
|
|
|
15100
17534
|
restoreBusyPromptBehaviorSetting();
|
|
15101
17535
|
updateComposerModeButtons();
|
|
15102
17536
|
updateOptionalFeatureAvailability();
|
|
17537
|
+
refreshOptionalFeaturePackageStatuses({ announce: true });
|
|
15103
17538
|
renderAppRunnerControls();
|
|
15104
17539
|
renderLoadedPromptListPreview();
|
|
15105
17540
|
loadLastUserPromptCache();
|
|
@@ -15116,6 +17551,7 @@ restoreAgentDoneNotificationsSetting();
|
|
|
15116
17551
|
restoreThinkingVisibilitySetting();
|
|
15117
17552
|
restoreTerminalTabsLayoutSetting();
|
|
15118
17553
|
restoreToolOutputExpansionSetting();
|
|
17554
|
+
restoreWorkspaceDashboardState();
|
|
15119
17555
|
restoreSidePanelSectionState();
|
|
15120
17556
|
bindSidePanelSectionToggles();
|
|
15121
17557
|
restoreSidePanelState();
|