@firstpick/pi-package-webui 0.3.2 → 0.3.4
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 +12 -3
- package/bin/pi-webui.mjs +1261 -16
- package/package.json +2 -1
- package/public/app.js +1536 -42
- package/public/index.html +79 -3
- package/public/service-worker.js +1 -1
- package/public/styles.css +688 -27
- package/tests/mobile-static.test.mjs +101 -9
package/public/app.js
CHANGED
|
@@ -50,6 +50,10 @@ const elements = {
|
|
|
50
50
|
nativeCommandMenu: $("#nativeCommandMenu"),
|
|
51
51
|
nativeSkillsButton: $("#nativeSkillsButton"),
|
|
52
52
|
nativeToolsButton: $("#nativeToolsButton"),
|
|
53
|
+
appRunnerMenu: $("#appRunnerMenu"),
|
|
54
|
+
appRunnerInfoButton: $("#appRunnerInfoButton"),
|
|
55
|
+
appRunnerMenuButton: $("#appRunnerMenuButton"),
|
|
56
|
+
appRunnerMenuPanel: $("#appRunnerMenuPanel"),
|
|
53
57
|
optionsMenuButton: $("#optionsMenuButton"),
|
|
54
58
|
optionsMenu: $("#optionsMenu"),
|
|
55
59
|
optionsResumeButton: $("#optionsResumeButton"),
|
|
@@ -67,12 +71,20 @@ const elements = {
|
|
|
67
71
|
gitWorkflowOutput: $("#gitWorkflowOutput"),
|
|
68
72
|
gitWorkflowActions: $("#gitWorkflowActions"),
|
|
69
73
|
gitWorkflowCancelButton: $("#gitWorkflowCancelButton"),
|
|
74
|
+
gitPrDialog: $("#gitPrDialog"),
|
|
75
|
+
gitPrTitleInput: $("#gitPrTitleInput"),
|
|
76
|
+
gitPrBodyEditor: $("#gitPrBodyEditor"),
|
|
77
|
+
gitPrStatus: $("#gitPrStatus"),
|
|
78
|
+
gitPrCancelButton: $("#gitPrCancelButton"),
|
|
79
|
+
gitPrCreateButton: $("#gitPrCreateButton"),
|
|
70
80
|
modelSelect: $("#modelSelect"),
|
|
71
81
|
setModelButton: $("#setModelButton"),
|
|
72
82
|
thinkingSelect: $("#thinkingSelect"),
|
|
73
83
|
setThinkingButton: $("#setThinkingButton"),
|
|
74
84
|
thinkingVisibilityToggle: $("#thinkingVisibilityToggle"),
|
|
75
85
|
thinkingVisibilityStatus: $("#thinkingVisibilityStatus"),
|
|
86
|
+
terminalTabsLayoutSelect: $("#terminalTabsLayoutSelect"),
|
|
87
|
+
terminalTabsLayoutStatus: $("#terminalTabsLayoutStatus"),
|
|
76
88
|
themeSelect: $("#themeSelect"),
|
|
77
89
|
backgroundInput: $("#backgroundInput"),
|
|
78
90
|
backgroundChooseButton: $("#backgroundChooseButton"),
|
|
@@ -113,6 +125,13 @@ const elements = {
|
|
|
113
125
|
promptListDialogLoadButton: $("#promptListDialogLoadButton"),
|
|
114
126
|
promptListSaveButton: $("#promptListSaveButton"),
|
|
115
127
|
promptListRunListButton: $("#promptListRunListButton"),
|
|
128
|
+
attachmentTextDialog: $("#attachmentTextDialog"),
|
|
129
|
+
attachmentTextTitle: $("#attachmentTextTitle"),
|
|
130
|
+
attachmentTextMeta: $("#attachmentTextMeta"),
|
|
131
|
+
attachmentTextEditor: $("#attachmentTextEditor"),
|
|
132
|
+
attachmentTextStatus: $("#attachmentTextStatus"),
|
|
133
|
+
attachmentTextCancelButton: $("#attachmentTextCancelButton"),
|
|
134
|
+
attachmentTextSaveButton: $("#attachmentTextSaveButton"),
|
|
116
135
|
commandSearchInput: $("#commandSearchInput"),
|
|
117
136
|
commandsBox: $("#commandsBox"),
|
|
118
137
|
eventLog: $("#eventLog"),
|
|
@@ -143,6 +162,9 @@ const elements = {
|
|
|
143
162
|
nativeCommandBody: $("#nativeCommandBody"),
|
|
144
163
|
nativeCommandError: $("#nativeCommandError"),
|
|
145
164
|
nativeCommandActions: $("#nativeCommandActions"),
|
|
165
|
+
appRunnerInfoDialog: $("#appRunnerInfoDialog"),
|
|
166
|
+
appRunnerInfoBody: $("#appRunnerInfoBody"),
|
|
167
|
+
appRunnerInfoCloseButton: $("#appRunnerInfoCloseButton"),
|
|
146
168
|
};
|
|
147
169
|
|
|
148
170
|
let currentState = null;
|
|
@@ -151,6 +173,7 @@ let activeTabId = null;
|
|
|
151
173
|
let activeTabGeneration = 0;
|
|
152
174
|
let tabDrafts = new Map();
|
|
153
175
|
let tabAttachments = new Map();
|
|
176
|
+
let activeTextAttachmentEditor = null;
|
|
154
177
|
let tabActivities = new Map();
|
|
155
178
|
let tabSeenCompletionSerials = new Map();
|
|
156
179
|
let streamBubble = null;
|
|
@@ -178,8 +201,10 @@ let refreshTabsTimer = null;
|
|
|
178
201
|
let foregroundReconcileTimer = null;
|
|
179
202
|
let eventSource = null;
|
|
180
203
|
let activeDialog = null;
|
|
204
|
+
let activeGitPrDialogResolve = null;
|
|
181
205
|
let nativeCommandTabId = null;
|
|
182
206
|
let pathPickerState = null;
|
|
207
|
+
let firstTerminalCwdPromptShown = false;
|
|
183
208
|
let pathFastPicks = [];
|
|
184
209
|
let pathFastPicksReady = false;
|
|
185
210
|
let pathFastPicksLoadPromise = null;
|
|
@@ -187,6 +212,9 @@ let mobileTabsExpanded = false;
|
|
|
187
212
|
let openTerminalTabGroupKey = null;
|
|
188
213
|
let newTabMenuOpen = false;
|
|
189
214
|
let nativeCommandMenuOpen = false;
|
|
215
|
+
let appRunnerMenuOpen = false;
|
|
216
|
+
let appRunnerCustomDraft = { id: "", label: "", command: "./", path: "", args: "" };
|
|
217
|
+
let appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
190
218
|
let optionsMenuOpen = false;
|
|
191
219
|
let availableCommands = [];
|
|
192
220
|
let rawAvailableCommands = [];
|
|
@@ -225,6 +253,7 @@ let blockedTabNotificationPermissionRequested = false;
|
|
|
225
253
|
let blockedTabNotificationFallbackNoted = false;
|
|
226
254
|
let agentDoneNotificationsEnabled = false;
|
|
227
255
|
let thinkingOutputVisible = true;
|
|
256
|
+
let terminalTabsLayout = "top";
|
|
228
257
|
let webuiSettings = {};
|
|
229
258
|
let busyPromptBehavior = "followUp";
|
|
230
259
|
let autocompleteMaxVisible = 12;
|
|
@@ -244,6 +273,7 @@ let customBackgroundLoading = false;
|
|
|
244
273
|
let footerScopedModels = [];
|
|
245
274
|
let footerScopedModelPatterns = [];
|
|
246
275
|
let footerScopedModelSource = "none";
|
|
276
|
+
const contextUsageUnknownAfterCompactionByTab = new Map();
|
|
247
277
|
let autoFollowChat = true;
|
|
248
278
|
let chatFollowFrame = null;
|
|
249
279
|
let chatFollowSettleTimer = null;
|
|
@@ -269,6 +299,7 @@ const TAB_STORAGE_KEY = "pi-webui-active-tab";
|
|
|
269
299
|
const PATH_FAST_PICKS_STORAGE_KEY = "pi-webui-path-fast-picks";
|
|
270
300
|
const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications";
|
|
271
301
|
const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible";
|
|
302
|
+
const TERMINAL_TABS_LAYOUT_STORAGE_KEY = "pi-webui-terminal-tabs-layout";
|
|
272
303
|
const TOOL_OUTPUT_EXPANDED_STORAGE_KEY = "pi-webui-tool-output-expanded";
|
|
273
304
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
274
305
|
const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background";
|
|
@@ -293,10 +324,15 @@ const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
|
293
324
|
const ATTACHMENT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
294
325
|
const ATTACHMENT_INLINE_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
|
|
295
326
|
const ATTACHMENT_INLINE_IMAGE_TOTAL_MAX_BYTES = 16 * 1024 * 1024;
|
|
327
|
+
const LONG_INPUT_ATTACHMENT_LINE_THRESHOLD = 20;
|
|
328
|
+
const LONG_INPUT_ATTACHMENT_MIME_TYPE = "text/plain";
|
|
296
329
|
const INLINE_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
297
330
|
const BACKGROUND_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
298
331
|
const DEFAULT_THEME_NAME = "catppuccin-mocha";
|
|
332
|
+
const TERMINAL_TABS_LAYOUTS = new Set(["top", "left"]);
|
|
333
|
+
const TERMINAL_TABS_LAYOUT_LABELS = { top: "Top bar", left: "Left sidebar" };
|
|
299
334
|
const MOBILE_VIEW_QUERY = "(max-width: 720px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
335
|
+
const SIDE_PANEL_OVERLAY_QUERY = "(max-width: 1050px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
300
336
|
const CHAT_BOTTOM_THRESHOLD_PX = 96;
|
|
301
337
|
const STICKY_USER_PROMPT_PREVIEW_LIMIT = 220;
|
|
302
338
|
const STICKY_USER_PROMPT_TOP_GAP_PX = 12;
|
|
@@ -325,10 +361,12 @@ const BLOCKED_TAB_NOTIFICATION_TAG_PREFIX = "pi-webui-blocked-tab";
|
|
|
325
361
|
const AGENT_DONE_NOTIFICATION_TAG_PREFIX = "pi-webui-agent-done";
|
|
326
362
|
const BLOCKED_TAB_NOTIFICATION_ICON = "/icon-192.png";
|
|
327
363
|
const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
364
|
+
const sidePanelOverlayMedia = window.matchMedia?.(SIDE_PANEL_OVERLAY_QUERY) || mobileViewMedia;
|
|
328
365
|
const statusEntries = new Map();
|
|
329
366
|
const widgets = new Map();
|
|
330
367
|
const todoProgressWidgetExpandedByTab = new Map();
|
|
331
368
|
const releaseNpmOutputExpandedByTab = new Map();
|
|
369
|
+
const appRunnerDataByTab = new Map();
|
|
332
370
|
const liveToolRuns = new Map();
|
|
333
371
|
const liveToolCards = new Map();
|
|
334
372
|
const liveToolRenderQueue = new Map();
|
|
@@ -415,6 +453,8 @@ const OPTIONAL_FEATURES = [
|
|
|
415
453
|
const OPTIONAL_FEATURE_BY_ID = new Map(OPTIONAL_FEATURES.map((feature) => [feature.id, feature]));
|
|
416
454
|
const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
417
455
|
["git-staged-msg", "gitWorkflow"],
|
|
456
|
+
["git-branch-name", "gitWorkflow"],
|
|
457
|
+
["pr", "gitWorkflow"],
|
|
418
458
|
["release-npm", "releaseNpm"],
|
|
419
459
|
["release-aur", "releaseAur"],
|
|
420
460
|
["skills", "tuiSkillsCommand"],
|
|
@@ -446,16 +486,42 @@ const SETTINGS_AUTOCOMPLETE_OPTIONS = ["3", "5", "7", "10", "15", "20"];
|
|
|
446
486
|
const optionalFeatureInstallInProgress = new Set();
|
|
447
487
|
const gitFooterPayloadRefreshInFlightByTab = new Set();
|
|
448
488
|
|
|
489
|
+
function createGitWorkflowActionsDone(patch = {}) {
|
|
490
|
+
return {
|
|
491
|
+
stage: false,
|
|
492
|
+
message: false,
|
|
493
|
+
commit: false,
|
|
494
|
+
push: false,
|
|
495
|
+
...patch,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function gitWorkflowActionDone(workflow, process) {
|
|
500
|
+
return !!createGitWorkflowActionsDone(workflow?.actionsDone)[process];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function gitWorkflowActionDonePatch(workflow, process) {
|
|
504
|
+
return { actionsDone: createGitWorkflowActionsDone({ ...workflow?.actionsDone, [process]: true }) };
|
|
505
|
+
}
|
|
506
|
+
|
|
449
507
|
function createGitWorkflowState() {
|
|
450
508
|
return {
|
|
451
509
|
active: false,
|
|
452
510
|
step: "idle",
|
|
511
|
+
process: "stage",
|
|
453
512
|
busy: false,
|
|
454
513
|
runId: 0,
|
|
455
514
|
output: "",
|
|
456
515
|
error: "",
|
|
457
516
|
message: null,
|
|
458
517
|
messageRequestedAt: 0,
|
|
518
|
+
branchName: "",
|
|
519
|
+
branchNameRequestedAt: 0,
|
|
520
|
+
actionsDone: createGitWorkflowActionsDone(),
|
|
521
|
+
prMode: false,
|
|
522
|
+
prBranch: "",
|
|
523
|
+
pr: null,
|
|
524
|
+
prRequestedAt: 0,
|
|
459
525
|
};
|
|
460
526
|
}
|
|
461
527
|
|
|
@@ -499,7 +565,13 @@ function clearGitWorkflowForTab(tabId) {
|
|
|
499
565
|
}
|
|
500
566
|
}
|
|
501
567
|
|
|
502
|
-
const
|
|
568
|
+
const GIT_WORKFLOW_PROCESSES = [
|
|
569
|
+
{ value: "stage", label: "Stage" },
|
|
570
|
+
{ value: "message", label: "Message" },
|
|
571
|
+
{ value: "commit", label: "Commit" },
|
|
572
|
+
{ value: "push", label: "Push" },
|
|
573
|
+
];
|
|
574
|
+
const GIT_WORKFLOW_PROCESS_VALUES = new Set(GIT_WORKFLOW_PROCESSES.map((process) => process.value));
|
|
503
575
|
const ACTION_FEEDBACK_REACTIONS = {
|
|
504
576
|
up: { icon: "👍", label: "Good job", title: "Good job!" },
|
|
505
577
|
down: { icon: "👎", label: "Avoid this", title: "Avoid this" },
|
|
@@ -511,11 +583,32 @@ const GIT_WORKFLOW_ACTIVE_INDEX = {
|
|
|
511
583
|
generate: 1,
|
|
512
584
|
generating: 1,
|
|
513
585
|
message: 2,
|
|
586
|
+
branchNaming: 2,
|
|
587
|
+
branching: 2,
|
|
514
588
|
committing: 2,
|
|
515
589
|
push: 3,
|
|
516
590
|
pushing: 3,
|
|
591
|
+
prGenerating: 3,
|
|
592
|
+
prReview: 3,
|
|
593
|
+
prCreating: 3,
|
|
517
594
|
done: 4,
|
|
518
595
|
};
|
|
596
|
+
const GIT_WORKFLOW_CREATE_PR_TOOLTIP = [
|
|
597
|
+
"Create PR:",
|
|
598
|
+
"1. Ask Pi to generate a type/feature-name branch from staged changes.",
|
|
599
|
+
"2. Read dev/COMMIT/staged-branch-name.txt.",
|
|
600
|
+
"3. Let you confirm or edit the generated branch name.",
|
|
601
|
+
"4. Run git switch -c <branch>.",
|
|
602
|
+
"5. Return here so you can choose Commit short or Commit long on that branch.",
|
|
603
|
+
].join("\n");
|
|
604
|
+
const GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP = [
|
|
605
|
+
"Manual branch:",
|
|
606
|
+
"1. Skip agent branch-name generation.",
|
|
607
|
+
"2. Prefill a branch from the commit message if possible.",
|
|
608
|
+
"3. Let you type or edit the type/feature-name branch name.",
|
|
609
|
+
"4. Run git switch -c <branch>.",
|
|
610
|
+
"5. Return here so you can choose Commit short or Commit long on that branch.",
|
|
611
|
+
].join("\n");
|
|
519
612
|
|
|
520
613
|
function make(tag, className, text) {
|
|
521
614
|
const node = document.createElement(tag);
|
|
@@ -532,6 +625,10 @@ function isMobileView() {
|
|
|
532
625
|
return mobileViewMedia?.matches || false;
|
|
533
626
|
}
|
|
534
627
|
|
|
628
|
+
function isSidePanelOverlayView() {
|
|
629
|
+
return sidePanelOverlayMedia?.matches || false;
|
|
630
|
+
}
|
|
631
|
+
|
|
535
632
|
function readStoredSidePanelCollapsed() {
|
|
536
633
|
try {
|
|
537
634
|
const stored = localStorage.getItem(SIDE_PANEL_STORAGE_KEY);
|
|
@@ -698,6 +795,26 @@ function persistThinkingOutputVisible(visible) {
|
|
|
698
795
|
}
|
|
699
796
|
}
|
|
700
797
|
|
|
798
|
+
function normalizeTerminalTabsLayout(value) {
|
|
799
|
+
return TERMINAL_TABS_LAYOUTS.has(value) ? value : "top";
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function readStoredTerminalTabsLayout() {
|
|
803
|
+
try {
|
|
804
|
+
return normalizeTerminalTabsLayout(localStorage.getItem(TERMINAL_TABS_LAYOUT_STORAGE_KEY));
|
|
805
|
+
} catch {
|
|
806
|
+
return "top";
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function persistTerminalTabsLayout(layout) {
|
|
811
|
+
try {
|
|
812
|
+
localStorage.setItem(TERMINAL_TABS_LAYOUT_STORAGE_KEY, normalizeTerminalTabsLayout(layout));
|
|
813
|
+
} catch {
|
|
814
|
+
// Ignore storage failures; the layout control should still work for this page load.
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
701
818
|
function readStoredToolOutputExpanded() {
|
|
702
819
|
try {
|
|
703
820
|
return localStorage.getItem(TOOL_OUTPUT_EXPANDED_STORAGE_KEY) === "1";
|
|
@@ -725,6 +842,30 @@ function renderThinkingVisibilityToggle() {
|
|
|
725
842
|
if (elements.thinkingVisibilityStatus) elements.thinkingVisibilityStatus.textContent = thinkingVisibilityStatusText();
|
|
726
843
|
}
|
|
727
844
|
|
|
845
|
+
function terminalTabsLayoutStatusText(layout = terminalTabsLayout) {
|
|
846
|
+
return TERMINAL_TABS_LAYOUT_LABELS[normalizeTerminalTabsLayout(layout)] || TERMINAL_TABS_LAYOUT_LABELS.top;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function renderTerminalTabsLayoutControl() {
|
|
850
|
+
const layout = normalizeTerminalTabsLayout(terminalTabsLayout);
|
|
851
|
+
if (elements.terminalTabsLayoutSelect) elements.terminalTabsLayoutSelect.value = layout;
|
|
852
|
+
if (elements.terminalTabsLayoutStatus) elements.terminalTabsLayoutStatus.textContent = terminalTabsLayoutStatusText(layout);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function setTerminalTabsLayout(layout, { persist = true, announce = false } = {}) {
|
|
856
|
+
const next = normalizeTerminalTabsLayout(layout);
|
|
857
|
+
terminalTabsLayout = next;
|
|
858
|
+
document.body.classList.toggle("terminal-tabs-left", next === "left");
|
|
859
|
+
if (next === "left" && mobileTabsExpanded) setMobileTabsExpanded(false);
|
|
860
|
+
if (persist) persistTerminalTabsLayout(next);
|
|
861
|
+
renderTerminalTabsLayoutControl();
|
|
862
|
+
if (announce) addEvent(`terminal tabs layout changed to ${terminalTabsLayoutStatusText(next).toLowerCase()}`);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function restoreTerminalTabsLayoutSetting() {
|
|
866
|
+
setTerminalTabsLayout(readStoredTerminalTabsLayout(), { persist: false });
|
|
867
|
+
}
|
|
868
|
+
|
|
728
869
|
function removeStreamingThinkingBubble() {
|
|
729
870
|
streamThinkingBubble?.remove();
|
|
730
871
|
streamThinkingBubble = null;
|
|
@@ -793,6 +934,7 @@ function setComposerActionsOpen(open) {
|
|
|
793
934
|
if (!shouldOpen) {
|
|
794
935
|
setPublishMenuOpen(false);
|
|
795
936
|
setNativeCommandMenuOpen(false);
|
|
937
|
+
setAppRunnerMenuOpen(false);
|
|
796
938
|
setOptionsMenuOpen(false);
|
|
797
939
|
}
|
|
798
940
|
}
|
|
@@ -928,7 +1070,7 @@ function setMobileTabsExpanded(expanded) {
|
|
|
928
1070
|
}
|
|
929
1071
|
|
|
930
1072
|
function syncMobileSidePanelState(collapsed) {
|
|
931
|
-
const showBackdrop = !collapsed &&
|
|
1073
|
+
const showBackdrop = !collapsed && isSidePanelOverlayView();
|
|
932
1074
|
elements.sidePanelBackdrop.hidden = !showBackdrop;
|
|
933
1075
|
if (showBackdrop) elements.sidePanel.setAttribute("aria-modal", "true");
|
|
934
1076
|
else elements.sidePanel.removeAttribute("aria-modal");
|
|
@@ -942,11 +1084,11 @@ function setSidePanelCollapsed(collapsed, { persist = true, focusPanel = false }
|
|
|
942
1084
|
elements.sidePanelExpandButton.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
943
1085
|
syncMobileSidePanelState(collapsed);
|
|
944
1086
|
|
|
945
|
-
if (!collapsed && focusPanel &&
|
|
1087
|
+
if (!collapsed && focusPanel && isSidePanelOverlayView()) {
|
|
946
1088
|
requestAnimationFrame(() => elements.toggleSidePanelButton.focus());
|
|
947
1089
|
}
|
|
948
1090
|
|
|
949
|
-
if (!persist ||
|
|
1091
|
+
if (!persist || isSidePanelOverlayView()) return;
|
|
950
1092
|
try {
|
|
951
1093
|
localStorage.setItem(SIDE_PANEL_STORAGE_KEY, collapsed ? "1" : "0");
|
|
952
1094
|
} catch {
|
|
@@ -955,7 +1097,7 @@ function setSidePanelCollapsed(collapsed, { persist = true, focusPanel = false }
|
|
|
955
1097
|
}
|
|
956
1098
|
|
|
957
1099
|
function restoreSidePanelState() {
|
|
958
|
-
if (
|
|
1100
|
+
if (isSidePanelOverlayView()) {
|
|
959
1101
|
setSidePanelCollapsed(true, { persist: false });
|
|
960
1102
|
return;
|
|
961
1103
|
}
|
|
@@ -969,7 +1111,7 @@ function bindMobileViewChanges() {
|
|
|
969
1111
|
setComposerActionsOpen(false);
|
|
970
1112
|
setMobileFooterExpanded(false);
|
|
971
1113
|
setMobileTabsExpanded(false);
|
|
972
|
-
if (event.matches) {
|
|
1114
|
+
if (event.matches || isSidePanelOverlayView()) {
|
|
973
1115
|
setSidePanelCollapsed(true, { persist: false });
|
|
974
1116
|
return;
|
|
975
1117
|
}
|
|
@@ -980,6 +1122,20 @@ function bindMobileViewChanges() {
|
|
|
980
1122
|
else mobileViewMedia.addListener?.(syncForViewport);
|
|
981
1123
|
}
|
|
982
1124
|
|
|
1125
|
+
function bindSidePanelOverlayViewChanges() {
|
|
1126
|
+
if (!sidePanelOverlayMedia || sidePanelOverlayMedia === mobileViewMedia) return;
|
|
1127
|
+
const syncForViewport = (event) => {
|
|
1128
|
+
if (event.matches) {
|
|
1129
|
+
setSidePanelCollapsed(true, { persist: false });
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const stored = readStoredSidePanelCollapsed();
|
|
1133
|
+
setSidePanelCollapsed(stored ?? false, { persist: false });
|
|
1134
|
+
};
|
|
1135
|
+
if (typeof sidePanelOverlayMedia.addEventListener === "function") sidePanelOverlayMedia.addEventListener("change", syncForViewport);
|
|
1136
|
+
else sidePanelOverlayMedia.addListener?.(syncForViewport);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
983
1139
|
function updateVisualViewportVars() {
|
|
984
1140
|
const viewport = window.visualViewport;
|
|
985
1141
|
const viewportHeight = viewport?.height || window.innerHeight || document.documentElement.clientHeight;
|
|
@@ -1055,8 +1211,7 @@ function currentPortArg() {
|
|
|
1055
1211
|
}
|
|
1056
1212
|
|
|
1057
1213
|
function serverStartCommandText() {
|
|
1058
|
-
|
|
1059
|
-
return `pi-webui --cwd ${quoteCommandArg(cwd)}${currentPortArg()}`;
|
|
1214
|
+
return `pi-webui${currentPortArg()}`;
|
|
1060
1215
|
}
|
|
1061
1216
|
|
|
1062
1217
|
function serverStartSlashCommandText() {
|
|
@@ -1376,6 +1531,46 @@ function attachmentIcon(kind) {
|
|
|
1376
1531
|
return kind === "image" ? "🖼️" : kind === "video" ? "🎞️" : kind === "audio" ? "🎵" : kind === "doc" ? "📄" : "📎";
|
|
1377
1532
|
}
|
|
1378
1533
|
|
|
1534
|
+
function normalizeTextAttachmentContent(text) {
|
|
1535
|
+
return String(text || "").replace(/\r\n?/g, "\n");
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function textLineCount(text) {
|
|
1539
|
+
const normalized = normalizeTextAttachmentContent(text);
|
|
1540
|
+
return normalized ? normalized.split("\n").length : 0;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function shouldAttachTextInsteadOfComposerInput(text) {
|
|
1544
|
+
const normalized = normalizeTextAttachmentContent(text);
|
|
1545
|
+
return normalized.trim().length > 0 && textLineCount(normalized) > LONG_INPUT_ATTACHMENT_LINE_THRESHOLD;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function longInputAttachmentFileName() {
|
|
1549
|
+
const stamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z").replace(/:/g, "-");
|
|
1550
|
+
return `webui-input-${stamp}.txt`;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function makeTextAttachmentFile(text, name = longInputAttachmentFileName(), mimeType = LONG_INPUT_ATTACHMENT_MIME_TYPE) {
|
|
1554
|
+
const normalized = normalizeTextAttachmentContent(text);
|
|
1555
|
+
const fileName = String(name || longInputAttachmentFileName());
|
|
1556
|
+
const type = String(mimeType || LONG_INPUT_ATTACHMENT_MIME_TYPE);
|
|
1557
|
+
if (typeof File === "function") return new File([normalized], fileName, { type });
|
|
1558
|
+
const blob = new Blob([normalized], { type });
|
|
1559
|
+
try {
|
|
1560
|
+
blob.name = fileName;
|
|
1561
|
+
blob.lastModified = Date.now();
|
|
1562
|
+
} catch {
|
|
1563
|
+
// Older browsers may expose non-extensible Blob instances; the attachment record still carries the name.
|
|
1564
|
+
}
|
|
1565
|
+
return blob;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function isEditableTextAttachment(attachment) {
|
|
1569
|
+
const name = String(attachment?.name || "");
|
|
1570
|
+
const mimeType = String(attachment?.mimeType || attachment?.file?.type || inferMimeTypeFromName(name)).split(";", 1)[0].trim().toLowerCase();
|
|
1571
|
+
return mimeType.startsWith("text/") || /(?:json|xml|yaml|toml|markdown|csv)/i.test(mimeType) || /\.(?:txt|md|markdown|csv|json|xml|ya?ml|toml|ini|log)$/i.test(name);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1379
1574
|
function attachmentsForTab(tabId = activeTabId) {
|
|
1380
1575
|
return tabId ? tabAttachments.get(tabId) || [] : [];
|
|
1381
1576
|
}
|
|
@@ -1404,11 +1599,19 @@ function renderAttachmentTray() {
|
|
|
1404
1599
|
const icon = make("span", "attachment-pill-icon", attachmentIcon(attachment.kind));
|
|
1405
1600
|
const name = make("span", "attachment-pill-name", attachment.name);
|
|
1406
1601
|
const meta = make("span", "attachment-pill-meta", `${attachment.kind} · ${formatBytes(attachment.size)}`);
|
|
1602
|
+
const edit = isEditableTextAttachment(attachment) ? make("button", "attachment-edit-button", "Edit") : null;
|
|
1603
|
+
if (edit) {
|
|
1604
|
+
edit.type = "button";
|
|
1605
|
+
edit.setAttribute("aria-label", `Open and edit ${attachment.name}`);
|
|
1606
|
+
edit.addEventListener("click", () => openTextAttachmentEditor(attachment.id));
|
|
1607
|
+
}
|
|
1407
1608
|
const remove = make("button", "attachment-remove-button", "×");
|
|
1408
1609
|
remove.type = "button";
|
|
1409
1610
|
remove.setAttribute("aria-label", `Remove ${attachment.name}`);
|
|
1410
1611
|
remove.addEventListener("click", () => removeAttachment(attachment.id));
|
|
1411
|
-
pill.append(icon, name, meta
|
|
1612
|
+
pill.append(icon, name, meta);
|
|
1613
|
+
if (edit) pill.append(edit);
|
|
1614
|
+
pill.append(remove);
|
|
1412
1615
|
tray.append(pill);
|
|
1413
1616
|
}
|
|
1414
1617
|
}
|
|
@@ -1419,6 +1622,7 @@ function removeAttachment(id, tabId = activeTabId) {
|
|
|
1419
1622
|
if (index === -1) return;
|
|
1420
1623
|
const [removed] = attachments.splice(index, 1);
|
|
1421
1624
|
if (removed?.previewUrl) URL.revokeObjectURL(removed.previewUrl);
|
|
1625
|
+
if (activeTextAttachmentEditor?.tabId === tabId && activeTextAttachmentEditor?.attachmentId === id) closeTextAttachmentEditor();
|
|
1422
1626
|
if (attachments.length === 0) tabAttachments.delete(tabId);
|
|
1423
1627
|
if (tabId === activeTabId) renderAttachmentTray();
|
|
1424
1628
|
}
|
|
@@ -1428,15 +1632,16 @@ function clearAttachments(tabId = activeTabId) {
|
|
|
1428
1632
|
for (const attachment of attachments) {
|
|
1429
1633
|
if (attachment.previewUrl) URL.revokeObjectURL(attachment.previewUrl);
|
|
1430
1634
|
}
|
|
1635
|
+
if (activeTextAttachmentEditor?.tabId === tabId) closeTextAttachmentEditor();
|
|
1431
1636
|
if (tabId) tabAttachments.delete(tabId);
|
|
1432
1637
|
if (tabId === activeTabId) renderAttachmentTray();
|
|
1433
1638
|
}
|
|
1434
1639
|
|
|
1435
1640
|
function addAttachmentFiles(fileList, source = "picker") {
|
|
1436
1641
|
const files = Array.from(fileList || []).filter(Boolean);
|
|
1437
|
-
if (!files.length) return;
|
|
1642
|
+
if (!files.length) return { added: 0, skipped: [] };
|
|
1438
1643
|
const attachments = ensureAttachmentsForTab();
|
|
1439
|
-
if (!attachments.length && !activeTabId) return;
|
|
1644
|
+
if (!attachments.length && !activeTabId) return { added: 0, skipped: ["no active tab"] };
|
|
1440
1645
|
let totalBytes = attachments.reduce((sum, attachment) => sum + attachment.size, 0);
|
|
1441
1646
|
let added = 0;
|
|
1442
1647
|
const skipped = [];
|
|
@@ -1474,6 +1679,129 @@ function addAttachmentFiles(fileList, source = "picker") {
|
|
|
1474
1679
|
renderAttachmentTray();
|
|
1475
1680
|
if (added) addEvent(`attached ${added} ${added === 1 ? "file" : "files"} from ${source}`, "info");
|
|
1476
1681
|
if (skipped.length) addEvent(`skipped attachments: ${skipped.join("; ")}`, "warn");
|
|
1682
|
+
return { added, skipped };
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
function attachLongTextAsFile(text, source = "input text") {
|
|
1686
|
+
if (!shouldAttachTextInsteadOfComposerInput(text)) return false;
|
|
1687
|
+
const normalized = normalizeTextAttachmentContent(text);
|
|
1688
|
+
const lineCount = textLineCount(normalized);
|
|
1689
|
+
const result = addAttachmentFiles([makeTextAttachmentFile(normalized)], `${lineCount}-line ${source}`);
|
|
1690
|
+
return result.added > 0;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function moveLongPromptInputToAttachment() {
|
|
1694
|
+
const text = elements.promptInput.value || "";
|
|
1695
|
+
if (!attachLongTextAsFile(text, "input text")) return false;
|
|
1696
|
+
elements.promptInput.value = "";
|
|
1697
|
+
resizePromptInput();
|
|
1698
|
+
hideCommandSuggestions();
|
|
1699
|
+
return true;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function attachmentById(tabId, id) {
|
|
1703
|
+
return attachmentsForTab(tabId).find((attachment) => attachment.id === id) || null;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function closeTextAttachmentEditor() {
|
|
1707
|
+
if (elements.attachmentTextDialog?.open) elements.attachmentTextDialog.close();
|
|
1708
|
+
else activeTextAttachmentEditor = null;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
function setAttachmentTextStatus(message = "", level = "muted") {
|
|
1712
|
+
if (!elements.attachmentTextStatus) return;
|
|
1713
|
+
elements.attachmentTextStatus.textContent = message;
|
|
1714
|
+
elements.attachmentTextStatus.className = `attachment-text-status ${level || "muted"}`;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function renderTextAttachmentEditorMeta() {
|
|
1718
|
+
if (!activeTextAttachmentEditor || !elements.attachmentTextMeta) return;
|
|
1719
|
+
const attachment = attachmentById(activeTextAttachmentEditor.tabId, activeTextAttachmentEditor.attachmentId);
|
|
1720
|
+
if (!attachment) {
|
|
1721
|
+
elements.attachmentTextMeta.textContent = "Attachment no longer exists.";
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
const text = elements.attachmentTextEditor?.value || "";
|
|
1725
|
+
const lineCount = textLineCount(text);
|
|
1726
|
+
elements.attachmentTextMeta.textContent = `${attachment.name} · ${attachment.mimeType} · ${formatBytes(attachment.size)} · ${lineCount} ${lineCount === 1 ? "line" : "lines"}`;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function readFileAsText(file) {
|
|
1730
|
+
if (typeof file?.text === "function") return file.text();
|
|
1731
|
+
return new Promise((resolve, reject) => {
|
|
1732
|
+
const reader = new FileReader();
|
|
1733
|
+
reader.onerror = () => reject(reader.error || new Error("Failed to read text attachment"));
|
|
1734
|
+
reader.onload = () => resolve(String(reader.result || ""));
|
|
1735
|
+
reader.readAsText(file);
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
async function openTextAttachmentEditor(attachmentId, tabId = activeTabId) {
|
|
1740
|
+
const attachment = attachmentById(tabId, attachmentId);
|
|
1741
|
+
if (!attachment) return;
|
|
1742
|
+
if (!isEditableTextAttachment(attachment)) {
|
|
1743
|
+
addEvent(`${attachment.name || "attachment"} is not editable text`, "warn");
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
activeTextAttachmentEditor = { tabId, attachmentId };
|
|
1748
|
+
if (elements.attachmentTextTitle) elements.attachmentTextTitle.textContent = `Edit ${attachment.name || "text attachment"}`;
|
|
1749
|
+
if (elements.attachmentTextEditor) elements.attachmentTextEditor.value = "";
|
|
1750
|
+
if (elements.attachmentTextSaveButton) elements.attachmentTextSaveButton.disabled = true;
|
|
1751
|
+
renderTextAttachmentEditorMeta();
|
|
1752
|
+
setAttachmentTextStatus("Loading text attachment…", "muted");
|
|
1753
|
+
if (elements.attachmentTextDialog && !elements.attachmentTextDialog.open) elements.attachmentTextDialog.showModal();
|
|
1754
|
+
|
|
1755
|
+
try {
|
|
1756
|
+
const text = await readFileAsText(attachment.file);
|
|
1757
|
+
if (activeTextAttachmentEditor?.tabId !== tabId || activeTextAttachmentEditor?.attachmentId !== attachmentId) return;
|
|
1758
|
+
if (elements.attachmentTextEditor) elements.attachmentTextEditor.value = normalizeTextAttachmentContent(text);
|
|
1759
|
+
if (elements.attachmentTextSaveButton) elements.attachmentTextSaveButton.disabled = false;
|
|
1760
|
+
renderTextAttachmentEditorMeta();
|
|
1761
|
+
setAttachmentTextStatus("Edit the text, then save it back to the attachment.", "muted");
|
|
1762
|
+
queueMicrotask(() => elements.attachmentTextEditor?.focus());
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
if (elements.attachmentTextSaveButton) elements.attachmentTextSaveButton.disabled = true;
|
|
1765
|
+
setAttachmentTextStatus(`Failed to open text attachment: ${error.message || String(error)}`, "error");
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function totalAttachmentBytesWithReplacement(tabId, attachmentId, nextSize) {
|
|
1770
|
+
return attachmentsForTab(tabId).reduce((sum, attachment) => sum + (attachment.id === attachmentId ? nextSize : attachment.size || 0), 0);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function saveTextAttachmentEdit() {
|
|
1774
|
+
if (!activeTextAttachmentEditor) return;
|
|
1775
|
+
const { tabId, attachmentId } = activeTextAttachmentEditor;
|
|
1776
|
+
const attachment = attachmentById(tabId, attachmentId);
|
|
1777
|
+
if (!attachment) {
|
|
1778
|
+
setAttachmentTextStatus("Attachment no longer exists.", "error");
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
const text = elements.attachmentTextEditor?.value || "";
|
|
1783
|
+
const name = attachment.name || longInputAttachmentFileName();
|
|
1784
|
+
const mimeType = attachment.mimeType || inferMimeTypeFromName(name) || LONG_INPUT_ATTACHMENT_MIME_TYPE;
|
|
1785
|
+
const nextFile = makeTextAttachmentFile(text, name, mimeType);
|
|
1786
|
+
if (nextFile.size > ATTACHMENT_MAX_FILE_BYTES) {
|
|
1787
|
+
setAttachmentTextStatus(`Edited file is larger than ${formatBytes(ATTACHMENT_MAX_FILE_BYTES)}.`, "error");
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
if (totalAttachmentBytesWithReplacement(tabId, attachmentId, nextFile.size) > ATTACHMENT_MAX_TOTAL_BYTES) {
|
|
1791
|
+
setAttachmentTextStatus(`Edited attachments exceed ${formatBytes(ATTACHMENT_MAX_TOTAL_BYTES)} total.`, "error");
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
if (attachment.previewUrl) URL.revokeObjectURL(attachment.previewUrl);
|
|
1796
|
+
attachment.file = nextFile;
|
|
1797
|
+
attachment.name = name;
|
|
1798
|
+
attachment.mimeType = nextFile.type || mimeType;
|
|
1799
|
+
attachment.size = nextFile.size || 0;
|
|
1800
|
+
attachment.kind = attachmentKind(attachment.mimeType, attachment.name);
|
|
1801
|
+
attachment.previewUrl = undefined;
|
|
1802
|
+
if (tabId === activeTabId) renderAttachmentTray();
|
|
1803
|
+
addEvent(`updated text attachment ${attachment.name} (${formatBytes(attachment.size)})`, "info");
|
|
1804
|
+
closeTextAttachmentEditor();
|
|
1477
1805
|
}
|
|
1478
1806
|
|
|
1479
1807
|
function clipboardFiles(dataTransfer) {
|
|
@@ -1501,9 +1829,15 @@ function clipboardFiles(dataTransfer) {
|
|
|
1501
1829
|
|
|
1502
1830
|
function handleAttachmentPaste(event) {
|
|
1503
1831
|
const files = clipboardFiles(event.clipboardData);
|
|
1504
|
-
if (
|
|
1832
|
+
if (files.length) {
|
|
1833
|
+
event.preventDefault();
|
|
1834
|
+
addAttachmentFiles(files, "clipboard");
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
const text = event.clipboardData?.getData("text/plain") || "";
|
|
1839
|
+
if (!attachLongTextAsFile(text, "clipboard text")) return;
|
|
1505
1840
|
event.preventDefault();
|
|
1506
|
-
addAttachmentFiles(files, "clipboard");
|
|
1507
1841
|
}
|
|
1508
1842
|
|
|
1509
1843
|
function isFileDrag(event) {
|
|
@@ -2592,7 +2926,7 @@ function restoreActiveDraft() {
|
|
|
2592
2926
|
|
|
2593
2927
|
function focusPromptInput({ defer = false } = {}) {
|
|
2594
2928
|
const focus = () => {
|
|
2595
|
-
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.nativeCommandDialog.open || elements.promptListDialog?.open || document.visibilityState === "hidden") return;
|
|
2929
|
+
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.nativeCommandDialog.open || elements.appRunnerInfoDialog?.open || elements.promptListDialog?.open || elements.attachmentTextDialog?.open || document.visibilityState === "hidden") return;
|
|
2596
2930
|
try {
|
|
2597
2931
|
elements.promptInput.focus({ preventScroll: true });
|
|
2598
2932
|
} catch {
|
|
@@ -2666,6 +3000,7 @@ function resetActiveTabUi() {
|
|
|
2666
3000
|
else renderQueue({ tabId: activeTabId, steering: [], followUp: [] });
|
|
2667
3001
|
elements.commandsBox.textContent = "Loading…";
|
|
2668
3002
|
elements.commandsBox.classList.add("muted");
|
|
3003
|
+
renderAppRunnerControls();
|
|
2669
3004
|
renderWidgets();
|
|
2670
3005
|
renderGitWorkflow();
|
|
2671
3006
|
renderFooter();
|
|
@@ -2888,6 +3223,7 @@ function setNewTabMenuOpen(open) {
|
|
|
2888
3223
|
function openNewTabMenu() {
|
|
2889
3224
|
setPublishMenuOpen(false);
|
|
2890
3225
|
setNativeCommandMenuOpen(false);
|
|
3226
|
+
setAppRunnerMenuOpen(false);
|
|
2891
3227
|
setOptionsMenuOpen(false);
|
|
2892
3228
|
setNewTabMenuOpen(true);
|
|
2893
3229
|
}
|
|
@@ -2973,10 +3309,15 @@ function currentDirectoryForNewTab() {
|
|
|
2973
3309
|
async function createTerminalTab(cwd = currentDirectoryForNewTab(), { triggerButton = elements.newTabButton } = {}) {
|
|
2974
3310
|
setMobileTabsExpanded(false);
|
|
2975
3311
|
setNewTabMenuOpen(false);
|
|
3312
|
+
const resolvedCwd = cwd || currentDirectoryForNewTab();
|
|
3313
|
+
if (!resolvedCwd && tabs.length === 0) {
|
|
3314
|
+
await createTerminalTabFromChosenDirectory({ triggerButton });
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
2976
3317
|
const disabledButtons = new Set([elements.newTabButton, triggerButton].filter(Boolean));
|
|
2977
3318
|
for (const button of disabledButtons) button.disabled = true;
|
|
2978
3319
|
try {
|
|
2979
|
-
const response = await api("/api/tabs", { method: "POST", body: { cwd:
|
|
3320
|
+
const response = await api("/api/tabs", { method: "POST", body: { cwd: resolvedCwd }, scoped: false });
|
|
2980
3321
|
tabs = response.data?.tabs || tabs;
|
|
2981
3322
|
syncTabMetadata(tabs);
|
|
2982
3323
|
const tab = response.data?.tab;
|
|
@@ -3002,6 +3343,17 @@ async function createTerminalTabFromChosenDirectory({ triggerButton = elements.n
|
|
|
3002
3343
|
await createTerminalTab(cwd, { triggerButton });
|
|
3003
3344
|
}
|
|
3004
3345
|
|
|
3346
|
+
async function createFirstTerminalTabFromChosenDirectory() {
|
|
3347
|
+
if (firstTerminalCwdPromptShown || tabs.length > 0) return;
|
|
3348
|
+
firstTerminalCwdPromptShown = true;
|
|
3349
|
+
const cwd = await pickCwd({ id: "first-terminal", title: "first terminal" }, "", { title: "Choose CWD for first terminal" });
|
|
3350
|
+
if (!cwd) {
|
|
3351
|
+
addEvent("choose a CWD to start the first terminal", "warn");
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
await createTerminalTab(cwd, { triggerButton: null });
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3005
3357
|
function tabHasActiveAgent(tab) {
|
|
3006
3358
|
const activity = activityForTab(tab);
|
|
3007
3359
|
const indicator = tabIndicator(tab);
|
|
@@ -3051,6 +3403,7 @@ async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } =
|
|
|
3051
3403
|
tabDrafts.delete(id);
|
|
3052
3404
|
clearAttachments(id);
|
|
3053
3405
|
clearGitWorkflowForTab(id);
|
|
3406
|
+
appRunnerDataByTab.delete(id);
|
|
3054
3407
|
}
|
|
3055
3408
|
clearOpenTerminalTabGroup(null, { force: true });
|
|
3056
3409
|
|
|
@@ -3093,10 +3446,14 @@ async function closeAllTerminalTabs() {
|
|
|
3093
3446
|
}
|
|
3094
3447
|
|
|
3095
3448
|
async function initializeTabs() {
|
|
3096
|
-
await refreshTabs({ selectStored: true });
|
|
3449
|
+
const loadedTabs = await refreshTabs({ selectStored: true });
|
|
3097
3450
|
resetActiveTabUi();
|
|
3098
3451
|
renderTabs();
|
|
3099
3452
|
restoreActiveDraft();
|
|
3453
|
+
if (!loadedTabs.length) {
|
|
3454
|
+
await createFirstTerminalTabFromChosenDirectory();
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3100
3457
|
focusPromptInput({ defer: true });
|
|
3101
3458
|
const tabContext = activeTabContext();
|
|
3102
3459
|
connectEvents(tabContext);
|
|
@@ -3582,6 +3939,50 @@ function footerCostAuthLabel() {
|
|
|
3582
3939
|
return /codex|copilot|chatgpt/i.test(provider) ? "sub" : "api";
|
|
3583
3940
|
}
|
|
3584
3941
|
|
|
3942
|
+
function contextWindowFromSources(...sources) {
|
|
3943
|
+
for (const source of sources) {
|
|
3944
|
+
const value = typeof source === "object" && source !== null ? source.contextWindow : source;
|
|
3945
|
+
const contextWindow = Number(value);
|
|
3946
|
+
if (Number.isFinite(contextWindow) && contextWindow > 0) return contextWindow;
|
|
3947
|
+
}
|
|
3948
|
+
return 0;
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
function contextUsageUnknownAfterCompaction(tabId = activeTabId) {
|
|
3952
|
+
return !!tabId && contextUsageUnknownAfterCompactionByTab.has(tabId);
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
function unknownFooterContextText(contextUsage = null) {
|
|
3956
|
+
const contextWindow = contextWindowFromSources(
|
|
3957
|
+
contextUsage,
|
|
3958
|
+
latestStats?.contextUsage,
|
|
3959
|
+
currentState?.contextUsage,
|
|
3960
|
+
currentState?.model?.contextWindow,
|
|
3961
|
+
);
|
|
3962
|
+
return contextWindow ? `?/${formatFooterTokenCount(contextWindow)}` : "?";
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
function contextUsageWithUnknownPercent(contextUsage = null) {
|
|
3966
|
+
return {
|
|
3967
|
+
...(contextUsage || {}),
|
|
3968
|
+
percent: null,
|
|
3969
|
+
contextWindow: contextWindowFromSources(contextUsage, latestStats?.contextUsage, currentState?.contextUsage, currentState?.model?.contextWindow),
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
function markContextUsageUnknownAfterCompaction(tabId = activeTabId) {
|
|
3974
|
+
if (!tabId) return;
|
|
3975
|
+
contextUsageUnknownAfterCompactionByTab.set(tabId, Date.now());
|
|
3976
|
+
if (tabId !== activeTabId) return;
|
|
3977
|
+
if (currentState) currentState = { ...currentState, contextUsage: contextUsageWithUnknownPercent(currentState.contextUsage) };
|
|
3978
|
+
if (latestStats) latestStats = { ...latestStats, contextUsage: contextUsageWithUnknownPercent(latestStats.contextUsage) };
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
function clearContextUsageUnknownAfterCompaction(tabId = activeTabId) {
|
|
3982
|
+
if (!tabId) return;
|
|
3983
|
+
contextUsageUnknownAfterCompactionByTab.delete(tabId);
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3585
3986
|
function footerStatsTokensDisplay(stats = latestStats) {
|
|
3586
3987
|
const tokens = stats?.tokens;
|
|
3587
3988
|
if (!tokens) return "";
|
|
@@ -3602,7 +4003,8 @@ function footerContextDisplayWithAuto(value, state = currentState) {
|
|
|
3602
4003
|
|
|
3603
4004
|
function footerStatsContextDisplay(stats = latestStats) {
|
|
3604
4005
|
const usage = stats?.contextUsage || currentState?.contextUsage;
|
|
3605
|
-
const contextWindow = usage
|
|
4006
|
+
const contextWindow = contextWindowFromSources(usage, currentState?.model?.contextWindow);
|
|
4007
|
+
if (contextUsageUnknownAfterCompaction()) return footerContextDisplayWithAuto(unknownFooterContextText(usage));
|
|
3606
4008
|
if (!contextWindow) return "";
|
|
3607
4009
|
const rawPercent = Number(usage?.percent);
|
|
3608
4010
|
const percent = Number.isFinite(rawPercent) ? `${rawPercent.toFixed(1)}%` : "?";
|
|
@@ -3901,8 +4303,10 @@ function footerPayloadWithLiveModel(payload) {
|
|
|
3901
4303
|
const effort = footerThinkingDisplay();
|
|
3902
4304
|
const hasThinkingChip = [...payload.main, ...payload.meta].some((chip) => chip?.key === "thinking");
|
|
3903
4305
|
const contextChip = (chip) => {
|
|
3904
|
-
const
|
|
3905
|
-
|
|
4306
|
+
const usageUnknown = contextUsageUnknownAfterCompaction();
|
|
4307
|
+
const value = usageUnknown ? footerContextDisplayWithAuto(unknownFooterContextText(chip?.contextUsage)) : footerContextDisplayWithAuto(chip?.value);
|
|
4308
|
+
const contextUsage = usageUnknown ? contextUsageWithUnknownPercent(chip?.contextUsage) : chip?.contextUsage;
|
|
4309
|
+
return { ...chip, value, title: `context: ${value}`, ...(contextUsage ? { contextUsage } : {}) };
|
|
3906
4310
|
};
|
|
3907
4311
|
const effortChip = (chip) => ({ ...chip, key: "thinking", label: "effort", value: effort, title: `effort: ${effort}`, tone: "mauve" });
|
|
3908
4312
|
const splitChip = (chip) => {
|
|
@@ -5246,10 +5650,534 @@ function renderReleaseAurLogWidget() {
|
|
|
5246
5650
|
const logLines = lines.slice(2).filter((line, index) => index > 0 || stripAnsi(line).trim());
|
|
5247
5651
|
const streamHeader = releaseNpmStreamHeader("Saved AUR output stream", logLines.length);
|
|
5248
5652
|
const terminal = make("div", "release-npm-terminal");
|
|
5249
|
-
for (const line of logLines) {
|
|
5250
|
-
appendReleaseNpmTerminalLine(terminal, line);
|
|
5251
|
-
}
|
|
5252
|
-
const outputDetails = renderReleaseNpmOutputDetails("release-aur:logs", streamHeader, terminal);
|
|
5653
|
+
for (const line of logLines) {
|
|
5654
|
+
appendReleaseNpmTerminalLine(terminal, line);
|
|
5655
|
+
}
|
|
5656
|
+
const outputDetails = renderReleaseNpmOutputDetails("release-aur:logs", streamHeader, terminal);
|
|
5657
|
+
node.append(header, outputDetails);
|
|
5658
|
+
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
5659
|
+
return node;
|
|
5660
|
+
}
|
|
5661
|
+
|
|
5662
|
+
function activeAppRunnerData() {
|
|
5663
|
+
return activeTabId ? appRunnerDataByTab.get(activeTabId) || { runners: [], activeRun: null } : { runners: [], activeRun: null };
|
|
5664
|
+
}
|
|
5665
|
+
|
|
5666
|
+
function setAppRunnerData(tabId, data = {}) {
|
|
5667
|
+
if (!tabId) return;
|
|
5668
|
+
const previous = appRunnerDataByTab.get(tabId) || { runners: [], activeRun: null, customRunnerConfig: null };
|
|
5669
|
+
appRunnerDataByTab.set(tabId, {
|
|
5670
|
+
cwd: data.cwd || previous.cwd || "",
|
|
5671
|
+
runners: Array.isArray(data.runners) ? data.runners : previous.runners || [],
|
|
5672
|
+
customRunnerConfig: data.customRunnerConfig || previous.customRunnerConfig || null,
|
|
5673
|
+
activeRun: Object.prototype.hasOwnProperty.call(data, "activeRun") ? data.activeRun : previous.activeRun || null,
|
|
5674
|
+
});
|
|
5675
|
+
}
|
|
5676
|
+
|
|
5677
|
+
function appRunnerIsRunning(run) {
|
|
5678
|
+
return run?.status === "running" || run?.stopping === true;
|
|
5679
|
+
}
|
|
5680
|
+
|
|
5681
|
+
function appRunnerStatusLabel(run) {
|
|
5682
|
+
if (run?.stopping && run.status === "running") return "stopping";
|
|
5683
|
+
if (run?.status === "done") return "exit 0";
|
|
5684
|
+
if (run?.status === "failed") return run.signal ? `signal ${run.signal}` : `exit ${run.exitCode ?? "?"}`;
|
|
5685
|
+
if (run?.status === "error") return "error";
|
|
5686
|
+
return run?.status || "running";
|
|
5687
|
+
}
|
|
5688
|
+
|
|
5689
|
+
function appRunnerElapsedLabel(run) {
|
|
5690
|
+
const startedAt = Date.parse(run?.startedAt || "");
|
|
5691
|
+
if (!Number.isFinite(startedAt)) return "";
|
|
5692
|
+
const endedAt = Date.parse(run?.endedAt || "");
|
|
5693
|
+
const end = Number.isFinite(endedAt) ? endedAt : Date.now();
|
|
5694
|
+
return formatDuration(end - startedAt);
|
|
5695
|
+
}
|
|
5696
|
+
|
|
5697
|
+
function appRunnerActionButton(label, handler, className = "") {
|
|
5698
|
+
const button = make("button", `release-npm-action ${className}`.trim(), label);
|
|
5699
|
+
button.type = "button";
|
|
5700
|
+
button.addEventListener("click", handler);
|
|
5701
|
+
return button;
|
|
5702
|
+
}
|
|
5703
|
+
|
|
5704
|
+
async function refreshAppRunners(tabContext = activeTabContext()) {
|
|
5705
|
+
if (!tabContext.tabId) return;
|
|
5706
|
+
const response = await api("/api/app-runners", { tabId: tabContext.tabId });
|
|
5707
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5708
|
+
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
5709
|
+
renderAppRunnerControls();
|
|
5710
|
+
renderWidgets();
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
async function runAppRunner(runnerId) {
|
|
5714
|
+
const tabContext = activeTabContext();
|
|
5715
|
+
if (!tabContext.tabId || !runnerId) return;
|
|
5716
|
+
setComposerActionsOpen(false);
|
|
5717
|
+
setAppRunnerMenuOpen(false);
|
|
5718
|
+
try {
|
|
5719
|
+
const response = await api("/api/app-runner", { method: "POST", body: { runnerId }, tabId: tabContext.tabId });
|
|
5720
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5721
|
+
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
5722
|
+
renderAppRunnerControls();
|
|
5723
|
+
renderWidgets();
|
|
5724
|
+
const command = response.data?.activeRun?.displayCommand || "app runner";
|
|
5725
|
+
addEvent(`started ${command}`, "info");
|
|
5726
|
+
} catch (error) {
|
|
5727
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
|
|
5731
|
+
async function stopAppRunner() {
|
|
5732
|
+
const tabContext = activeTabContext();
|
|
5733
|
+
if (!tabContext.tabId) return;
|
|
5734
|
+
try {
|
|
5735
|
+
const response = await api("/api/app-runner/stop", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
5736
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5737
|
+
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
5738
|
+
renderAppRunnerControls();
|
|
5739
|
+
renderWidgets();
|
|
5740
|
+
addEvent("app runner stop requested", "warn");
|
|
5741
|
+
} catch (error) {
|
|
5742
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
5743
|
+
}
|
|
5744
|
+
}
|
|
5745
|
+
|
|
5746
|
+
async function clearAppRunner() {
|
|
5747
|
+
const tabContext = activeTabContext();
|
|
5748
|
+
if (!tabContext.tabId) return;
|
|
5749
|
+
try {
|
|
5750
|
+
const response = await api("/api/app-runner/clear", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
5751
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5752
|
+
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
5753
|
+
renderAppRunnerControls();
|
|
5754
|
+
renderWidgets();
|
|
5755
|
+
} catch (error) {
|
|
5756
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
|
|
5760
|
+
function appRunnerOutputText(run) {
|
|
5761
|
+
const lines = Array.isArray(run?.lines) ? run.lines : [];
|
|
5762
|
+
return lines.join("\n").trimEnd();
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5765
|
+
async function copyAppRunnerOutput(run) {
|
|
5766
|
+
const text = appRunnerOutputText(run);
|
|
5767
|
+
if (!text.trim()) {
|
|
5768
|
+
addEvent("app runner output is empty", "warn");
|
|
5769
|
+
return;
|
|
5770
|
+
}
|
|
5771
|
+
try {
|
|
5772
|
+
await copyText(text);
|
|
5773
|
+
addEvent("copied app runner output", "info");
|
|
5774
|
+
} catch (error) {
|
|
5775
|
+
addEvent(`app runner output copy failed: ${error.message || String(error)}`, "warn");
|
|
5776
|
+
}
|
|
5777
|
+
}
|
|
5778
|
+
|
|
5779
|
+
const APP_RUNNER_SUPPORTED_ITEMS = [
|
|
5780
|
+
"Project-local custom runners from .pi-webui-runners.json",
|
|
5781
|
+
"package.json scripts: bun/npm/pnpm/yarn dev, start, serve",
|
|
5782
|
+
"npx frameworks: Vite, Next, Astro, Storybook",
|
|
5783
|
+
"Rust: cargo run",
|
|
5784
|
+
"Python: uv run or python entry files such as Main.py, main.py, src/main.py",
|
|
5785
|
+
"Go/Golang: go run",
|
|
5786
|
+
"Zig: zig build run or zig run",
|
|
5787
|
+
"C/C++: CMake, cc/c++ main files",
|
|
5788
|
+
"Docker Compose: docker compose up",
|
|
5789
|
+
"Shell scripts: bash/zsh/fish in root, dev/, scripts/, dev/scripts/",
|
|
5790
|
+
"Deno, make, just, and plain Node entry files",
|
|
5791
|
+
];
|
|
5792
|
+
const APP_RUNNER_SUPPORTED_TOOLTIP = [
|
|
5793
|
+
"No app runner detected for this tab cwd.",
|
|
5794
|
+
"",
|
|
5795
|
+
"Currently supported:",
|
|
5796
|
+
...APP_RUNNER_SUPPORTED_ITEMS.map((item) => `• ${item}`),
|
|
5797
|
+
].join("\n");
|
|
5798
|
+
|
|
5799
|
+
function appRunnerMenuCanOpen() {
|
|
5800
|
+
const data = activeAppRunnerData();
|
|
5801
|
+
return Array.isArray(data.runners) && data.runners.length > 0 && !appRunnerIsRunning(data.activeRun);
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
function activeAppRunnerCustomConfig() {
|
|
5805
|
+
return activeAppRunnerData().customRunnerConfig || { runners: [], projectRoot: "", displayProjectRoot: "", displayConfigFile: "" };
|
|
5806
|
+
}
|
|
5807
|
+
|
|
5808
|
+
function resetAppRunnerCustomDraft() {
|
|
5809
|
+
appRunnerCustomDraft = { id: "", label: "", command: "./", path: "", args: "" };
|
|
5810
|
+
appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
function appRunnerRelativeDir(filePath) {
|
|
5814
|
+
const normalized = String(filePath || "").replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
5815
|
+
const index = normalized.lastIndexOf("/");
|
|
5816
|
+
return index === -1 ? "" : normalized.slice(0, index);
|
|
5817
|
+
}
|
|
5818
|
+
|
|
5819
|
+
function appRunnerCustomArgsText(args) {
|
|
5820
|
+
return Array.isArray(args) ? args.join(" ") : String(args || "");
|
|
5821
|
+
}
|
|
5822
|
+
|
|
5823
|
+
function appRunnerCustomDraftPayload() {
|
|
5824
|
+
return {
|
|
5825
|
+
id: appRunnerCustomDraft.id || undefined,
|
|
5826
|
+
label: appRunnerCustomDraft.label.trim(),
|
|
5827
|
+
command: appRunnerCustomDraft.command.trim() || "./",
|
|
5828
|
+
path: appRunnerCustomDraft.path.trim(),
|
|
5829
|
+
args: appRunnerCustomDraft.args.trim(),
|
|
5830
|
+
};
|
|
5831
|
+
}
|
|
5832
|
+
|
|
5833
|
+
function updateAppRunnerCustomDraftFrom(container) {
|
|
5834
|
+
if (!container) return;
|
|
5835
|
+
appRunnerCustomDraft = {
|
|
5836
|
+
id: appRunnerCustomDraft.id || "",
|
|
5837
|
+
label: container.querySelector("#appRunnerCustomLabelInput")?.value || "",
|
|
5838
|
+
command: container.querySelector("#appRunnerCustomCommandInput")?.value || "./",
|
|
5839
|
+
path: container.querySelector("#appRunnerCustomPathInput")?.value || "",
|
|
5840
|
+
args: container.querySelector("#appRunnerCustomArgsInput")?.value || "",
|
|
5841
|
+
};
|
|
5842
|
+
}
|
|
5843
|
+
|
|
5844
|
+
function appRunnerInputField({ id, label, value, placeholder = "", hint = "" }) {
|
|
5845
|
+
const field = make("label", "app-runner-custom-field");
|
|
5846
|
+
field.setAttribute("for", id);
|
|
5847
|
+
field.append(make("span", "", label));
|
|
5848
|
+
const input = make("input", "dialog-input");
|
|
5849
|
+
input.id = id;
|
|
5850
|
+
input.type = "text";
|
|
5851
|
+
input.value = value || "";
|
|
5852
|
+
input.placeholder = placeholder;
|
|
5853
|
+
input.autocomplete = "off";
|
|
5854
|
+
input.spellcheck = false;
|
|
5855
|
+
field.append(input);
|
|
5856
|
+
if (hint) field.append(make("small", "muted", hint));
|
|
5857
|
+
input.addEventListener("input", () => updateAppRunnerCustomDraftFrom(field.closest(".app-runner-custom-form")));
|
|
5858
|
+
input.addEventListener("keydown", (event) => {
|
|
5859
|
+
if (event.key !== "Enter") return;
|
|
5860
|
+
event.preventDefault();
|
|
5861
|
+
saveAppRunnerCustomRunner(field.closest(".app-runner-custom-form"));
|
|
5862
|
+
});
|
|
5863
|
+
return { field, input };
|
|
5864
|
+
}
|
|
5865
|
+
|
|
5866
|
+
async function saveAppRunnerCustomRunner(form) {
|
|
5867
|
+
updateAppRunnerCustomDraftFrom(form);
|
|
5868
|
+
const payload = appRunnerCustomDraftPayload();
|
|
5869
|
+
if (!payload.path) {
|
|
5870
|
+
addEvent("custom app runner path is required", "warn");
|
|
5871
|
+
form?.querySelector("#appRunnerCustomPathInput")?.focus();
|
|
5872
|
+
return;
|
|
5873
|
+
}
|
|
5874
|
+
const tabContext = activeTabContext();
|
|
5875
|
+
try {
|
|
5876
|
+
const response = await api("/api/app-runner-config", { method: "POST", body: { runner: payload }, tabId: tabContext.tabId });
|
|
5877
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5878
|
+
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
5879
|
+
resetAppRunnerCustomDraft();
|
|
5880
|
+
renderAppRunnerControls();
|
|
5881
|
+
renderWidgets();
|
|
5882
|
+
renderAppRunnerInfoDialog();
|
|
5883
|
+
addEvent("saved custom app runner", "info");
|
|
5884
|
+
} catch (error) {
|
|
5885
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
5886
|
+
}
|
|
5887
|
+
}
|
|
5888
|
+
|
|
5889
|
+
async function deleteAppRunnerCustomRunner(id) {
|
|
5890
|
+
const tabContext = activeTabContext();
|
|
5891
|
+
try {
|
|
5892
|
+
const response = await api("/api/app-runner-config", { method: "DELETE", body: { id }, tabId: tabContext.tabId });
|
|
5893
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5894
|
+
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
5895
|
+
if (appRunnerCustomDraft.id === id) resetAppRunnerCustomDraft();
|
|
5896
|
+
renderAppRunnerControls();
|
|
5897
|
+
renderWidgets();
|
|
5898
|
+
renderAppRunnerInfoDialog();
|
|
5899
|
+
addEvent("deleted custom app runner", "warn");
|
|
5900
|
+
} catch (error) {
|
|
5901
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5905
|
+
async function loadAppRunnerFileBrowser(relativePath = "") {
|
|
5906
|
+
const tabContext = activeTabContext();
|
|
5907
|
+
const path = String(relativePath || "").replace(/^\.\/+/, "").replace(/\/+$/g, "");
|
|
5908
|
+
appRunnerFileBrowserState = { open: true, loading: true, path, data: null, error: "" };
|
|
5909
|
+
renderAppRunnerInfoDialog();
|
|
5910
|
+
try {
|
|
5911
|
+
const response = await api(`/api/app-runner-files?path=${encodeURIComponent(path)}`, { tabId: tabContext.tabId });
|
|
5912
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5913
|
+
appRunnerFileBrowserState = { open: true, loading: false, path, data: response.data || {}, error: "" };
|
|
5914
|
+
renderAppRunnerInfoDialog();
|
|
5915
|
+
} catch (error) {
|
|
5916
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5917
|
+
appRunnerFileBrowserState = { open: true, loading: false, path, data: null, error: error.message || String(error) };
|
|
5918
|
+
renderAppRunnerInfoDialog();
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
|
|
5922
|
+
function renderAppRunnerFileBrowser() {
|
|
5923
|
+
if (!appRunnerFileBrowserState.open) return null;
|
|
5924
|
+
const browser = make("div", "app-runner-file-browser");
|
|
5925
|
+
if (appRunnerFileBrowserState.loading) {
|
|
5926
|
+
browser.append(make("div", "muted", "Loading project files…"));
|
|
5927
|
+
return browser;
|
|
5928
|
+
}
|
|
5929
|
+
if (appRunnerFileBrowserState.error) {
|
|
5930
|
+
browser.append(make("div", "path-picker-error", appRunnerFileBrowserState.error));
|
|
5931
|
+
return browser;
|
|
5932
|
+
}
|
|
5933
|
+
const data = appRunnerFileBrowserState.data || {};
|
|
5934
|
+
const header = make("div", "app-runner-file-browser-header");
|
|
5935
|
+
header.append(make("strong", "", data.displayRelativeDir || "."));
|
|
5936
|
+
const close = make("button", "app-runner-file-browser-close", "Hide browser");
|
|
5937
|
+
close.type = "button";
|
|
5938
|
+
close.addEventListener("click", () => {
|
|
5939
|
+
appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
5940
|
+
renderAppRunnerInfoDialog();
|
|
5941
|
+
});
|
|
5942
|
+
header.append(close);
|
|
5943
|
+
browser.append(header);
|
|
5944
|
+
|
|
5945
|
+
const roots = make("div", "path-picker-roots app-runner-file-browser-roots");
|
|
5946
|
+
if (data.parent !== null && data.parent !== undefined) roots.append(pathPickerButton("↑ Parent", data.parent || ".", () => loadAppRunnerFileBrowser(data.parent || ""), "path-picker-root-button"));
|
|
5947
|
+
roots.append(pathPickerButton("Project root", data.displayProjectRoot || "Project root", () => loadAppRunnerFileBrowser(""), "path-picker-root-button"));
|
|
5948
|
+
browser.append(roots);
|
|
5949
|
+
|
|
5950
|
+
const list = make("div", "path-picker-list app-runner-file-browser-list");
|
|
5951
|
+
const directories = Array.isArray(data.directories) ? data.directories : [];
|
|
5952
|
+
const files = Array.isArray(data.files) ? data.files : [];
|
|
5953
|
+
for (const directory of directories) {
|
|
5954
|
+
const button = pathPickerButton(`${directory.name}/`, directory.path, () => loadAppRunnerFileBrowser(directory.path), `path-picker-directory${directory.hidden ? " hidden-directory" : ""}`);
|
|
5955
|
+
list.append(button);
|
|
5956
|
+
}
|
|
5957
|
+
for (const file of files) {
|
|
5958
|
+
const button = pathPickerButton(file.name, file.path, () => {
|
|
5959
|
+
appRunnerCustomDraft.path = file.path;
|
|
5960
|
+
appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
5961
|
+
renderAppRunnerInfoDialog();
|
|
5962
|
+
}, `path-picker-directory app-runner-file-choice${file.hidden ? " hidden-directory" : ""}`);
|
|
5963
|
+
list.append(button);
|
|
5964
|
+
}
|
|
5965
|
+
if (!directories.length && !files.length) list.append(make("div", "path-picker-empty muted", "No files in this directory."));
|
|
5966
|
+
browser.append(list);
|
|
5967
|
+
if (data.truncated) browser.append(make("div", "path-picker-error", "Showing the first project entries only."));
|
|
5968
|
+
return browser;
|
|
5969
|
+
}
|
|
5970
|
+
|
|
5971
|
+
function renderAppRunnerCustomSection() {
|
|
5972
|
+
const config = activeAppRunnerCustomConfig();
|
|
5973
|
+
const section = make("section", "app-runner-info-section app-runner-custom-section");
|
|
5974
|
+
const titleRow = make("div", "app-runner-section-title-row");
|
|
5975
|
+
titleRow.append(make("h3", "", "Custom project runners"));
|
|
5976
|
+
if (config.displayConfigFile) titleRow.append(make("code", "", config.displayConfigFile));
|
|
5977
|
+
section.append(titleRow);
|
|
5978
|
+
section.append(make("p", "muted", "Add project-local runners saved in .pi-webui-runners.json. Command defaults to ./, so a selected file runs as ./path/to/file."));
|
|
5979
|
+
|
|
5980
|
+
const existing = make("div", "app-runner-custom-list");
|
|
5981
|
+
const customRunners = Array.isArray(config.runners) ? config.runners : [];
|
|
5982
|
+
if (!customRunners.length) {
|
|
5983
|
+
existing.append(make("div", "app-runner-custom-empty muted", "No custom runners saved for this project yet."));
|
|
5984
|
+
} else {
|
|
5985
|
+
for (const runner of customRunners) {
|
|
5986
|
+
const row = make("div", "app-runner-custom-item");
|
|
5987
|
+
const details = make("div", "app-runner-custom-item-details");
|
|
5988
|
+
details.append(make("strong", "", runner.label || runner.path || "custom runner"), make("code", "", runner.displayCommand || runner.path || ""));
|
|
5989
|
+
const actions = make("div", "app-runner-custom-item-actions");
|
|
5990
|
+
const edit = make("button", "", "Edit");
|
|
5991
|
+
edit.type = "button";
|
|
5992
|
+
edit.addEventListener("click", () => {
|
|
5993
|
+
appRunnerCustomDraft = { id: runner.id || "", label: runner.label || "", command: runner.command || "./", path: runner.path || "", args: appRunnerCustomArgsText(runner.args) };
|
|
5994
|
+
appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
5995
|
+
renderAppRunnerInfoDialog();
|
|
5996
|
+
});
|
|
5997
|
+
const remove = make("button", "danger", "Delete");
|
|
5998
|
+
remove.type = "button";
|
|
5999
|
+
remove.addEventListener("click", () => {
|
|
6000
|
+
if (!confirm(`Delete custom app runner “${runner.label || runner.path || runner.id}”?`)) return;
|
|
6001
|
+
deleteAppRunnerCustomRunner(runner.id);
|
|
6002
|
+
});
|
|
6003
|
+
actions.append(edit, remove);
|
|
6004
|
+
row.append(details, actions);
|
|
6005
|
+
existing.append(row);
|
|
6006
|
+
}
|
|
6007
|
+
}
|
|
6008
|
+
section.append(existing);
|
|
6009
|
+
|
|
6010
|
+
const form = make("div", "app-runner-custom-form");
|
|
6011
|
+
const labelField = appRunnerInputField({ id: "appRunnerCustomLabelInput", label: "Label", value: appRunnerCustomDraft.label, placeholder: "My app" });
|
|
6012
|
+
const commandField = appRunnerInputField({ id: "appRunnerCustomCommandInput", label: "Command", value: appRunnerCustomDraft.command || "./", placeholder: "./", hint: "Use ./ to execute the selected file directly, or use bash, python3, node, bun, uv run, etc." });
|
|
6013
|
+
const pathField = appRunnerInputField({ id: "appRunnerCustomPathInput", label: "Path to file", value: appRunnerCustomDraft.path, placeholder: "dev/scripts/start.sh" });
|
|
6014
|
+
const pathRow = make("div", "app-runner-custom-path-row");
|
|
6015
|
+
pathRow.append(pathField.field);
|
|
6016
|
+
const browse = make("button", "app-runner-custom-browse", "Browse…");
|
|
6017
|
+
browse.type = "button";
|
|
6018
|
+
browse.addEventListener("click", () => {
|
|
6019
|
+
updateAppRunnerCustomDraftFrom(form);
|
|
6020
|
+
loadAppRunnerFileBrowser(appRunnerRelativeDir(appRunnerCustomDraft.path));
|
|
6021
|
+
});
|
|
6022
|
+
pathRow.append(browse);
|
|
6023
|
+
const argsField = appRunnerInputField({ id: "appRunnerCustomArgsInput", label: "Args", value: appRunnerCustomDraft.args, placeholder: "--port 3000", hint: "Optional extra args, space-separated." });
|
|
6024
|
+
form.append(labelField.field, commandField.field, pathRow, argsField.field);
|
|
6025
|
+
const formActions = make("div", "app-runner-custom-form-actions");
|
|
6026
|
+
const save = make("button", "primary", appRunnerCustomDraft.id ? "Save changes" : "Add runner");
|
|
6027
|
+
save.type = "button";
|
|
6028
|
+
save.addEventListener("click", () => saveAppRunnerCustomRunner(form));
|
|
6029
|
+
const reset = make("button", "", "Reset");
|
|
6030
|
+
reset.type = "button";
|
|
6031
|
+
reset.addEventListener("click", () => { resetAppRunnerCustomDraft(); renderAppRunnerInfoDialog(); });
|
|
6032
|
+
formActions.append(save, reset);
|
|
6033
|
+
form.append(formActions);
|
|
6034
|
+
const browser = renderAppRunnerFileBrowser();
|
|
6035
|
+
if (browser) form.append(browser);
|
|
6036
|
+
section.append(form);
|
|
6037
|
+
return section;
|
|
6038
|
+
}
|
|
6039
|
+
|
|
6040
|
+
function renderAppRunnerControls() {
|
|
6041
|
+
const menu = elements.appRunnerMenu;
|
|
6042
|
+
const button = elements.appRunnerMenuButton;
|
|
6043
|
+
const panel = elements.appRunnerMenuPanel;
|
|
6044
|
+
if (!menu || !button || !panel) return;
|
|
6045
|
+
const data = activeAppRunnerData();
|
|
6046
|
+
const runners = Array.isArray(data.runners) ? data.runners : [];
|
|
6047
|
+
const activeRun = data.activeRun;
|
|
6048
|
+
const running = appRunnerIsRunning(activeRun);
|
|
6049
|
+
menu.hidden = false;
|
|
6050
|
+
menu.classList.toggle("has-runners", runners.length > 0);
|
|
6051
|
+
if (elements.appRunnerInfoButton) {
|
|
6052
|
+
elements.appRunnerInfoButton.hidden = runners.length === 0;
|
|
6053
|
+
elements.appRunnerInfoButton.disabled = runners.length === 0;
|
|
6054
|
+
}
|
|
6055
|
+
button.disabled = running;
|
|
6056
|
+
button.title = running
|
|
6057
|
+
? `App runner already running: ${activeRun.displayCommand || activeRun.label || "runner"}`
|
|
6058
|
+
: runners.length
|
|
6059
|
+
? "Run a detected app runner"
|
|
6060
|
+
: "No app runners detected in this tab working directory";
|
|
6061
|
+
button.dataset.tooltip = runners.length ? "App runners: run detected project commands in this tab's working directory." : APP_RUNNER_SUPPORTED_TOOLTIP;
|
|
6062
|
+
button.setAttribute("aria-label", button.title);
|
|
6063
|
+
if (!runners.length || running) setAppRunnerMenuOpen(false);
|
|
6064
|
+
|
|
6065
|
+
panel.replaceChildren();
|
|
6066
|
+
for (const runner of runners) {
|
|
6067
|
+
const item = make("button", "composer-publish-menu-item composer-app-runner-menu-item");
|
|
6068
|
+
item.type = "button";
|
|
6069
|
+
item.setAttribute("role", "menuitem");
|
|
6070
|
+
const runnerDisplayCommand = runner.shortDisplayCommand || runner.displayCommand;
|
|
6071
|
+
item.title = runner.description ? `${runnerDisplayCommand}\n${runner.description}` : runnerDisplayCommand;
|
|
6072
|
+
item.addEventListener("click", () => runAppRunner(runner.id));
|
|
6073
|
+
const label = make("span", "app-runner-menu-item-label", runner.label || runnerDisplayCommand);
|
|
6074
|
+
const command = make("span", "app-runner-menu-item-command", runnerDisplayCommand);
|
|
6075
|
+
item.append(label, command);
|
|
6076
|
+
panel.append(item);
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
|
|
6080
|
+
function renderAppRunnerInfoDialog() {
|
|
6081
|
+
const body = elements.appRunnerInfoBody;
|
|
6082
|
+
if (!body) return;
|
|
6083
|
+
const data = activeAppRunnerData();
|
|
6084
|
+
const runners = Array.isArray(data.runners) ? data.runners : [];
|
|
6085
|
+
body.replaceChildren();
|
|
6086
|
+
|
|
6087
|
+
const current = make("section", "app-runner-info-section");
|
|
6088
|
+
current.append(make("h3", "", "Detected in this tab"));
|
|
6089
|
+
if (runners.length) {
|
|
6090
|
+
const list = make("ul", "app-runner-info-list app-runner-info-detected-list");
|
|
6091
|
+
for (const runner of runners) {
|
|
6092
|
+
const command = runner.shortDisplayCommand || runner.displayCommand || runner.command || runner.id;
|
|
6093
|
+
const item = make("li");
|
|
6094
|
+
item.append(
|
|
6095
|
+
make("strong", "", runner.label || command || "runner"),
|
|
6096
|
+
make("code", "", command || "detected command"),
|
|
6097
|
+
);
|
|
6098
|
+
if (runner.description) item.append(make("span", "", runner.description));
|
|
6099
|
+
list.append(item);
|
|
6100
|
+
}
|
|
6101
|
+
current.append(list);
|
|
6102
|
+
} else {
|
|
6103
|
+
current.append(make("p", "muted", "No runners are currently detected for this tab working directory."));
|
|
6104
|
+
}
|
|
6105
|
+
|
|
6106
|
+
const how = make("section", "app-runner-info-section");
|
|
6107
|
+
how.append(make("h3", "", "How it works"));
|
|
6108
|
+
const howList = make("ul", "app-runner-info-list");
|
|
6109
|
+
for (const line of [
|
|
6110
|
+
"Detection is scoped to the active terminal tab's current working directory.",
|
|
6111
|
+
"Only commands/files that exist and runner binaries available on this system are shown.",
|
|
6112
|
+
"Starting a runner keeps live output pinned above the chat/terminal area.",
|
|
6113
|
+
"Only one app runner can be active per tab; Close/Stop terminates the process/server.",
|
|
6114
|
+
]) howList.append(make("li", "", line));
|
|
6115
|
+
how.append(howList);
|
|
6116
|
+
|
|
6117
|
+
const supported = make("section", "app-runner-info-section");
|
|
6118
|
+
supported.append(make("h3", "", "Supported runner types"));
|
|
6119
|
+
const supportedList = make("ul", "app-runner-info-list app-runner-info-supported-list");
|
|
6120
|
+
for (const itemText of APP_RUNNER_SUPPORTED_ITEMS) supportedList.append(make("li", "", itemText));
|
|
6121
|
+
supported.append(supportedList);
|
|
6122
|
+
|
|
6123
|
+
body.append(current, renderAppRunnerCustomSection(), how, supported);
|
|
6124
|
+
}
|
|
6125
|
+
|
|
6126
|
+
function openAppRunnerInfoDialog() {
|
|
6127
|
+
if (!elements.appRunnerInfoDialog) return;
|
|
6128
|
+
renderAppRunnerInfoDialog();
|
|
6129
|
+
setAppRunnerMenuOpen(false);
|
|
6130
|
+
if (!elements.appRunnerInfoDialog.open) elements.appRunnerInfoDialog.showModal();
|
|
6131
|
+
}
|
|
6132
|
+
|
|
6133
|
+
function closeAppRunnerInfoDialog() {
|
|
6134
|
+
if (elements.appRunnerInfoDialog?.open) elements.appRunnerInfoDialog.close();
|
|
6135
|
+
}
|
|
6136
|
+
|
|
6137
|
+
function renderAppRunnerWidget() {
|
|
6138
|
+
const data = activeAppRunnerData();
|
|
6139
|
+
const run = data.activeRun;
|
|
6140
|
+
if (!run) return null;
|
|
6141
|
+
const running = appRunnerIsRunning(run);
|
|
6142
|
+
const status = appRunnerStatusLabel(run);
|
|
6143
|
+
const node = make("section", `widget release-npm-widget app-runner-widget${running ? " app-runner-live-widget" : " app-runner-log-widget"}`);
|
|
6144
|
+
node.setAttribute("aria-label", "app runner output");
|
|
6145
|
+
|
|
6146
|
+
const header = make("div", "release-npm-header");
|
|
6147
|
+
const titleWrap = make("div", "release-npm-title-wrap");
|
|
6148
|
+
titleWrap.append(make("span", "release-npm-kicker", "app runner"), make("strong", "release-npm-title", run.label || run.displayCommand || "app runner"));
|
|
6149
|
+
|
|
6150
|
+
const elapsed = appRunnerElapsedLabel(run);
|
|
6151
|
+
header.append(titleWrap);
|
|
6152
|
+
|
|
6153
|
+
const lines = Array.isArray(run.lines) && run.lines.length ? run.lines : [run.displayCommand ? `$ ${run.displayCommand}` : "Waiting for app runner output..."];
|
|
6154
|
+
const streamHeader = releaseNpmStreamHeader(running ? "Live app output" : "App output", run.lineCount || lines.length, { live: running });
|
|
6155
|
+
const terminal = make("div", "release-npm-terminal");
|
|
6156
|
+
terminal.setAttribute("role", "log");
|
|
6157
|
+
terminal.setAttribute("aria-live", running ? "polite" : "off");
|
|
6158
|
+
for (const line of lines) appendReleaseNpmTerminalLine(terminal, line);
|
|
6159
|
+
|
|
6160
|
+
const controlParts = [run.displayCommand, run.cwd, run.truncated ? "output truncated" : ""].map(cleanStatusText).filter(Boolean);
|
|
6161
|
+
const controls = make("div", "release-npm-controls app-runner-output-controls");
|
|
6162
|
+
const actions = make("div", "app-runner-output-actions");
|
|
6163
|
+
const closeButton = appRunnerActionButton("Close", running ? stopAppRunner : clearAppRunner, running ? "danger app-runner-close-action" : "app-runner-close-action");
|
|
6164
|
+
closeButton.title = running ? "Stop this app runner/process/server" : "Close app runner output";
|
|
6165
|
+
const copyButton = appRunnerActionButton("Copy output", () => copyAppRunnerOutput(run), "app-runner-copy-action");
|
|
6166
|
+
copyButton.title = "Copy app runner output";
|
|
6167
|
+
actions.append(closeButton, copyButton);
|
|
6168
|
+
if (running) {
|
|
6169
|
+
actions.append(appRunnerActionButton("Stop", stopAppRunner, "danger"));
|
|
6170
|
+
} else {
|
|
6171
|
+
const canRunAgain = (data.runners || []).some((runner) => runner.id === run.runnerId);
|
|
6172
|
+
if (canRunAgain) actions.append(appRunnerActionButton("Run again", () => runAppRunner(run.runnerId)));
|
|
6173
|
+
actions.append(appRunnerActionButton("Clear", clearAppRunner));
|
|
6174
|
+
}
|
|
6175
|
+
const pills = make("div", "app-runner-output-pills");
|
|
6176
|
+
if (run.kind) pills.append(make("span", "release-npm-pill", run.kind));
|
|
6177
|
+
pills.append(make("span", `release-npm-pill app-runner-status ${run.status || "running"}`.trim(), status));
|
|
6178
|
+
if (elapsed) pills.append(make("span", "release-npm-pill elapsed", elapsed));
|
|
6179
|
+
controls.append(actions, pills, make("span", "app-runner-output-meta", controlParts.join(" · ")));
|
|
6180
|
+
const outputDetails = renderReleaseNpmOutputDetails(`app-runner:${run.id || run.runnerId || "active"}`, streamHeader, terminal, controls);
|
|
5253
6181
|
node.append(header, outputDetails);
|
|
5254
6182
|
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
5255
6183
|
return node;
|
|
@@ -5265,6 +6193,8 @@ function renderWidgets() {
|
|
|
5265
6193
|
if (releaseAurOutput) elements.widgetArea.append(releaseAurOutput);
|
|
5266
6194
|
const releaseAurLog = renderReleaseAurLogWidget();
|
|
5267
6195
|
if (releaseAurLog) elements.widgetArea.append(releaseAurLog);
|
|
6196
|
+
const appRunnerWidget = renderAppRunnerWidget();
|
|
6197
|
+
if (appRunnerWidget) elements.widgetArea.append(appRunnerWidget);
|
|
5268
6198
|
|
|
5269
6199
|
for (const [key, value] of widgets) {
|
|
5270
6200
|
const widgetFeatureId = optionalFeatureWidgetFeatureId(key);
|
|
@@ -5288,6 +6218,8 @@ function setGitWorkflow(patch, { tabId = activeTabId } = {}) {
|
|
|
5288
6218
|
const workflow = gitWorkflowForTab(tabId);
|
|
5289
6219
|
if (!workflow) return null;
|
|
5290
6220
|
Object.assign(workflow, patch);
|
|
6221
|
+
workflow.actionsDone = createGitWorkflowActionsDone(workflow.actionsDone);
|
|
6222
|
+
if (patch.step && !("process" in patch)) workflow.process = gitWorkflowProcessForStep(workflow.step, workflow.process);
|
|
5291
6223
|
if (tabId === activeTabId) {
|
|
5292
6224
|
gitWorkflow = workflow;
|
|
5293
6225
|
renderGitWorkflow();
|
|
@@ -5323,24 +6255,141 @@ function formatCommitMessagePreview(message) {
|
|
|
5323
6255
|
return [`=== SHORT ===`, message.short || "(empty)", "", "=== LONG ===", message.long || "(empty)"].join("\n");
|
|
5324
6256
|
}
|
|
5325
6257
|
|
|
5326
|
-
function
|
|
6258
|
+
function gitWorkflowMessageTitle(message) {
|
|
6259
|
+
return String(message?.short || message?.long || "").split("\n").find((line) => line.trim())?.trim() || "Pull request";
|
|
6260
|
+
}
|
|
6261
|
+
|
|
6262
|
+
function slugifyGitBranchPart(value) {
|
|
6263
|
+
return String(value || "")
|
|
6264
|
+
.normalize("NFKD")
|
|
6265
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
6266
|
+
.toLowerCase()
|
|
6267
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
6268
|
+
.replace(/^-+|-+$/g, "")
|
|
6269
|
+
.slice(0, 48);
|
|
6270
|
+
}
|
|
6271
|
+
|
|
6272
|
+
function defaultGitPrBranchName(message = gitWorkflow.message) {
|
|
6273
|
+
const title = gitWorkflowMessageTitle(message);
|
|
6274
|
+
const match = title.match(/^([a-z][a-z0-9-]*)(?:\([^)]*\))?:\s*(.+)$/i);
|
|
6275
|
+
const type = slugifyGitBranchPart(match?.[1] || "feat") || "feat";
|
|
6276
|
+
const summary = slugifyGitBranchPart(match?.[2] || title) || "feature";
|
|
6277
|
+
return `${type}/${summary}`;
|
|
6278
|
+
}
|
|
6279
|
+
|
|
6280
|
+
function formatGitPrPreview(pr) {
|
|
6281
|
+
if (!pr) return "No PR description loaded yet.";
|
|
6282
|
+
const header = [`=== PR DESCRIPTION ===`, `Branch: ${pr.branch || gitWorkflow.prBranch || "current branch"}`];
|
|
6283
|
+
if (pr.path) header.push(`File: ${pr.path}`);
|
|
6284
|
+
return [...header, "", pr.body || "(empty)"].join("\n");
|
|
6285
|
+
}
|
|
6286
|
+
|
|
6287
|
+
function addGitWorkflowAction(label, handler, className = "", disabled = gitWorkflow.busy, tooltip = "") {
|
|
5327
6288
|
const button = make("button", className, label);
|
|
5328
6289
|
button.type = "button";
|
|
5329
6290
|
button.disabled = disabled;
|
|
6291
|
+
if (tooltip) {
|
|
6292
|
+
button.title = tooltip;
|
|
6293
|
+
button.dataset.tooltip = tooltip;
|
|
6294
|
+
button.setAttribute("aria-label", `${label}. ${tooltip.replace(/\s+/g, " ")}`);
|
|
6295
|
+
}
|
|
5330
6296
|
button.addEventListener("click", handler);
|
|
5331
6297
|
elements.gitWorkflowActions.append(button);
|
|
5332
6298
|
return button;
|
|
5333
6299
|
}
|
|
5334
6300
|
|
|
6301
|
+
function setGitPrDialogStatus(message = "", level = "muted") {
|
|
6302
|
+
if (!elements.gitPrStatus) return;
|
|
6303
|
+
elements.gitPrStatus.textContent = message;
|
|
6304
|
+
elements.gitPrStatus.className = `git-pr-status ${level || "muted"}`;
|
|
6305
|
+
}
|
|
6306
|
+
|
|
6307
|
+
function resolveGitPrDialog(value) {
|
|
6308
|
+
const resolve = activeGitPrDialogResolve;
|
|
6309
|
+
activeGitPrDialogResolve = null;
|
|
6310
|
+
if (elements.gitPrDialog?.open) elements.gitPrDialog.close();
|
|
6311
|
+
if (resolve) resolve(value);
|
|
6312
|
+
}
|
|
6313
|
+
|
|
6314
|
+
function openGitPrReviewDialog(pr, { title = "" } = {}) {
|
|
6315
|
+
if (!elements.gitPrDialog || !elements.gitPrTitleInput || !elements.gitPrBodyEditor) return Promise.resolve(null);
|
|
6316
|
+
if (activeGitPrDialogResolve) resolveGitPrDialog(null);
|
|
6317
|
+
elements.gitPrTitleInput.value = title || gitWorkflowMessageTitle(gitWorkflow.message);
|
|
6318
|
+
elements.gitPrBodyEditor.value = pr?.body || "";
|
|
6319
|
+
setGitPrDialogStatus(`Review ${pr?.path || "the generated PR description"}. Edit if needed, then create the pull request.`);
|
|
6320
|
+
return new Promise((resolve) => {
|
|
6321
|
+
activeGitPrDialogResolve = resolve;
|
|
6322
|
+
elements.gitPrDialog.showModal();
|
|
6323
|
+
queueMicrotask(() => elements.gitPrBodyEditor.focus());
|
|
6324
|
+
});
|
|
6325
|
+
}
|
|
6326
|
+
|
|
6327
|
+
function gitWorkflowProcessForStep(step = gitWorkflow.step, fallback = gitWorkflow.process || "stage") {
|
|
6328
|
+
switch (step) {
|
|
6329
|
+
case "generate":
|
|
6330
|
+
case "generating":
|
|
6331
|
+
return "message";
|
|
6332
|
+
case "message":
|
|
6333
|
+
case "branchNaming":
|
|
6334
|
+
case "branching":
|
|
6335
|
+
case "committing":
|
|
6336
|
+
return "commit";
|
|
6337
|
+
case "push":
|
|
6338
|
+
case "pushing":
|
|
6339
|
+
case "prGenerating":
|
|
6340
|
+
case "prReview":
|
|
6341
|
+
case "prCreating":
|
|
6342
|
+
case "done":
|
|
6343
|
+
return "push";
|
|
6344
|
+
case "add":
|
|
6345
|
+
case "idle":
|
|
6346
|
+
return "stage";
|
|
6347
|
+
case "cancelled":
|
|
6348
|
+
case "error":
|
|
6349
|
+
return GIT_WORKFLOW_PROCESS_VALUES.has(fallback) ? fallback : "stage";
|
|
6350
|
+
default:
|
|
6351
|
+
return "stage";
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
|
|
6355
|
+
function selectGitWorkflowProcess(processValue, tabId = gitWorkflowActionTabId()) {
|
|
6356
|
+
const workflow = gitWorkflowForTab(tabId);
|
|
6357
|
+
if (!workflow) return;
|
|
6358
|
+
const process = GIT_WORKFLOW_PROCESS_VALUES.has(processValue) ? processValue : "stage";
|
|
6359
|
+
workflow.runId += 1;
|
|
6360
|
+
const runId = workflow.runId;
|
|
6361
|
+
const base = { active: true, process, busy: false, error: "", messageRequestedAt: 0, branchName: "", branchNameRequestedAt: 0, prMode: false, prBranch: "", pr: null, prRequestedAt: 0 };
|
|
6362
|
+
|
|
6363
|
+
if (process === "stage") {
|
|
6364
|
+
setGitWorkflow({ ...base, step: "add", message: null, output: "Ready to stage all changes with git add ." }, { tabId });
|
|
6365
|
+
return;
|
|
6366
|
+
}
|
|
6367
|
+
if (process === "message") {
|
|
6368
|
+
setGitWorkflow({ ...base, step: "generate", message: null, output: "Ready to generate a commit message from the currently staged changes." }, { tabId });
|
|
6369
|
+
return;
|
|
6370
|
+
}
|
|
6371
|
+
if (process === "commit") {
|
|
6372
|
+
setGitWorkflow({ ...base, step: "message", message: null, output: "Loading current generated commit message files…" }, { tabId });
|
|
6373
|
+
loadGitWorkflowMessage({ requireFresh: false, runId, tabId });
|
|
6374
|
+
return;
|
|
6375
|
+
}
|
|
6376
|
+
setGitWorkflow({ ...base, step: "push", output: "Ready to run git push for the current branch." }, { tabId });
|
|
6377
|
+
}
|
|
6378
|
+
|
|
5335
6379
|
function gitWorkflowTitle() {
|
|
5336
6380
|
switch (gitWorkflow.step) {
|
|
5337
6381
|
case "add": return "Stage all changes";
|
|
5338
6382
|
case "generate": return "Generate staged commit message";
|
|
5339
6383
|
case "generating": return "Waiting for /git-staged-msg";
|
|
5340
|
-
case "message": return "Choose commit message";
|
|
6384
|
+
case "message": return gitWorkflow.prMode ? "Choose PR branch commit message" : "Choose commit message";
|
|
6385
|
+
case "branchNaming": return "Waiting for branch name";
|
|
6386
|
+
case "branching": return "Creating PR branch";
|
|
5341
6387
|
case "committing": return "Committing";
|
|
5342
|
-
case "push": return "Push commit";
|
|
6388
|
+
case "push": return gitWorkflow.prMode ? "Push branch and create PR" : "Push commit";
|
|
5343
6389
|
case "pushing": return "Pushing";
|
|
6390
|
+
case "prGenerating": return "Waiting for /pr";
|
|
6391
|
+
case "prReview": return "Review PR description";
|
|
6392
|
+
case "prCreating": return "Creating pull request";
|
|
5344
6393
|
case "done": return "Git workflow complete";
|
|
5345
6394
|
case "cancelled": return "Git workflow cancelled";
|
|
5346
6395
|
case "error": return "Git workflow needs attention";
|
|
@@ -5353,11 +6402,16 @@ function gitWorkflowHint() {
|
|
|
5353
6402
|
case "add": return "Step 1: run git add . in the current Pi working directory.";
|
|
5354
6403
|
case "generate": return "Step 2: run /git-staged-msg, then preview the generated files.";
|
|
5355
6404
|
case "generating": return "Pi is generating dev/COMMIT/staged-commit-short.txt and staged-commit-long.txt.";
|
|
5356
|
-
case "message": return "Step 3/4: preview the native g-msg output
|
|
6405
|
+
case "message": return gitWorkflow.prMode ? `Branch ${gitWorkflow.prBranch || "created"}: choose short or long commit before opening a PR.` : "Step 3/4: preview the native g-msg output, commit here, or create a PR branch first.";
|
|
6406
|
+
case "branchNaming": return "Pi is generating dev/COMMIT/staged-branch-name.txt. Cancel will request Pi abort.";
|
|
6407
|
+
case "branching": return "Creating a new branch with git switch -c before committing.";
|
|
5357
6408
|
case "committing": return "Running native git commit from the generated message file.";
|
|
5358
|
-
case "push": return "Step 5: push the new commit to the configured remote.";
|
|
6409
|
+
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.";
|
|
5359
6410
|
case "pushing": return "Running git push. Cancel will request process termination.";
|
|
5360
|
-
case "
|
|
6411
|
+
case "prGenerating": return "Pi is generating dev/PR/<current-branch>.md with /pr.";
|
|
6412
|
+
case "prReview": return "Review or edit the generated PR description before creating the pull request.";
|
|
6413
|
+
case "prCreating": return "Running gh pr create with the confirmed description.";
|
|
6414
|
+
case "done": return gitWorkflow.prMode ? "PR workflow finished. Review the output below." : "Push finished. Review the output below.";
|
|
5361
6415
|
case "cancelled": return "No further workflow steps will run.";
|
|
5362
6416
|
case "error": return gitWorkflow.error || "Fix the issue, then retry or restart.";
|
|
5363
6417
|
default: return "Stage changes, generate a commit message, commit, and push.";
|
|
@@ -5375,9 +6429,14 @@ function renderGitWorkflow() {
|
|
|
5375
6429
|
elements.gitWorkflowActions.replaceChildren();
|
|
5376
6430
|
|
|
5377
6431
|
const activeIndex = GIT_WORKFLOW_ACTIVE_INDEX[gitWorkflow.step] ?? 0;
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
6432
|
+
const activeProcess = gitWorkflowProcessForStep(gitWorkflow.step, gitWorkflow.process);
|
|
6433
|
+
for (const [index, process] of GIT_WORKFLOW_PROCESSES.entries()) {
|
|
6434
|
+
const item = make("button", "git-workflow-step", process.label);
|
|
6435
|
+
item.type = "button";
|
|
6436
|
+
item.dataset.gitWorkflowProcess = process.value;
|
|
6437
|
+
item.disabled = !!gitWorkflow.busy;
|
|
6438
|
+
item.setAttribute("aria-pressed", String(process.value === activeProcess));
|
|
6439
|
+
if (gitWorkflowActionDone(gitWorkflow, process.value)) item.classList.add("done");
|
|
5381
6440
|
if (index === activeIndex && !["done", "cancelled", "error"].includes(gitWorkflow.step)) item.classList.add("active");
|
|
5382
6441
|
elements.gitWorkflowSteps.append(item);
|
|
5383
6442
|
}
|
|
@@ -5393,11 +6452,28 @@ function renderGitWorkflow() {
|
|
|
5393
6452
|
} else if (gitWorkflow.step === "generating") {
|
|
5394
6453
|
addGitWorkflowAction("Refresh message preview", () => loadGitWorkflowMessage({ requireFresh: true }), "", false);
|
|
5395
6454
|
} else if (gitWorkflow.step === "message") {
|
|
5396
|
-
|
|
5397
|
-
|
|
6455
|
+
if (!gitWorkflow.prMode) {
|
|
6456
|
+
addGitWorkflowAction("Create PR", () => createGitPrBranch(), "primary", false, GIT_WORKFLOW_CREATE_PR_TOOLTIP);
|
|
6457
|
+
addGitWorkflowAction("Manual branch", () => createGitPrBranchManually(), "", false, GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP);
|
|
6458
|
+
}
|
|
6459
|
+
addGitWorkflowAction("Commit short", () => commitGitWorkflow("short"), gitWorkflow.prMode ? "primary" : "", false);
|
|
6460
|
+
addGitWorkflowAction("Commit long", () => commitGitWorkflow("long"), gitWorkflow.prMode ? "primary" : "", false);
|
|
5398
6461
|
addGitWorkflowAction("Regenerate", () => runGitMessagePrompt(), "", false);
|
|
6462
|
+
} else if (gitWorkflow.step === "branchNaming") {
|
|
6463
|
+
addGitWorkflowAction("Refresh branch name", () => loadGitWorkflowBranchName({ requireFresh: true }), "", false);
|
|
6464
|
+
addGitWorkflowAction("Manual branch", () => createGitPrBranchManually(), "", !!gitWorkflow.busy, GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP);
|
|
6465
|
+
} else if (gitWorkflow.step === "branching") {
|
|
6466
|
+
addGitWorkflowAction("Creating branch…", () => {}, "primary", true);
|
|
5399
6467
|
} else if (gitWorkflow.step === "push") {
|
|
5400
|
-
addGitWorkflowAction("
|
|
6468
|
+
if (gitWorkflow.prMode) addGitWorkflowAction("Push and Create PR", () => pushAndCreatePrGitWorkflow(), "primary", false);
|
|
6469
|
+
else addGitWorkflowAction("Run git push", () => pushGitWorkflow(), "primary", false);
|
|
6470
|
+
} else if (gitWorkflow.step === "prGenerating") {
|
|
6471
|
+
addGitWorkflowAction("Refresh PR description", () => loadGitWorkflowPr({ requireFresh: true }), "", false);
|
|
6472
|
+
} else if (gitWorkflow.step === "prReview") {
|
|
6473
|
+
addGitWorkflowAction("Create PR", () => createGitPrFromReview(), "primary", false);
|
|
6474
|
+
addGitWorkflowAction("Regenerate /pr", () => runGitPrPrompt(), "", false);
|
|
6475
|
+
} else if (gitWorkflow.step === "prCreating") {
|
|
6476
|
+
addGitWorkflowAction("Creating PR…", () => {}, "primary", true);
|
|
5401
6477
|
} else if (gitWorkflow.step === "done") {
|
|
5402
6478
|
addGitWorkflowAction("Close", () => setGitWorkflow({ active: false }), "primary", false);
|
|
5403
6479
|
addGitWorkflowAction("Start another", () => startGitWorkflow(), "", false);
|
|
@@ -5447,11 +6523,19 @@ function startGitWorkflow(tabId = activeTabId) {
|
|
|
5447
6523
|
setGitWorkflow({
|
|
5448
6524
|
active: true,
|
|
5449
6525
|
step: "add",
|
|
6526
|
+
process: "stage",
|
|
5450
6527
|
busy: false,
|
|
5451
|
-
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.",
|
|
6528
|
+
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.",
|
|
5452
6529
|
error: "",
|
|
5453
6530
|
message: null,
|
|
5454
6531
|
messageRequestedAt: 0,
|
|
6532
|
+
branchName: "",
|
|
6533
|
+
branchNameRequestedAt: 0,
|
|
6534
|
+
actionsDone: createGitWorkflowActionsDone(),
|
|
6535
|
+
prMode: false,
|
|
6536
|
+
prBranch: "",
|
|
6537
|
+
pr: null,
|
|
6538
|
+
prRequestedAt: 0,
|
|
5455
6539
|
}, { tabId });
|
|
5456
6540
|
}
|
|
5457
6541
|
|
|
@@ -5459,7 +6543,8 @@ async function cancelGitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
|
5459
6543
|
const tabContext = activeTabContext(tabId);
|
|
5460
6544
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
5461
6545
|
if (!workflow?.active) return;
|
|
5462
|
-
const shouldAbortPi = workflow.step === "generating";
|
|
6546
|
+
const shouldAbortPi = workflow.step === "generating" || workflow.step === "branchNaming" || workflow.step === "prGenerating";
|
|
6547
|
+
if (activeGitPrDialogResolve) resolveGitPrDialog(null);
|
|
5463
6548
|
workflow.runId += 1;
|
|
5464
6549
|
setGitWorkflow({ step: "cancelled", busy: false, error: "", output: `${workflow.output || ""}${workflow.output ? "\n\n" : ""}Cancelled by user.` }, { tabId });
|
|
5465
6550
|
if (shouldAbortPi && isCurrentTabContext(tabContext)) setRunIndicatorActivity("Abort requested; checking whether Pi stopped…");
|
|
@@ -5479,7 +6564,7 @@ async function runGitAdd(tabId = gitWorkflowActionTabId()) {
|
|
|
5479
6564
|
try {
|
|
5480
6565
|
const result = await gitWorkflowRequest("/api/git-workflow/add", { runId, tabId });
|
|
5481
6566
|
if (!result) return;
|
|
5482
|
-
setGitWorkflow({ step: "generate", busy: false, output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` }, { tabId });
|
|
6567
|
+
setGitWorkflow({ step: "generate", busy: false, ...gitWorkflowActionDonePatch(workflow, "stage"), output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` }, { tabId });
|
|
5483
6568
|
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
5484
6569
|
} catch (error) {
|
|
5485
6570
|
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "add", { tabId });
|
|
@@ -5543,6 +6628,7 @@ async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId
|
|
|
5543
6628
|
busy: false,
|
|
5544
6629
|
error: "",
|
|
5545
6630
|
message,
|
|
6631
|
+
...(requireFresh && currentWorkflow.messageRequestedAt ? gitWorkflowActionDonePatch(currentWorkflow, "message") : {}),
|
|
5546
6632
|
output: formatCommitMessagePreview(message),
|
|
5547
6633
|
}, { tabId });
|
|
5548
6634
|
} catch (error) {
|
|
@@ -5556,6 +6642,130 @@ async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId
|
|
|
5556
6642
|
}
|
|
5557
6643
|
}
|
|
5558
6644
|
|
|
6645
|
+
function gitBranchNamePromptMessage() {
|
|
6646
|
+
if (hasAvailableCommand("git-branch-name")) return "/git-branch-name";
|
|
6647
|
+
return [
|
|
6648
|
+
"Generate one PR branch name for the current staged work.",
|
|
6649
|
+
"Inspect only staged changes (`git diff --cached`) and the generated commit message files if present:",
|
|
6650
|
+
"- dev/COMMIT/staged-commit-short.txt",
|
|
6651
|
+
"- dev/COMMIT/staged-commit-long.txt",
|
|
6652
|
+
"",
|
|
6653
|
+
"Write exactly one line to dev/COMMIT/staged-branch-name.txt in this format:",
|
|
6654
|
+
"<type>/<short-feature-name>",
|
|
6655
|
+
"",
|
|
6656
|
+
"Rules: use lowercase kebab-case, no spaces/underscores/uppercase/trailing punctuation, 2-5 words after the slash, and no extra lines or prose in the file.",
|
|
6657
|
+
].join("\n");
|
|
6658
|
+
}
|
|
6659
|
+
|
|
6660
|
+
async function createGitPrBranch(tabId = gitWorkflowActionTabId()) {
|
|
6661
|
+
await runGitBranchNamePrompt(tabId);
|
|
6662
|
+
}
|
|
6663
|
+
|
|
6664
|
+
async function createGitPrBranchManually(tabId = gitWorkflowActionTabId()) {
|
|
6665
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6666
|
+
if (!workflow) return;
|
|
6667
|
+
await createGitPrBranchWithSuggestion(workflow.branchName || defaultGitPrBranchName(workflow.message), tabId);
|
|
6668
|
+
}
|
|
6669
|
+
|
|
6670
|
+
async function runGitBranchNamePrompt(tabId = gitWorkflowActionTabId()) {
|
|
6671
|
+
const tabContext = activeTabContext(tabId);
|
|
6672
|
+
const targetTab = tabs.find((tab) => tab.id === tabId);
|
|
6673
|
+
const targetBusy = tabId === activeTabId ? !!currentState?.isStreaming : activityForTab(targetTab).isWorking;
|
|
6674
|
+
if (targetBusy) {
|
|
6675
|
+
failGitWorkflow(new Error("Pi is currently running. Wait for it to finish or abort before generating a branch name."), "message", { tabId });
|
|
6676
|
+
return;
|
|
6677
|
+
}
|
|
6678
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6679
|
+
if (!workflow) return;
|
|
6680
|
+
const runId = workflow.runId;
|
|
6681
|
+
const requestedAt = Date.now();
|
|
6682
|
+
setGitWorkflow({
|
|
6683
|
+
step: "branchNaming",
|
|
6684
|
+
busy: true,
|
|
6685
|
+
error: "",
|
|
6686
|
+
branchNameRequestedAt: requestedAt,
|
|
6687
|
+
output: "Sending branch-name request to Pi.\n\nCancel will request Pi abort.",
|
|
6688
|
+
}, { tabId });
|
|
6689
|
+
if (isCurrentTabContext(tabContext)) setRunIndicatorActivity("Sending branch-name request to Pi…");
|
|
6690
|
+
try {
|
|
6691
|
+
await api("/api/prompt", { method: "POST", body: { message: gitBranchNamePromptMessage() }, tabId });
|
|
6692
|
+
if (!isCurrentGitWorkflowRun(runId, tabId)) return;
|
|
6693
|
+
appendGitWorkflowOutput("Branch-name request accepted. Waiting for agent_end, then the branch name will be loaded.", { tabId });
|
|
6694
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshState(120, tabContext);
|
|
6695
|
+
setTimeout(() => {
|
|
6696
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
6697
|
+
const targetStillBusy = tabId === activeTabId && currentState?.isStreaming;
|
|
6698
|
+
if (isCurrentGitWorkflowRun(runId, tabId) && currentWorkflow?.step === "branchNaming" && !targetStillBusy) {
|
|
6699
|
+
loadGitWorkflowBranchName({ requireFresh: true, retries: 1, runId, tabId });
|
|
6700
|
+
}
|
|
6701
|
+
}, 2500);
|
|
6702
|
+
} catch (error) {
|
|
6703
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) {
|
|
6704
|
+
if (isCurrentTabContext(tabContext)) clearRunIndicatorActivity();
|
|
6705
|
+
failGitWorkflow(error, "message", { tabId });
|
|
6706
|
+
}
|
|
6707
|
+
}
|
|
6708
|
+
}
|
|
6709
|
+
|
|
6710
|
+
async function loadGitWorkflowBranchName({ requireFresh = false, retries = 0, runId, tabId = activeTabId } = {}) {
|
|
6711
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6712
|
+
const expectedRunId = runId ?? workflow?.runId;
|
|
6713
|
+
try {
|
|
6714
|
+
const branchName = await gitWorkflowRequest("/api/git-workflow/branch-name", { method: "GET", runId: expectedRunId, tabId });
|
|
6715
|
+
if (!branchName) return;
|
|
6716
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
6717
|
+
if (!currentWorkflow) return;
|
|
6718
|
+
if (requireFresh && currentWorkflow.branchNameRequestedAt && (branchName.mtimeMs || 0) + 10000 < currentWorkflow.branchNameRequestedAt) {
|
|
6719
|
+
throw new Error("Generated branch name has not refreshed yet.");
|
|
6720
|
+
}
|
|
6721
|
+
const branch = branchName.branch || defaultGitPrBranchName(currentWorkflow.message);
|
|
6722
|
+
setGitWorkflow({
|
|
6723
|
+
step: "message",
|
|
6724
|
+
busy: false,
|
|
6725
|
+
error: "",
|
|
6726
|
+
branchName: branch,
|
|
6727
|
+
output: `${formatCommitMessagePreview(currentWorkflow.message)}\n\nGenerated branch name: ${branch}`,
|
|
6728
|
+
}, { tabId });
|
|
6729
|
+
await createGitPrBranchWithSuggestion(branch, tabId, expectedRunId);
|
|
6730
|
+
} catch (error) {
|
|
6731
|
+
if (!isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
6732
|
+
if (retries > 0) {
|
|
6733
|
+
setTimeout(() => loadGitWorkflowBranchName({ requireFresh, retries: retries - 1, runId: expectedRunId, tabId }), 1400);
|
|
6734
|
+
return;
|
|
6735
|
+
}
|
|
6736
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
6737
|
+
failGitWorkflow(error, currentWorkflow?.step === "branchNaming" ? "message" : currentWorkflow?.step, { tabId });
|
|
6738
|
+
}
|
|
6739
|
+
}
|
|
6740
|
+
|
|
6741
|
+
async function createGitPrBranchWithSuggestion(suggestion, tabId = gitWorkflowActionTabId(), expectedRunId) {
|
|
6742
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6743
|
+
if (!workflow) return;
|
|
6744
|
+
const proposedBranch = prompt("New PR branch name (example: type/feature-name)", suggestion || defaultGitPrBranchName(workflow.message));
|
|
6745
|
+
if (expectedRunId !== undefined && !isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
6746
|
+
if (proposedBranch === null) {
|
|
6747
|
+
setGitWorkflow({ step: "message", busy: false, output: `${formatCommitMessagePreview(workflow.message)}\n\nPR branch creation cancelled. Use Create PR to generate a branch name again or Manual branch to type one.` }, { tabId });
|
|
6748
|
+
return;
|
|
6749
|
+
}
|
|
6750
|
+
const branch = proposedBranch.trim();
|
|
6751
|
+
if (!branch) {
|
|
6752
|
+
failGitWorkflow(new Error("Branch name is required to create a PR branch."), "message", { tabId });
|
|
6753
|
+
return;
|
|
6754
|
+
}
|
|
6755
|
+
const runId = workflow.runId;
|
|
6756
|
+
setGitWorkflow({ step: "branching", prMode: true, prBranch: branch, branchName: branch, busy: true, error: "", output: `${formatCommitMessagePreview(workflow.message)}\n\nRunning git switch -c ${branch}…` }, { tabId });
|
|
6757
|
+
try {
|
|
6758
|
+
const result = await gitWorkflowRequest("/api/git-workflow/branch", { body: { branch }, runId, tabId });
|
|
6759
|
+
if (!result) return;
|
|
6760
|
+
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 long to commit on this branch.` }, { tabId });
|
|
6761
|
+
} catch (error) {
|
|
6762
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) {
|
|
6763
|
+
setGitWorkflow({ prMode: false, prBranch: "" }, { tabId });
|
|
6764
|
+
failGitWorkflow(error, "message", { tabId });
|
|
6765
|
+
}
|
|
6766
|
+
}
|
|
6767
|
+
}
|
|
6768
|
+
|
|
5559
6769
|
async function commitGitWorkflow(variant, tabId = gitWorkflowActionTabId()) {
|
|
5560
6770
|
const tabContext = activeTabContext(tabId);
|
|
5561
6771
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
@@ -5565,7 +6775,8 @@ async function commitGitWorkflow(variant, tabId = gitWorkflowActionTabId()) {
|
|
|
5565
6775
|
try {
|
|
5566
6776
|
const result = await gitWorkflowRequest("/api/git-workflow/commit", { body: { variant }, runId, tabId });
|
|
5567
6777
|
if (!result) return;
|
|
5568
|
-
|
|
6778
|
+
const nextAction = workflow.prMode ? "Push and Create PR." : "git push.";
|
|
6779
|
+
setGitWorkflow({ step: "push", busy: false, ...gitWorkflowActionDonePatch(workflow, "commit"), output: `${formatGitCommandResult(result)}\n\nCommit created. Next: ${nextAction}` }, { tabId });
|
|
5569
6780
|
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
5570
6781
|
} catch (error) {
|
|
5571
6782
|
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "message", { tabId });
|
|
@@ -5581,13 +6792,129 @@ async function pushGitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
|
5581
6792
|
try {
|
|
5582
6793
|
const result = await gitWorkflowRequest("/api/git-workflow/push", { runId, tabId });
|
|
5583
6794
|
if (!result) return;
|
|
5584
|
-
setGitWorkflow({ step: "done", busy: false, output: formatGitCommandResult(result) || "git push finished." }, { tabId });
|
|
6795
|
+
setGitWorkflow({ step: "done", busy: false, ...gitWorkflowActionDonePatch(workflow, "push"), output: formatGitCommandResult(result) || "git push finished." }, { tabId });
|
|
6796
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
6797
|
+
} catch (error) {
|
|
6798
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "push", { tabId });
|
|
6799
|
+
}
|
|
6800
|
+
}
|
|
6801
|
+
|
|
6802
|
+
async function runGitPrPrompt(tabId = gitWorkflowActionTabId(), { prefixOutput = "" } = {}) {
|
|
6803
|
+
const tabContext = activeTabContext(tabId);
|
|
6804
|
+
const targetTab = tabs.find((tab) => tab.id === tabId);
|
|
6805
|
+
const targetBusy = tabId === activeTabId ? !!currentState?.isStreaming : activityForTab(targetTab).isWorking;
|
|
6806
|
+
if (targetBusy) {
|
|
6807
|
+
failGitWorkflow(new Error("Pi is currently running. Wait for it to finish or abort before generating a PR description."), "push", { tabId });
|
|
6808
|
+
return;
|
|
6809
|
+
}
|
|
6810
|
+
if (!hasAvailableCommand("pr")) {
|
|
6811
|
+
failGitWorkflow(new Error(commandUnavailableMessage("pr")), "push", { tabId });
|
|
6812
|
+
return;
|
|
6813
|
+
}
|
|
6814
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6815
|
+
if (!workflow) return;
|
|
6816
|
+
const runId = workflow.runId;
|
|
6817
|
+
const requestedAt = Date.now();
|
|
6818
|
+
setGitWorkflow({
|
|
6819
|
+
step: "prGenerating",
|
|
6820
|
+
busy: true,
|
|
6821
|
+
error: "",
|
|
6822
|
+
prRequestedAt: requestedAt,
|
|
6823
|
+
output: `${prefixOutput ? `${prefixOutput}\n\n` : ""}Sending /pr to Pi.\n\nCancel will request Pi abort.`,
|
|
6824
|
+
}, { tabId });
|
|
6825
|
+
if (isCurrentTabContext(tabContext)) setRunIndicatorActivity("Sending /pr to Pi…");
|
|
6826
|
+
try {
|
|
6827
|
+
await api("/api/prompt", { method: "POST", body: { message: "/pr" }, tabId });
|
|
6828
|
+
if (!isCurrentGitWorkflowRun(runId, tabId)) return;
|
|
6829
|
+
appendGitWorkflowOutput("/pr accepted. Waiting for agent_end, then the PR description will be loaded.", { tabId });
|
|
6830
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshState(120, tabContext);
|
|
6831
|
+
setTimeout(() => {
|
|
6832
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
6833
|
+
const targetStillBusy = tabId === activeTabId && currentState?.isStreaming;
|
|
6834
|
+
if (isCurrentGitWorkflowRun(runId, tabId) && currentWorkflow?.step === "prGenerating" && !targetStillBusy) {
|
|
6835
|
+
loadGitWorkflowPr({ requireFresh: true, retries: 1, runId, tabId });
|
|
6836
|
+
}
|
|
6837
|
+
}, 2500);
|
|
6838
|
+
} catch (error) {
|
|
6839
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) {
|
|
6840
|
+
if (isCurrentTabContext(tabContext)) clearRunIndicatorActivity();
|
|
6841
|
+
failGitWorkflow(error, "push", { tabId });
|
|
6842
|
+
}
|
|
6843
|
+
}
|
|
6844
|
+
}
|
|
6845
|
+
|
|
6846
|
+
async function loadGitWorkflowPr({ requireFresh = false, retries = 0, runId, tabId = activeTabId } = {}) {
|
|
6847
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6848
|
+
const expectedRunId = runId ?? workflow?.runId;
|
|
6849
|
+
try {
|
|
6850
|
+
const pr = await gitWorkflowRequest("/api/git-workflow/pr-description", { method: "GET", runId: expectedRunId, tabId });
|
|
6851
|
+
if (!pr) return;
|
|
6852
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
6853
|
+
if (!currentWorkflow) return;
|
|
6854
|
+
if (requireFresh && currentWorkflow.prRequestedAt && (pr.mtimeMs || 0) + 10000 < currentWorkflow.prRequestedAt) {
|
|
6855
|
+
throw new Error("Generated PR description has not refreshed yet.");
|
|
6856
|
+
}
|
|
6857
|
+
setGitWorkflow({
|
|
6858
|
+
step: "prReview",
|
|
6859
|
+
busy: false,
|
|
6860
|
+
error: "",
|
|
6861
|
+
pr,
|
|
6862
|
+
prBranch: pr.branch || currentWorkflow.prBranch,
|
|
6863
|
+
output: formatGitPrPreview(pr),
|
|
6864
|
+
}, { tabId });
|
|
6865
|
+
} catch (error) {
|
|
6866
|
+
if (!isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
6867
|
+
if (retries > 0) {
|
|
6868
|
+
setTimeout(() => loadGitWorkflowPr({ requireFresh, retries: retries - 1, runId: expectedRunId, tabId }), 1400);
|
|
6869
|
+
return;
|
|
6870
|
+
}
|
|
6871
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
6872
|
+
failGitWorkflow(error, currentWorkflow?.step === "prGenerating" ? "push" : currentWorkflow?.step, { tabId });
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
|
|
6876
|
+
async function pushAndCreatePrGitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
6877
|
+
const tabContext = activeTabContext(tabId);
|
|
6878
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6879
|
+
if (!workflow) return;
|
|
6880
|
+
const runId = workflow.runId;
|
|
6881
|
+
const branch = workflow.prBranch || "current branch";
|
|
6882
|
+
setGitWorkflow({ step: "pushing", busy: true, error: "", output: `Pushing PR branch ${branch}…` }, { tabId });
|
|
6883
|
+
try {
|
|
6884
|
+
const result = await gitWorkflowRequest("/api/git-workflow/push", { body: { setUpstream: true, branch: workflow.prBranch }, runId, tabId });
|
|
6885
|
+
if (!result) return;
|
|
6886
|
+
setGitWorkflow({ ...gitWorkflowActionDonePatch(workflow, "push") }, { tabId });
|
|
5585
6887
|
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
6888
|
+
await runGitPrPrompt(tabId, { prefixOutput: `${formatGitCommandResult(result)}\n\nPushed PR branch ${result.branch || branch}.` });
|
|
5586
6889
|
} catch (error) {
|
|
5587
6890
|
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "push", { tabId });
|
|
5588
6891
|
}
|
|
5589
6892
|
}
|
|
5590
6893
|
|
|
6894
|
+
async function createGitPrFromReview(tabId = gitWorkflowActionTabId()) {
|
|
6895
|
+
const tabContext = activeTabContext(tabId);
|
|
6896
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6897
|
+
if (!workflow?.pr) return;
|
|
6898
|
+
const runId = workflow.runId;
|
|
6899
|
+
const review = await openGitPrReviewDialog(workflow.pr, { title: gitWorkflowMessageTitle(workflow.message) });
|
|
6900
|
+
if (!isCurrentGitWorkflowRun(runId, tabId)) return;
|
|
6901
|
+
if (!review) {
|
|
6902
|
+
setGitWorkflow({ step: "prReview", busy: false, output: `${formatGitPrPreview(workflow.pr)}\n\nPR creation cancelled. Edit the description, regenerate /pr, or press Create PR again.` }, { tabId });
|
|
6903
|
+
return;
|
|
6904
|
+
}
|
|
6905
|
+
const title = review.title.trim();
|
|
6906
|
+
const body = review.body.trimEnd();
|
|
6907
|
+
setGitWorkflow({ step: "prCreating", busy: true, error: "", output: `${formatGitPrPreview({ ...workflow.pr, body })}\n\nCreating pull request with gh pr create…` }, { tabId });
|
|
6908
|
+
try {
|
|
6909
|
+
const result = await gitWorkflowRequest("/api/git-workflow/create-pr", { body: { title, body }, runId, tabId });
|
|
6910
|
+
if (!result) return;
|
|
6911
|
+
setGitWorkflow({ step: "done", busy: false, ...gitWorkflowActionDonePatch(workflow, "push"), output: `${formatGitCommandResult(result)}\n\nPull request created.` }, { tabId });
|
|
6912
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
6913
|
+
} catch (error) {
|
|
6914
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "prReview", { tabId });
|
|
6915
|
+
}
|
|
6916
|
+
}
|
|
6917
|
+
|
|
5591
6918
|
function resumeGitWorkflowForActiveTab(tabContext = activeTabContext()) {
|
|
5592
6919
|
if (!isCurrentTabContext(tabContext)) return;
|
|
5593
6920
|
bindGitWorkflowToActiveTab();
|
|
@@ -5601,6 +6928,22 @@ function resumeGitWorkflowForActiveTab(tabContext = activeTabContext()) {
|
|
|
5601
6928
|
}
|
|
5602
6929
|
loadGitWorkflowMessage({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId: workflowTabId });
|
|
5603
6930
|
}
|
|
6931
|
+
if (workflowTabId === tabContext.tabId && gitWorkflow.active && gitWorkflow.step === "branchNaming" && !currentState?.isStreaming) {
|
|
6932
|
+
const retryDelayMs = Math.max(0, 2500 - (Date.now() - (gitWorkflow.branchNameRequestedAt || 0)));
|
|
6933
|
+
if (retryDelayMs > 0) {
|
|
6934
|
+
setTimeout(() => resumeGitWorkflowForActiveTab(tabContext), retryDelayMs);
|
|
6935
|
+
return;
|
|
6936
|
+
}
|
|
6937
|
+
loadGitWorkflowBranchName({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId: workflowTabId });
|
|
6938
|
+
}
|
|
6939
|
+
if (workflowTabId === tabContext.tabId && gitWorkflow.active && gitWorkflow.step === "prGenerating" && !currentState?.isStreaming) {
|
|
6940
|
+
const retryDelayMs = Math.max(0, 2500 - (Date.now() - (gitWorkflow.prRequestedAt || 0)));
|
|
6941
|
+
if (retryDelayMs > 0) {
|
|
6942
|
+
setTimeout(() => resumeGitWorkflowForActiveTab(tabContext), retryDelayMs);
|
|
6943
|
+
return;
|
|
6944
|
+
}
|
|
6945
|
+
loadGitWorkflowPr({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId: workflowTabId });
|
|
6946
|
+
}
|
|
5604
6947
|
}
|
|
5605
6948
|
|
|
5606
6949
|
function normalizeQueuedMessages(event) {
|
|
@@ -8188,6 +9531,13 @@ function setNativeCommandMenuOpen(open) {
|
|
|
8188
9531
|
elements.nativeCommandMenuButton.parentElement?.classList.toggle("open", nativeCommandMenuOpen);
|
|
8189
9532
|
}
|
|
8190
9533
|
|
|
9534
|
+
function setAppRunnerMenuOpen(open) {
|
|
9535
|
+
appRunnerMenuOpen = !!open;
|
|
9536
|
+
elements.appRunnerMenuButton?.setAttribute("aria-expanded", appRunnerMenuOpen ? "true" : "false");
|
|
9537
|
+
elements.appRunnerMenuButton?.classList.toggle("menu-open", appRunnerMenuOpen);
|
|
9538
|
+
elements.appRunnerMenuButton?.parentElement?.classList.toggle("open", appRunnerMenuOpen);
|
|
9539
|
+
}
|
|
9540
|
+
|
|
8191
9541
|
function setOptionsMenuOpen(open) {
|
|
8192
9542
|
optionsMenuOpen = !!open;
|
|
8193
9543
|
elements.optionsMenuButton.setAttribute("aria-expanded", optionsMenuOpen ? "true" : "false");
|
|
@@ -8435,6 +9785,7 @@ async function installOptionalFeature(featureId) {
|
|
|
8435
9785
|
function runPublishWorkflow(command) {
|
|
8436
9786
|
setComposerActionsOpen(false);
|
|
8437
9787
|
setPublishMenuOpen(false);
|
|
9788
|
+
setAppRunnerMenuOpen(false);
|
|
8438
9789
|
setOptionsMenuOpen(false);
|
|
8439
9790
|
const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0];
|
|
8440
9791
|
const featureId = OPTIONAL_COMMAND_FEATURES.get(commandName);
|
|
@@ -8453,6 +9804,7 @@ async function runNativeCommandMenu(command) {
|
|
|
8453
9804
|
setComposerActionsOpen(false);
|
|
8454
9805
|
setPublishMenuOpen(false);
|
|
8455
9806
|
setNativeCommandMenuOpen(false);
|
|
9807
|
+
setAppRunnerMenuOpen(false);
|
|
8456
9808
|
setOptionsMenuOpen(false);
|
|
8457
9809
|
const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0].toLowerCase();
|
|
8458
9810
|
const featureId = optionalFeatureIdForCommand(commandName);
|
|
@@ -10130,6 +11482,7 @@ async function refreshAll(tabContext = activeTabContext()) {
|
|
|
10130
11482
|
refreshCommands(tabContext),
|
|
10131
11483
|
refreshStats(tabContext),
|
|
10132
11484
|
refreshWorkspace(tabContext),
|
|
11485
|
+
refreshAppRunners(tabContext),
|
|
10133
11486
|
refreshNativeSettings(tabContext),
|
|
10134
11487
|
refreshNetworkStatus(),
|
|
10135
11488
|
refreshWebuiVersion(),
|
|
@@ -10844,6 +12197,11 @@ function handleEvent(event) {
|
|
|
10844
12197
|
case "webui_connected":
|
|
10845
12198
|
setWebuiVersion(event.version);
|
|
10846
12199
|
setWebuiDevServer(isWebuiDevMetadata(event));
|
|
12200
|
+
if (Object.prototype.hasOwnProperty.call(event, "activeRun")) {
|
|
12201
|
+
setAppRunnerData(event.tabId || activeTabId, { cwd: event.cwd, activeRun: event.activeRun });
|
|
12202
|
+
renderAppRunnerControls();
|
|
12203
|
+
renderWidgets();
|
|
12204
|
+
}
|
|
10847
12205
|
addEvent(`connected to ${event.tabTitle || "terminal"} for ${event.cwd}`);
|
|
10848
12206
|
scheduleForegroundReconcile("event stream reconnect", 0);
|
|
10849
12207
|
break;
|
|
@@ -10865,6 +12223,7 @@ function handleEvent(event) {
|
|
|
10865
12223
|
case "webui_tab_reloaded":
|
|
10866
12224
|
addEvent(`${event.tabTitle || "terminal"} reloaded`);
|
|
10867
12225
|
addTransientMessage({ role: "native", title: "/reload", content: `${event.tabTitle || "terminal"} reloaded. Keybindings, extensions, skills, prompts, and themes were refreshed by restarting the RPC tab${event.sessionFile ? ` and resuming ${event.sessionFile}` : ""}.`, level: "info" });
|
|
12226
|
+
clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
10868
12227
|
statusEntries.clear();
|
|
10869
12228
|
widgets.clear();
|
|
10870
12229
|
resetOptionalFeatureAvailability();
|
|
@@ -10882,9 +12241,18 @@ function handleEvent(event) {
|
|
|
10882
12241
|
removeQueuedDialogRequests(event.ids || []);
|
|
10883
12242
|
addEvent(`cancelled ${event.ids?.length || 0} pending extension UI request(s)`, "warn");
|
|
10884
12243
|
break;
|
|
12244
|
+
case "webui_app_runner_update":
|
|
12245
|
+
setAppRunnerData(event.tabId || activeTabId, { cwd: event.cwd, activeRun: event.activeRun });
|
|
12246
|
+
renderAppRunnerControls();
|
|
12247
|
+
renderWidgets();
|
|
12248
|
+
break;
|
|
10885
12249
|
case "webui_cwd_changed":
|
|
10886
12250
|
addEvent(`${event.tabTitle || "terminal"} cwd changed to ${event.cwd}`);
|
|
12251
|
+
setAppRunnerData(event.tabId || activeTabId, { cwd: event.cwd, activeRun: null, runners: [] });
|
|
12252
|
+
renderAppRunnerControls();
|
|
12253
|
+
renderWidgets();
|
|
10887
12254
|
refreshTabs().catch((error) => addEvent(error.message, "error"));
|
|
12255
|
+
refreshAppRunners(tabContext).catch((error) => addEvent(error.message, "error"));
|
|
10888
12256
|
scheduleRefreshFooter();
|
|
10889
12257
|
break;
|
|
10890
12258
|
case "webui_network_rebinding": {
|
|
@@ -10928,6 +12296,7 @@ function handleEvent(event) {
|
|
|
10928
12296
|
case "agent_end":
|
|
10929
12297
|
addEvent("agent finished");
|
|
10930
12298
|
notifyAgentDone(event.tabId || activeTabId, { activity: event.tabActivity, tabTitle: event.tabTitle });
|
|
12299
|
+
clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
10931
12300
|
if (currentState) currentState = { ...currentState, isStreaming: false };
|
|
10932
12301
|
clearRunIndicatorActivity();
|
|
10933
12302
|
markTabOutputSeen();
|
|
@@ -10941,6 +12310,10 @@ function handleEvent(event) {
|
|
|
10941
12310
|
const workflow = gitWorkflowForTab(workflowTabId, { create: false });
|
|
10942
12311
|
if (workflow?.active && workflow.step === "generating") {
|
|
10943
12312
|
loadGitWorkflowMessage({ requireFresh: true, retries: 3, runId: workflow.runId, tabId: workflowTabId });
|
|
12313
|
+
} else if (workflow?.active && workflow.step === "branchNaming") {
|
|
12314
|
+
loadGitWorkflowBranchName({ requireFresh: true, retries: 3, runId: workflow.runId, tabId: workflowTabId });
|
|
12315
|
+
} else if (workflow?.active && workflow.step === "prGenerating") {
|
|
12316
|
+
loadGitWorkflowPr({ requireFresh: true, retries: 3, runId: workflow.runId, tabId: workflowTabId });
|
|
10944
12317
|
}
|
|
10945
12318
|
}
|
|
10946
12319
|
break;
|
|
@@ -10979,15 +12352,22 @@ function handleEvent(event) {
|
|
|
10979
12352
|
break;
|
|
10980
12353
|
case "compaction_start":
|
|
10981
12354
|
if (currentState) currentState = { ...currentState, isCompacting: true };
|
|
12355
|
+
markContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
10982
12356
|
setRunIndicatorActivity(`Compacting context${event.reason ? ` (${event.reason})` : ""}…`);
|
|
10983
12357
|
addEvent(`compaction started (${event.reason})`);
|
|
12358
|
+
renderStatus();
|
|
10984
12359
|
break;
|
|
10985
12360
|
case "compaction_end":
|
|
10986
12361
|
if (currentState) currentState = { ...currentState, isCompacting: false };
|
|
12362
|
+
if (event.aborted) clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
12363
|
+
else markContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
10987
12364
|
addEvent(`compaction ${event.aborted ? "aborted" : "finished"}`);
|
|
10988
12365
|
if (!currentState?.isStreaming) clearRunIndicatorActivity();
|
|
10989
12366
|
markTabOutputSeen();
|
|
12367
|
+
renderStatus();
|
|
12368
|
+
scheduleRefreshState();
|
|
10990
12369
|
scheduleRefreshMessages();
|
|
12370
|
+
scheduleRefreshFooter();
|
|
10991
12371
|
break;
|
|
10992
12372
|
case "auto_retry_start": {
|
|
10993
12373
|
const seconds = Math.max(0, Math.ceil(Number(event.delayMs || 0) / 1000));
|
|
@@ -11031,6 +12411,7 @@ function handleEvent(event) {
|
|
|
11031
12411
|
applyOptimisticThinkingSelection(event.data, tabContext);
|
|
11032
12412
|
} else if (event.command === "new_session") {
|
|
11033
12413
|
const tabId = event.tabId || activeTabId;
|
|
12414
|
+
clearContextUsageUnknownAfterCompaction(tabId);
|
|
11034
12415
|
forgetLastUserPrompt(tabId);
|
|
11035
12416
|
resetGitWorkflowForTab(tabId);
|
|
11036
12417
|
}
|
|
@@ -11087,6 +12468,23 @@ elements.promptListSaveButton?.addEventListener("click", saveDisplayedPromptList
|
|
|
11087
12468
|
elements.promptListRunListButton?.addEventListener("click", () => runDisplayedPromptList());
|
|
11088
12469
|
elements.promptListCloseButton?.addEventListener("click", () => elements.promptListDialog?.close());
|
|
11089
12470
|
elements.promptListDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
12471
|
+
elements.attachmentTextCancelButton?.addEventListener("click", closeTextAttachmentEditor);
|
|
12472
|
+
elements.attachmentTextSaveButton?.addEventListener("click", saveTextAttachmentEdit);
|
|
12473
|
+
elements.attachmentTextEditor?.addEventListener("input", () => {
|
|
12474
|
+
renderTextAttachmentEditorMeta();
|
|
12475
|
+
setAttachmentTextStatus("Unsaved attachment edits.", "warn");
|
|
12476
|
+
});
|
|
12477
|
+
elements.attachmentTextDialog?.addEventListener("close", () => {
|
|
12478
|
+
activeTextAttachmentEditor = null;
|
|
12479
|
+
if (elements.attachmentTextEditor) elements.attachmentTextEditor.value = "";
|
|
12480
|
+
setAttachmentTextStatus("");
|
|
12481
|
+
});
|
|
12482
|
+
elements.attachmentTextDialog?.addEventListener("keydown", (event) => {
|
|
12483
|
+
if (!(event.ctrlKey || event.metaKey) || event.altKey || event.shiftKey || event.key.toLowerCase() !== "s") return;
|
|
12484
|
+
event.preventDefault();
|
|
12485
|
+
if (!elements.attachmentTextSaveButton?.disabled) saveTextAttachmentEdit();
|
|
12486
|
+
});
|
|
12487
|
+
elements.attachmentTextDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
11090
12488
|
elements.sendFeedbackButton.addEventListener("click", () => submitQueuedActionFeedback());
|
|
11091
12489
|
elements.composer.addEventListener("submit", (event) => {
|
|
11092
12490
|
event.preventDefault();
|
|
@@ -11149,17 +12547,20 @@ elements.gitWorkflowButton.addEventListener("click", () => {
|
|
|
11149
12547
|
const publishMenuContainer = elements.publishButton.parentElement;
|
|
11150
12548
|
elements.publishButton.addEventListener("click", () => {
|
|
11151
12549
|
setNativeCommandMenuOpen(false);
|
|
12550
|
+
setAppRunnerMenuOpen(false);
|
|
11152
12551
|
setOptionsMenuOpen(false);
|
|
11153
12552
|
setPublishMenuOpen(true);
|
|
11154
12553
|
});
|
|
11155
12554
|
publishMenuContainer?.addEventListener("pointerenter", () => {
|
|
11156
12555
|
setNativeCommandMenuOpen(false);
|
|
12556
|
+
setAppRunnerMenuOpen(false);
|
|
11157
12557
|
setOptionsMenuOpen(false);
|
|
11158
12558
|
setPublishMenuOpen(true);
|
|
11159
12559
|
});
|
|
11160
12560
|
publishMenuContainer?.addEventListener("pointerleave", () => setPublishMenuOpen(false));
|
|
11161
12561
|
publishMenuContainer?.addEventListener("focusin", () => {
|
|
11162
12562
|
setNativeCommandMenuOpen(false);
|
|
12563
|
+
setAppRunnerMenuOpen(false);
|
|
11163
12564
|
setOptionsMenuOpen(false);
|
|
11164
12565
|
setPublishMenuOpen(true);
|
|
11165
12566
|
});
|
|
@@ -11171,17 +12572,20 @@ publishMenuContainer?.addEventListener("focusout", () => {
|
|
|
11171
12572
|
const nativeCommandMenuContainer = elements.nativeCommandMenuButton.parentElement;
|
|
11172
12573
|
elements.nativeCommandMenuButton.addEventListener("click", () => {
|
|
11173
12574
|
setPublishMenuOpen(false);
|
|
12575
|
+
setAppRunnerMenuOpen(false);
|
|
11174
12576
|
setOptionsMenuOpen(false);
|
|
11175
12577
|
setNativeCommandMenuOpen(true);
|
|
11176
12578
|
});
|
|
11177
12579
|
nativeCommandMenuContainer?.addEventListener("pointerenter", () => {
|
|
11178
12580
|
setPublishMenuOpen(false);
|
|
12581
|
+
setAppRunnerMenuOpen(false);
|
|
11179
12582
|
setOptionsMenuOpen(false);
|
|
11180
12583
|
setNativeCommandMenuOpen(true);
|
|
11181
12584
|
});
|
|
11182
12585
|
nativeCommandMenuContainer?.addEventListener("pointerleave", () => setNativeCommandMenuOpen(false));
|
|
11183
12586
|
nativeCommandMenuContainer?.addEventListener("focusin", () => {
|
|
11184
12587
|
setPublishMenuOpen(false);
|
|
12588
|
+
setAppRunnerMenuOpen(false);
|
|
11185
12589
|
setOptionsMenuOpen(false);
|
|
11186
12590
|
setNativeCommandMenuOpen(true);
|
|
11187
12591
|
});
|
|
@@ -11190,21 +12594,66 @@ nativeCommandMenuContainer?.addEventListener("focusout", () => {
|
|
|
11190
12594
|
if (!nativeCommandMenuContainer?.contains(document.activeElement)) setNativeCommandMenuOpen(false);
|
|
11191
12595
|
}, 0);
|
|
11192
12596
|
});
|
|
12597
|
+
const appRunnerMenuContainer = elements.appRunnerMenuButton?.parentElement;
|
|
12598
|
+
elements.appRunnerInfoButton?.addEventListener("click", (event) => {
|
|
12599
|
+
event.preventDefault();
|
|
12600
|
+
event.stopPropagation();
|
|
12601
|
+
openAppRunnerInfoDialog();
|
|
12602
|
+
});
|
|
12603
|
+
elements.appRunnerInfoCloseButton?.addEventListener("click", closeAppRunnerInfoDialog);
|
|
12604
|
+
elements.appRunnerMenuButton?.addEventListener("click", async () => {
|
|
12605
|
+
setPublishMenuOpen(false);
|
|
12606
|
+
setNativeCommandMenuOpen(false);
|
|
12607
|
+
setOptionsMenuOpen(false);
|
|
12608
|
+
setAppRunnerMenuOpen(false);
|
|
12609
|
+
const tabContext = activeTabContext();
|
|
12610
|
+
try {
|
|
12611
|
+
await refreshAppRunners(tabContext);
|
|
12612
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
12613
|
+
if (appRunnerMenuCanOpen()) setAppRunnerMenuOpen(true);
|
|
12614
|
+
else if (!appRunnerIsRunning(activeAppRunnerData().activeRun)) openAppRunnerInfoDialog();
|
|
12615
|
+
} catch (error) {
|
|
12616
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
12617
|
+
}
|
|
12618
|
+
});
|
|
12619
|
+
appRunnerMenuContainer?.addEventListener("pointerenter", () => {
|
|
12620
|
+
if (elements.appRunnerMenuButton?.disabled || !appRunnerMenuCanOpen()) return;
|
|
12621
|
+
setPublishMenuOpen(false);
|
|
12622
|
+
setNativeCommandMenuOpen(false);
|
|
12623
|
+
setOptionsMenuOpen(false);
|
|
12624
|
+
setAppRunnerMenuOpen(true);
|
|
12625
|
+
});
|
|
12626
|
+
appRunnerMenuContainer?.addEventListener("pointerleave", () => setAppRunnerMenuOpen(false));
|
|
12627
|
+
appRunnerMenuContainer?.addEventListener("focusin", () => {
|
|
12628
|
+
if (elements.appRunnerMenuButton?.disabled || !appRunnerMenuCanOpen()) return;
|
|
12629
|
+
setPublishMenuOpen(false);
|
|
12630
|
+
setNativeCommandMenuOpen(false);
|
|
12631
|
+
setOptionsMenuOpen(false);
|
|
12632
|
+
setAppRunnerMenuOpen(true);
|
|
12633
|
+
});
|
|
12634
|
+
appRunnerMenuContainer?.addEventListener("focusout", () => {
|
|
12635
|
+
setTimeout(() => {
|
|
12636
|
+
if (!appRunnerMenuContainer?.contains(document.activeElement)) setAppRunnerMenuOpen(false);
|
|
12637
|
+
}, 0);
|
|
12638
|
+
});
|
|
11193
12639
|
const optionsMenuContainer = elements.optionsMenuButton.parentElement;
|
|
11194
12640
|
elements.optionsMenuButton.addEventListener("click", () => {
|
|
11195
12641
|
setPublishMenuOpen(false);
|
|
11196
12642
|
setNativeCommandMenuOpen(false);
|
|
12643
|
+
setAppRunnerMenuOpen(false);
|
|
11197
12644
|
setOptionsMenuOpen(true);
|
|
11198
12645
|
});
|
|
11199
12646
|
optionsMenuContainer?.addEventListener("pointerenter", () => {
|
|
11200
12647
|
setPublishMenuOpen(false);
|
|
11201
12648
|
setNativeCommandMenuOpen(false);
|
|
12649
|
+
setAppRunnerMenuOpen(false);
|
|
11202
12650
|
setOptionsMenuOpen(true);
|
|
11203
12651
|
});
|
|
11204
12652
|
optionsMenuContainer?.addEventListener("pointerleave", () => setOptionsMenuOpen(false));
|
|
11205
12653
|
optionsMenuContainer?.addEventListener("focusin", () => {
|
|
11206
12654
|
setPublishMenuOpen(false);
|
|
11207
12655
|
setNativeCommandMenuOpen(false);
|
|
12656
|
+
setAppRunnerMenuOpen(false);
|
|
11208
12657
|
setOptionsMenuOpen(true);
|
|
11209
12658
|
});
|
|
11210
12659
|
optionsMenuContainer?.addEventListener("focusout", () => {
|
|
@@ -11224,7 +12673,32 @@ elements.optionsSettingsButton.addEventListener("click", () => runNativeCommandM
|
|
|
11224
12673
|
elements.optionsExportButton.addEventListener("click", () => runNativeCommandMenu("/export"));
|
|
11225
12674
|
elements.optionsForkButton.addEventListener("click", () => runNativeCommandMenu("/fork"));
|
|
11226
12675
|
elements.optionsTreeButton.addEventListener("click", () => runNativeCommandMenu("/tree"));
|
|
12676
|
+
elements.gitWorkflowSteps.addEventListener("click", (event) => {
|
|
12677
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
12678
|
+
const button = target?.closest("[data-git-workflow-process]");
|
|
12679
|
+
if (!button || !elements.gitWorkflowSteps.contains(button) || button.disabled) return;
|
|
12680
|
+
selectGitWorkflowProcess(button.dataset.gitWorkflowProcess);
|
|
12681
|
+
});
|
|
11227
12682
|
elements.gitWorkflowCancelButton.addEventListener("click", () => cancelGitWorkflow());
|
|
12683
|
+
elements.gitPrCancelButton?.addEventListener("click", () => resolveGitPrDialog(null));
|
|
12684
|
+
elements.gitPrCreateButton?.addEventListener("click", () => {
|
|
12685
|
+
const title = elements.gitPrTitleInput?.value.trim() || "";
|
|
12686
|
+
const body = elements.gitPrBodyEditor?.value.trimEnd() || "";
|
|
12687
|
+
if (!title) {
|
|
12688
|
+
setGitPrDialogStatus("PR title is required.", "error");
|
|
12689
|
+
elements.gitPrTitleInput?.focus();
|
|
12690
|
+
return;
|
|
12691
|
+
}
|
|
12692
|
+
if (!body.trim()) {
|
|
12693
|
+
setGitPrDialogStatus("PR description is required.", "error");
|
|
12694
|
+
elements.gitPrBodyEditor?.focus();
|
|
12695
|
+
return;
|
|
12696
|
+
}
|
|
12697
|
+
resolveGitPrDialog({ title, body });
|
|
12698
|
+
});
|
|
12699
|
+
elements.gitPrDialog?.addEventListener("close", () => {
|
|
12700
|
+
if (activeGitPrDialogResolve) resolveGitPrDialog(null);
|
|
12701
|
+
});
|
|
11228
12702
|
elements.nativeCommandDialog.addEventListener("close", () => {
|
|
11229
12703
|
elements.nativeCommandSearch.oninput = null;
|
|
11230
12704
|
nativeCommandTabId = null;
|
|
@@ -11320,6 +12794,8 @@ elements.compactButton.addEventListener("click", async () => {
|
|
|
11320
12794
|
elements.compactButton.textContent = "Compacting…";
|
|
11321
12795
|
setRunIndicatorActivity("Requesting context compaction…");
|
|
11322
12796
|
scrollChatToBottom({ force: true });
|
|
12797
|
+
markContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
12798
|
+
renderFooter();
|
|
11323
12799
|
addEvent("manual compaction requested");
|
|
11324
12800
|
await api("/api/compact", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
11325
12801
|
if (!isCurrentTabContext(tabContext)) return;
|
|
@@ -11328,7 +12804,9 @@ elements.compactButton.addEventListener("click", async () => {
|
|
|
11328
12804
|
scheduleRefreshFooter(600, tabContext);
|
|
11329
12805
|
} catch (error) {
|
|
11330
12806
|
if (isCurrentTabContext(tabContext)) {
|
|
12807
|
+
clearContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
11331
12808
|
clearRunIndicatorActivity();
|
|
12809
|
+
renderFooter();
|
|
11332
12810
|
addEvent(error.message, "error");
|
|
11333
12811
|
}
|
|
11334
12812
|
} finally {
|
|
@@ -11403,6 +12881,11 @@ if (elements.thinkingVisibilityToggle) {
|
|
|
11403
12881
|
setThinkingOutputVisible(elements.thinkingVisibilityToggle.checked, { announce: true });
|
|
11404
12882
|
});
|
|
11405
12883
|
}
|
|
12884
|
+
if (elements.terminalTabsLayoutSelect) {
|
|
12885
|
+
elements.terminalTabsLayoutSelect.addEventListener("change", () => {
|
|
12886
|
+
setTerminalTabsLayout(elements.terminalTabsLayoutSelect.value, { announce: true });
|
|
12887
|
+
});
|
|
12888
|
+
}
|
|
11406
12889
|
elements.toggleSidePanelButton.addEventListener("click", () => {
|
|
11407
12890
|
setSidePanelCollapsed(true);
|
|
11408
12891
|
});
|
|
@@ -11440,6 +12923,9 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
11440
12923
|
if (nativeCommandMenuOpen && !event.target?.closest?.(".composer-native-command-menu")) {
|
|
11441
12924
|
setNativeCommandMenuOpen(false);
|
|
11442
12925
|
}
|
|
12926
|
+
if (appRunnerMenuOpen && !event.target?.closest?.(".composer-app-runner-menu")) {
|
|
12927
|
+
setAppRunnerMenuOpen(false);
|
|
12928
|
+
}
|
|
11443
12929
|
if (optionsMenuOpen && !event.target?.closest?.(".composer-options-menu")) {
|
|
11444
12930
|
setOptionsMenuOpen(false);
|
|
11445
12931
|
}
|
|
@@ -11467,7 +12953,7 @@ function isTextEntryTarget(target) {
|
|
|
11467
12953
|
|
|
11468
12954
|
function shouldHandleNativeAppShortcut(event) {
|
|
11469
12955
|
if (event.defaultPrevented) return false;
|
|
11470
|
-
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.nativeCommandDialog?.open) return false;
|
|
12956
|
+
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.nativeCommandDialog?.open || elements.appRunnerInfoDialog?.open) return false;
|
|
11471
12957
|
return event.target === elements.promptInput || !isTextEntryTarget(event.target);
|
|
11472
12958
|
}
|
|
11473
12959
|
|
|
@@ -11535,6 +13021,10 @@ window.addEventListener("keydown", (event) => {
|
|
|
11535
13021
|
setNativeCommandMenuOpen(false);
|
|
11536
13022
|
return;
|
|
11537
13023
|
}
|
|
13024
|
+
if (appRunnerMenuOpen) {
|
|
13025
|
+
setAppRunnerMenuOpen(false);
|
|
13026
|
+
return;
|
|
13027
|
+
}
|
|
11538
13028
|
if (optionsMenuOpen) {
|
|
11539
13029
|
setOptionsMenuOpen(false);
|
|
11540
13030
|
return;
|
|
@@ -11571,7 +13061,7 @@ window.addEventListener("keydown", (event) => {
|
|
|
11571
13061
|
}
|
|
11572
13062
|
lastEmptyPromptEscapeTime = now;
|
|
11573
13063
|
}
|
|
11574
|
-
if (
|
|
13064
|
+
if (isSidePanelOverlayView() && !document.body.classList.contains("side-panel-collapsed")) {
|
|
11575
13065
|
setSidePanelCollapsed(true);
|
|
11576
13066
|
return;
|
|
11577
13067
|
}
|
|
@@ -11664,6 +13154,7 @@ elements.promptInput.addEventListener("keydown", (event) => {
|
|
|
11664
13154
|
|
|
11665
13155
|
elements.promptInput.addEventListener("input", () => {
|
|
11666
13156
|
resetPromptHistoryNavigation();
|
|
13157
|
+
if (moveLongPromptInputToAttachment()) return;
|
|
11667
13158
|
resizePromptInput();
|
|
11668
13159
|
renderCommandSuggestions();
|
|
11669
13160
|
});
|
|
@@ -11692,6 +13183,7 @@ resizePromptInput();
|
|
|
11692
13183
|
focusPromptInput({ defer: true });
|
|
11693
13184
|
updateComposerModeButtons();
|
|
11694
13185
|
updateOptionalFeatureAvailability();
|
|
13186
|
+
renderAppRunnerControls();
|
|
11695
13187
|
renderLoadedPromptListPreview();
|
|
11696
13188
|
loadLastUserPromptCache();
|
|
11697
13189
|
loadPromptHistoryCache();
|
|
@@ -11705,12 +13197,14 @@ initializeThemes().catch((error) => {
|
|
|
11705
13197
|
initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast picks: ${error.message}`, "error"));
|
|
11706
13198
|
restoreAgentDoneNotificationsSetting();
|
|
11707
13199
|
restoreThinkingVisibilitySetting();
|
|
13200
|
+
restoreTerminalTabsLayoutSetting();
|
|
11708
13201
|
restoreToolOutputExpansionSetting();
|
|
11709
13202
|
restoreSidePanelSectionState();
|
|
11710
13203
|
bindSidePanelSectionToggles();
|
|
11711
13204
|
restoreSidePanelState();
|
|
11712
13205
|
initializeCodexUsage();
|
|
11713
13206
|
bindMobileViewChanges();
|
|
13207
|
+
bindSidePanelOverlayViewChanges();
|
|
11714
13208
|
registerPwaServiceWorker();
|
|
11715
13209
|
renderServerOfflinePanel();
|
|
11716
13210
|
initializeTabs().catch((error) => addEvent(error.message, "error"));
|