@firstpick/pi-package-webui 0.3.1 → 0.3.3
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 +1580 -44
- package/public/index.html +78 -4
- package/public/service-worker.js +1 -1
- package/public/styles.css +655 -31
- package/tests/mobile-static.test.mjs +98 -9
package/public/app.js
CHANGED
|
@@ -5,7 +5,11 @@ const elements = {
|
|
|
5
5
|
webuiDevBadge: $("#webuiDevBadge"),
|
|
6
6
|
tabBar: $("#tabBar"),
|
|
7
7
|
terminalTabsToggleButton: $("#terminalTabsToggleButton"),
|
|
8
|
+
newTabMenu: $("#newTabMenu"),
|
|
8
9
|
newTabButton: $("#newTabButton"),
|
|
10
|
+
newTabMenuPanel: $("#newTabMenuPanel"),
|
|
11
|
+
newTabCurrentDirectoryButton: $("#newTabCurrentDirectoryButton"),
|
|
12
|
+
newTabChooseDirectoryButton: $("#newTabChooseDirectoryButton"),
|
|
9
13
|
closeAllTabsButton: $("#closeAllTabsButton"),
|
|
10
14
|
statusBar: $("#statusBar"),
|
|
11
15
|
serverOfflinePanel: $("#serverOfflinePanel"),
|
|
@@ -46,6 +50,10 @@ const elements = {
|
|
|
46
50
|
nativeCommandMenu: $("#nativeCommandMenu"),
|
|
47
51
|
nativeSkillsButton: $("#nativeSkillsButton"),
|
|
48
52
|
nativeToolsButton: $("#nativeToolsButton"),
|
|
53
|
+
appRunnerMenu: $("#appRunnerMenu"),
|
|
54
|
+
appRunnerInfoButton: $("#appRunnerInfoButton"),
|
|
55
|
+
appRunnerMenuButton: $("#appRunnerMenuButton"),
|
|
56
|
+
appRunnerMenuPanel: $("#appRunnerMenuPanel"),
|
|
49
57
|
optionsMenuButton: $("#optionsMenuButton"),
|
|
50
58
|
optionsMenu: $("#optionsMenu"),
|
|
51
59
|
optionsResumeButton: $("#optionsResumeButton"),
|
|
@@ -63,6 +71,12 @@ const elements = {
|
|
|
63
71
|
gitWorkflowOutput: $("#gitWorkflowOutput"),
|
|
64
72
|
gitWorkflowActions: $("#gitWorkflowActions"),
|
|
65
73
|
gitWorkflowCancelButton: $("#gitWorkflowCancelButton"),
|
|
74
|
+
gitPrDialog: $("#gitPrDialog"),
|
|
75
|
+
gitPrTitleInput: $("#gitPrTitleInput"),
|
|
76
|
+
gitPrBodyEditor: $("#gitPrBodyEditor"),
|
|
77
|
+
gitPrStatus: $("#gitPrStatus"),
|
|
78
|
+
gitPrCancelButton: $("#gitPrCancelButton"),
|
|
79
|
+
gitPrCreateButton: $("#gitPrCreateButton"),
|
|
66
80
|
modelSelect: $("#modelSelect"),
|
|
67
81
|
setModelButton: $("#setModelButton"),
|
|
68
82
|
thinkingSelect: $("#thinkingSelect"),
|
|
@@ -109,6 +123,13 @@ const elements = {
|
|
|
109
123
|
promptListDialogLoadButton: $("#promptListDialogLoadButton"),
|
|
110
124
|
promptListSaveButton: $("#promptListSaveButton"),
|
|
111
125
|
promptListRunListButton: $("#promptListRunListButton"),
|
|
126
|
+
attachmentTextDialog: $("#attachmentTextDialog"),
|
|
127
|
+
attachmentTextTitle: $("#attachmentTextTitle"),
|
|
128
|
+
attachmentTextMeta: $("#attachmentTextMeta"),
|
|
129
|
+
attachmentTextEditor: $("#attachmentTextEditor"),
|
|
130
|
+
attachmentTextStatus: $("#attachmentTextStatus"),
|
|
131
|
+
attachmentTextCancelButton: $("#attachmentTextCancelButton"),
|
|
132
|
+
attachmentTextSaveButton: $("#attachmentTextSaveButton"),
|
|
112
133
|
commandSearchInput: $("#commandSearchInput"),
|
|
113
134
|
commandsBox: $("#commandsBox"),
|
|
114
135
|
eventLog: $("#eventLog"),
|
|
@@ -139,6 +160,9 @@ const elements = {
|
|
|
139
160
|
nativeCommandBody: $("#nativeCommandBody"),
|
|
140
161
|
nativeCommandError: $("#nativeCommandError"),
|
|
141
162
|
nativeCommandActions: $("#nativeCommandActions"),
|
|
163
|
+
appRunnerInfoDialog: $("#appRunnerInfoDialog"),
|
|
164
|
+
appRunnerInfoBody: $("#appRunnerInfoBody"),
|
|
165
|
+
appRunnerInfoCloseButton: $("#appRunnerInfoCloseButton"),
|
|
142
166
|
};
|
|
143
167
|
|
|
144
168
|
let currentState = null;
|
|
@@ -147,6 +171,7 @@ let activeTabId = null;
|
|
|
147
171
|
let activeTabGeneration = 0;
|
|
148
172
|
let tabDrafts = new Map();
|
|
149
173
|
let tabAttachments = new Map();
|
|
174
|
+
let activeTextAttachmentEditor = null;
|
|
150
175
|
let tabActivities = new Map();
|
|
151
176
|
let tabSeenCompletionSerials = new Map();
|
|
152
177
|
let streamBubble = null;
|
|
@@ -174,14 +199,20 @@ let refreshTabsTimer = null;
|
|
|
174
199
|
let foregroundReconcileTimer = null;
|
|
175
200
|
let eventSource = null;
|
|
176
201
|
let activeDialog = null;
|
|
202
|
+
let activeGitPrDialogResolve = null;
|
|
177
203
|
let nativeCommandTabId = null;
|
|
178
204
|
let pathPickerState = null;
|
|
205
|
+
let firstTerminalCwdPromptShown = false;
|
|
179
206
|
let pathFastPicks = [];
|
|
180
207
|
let pathFastPicksReady = false;
|
|
181
208
|
let pathFastPicksLoadPromise = null;
|
|
182
209
|
let mobileTabsExpanded = false;
|
|
183
210
|
let openTerminalTabGroupKey = null;
|
|
211
|
+
let newTabMenuOpen = false;
|
|
184
212
|
let nativeCommandMenuOpen = false;
|
|
213
|
+
let appRunnerMenuOpen = false;
|
|
214
|
+
let appRunnerCustomDraft = { id: "", label: "", command: "./", path: "", args: "" };
|
|
215
|
+
let appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
185
216
|
let optionsMenuOpen = false;
|
|
186
217
|
let availableCommands = [];
|
|
187
218
|
let rawAvailableCommands = [];
|
|
@@ -239,6 +270,7 @@ let customBackgroundLoading = false;
|
|
|
239
270
|
let footerScopedModels = [];
|
|
240
271
|
let footerScopedModelPatterns = [];
|
|
241
272
|
let footerScopedModelSource = "none";
|
|
273
|
+
const contextUsageUnknownAfterCompactionByTab = new Map();
|
|
242
274
|
let autoFollowChat = true;
|
|
243
275
|
let chatFollowFrame = null;
|
|
244
276
|
let chatFollowSettleTimer = null;
|
|
@@ -288,10 +320,13 @@ const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
|
288
320
|
const ATTACHMENT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
289
321
|
const ATTACHMENT_INLINE_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
|
|
290
322
|
const ATTACHMENT_INLINE_IMAGE_TOTAL_MAX_BYTES = 16 * 1024 * 1024;
|
|
323
|
+
const LONG_INPUT_ATTACHMENT_LINE_THRESHOLD = 20;
|
|
324
|
+
const LONG_INPUT_ATTACHMENT_MIME_TYPE = "text/plain";
|
|
291
325
|
const INLINE_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
292
326
|
const BACKGROUND_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
293
327
|
const DEFAULT_THEME_NAME = "catppuccin-mocha";
|
|
294
328
|
const MOBILE_VIEW_QUERY = "(max-width: 720px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
329
|
+
const SIDE_PANEL_OVERLAY_QUERY = "(max-width: 1050px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
295
330
|
const CHAT_BOTTOM_THRESHOLD_PX = 96;
|
|
296
331
|
const STICKY_USER_PROMPT_PREVIEW_LIMIT = 220;
|
|
297
332
|
const STICKY_USER_PROMPT_TOP_GAP_PX = 12;
|
|
@@ -320,10 +355,12 @@ const BLOCKED_TAB_NOTIFICATION_TAG_PREFIX = "pi-webui-blocked-tab";
|
|
|
320
355
|
const AGENT_DONE_NOTIFICATION_TAG_PREFIX = "pi-webui-agent-done";
|
|
321
356
|
const BLOCKED_TAB_NOTIFICATION_ICON = "/icon-192.png";
|
|
322
357
|
const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
358
|
+
const sidePanelOverlayMedia = window.matchMedia?.(SIDE_PANEL_OVERLAY_QUERY) || mobileViewMedia;
|
|
323
359
|
const statusEntries = new Map();
|
|
324
360
|
const widgets = new Map();
|
|
325
361
|
const todoProgressWidgetExpandedByTab = new Map();
|
|
326
362
|
const releaseNpmOutputExpandedByTab = new Map();
|
|
363
|
+
const appRunnerDataByTab = new Map();
|
|
327
364
|
const liveToolRuns = new Map();
|
|
328
365
|
const liveToolCards = new Map();
|
|
329
366
|
const liveToolRenderQueue = new Map();
|
|
@@ -410,6 +447,8 @@ const OPTIONAL_FEATURES = [
|
|
|
410
447
|
const OPTIONAL_FEATURE_BY_ID = new Map(OPTIONAL_FEATURES.map((feature) => [feature.id, feature]));
|
|
411
448
|
const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
412
449
|
["git-staged-msg", "gitWorkflow"],
|
|
450
|
+
["git-branch-name", "gitWorkflow"],
|
|
451
|
+
["pr", "gitWorkflow"],
|
|
413
452
|
["release-npm", "releaseNpm"],
|
|
414
453
|
["release-aur", "releaseAur"],
|
|
415
454
|
["skills", "tuiSkillsCommand"],
|
|
@@ -441,16 +480,42 @@ const SETTINGS_AUTOCOMPLETE_OPTIONS = ["3", "5", "7", "10", "15", "20"];
|
|
|
441
480
|
const optionalFeatureInstallInProgress = new Set();
|
|
442
481
|
const gitFooterPayloadRefreshInFlightByTab = new Set();
|
|
443
482
|
|
|
483
|
+
function createGitWorkflowActionsDone(patch = {}) {
|
|
484
|
+
return {
|
|
485
|
+
stage: false,
|
|
486
|
+
message: false,
|
|
487
|
+
commit: false,
|
|
488
|
+
push: false,
|
|
489
|
+
...patch,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function gitWorkflowActionDone(workflow, process) {
|
|
494
|
+
return !!createGitWorkflowActionsDone(workflow?.actionsDone)[process];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function gitWorkflowActionDonePatch(workflow, process) {
|
|
498
|
+
return { actionsDone: createGitWorkflowActionsDone({ ...workflow?.actionsDone, [process]: true }) };
|
|
499
|
+
}
|
|
500
|
+
|
|
444
501
|
function createGitWorkflowState() {
|
|
445
502
|
return {
|
|
446
503
|
active: false,
|
|
447
504
|
step: "idle",
|
|
505
|
+
process: "stage",
|
|
448
506
|
busy: false,
|
|
449
507
|
runId: 0,
|
|
450
508
|
output: "",
|
|
451
509
|
error: "",
|
|
452
510
|
message: null,
|
|
453
511
|
messageRequestedAt: 0,
|
|
512
|
+
branchName: "",
|
|
513
|
+
branchNameRequestedAt: 0,
|
|
514
|
+
actionsDone: createGitWorkflowActionsDone(),
|
|
515
|
+
prMode: false,
|
|
516
|
+
prBranch: "",
|
|
517
|
+
pr: null,
|
|
518
|
+
prRequestedAt: 0,
|
|
454
519
|
};
|
|
455
520
|
}
|
|
456
521
|
|
|
@@ -494,7 +559,13 @@ function clearGitWorkflowForTab(tabId) {
|
|
|
494
559
|
}
|
|
495
560
|
}
|
|
496
561
|
|
|
497
|
-
const
|
|
562
|
+
const GIT_WORKFLOW_PROCESSES = [
|
|
563
|
+
{ value: "stage", label: "Stage" },
|
|
564
|
+
{ value: "message", label: "Message" },
|
|
565
|
+
{ value: "commit", label: "Commit" },
|
|
566
|
+
{ value: "push", label: "Push" },
|
|
567
|
+
];
|
|
568
|
+
const GIT_WORKFLOW_PROCESS_VALUES = new Set(GIT_WORKFLOW_PROCESSES.map((process) => process.value));
|
|
498
569
|
const ACTION_FEEDBACK_REACTIONS = {
|
|
499
570
|
up: { icon: "👍", label: "Good job", title: "Good job!" },
|
|
500
571
|
down: { icon: "👎", label: "Avoid this", title: "Avoid this" },
|
|
@@ -506,11 +577,32 @@ const GIT_WORKFLOW_ACTIVE_INDEX = {
|
|
|
506
577
|
generate: 1,
|
|
507
578
|
generating: 1,
|
|
508
579
|
message: 2,
|
|
580
|
+
branchNaming: 2,
|
|
581
|
+
branching: 2,
|
|
509
582
|
committing: 2,
|
|
510
583
|
push: 3,
|
|
511
584
|
pushing: 3,
|
|
585
|
+
prGenerating: 3,
|
|
586
|
+
prReview: 3,
|
|
587
|
+
prCreating: 3,
|
|
512
588
|
done: 4,
|
|
513
589
|
};
|
|
590
|
+
const GIT_WORKFLOW_CREATE_PR_TOOLTIP = [
|
|
591
|
+
"Create PR:",
|
|
592
|
+
"1. Ask Pi to generate a type/feature-name branch from staged changes.",
|
|
593
|
+
"2. Read dev/COMMIT/staged-branch-name.txt.",
|
|
594
|
+
"3. Let you confirm or edit the generated branch name.",
|
|
595
|
+
"4. Run git switch -c <branch>.",
|
|
596
|
+
"5. Return here so you can choose Commit short or Commit long on that branch.",
|
|
597
|
+
].join("\n");
|
|
598
|
+
const GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP = [
|
|
599
|
+
"Manual branch:",
|
|
600
|
+
"1. Skip agent branch-name generation.",
|
|
601
|
+
"2. Prefill a branch from the commit message if possible.",
|
|
602
|
+
"3. Let you type or edit the type/feature-name branch name.",
|
|
603
|
+
"4. Run git switch -c <branch>.",
|
|
604
|
+
"5. Return here so you can choose Commit short or Commit long on that branch.",
|
|
605
|
+
].join("\n");
|
|
514
606
|
|
|
515
607
|
function make(tag, className, text) {
|
|
516
608
|
const node = document.createElement(tag);
|
|
@@ -527,6 +619,10 @@ function isMobileView() {
|
|
|
527
619
|
return mobileViewMedia?.matches || false;
|
|
528
620
|
}
|
|
529
621
|
|
|
622
|
+
function isSidePanelOverlayView() {
|
|
623
|
+
return sidePanelOverlayMedia?.matches || false;
|
|
624
|
+
}
|
|
625
|
+
|
|
530
626
|
function readStoredSidePanelCollapsed() {
|
|
531
627
|
try {
|
|
532
628
|
const stored = localStorage.getItem(SIDE_PANEL_STORAGE_KEY);
|
|
@@ -788,6 +884,7 @@ function setComposerActionsOpen(open) {
|
|
|
788
884
|
if (!shouldOpen) {
|
|
789
885
|
setPublishMenuOpen(false);
|
|
790
886
|
setNativeCommandMenuOpen(false);
|
|
887
|
+
setAppRunnerMenuOpen(false);
|
|
791
888
|
setOptionsMenuOpen(false);
|
|
792
889
|
}
|
|
793
890
|
}
|
|
@@ -923,7 +1020,7 @@ function setMobileTabsExpanded(expanded) {
|
|
|
923
1020
|
}
|
|
924
1021
|
|
|
925
1022
|
function syncMobileSidePanelState(collapsed) {
|
|
926
|
-
const showBackdrop = !collapsed &&
|
|
1023
|
+
const showBackdrop = !collapsed && isSidePanelOverlayView();
|
|
927
1024
|
elements.sidePanelBackdrop.hidden = !showBackdrop;
|
|
928
1025
|
if (showBackdrop) elements.sidePanel.setAttribute("aria-modal", "true");
|
|
929
1026
|
else elements.sidePanel.removeAttribute("aria-modal");
|
|
@@ -937,11 +1034,11 @@ function setSidePanelCollapsed(collapsed, { persist = true, focusPanel = false }
|
|
|
937
1034
|
elements.sidePanelExpandButton.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
938
1035
|
syncMobileSidePanelState(collapsed);
|
|
939
1036
|
|
|
940
|
-
if (!collapsed && focusPanel &&
|
|
1037
|
+
if (!collapsed && focusPanel && isSidePanelOverlayView()) {
|
|
941
1038
|
requestAnimationFrame(() => elements.toggleSidePanelButton.focus());
|
|
942
1039
|
}
|
|
943
1040
|
|
|
944
|
-
if (!persist ||
|
|
1041
|
+
if (!persist || isSidePanelOverlayView()) return;
|
|
945
1042
|
try {
|
|
946
1043
|
localStorage.setItem(SIDE_PANEL_STORAGE_KEY, collapsed ? "1" : "0");
|
|
947
1044
|
} catch {
|
|
@@ -950,7 +1047,7 @@ function setSidePanelCollapsed(collapsed, { persist = true, focusPanel = false }
|
|
|
950
1047
|
}
|
|
951
1048
|
|
|
952
1049
|
function restoreSidePanelState() {
|
|
953
|
-
if (
|
|
1050
|
+
if (isSidePanelOverlayView()) {
|
|
954
1051
|
setSidePanelCollapsed(true, { persist: false });
|
|
955
1052
|
return;
|
|
956
1053
|
}
|
|
@@ -964,7 +1061,7 @@ function bindMobileViewChanges() {
|
|
|
964
1061
|
setComposerActionsOpen(false);
|
|
965
1062
|
setMobileFooterExpanded(false);
|
|
966
1063
|
setMobileTabsExpanded(false);
|
|
967
|
-
if (event.matches) {
|
|
1064
|
+
if (event.matches || isSidePanelOverlayView()) {
|
|
968
1065
|
setSidePanelCollapsed(true, { persist: false });
|
|
969
1066
|
return;
|
|
970
1067
|
}
|
|
@@ -975,6 +1072,20 @@ function bindMobileViewChanges() {
|
|
|
975
1072
|
else mobileViewMedia.addListener?.(syncForViewport);
|
|
976
1073
|
}
|
|
977
1074
|
|
|
1075
|
+
function bindSidePanelOverlayViewChanges() {
|
|
1076
|
+
if (!sidePanelOverlayMedia || sidePanelOverlayMedia === mobileViewMedia) return;
|
|
1077
|
+
const syncForViewport = (event) => {
|
|
1078
|
+
if (event.matches) {
|
|
1079
|
+
setSidePanelCollapsed(true, { persist: false });
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
const stored = readStoredSidePanelCollapsed();
|
|
1083
|
+
setSidePanelCollapsed(stored ?? false, { persist: false });
|
|
1084
|
+
};
|
|
1085
|
+
if (typeof sidePanelOverlayMedia.addEventListener === "function") sidePanelOverlayMedia.addEventListener("change", syncForViewport);
|
|
1086
|
+
else sidePanelOverlayMedia.addListener?.(syncForViewport);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
978
1089
|
function updateVisualViewportVars() {
|
|
979
1090
|
const viewport = window.visualViewport;
|
|
980
1091
|
const viewportHeight = viewport?.height || window.innerHeight || document.documentElement.clientHeight;
|
|
@@ -1050,8 +1161,7 @@ function currentPortArg() {
|
|
|
1050
1161
|
}
|
|
1051
1162
|
|
|
1052
1163
|
function serverStartCommandText() {
|
|
1053
|
-
|
|
1054
|
-
return `pi-webui --cwd ${quoteCommandArg(cwd)}${currentPortArg()}`;
|
|
1164
|
+
return `pi-webui${currentPortArg()}`;
|
|
1055
1165
|
}
|
|
1056
1166
|
|
|
1057
1167
|
function serverStartSlashCommandText() {
|
|
@@ -1371,6 +1481,46 @@ function attachmentIcon(kind) {
|
|
|
1371
1481
|
return kind === "image" ? "🖼️" : kind === "video" ? "🎞️" : kind === "audio" ? "🎵" : kind === "doc" ? "📄" : "📎";
|
|
1372
1482
|
}
|
|
1373
1483
|
|
|
1484
|
+
function normalizeTextAttachmentContent(text) {
|
|
1485
|
+
return String(text || "").replace(/\r\n?/g, "\n");
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function textLineCount(text) {
|
|
1489
|
+
const normalized = normalizeTextAttachmentContent(text);
|
|
1490
|
+
return normalized ? normalized.split("\n").length : 0;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function shouldAttachTextInsteadOfComposerInput(text) {
|
|
1494
|
+
const normalized = normalizeTextAttachmentContent(text);
|
|
1495
|
+
return normalized.trim().length > 0 && textLineCount(normalized) > LONG_INPUT_ATTACHMENT_LINE_THRESHOLD;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function longInputAttachmentFileName() {
|
|
1499
|
+
const stamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z").replace(/:/g, "-");
|
|
1500
|
+
return `webui-input-${stamp}.txt`;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function makeTextAttachmentFile(text, name = longInputAttachmentFileName(), mimeType = LONG_INPUT_ATTACHMENT_MIME_TYPE) {
|
|
1504
|
+
const normalized = normalizeTextAttachmentContent(text);
|
|
1505
|
+
const fileName = String(name || longInputAttachmentFileName());
|
|
1506
|
+
const type = String(mimeType || LONG_INPUT_ATTACHMENT_MIME_TYPE);
|
|
1507
|
+
if (typeof File === "function") return new File([normalized], fileName, { type });
|
|
1508
|
+
const blob = new Blob([normalized], { type });
|
|
1509
|
+
try {
|
|
1510
|
+
blob.name = fileName;
|
|
1511
|
+
blob.lastModified = Date.now();
|
|
1512
|
+
} catch {
|
|
1513
|
+
// Older browsers may expose non-extensible Blob instances; the attachment record still carries the name.
|
|
1514
|
+
}
|
|
1515
|
+
return blob;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function isEditableTextAttachment(attachment) {
|
|
1519
|
+
const name = String(attachment?.name || "");
|
|
1520
|
+
const mimeType = String(attachment?.mimeType || attachment?.file?.type || inferMimeTypeFromName(name)).split(";", 1)[0].trim().toLowerCase();
|
|
1521
|
+
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);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1374
1524
|
function attachmentsForTab(tabId = activeTabId) {
|
|
1375
1525
|
return tabId ? tabAttachments.get(tabId) || [] : [];
|
|
1376
1526
|
}
|
|
@@ -1399,11 +1549,19 @@ function renderAttachmentTray() {
|
|
|
1399
1549
|
const icon = make("span", "attachment-pill-icon", attachmentIcon(attachment.kind));
|
|
1400
1550
|
const name = make("span", "attachment-pill-name", attachment.name);
|
|
1401
1551
|
const meta = make("span", "attachment-pill-meta", `${attachment.kind} · ${formatBytes(attachment.size)}`);
|
|
1552
|
+
const edit = isEditableTextAttachment(attachment) ? make("button", "attachment-edit-button", "Edit") : null;
|
|
1553
|
+
if (edit) {
|
|
1554
|
+
edit.type = "button";
|
|
1555
|
+
edit.setAttribute("aria-label", `Open and edit ${attachment.name}`);
|
|
1556
|
+
edit.addEventListener("click", () => openTextAttachmentEditor(attachment.id));
|
|
1557
|
+
}
|
|
1402
1558
|
const remove = make("button", "attachment-remove-button", "×");
|
|
1403
1559
|
remove.type = "button";
|
|
1404
1560
|
remove.setAttribute("aria-label", `Remove ${attachment.name}`);
|
|
1405
1561
|
remove.addEventListener("click", () => removeAttachment(attachment.id));
|
|
1406
|
-
pill.append(icon, name, meta
|
|
1562
|
+
pill.append(icon, name, meta);
|
|
1563
|
+
if (edit) pill.append(edit);
|
|
1564
|
+
pill.append(remove);
|
|
1407
1565
|
tray.append(pill);
|
|
1408
1566
|
}
|
|
1409
1567
|
}
|
|
@@ -1414,6 +1572,7 @@ function removeAttachment(id, tabId = activeTabId) {
|
|
|
1414
1572
|
if (index === -1) return;
|
|
1415
1573
|
const [removed] = attachments.splice(index, 1);
|
|
1416
1574
|
if (removed?.previewUrl) URL.revokeObjectURL(removed.previewUrl);
|
|
1575
|
+
if (activeTextAttachmentEditor?.tabId === tabId && activeTextAttachmentEditor?.attachmentId === id) closeTextAttachmentEditor();
|
|
1417
1576
|
if (attachments.length === 0) tabAttachments.delete(tabId);
|
|
1418
1577
|
if (tabId === activeTabId) renderAttachmentTray();
|
|
1419
1578
|
}
|
|
@@ -1423,15 +1582,16 @@ function clearAttachments(tabId = activeTabId) {
|
|
|
1423
1582
|
for (const attachment of attachments) {
|
|
1424
1583
|
if (attachment.previewUrl) URL.revokeObjectURL(attachment.previewUrl);
|
|
1425
1584
|
}
|
|
1585
|
+
if (activeTextAttachmentEditor?.tabId === tabId) closeTextAttachmentEditor();
|
|
1426
1586
|
if (tabId) tabAttachments.delete(tabId);
|
|
1427
1587
|
if (tabId === activeTabId) renderAttachmentTray();
|
|
1428
1588
|
}
|
|
1429
1589
|
|
|
1430
1590
|
function addAttachmentFiles(fileList, source = "picker") {
|
|
1431
1591
|
const files = Array.from(fileList || []).filter(Boolean);
|
|
1432
|
-
if (!files.length) return;
|
|
1592
|
+
if (!files.length) return { added: 0, skipped: [] };
|
|
1433
1593
|
const attachments = ensureAttachmentsForTab();
|
|
1434
|
-
if (!attachments.length && !activeTabId) return;
|
|
1594
|
+
if (!attachments.length && !activeTabId) return { added: 0, skipped: ["no active tab"] };
|
|
1435
1595
|
let totalBytes = attachments.reduce((sum, attachment) => sum + attachment.size, 0);
|
|
1436
1596
|
let added = 0;
|
|
1437
1597
|
const skipped = [];
|
|
@@ -1469,6 +1629,129 @@ function addAttachmentFiles(fileList, source = "picker") {
|
|
|
1469
1629
|
renderAttachmentTray();
|
|
1470
1630
|
if (added) addEvent(`attached ${added} ${added === 1 ? "file" : "files"} from ${source}`, "info");
|
|
1471
1631
|
if (skipped.length) addEvent(`skipped attachments: ${skipped.join("; ")}`, "warn");
|
|
1632
|
+
return { added, skipped };
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function attachLongTextAsFile(text, source = "input text") {
|
|
1636
|
+
if (!shouldAttachTextInsteadOfComposerInput(text)) return false;
|
|
1637
|
+
const normalized = normalizeTextAttachmentContent(text);
|
|
1638
|
+
const lineCount = textLineCount(normalized);
|
|
1639
|
+
const result = addAttachmentFiles([makeTextAttachmentFile(normalized)], `${lineCount}-line ${source}`);
|
|
1640
|
+
return result.added > 0;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function moveLongPromptInputToAttachment() {
|
|
1644
|
+
const text = elements.promptInput.value || "";
|
|
1645
|
+
if (!attachLongTextAsFile(text, "input text")) return false;
|
|
1646
|
+
elements.promptInput.value = "";
|
|
1647
|
+
resizePromptInput();
|
|
1648
|
+
hideCommandSuggestions();
|
|
1649
|
+
return true;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function attachmentById(tabId, id) {
|
|
1653
|
+
return attachmentsForTab(tabId).find((attachment) => attachment.id === id) || null;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function closeTextAttachmentEditor() {
|
|
1657
|
+
if (elements.attachmentTextDialog?.open) elements.attachmentTextDialog.close();
|
|
1658
|
+
else activeTextAttachmentEditor = null;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function setAttachmentTextStatus(message = "", level = "muted") {
|
|
1662
|
+
if (!elements.attachmentTextStatus) return;
|
|
1663
|
+
elements.attachmentTextStatus.textContent = message;
|
|
1664
|
+
elements.attachmentTextStatus.className = `attachment-text-status ${level || "muted"}`;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function renderTextAttachmentEditorMeta() {
|
|
1668
|
+
if (!activeTextAttachmentEditor || !elements.attachmentTextMeta) return;
|
|
1669
|
+
const attachment = attachmentById(activeTextAttachmentEditor.tabId, activeTextAttachmentEditor.attachmentId);
|
|
1670
|
+
if (!attachment) {
|
|
1671
|
+
elements.attachmentTextMeta.textContent = "Attachment no longer exists.";
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
const text = elements.attachmentTextEditor?.value || "";
|
|
1675
|
+
const lineCount = textLineCount(text);
|
|
1676
|
+
elements.attachmentTextMeta.textContent = `${attachment.name} · ${attachment.mimeType} · ${formatBytes(attachment.size)} · ${lineCount} ${lineCount === 1 ? "line" : "lines"}`;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function readFileAsText(file) {
|
|
1680
|
+
if (typeof file?.text === "function") return file.text();
|
|
1681
|
+
return new Promise((resolve, reject) => {
|
|
1682
|
+
const reader = new FileReader();
|
|
1683
|
+
reader.onerror = () => reject(reader.error || new Error("Failed to read text attachment"));
|
|
1684
|
+
reader.onload = () => resolve(String(reader.result || ""));
|
|
1685
|
+
reader.readAsText(file);
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
async function openTextAttachmentEditor(attachmentId, tabId = activeTabId) {
|
|
1690
|
+
const attachment = attachmentById(tabId, attachmentId);
|
|
1691
|
+
if (!attachment) return;
|
|
1692
|
+
if (!isEditableTextAttachment(attachment)) {
|
|
1693
|
+
addEvent(`${attachment.name || "attachment"} is not editable text`, "warn");
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
activeTextAttachmentEditor = { tabId, attachmentId };
|
|
1698
|
+
if (elements.attachmentTextTitle) elements.attachmentTextTitle.textContent = `Edit ${attachment.name || "text attachment"}`;
|
|
1699
|
+
if (elements.attachmentTextEditor) elements.attachmentTextEditor.value = "";
|
|
1700
|
+
if (elements.attachmentTextSaveButton) elements.attachmentTextSaveButton.disabled = true;
|
|
1701
|
+
renderTextAttachmentEditorMeta();
|
|
1702
|
+
setAttachmentTextStatus("Loading text attachment…", "muted");
|
|
1703
|
+
if (elements.attachmentTextDialog && !elements.attachmentTextDialog.open) elements.attachmentTextDialog.showModal();
|
|
1704
|
+
|
|
1705
|
+
try {
|
|
1706
|
+
const text = await readFileAsText(attachment.file);
|
|
1707
|
+
if (activeTextAttachmentEditor?.tabId !== tabId || activeTextAttachmentEditor?.attachmentId !== attachmentId) return;
|
|
1708
|
+
if (elements.attachmentTextEditor) elements.attachmentTextEditor.value = normalizeTextAttachmentContent(text);
|
|
1709
|
+
if (elements.attachmentTextSaveButton) elements.attachmentTextSaveButton.disabled = false;
|
|
1710
|
+
renderTextAttachmentEditorMeta();
|
|
1711
|
+
setAttachmentTextStatus("Edit the text, then save it back to the attachment.", "muted");
|
|
1712
|
+
queueMicrotask(() => elements.attachmentTextEditor?.focus());
|
|
1713
|
+
} catch (error) {
|
|
1714
|
+
if (elements.attachmentTextSaveButton) elements.attachmentTextSaveButton.disabled = true;
|
|
1715
|
+
setAttachmentTextStatus(`Failed to open text attachment: ${error.message || String(error)}`, "error");
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
function totalAttachmentBytesWithReplacement(tabId, attachmentId, nextSize) {
|
|
1720
|
+
return attachmentsForTab(tabId).reduce((sum, attachment) => sum + (attachment.id === attachmentId ? nextSize : attachment.size || 0), 0);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function saveTextAttachmentEdit() {
|
|
1724
|
+
if (!activeTextAttachmentEditor) return;
|
|
1725
|
+
const { tabId, attachmentId } = activeTextAttachmentEditor;
|
|
1726
|
+
const attachment = attachmentById(tabId, attachmentId);
|
|
1727
|
+
if (!attachment) {
|
|
1728
|
+
setAttachmentTextStatus("Attachment no longer exists.", "error");
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
const text = elements.attachmentTextEditor?.value || "";
|
|
1733
|
+
const name = attachment.name || longInputAttachmentFileName();
|
|
1734
|
+
const mimeType = attachment.mimeType || inferMimeTypeFromName(name) || LONG_INPUT_ATTACHMENT_MIME_TYPE;
|
|
1735
|
+
const nextFile = makeTextAttachmentFile(text, name, mimeType);
|
|
1736
|
+
if (nextFile.size > ATTACHMENT_MAX_FILE_BYTES) {
|
|
1737
|
+
setAttachmentTextStatus(`Edited file is larger than ${formatBytes(ATTACHMENT_MAX_FILE_BYTES)}.`, "error");
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
if (totalAttachmentBytesWithReplacement(tabId, attachmentId, nextFile.size) > ATTACHMENT_MAX_TOTAL_BYTES) {
|
|
1741
|
+
setAttachmentTextStatus(`Edited attachments exceed ${formatBytes(ATTACHMENT_MAX_TOTAL_BYTES)} total.`, "error");
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
if (attachment.previewUrl) URL.revokeObjectURL(attachment.previewUrl);
|
|
1746
|
+
attachment.file = nextFile;
|
|
1747
|
+
attachment.name = name;
|
|
1748
|
+
attachment.mimeType = nextFile.type || mimeType;
|
|
1749
|
+
attachment.size = nextFile.size || 0;
|
|
1750
|
+
attachment.kind = attachmentKind(attachment.mimeType, attachment.name);
|
|
1751
|
+
attachment.previewUrl = undefined;
|
|
1752
|
+
if (tabId === activeTabId) renderAttachmentTray();
|
|
1753
|
+
addEvent(`updated text attachment ${attachment.name} (${formatBytes(attachment.size)})`, "info");
|
|
1754
|
+
closeTextAttachmentEditor();
|
|
1472
1755
|
}
|
|
1473
1756
|
|
|
1474
1757
|
function clipboardFiles(dataTransfer) {
|
|
@@ -1496,9 +1779,15 @@ function clipboardFiles(dataTransfer) {
|
|
|
1496
1779
|
|
|
1497
1780
|
function handleAttachmentPaste(event) {
|
|
1498
1781
|
const files = clipboardFiles(event.clipboardData);
|
|
1499
|
-
if (
|
|
1782
|
+
if (files.length) {
|
|
1783
|
+
event.preventDefault();
|
|
1784
|
+
addAttachmentFiles(files, "clipboard");
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
const text = event.clipboardData?.getData("text/plain") || "";
|
|
1789
|
+
if (!attachLongTextAsFile(text, "clipboard text")) return;
|
|
1500
1790
|
event.preventDefault();
|
|
1501
|
-
addAttachmentFiles(files, "clipboard");
|
|
1502
1791
|
}
|
|
1503
1792
|
|
|
1504
1793
|
function isFileDrag(event) {
|
|
@@ -2587,7 +2876,7 @@ function restoreActiveDraft() {
|
|
|
2587
2876
|
|
|
2588
2877
|
function focusPromptInput({ defer = false } = {}) {
|
|
2589
2878
|
const focus = () => {
|
|
2590
|
-
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.nativeCommandDialog.open || elements.promptListDialog?.open || document.visibilityState === "hidden") return;
|
|
2879
|
+
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;
|
|
2591
2880
|
try {
|
|
2592
2881
|
elements.promptInput.focus({ preventScroll: true });
|
|
2593
2882
|
} catch {
|
|
@@ -2661,6 +2950,7 @@ function resetActiveTabUi() {
|
|
|
2661
2950
|
else renderQueue({ tabId: activeTabId, steering: [], followUp: [] });
|
|
2662
2951
|
elements.commandsBox.textContent = "Loading…";
|
|
2663
2952
|
elements.commandsBox.classList.add("muted");
|
|
2953
|
+
renderAppRunnerControls();
|
|
2664
2954
|
renderWidgets();
|
|
2665
2955
|
renderGitWorkflow();
|
|
2666
2956
|
renderFooter();
|
|
@@ -2873,6 +3163,35 @@ function clearOpenTerminalTabGroup(groupKey, { force = false } = {}) {
|
|
|
2873
3163
|
syncTabPolling();
|
|
2874
3164
|
}
|
|
2875
3165
|
|
|
3166
|
+
function setNewTabMenuOpen(open) {
|
|
3167
|
+
newTabMenuOpen = !!open;
|
|
3168
|
+
elements.newTabButton?.setAttribute("aria-expanded", newTabMenuOpen ? "true" : "false");
|
|
3169
|
+
elements.newTabButton?.classList.toggle("menu-open", newTabMenuOpen);
|
|
3170
|
+
elements.newTabMenu?.classList.toggle("open", newTabMenuOpen);
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
function openNewTabMenu() {
|
|
3174
|
+
setPublishMenuOpen(false);
|
|
3175
|
+
setNativeCommandMenuOpen(false);
|
|
3176
|
+
setAppRunnerMenuOpen(false);
|
|
3177
|
+
setOptionsMenuOpen(false);
|
|
3178
|
+
setNewTabMenuOpen(true);
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
function focusNewTabMenuItem(direction = "first") {
|
|
3182
|
+
const items = [elements.newTabCurrentDirectoryButton, elements.newTabChooseDirectoryButton].filter(Boolean);
|
|
3183
|
+
const item = direction === "last" ? items.at(-1) : items[0];
|
|
3184
|
+
item?.focus({ preventScroll: true });
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
function moveNewTabMenuFocus(delta) {
|
|
3188
|
+
const items = [elements.newTabCurrentDirectoryButton, elements.newTabChooseDirectoryButton].filter(Boolean);
|
|
3189
|
+
if (!items.length) return;
|
|
3190
|
+
const currentIndex = Math.max(0, items.indexOf(document.activeElement));
|
|
3191
|
+
const nextIndex = (currentIndex + delta + items.length) % items.length;
|
|
3192
|
+
items[nextIndex].focus({ preventScroll: true });
|
|
3193
|
+
}
|
|
3194
|
+
|
|
2876
3195
|
function renderTabs() {
|
|
2877
3196
|
const active = activeTab();
|
|
2878
3197
|
const activeIndicator = active ? tabIndicator(active) : null;
|
|
@@ -2891,7 +3210,7 @@ function renderTabs() {
|
|
|
2891
3210
|
for (const tab of group.tabs) elements.tabBar.append(renderTerminalTab(tab));
|
|
2892
3211
|
}
|
|
2893
3212
|
}
|
|
2894
|
-
elements.tabBar.append(elements.
|
|
3213
|
+
elements.tabBar.append(elements.newTabMenu);
|
|
2895
3214
|
elements.closeAllTabsButton.disabled = tabs.length === 0;
|
|
2896
3215
|
updateTerminalTabGroupOpenState();
|
|
2897
3216
|
setMobileTabsExpanded(mobileTabsExpanded);
|
|
@@ -2933,12 +3252,22 @@ async function switchTab(tabId) {
|
|
|
2933
3252
|
if (isCurrentTabContext(tabContext)) markTabOutputSeen();
|
|
2934
3253
|
}
|
|
2935
3254
|
|
|
2936
|
-
|
|
3255
|
+
function currentDirectoryForNewTab() {
|
|
3256
|
+
return latestWorkspace?.cwd || activeTab()?.cwd || "";
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
async function createTerminalTab(cwd = currentDirectoryForNewTab(), { triggerButton = elements.newTabButton } = {}) {
|
|
2937
3260
|
setMobileTabsExpanded(false);
|
|
3261
|
+
setNewTabMenuOpen(false);
|
|
3262
|
+
const resolvedCwd = cwd || currentDirectoryForNewTab();
|
|
3263
|
+
if (!resolvedCwd && tabs.length === 0) {
|
|
3264
|
+
await createTerminalTabFromChosenDirectory({ triggerButton });
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
2938
3267
|
const disabledButtons = new Set([elements.newTabButton, triggerButton].filter(Boolean));
|
|
2939
3268
|
for (const button of disabledButtons) button.disabled = true;
|
|
2940
3269
|
try {
|
|
2941
|
-
const response = await api("/api/tabs", { method: "POST", body: { cwd:
|
|
3270
|
+
const response = await api("/api/tabs", { method: "POST", body: { cwd: resolvedCwd }, scoped: false });
|
|
2942
3271
|
tabs = response.data?.tabs || tabs;
|
|
2943
3272
|
syncTabMetadata(tabs);
|
|
2944
3273
|
const tab = response.data?.tab;
|
|
@@ -2954,6 +3283,27 @@ async function createTerminalTab(cwd = activeTab()?.cwd, { triggerButton = eleme
|
|
|
2954
3283
|
}
|
|
2955
3284
|
}
|
|
2956
3285
|
|
|
3286
|
+
async function createTerminalTabFromChosenDirectory({ triggerButton = elements.newTabChooseDirectoryButton } = {}) {
|
|
3287
|
+
const sourceTab = activeTab();
|
|
3288
|
+
const initialCwd = currentDirectoryForNewTab();
|
|
3289
|
+
setMobileTabsExpanded(false);
|
|
3290
|
+
setNewTabMenuOpen(false);
|
|
3291
|
+
const cwd = await pickCwd(sourceTab || { id: "new-tab", title: "new tab" }, initialCwd, { title: "Choose CWD for new tab" });
|
|
3292
|
+
if (!cwd) return;
|
|
3293
|
+
await createTerminalTab(cwd, { triggerButton });
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
async function createFirstTerminalTabFromChosenDirectory() {
|
|
3297
|
+
if (firstTerminalCwdPromptShown || tabs.length > 0) return;
|
|
3298
|
+
firstTerminalCwdPromptShown = true;
|
|
3299
|
+
const cwd = await pickCwd({ id: "first-terminal", title: "first terminal" }, "", { title: "Choose CWD for first terminal" });
|
|
3300
|
+
if (!cwd) {
|
|
3301
|
+
addEvent("choose a CWD to start the first terminal", "warn");
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
await createTerminalTab(cwd, { triggerButton: null });
|
|
3305
|
+
}
|
|
3306
|
+
|
|
2957
3307
|
function tabHasActiveAgent(tab) {
|
|
2958
3308
|
const activity = activityForTab(tab);
|
|
2959
3309
|
const indicator = tabIndicator(tab);
|
|
@@ -3003,6 +3353,7 @@ async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } =
|
|
|
3003
3353
|
tabDrafts.delete(id);
|
|
3004
3354
|
clearAttachments(id);
|
|
3005
3355
|
clearGitWorkflowForTab(id);
|
|
3356
|
+
appRunnerDataByTab.delete(id);
|
|
3006
3357
|
}
|
|
3007
3358
|
clearOpenTerminalTabGroup(null, { force: true });
|
|
3008
3359
|
|
|
@@ -3045,10 +3396,14 @@ async function closeAllTerminalTabs() {
|
|
|
3045
3396
|
}
|
|
3046
3397
|
|
|
3047
3398
|
async function initializeTabs() {
|
|
3048
|
-
await refreshTabs({ selectStored: true });
|
|
3399
|
+
const loadedTabs = await refreshTabs({ selectStored: true });
|
|
3049
3400
|
resetActiveTabUi();
|
|
3050
3401
|
renderTabs();
|
|
3051
3402
|
restoreActiveDraft();
|
|
3403
|
+
if (!loadedTabs.length) {
|
|
3404
|
+
await createFirstTerminalTabFromChosenDirectory();
|
|
3405
|
+
return;
|
|
3406
|
+
}
|
|
3052
3407
|
focusPromptInput({ defer: true });
|
|
3053
3408
|
const tabContext = activeTabContext();
|
|
3054
3409
|
connectEvents(tabContext);
|
|
@@ -3534,6 +3889,50 @@ function footerCostAuthLabel() {
|
|
|
3534
3889
|
return /codex|copilot|chatgpt/i.test(provider) ? "sub" : "api";
|
|
3535
3890
|
}
|
|
3536
3891
|
|
|
3892
|
+
function contextWindowFromSources(...sources) {
|
|
3893
|
+
for (const source of sources) {
|
|
3894
|
+
const value = typeof source === "object" && source !== null ? source.contextWindow : source;
|
|
3895
|
+
const contextWindow = Number(value);
|
|
3896
|
+
if (Number.isFinite(contextWindow) && contextWindow > 0) return contextWindow;
|
|
3897
|
+
}
|
|
3898
|
+
return 0;
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
function contextUsageUnknownAfterCompaction(tabId = activeTabId) {
|
|
3902
|
+
return !!tabId && contextUsageUnknownAfterCompactionByTab.has(tabId);
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
function unknownFooterContextText(contextUsage = null) {
|
|
3906
|
+
const contextWindow = contextWindowFromSources(
|
|
3907
|
+
contextUsage,
|
|
3908
|
+
latestStats?.contextUsage,
|
|
3909
|
+
currentState?.contextUsage,
|
|
3910
|
+
currentState?.model?.contextWindow,
|
|
3911
|
+
);
|
|
3912
|
+
return contextWindow ? `?/${formatFooterTokenCount(contextWindow)}` : "?";
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
function contextUsageWithUnknownPercent(contextUsage = null) {
|
|
3916
|
+
return {
|
|
3917
|
+
...(contextUsage || {}),
|
|
3918
|
+
percent: null,
|
|
3919
|
+
contextWindow: contextWindowFromSources(contextUsage, latestStats?.contextUsage, currentState?.contextUsage, currentState?.model?.contextWindow),
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
function markContextUsageUnknownAfterCompaction(tabId = activeTabId) {
|
|
3924
|
+
if (!tabId) return;
|
|
3925
|
+
contextUsageUnknownAfterCompactionByTab.set(tabId, Date.now());
|
|
3926
|
+
if (tabId !== activeTabId) return;
|
|
3927
|
+
if (currentState) currentState = { ...currentState, contextUsage: contextUsageWithUnknownPercent(currentState.contextUsage) };
|
|
3928
|
+
if (latestStats) latestStats = { ...latestStats, contextUsage: contextUsageWithUnknownPercent(latestStats.contextUsage) };
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
function clearContextUsageUnknownAfterCompaction(tabId = activeTabId) {
|
|
3932
|
+
if (!tabId) return;
|
|
3933
|
+
contextUsageUnknownAfterCompactionByTab.delete(tabId);
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3537
3936
|
function footerStatsTokensDisplay(stats = latestStats) {
|
|
3538
3937
|
const tokens = stats?.tokens;
|
|
3539
3938
|
if (!tokens) return "";
|
|
@@ -3554,7 +3953,8 @@ function footerContextDisplayWithAuto(value, state = currentState) {
|
|
|
3554
3953
|
|
|
3555
3954
|
function footerStatsContextDisplay(stats = latestStats) {
|
|
3556
3955
|
const usage = stats?.contextUsage || currentState?.contextUsage;
|
|
3557
|
-
const contextWindow = usage
|
|
3956
|
+
const contextWindow = contextWindowFromSources(usage, currentState?.model?.contextWindow);
|
|
3957
|
+
if (contextUsageUnknownAfterCompaction()) return footerContextDisplayWithAuto(unknownFooterContextText(usage));
|
|
3558
3958
|
if (!contextWindow) return "";
|
|
3559
3959
|
const rawPercent = Number(usage?.percent);
|
|
3560
3960
|
const percent = Number.isFinite(rawPercent) ? `${rawPercent.toFixed(1)}%` : "?";
|
|
@@ -3853,8 +4253,10 @@ function footerPayloadWithLiveModel(payload) {
|
|
|
3853
4253
|
const effort = footerThinkingDisplay();
|
|
3854
4254
|
const hasThinkingChip = [...payload.main, ...payload.meta].some((chip) => chip?.key === "thinking");
|
|
3855
4255
|
const contextChip = (chip) => {
|
|
3856
|
-
const
|
|
3857
|
-
|
|
4256
|
+
const usageUnknown = contextUsageUnknownAfterCompaction();
|
|
4257
|
+
const value = usageUnknown ? footerContextDisplayWithAuto(unknownFooterContextText(chip?.contextUsage)) : footerContextDisplayWithAuto(chip?.value);
|
|
4258
|
+
const contextUsage = usageUnknown ? contextUsageWithUnknownPercent(chip?.contextUsage) : chip?.contextUsage;
|
|
4259
|
+
return { ...chip, value, title: `context: ${value}`, ...(contextUsage ? { contextUsage } : {}) };
|
|
3858
4260
|
};
|
|
3859
4261
|
const effortChip = (chip) => ({ ...chip, key: "thinking", label: "effort", value: effort, title: `effort: ${effort}`, tone: "mauve" });
|
|
3860
4262
|
const splitChip = (chip) => {
|
|
@@ -4469,12 +4871,13 @@ function closePathPicker(cwd) {
|
|
|
4469
4871
|
state.resolve(cwd || null);
|
|
4470
4872
|
}
|
|
4471
4873
|
|
|
4472
|
-
function pickCwd(tab, initialCwd) {
|
|
4874
|
+
function pickCwd(tab, initialCwd, { title } = {}) {
|
|
4473
4875
|
if (pathPickerState) return Promise.resolve(null);
|
|
4474
4876
|
|
|
4475
4877
|
return new Promise((resolve) => {
|
|
4476
|
-
|
|
4477
|
-
|
|
4878
|
+
const pickerTab = tab || { id: "path-picker", title: "tab" };
|
|
4879
|
+
pathPickerState = { tabId: pickerTab.id, cwd: initialCwd, requestId: 0, loading: false, creatingDirectory: false, directories: [], filteredDirectories: [], resolve };
|
|
4880
|
+
elements.pathPickerTitle.textContent = title || `Choose CWD for ${pickerTab.title}`;
|
|
4478
4881
|
elements.pathPickerCurrent.textContent = "Loading…";
|
|
4479
4882
|
elements.pathPickerCreateNameInput.value = "";
|
|
4480
4883
|
elements.pathPickerSearchInput.value = "";
|
|
@@ -5206,6 +5609,530 @@ function renderReleaseAurLogWidget() {
|
|
|
5206
5609
|
return node;
|
|
5207
5610
|
}
|
|
5208
5611
|
|
|
5612
|
+
function activeAppRunnerData() {
|
|
5613
|
+
return activeTabId ? appRunnerDataByTab.get(activeTabId) || { runners: [], activeRun: null } : { runners: [], activeRun: null };
|
|
5614
|
+
}
|
|
5615
|
+
|
|
5616
|
+
function setAppRunnerData(tabId, data = {}) {
|
|
5617
|
+
if (!tabId) return;
|
|
5618
|
+
const previous = appRunnerDataByTab.get(tabId) || { runners: [], activeRun: null, customRunnerConfig: null };
|
|
5619
|
+
appRunnerDataByTab.set(tabId, {
|
|
5620
|
+
cwd: data.cwd || previous.cwd || "",
|
|
5621
|
+
runners: Array.isArray(data.runners) ? data.runners : previous.runners || [],
|
|
5622
|
+
customRunnerConfig: data.customRunnerConfig || previous.customRunnerConfig || null,
|
|
5623
|
+
activeRun: Object.prototype.hasOwnProperty.call(data, "activeRun") ? data.activeRun : previous.activeRun || null,
|
|
5624
|
+
});
|
|
5625
|
+
}
|
|
5626
|
+
|
|
5627
|
+
function appRunnerIsRunning(run) {
|
|
5628
|
+
return run?.status === "running" || run?.stopping === true;
|
|
5629
|
+
}
|
|
5630
|
+
|
|
5631
|
+
function appRunnerStatusLabel(run) {
|
|
5632
|
+
if (run?.stopping && run.status === "running") return "stopping";
|
|
5633
|
+
if (run?.status === "done") return "exit 0";
|
|
5634
|
+
if (run?.status === "failed") return run.signal ? `signal ${run.signal}` : `exit ${run.exitCode ?? "?"}`;
|
|
5635
|
+
if (run?.status === "error") return "error";
|
|
5636
|
+
return run?.status || "running";
|
|
5637
|
+
}
|
|
5638
|
+
|
|
5639
|
+
function appRunnerElapsedLabel(run) {
|
|
5640
|
+
const startedAt = Date.parse(run?.startedAt || "");
|
|
5641
|
+
if (!Number.isFinite(startedAt)) return "";
|
|
5642
|
+
const endedAt = Date.parse(run?.endedAt || "");
|
|
5643
|
+
const end = Number.isFinite(endedAt) ? endedAt : Date.now();
|
|
5644
|
+
return formatDuration(end - startedAt);
|
|
5645
|
+
}
|
|
5646
|
+
|
|
5647
|
+
function appRunnerActionButton(label, handler, className = "") {
|
|
5648
|
+
const button = make("button", `release-npm-action ${className}`.trim(), label);
|
|
5649
|
+
button.type = "button";
|
|
5650
|
+
button.addEventListener("click", handler);
|
|
5651
|
+
return button;
|
|
5652
|
+
}
|
|
5653
|
+
|
|
5654
|
+
async function refreshAppRunners(tabContext = activeTabContext()) {
|
|
5655
|
+
if (!tabContext.tabId) return;
|
|
5656
|
+
const response = await api("/api/app-runners", { tabId: tabContext.tabId });
|
|
5657
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5658
|
+
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
5659
|
+
renderAppRunnerControls();
|
|
5660
|
+
renderWidgets();
|
|
5661
|
+
}
|
|
5662
|
+
|
|
5663
|
+
async function runAppRunner(runnerId) {
|
|
5664
|
+
const tabContext = activeTabContext();
|
|
5665
|
+
if (!tabContext.tabId || !runnerId) return;
|
|
5666
|
+
setComposerActionsOpen(false);
|
|
5667
|
+
setAppRunnerMenuOpen(false);
|
|
5668
|
+
try {
|
|
5669
|
+
const response = await api("/api/app-runner", { method: "POST", body: { runnerId }, tabId: tabContext.tabId });
|
|
5670
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5671
|
+
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
5672
|
+
renderAppRunnerControls();
|
|
5673
|
+
renderWidgets();
|
|
5674
|
+
const command = response.data?.activeRun?.displayCommand || "app runner";
|
|
5675
|
+
addEvent(`started ${command}`, "info");
|
|
5676
|
+
} catch (error) {
|
|
5677
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
5678
|
+
}
|
|
5679
|
+
}
|
|
5680
|
+
|
|
5681
|
+
async function stopAppRunner() {
|
|
5682
|
+
const tabContext = activeTabContext();
|
|
5683
|
+
if (!tabContext.tabId) return;
|
|
5684
|
+
try {
|
|
5685
|
+
const response = await api("/api/app-runner/stop", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
5686
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5687
|
+
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
5688
|
+
renderAppRunnerControls();
|
|
5689
|
+
renderWidgets();
|
|
5690
|
+
addEvent("app runner stop requested", "warn");
|
|
5691
|
+
} catch (error) {
|
|
5692
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
|
|
5696
|
+
async function clearAppRunner() {
|
|
5697
|
+
const tabContext = activeTabContext();
|
|
5698
|
+
if (!tabContext.tabId) return;
|
|
5699
|
+
try {
|
|
5700
|
+
const response = await api("/api/app-runner/clear", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
5701
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5702
|
+
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
5703
|
+
renderAppRunnerControls();
|
|
5704
|
+
renderWidgets();
|
|
5705
|
+
} catch (error) {
|
|
5706
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
|
|
5710
|
+
function appRunnerOutputText(run) {
|
|
5711
|
+
const lines = Array.isArray(run?.lines) ? run.lines : [];
|
|
5712
|
+
return lines.join("\n").trimEnd();
|
|
5713
|
+
}
|
|
5714
|
+
|
|
5715
|
+
async function copyAppRunnerOutput(run) {
|
|
5716
|
+
const text = appRunnerOutputText(run);
|
|
5717
|
+
if (!text.trim()) {
|
|
5718
|
+
addEvent("app runner output is empty", "warn");
|
|
5719
|
+
return;
|
|
5720
|
+
}
|
|
5721
|
+
try {
|
|
5722
|
+
await copyText(text);
|
|
5723
|
+
addEvent("copied app runner output", "info");
|
|
5724
|
+
} catch (error) {
|
|
5725
|
+
addEvent(`app runner output copy failed: ${error.message || String(error)}`, "warn");
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
|
|
5729
|
+
const APP_RUNNER_SUPPORTED_ITEMS = [
|
|
5730
|
+
"Project-local custom runners from .pi-webui-runners.json",
|
|
5731
|
+
"package.json scripts: bun/npm/pnpm/yarn dev, start, serve",
|
|
5732
|
+
"npx frameworks: Vite, Next, Astro, Storybook",
|
|
5733
|
+
"Rust: cargo run",
|
|
5734
|
+
"Python: uv run or python entry files such as Main.py, main.py, src/main.py",
|
|
5735
|
+
"Go/Golang: go run",
|
|
5736
|
+
"Zig: zig build run or zig run",
|
|
5737
|
+
"C/C++: CMake, cc/c++ main files",
|
|
5738
|
+
"Docker Compose: docker compose up",
|
|
5739
|
+
"Shell scripts: bash/zsh/fish in root, dev/, scripts/, dev/scripts/",
|
|
5740
|
+
"Deno, make, just, and plain Node entry files",
|
|
5741
|
+
];
|
|
5742
|
+
const APP_RUNNER_SUPPORTED_TOOLTIP = [
|
|
5743
|
+
"No app runner detected for this tab cwd.",
|
|
5744
|
+
"",
|
|
5745
|
+
"Currently supported:",
|
|
5746
|
+
...APP_RUNNER_SUPPORTED_ITEMS.map((item) => `• ${item}`),
|
|
5747
|
+
].join("\n");
|
|
5748
|
+
|
|
5749
|
+
function appRunnerMenuCanOpen() {
|
|
5750
|
+
const data = activeAppRunnerData();
|
|
5751
|
+
return Array.isArray(data.runners) && data.runners.length > 0 && !appRunnerIsRunning(data.activeRun);
|
|
5752
|
+
}
|
|
5753
|
+
|
|
5754
|
+
function activeAppRunnerCustomConfig() {
|
|
5755
|
+
return activeAppRunnerData().customRunnerConfig || { runners: [], projectRoot: "", displayProjectRoot: "", displayConfigFile: "" };
|
|
5756
|
+
}
|
|
5757
|
+
|
|
5758
|
+
function resetAppRunnerCustomDraft() {
|
|
5759
|
+
appRunnerCustomDraft = { id: "", label: "", command: "./", path: "", args: "" };
|
|
5760
|
+
appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
5761
|
+
}
|
|
5762
|
+
|
|
5763
|
+
function appRunnerRelativeDir(filePath) {
|
|
5764
|
+
const normalized = String(filePath || "").replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
5765
|
+
const index = normalized.lastIndexOf("/");
|
|
5766
|
+
return index === -1 ? "" : normalized.slice(0, index);
|
|
5767
|
+
}
|
|
5768
|
+
|
|
5769
|
+
function appRunnerCustomArgsText(args) {
|
|
5770
|
+
return Array.isArray(args) ? args.join(" ") : String(args || "");
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5773
|
+
function appRunnerCustomDraftPayload() {
|
|
5774
|
+
return {
|
|
5775
|
+
id: appRunnerCustomDraft.id || undefined,
|
|
5776
|
+
label: appRunnerCustomDraft.label.trim(),
|
|
5777
|
+
command: appRunnerCustomDraft.command.trim() || "./",
|
|
5778
|
+
path: appRunnerCustomDraft.path.trim(),
|
|
5779
|
+
args: appRunnerCustomDraft.args.trim(),
|
|
5780
|
+
};
|
|
5781
|
+
}
|
|
5782
|
+
|
|
5783
|
+
function updateAppRunnerCustomDraftFrom(container) {
|
|
5784
|
+
if (!container) return;
|
|
5785
|
+
appRunnerCustomDraft = {
|
|
5786
|
+
id: appRunnerCustomDraft.id || "",
|
|
5787
|
+
label: container.querySelector("#appRunnerCustomLabelInput")?.value || "",
|
|
5788
|
+
command: container.querySelector("#appRunnerCustomCommandInput")?.value || "./",
|
|
5789
|
+
path: container.querySelector("#appRunnerCustomPathInput")?.value || "",
|
|
5790
|
+
args: container.querySelector("#appRunnerCustomArgsInput")?.value || "",
|
|
5791
|
+
};
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5794
|
+
function appRunnerInputField({ id, label, value, placeholder = "", hint = "" }) {
|
|
5795
|
+
const field = make("label", "app-runner-custom-field");
|
|
5796
|
+
field.setAttribute("for", id);
|
|
5797
|
+
field.append(make("span", "", label));
|
|
5798
|
+
const input = make("input", "dialog-input");
|
|
5799
|
+
input.id = id;
|
|
5800
|
+
input.type = "text";
|
|
5801
|
+
input.value = value || "";
|
|
5802
|
+
input.placeholder = placeholder;
|
|
5803
|
+
input.autocomplete = "off";
|
|
5804
|
+
input.spellcheck = false;
|
|
5805
|
+
field.append(input);
|
|
5806
|
+
if (hint) field.append(make("small", "muted", hint));
|
|
5807
|
+
input.addEventListener("input", () => updateAppRunnerCustomDraftFrom(field.closest(".app-runner-custom-form")));
|
|
5808
|
+
input.addEventListener("keydown", (event) => {
|
|
5809
|
+
if (event.key !== "Enter") return;
|
|
5810
|
+
event.preventDefault();
|
|
5811
|
+
saveAppRunnerCustomRunner(field.closest(".app-runner-custom-form"));
|
|
5812
|
+
});
|
|
5813
|
+
return { field, input };
|
|
5814
|
+
}
|
|
5815
|
+
|
|
5816
|
+
async function saveAppRunnerCustomRunner(form) {
|
|
5817
|
+
updateAppRunnerCustomDraftFrom(form);
|
|
5818
|
+
const payload = appRunnerCustomDraftPayload();
|
|
5819
|
+
if (!payload.path) {
|
|
5820
|
+
addEvent("custom app runner path is required", "warn");
|
|
5821
|
+
form?.querySelector("#appRunnerCustomPathInput")?.focus();
|
|
5822
|
+
return;
|
|
5823
|
+
}
|
|
5824
|
+
const tabContext = activeTabContext();
|
|
5825
|
+
try {
|
|
5826
|
+
const response = await api("/api/app-runner-config", { method: "POST", body: { runner: payload }, tabId: tabContext.tabId });
|
|
5827
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5828
|
+
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
5829
|
+
resetAppRunnerCustomDraft();
|
|
5830
|
+
renderAppRunnerControls();
|
|
5831
|
+
renderWidgets();
|
|
5832
|
+
renderAppRunnerInfoDialog();
|
|
5833
|
+
addEvent("saved custom app runner", "info");
|
|
5834
|
+
} catch (error) {
|
|
5835
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5839
|
+
async function deleteAppRunnerCustomRunner(id) {
|
|
5840
|
+
const tabContext = activeTabContext();
|
|
5841
|
+
try {
|
|
5842
|
+
const response = await api("/api/app-runner-config", { method: "DELETE", body: { id }, tabId: tabContext.tabId });
|
|
5843
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5844
|
+
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
5845
|
+
if (appRunnerCustomDraft.id === id) resetAppRunnerCustomDraft();
|
|
5846
|
+
renderAppRunnerControls();
|
|
5847
|
+
renderWidgets();
|
|
5848
|
+
renderAppRunnerInfoDialog();
|
|
5849
|
+
addEvent("deleted custom app runner", "warn");
|
|
5850
|
+
} catch (error) {
|
|
5851
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
5852
|
+
}
|
|
5853
|
+
}
|
|
5854
|
+
|
|
5855
|
+
async function loadAppRunnerFileBrowser(relativePath = "") {
|
|
5856
|
+
const tabContext = activeTabContext();
|
|
5857
|
+
const path = String(relativePath || "").replace(/^\.\/+/, "").replace(/\/+$/g, "");
|
|
5858
|
+
appRunnerFileBrowserState = { open: true, loading: true, path, data: null, error: "" };
|
|
5859
|
+
renderAppRunnerInfoDialog();
|
|
5860
|
+
try {
|
|
5861
|
+
const response = await api(`/api/app-runner-files?path=${encodeURIComponent(path)}`, { tabId: tabContext.tabId });
|
|
5862
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5863
|
+
appRunnerFileBrowserState = { open: true, loading: false, path, data: response.data || {}, error: "" };
|
|
5864
|
+
renderAppRunnerInfoDialog();
|
|
5865
|
+
} catch (error) {
|
|
5866
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5867
|
+
appRunnerFileBrowserState = { open: true, loading: false, path, data: null, error: error.message || String(error) };
|
|
5868
|
+
renderAppRunnerInfoDialog();
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
|
|
5872
|
+
function renderAppRunnerFileBrowser() {
|
|
5873
|
+
if (!appRunnerFileBrowserState.open) return null;
|
|
5874
|
+
const browser = make("div", "app-runner-file-browser");
|
|
5875
|
+
if (appRunnerFileBrowserState.loading) {
|
|
5876
|
+
browser.append(make("div", "muted", "Loading project files…"));
|
|
5877
|
+
return browser;
|
|
5878
|
+
}
|
|
5879
|
+
if (appRunnerFileBrowserState.error) {
|
|
5880
|
+
browser.append(make("div", "path-picker-error", appRunnerFileBrowserState.error));
|
|
5881
|
+
return browser;
|
|
5882
|
+
}
|
|
5883
|
+
const data = appRunnerFileBrowserState.data || {};
|
|
5884
|
+
const header = make("div", "app-runner-file-browser-header");
|
|
5885
|
+
header.append(make("strong", "", data.displayRelativeDir || "."));
|
|
5886
|
+
const close = make("button", "app-runner-file-browser-close", "Hide browser");
|
|
5887
|
+
close.type = "button";
|
|
5888
|
+
close.addEventListener("click", () => {
|
|
5889
|
+
appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
5890
|
+
renderAppRunnerInfoDialog();
|
|
5891
|
+
});
|
|
5892
|
+
header.append(close);
|
|
5893
|
+
browser.append(header);
|
|
5894
|
+
|
|
5895
|
+
const roots = make("div", "path-picker-roots app-runner-file-browser-roots");
|
|
5896
|
+
if (data.parent !== null && data.parent !== undefined) roots.append(pathPickerButton("↑ Parent", data.parent || ".", () => loadAppRunnerFileBrowser(data.parent || ""), "path-picker-root-button"));
|
|
5897
|
+
roots.append(pathPickerButton("Project root", data.displayProjectRoot || "Project root", () => loadAppRunnerFileBrowser(""), "path-picker-root-button"));
|
|
5898
|
+
browser.append(roots);
|
|
5899
|
+
|
|
5900
|
+
const list = make("div", "path-picker-list app-runner-file-browser-list");
|
|
5901
|
+
const directories = Array.isArray(data.directories) ? data.directories : [];
|
|
5902
|
+
const files = Array.isArray(data.files) ? data.files : [];
|
|
5903
|
+
for (const directory of directories) {
|
|
5904
|
+
const button = pathPickerButton(`${directory.name}/`, directory.path, () => loadAppRunnerFileBrowser(directory.path), `path-picker-directory${directory.hidden ? " hidden-directory" : ""}`);
|
|
5905
|
+
list.append(button);
|
|
5906
|
+
}
|
|
5907
|
+
for (const file of files) {
|
|
5908
|
+
const button = pathPickerButton(file.name, file.path, () => {
|
|
5909
|
+
appRunnerCustomDraft.path = file.path;
|
|
5910
|
+
appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
5911
|
+
renderAppRunnerInfoDialog();
|
|
5912
|
+
}, `path-picker-directory app-runner-file-choice${file.hidden ? " hidden-directory" : ""}`);
|
|
5913
|
+
list.append(button);
|
|
5914
|
+
}
|
|
5915
|
+
if (!directories.length && !files.length) list.append(make("div", "path-picker-empty muted", "No files in this directory."));
|
|
5916
|
+
browser.append(list);
|
|
5917
|
+
if (data.truncated) browser.append(make("div", "path-picker-error", "Showing the first project entries only."));
|
|
5918
|
+
return browser;
|
|
5919
|
+
}
|
|
5920
|
+
|
|
5921
|
+
function renderAppRunnerCustomSection() {
|
|
5922
|
+
const config = activeAppRunnerCustomConfig();
|
|
5923
|
+
const section = make("section", "app-runner-info-section app-runner-custom-section");
|
|
5924
|
+
const titleRow = make("div", "app-runner-section-title-row");
|
|
5925
|
+
titleRow.append(make("h3", "", "Custom project runners"));
|
|
5926
|
+
if (config.displayConfigFile) titleRow.append(make("code", "", config.displayConfigFile));
|
|
5927
|
+
section.append(titleRow);
|
|
5928
|
+
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."));
|
|
5929
|
+
|
|
5930
|
+
const existing = make("div", "app-runner-custom-list");
|
|
5931
|
+
const customRunners = Array.isArray(config.runners) ? config.runners : [];
|
|
5932
|
+
if (!customRunners.length) {
|
|
5933
|
+
existing.append(make("div", "app-runner-custom-empty muted", "No custom runners saved for this project yet."));
|
|
5934
|
+
} else {
|
|
5935
|
+
for (const runner of customRunners) {
|
|
5936
|
+
const row = make("div", "app-runner-custom-item");
|
|
5937
|
+
const details = make("div", "app-runner-custom-item-details");
|
|
5938
|
+
details.append(make("strong", "", runner.label || runner.path || "custom runner"), make("code", "", runner.displayCommand || runner.path || ""));
|
|
5939
|
+
const actions = make("div", "app-runner-custom-item-actions");
|
|
5940
|
+
const edit = make("button", "", "Edit");
|
|
5941
|
+
edit.type = "button";
|
|
5942
|
+
edit.addEventListener("click", () => {
|
|
5943
|
+
appRunnerCustomDraft = { id: runner.id || "", label: runner.label || "", command: runner.command || "./", path: runner.path || "", args: appRunnerCustomArgsText(runner.args) };
|
|
5944
|
+
appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
5945
|
+
renderAppRunnerInfoDialog();
|
|
5946
|
+
});
|
|
5947
|
+
const remove = make("button", "danger", "Delete");
|
|
5948
|
+
remove.type = "button";
|
|
5949
|
+
remove.addEventListener("click", () => {
|
|
5950
|
+
if (!confirm(`Delete custom app runner “${runner.label || runner.path || runner.id}”?`)) return;
|
|
5951
|
+
deleteAppRunnerCustomRunner(runner.id);
|
|
5952
|
+
});
|
|
5953
|
+
actions.append(edit, remove);
|
|
5954
|
+
row.append(details, actions);
|
|
5955
|
+
existing.append(row);
|
|
5956
|
+
}
|
|
5957
|
+
}
|
|
5958
|
+
section.append(existing);
|
|
5959
|
+
|
|
5960
|
+
const form = make("div", "app-runner-custom-form");
|
|
5961
|
+
const labelField = appRunnerInputField({ id: "appRunnerCustomLabelInput", label: "Label", value: appRunnerCustomDraft.label, placeholder: "My app" });
|
|
5962
|
+
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." });
|
|
5963
|
+
const pathField = appRunnerInputField({ id: "appRunnerCustomPathInput", label: "Path to file", value: appRunnerCustomDraft.path, placeholder: "dev/scripts/start.sh" });
|
|
5964
|
+
const pathRow = make("div", "app-runner-custom-path-row");
|
|
5965
|
+
pathRow.append(pathField.field);
|
|
5966
|
+
const browse = make("button", "app-runner-custom-browse", "Browse…");
|
|
5967
|
+
browse.type = "button";
|
|
5968
|
+
browse.addEventListener("click", () => {
|
|
5969
|
+
updateAppRunnerCustomDraftFrom(form);
|
|
5970
|
+
loadAppRunnerFileBrowser(appRunnerRelativeDir(appRunnerCustomDraft.path));
|
|
5971
|
+
});
|
|
5972
|
+
pathRow.append(browse);
|
|
5973
|
+
const argsField = appRunnerInputField({ id: "appRunnerCustomArgsInput", label: "Args", value: appRunnerCustomDraft.args, placeholder: "--port 3000", hint: "Optional extra args, space-separated." });
|
|
5974
|
+
form.append(labelField.field, commandField.field, pathRow, argsField.field);
|
|
5975
|
+
const formActions = make("div", "app-runner-custom-form-actions");
|
|
5976
|
+
const save = make("button", "primary", appRunnerCustomDraft.id ? "Save changes" : "Add runner");
|
|
5977
|
+
save.type = "button";
|
|
5978
|
+
save.addEventListener("click", () => saveAppRunnerCustomRunner(form));
|
|
5979
|
+
const reset = make("button", "", "Reset");
|
|
5980
|
+
reset.type = "button";
|
|
5981
|
+
reset.addEventListener("click", () => { resetAppRunnerCustomDraft(); renderAppRunnerInfoDialog(); });
|
|
5982
|
+
formActions.append(save, reset);
|
|
5983
|
+
form.append(formActions);
|
|
5984
|
+
const browser = renderAppRunnerFileBrowser();
|
|
5985
|
+
if (browser) form.append(browser);
|
|
5986
|
+
section.append(form);
|
|
5987
|
+
return section;
|
|
5988
|
+
}
|
|
5989
|
+
|
|
5990
|
+
function renderAppRunnerControls() {
|
|
5991
|
+
const menu = elements.appRunnerMenu;
|
|
5992
|
+
const button = elements.appRunnerMenuButton;
|
|
5993
|
+
const panel = elements.appRunnerMenuPanel;
|
|
5994
|
+
if (!menu || !button || !panel) return;
|
|
5995
|
+
const data = activeAppRunnerData();
|
|
5996
|
+
const runners = Array.isArray(data.runners) ? data.runners : [];
|
|
5997
|
+
const activeRun = data.activeRun;
|
|
5998
|
+
const running = appRunnerIsRunning(activeRun);
|
|
5999
|
+
menu.hidden = false;
|
|
6000
|
+
menu.classList.toggle("has-runners", runners.length > 0);
|
|
6001
|
+
if (elements.appRunnerInfoButton) {
|
|
6002
|
+
elements.appRunnerInfoButton.hidden = runners.length === 0;
|
|
6003
|
+
elements.appRunnerInfoButton.disabled = runners.length === 0;
|
|
6004
|
+
}
|
|
6005
|
+
button.disabled = running;
|
|
6006
|
+
button.title = running
|
|
6007
|
+
? `App runner already running: ${activeRun.displayCommand || activeRun.label || "runner"}`
|
|
6008
|
+
: runners.length
|
|
6009
|
+
? "Run a detected app runner"
|
|
6010
|
+
: "No app runners detected in this tab working directory";
|
|
6011
|
+
button.dataset.tooltip = runners.length ? "App runners: run detected project commands in this tab's working directory." : APP_RUNNER_SUPPORTED_TOOLTIP;
|
|
6012
|
+
button.setAttribute("aria-label", button.title);
|
|
6013
|
+
if (!runners.length || running) setAppRunnerMenuOpen(false);
|
|
6014
|
+
|
|
6015
|
+
panel.replaceChildren();
|
|
6016
|
+
for (const runner of runners) {
|
|
6017
|
+
const item = make("button", "composer-publish-menu-item composer-app-runner-menu-item");
|
|
6018
|
+
item.type = "button";
|
|
6019
|
+
item.setAttribute("role", "menuitem");
|
|
6020
|
+
const runnerDisplayCommand = runner.shortDisplayCommand || runner.displayCommand;
|
|
6021
|
+
item.title = runner.description ? `${runnerDisplayCommand}\n${runner.description}` : runnerDisplayCommand;
|
|
6022
|
+
item.addEventListener("click", () => runAppRunner(runner.id));
|
|
6023
|
+
const label = make("span", "app-runner-menu-item-label", runner.label || runnerDisplayCommand);
|
|
6024
|
+
const command = make("span", "app-runner-menu-item-command", runnerDisplayCommand);
|
|
6025
|
+
item.append(label, command);
|
|
6026
|
+
panel.append(item);
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
|
|
6030
|
+
function renderAppRunnerInfoDialog() {
|
|
6031
|
+
const body = elements.appRunnerInfoBody;
|
|
6032
|
+
if (!body) return;
|
|
6033
|
+
const data = activeAppRunnerData();
|
|
6034
|
+
const runners = Array.isArray(data.runners) ? data.runners : [];
|
|
6035
|
+
body.replaceChildren();
|
|
6036
|
+
|
|
6037
|
+
const current = make("section", "app-runner-info-section");
|
|
6038
|
+
current.append(make("h3", "", "Detected in this tab"));
|
|
6039
|
+
if (runners.length) {
|
|
6040
|
+
const list = make("ul", "app-runner-info-list app-runner-info-detected-list");
|
|
6041
|
+
for (const runner of runners) {
|
|
6042
|
+
const command = runner.shortDisplayCommand || runner.displayCommand || runner.command || runner.id;
|
|
6043
|
+
const item = make("li");
|
|
6044
|
+
item.append(
|
|
6045
|
+
make("strong", "", runner.label || command || "runner"),
|
|
6046
|
+
make("code", "", command || "detected command"),
|
|
6047
|
+
);
|
|
6048
|
+
if (runner.description) item.append(make("span", "", runner.description));
|
|
6049
|
+
list.append(item);
|
|
6050
|
+
}
|
|
6051
|
+
current.append(list);
|
|
6052
|
+
} else {
|
|
6053
|
+
current.append(make("p", "muted", "No runners are currently detected for this tab working directory."));
|
|
6054
|
+
}
|
|
6055
|
+
|
|
6056
|
+
const how = make("section", "app-runner-info-section");
|
|
6057
|
+
how.append(make("h3", "", "How it works"));
|
|
6058
|
+
const howList = make("ul", "app-runner-info-list");
|
|
6059
|
+
for (const line of [
|
|
6060
|
+
"Detection is scoped to the active terminal tab's current working directory.",
|
|
6061
|
+
"Only commands/files that exist and runner binaries available on this system are shown.",
|
|
6062
|
+
"Starting a runner keeps live output pinned above the chat/terminal area.",
|
|
6063
|
+
"Only one app runner can be active per tab; Close/Stop terminates the process/server.",
|
|
6064
|
+
]) howList.append(make("li", "", line));
|
|
6065
|
+
how.append(howList);
|
|
6066
|
+
|
|
6067
|
+
const supported = make("section", "app-runner-info-section");
|
|
6068
|
+
supported.append(make("h3", "", "Supported runner types"));
|
|
6069
|
+
const supportedList = make("ul", "app-runner-info-list app-runner-info-supported-list");
|
|
6070
|
+
for (const itemText of APP_RUNNER_SUPPORTED_ITEMS) supportedList.append(make("li", "", itemText));
|
|
6071
|
+
supported.append(supportedList);
|
|
6072
|
+
|
|
6073
|
+
body.append(current, renderAppRunnerCustomSection(), how, supported);
|
|
6074
|
+
}
|
|
6075
|
+
|
|
6076
|
+
function openAppRunnerInfoDialog() {
|
|
6077
|
+
if (!elements.appRunnerInfoDialog) return;
|
|
6078
|
+
renderAppRunnerInfoDialog();
|
|
6079
|
+
setAppRunnerMenuOpen(false);
|
|
6080
|
+
if (!elements.appRunnerInfoDialog.open) elements.appRunnerInfoDialog.showModal();
|
|
6081
|
+
}
|
|
6082
|
+
|
|
6083
|
+
function closeAppRunnerInfoDialog() {
|
|
6084
|
+
if (elements.appRunnerInfoDialog?.open) elements.appRunnerInfoDialog.close();
|
|
6085
|
+
}
|
|
6086
|
+
|
|
6087
|
+
function renderAppRunnerWidget() {
|
|
6088
|
+
const data = activeAppRunnerData();
|
|
6089
|
+
const run = data.activeRun;
|
|
6090
|
+
if (!run) return null;
|
|
6091
|
+
const running = appRunnerIsRunning(run);
|
|
6092
|
+
const status = appRunnerStatusLabel(run);
|
|
6093
|
+
const node = make("section", `widget release-npm-widget app-runner-widget${running ? " app-runner-live-widget" : " app-runner-log-widget"}`);
|
|
6094
|
+
node.setAttribute("aria-label", "app runner output");
|
|
6095
|
+
|
|
6096
|
+
const header = make("div", "release-npm-header");
|
|
6097
|
+
const titleWrap = make("div", "release-npm-title-wrap");
|
|
6098
|
+
titleWrap.append(make("span", "release-npm-kicker", "app runner"), make("strong", "release-npm-title", run.label || run.displayCommand || "app runner"));
|
|
6099
|
+
|
|
6100
|
+
const elapsed = appRunnerElapsedLabel(run);
|
|
6101
|
+
header.append(titleWrap);
|
|
6102
|
+
|
|
6103
|
+
const lines = Array.isArray(run.lines) && run.lines.length ? run.lines : [run.displayCommand ? `$ ${run.displayCommand}` : "Waiting for app runner output..."];
|
|
6104
|
+
const streamHeader = releaseNpmStreamHeader(running ? "Live app output" : "App output", run.lineCount || lines.length, { live: running });
|
|
6105
|
+
const terminal = make("div", "release-npm-terminal");
|
|
6106
|
+
terminal.setAttribute("role", "log");
|
|
6107
|
+
terminal.setAttribute("aria-live", running ? "polite" : "off");
|
|
6108
|
+
for (const line of lines) appendReleaseNpmTerminalLine(terminal, line);
|
|
6109
|
+
|
|
6110
|
+
const controlParts = [run.displayCommand, run.cwd, run.truncated ? "output truncated" : ""].map(cleanStatusText).filter(Boolean);
|
|
6111
|
+
const controls = make("div", "release-npm-controls app-runner-output-controls");
|
|
6112
|
+
const actions = make("div", "app-runner-output-actions");
|
|
6113
|
+
const closeButton = appRunnerActionButton("Close", running ? stopAppRunner : clearAppRunner, running ? "danger app-runner-close-action" : "app-runner-close-action");
|
|
6114
|
+
closeButton.title = running ? "Stop this app runner/process/server" : "Close app runner output";
|
|
6115
|
+
const copyButton = appRunnerActionButton("Copy output", () => copyAppRunnerOutput(run), "app-runner-copy-action");
|
|
6116
|
+
copyButton.title = "Copy app runner output";
|
|
6117
|
+
actions.append(closeButton, copyButton);
|
|
6118
|
+
if (running) {
|
|
6119
|
+
actions.append(appRunnerActionButton("Stop", stopAppRunner, "danger"));
|
|
6120
|
+
} else {
|
|
6121
|
+
const canRunAgain = (data.runners || []).some((runner) => runner.id === run.runnerId);
|
|
6122
|
+
if (canRunAgain) actions.append(appRunnerActionButton("Run again", () => runAppRunner(run.runnerId)));
|
|
6123
|
+
actions.append(appRunnerActionButton("Clear", clearAppRunner));
|
|
6124
|
+
}
|
|
6125
|
+
const pills = make("div", "app-runner-output-pills");
|
|
6126
|
+
if (run.kind) pills.append(make("span", "release-npm-pill", run.kind));
|
|
6127
|
+
pills.append(make("span", `release-npm-pill app-runner-status ${run.status || "running"}`.trim(), status));
|
|
6128
|
+
if (elapsed) pills.append(make("span", "release-npm-pill elapsed", elapsed));
|
|
6129
|
+
controls.append(actions, pills, make("span", "app-runner-output-meta", controlParts.join(" · ")));
|
|
6130
|
+
const outputDetails = renderReleaseNpmOutputDetails(`app-runner:${run.id || run.runnerId || "active"}`, streamHeader, terminal, controls);
|
|
6131
|
+
node.append(header, outputDetails);
|
|
6132
|
+
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
6133
|
+
return node;
|
|
6134
|
+
}
|
|
6135
|
+
|
|
5209
6136
|
function renderWidgets() {
|
|
5210
6137
|
elements.widgetArea.replaceChildren();
|
|
5211
6138
|
const releaseOutput = renderReleaseNpmOutputWidget();
|
|
@@ -5216,6 +6143,8 @@ function renderWidgets() {
|
|
|
5216
6143
|
if (releaseAurOutput) elements.widgetArea.append(releaseAurOutput);
|
|
5217
6144
|
const releaseAurLog = renderReleaseAurLogWidget();
|
|
5218
6145
|
if (releaseAurLog) elements.widgetArea.append(releaseAurLog);
|
|
6146
|
+
const appRunnerWidget = renderAppRunnerWidget();
|
|
6147
|
+
if (appRunnerWidget) elements.widgetArea.append(appRunnerWidget);
|
|
5219
6148
|
|
|
5220
6149
|
for (const [key, value] of widgets) {
|
|
5221
6150
|
const widgetFeatureId = optionalFeatureWidgetFeatureId(key);
|
|
@@ -5239,6 +6168,8 @@ function setGitWorkflow(patch, { tabId = activeTabId } = {}) {
|
|
|
5239
6168
|
const workflow = gitWorkflowForTab(tabId);
|
|
5240
6169
|
if (!workflow) return null;
|
|
5241
6170
|
Object.assign(workflow, patch);
|
|
6171
|
+
workflow.actionsDone = createGitWorkflowActionsDone(workflow.actionsDone);
|
|
6172
|
+
if (patch.step && !("process" in patch)) workflow.process = gitWorkflowProcessForStep(workflow.step, workflow.process);
|
|
5242
6173
|
if (tabId === activeTabId) {
|
|
5243
6174
|
gitWorkflow = workflow;
|
|
5244
6175
|
renderGitWorkflow();
|
|
@@ -5274,24 +6205,141 @@ function formatCommitMessagePreview(message) {
|
|
|
5274
6205
|
return [`=== SHORT ===`, message.short || "(empty)", "", "=== LONG ===", message.long || "(empty)"].join("\n");
|
|
5275
6206
|
}
|
|
5276
6207
|
|
|
5277
|
-
function
|
|
6208
|
+
function gitWorkflowMessageTitle(message) {
|
|
6209
|
+
return String(message?.short || message?.long || "").split("\n").find((line) => line.trim())?.trim() || "Pull request";
|
|
6210
|
+
}
|
|
6211
|
+
|
|
6212
|
+
function slugifyGitBranchPart(value) {
|
|
6213
|
+
return String(value || "")
|
|
6214
|
+
.normalize("NFKD")
|
|
6215
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
6216
|
+
.toLowerCase()
|
|
6217
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
6218
|
+
.replace(/^-+|-+$/g, "")
|
|
6219
|
+
.slice(0, 48);
|
|
6220
|
+
}
|
|
6221
|
+
|
|
6222
|
+
function defaultGitPrBranchName(message = gitWorkflow.message) {
|
|
6223
|
+
const title = gitWorkflowMessageTitle(message);
|
|
6224
|
+
const match = title.match(/^([a-z][a-z0-9-]*)(?:\([^)]*\))?:\s*(.+)$/i);
|
|
6225
|
+
const type = slugifyGitBranchPart(match?.[1] || "feat") || "feat";
|
|
6226
|
+
const summary = slugifyGitBranchPart(match?.[2] || title) || "feature";
|
|
6227
|
+
return `${type}/${summary}`;
|
|
6228
|
+
}
|
|
6229
|
+
|
|
6230
|
+
function formatGitPrPreview(pr) {
|
|
6231
|
+
if (!pr) return "No PR description loaded yet.";
|
|
6232
|
+
const header = [`=== PR DESCRIPTION ===`, `Branch: ${pr.branch || gitWorkflow.prBranch || "current branch"}`];
|
|
6233
|
+
if (pr.path) header.push(`File: ${pr.path}`);
|
|
6234
|
+
return [...header, "", pr.body || "(empty)"].join("\n");
|
|
6235
|
+
}
|
|
6236
|
+
|
|
6237
|
+
function addGitWorkflowAction(label, handler, className = "", disabled = gitWorkflow.busy, tooltip = "") {
|
|
5278
6238
|
const button = make("button", className, label);
|
|
5279
6239
|
button.type = "button";
|
|
5280
6240
|
button.disabled = disabled;
|
|
6241
|
+
if (tooltip) {
|
|
6242
|
+
button.title = tooltip;
|
|
6243
|
+
button.dataset.tooltip = tooltip;
|
|
6244
|
+
button.setAttribute("aria-label", `${label}. ${tooltip.replace(/\s+/g, " ")}`);
|
|
6245
|
+
}
|
|
5281
6246
|
button.addEventListener("click", handler);
|
|
5282
6247
|
elements.gitWorkflowActions.append(button);
|
|
5283
6248
|
return button;
|
|
5284
6249
|
}
|
|
5285
6250
|
|
|
6251
|
+
function setGitPrDialogStatus(message = "", level = "muted") {
|
|
6252
|
+
if (!elements.gitPrStatus) return;
|
|
6253
|
+
elements.gitPrStatus.textContent = message;
|
|
6254
|
+
elements.gitPrStatus.className = `git-pr-status ${level || "muted"}`;
|
|
6255
|
+
}
|
|
6256
|
+
|
|
6257
|
+
function resolveGitPrDialog(value) {
|
|
6258
|
+
const resolve = activeGitPrDialogResolve;
|
|
6259
|
+
activeGitPrDialogResolve = null;
|
|
6260
|
+
if (elements.gitPrDialog?.open) elements.gitPrDialog.close();
|
|
6261
|
+
if (resolve) resolve(value);
|
|
6262
|
+
}
|
|
6263
|
+
|
|
6264
|
+
function openGitPrReviewDialog(pr, { title = "" } = {}) {
|
|
6265
|
+
if (!elements.gitPrDialog || !elements.gitPrTitleInput || !elements.gitPrBodyEditor) return Promise.resolve(null);
|
|
6266
|
+
if (activeGitPrDialogResolve) resolveGitPrDialog(null);
|
|
6267
|
+
elements.gitPrTitleInput.value = title || gitWorkflowMessageTitle(gitWorkflow.message);
|
|
6268
|
+
elements.gitPrBodyEditor.value = pr?.body || "";
|
|
6269
|
+
setGitPrDialogStatus(`Review ${pr?.path || "the generated PR description"}. Edit if needed, then create the pull request.`);
|
|
6270
|
+
return new Promise((resolve) => {
|
|
6271
|
+
activeGitPrDialogResolve = resolve;
|
|
6272
|
+
elements.gitPrDialog.showModal();
|
|
6273
|
+
queueMicrotask(() => elements.gitPrBodyEditor.focus());
|
|
6274
|
+
});
|
|
6275
|
+
}
|
|
6276
|
+
|
|
6277
|
+
function gitWorkflowProcessForStep(step = gitWorkflow.step, fallback = gitWorkflow.process || "stage") {
|
|
6278
|
+
switch (step) {
|
|
6279
|
+
case "generate":
|
|
6280
|
+
case "generating":
|
|
6281
|
+
return "message";
|
|
6282
|
+
case "message":
|
|
6283
|
+
case "branchNaming":
|
|
6284
|
+
case "branching":
|
|
6285
|
+
case "committing":
|
|
6286
|
+
return "commit";
|
|
6287
|
+
case "push":
|
|
6288
|
+
case "pushing":
|
|
6289
|
+
case "prGenerating":
|
|
6290
|
+
case "prReview":
|
|
6291
|
+
case "prCreating":
|
|
6292
|
+
case "done":
|
|
6293
|
+
return "push";
|
|
6294
|
+
case "add":
|
|
6295
|
+
case "idle":
|
|
6296
|
+
return "stage";
|
|
6297
|
+
case "cancelled":
|
|
6298
|
+
case "error":
|
|
6299
|
+
return GIT_WORKFLOW_PROCESS_VALUES.has(fallback) ? fallback : "stage";
|
|
6300
|
+
default:
|
|
6301
|
+
return "stage";
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6304
|
+
|
|
6305
|
+
function selectGitWorkflowProcess(processValue, tabId = gitWorkflowActionTabId()) {
|
|
6306
|
+
const workflow = gitWorkflowForTab(tabId);
|
|
6307
|
+
if (!workflow) return;
|
|
6308
|
+
const process = GIT_WORKFLOW_PROCESS_VALUES.has(processValue) ? processValue : "stage";
|
|
6309
|
+
workflow.runId += 1;
|
|
6310
|
+
const runId = workflow.runId;
|
|
6311
|
+
const base = { active: true, process, busy: false, error: "", messageRequestedAt: 0, branchName: "", branchNameRequestedAt: 0, prMode: false, prBranch: "", pr: null, prRequestedAt: 0 };
|
|
6312
|
+
|
|
6313
|
+
if (process === "stage") {
|
|
6314
|
+
setGitWorkflow({ ...base, step: "add", message: null, output: "Ready to stage all changes with git add ." }, { tabId });
|
|
6315
|
+
return;
|
|
6316
|
+
}
|
|
6317
|
+
if (process === "message") {
|
|
6318
|
+
setGitWorkflow({ ...base, step: "generate", message: null, output: "Ready to generate a commit message from the currently staged changes." }, { tabId });
|
|
6319
|
+
return;
|
|
6320
|
+
}
|
|
6321
|
+
if (process === "commit") {
|
|
6322
|
+
setGitWorkflow({ ...base, step: "message", message: null, output: "Loading current generated commit message files…" }, { tabId });
|
|
6323
|
+
loadGitWorkflowMessage({ requireFresh: false, runId, tabId });
|
|
6324
|
+
return;
|
|
6325
|
+
}
|
|
6326
|
+
setGitWorkflow({ ...base, step: "push", output: "Ready to run git push for the current branch." }, { tabId });
|
|
6327
|
+
}
|
|
6328
|
+
|
|
5286
6329
|
function gitWorkflowTitle() {
|
|
5287
6330
|
switch (gitWorkflow.step) {
|
|
5288
6331
|
case "add": return "Stage all changes";
|
|
5289
6332
|
case "generate": return "Generate staged commit message";
|
|
5290
6333
|
case "generating": return "Waiting for /git-staged-msg";
|
|
5291
|
-
case "message": return "Choose commit message";
|
|
6334
|
+
case "message": return gitWorkflow.prMode ? "Choose PR branch commit message" : "Choose commit message";
|
|
6335
|
+
case "branchNaming": return "Waiting for branch name";
|
|
6336
|
+
case "branching": return "Creating PR branch";
|
|
5292
6337
|
case "committing": return "Committing";
|
|
5293
|
-
case "push": return "Push commit";
|
|
6338
|
+
case "push": return gitWorkflow.prMode ? "Push branch and create PR" : "Push commit";
|
|
5294
6339
|
case "pushing": return "Pushing";
|
|
6340
|
+
case "prGenerating": return "Waiting for /pr";
|
|
6341
|
+
case "prReview": return "Review PR description";
|
|
6342
|
+
case "prCreating": return "Creating pull request";
|
|
5295
6343
|
case "done": return "Git workflow complete";
|
|
5296
6344
|
case "cancelled": return "Git workflow cancelled";
|
|
5297
6345
|
case "error": return "Git workflow needs attention";
|
|
@@ -5304,11 +6352,16 @@ function gitWorkflowHint() {
|
|
|
5304
6352
|
case "add": return "Step 1: run git add . in the current Pi working directory.";
|
|
5305
6353
|
case "generate": return "Step 2: run /git-staged-msg, then preview the generated files.";
|
|
5306
6354
|
case "generating": return "Pi is generating dev/COMMIT/staged-commit-short.txt and staged-commit-long.txt.";
|
|
5307
|
-
case "message": return "Step 3/4: preview the native g-msg output
|
|
6355
|
+
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.";
|
|
6356
|
+
case "branchNaming": return "Pi is generating dev/COMMIT/staged-branch-name.txt. Cancel will request Pi abort.";
|
|
6357
|
+
case "branching": return "Creating a new branch with git switch -c before committing.";
|
|
5308
6358
|
case "committing": return "Running native git commit from the generated message file.";
|
|
5309
|
-
case "push": return "Step 5: push the new commit to the configured remote.";
|
|
6359
|
+
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.";
|
|
5310
6360
|
case "pushing": return "Running git push. Cancel will request process termination.";
|
|
5311
|
-
case "
|
|
6361
|
+
case "prGenerating": return "Pi is generating dev/PR/<current-branch>.md with /pr.";
|
|
6362
|
+
case "prReview": return "Review or edit the generated PR description before creating the pull request.";
|
|
6363
|
+
case "prCreating": return "Running gh pr create with the confirmed description.";
|
|
6364
|
+
case "done": return gitWorkflow.prMode ? "PR workflow finished. Review the output below." : "Push finished. Review the output below.";
|
|
5312
6365
|
case "cancelled": return "No further workflow steps will run.";
|
|
5313
6366
|
case "error": return gitWorkflow.error || "Fix the issue, then retry or restart.";
|
|
5314
6367
|
default: return "Stage changes, generate a commit message, commit, and push.";
|
|
@@ -5326,9 +6379,14 @@ function renderGitWorkflow() {
|
|
|
5326
6379
|
elements.gitWorkflowActions.replaceChildren();
|
|
5327
6380
|
|
|
5328
6381
|
const activeIndex = GIT_WORKFLOW_ACTIVE_INDEX[gitWorkflow.step] ?? 0;
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
6382
|
+
const activeProcess = gitWorkflowProcessForStep(gitWorkflow.step, gitWorkflow.process);
|
|
6383
|
+
for (const [index, process] of GIT_WORKFLOW_PROCESSES.entries()) {
|
|
6384
|
+
const item = make("button", "git-workflow-step", process.label);
|
|
6385
|
+
item.type = "button";
|
|
6386
|
+
item.dataset.gitWorkflowProcess = process.value;
|
|
6387
|
+
item.disabled = !!gitWorkflow.busy;
|
|
6388
|
+
item.setAttribute("aria-pressed", String(process.value === activeProcess));
|
|
6389
|
+
if (gitWorkflowActionDone(gitWorkflow, process.value)) item.classList.add("done");
|
|
5332
6390
|
if (index === activeIndex && !["done", "cancelled", "error"].includes(gitWorkflow.step)) item.classList.add("active");
|
|
5333
6391
|
elements.gitWorkflowSteps.append(item);
|
|
5334
6392
|
}
|
|
@@ -5344,11 +6402,28 @@ function renderGitWorkflow() {
|
|
|
5344
6402
|
} else if (gitWorkflow.step === "generating") {
|
|
5345
6403
|
addGitWorkflowAction("Refresh message preview", () => loadGitWorkflowMessage({ requireFresh: true }), "", false);
|
|
5346
6404
|
} else if (gitWorkflow.step === "message") {
|
|
5347
|
-
|
|
5348
|
-
|
|
6405
|
+
if (!gitWorkflow.prMode) {
|
|
6406
|
+
addGitWorkflowAction("Create PR", () => createGitPrBranch(), "primary", false, GIT_WORKFLOW_CREATE_PR_TOOLTIP);
|
|
6407
|
+
addGitWorkflowAction("Manual branch", () => createGitPrBranchManually(), "", false, GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP);
|
|
6408
|
+
}
|
|
6409
|
+
addGitWorkflowAction("Commit short", () => commitGitWorkflow("short"), gitWorkflow.prMode ? "primary" : "", false);
|
|
6410
|
+
addGitWorkflowAction("Commit long", () => commitGitWorkflow("long"), gitWorkflow.prMode ? "primary" : "", false);
|
|
5349
6411
|
addGitWorkflowAction("Regenerate", () => runGitMessagePrompt(), "", false);
|
|
6412
|
+
} else if (gitWorkflow.step === "branchNaming") {
|
|
6413
|
+
addGitWorkflowAction("Refresh branch name", () => loadGitWorkflowBranchName({ requireFresh: true }), "", false);
|
|
6414
|
+
addGitWorkflowAction("Manual branch", () => createGitPrBranchManually(), "", !!gitWorkflow.busy, GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP);
|
|
6415
|
+
} else if (gitWorkflow.step === "branching") {
|
|
6416
|
+
addGitWorkflowAction("Creating branch…", () => {}, "primary", true);
|
|
5350
6417
|
} else if (gitWorkflow.step === "push") {
|
|
5351
|
-
addGitWorkflowAction("
|
|
6418
|
+
if (gitWorkflow.prMode) addGitWorkflowAction("Push and Create PR", () => pushAndCreatePrGitWorkflow(), "primary", false);
|
|
6419
|
+
else addGitWorkflowAction("Run git push", () => pushGitWorkflow(), "primary", false);
|
|
6420
|
+
} else if (gitWorkflow.step === "prGenerating") {
|
|
6421
|
+
addGitWorkflowAction("Refresh PR description", () => loadGitWorkflowPr({ requireFresh: true }), "", false);
|
|
6422
|
+
} else if (gitWorkflow.step === "prReview") {
|
|
6423
|
+
addGitWorkflowAction("Create PR", () => createGitPrFromReview(), "primary", false);
|
|
6424
|
+
addGitWorkflowAction("Regenerate /pr", () => runGitPrPrompt(), "", false);
|
|
6425
|
+
} else if (gitWorkflow.step === "prCreating") {
|
|
6426
|
+
addGitWorkflowAction("Creating PR…", () => {}, "primary", true);
|
|
5352
6427
|
} else if (gitWorkflow.step === "done") {
|
|
5353
6428
|
addGitWorkflowAction("Close", () => setGitWorkflow({ active: false }), "primary", false);
|
|
5354
6429
|
addGitWorkflowAction("Start another", () => startGitWorkflow(), "", false);
|
|
@@ -5398,11 +6473,19 @@ function startGitWorkflow(tabId = activeTabId) {
|
|
|
5398
6473
|
setGitWorkflow({
|
|
5399
6474
|
active: true,
|
|
5400
6475
|
step: "add",
|
|
6476
|
+
process: "stage",
|
|
5401
6477
|
busy: false,
|
|
5402
|
-
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.",
|
|
6478
|
+
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.",
|
|
5403
6479
|
error: "",
|
|
5404
6480
|
message: null,
|
|
5405
6481
|
messageRequestedAt: 0,
|
|
6482
|
+
branchName: "",
|
|
6483
|
+
branchNameRequestedAt: 0,
|
|
6484
|
+
actionsDone: createGitWorkflowActionsDone(),
|
|
6485
|
+
prMode: false,
|
|
6486
|
+
prBranch: "",
|
|
6487
|
+
pr: null,
|
|
6488
|
+
prRequestedAt: 0,
|
|
5406
6489
|
}, { tabId });
|
|
5407
6490
|
}
|
|
5408
6491
|
|
|
@@ -5410,7 +6493,8 @@ async function cancelGitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
|
5410
6493
|
const tabContext = activeTabContext(tabId);
|
|
5411
6494
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
5412
6495
|
if (!workflow?.active) return;
|
|
5413
|
-
const shouldAbortPi = workflow.step === "generating";
|
|
6496
|
+
const shouldAbortPi = workflow.step === "generating" || workflow.step === "branchNaming" || workflow.step === "prGenerating";
|
|
6497
|
+
if (activeGitPrDialogResolve) resolveGitPrDialog(null);
|
|
5414
6498
|
workflow.runId += 1;
|
|
5415
6499
|
setGitWorkflow({ step: "cancelled", busy: false, error: "", output: `${workflow.output || ""}${workflow.output ? "\n\n" : ""}Cancelled by user.` }, { tabId });
|
|
5416
6500
|
if (shouldAbortPi && isCurrentTabContext(tabContext)) setRunIndicatorActivity("Abort requested; checking whether Pi stopped…");
|
|
@@ -5430,7 +6514,7 @@ async function runGitAdd(tabId = gitWorkflowActionTabId()) {
|
|
|
5430
6514
|
try {
|
|
5431
6515
|
const result = await gitWorkflowRequest("/api/git-workflow/add", { runId, tabId });
|
|
5432
6516
|
if (!result) return;
|
|
5433
|
-
setGitWorkflow({ step: "generate", busy: false, output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` }, { tabId });
|
|
6517
|
+
setGitWorkflow({ step: "generate", busy: false, ...gitWorkflowActionDonePatch(workflow, "stage"), output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` }, { tabId });
|
|
5434
6518
|
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
5435
6519
|
} catch (error) {
|
|
5436
6520
|
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "add", { tabId });
|
|
@@ -5494,6 +6578,7 @@ async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId
|
|
|
5494
6578
|
busy: false,
|
|
5495
6579
|
error: "",
|
|
5496
6580
|
message,
|
|
6581
|
+
...(requireFresh && currentWorkflow.messageRequestedAt ? gitWorkflowActionDonePatch(currentWorkflow, "message") : {}),
|
|
5497
6582
|
output: formatCommitMessagePreview(message),
|
|
5498
6583
|
}, { tabId });
|
|
5499
6584
|
} catch (error) {
|
|
@@ -5507,6 +6592,130 @@ async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId
|
|
|
5507
6592
|
}
|
|
5508
6593
|
}
|
|
5509
6594
|
|
|
6595
|
+
function gitBranchNamePromptMessage() {
|
|
6596
|
+
if (hasAvailableCommand("git-branch-name")) return "/git-branch-name";
|
|
6597
|
+
return [
|
|
6598
|
+
"Generate one PR branch name for the current staged work.",
|
|
6599
|
+
"Inspect only staged changes (`git diff --cached`) and the generated commit message files if present:",
|
|
6600
|
+
"- dev/COMMIT/staged-commit-short.txt",
|
|
6601
|
+
"- dev/COMMIT/staged-commit-long.txt",
|
|
6602
|
+
"",
|
|
6603
|
+
"Write exactly one line to dev/COMMIT/staged-branch-name.txt in this format:",
|
|
6604
|
+
"<type>/<short-feature-name>",
|
|
6605
|
+
"",
|
|
6606
|
+
"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.",
|
|
6607
|
+
].join("\n");
|
|
6608
|
+
}
|
|
6609
|
+
|
|
6610
|
+
async function createGitPrBranch(tabId = gitWorkflowActionTabId()) {
|
|
6611
|
+
await runGitBranchNamePrompt(tabId);
|
|
6612
|
+
}
|
|
6613
|
+
|
|
6614
|
+
async function createGitPrBranchManually(tabId = gitWorkflowActionTabId()) {
|
|
6615
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6616
|
+
if (!workflow) return;
|
|
6617
|
+
await createGitPrBranchWithSuggestion(workflow.branchName || defaultGitPrBranchName(workflow.message), tabId);
|
|
6618
|
+
}
|
|
6619
|
+
|
|
6620
|
+
async function runGitBranchNamePrompt(tabId = gitWorkflowActionTabId()) {
|
|
6621
|
+
const tabContext = activeTabContext(tabId);
|
|
6622
|
+
const targetTab = tabs.find((tab) => tab.id === tabId);
|
|
6623
|
+
const targetBusy = tabId === activeTabId ? !!currentState?.isStreaming : activityForTab(targetTab).isWorking;
|
|
6624
|
+
if (targetBusy) {
|
|
6625
|
+
failGitWorkflow(new Error("Pi is currently running. Wait for it to finish or abort before generating a branch name."), "message", { tabId });
|
|
6626
|
+
return;
|
|
6627
|
+
}
|
|
6628
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6629
|
+
if (!workflow) return;
|
|
6630
|
+
const runId = workflow.runId;
|
|
6631
|
+
const requestedAt = Date.now();
|
|
6632
|
+
setGitWorkflow({
|
|
6633
|
+
step: "branchNaming",
|
|
6634
|
+
busy: true,
|
|
6635
|
+
error: "",
|
|
6636
|
+
branchNameRequestedAt: requestedAt,
|
|
6637
|
+
output: "Sending branch-name request to Pi.\n\nCancel will request Pi abort.",
|
|
6638
|
+
}, { tabId });
|
|
6639
|
+
if (isCurrentTabContext(tabContext)) setRunIndicatorActivity("Sending branch-name request to Pi…");
|
|
6640
|
+
try {
|
|
6641
|
+
await api("/api/prompt", { method: "POST", body: { message: gitBranchNamePromptMessage() }, tabId });
|
|
6642
|
+
if (!isCurrentGitWorkflowRun(runId, tabId)) return;
|
|
6643
|
+
appendGitWorkflowOutput("Branch-name request accepted. Waiting for agent_end, then the branch name will be loaded.", { tabId });
|
|
6644
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshState(120, tabContext);
|
|
6645
|
+
setTimeout(() => {
|
|
6646
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
6647
|
+
const targetStillBusy = tabId === activeTabId && currentState?.isStreaming;
|
|
6648
|
+
if (isCurrentGitWorkflowRun(runId, tabId) && currentWorkflow?.step === "branchNaming" && !targetStillBusy) {
|
|
6649
|
+
loadGitWorkflowBranchName({ requireFresh: true, retries: 1, runId, tabId });
|
|
6650
|
+
}
|
|
6651
|
+
}, 2500);
|
|
6652
|
+
} catch (error) {
|
|
6653
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) {
|
|
6654
|
+
if (isCurrentTabContext(tabContext)) clearRunIndicatorActivity();
|
|
6655
|
+
failGitWorkflow(error, "message", { tabId });
|
|
6656
|
+
}
|
|
6657
|
+
}
|
|
6658
|
+
}
|
|
6659
|
+
|
|
6660
|
+
async function loadGitWorkflowBranchName({ requireFresh = false, retries = 0, runId, tabId = activeTabId } = {}) {
|
|
6661
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6662
|
+
const expectedRunId = runId ?? workflow?.runId;
|
|
6663
|
+
try {
|
|
6664
|
+
const branchName = await gitWorkflowRequest("/api/git-workflow/branch-name", { method: "GET", runId: expectedRunId, tabId });
|
|
6665
|
+
if (!branchName) return;
|
|
6666
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
6667
|
+
if (!currentWorkflow) return;
|
|
6668
|
+
if (requireFresh && currentWorkflow.branchNameRequestedAt && (branchName.mtimeMs || 0) + 10000 < currentWorkflow.branchNameRequestedAt) {
|
|
6669
|
+
throw new Error("Generated branch name has not refreshed yet.");
|
|
6670
|
+
}
|
|
6671
|
+
const branch = branchName.branch || defaultGitPrBranchName(currentWorkflow.message);
|
|
6672
|
+
setGitWorkflow({
|
|
6673
|
+
step: "message",
|
|
6674
|
+
busy: false,
|
|
6675
|
+
error: "",
|
|
6676
|
+
branchName: branch,
|
|
6677
|
+
output: `${formatCommitMessagePreview(currentWorkflow.message)}\n\nGenerated branch name: ${branch}`,
|
|
6678
|
+
}, { tabId });
|
|
6679
|
+
await createGitPrBranchWithSuggestion(branch, tabId, expectedRunId);
|
|
6680
|
+
} catch (error) {
|
|
6681
|
+
if (!isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
6682
|
+
if (retries > 0) {
|
|
6683
|
+
setTimeout(() => loadGitWorkflowBranchName({ requireFresh, retries: retries - 1, runId: expectedRunId, tabId }), 1400);
|
|
6684
|
+
return;
|
|
6685
|
+
}
|
|
6686
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
6687
|
+
failGitWorkflow(error, currentWorkflow?.step === "branchNaming" ? "message" : currentWorkflow?.step, { tabId });
|
|
6688
|
+
}
|
|
6689
|
+
}
|
|
6690
|
+
|
|
6691
|
+
async function createGitPrBranchWithSuggestion(suggestion, tabId = gitWorkflowActionTabId(), expectedRunId) {
|
|
6692
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6693
|
+
if (!workflow) return;
|
|
6694
|
+
const proposedBranch = prompt("New PR branch name (example: type/feature-name)", suggestion || defaultGitPrBranchName(workflow.message));
|
|
6695
|
+
if (expectedRunId !== undefined && !isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
6696
|
+
if (proposedBranch === null) {
|
|
6697
|
+
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 });
|
|
6698
|
+
return;
|
|
6699
|
+
}
|
|
6700
|
+
const branch = proposedBranch.trim();
|
|
6701
|
+
if (!branch) {
|
|
6702
|
+
failGitWorkflow(new Error("Branch name is required to create a PR branch."), "message", { tabId });
|
|
6703
|
+
return;
|
|
6704
|
+
}
|
|
6705
|
+
const runId = workflow.runId;
|
|
6706
|
+
setGitWorkflow({ step: "branching", prMode: true, prBranch: branch, branchName: branch, busy: true, error: "", output: `${formatCommitMessagePreview(workflow.message)}\n\nRunning git switch -c ${branch}…` }, { tabId });
|
|
6707
|
+
try {
|
|
6708
|
+
const result = await gitWorkflowRequest("/api/git-workflow/branch", { body: { branch }, runId, tabId });
|
|
6709
|
+
if (!result) return;
|
|
6710
|
+
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 });
|
|
6711
|
+
} catch (error) {
|
|
6712
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) {
|
|
6713
|
+
setGitWorkflow({ prMode: false, prBranch: "" }, { tabId });
|
|
6714
|
+
failGitWorkflow(error, "message", { tabId });
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6718
|
+
|
|
5510
6719
|
async function commitGitWorkflow(variant, tabId = gitWorkflowActionTabId()) {
|
|
5511
6720
|
const tabContext = activeTabContext(tabId);
|
|
5512
6721
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
@@ -5516,7 +6725,8 @@ async function commitGitWorkflow(variant, tabId = gitWorkflowActionTabId()) {
|
|
|
5516
6725
|
try {
|
|
5517
6726
|
const result = await gitWorkflowRequest("/api/git-workflow/commit", { body: { variant }, runId, tabId });
|
|
5518
6727
|
if (!result) return;
|
|
5519
|
-
|
|
6728
|
+
const nextAction = workflow.prMode ? "Push and Create PR." : "git push.";
|
|
6729
|
+
setGitWorkflow({ step: "push", busy: false, ...gitWorkflowActionDonePatch(workflow, "commit"), output: `${formatGitCommandResult(result)}\n\nCommit created. Next: ${nextAction}` }, { tabId });
|
|
5520
6730
|
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
5521
6731
|
} catch (error) {
|
|
5522
6732
|
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "message", { tabId });
|
|
@@ -5532,13 +6742,129 @@ async function pushGitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
|
5532
6742
|
try {
|
|
5533
6743
|
const result = await gitWorkflowRequest("/api/git-workflow/push", { runId, tabId });
|
|
5534
6744
|
if (!result) return;
|
|
5535
|
-
setGitWorkflow({ step: "done", busy: false, output: formatGitCommandResult(result) || "git push finished." }, { tabId });
|
|
6745
|
+
setGitWorkflow({ step: "done", busy: false, ...gitWorkflowActionDonePatch(workflow, "push"), output: formatGitCommandResult(result) || "git push finished." }, { tabId });
|
|
6746
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
6747
|
+
} catch (error) {
|
|
6748
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "push", { tabId });
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
|
|
6752
|
+
async function runGitPrPrompt(tabId = gitWorkflowActionTabId(), { prefixOutput = "" } = {}) {
|
|
6753
|
+
const tabContext = activeTabContext(tabId);
|
|
6754
|
+
const targetTab = tabs.find((tab) => tab.id === tabId);
|
|
6755
|
+
const targetBusy = tabId === activeTabId ? !!currentState?.isStreaming : activityForTab(targetTab).isWorking;
|
|
6756
|
+
if (targetBusy) {
|
|
6757
|
+
failGitWorkflow(new Error("Pi is currently running. Wait for it to finish or abort before generating a PR description."), "push", { tabId });
|
|
6758
|
+
return;
|
|
6759
|
+
}
|
|
6760
|
+
if (!hasAvailableCommand("pr")) {
|
|
6761
|
+
failGitWorkflow(new Error(commandUnavailableMessage("pr")), "push", { tabId });
|
|
6762
|
+
return;
|
|
6763
|
+
}
|
|
6764
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6765
|
+
if (!workflow) return;
|
|
6766
|
+
const runId = workflow.runId;
|
|
6767
|
+
const requestedAt = Date.now();
|
|
6768
|
+
setGitWorkflow({
|
|
6769
|
+
step: "prGenerating",
|
|
6770
|
+
busy: true,
|
|
6771
|
+
error: "",
|
|
6772
|
+
prRequestedAt: requestedAt,
|
|
6773
|
+
output: `${prefixOutput ? `${prefixOutput}\n\n` : ""}Sending /pr to Pi.\n\nCancel will request Pi abort.`,
|
|
6774
|
+
}, { tabId });
|
|
6775
|
+
if (isCurrentTabContext(tabContext)) setRunIndicatorActivity("Sending /pr to Pi…");
|
|
6776
|
+
try {
|
|
6777
|
+
await api("/api/prompt", { method: "POST", body: { message: "/pr" }, tabId });
|
|
6778
|
+
if (!isCurrentGitWorkflowRun(runId, tabId)) return;
|
|
6779
|
+
appendGitWorkflowOutput("/pr accepted. Waiting for agent_end, then the PR description will be loaded.", { tabId });
|
|
6780
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshState(120, tabContext);
|
|
6781
|
+
setTimeout(() => {
|
|
6782
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
6783
|
+
const targetStillBusy = tabId === activeTabId && currentState?.isStreaming;
|
|
6784
|
+
if (isCurrentGitWorkflowRun(runId, tabId) && currentWorkflow?.step === "prGenerating" && !targetStillBusy) {
|
|
6785
|
+
loadGitWorkflowPr({ requireFresh: true, retries: 1, runId, tabId });
|
|
6786
|
+
}
|
|
6787
|
+
}, 2500);
|
|
6788
|
+
} catch (error) {
|
|
6789
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) {
|
|
6790
|
+
if (isCurrentTabContext(tabContext)) clearRunIndicatorActivity();
|
|
6791
|
+
failGitWorkflow(error, "push", { tabId });
|
|
6792
|
+
}
|
|
6793
|
+
}
|
|
6794
|
+
}
|
|
6795
|
+
|
|
6796
|
+
async function loadGitWorkflowPr({ requireFresh = false, retries = 0, runId, tabId = activeTabId } = {}) {
|
|
6797
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6798
|
+
const expectedRunId = runId ?? workflow?.runId;
|
|
6799
|
+
try {
|
|
6800
|
+
const pr = await gitWorkflowRequest("/api/git-workflow/pr-description", { method: "GET", runId: expectedRunId, tabId });
|
|
6801
|
+
if (!pr) return;
|
|
6802
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
6803
|
+
if (!currentWorkflow) return;
|
|
6804
|
+
if (requireFresh && currentWorkflow.prRequestedAt && (pr.mtimeMs || 0) + 10000 < currentWorkflow.prRequestedAt) {
|
|
6805
|
+
throw new Error("Generated PR description has not refreshed yet.");
|
|
6806
|
+
}
|
|
6807
|
+
setGitWorkflow({
|
|
6808
|
+
step: "prReview",
|
|
6809
|
+
busy: false,
|
|
6810
|
+
error: "",
|
|
6811
|
+
pr,
|
|
6812
|
+
prBranch: pr.branch || currentWorkflow.prBranch,
|
|
6813
|
+
output: formatGitPrPreview(pr),
|
|
6814
|
+
}, { tabId });
|
|
6815
|
+
} catch (error) {
|
|
6816
|
+
if (!isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
6817
|
+
if (retries > 0) {
|
|
6818
|
+
setTimeout(() => loadGitWorkflowPr({ requireFresh, retries: retries - 1, runId: expectedRunId, tabId }), 1400);
|
|
6819
|
+
return;
|
|
6820
|
+
}
|
|
6821
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
6822
|
+
failGitWorkflow(error, currentWorkflow?.step === "prGenerating" ? "push" : currentWorkflow?.step, { tabId });
|
|
6823
|
+
}
|
|
6824
|
+
}
|
|
6825
|
+
|
|
6826
|
+
async function pushAndCreatePrGitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
6827
|
+
const tabContext = activeTabContext(tabId);
|
|
6828
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6829
|
+
if (!workflow) return;
|
|
6830
|
+
const runId = workflow.runId;
|
|
6831
|
+
const branch = workflow.prBranch || "current branch";
|
|
6832
|
+
setGitWorkflow({ step: "pushing", busy: true, error: "", output: `Pushing PR branch ${branch}…` }, { tabId });
|
|
6833
|
+
try {
|
|
6834
|
+
const result = await gitWorkflowRequest("/api/git-workflow/push", { body: { setUpstream: true, branch: workflow.prBranch }, runId, tabId });
|
|
6835
|
+
if (!result) return;
|
|
6836
|
+
setGitWorkflow({ ...gitWorkflowActionDonePatch(workflow, "push") }, { tabId });
|
|
5536
6837
|
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
6838
|
+
await runGitPrPrompt(tabId, { prefixOutput: `${formatGitCommandResult(result)}\n\nPushed PR branch ${result.branch || branch}.` });
|
|
5537
6839
|
} catch (error) {
|
|
5538
6840
|
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "push", { tabId });
|
|
5539
6841
|
}
|
|
5540
6842
|
}
|
|
5541
6843
|
|
|
6844
|
+
async function createGitPrFromReview(tabId = gitWorkflowActionTabId()) {
|
|
6845
|
+
const tabContext = activeTabContext(tabId);
|
|
6846
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
6847
|
+
if (!workflow?.pr) return;
|
|
6848
|
+
const runId = workflow.runId;
|
|
6849
|
+
const review = await openGitPrReviewDialog(workflow.pr, { title: gitWorkflowMessageTitle(workflow.message) });
|
|
6850
|
+
if (!isCurrentGitWorkflowRun(runId, tabId)) return;
|
|
6851
|
+
if (!review) {
|
|
6852
|
+
setGitWorkflow({ step: "prReview", busy: false, output: `${formatGitPrPreview(workflow.pr)}\n\nPR creation cancelled. Edit the description, regenerate /pr, or press Create PR again.` }, { tabId });
|
|
6853
|
+
return;
|
|
6854
|
+
}
|
|
6855
|
+
const title = review.title.trim();
|
|
6856
|
+
const body = review.body.trimEnd();
|
|
6857
|
+
setGitWorkflow({ step: "prCreating", busy: true, error: "", output: `${formatGitPrPreview({ ...workflow.pr, body })}\n\nCreating pull request with gh pr create…` }, { tabId });
|
|
6858
|
+
try {
|
|
6859
|
+
const result = await gitWorkflowRequest("/api/git-workflow/create-pr", { body: { title, body }, runId, tabId });
|
|
6860
|
+
if (!result) return;
|
|
6861
|
+
setGitWorkflow({ step: "done", busy: false, ...gitWorkflowActionDonePatch(workflow, "push"), output: `${formatGitCommandResult(result)}\n\nPull request created.` }, { tabId });
|
|
6862
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
6863
|
+
} catch (error) {
|
|
6864
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "prReview", { tabId });
|
|
6865
|
+
}
|
|
6866
|
+
}
|
|
6867
|
+
|
|
5542
6868
|
function resumeGitWorkflowForActiveTab(tabContext = activeTabContext()) {
|
|
5543
6869
|
if (!isCurrentTabContext(tabContext)) return;
|
|
5544
6870
|
bindGitWorkflowToActiveTab();
|
|
@@ -5552,6 +6878,22 @@ function resumeGitWorkflowForActiveTab(tabContext = activeTabContext()) {
|
|
|
5552
6878
|
}
|
|
5553
6879
|
loadGitWorkflowMessage({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId: workflowTabId });
|
|
5554
6880
|
}
|
|
6881
|
+
if (workflowTabId === tabContext.tabId && gitWorkflow.active && gitWorkflow.step === "branchNaming" && !currentState?.isStreaming) {
|
|
6882
|
+
const retryDelayMs = Math.max(0, 2500 - (Date.now() - (gitWorkflow.branchNameRequestedAt || 0)));
|
|
6883
|
+
if (retryDelayMs > 0) {
|
|
6884
|
+
setTimeout(() => resumeGitWorkflowForActiveTab(tabContext), retryDelayMs);
|
|
6885
|
+
return;
|
|
6886
|
+
}
|
|
6887
|
+
loadGitWorkflowBranchName({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId: workflowTabId });
|
|
6888
|
+
}
|
|
6889
|
+
if (workflowTabId === tabContext.tabId && gitWorkflow.active && gitWorkflow.step === "prGenerating" && !currentState?.isStreaming) {
|
|
6890
|
+
const retryDelayMs = Math.max(0, 2500 - (Date.now() - (gitWorkflow.prRequestedAt || 0)));
|
|
6891
|
+
if (retryDelayMs > 0) {
|
|
6892
|
+
setTimeout(() => resumeGitWorkflowForActiveTab(tabContext), retryDelayMs);
|
|
6893
|
+
return;
|
|
6894
|
+
}
|
|
6895
|
+
loadGitWorkflowPr({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId: workflowTabId });
|
|
6896
|
+
}
|
|
5555
6897
|
}
|
|
5556
6898
|
|
|
5557
6899
|
function normalizeQueuedMessages(event) {
|
|
@@ -8139,6 +9481,13 @@ function setNativeCommandMenuOpen(open) {
|
|
|
8139
9481
|
elements.nativeCommandMenuButton.parentElement?.classList.toggle("open", nativeCommandMenuOpen);
|
|
8140
9482
|
}
|
|
8141
9483
|
|
|
9484
|
+
function setAppRunnerMenuOpen(open) {
|
|
9485
|
+
appRunnerMenuOpen = !!open;
|
|
9486
|
+
elements.appRunnerMenuButton?.setAttribute("aria-expanded", appRunnerMenuOpen ? "true" : "false");
|
|
9487
|
+
elements.appRunnerMenuButton?.classList.toggle("menu-open", appRunnerMenuOpen);
|
|
9488
|
+
elements.appRunnerMenuButton?.parentElement?.classList.toggle("open", appRunnerMenuOpen);
|
|
9489
|
+
}
|
|
9490
|
+
|
|
8142
9491
|
function setOptionsMenuOpen(open) {
|
|
8143
9492
|
optionsMenuOpen = !!open;
|
|
8144
9493
|
elements.optionsMenuButton.setAttribute("aria-expanded", optionsMenuOpen ? "true" : "false");
|
|
@@ -8386,6 +9735,7 @@ async function installOptionalFeature(featureId) {
|
|
|
8386
9735
|
function runPublishWorkflow(command) {
|
|
8387
9736
|
setComposerActionsOpen(false);
|
|
8388
9737
|
setPublishMenuOpen(false);
|
|
9738
|
+
setAppRunnerMenuOpen(false);
|
|
8389
9739
|
setOptionsMenuOpen(false);
|
|
8390
9740
|
const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0];
|
|
8391
9741
|
const featureId = OPTIONAL_COMMAND_FEATURES.get(commandName);
|
|
@@ -8404,6 +9754,7 @@ async function runNativeCommandMenu(command) {
|
|
|
8404
9754
|
setComposerActionsOpen(false);
|
|
8405
9755
|
setPublishMenuOpen(false);
|
|
8406
9756
|
setNativeCommandMenuOpen(false);
|
|
9757
|
+
setAppRunnerMenuOpen(false);
|
|
8407
9758
|
setOptionsMenuOpen(false);
|
|
8408
9759
|
const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0].toLowerCase();
|
|
8409
9760
|
const featureId = optionalFeatureIdForCommand(commandName);
|
|
@@ -10081,6 +11432,7 @@ async function refreshAll(tabContext = activeTabContext()) {
|
|
|
10081
11432
|
refreshCommands(tabContext),
|
|
10082
11433
|
refreshStats(tabContext),
|
|
10083
11434
|
refreshWorkspace(tabContext),
|
|
11435
|
+
refreshAppRunners(tabContext),
|
|
10084
11436
|
refreshNativeSettings(tabContext),
|
|
10085
11437
|
refreshNetworkStatus(),
|
|
10086
11438
|
refreshWebuiVersion(),
|
|
@@ -10795,6 +12147,11 @@ function handleEvent(event) {
|
|
|
10795
12147
|
case "webui_connected":
|
|
10796
12148
|
setWebuiVersion(event.version);
|
|
10797
12149
|
setWebuiDevServer(isWebuiDevMetadata(event));
|
|
12150
|
+
if (Object.prototype.hasOwnProperty.call(event, "activeRun")) {
|
|
12151
|
+
setAppRunnerData(event.tabId || activeTabId, { cwd: event.cwd, activeRun: event.activeRun });
|
|
12152
|
+
renderAppRunnerControls();
|
|
12153
|
+
renderWidgets();
|
|
12154
|
+
}
|
|
10798
12155
|
addEvent(`connected to ${event.tabTitle || "terminal"} for ${event.cwd}`);
|
|
10799
12156
|
scheduleForegroundReconcile("event stream reconnect", 0);
|
|
10800
12157
|
break;
|
|
@@ -10816,6 +12173,7 @@ function handleEvent(event) {
|
|
|
10816
12173
|
case "webui_tab_reloaded":
|
|
10817
12174
|
addEvent(`${event.tabTitle || "terminal"} reloaded`);
|
|
10818
12175
|
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" });
|
|
12176
|
+
clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
10819
12177
|
statusEntries.clear();
|
|
10820
12178
|
widgets.clear();
|
|
10821
12179
|
resetOptionalFeatureAvailability();
|
|
@@ -10833,9 +12191,18 @@ function handleEvent(event) {
|
|
|
10833
12191
|
removeQueuedDialogRequests(event.ids || []);
|
|
10834
12192
|
addEvent(`cancelled ${event.ids?.length || 0} pending extension UI request(s)`, "warn");
|
|
10835
12193
|
break;
|
|
12194
|
+
case "webui_app_runner_update":
|
|
12195
|
+
setAppRunnerData(event.tabId || activeTabId, { cwd: event.cwd, activeRun: event.activeRun });
|
|
12196
|
+
renderAppRunnerControls();
|
|
12197
|
+
renderWidgets();
|
|
12198
|
+
break;
|
|
10836
12199
|
case "webui_cwd_changed":
|
|
10837
12200
|
addEvent(`${event.tabTitle || "terminal"} cwd changed to ${event.cwd}`);
|
|
12201
|
+
setAppRunnerData(event.tabId || activeTabId, { cwd: event.cwd, activeRun: null, runners: [] });
|
|
12202
|
+
renderAppRunnerControls();
|
|
12203
|
+
renderWidgets();
|
|
10838
12204
|
refreshTabs().catch((error) => addEvent(error.message, "error"));
|
|
12205
|
+
refreshAppRunners(tabContext).catch((error) => addEvent(error.message, "error"));
|
|
10839
12206
|
scheduleRefreshFooter();
|
|
10840
12207
|
break;
|
|
10841
12208
|
case "webui_network_rebinding": {
|
|
@@ -10879,6 +12246,7 @@ function handleEvent(event) {
|
|
|
10879
12246
|
case "agent_end":
|
|
10880
12247
|
addEvent("agent finished");
|
|
10881
12248
|
notifyAgentDone(event.tabId || activeTabId, { activity: event.tabActivity, tabTitle: event.tabTitle });
|
|
12249
|
+
clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
10882
12250
|
if (currentState) currentState = { ...currentState, isStreaming: false };
|
|
10883
12251
|
clearRunIndicatorActivity();
|
|
10884
12252
|
markTabOutputSeen();
|
|
@@ -10892,6 +12260,10 @@ function handleEvent(event) {
|
|
|
10892
12260
|
const workflow = gitWorkflowForTab(workflowTabId, { create: false });
|
|
10893
12261
|
if (workflow?.active && workflow.step === "generating") {
|
|
10894
12262
|
loadGitWorkflowMessage({ requireFresh: true, retries: 3, runId: workflow.runId, tabId: workflowTabId });
|
|
12263
|
+
} else if (workflow?.active && workflow.step === "branchNaming") {
|
|
12264
|
+
loadGitWorkflowBranchName({ requireFresh: true, retries: 3, runId: workflow.runId, tabId: workflowTabId });
|
|
12265
|
+
} else if (workflow?.active && workflow.step === "prGenerating") {
|
|
12266
|
+
loadGitWorkflowPr({ requireFresh: true, retries: 3, runId: workflow.runId, tabId: workflowTabId });
|
|
10895
12267
|
}
|
|
10896
12268
|
}
|
|
10897
12269
|
break;
|
|
@@ -10930,15 +12302,22 @@ function handleEvent(event) {
|
|
|
10930
12302
|
break;
|
|
10931
12303
|
case "compaction_start":
|
|
10932
12304
|
if (currentState) currentState = { ...currentState, isCompacting: true };
|
|
12305
|
+
markContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
10933
12306
|
setRunIndicatorActivity(`Compacting context${event.reason ? ` (${event.reason})` : ""}…`);
|
|
10934
12307
|
addEvent(`compaction started (${event.reason})`);
|
|
12308
|
+
renderStatus();
|
|
10935
12309
|
break;
|
|
10936
12310
|
case "compaction_end":
|
|
10937
12311
|
if (currentState) currentState = { ...currentState, isCompacting: false };
|
|
12312
|
+
if (event.aborted) clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
12313
|
+
else markContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
10938
12314
|
addEvent(`compaction ${event.aborted ? "aborted" : "finished"}`);
|
|
10939
12315
|
if (!currentState?.isStreaming) clearRunIndicatorActivity();
|
|
10940
12316
|
markTabOutputSeen();
|
|
12317
|
+
renderStatus();
|
|
12318
|
+
scheduleRefreshState();
|
|
10941
12319
|
scheduleRefreshMessages();
|
|
12320
|
+
scheduleRefreshFooter();
|
|
10942
12321
|
break;
|
|
10943
12322
|
case "auto_retry_start": {
|
|
10944
12323
|
const seconds = Math.max(0, Math.ceil(Number(event.delayMs || 0) / 1000));
|
|
@@ -10982,6 +12361,7 @@ function handleEvent(event) {
|
|
|
10982
12361
|
applyOptimisticThinkingSelection(event.data, tabContext);
|
|
10983
12362
|
} else if (event.command === "new_session") {
|
|
10984
12363
|
const tabId = event.tabId || activeTabId;
|
|
12364
|
+
clearContextUsageUnknownAfterCompaction(tabId);
|
|
10985
12365
|
forgetLastUserPrompt(tabId);
|
|
10986
12366
|
resetGitWorkflowForTab(tabId);
|
|
10987
12367
|
}
|
|
@@ -11038,6 +12418,23 @@ elements.promptListSaveButton?.addEventListener("click", saveDisplayedPromptList
|
|
|
11038
12418
|
elements.promptListRunListButton?.addEventListener("click", () => runDisplayedPromptList());
|
|
11039
12419
|
elements.promptListCloseButton?.addEventListener("click", () => elements.promptListDialog?.close());
|
|
11040
12420
|
elements.promptListDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
12421
|
+
elements.attachmentTextCancelButton?.addEventListener("click", closeTextAttachmentEditor);
|
|
12422
|
+
elements.attachmentTextSaveButton?.addEventListener("click", saveTextAttachmentEdit);
|
|
12423
|
+
elements.attachmentTextEditor?.addEventListener("input", () => {
|
|
12424
|
+
renderTextAttachmentEditorMeta();
|
|
12425
|
+
setAttachmentTextStatus("Unsaved attachment edits.", "warn");
|
|
12426
|
+
});
|
|
12427
|
+
elements.attachmentTextDialog?.addEventListener("close", () => {
|
|
12428
|
+
activeTextAttachmentEditor = null;
|
|
12429
|
+
if (elements.attachmentTextEditor) elements.attachmentTextEditor.value = "";
|
|
12430
|
+
setAttachmentTextStatus("");
|
|
12431
|
+
});
|
|
12432
|
+
elements.attachmentTextDialog?.addEventListener("keydown", (event) => {
|
|
12433
|
+
if (!(event.ctrlKey || event.metaKey) || event.altKey || event.shiftKey || event.key.toLowerCase() !== "s") return;
|
|
12434
|
+
event.preventDefault();
|
|
12435
|
+
if (!elements.attachmentTextSaveButton?.disabled) saveTextAttachmentEdit();
|
|
12436
|
+
});
|
|
12437
|
+
elements.attachmentTextDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
11041
12438
|
elements.sendFeedbackButton.addEventListener("click", () => submitQueuedActionFeedback());
|
|
11042
12439
|
elements.composer.addEventListener("submit", (event) => {
|
|
11043
12440
|
event.preventDefault();
|
|
@@ -11051,7 +12448,47 @@ elements.followUpButton.addEventListener("click", () => sendPromptFromModeButton
|
|
|
11051
12448
|
elements.terminalTabsToggleButton.addEventListener("click", () => {
|
|
11052
12449
|
setMobileTabsExpanded(!document.body.classList.contains("mobile-tabs-expanded"));
|
|
11053
12450
|
});
|
|
11054
|
-
elements.newTabButton.addEventListener("click", () =>
|
|
12451
|
+
elements.newTabButton.addEventListener("click", (event) => {
|
|
12452
|
+
event.stopPropagation();
|
|
12453
|
+
openNewTabMenu();
|
|
12454
|
+
});
|
|
12455
|
+
elements.newTabButton.addEventListener("keydown", (event) => {
|
|
12456
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp" && event.key !== "Enter" && event.key !== " ") return;
|
|
12457
|
+
event.preventDefault();
|
|
12458
|
+
openNewTabMenu();
|
|
12459
|
+
focusNewTabMenuItem(event.key === "ArrowUp" ? "last" : "first");
|
|
12460
|
+
});
|
|
12461
|
+
elements.newTabMenuPanel?.addEventListener("keydown", (event) => {
|
|
12462
|
+
if (event.key === "Escape") {
|
|
12463
|
+
event.preventDefault();
|
|
12464
|
+
setNewTabMenuOpen(false);
|
|
12465
|
+
elements.newTabButton.focus({ preventScroll: true });
|
|
12466
|
+
} else if (event.key === "ArrowDown") {
|
|
12467
|
+
event.preventDefault();
|
|
12468
|
+
moveNewTabMenuFocus(1);
|
|
12469
|
+
} else if (event.key === "ArrowUp") {
|
|
12470
|
+
event.preventDefault();
|
|
12471
|
+
moveNewTabMenuFocus(-1);
|
|
12472
|
+
} else if (event.key === "Home") {
|
|
12473
|
+
event.preventDefault();
|
|
12474
|
+
focusNewTabMenuItem("first");
|
|
12475
|
+
} else if (event.key === "End") {
|
|
12476
|
+
event.preventDefault();
|
|
12477
|
+
focusNewTabMenuItem("last");
|
|
12478
|
+
}
|
|
12479
|
+
});
|
|
12480
|
+
elements.newTabMenu?.addEventListener("pointerenter", () => openNewTabMenu());
|
|
12481
|
+
elements.newTabMenu?.addEventListener("pointerleave", () => {
|
|
12482
|
+
if (!elements.newTabMenu?.contains(document.activeElement)) setNewTabMenuOpen(false);
|
|
12483
|
+
});
|
|
12484
|
+
elements.newTabMenu?.addEventListener("focusin", () => openNewTabMenu());
|
|
12485
|
+
elements.newTabMenu?.addEventListener("focusout", () => {
|
|
12486
|
+
setTimeout(() => {
|
|
12487
|
+
if (!elements.newTabMenu?.contains(document.activeElement)) setNewTabMenuOpen(false);
|
|
12488
|
+
}, 0);
|
|
12489
|
+
});
|
|
12490
|
+
elements.newTabCurrentDirectoryButton?.addEventListener("click", () => createTerminalTab(currentDirectoryForNewTab(), { triggerButton: elements.newTabCurrentDirectoryButton }));
|
|
12491
|
+
elements.newTabChooseDirectoryButton?.addEventListener("click", () => createTerminalTabFromChosenDirectory({ triggerButton: elements.newTabChooseDirectoryButton }));
|
|
11055
12492
|
elements.closeAllTabsButton.addEventListener("click", () => closeAllTerminalTabs());
|
|
11056
12493
|
elements.gitWorkflowButton.addEventListener("click", () => {
|
|
11057
12494
|
setComposerActionsOpen(false);
|
|
@@ -11060,17 +12497,20 @@ elements.gitWorkflowButton.addEventListener("click", () => {
|
|
|
11060
12497
|
const publishMenuContainer = elements.publishButton.parentElement;
|
|
11061
12498
|
elements.publishButton.addEventListener("click", () => {
|
|
11062
12499
|
setNativeCommandMenuOpen(false);
|
|
12500
|
+
setAppRunnerMenuOpen(false);
|
|
11063
12501
|
setOptionsMenuOpen(false);
|
|
11064
12502
|
setPublishMenuOpen(true);
|
|
11065
12503
|
});
|
|
11066
12504
|
publishMenuContainer?.addEventListener("pointerenter", () => {
|
|
11067
12505
|
setNativeCommandMenuOpen(false);
|
|
12506
|
+
setAppRunnerMenuOpen(false);
|
|
11068
12507
|
setOptionsMenuOpen(false);
|
|
11069
12508
|
setPublishMenuOpen(true);
|
|
11070
12509
|
});
|
|
11071
12510
|
publishMenuContainer?.addEventListener("pointerleave", () => setPublishMenuOpen(false));
|
|
11072
12511
|
publishMenuContainer?.addEventListener("focusin", () => {
|
|
11073
12512
|
setNativeCommandMenuOpen(false);
|
|
12513
|
+
setAppRunnerMenuOpen(false);
|
|
11074
12514
|
setOptionsMenuOpen(false);
|
|
11075
12515
|
setPublishMenuOpen(true);
|
|
11076
12516
|
});
|
|
@@ -11082,17 +12522,20 @@ publishMenuContainer?.addEventListener("focusout", () => {
|
|
|
11082
12522
|
const nativeCommandMenuContainer = elements.nativeCommandMenuButton.parentElement;
|
|
11083
12523
|
elements.nativeCommandMenuButton.addEventListener("click", () => {
|
|
11084
12524
|
setPublishMenuOpen(false);
|
|
12525
|
+
setAppRunnerMenuOpen(false);
|
|
11085
12526
|
setOptionsMenuOpen(false);
|
|
11086
12527
|
setNativeCommandMenuOpen(true);
|
|
11087
12528
|
});
|
|
11088
12529
|
nativeCommandMenuContainer?.addEventListener("pointerenter", () => {
|
|
11089
12530
|
setPublishMenuOpen(false);
|
|
12531
|
+
setAppRunnerMenuOpen(false);
|
|
11090
12532
|
setOptionsMenuOpen(false);
|
|
11091
12533
|
setNativeCommandMenuOpen(true);
|
|
11092
12534
|
});
|
|
11093
12535
|
nativeCommandMenuContainer?.addEventListener("pointerleave", () => setNativeCommandMenuOpen(false));
|
|
11094
12536
|
nativeCommandMenuContainer?.addEventListener("focusin", () => {
|
|
11095
12537
|
setPublishMenuOpen(false);
|
|
12538
|
+
setAppRunnerMenuOpen(false);
|
|
11096
12539
|
setOptionsMenuOpen(false);
|
|
11097
12540
|
setNativeCommandMenuOpen(true);
|
|
11098
12541
|
});
|
|
@@ -11101,21 +12544,66 @@ nativeCommandMenuContainer?.addEventListener("focusout", () => {
|
|
|
11101
12544
|
if (!nativeCommandMenuContainer?.contains(document.activeElement)) setNativeCommandMenuOpen(false);
|
|
11102
12545
|
}, 0);
|
|
11103
12546
|
});
|
|
12547
|
+
const appRunnerMenuContainer = elements.appRunnerMenuButton?.parentElement;
|
|
12548
|
+
elements.appRunnerInfoButton?.addEventListener("click", (event) => {
|
|
12549
|
+
event.preventDefault();
|
|
12550
|
+
event.stopPropagation();
|
|
12551
|
+
openAppRunnerInfoDialog();
|
|
12552
|
+
});
|
|
12553
|
+
elements.appRunnerInfoCloseButton?.addEventListener("click", closeAppRunnerInfoDialog);
|
|
12554
|
+
elements.appRunnerMenuButton?.addEventListener("click", async () => {
|
|
12555
|
+
setPublishMenuOpen(false);
|
|
12556
|
+
setNativeCommandMenuOpen(false);
|
|
12557
|
+
setOptionsMenuOpen(false);
|
|
12558
|
+
setAppRunnerMenuOpen(false);
|
|
12559
|
+
const tabContext = activeTabContext();
|
|
12560
|
+
try {
|
|
12561
|
+
await refreshAppRunners(tabContext);
|
|
12562
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
12563
|
+
if (appRunnerMenuCanOpen()) setAppRunnerMenuOpen(true);
|
|
12564
|
+
else if (!appRunnerIsRunning(activeAppRunnerData().activeRun)) openAppRunnerInfoDialog();
|
|
12565
|
+
} catch (error) {
|
|
12566
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
12567
|
+
}
|
|
12568
|
+
});
|
|
12569
|
+
appRunnerMenuContainer?.addEventListener("pointerenter", () => {
|
|
12570
|
+
if (elements.appRunnerMenuButton?.disabled || !appRunnerMenuCanOpen()) return;
|
|
12571
|
+
setPublishMenuOpen(false);
|
|
12572
|
+
setNativeCommandMenuOpen(false);
|
|
12573
|
+
setOptionsMenuOpen(false);
|
|
12574
|
+
setAppRunnerMenuOpen(true);
|
|
12575
|
+
});
|
|
12576
|
+
appRunnerMenuContainer?.addEventListener("pointerleave", () => setAppRunnerMenuOpen(false));
|
|
12577
|
+
appRunnerMenuContainer?.addEventListener("focusin", () => {
|
|
12578
|
+
if (elements.appRunnerMenuButton?.disabled || !appRunnerMenuCanOpen()) return;
|
|
12579
|
+
setPublishMenuOpen(false);
|
|
12580
|
+
setNativeCommandMenuOpen(false);
|
|
12581
|
+
setOptionsMenuOpen(false);
|
|
12582
|
+
setAppRunnerMenuOpen(true);
|
|
12583
|
+
});
|
|
12584
|
+
appRunnerMenuContainer?.addEventListener("focusout", () => {
|
|
12585
|
+
setTimeout(() => {
|
|
12586
|
+
if (!appRunnerMenuContainer?.contains(document.activeElement)) setAppRunnerMenuOpen(false);
|
|
12587
|
+
}, 0);
|
|
12588
|
+
});
|
|
11104
12589
|
const optionsMenuContainer = elements.optionsMenuButton.parentElement;
|
|
11105
12590
|
elements.optionsMenuButton.addEventListener("click", () => {
|
|
11106
12591
|
setPublishMenuOpen(false);
|
|
11107
12592
|
setNativeCommandMenuOpen(false);
|
|
12593
|
+
setAppRunnerMenuOpen(false);
|
|
11108
12594
|
setOptionsMenuOpen(true);
|
|
11109
12595
|
});
|
|
11110
12596
|
optionsMenuContainer?.addEventListener("pointerenter", () => {
|
|
11111
12597
|
setPublishMenuOpen(false);
|
|
11112
12598
|
setNativeCommandMenuOpen(false);
|
|
12599
|
+
setAppRunnerMenuOpen(false);
|
|
11113
12600
|
setOptionsMenuOpen(true);
|
|
11114
12601
|
});
|
|
11115
12602
|
optionsMenuContainer?.addEventListener("pointerleave", () => setOptionsMenuOpen(false));
|
|
11116
12603
|
optionsMenuContainer?.addEventListener("focusin", () => {
|
|
11117
12604
|
setPublishMenuOpen(false);
|
|
11118
12605
|
setNativeCommandMenuOpen(false);
|
|
12606
|
+
setAppRunnerMenuOpen(false);
|
|
11119
12607
|
setOptionsMenuOpen(true);
|
|
11120
12608
|
});
|
|
11121
12609
|
optionsMenuContainer?.addEventListener("focusout", () => {
|
|
@@ -11135,7 +12623,32 @@ elements.optionsSettingsButton.addEventListener("click", () => runNativeCommandM
|
|
|
11135
12623
|
elements.optionsExportButton.addEventListener("click", () => runNativeCommandMenu("/export"));
|
|
11136
12624
|
elements.optionsForkButton.addEventListener("click", () => runNativeCommandMenu("/fork"));
|
|
11137
12625
|
elements.optionsTreeButton.addEventListener("click", () => runNativeCommandMenu("/tree"));
|
|
12626
|
+
elements.gitWorkflowSteps.addEventListener("click", (event) => {
|
|
12627
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
12628
|
+
const button = target?.closest("[data-git-workflow-process]");
|
|
12629
|
+
if (!button || !elements.gitWorkflowSteps.contains(button) || button.disabled) return;
|
|
12630
|
+
selectGitWorkflowProcess(button.dataset.gitWorkflowProcess);
|
|
12631
|
+
});
|
|
11138
12632
|
elements.gitWorkflowCancelButton.addEventListener("click", () => cancelGitWorkflow());
|
|
12633
|
+
elements.gitPrCancelButton?.addEventListener("click", () => resolveGitPrDialog(null));
|
|
12634
|
+
elements.gitPrCreateButton?.addEventListener("click", () => {
|
|
12635
|
+
const title = elements.gitPrTitleInput?.value.trim() || "";
|
|
12636
|
+
const body = elements.gitPrBodyEditor?.value.trimEnd() || "";
|
|
12637
|
+
if (!title) {
|
|
12638
|
+
setGitPrDialogStatus("PR title is required.", "error");
|
|
12639
|
+
elements.gitPrTitleInput?.focus();
|
|
12640
|
+
return;
|
|
12641
|
+
}
|
|
12642
|
+
if (!body.trim()) {
|
|
12643
|
+
setGitPrDialogStatus("PR description is required.", "error");
|
|
12644
|
+
elements.gitPrBodyEditor?.focus();
|
|
12645
|
+
return;
|
|
12646
|
+
}
|
|
12647
|
+
resolveGitPrDialog({ title, body });
|
|
12648
|
+
});
|
|
12649
|
+
elements.gitPrDialog?.addEventListener("close", () => {
|
|
12650
|
+
if (activeGitPrDialogResolve) resolveGitPrDialog(null);
|
|
12651
|
+
});
|
|
11139
12652
|
elements.nativeCommandDialog.addEventListener("close", () => {
|
|
11140
12653
|
elements.nativeCommandSearch.oninput = null;
|
|
11141
12654
|
nativeCommandTabId = null;
|
|
@@ -11231,6 +12744,8 @@ elements.compactButton.addEventListener("click", async () => {
|
|
|
11231
12744
|
elements.compactButton.textContent = "Compacting…";
|
|
11232
12745
|
setRunIndicatorActivity("Requesting context compaction…");
|
|
11233
12746
|
scrollChatToBottom({ force: true });
|
|
12747
|
+
markContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
12748
|
+
renderFooter();
|
|
11234
12749
|
addEvent("manual compaction requested");
|
|
11235
12750
|
await api("/api/compact", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
11236
12751
|
if (!isCurrentTabContext(tabContext)) return;
|
|
@@ -11239,7 +12754,9 @@ elements.compactButton.addEventListener("click", async () => {
|
|
|
11239
12754
|
scheduleRefreshFooter(600, tabContext);
|
|
11240
12755
|
} catch (error) {
|
|
11241
12756
|
if (isCurrentTabContext(tabContext)) {
|
|
12757
|
+
clearContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
11242
12758
|
clearRunIndicatorActivity();
|
|
12759
|
+
renderFooter();
|
|
11243
12760
|
addEvent(error.message, "error");
|
|
11244
12761
|
}
|
|
11245
12762
|
} finally {
|
|
@@ -11339,6 +12856,9 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
11339
12856
|
if (openTerminalTabGroupKey && !event.target?.closest?.(".terminal-tab-group")) {
|
|
11340
12857
|
clearOpenTerminalTabGroup(openTerminalTabGroupKey);
|
|
11341
12858
|
}
|
|
12859
|
+
if (newTabMenuOpen && !event.target?.closest?.(".terminal-new-tab-menu")) {
|
|
12860
|
+
setNewTabMenuOpen(false);
|
|
12861
|
+
}
|
|
11342
12862
|
if (document.body.classList.contains("composer-actions-open") && !elements.composer.contains(event.target)) {
|
|
11343
12863
|
setComposerActionsOpen(false);
|
|
11344
12864
|
}
|
|
@@ -11348,10 +12868,14 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
11348
12868
|
if (nativeCommandMenuOpen && !event.target?.closest?.(".composer-native-command-menu")) {
|
|
11349
12869
|
setNativeCommandMenuOpen(false);
|
|
11350
12870
|
}
|
|
12871
|
+
if (appRunnerMenuOpen && !event.target?.closest?.(".composer-app-runner-menu")) {
|
|
12872
|
+
setAppRunnerMenuOpen(false);
|
|
12873
|
+
}
|
|
11351
12874
|
if (optionsMenuOpen && !event.target?.closest?.(".composer-options-menu")) {
|
|
11352
12875
|
setOptionsMenuOpen(false);
|
|
11353
12876
|
}
|
|
11354
12877
|
if (document.body.classList.contains("mobile-tabs-expanded") && !elements.tabBar.contains(event.target) && !elements.terminalTabsToggleButton.contains(event.target)) {
|
|
12878
|
+
setNewTabMenuOpen(false);
|
|
11355
12879
|
setMobileTabsExpanded(false);
|
|
11356
12880
|
}
|
|
11357
12881
|
if (isFooterPickerOpen() && !elements.statusBar.contains(event.target)) {
|
|
@@ -11374,7 +12898,7 @@ function isTextEntryTarget(target) {
|
|
|
11374
12898
|
|
|
11375
12899
|
function shouldHandleNativeAppShortcut(event) {
|
|
11376
12900
|
if (event.defaultPrevented) return false;
|
|
11377
|
-
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.nativeCommandDialog?.open) return false;
|
|
12901
|
+
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.nativeCommandDialog?.open || elements.appRunnerInfoDialog?.open) return false;
|
|
11378
12902
|
return event.target === elements.promptInput || !isTextEntryTarget(event.target);
|
|
11379
12903
|
}
|
|
11380
12904
|
|
|
@@ -11442,15 +12966,24 @@ window.addEventListener("keydown", (event) => {
|
|
|
11442
12966
|
setNativeCommandMenuOpen(false);
|
|
11443
12967
|
return;
|
|
11444
12968
|
}
|
|
12969
|
+
if (appRunnerMenuOpen) {
|
|
12970
|
+
setAppRunnerMenuOpen(false);
|
|
12971
|
+
return;
|
|
12972
|
+
}
|
|
11445
12973
|
if (optionsMenuOpen) {
|
|
11446
12974
|
setOptionsMenuOpen(false);
|
|
11447
12975
|
return;
|
|
11448
12976
|
}
|
|
12977
|
+
if (newTabMenuOpen) {
|
|
12978
|
+
setNewTabMenuOpen(false);
|
|
12979
|
+
return;
|
|
12980
|
+
}
|
|
11449
12981
|
if (document.body.classList.contains("composer-actions-open")) {
|
|
11450
12982
|
setComposerActionsOpen(false);
|
|
11451
12983
|
return;
|
|
11452
12984
|
}
|
|
11453
12985
|
if (document.body.classList.contains("mobile-tabs-expanded")) {
|
|
12986
|
+
setNewTabMenuOpen(false);
|
|
11454
12987
|
setMobileTabsExpanded(false);
|
|
11455
12988
|
return;
|
|
11456
12989
|
}
|
|
@@ -11473,7 +13006,7 @@ window.addEventListener("keydown", (event) => {
|
|
|
11473
13006
|
}
|
|
11474
13007
|
lastEmptyPromptEscapeTime = now;
|
|
11475
13008
|
}
|
|
11476
|
-
if (
|
|
13009
|
+
if (isSidePanelOverlayView() && !document.body.classList.contains("side-panel-collapsed")) {
|
|
11477
13010
|
setSidePanelCollapsed(true);
|
|
11478
13011
|
return;
|
|
11479
13012
|
}
|
|
@@ -11566,6 +13099,7 @@ elements.promptInput.addEventListener("keydown", (event) => {
|
|
|
11566
13099
|
|
|
11567
13100
|
elements.promptInput.addEventListener("input", () => {
|
|
11568
13101
|
resetPromptHistoryNavigation();
|
|
13102
|
+
if (moveLongPromptInputToAttachment()) return;
|
|
11569
13103
|
resizePromptInput();
|
|
11570
13104
|
renderCommandSuggestions();
|
|
11571
13105
|
});
|
|
@@ -11594,6 +13128,7 @@ resizePromptInput();
|
|
|
11594
13128
|
focusPromptInput({ defer: true });
|
|
11595
13129
|
updateComposerModeButtons();
|
|
11596
13130
|
updateOptionalFeatureAvailability();
|
|
13131
|
+
renderAppRunnerControls();
|
|
11597
13132
|
renderLoadedPromptListPreview();
|
|
11598
13133
|
loadLastUserPromptCache();
|
|
11599
13134
|
loadPromptHistoryCache();
|
|
@@ -11613,6 +13148,7 @@ bindSidePanelSectionToggles();
|
|
|
11613
13148
|
restoreSidePanelState();
|
|
11614
13149
|
initializeCodexUsage();
|
|
11615
13150
|
bindMobileViewChanges();
|
|
13151
|
+
bindSidePanelOverlayViewChanges();
|
|
11616
13152
|
registerPwaServiceWorker();
|
|
11617
13153
|
renderServerOfflinePanel();
|
|
11618
13154
|
initializeTabs().catch((error) => addEvent(error.message, "error"));
|