@firstpick/pi-package-webui 0.3.2 → 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 +1482 -44
- package/public/index.html +71 -3
- package/public/service-worker.js +1 -1
- package/public/styles.css +592 -27
- package/tests/mobile-static.test.mjs +91 -9
package/public/app.js
CHANGED
|
@@ -50,6 +50,10 @@ const elements = {
|
|
|
50
50
|
nativeCommandMenu: $("#nativeCommandMenu"),
|
|
51
51
|
nativeSkillsButton: $("#nativeSkillsButton"),
|
|
52
52
|
nativeToolsButton: $("#nativeToolsButton"),
|
|
53
|
+
appRunnerMenu: $("#appRunnerMenu"),
|
|
54
|
+
appRunnerInfoButton: $("#appRunnerInfoButton"),
|
|
55
|
+
appRunnerMenuButton: $("#appRunnerMenuButton"),
|
|
56
|
+
appRunnerMenuPanel: $("#appRunnerMenuPanel"),
|
|
53
57
|
optionsMenuButton: $("#optionsMenuButton"),
|
|
54
58
|
optionsMenu: $("#optionsMenu"),
|
|
55
59
|
optionsResumeButton: $("#optionsResumeButton"),
|
|
@@ -67,6 +71,12 @@ const elements = {
|
|
|
67
71
|
gitWorkflowOutput: $("#gitWorkflowOutput"),
|
|
68
72
|
gitWorkflowActions: $("#gitWorkflowActions"),
|
|
69
73
|
gitWorkflowCancelButton: $("#gitWorkflowCancelButton"),
|
|
74
|
+
gitPrDialog: $("#gitPrDialog"),
|
|
75
|
+
gitPrTitleInput: $("#gitPrTitleInput"),
|
|
76
|
+
gitPrBodyEditor: $("#gitPrBodyEditor"),
|
|
77
|
+
gitPrStatus: $("#gitPrStatus"),
|
|
78
|
+
gitPrCancelButton: $("#gitPrCancelButton"),
|
|
79
|
+
gitPrCreateButton: $("#gitPrCreateButton"),
|
|
70
80
|
modelSelect: $("#modelSelect"),
|
|
71
81
|
setModelButton: $("#setModelButton"),
|
|
72
82
|
thinkingSelect: $("#thinkingSelect"),
|
|
@@ -113,6 +123,13 @@ const elements = {
|
|
|
113
123
|
promptListDialogLoadButton: $("#promptListDialogLoadButton"),
|
|
114
124
|
promptListSaveButton: $("#promptListSaveButton"),
|
|
115
125
|
promptListRunListButton: $("#promptListRunListButton"),
|
|
126
|
+
attachmentTextDialog: $("#attachmentTextDialog"),
|
|
127
|
+
attachmentTextTitle: $("#attachmentTextTitle"),
|
|
128
|
+
attachmentTextMeta: $("#attachmentTextMeta"),
|
|
129
|
+
attachmentTextEditor: $("#attachmentTextEditor"),
|
|
130
|
+
attachmentTextStatus: $("#attachmentTextStatus"),
|
|
131
|
+
attachmentTextCancelButton: $("#attachmentTextCancelButton"),
|
|
132
|
+
attachmentTextSaveButton: $("#attachmentTextSaveButton"),
|
|
116
133
|
commandSearchInput: $("#commandSearchInput"),
|
|
117
134
|
commandsBox: $("#commandsBox"),
|
|
118
135
|
eventLog: $("#eventLog"),
|
|
@@ -143,6 +160,9 @@ const elements = {
|
|
|
143
160
|
nativeCommandBody: $("#nativeCommandBody"),
|
|
144
161
|
nativeCommandError: $("#nativeCommandError"),
|
|
145
162
|
nativeCommandActions: $("#nativeCommandActions"),
|
|
163
|
+
appRunnerInfoDialog: $("#appRunnerInfoDialog"),
|
|
164
|
+
appRunnerInfoBody: $("#appRunnerInfoBody"),
|
|
165
|
+
appRunnerInfoCloseButton: $("#appRunnerInfoCloseButton"),
|
|
146
166
|
};
|
|
147
167
|
|
|
148
168
|
let currentState = null;
|
|
@@ -151,6 +171,7 @@ let activeTabId = null;
|
|
|
151
171
|
let activeTabGeneration = 0;
|
|
152
172
|
let tabDrafts = new Map();
|
|
153
173
|
let tabAttachments = new Map();
|
|
174
|
+
let activeTextAttachmentEditor = null;
|
|
154
175
|
let tabActivities = new Map();
|
|
155
176
|
let tabSeenCompletionSerials = new Map();
|
|
156
177
|
let streamBubble = null;
|
|
@@ -178,8 +199,10 @@ let refreshTabsTimer = null;
|
|
|
178
199
|
let foregroundReconcileTimer = null;
|
|
179
200
|
let eventSource = null;
|
|
180
201
|
let activeDialog = null;
|
|
202
|
+
let activeGitPrDialogResolve = null;
|
|
181
203
|
let nativeCommandTabId = null;
|
|
182
204
|
let pathPickerState = null;
|
|
205
|
+
let firstTerminalCwdPromptShown = false;
|
|
183
206
|
let pathFastPicks = [];
|
|
184
207
|
let pathFastPicksReady = false;
|
|
185
208
|
let pathFastPicksLoadPromise = null;
|
|
@@ -187,6 +210,9 @@ let mobileTabsExpanded = false;
|
|
|
187
210
|
let openTerminalTabGroupKey = null;
|
|
188
211
|
let newTabMenuOpen = false;
|
|
189
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: "" };
|
|
190
216
|
let optionsMenuOpen = false;
|
|
191
217
|
let availableCommands = [];
|
|
192
218
|
let rawAvailableCommands = [];
|
|
@@ -244,6 +270,7 @@ let customBackgroundLoading = false;
|
|
|
244
270
|
let footerScopedModels = [];
|
|
245
271
|
let footerScopedModelPatterns = [];
|
|
246
272
|
let footerScopedModelSource = "none";
|
|
273
|
+
const contextUsageUnknownAfterCompactionByTab = new Map();
|
|
247
274
|
let autoFollowChat = true;
|
|
248
275
|
let chatFollowFrame = null;
|
|
249
276
|
let chatFollowSettleTimer = null;
|
|
@@ -293,10 +320,13 @@ const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
|
293
320
|
const ATTACHMENT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
294
321
|
const ATTACHMENT_INLINE_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
|
|
295
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";
|
|
296
325
|
const INLINE_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
297
326
|
const BACKGROUND_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
298
327
|
const DEFAULT_THEME_NAME = "catppuccin-mocha";
|
|
299
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)";
|
|
300
330
|
const CHAT_BOTTOM_THRESHOLD_PX = 96;
|
|
301
331
|
const STICKY_USER_PROMPT_PREVIEW_LIMIT = 220;
|
|
302
332
|
const STICKY_USER_PROMPT_TOP_GAP_PX = 12;
|
|
@@ -325,10 +355,12 @@ const BLOCKED_TAB_NOTIFICATION_TAG_PREFIX = "pi-webui-blocked-tab";
|
|
|
325
355
|
const AGENT_DONE_NOTIFICATION_TAG_PREFIX = "pi-webui-agent-done";
|
|
326
356
|
const BLOCKED_TAB_NOTIFICATION_ICON = "/icon-192.png";
|
|
327
357
|
const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
358
|
+
const sidePanelOverlayMedia = window.matchMedia?.(SIDE_PANEL_OVERLAY_QUERY) || mobileViewMedia;
|
|
328
359
|
const statusEntries = new Map();
|
|
329
360
|
const widgets = new Map();
|
|
330
361
|
const todoProgressWidgetExpandedByTab = new Map();
|
|
331
362
|
const releaseNpmOutputExpandedByTab = new Map();
|
|
363
|
+
const appRunnerDataByTab = new Map();
|
|
332
364
|
const liveToolRuns = new Map();
|
|
333
365
|
const liveToolCards = new Map();
|
|
334
366
|
const liveToolRenderQueue = new Map();
|
|
@@ -415,6 +447,8 @@ const OPTIONAL_FEATURES = [
|
|
|
415
447
|
const OPTIONAL_FEATURE_BY_ID = new Map(OPTIONAL_FEATURES.map((feature) => [feature.id, feature]));
|
|
416
448
|
const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
417
449
|
["git-staged-msg", "gitWorkflow"],
|
|
450
|
+
["git-branch-name", "gitWorkflow"],
|
|
451
|
+
["pr", "gitWorkflow"],
|
|
418
452
|
["release-npm", "releaseNpm"],
|
|
419
453
|
["release-aur", "releaseAur"],
|
|
420
454
|
["skills", "tuiSkillsCommand"],
|
|
@@ -446,16 +480,42 @@ const SETTINGS_AUTOCOMPLETE_OPTIONS = ["3", "5", "7", "10", "15", "20"];
|
|
|
446
480
|
const optionalFeatureInstallInProgress = new Set();
|
|
447
481
|
const gitFooterPayloadRefreshInFlightByTab = new Set();
|
|
448
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
|
+
|
|
449
501
|
function createGitWorkflowState() {
|
|
450
502
|
return {
|
|
451
503
|
active: false,
|
|
452
504
|
step: "idle",
|
|
505
|
+
process: "stage",
|
|
453
506
|
busy: false,
|
|
454
507
|
runId: 0,
|
|
455
508
|
output: "",
|
|
456
509
|
error: "",
|
|
457
510
|
message: null,
|
|
458
511
|
messageRequestedAt: 0,
|
|
512
|
+
branchName: "",
|
|
513
|
+
branchNameRequestedAt: 0,
|
|
514
|
+
actionsDone: createGitWorkflowActionsDone(),
|
|
515
|
+
prMode: false,
|
|
516
|
+
prBranch: "",
|
|
517
|
+
pr: null,
|
|
518
|
+
prRequestedAt: 0,
|
|
459
519
|
};
|
|
460
520
|
}
|
|
461
521
|
|
|
@@ -499,7 +559,13 @@ function clearGitWorkflowForTab(tabId) {
|
|
|
499
559
|
}
|
|
500
560
|
}
|
|
501
561
|
|
|
502
|
-
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));
|
|
503
569
|
const ACTION_FEEDBACK_REACTIONS = {
|
|
504
570
|
up: { icon: "👍", label: "Good job", title: "Good job!" },
|
|
505
571
|
down: { icon: "👎", label: "Avoid this", title: "Avoid this" },
|
|
@@ -511,11 +577,32 @@ const GIT_WORKFLOW_ACTIVE_INDEX = {
|
|
|
511
577
|
generate: 1,
|
|
512
578
|
generating: 1,
|
|
513
579
|
message: 2,
|
|
580
|
+
branchNaming: 2,
|
|
581
|
+
branching: 2,
|
|
514
582
|
committing: 2,
|
|
515
583
|
push: 3,
|
|
516
584
|
pushing: 3,
|
|
585
|
+
prGenerating: 3,
|
|
586
|
+
prReview: 3,
|
|
587
|
+
prCreating: 3,
|
|
517
588
|
done: 4,
|
|
518
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");
|
|
519
606
|
|
|
520
607
|
function make(tag, className, text) {
|
|
521
608
|
const node = document.createElement(tag);
|
|
@@ -532,6 +619,10 @@ function isMobileView() {
|
|
|
532
619
|
return mobileViewMedia?.matches || false;
|
|
533
620
|
}
|
|
534
621
|
|
|
622
|
+
function isSidePanelOverlayView() {
|
|
623
|
+
return sidePanelOverlayMedia?.matches || false;
|
|
624
|
+
}
|
|
625
|
+
|
|
535
626
|
function readStoredSidePanelCollapsed() {
|
|
536
627
|
try {
|
|
537
628
|
const stored = localStorage.getItem(SIDE_PANEL_STORAGE_KEY);
|
|
@@ -793,6 +884,7 @@ function setComposerActionsOpen(open) {
|
|
|
793
884
|
if (!shouldOpen) {
|
|
794
885
|
setPublishMenuOpen(false);
|
|
795
886
|
setNativeCommandMenuOpen(false);
|
|
887
|
+
setAppRunnerMenuOpen(false);
|
|
796
888
|
setOptionsMenuOpen(false);
|
|
797
889
|
}
|
|
798
890
|
}
|
|
@@ -928,7 +1020,7 @@ function setMobileTabsExpanded(expanded) {
|
|
|
928
1020
|
}
|
|
929
1021
|
|
|
930
1022
|
function syncMobileSidePanelState(collapsed) {
|
|
931
|
-
const showBackdrop = !collapsed &&
|
|
1023
|
+
const showBackdrop = !collapsed && isSidePanelOverlayView();
|
|
932
1024
|
elements.sidePanelBackdrop.hidden = !showBackdrop;
|
|
933
1025
|
if (showBackdrop) elements.sidePanel.setAttribute("aria-modal", "true");
|
|
934
1026
|
else elements.sidePanel.removeAttribute("aria-modal");
|
|
@@ -942,11 +1034,11 @@ function setSidePanelCollapsed(collapsed, { persist = true, focusPanel = false }
|
|
|
942
1034
|
elements.sidePanelExpandButton.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
943
1035
|
syncMobileSidePanelState(collapsed);
|
|
944
1036
|
|
|
945
|
-
if (!collapsed && focusPanel &&
|
|
1037
|
+
if (!collapsed && focusPanel && isSidePanelOverlayView()) {
|
|
946
1038
|
requestAnimationFrame(() => elements.toggleSidePanelButton.focus());
|
|
947
1039
|
}
|
|
948
1040
|
|
|
949
|
-
if (!persist ||
|
|
1041
|
+
if (!persist || isSidePanelOverlayView()) return;
|
|
950
1042
|
try {
|
|
951
1043
|
localStorage.setItem(SIDE_PANEL_STORAGE_KEY, collapsed ? "1" : "0");
|
|
952
1044
|
} catch {
|
|
@@ -955,7 +1047,7 @@ function setSidePanelCollapsed(collapsed, { persist = true, focusPanel = false }
|
|
|
955
1047
|
}
|
|
956
1048
|
|
|
957
1049
|
function restoreSidePanelState() {
|
|
958
|
-
if (
|
|
1050
|
+
if (isSidePanelOverlayView()) {
|
|
959
1051
|
setSidePanelCollapsed(true, { persist: false });
|
|
960
1052
|
return;
|
|
961
1053
|
}
|
|
@@ -969,7 +1061,7 @@ function bindMobileViewChanges() {
|
|
|
969
1061
|
setComposerActionsOpen(false);
|
|
970
1062
|
setMobileFooterExpanded(false);
|
|
971
1063
|
setMobileTabsExpanded(false);
|
|
972
|
-
if (event.matches) {
|
|
1064
|
+
if (event.matches || isSidePanelOverlayView()) {
|
|
973
1065
|
setSidePanelCollapsed(true, { persist: false });
|
|
974
1066
|
return;
|
|
975
1067
|
}
|
|
@@ -980,6 +1072,20 @@ function bindMobileViewChanges() {
|
|
|
980
1072
|
else mobileViewMedia.addListener?.(syncForViewport);
|
|
981
1073
|
}
|
|
982
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
|
+
|
|
983
1089
|
function updateVisualViewportVars() {
|
|
984
1090
|
const viewport = window.visualViewport;
|
|
985
1091
|
const viewportHeight = viewport?.height || window.innerHeight || document.documentElement.clientHeight;
|
|
@@ -1055,8 +1161,7 @@ function currentPortArg() {
|
|
|
1055
1161
|
}
|
|
1056
1162
|
|
|
1057
1163
|
function serverStartCommandText() {
|
|
1058
|
-
|
|
1059
|
-
return `pi-webui --cwd ${quoteCommandArg(cwd)}${currentPortArg()}`;
|
|
1164
|
+
return `pi-webui${currentPortArg()}`;
|
|
1060
1165
|
}
|
|
1061
1166
|
|
|
1062
1167
|
function serverStartSlashCommandText() {
|
|
@@ -1376,6 +1481,46 @@ function attachmentIcon(kind) {
|
|
|
1376
1481
|
return kind === "image" ? "🖼️" : kind === "video" ? "🎞️" : kind === "audio" ? "🎵" : kind === "doc" ? "📄" : "📎";
|
|
1377
1482
|
}
|
|
1378
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
|
+
|
|
1379
1524
|
function attachmentsForTab(tabId = activeTabId) {
|
|
1380
1525
|
return tabId ? tabAttachments.get(tabId) || [] : [];
|
|
1381
1526
|
}
|
|
@@ -1404,11 +1549,19 @@ function renderAttachmentTray() {
|
|
|
1404
1549
|
const icon = make("span", "attachment-pill-icon", attachmentIcon(attachment.kind));
|
|
1405
1550
|
const name = make("span", "attachment-pill-name", attachment.name);
|
|
1406
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
|
+
}
|
|
1407
1558
|
const remove = make("button", "attachment-remove-button", "×");
|
|
1408
1559
|
remove.type = "button";
|
|
1409
1560
|
remove.setAttribute("aria-label", `Remove ${attachment.name}`);
|
|
1410
1561
|
remove.addEventListener("click", () => removeAttachment(attachment.id));
|
|
1411
|
-
pill.append(icon, name, meta
|
|
1562
|
+
pill.append(icon, name, meta);
|
|
1563
|
+
if (edit) pill.append(edit);
|
|
1564
|
+
pill.append(remove);
|
|
1412
1565
|
tray.append(pill);
|
|
1413
1566
|
}
|
|
1414
1567
|
}
|
|
@@ -1419,6 +1572,7 @@ function removeAttachment(id, tabId = activeTabId) {
|
|
|
1419
1572
|
if (index === -1) return;
|
|
1420
1573
|
const [removed] = attachments.splice(index, 1);
|
|
1421
1574
|
if (removed?.previewUrl) URL.revokeObjectURL(removed.previewUrl);
|
|
1575
|
+
if (activeTextAttachmentEditor?.tabId === tabId && activeTextAttachmentEditor?.attachmentId === id) closeTextAttachmentEditor();
|
|
1422
1576
|
if (attachments.length === 0) tabAttachments.delete(tabId);
|
|
1423
1577
|
if (tabId === activeTabId) renderAttachmentTray();
|
|
1424
1578
|
}
|
|
@@ -1428,15 +1582,16 @@ function clearAttachments(tabId = activeTabId) {
|
|
|
1428
1582
|
for (const attachment of attachments) {
|
|
1429
1583
|
if (attachment.previewUrl) URL.revokeObjectURL(attachment.previewUrl);
|
|
1430
1584
|
}
|
|
1585
|
+
if (activeTextAttachmentEditor?.tabId === tabId) closeTextAttachmentEditor();
|
|
1431
1586
|
if (tabId) tabAttachments.delete(tabId);
|
|
1432
1587
|
if (tabId === activeTabId) renderAttachmentTray();
|
|
1433
1588
|
}
|
|
1434
1589
|
|
|
1435
1590
|
function addAttachmentFiles(fileList, source = "picker") {
|
|
1436
1591
|
const files = Array.from(fileList || []).filter(Boolean);
|
|
1437
|
-
if (!files.length) return;
|
|
1592
|
+
if (!files.length) return { added: 0, skipped: [] };
|
|
1438
1593
|
const attachments = ensureAttachmentsForTab();
|
|
1439
|
-
if (!attachments.length && !activeTabId) return;
|
|
1594
|
+
if (!attachments.length && !activeTabId) return { added: 0, skipped: ["no active tab"] };
|
|
1440
1595
|
let totalBytes = attachments.reduce((sum, attachment) => sum + attachment.size, 0);
|
|
1441
1596
|
let added = 0;
|
|
1442
1597
|
const skipped = [];
|
|
@@ -1474,6 +1629,129 @@ function addAttachmentFiles(fileList, source = "picker") {
|
|
|
1474
1629
|
renderAttachmentTray();
|
|
1475
1630
|
if (added) addEvent(`attached ${added} ${added === 1 ? "file" : "files"} from ${source}`, "info");
|
|
1476
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();
|
|
1477
1755
|
}
|
|
1478
1756
|
|
|
1479
1757
|
function clipboardFiles(dataTransfer) {
|
|
@@ -1501,9 +1779,15 @@ function clipboardFiles(dataTransfer) {
|
|
|
1501
1779
|
|
|
1502
1780
|
function handleAttachmentPaste(event) {
|
|
1503
1781
|
const files = clipboardFiles(event.clipboardData);
|
|
1504
|
-
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;
|
|
1505
1790
|
event.preventDefault();
|
|
1506
|
-
addAttachmentFiles(files, "clipboard");
|
|
1507
1791
|
}
|
|
1508
1792
|
|
|
1509
1793
|
function isFileDrag(event) {
|
|
@@ -2592,7 +2876,7 @@ function restoreActiveDraft() {
|
|
|
2592
2876
|
|
|
2593
2877
|
function focusPromptInput({ defer = false } = {}) {
|
|
2594
2878
|
const focus = () => {
|
|
2595
|
-
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;
|
|
2596
2880
|
try {
|
|
2597
2881
|
elements.promptInput.focus({ preventScroll: true });
|
|
2598
2882
|
} catch {
|
|
@@ -2666,6 +2950,7 @@ function resetActiveTabUi() {
|
|
|
2666
2950
|
else renderQueue({ tabId: activeTabId, steering: [], followUp: [] });
|
|
2667
2951
|
elements.commandsBox.textContent = "Loading…";
|
|
2668
2952
|
elements.commandsBox.classList.add("muted");
|
|
2953
|
+
renderAppRunnerControls();
|
|
2669
2954
|
renderWidgets();
|
|
2670
2955
|
renderGitWorkflow();
|
|
2671
2956
|
renderFooter();
|
|
@@ -2888,6 +3173,7 @@ function setNewTabMenuOpen(open) {
|
|
|
2888
3173
|
function openNewTabMenu() {
|
|
2889
3174
|
setPublishMenuOpen(false);
|
|
2890
3175
|
setNativeCommandMenuOpen(false);
|
|
3176
|
+
setAppRunnerMenuOpen(false);
|
|
2891
3177
|
setOptionsMenuOpen(false);
|
|
2892
3178
|
setNewTabMenuOpen(true);
|
|
2893
3179
|
}
|
|
@@ -2973,10 +3259,15 @@ function currentDirectoryForNewTab() {
|
|
|
2973
3259
|
async function createTerminalTab(cwd = currentDirectoryForNewTab(), { triggerButton = elements.newTabButton } = {}) {
|
|
2974
3260
|
setMobileTabsExpanded(false);
|
|
2975
3261
|
setNewTabMenuOpen(false);
|
|
3262
|
+
const resolvedCwd = cwd || currentDirectoryForNewTab();
|
|
3263
|
+
if (!resolvedCwd && tabs.length === 0) {
|
|
3264
|
+
await createTerminalTabFromChosenDirectory({ triggerButton });
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
2976
3267
|
const disabledButtons = new Set([elements.newTabButton, triggerButton].filter(Boolean));
|
|
2977
3268
|
for (const button of disabledButtons) button.disabled = true;
|
|
2978
3269
|
try {
|
|
2979
|
-
const response = await api("/api/tabs", { method: "POST", body: { cwd:
|
|
3270
|
+
const response = await api("/api/tabs", { method: "POST", body: { cwd: resolvedCwd }, scoped: false });
|
|
2980
3271
|
tabs = response.data?.tabs || tabs;
|
|
2981
3272
|
syncTabMetadata(tabs);
|
|
2982
3273
|
const tab = response.data?.tab;
|
|
@@ -3002,6 +3293,17 @@ async function createTerminalTabFromChosenDirectory({ triggerButton = elements.n
|
|
|
3002
3293
|
await createTerminalTab(cwd, { triggerButton });
|
|
3003
3294
|
}
|
|
3004
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
|
+
|
|
3005
3307
|
function tabHasActiveAgent(tab) {
|
|
3006
3308
|
const activity = activityForTab(tab);
|
|
3007
3309
|
const indicator = tabIndicator(tab);
|
|
@@ -3051,6 +3353,7 @@ async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } =
|
|
|
3051
3353
|
tabDrafts.delete(id);
|
|
3052
3354
|
clearAttachments(id);
|
|
3053
3355
|
clearGitWorkflowForTab(id);
|
|
3356
|
+
appRunnerDataByTab.delete(id);
|
|
3054
3357
|
}
|
|
3055
3358
|
clearOpenTerminalTabGroup(null, { force: true });
|
|
3056
3359
|
|
|
@@ -3093,10 +3396,14 @@ async function closeAllTerminalTabs() {
|
|
|
3093
3396
|
}
|
|
3094
3397
|
|
|
3095
3398
|
async function initializeTabs() {
|
|
3096
|
-
await refreshTabs({ selectStored: true });
|
|
3399
|
+
const loadedTabs = await refreshTabs({ selectStored: true });
|
|
3097
3400
|
resetActiveTabUi();
|
|
3098
3401
|
renderTabs();
|
|
3099
3402
|
restoreActiveDraft();
|
|
3403
|
+
if (!loadedTabs.length) {
|
|
3404
|
+
await createFirstTerminalTabFromChosenDirectory();
|
|
3405
|
+
return;
|
|
3406
|
+
}
|
|
3100
3407
|
focusPromptInput({ defer: true });
|
|
3101
3408
|
const tabContext = activeTabContext();
|
|
3102
3409
|
connectEvents(tabContext);
|
|
@@ -3582,6 +3889,50 @@ function footerCostAuthLabel() {
|
|
|
3582
3889
|
return /codex|copilot|chatgpt/i.test(provider) ? "sub" : "api";
|
|
3583
3890
|
}
|
|
3584
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
|
+
|
|
3585
3936
|
function footerStatsTokensDisplay(stats = latestStats) {
|
|
3586
3937
|
const tokens = stats?.tokens;
|
|
3587
3938
|
if (!tokens) return "";
|
|
@@ -3602,7 +3953,8 @@ function footerContextDisplayWithAuto(value, state = currentState) {
|
|
|
3602
3953
|
|
|
3603
3954
|
function footerStatsContextDisplay(stats = latestStats) {
|
|
3604
3955
|
const usage = stats?.contextUsage || currentState?.contextUsage;
|
|
3605
|
-
const contextWindow = usage
|
|
3956
|
+
const contextWindow = contextWindowFromSources(usage, currentState?.model?.contextWindow);
|
|
3957
|
+
if (contextUsageUnknownAfterCompaction()) return footerContextDisplayWithAuto(unknownFooterContextText(usage));
|
|
3606
3958
|
if (!contextWindow) return "";
|
|
3607
3959
|
const rawPercent = Number(usage?.percent);
|
|
3608
3960
|
const percent = Number.isFinite(rawPercent) ? `${rawPercent.toFixed(1)}%` : "?";
|
|
@@ -3901,8 +4253,10 @@ function footerPayloadWithLiveModel(payload) {
|
|
|
3901
4253
|
const effort = footerThinkingDisplay();
|
|
3902
4254
|
const hasThinkingChip = [...payload.main, ...payload.meta].some((chip) => chip?.key === "thinking");
|
|
3903
4255
|
const contextChip = (chip) => {
|
|
3904
|
-
const
|
|
3905
|
-
|
|
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 } : {}) };
|
|
3906
4260
|
};
|
|
3907
4261
|
const effortChip = (chip) => ({ ...chip, key: "thinking", label: "effort", value: effort, title: `effort: ${effort}`, tone: "mauve" });
|
|
3908
4262
|
const splitChip = (chip) => {
|
|
@@ -5243,13 +5597,537 @@ function renderReleaseAurLogWidget() {
|
|
|
5243
5597
|
actions.append(releaseNpmActionButton("Close log", "/release-aur logs close"));
|
|
5244
5598
|
header.append(titleWrap, meta, actions);
|
|
5245
5599
|
|
|
5246
|
-
const logLines = lines.slice(2).filter((line, index) => index > 0 || stripAnsi(line).trim());
|
|
5247
|
-
const streamHeader = releaseNpmStreamHeader("Saved AUR output stream", logLines.length);
|
|
5600
|
+
const logLines = lines.slice(2).filter((line, index) => index > 0 || stripAnsi(line).trim());
|
|
5601
|
+
const streamHeader = releaseNpmStreamHeader("Saved AUR output stream", logLines.length);
|
|
5602
|
+
const terminal = make("div", "release-npm-terminal");
|
|
5603
|
+
for (const line of logLines) {
|
|
5604
|
+
appendReleaseNpmTerminalLine(terminal, line);
|
|
5605
|
+
}
|
|
5606
|
+
const outputDetails = renderReleaseNpmOutputDetails("release-aur:logs", streamHeader, terminal);
|
|
5607
|
+
node.append(header, outputDetails);
|
|
5608
|
+
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
5609
|
+
return node;
|
|
5610
|
+
}
|
|
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 });
|
|
5248
6105
|
const terminal = make("div", "release-npm-terminal");
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
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);
|
|
5253
6131
|
node.append(header, outputDetails);
|
|
5254
6132
|
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
5255
6133
|
return node;
|
|
@@ -5265,6 +6143,8 @@ function renderWidgets() {
|
|
|
5265
6143
|
if (releaseAurOutput) elements.widgetArea.append(releaseAurOutput);
|
|
5266
6144
|
const releaseAurLog = renderReleaseAurLogWidget();
|
|
5267
6145
|
if (releaseAurLog) elements.widgetArea.append(releaseAurLog);
|
|
6146
|
+
const appRunnerWidget = renderAppRunnerWidget();
|
|
6147
|
+
if (appRunnerWidget) elements.widgetArea.append(appRunnerWidget);
|
|
5268
6148
|
|
|
5269
6149
|
for (const [key, value] of widgets) {
|
|
5270
6150
|
const widgetFeatureId = optionalFeatureWidgetFeatureId(key);
|
|
@@ -5288,6 +6168,8 @@ function setGitWorkflow(patch, { tabId = activeTabId } = {}) {
|
|
|
5288
6168
|
const workflow = gitWorkflowForTab(tabId);
|
|
5289
6169
|
if (!workflow) return null;
|
|
5290
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);
|
|
5291
6173
|
if (tabId === activeTabId) {
|
|
5292
6174
|
gitWorkflow = workflow;
|
|
5293
6175
|
renderGitWorkflow();
|
|
@@ -5323,24 +6205,141 @@ function formatCommitMessagePreview(message) {
|
|
|
5323
6205
|
return [`=== SHORT ===`, message.short || "(empty)", "", "=== LONG ===", message.long || "(empty)"].join("\n");
|
|
5324
6206
|
}
|
|
5325
6207
|
|
|
5326
|
-
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 = "") {
|
|
5327
6238
|
const button = make("button", className, label);
|
|
5328
6239
|
button.type = "button";
|
|
5329
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
|
+
}
|
|
5330
6246
|
button.addEventListener("click", handler);
|
|
5331
6247
|
elements.gitWorkflowActions.append(button);
|
|
5332
6248
|
return button;
|
|
5333
6249
|
}
|
|
5334
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
|
+
|
|
5335
6329
|
function gitWorkflowTitle() {
|
|
5336
6330
|
switch (gitWorkflow.step) {
|
|
5337
6331
|
case "add": return "Stage all changes";
|
|
5338
6332
|
case "generate": return "Generate staged commit message";
|
|
5339
6333
|
case "generating": return "Waiting for /git-staged-msg";
|
|
5340
|
-
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";
|
|
5341
6337
|
case "committing": return "Committing";
|
|
5342
|
-
case "push": return "Push commit";
|
|
6338
|
+
case "push": return gitWorkflow.prMode ? "Push branch and create PR" : "Push commit";
|
|
5343
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";
|
|
5344
6343
|
case "done": return "Git workflow complete";
|
|
5345
6344
|
case "cancelled": return "Git workflow cancelled";
|
|
5346
6345
|
case "error": return "Git workflow needs attention";
|
|
@@ -5353,11 +6352,16 @@ function gitWorkflowHint() {
|
|
|
5353
6352
|
case "add": return "Step 1: run git add . in the current Pi working directory.";
|
|
5354
6353
|
case "generate": return "Step 2: run /git-staged-msg, then preview the generated files.";
|
|
5355
6354
|
case "generating": return "Pi is generating dev/COMMIT/staged-commit-short.txt and staged-commit-long.txt.";
|
|
5356
|
-
case "message": return "Step 3/4: preview the native g-msg output
|
|
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.";
|
|
5357
6358
|
case "committing": return "Running native git commit from the generated message file.";
|
|
5358
|
-
case "push": return "Step 5: push the new commit to the configured remote.";
|
|
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.";
|
|
5359
6360
|
case "pushing": return "Running git push. Cancel will request process termination.";
|
|
5360
|
-
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.";
|
|
5361
6365
|
case "cancelled": return "No further workflow steps will run.";
|
|
5362
6366
|
case "error": return gitWorkflow.error || "Fix the issue, then retry or restart.";
|
|
5363
6367
|
default: return "Stage changes, generate a commit message, commit, and push.";
|
|
@@ -5375,9 +6379,14 @@ function renderGitWorkflow() {
|
|
|
5375
6379
|
elements.gitWorkflowActions.replaceChildren();
|
|
5376
6380
|
|
|
5377
6381
|
const activeIndex = GIT_WORKFLOW_ACTIVE_INDEX[gitWorkflow.step] ?? 0;
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
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");
|
|
5381
6390
|
if (index === activeIndex && !["done", "cancelled", "error"].includes(gitWorkflow.step)) item.classList.add("active");
|
|
5382
6391
|
elements.gitWorkflowSteps.append(item);
|
|
5383
6392
|
}
|
|
@@ -5393,11 +6402,28 @@ function renderGitWorkflow() {
|
|
|
5393
6402
|
} else if (gitWorkflow.step === "generating") {
|
|
5394
6403
|
addGitWorkflowAction("Refresh message preview", () => loadGitWorkflowMessage({ requireFresh: true }), "", false);
|
|
5395
6404
|
} else if (gitWorkflow.step === "message") {
|
|
5396
|
-
|
|
5397
|
-
|
|
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);
|
|
5398
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);
|
|
5399
6417
|
} else if (gitWorkflow.step === "push") {
|
|
5400
|
-
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);
|
|
5401
6427
|
} else if (gitWorkflow.step === "done") {
|
|
5402
6428
|
addGitWorkflowAction("Close", () => setGitWorkflow({ active: false }), "primary", false);
|
|
5403
6429
|
addGitWorkflowAction("Start another", () => startGitWorkflow(), "", false);
|
|
@@ -5447,11 +6473,19 @@ function startGitWorkflow(tabId = activeTabId) {
|
|
|
5447
6473
|
setGitWorkflow({
|
|
5448
6474
|
active: true,
|
|
5449
6475
|
step: "add",
|
|
6476
|
+
process: "stage",
|
|
5450
6477
|
busy: false,
|
|
5451
|
-
output: "Ready to stage all changes with git add .\n\nNative mode is used for g-msg/g-short/g-long: dev/COMMIT message files are read directly and git commit is run without fish.",
|
|
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.",
|
|
5452
6479
|
error: "",
|
|
5453
6480
|
message: null,
|
|
5454
6481
|
messageRequestedAt: 0,
|
|
6482
|
+
branchName: "",
|
|
6483
|
+
branchNameRequestedAt: 0,
|
|
6484
|
+
actionsDone: createGitWorkflowActionsDone(),
|
|
6485
|
+
prMode: false,
|
|
6486
|
+
prBranch: "",
|
|
6487
|
+
pr: null,
|
|
6488
|
+
prRequestedAt: 0,
|
|
5455
6489
|
}, { tabId });
|
|
5456
6490
|
}
|
|
5457
6491
|
|
|
@@ -5459,7 +6493,8 @@ async function cancelGitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
|
5459
6493
|
const tabContext = activeTabContext(tabId);
|
|
5460
6494
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
5461
6495
|
if (!workflow?.active) return;
|
|
5462
|
-
const shouldAbortPi = workflow.step === "generating";
|
|
6496
|
+
const shouldAbortPi = workflow.step === "generating" || workflow.step === "branchNaming" || workflow.step === "prGenerating";
|
|
6497
|
+
if (activeGitPrDialogResolve) resolveGitPrDialog(null);
|
|
5463
6498
|
workflow.runId += 1;
|
|
5464
6499
|
setGitWorkflow({ step: "cancelled", busy: false, error: "", output: `${workflow.output || ""}${workflow.output ? "\n\n" : ""}Cancelled by user.` }, { tabId });
|
|
5465
6500
|
if (shouldAbortPi && isCurrentTabContext(tabContext)) setRunIndicatorActivity("Abort requested; checking whether Pi stopped…");
|
|
@@ -5479,7 +6514,7 @@ async function runGitAdd(tabId = gitWorkflowActionTabId()) {
|
|
|
5479
6514
|
try {
|
|
5480
6515
|
const result = await gitWorkflowRequest("/api/git-workflow/add", { runId, tabId });
|
|
5481
6516
|
if (!result) return;
|
|
5482
|
-
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 });
|
|
5483
6518
|
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
5484
6519
|
} catch (error) {
|
|
5485
6520
|
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "add", { tabId });
|
|
@@ -5543,6 +6578,7 @@ async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId
|
|
|
5543
6578
|
busy: false,
|
|
5544
6579
|
error: "",
|
|
5545
6580
|
message,
|
|
6581
|
+
...(requireFresh && currentWorkflow.messageRequestedAt ? gitWorkflowActionDonePatch(currentWorkflow, "message") : {}),
|
|
5546
6582
|
output: formatCommitMessagePreview(message),
|
|
5547
6583
|
}, { tabId });
|
|
5548
6584
|
} catch (error) {
|
|
@@ -5556,6 +6592,130 @@ async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId
|
|
|
5556
6592
|
}
|
|
5557
6593
|
}
|
|
5558
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
|
+
|
|
5559
6719
|
async function commitGitWorkflow(variant, tabId = gitWorkflowActionTabId()) {
|
|
5560
6720
|
const tabContext = activeTabContext(tabId);
|
|
5561
6721
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
@@ -5565,7 +6725,8 @@ async function commitGitWorkflow(variant, tabId = gitWorkflowActionTabId()) {
|
|
|
5565
6725
|
try {
|
|
5566
6726
|
const result = await gitWorkflowRequest("/api/git-workflow/commit", { body: { variant }, runId, tabId });
|
|
5567
6727
|
if (!result) return;
|
|
5568
|
-
|
|
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 });
|
|
5569
6730
|
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
5570
6731
|
} catch (error) {
|
|
5571
6732
|
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "message", { tabId });
|
|
@@ -5581,13 +6742,129 @@ async function pushGitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
|
5581
6742
|
try {
|
|
5582
6743
|
const result = await gitWorkflowRequest("/api/git-workflow/push", { runId, tabId });
|
|
5583
6744
|
if (!result) return;
|
|
5584
|
-
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 });
|
|
5585
6837
|
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
6838
|
+
await runGitPrPrompt(tabId, { prefixOutput: `${formatGitCommandResult(result)}\n\nPushed PR branch ${result.branch || branch}.` });
|
|
5586
6839
|
} catch (error) {
|
|
5587
6840
|
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "push", { tabId });
|
|
5588
6841
|
}
|
|
5589
6842
|
}
|
|
5590
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
|
+
|
|
5591
6868
|
function resumeGitWorkflowForActiveTab(tabContext = activeTabContext()) {
|
|
5592
6869
|
if (!isCurrentTabContext(tabContext)) return;
|
|
5593
6870
|
bindGitWorkflowToActiveTab();
|
|
@@ -5601,6 +6878,22 @@ function resumeGitWorkflowForActiveTab(tabContext = activeTabContext()) {
|
|
|
5601
6878
|
}
|
|
5602
6879
|
loadGitWorkflowMessage({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId: workflowTabId });
|
|
5603
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
|
+
}
|
|
5604
6897
|
}
|
|
5605
6898
|
|
|
5606
6899
|
function normalizeQueuedMessages(event) {
|
|
@@ -8188,6 +9481,13 @@ function setNativeCommandMenuOpen(open) {
|
|
|
8188
9481
|
elements.nativeCommandMenuButton.parentElement?.classList.toggle("open", nativeCommandMenuOpen);
|
|
8189
9482
|
}
|
|
8190
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
|
+
|
|
8191
9491
|
function setOptionsMenuOpen(open) {
|
|
8192
9492
|
optionsMenuOpen = !!open;
|
|
8193
9493
|
elements.optionsMenuButton.setAttribute("aria-expanded", optionsMenuOpen ? "true" : "false");
|
|
@@ -8435,6 +9735,7 @@ async function installOptionalFeature(featureId) {
|
|
|
8435
9735
|
function runPublishWorkflow(command) {
|
|
8436
9736
|
setComposerActionsOpen(false);
|
|
8437
9737
|
setPublishMenuOpen(false);
|
|
9738
|
+
setAppRunnerMenuOpen(false);
|
|
8438
9739
|
setOptionsMenuOpen(false);
|
|
8439
9740
|
const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0];
|
|
8440
9741
|
const featureId = OPTIONAL_COMMAND_FEATURES.get(commandName);
|
|
@@ -8453,6 +9754,7 @@ async function runNativeCommandMenu(command) {
|
|
|
8453
9754
|
setComposerActionsOpen(false);
|
|
8454
9755
|
setPublishMenuOpen(false);
|
|
8455
9756
|
setNativeCommandMenuOpen(false);
|
|
9757
|
+
setAppRunnerMenuOpen(false);
|
|
8456
9758
|
setOptionsMenuOpen(false);
|
|
8457
9759
|
const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0].toLowerCase();
|
|
8458
9760
|
const featureId = optionalFeatureIdForCommand(commandName);
|
|
@@ -10130,6 +11432,7 @@ async function refreshAll(tabContext = activeTabContext()) {
|
|
|
10130
11432
|
refreshCommands(tabContext),
|
|
10131
11433
|
refreshStats(tabContext),
|
|
10132
11434
|
refreshWorkspace(tabContext),
|
|
11435
|
+
refreshAppRunners(tabContext),
|
|
10133
11436
|
refreshNativeSettings(tabContext),
|
|
10134
11437
|
refreshNetworkStatus(),
|
|
10135
11438
|
refreshWebuiVersion(),
|
|
@@ -10844,6 +12147,11 @@ function handleEvent(event) {
|
|
|
10844
12147
|
case "webui_connected":
|
|
10845
12148
|
setWebuiVersion(event.version);
|
|
10846
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
|
+
}
|
|
10847
12155
|
addEvent(`connected to ${event.tabTitle || "terminal"} for ${event.cwd}`);
|
|
10848
12156
|
scheduleForegroundReconcile("event stream reconnect", 0);
|
|
10849
12157
|
break;
|
|
@@ -10865,6 +12173,7 @@ function handleEvent(event) {
|
|
|
10865
12173
|
case "webui_tab_reloaded":
|
|
10866
12174
|
addEvent(`${event.tabTitle || "terminal"} reloaded`);
|
|
10867
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);
|
|
10868
12177
|
statusEntries.clear();
|
|
10869
12178
|
widgets.clear();
|
|
10870
12179
|
resetOptionalFeatureAvailability();
|
|
@@ -10882,9 +12191,18 @@ function handleEvent(event) {
|
|
|
10882
12191
|
removeQueuedDialogRequests(event.ids || []);
|
|
10883
12192
|
addEvent(`cancelled ${event.ids?.length || 0} pending extension UI request(s)`, "warn");
|
|
10884
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;
|
|
10885
12199
|
case "webui_cwd_changed":
|
|
10886
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();
|
|
10887
12204
|
refreshTabs().catch((error) => addEvent(error.message, "error"));
|
|
12205
|
+
refreshAppRunners(tabContext).catch((error) => addEvent(error.message, "error"));
|
|
10888
12206
|
scheduleRefreshFooter();
|
|
10889
12207
|
break;
|
|
10890
12208
|
case "webui_network_rebinding": {
|
|
@@ -10928,6 +12246,7 @@ function handleEvent(event) {
|
|
|
10928
12246
|
case "agent_end":
|
|
10929
12247
|
addEvent("agent finished");
|
|
10930
12248
|
notifyAgentDone(event.tabId || activeTabId, { activity: event.tabActivity, tabTitle: event.tabTitle });
|
|
12249
|
+
clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
10931
12250
|
if (currentState) currentState = { ...currentState, isStreaming: false };
|
|
10932
12251
|
clearRunIndicatorActivity();
|
|
10933
12252
|
markTabOutputSeen();
|
|
@@ -10941,6 +12260,10 @@ function handleEvent(event) {
|
|
|
10941
12260
|
const workflow = gitWorkflowForTab(workflowTabId, { create: false });
|
|
10942
12261
|
if (workflow?.active && workflow.step === "generating") {
|
|
10943
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 });
|
|
10944
12267
|
}
|
|
10945
12268
|
}
|
|
10946
12269
|
break;
|
|
@@ -10979,15 +12302,22 @@ function handleEvent(event) {
|
|
|
10979
12302
|
break;
|
|
10980
12303
|
case "compaction_start":
|
|
10981
12304
|
if (currentState) currentState = { ...currentState, isCompacting: true };
|
|
12305
|
+
markContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
10982
12306
|
setRunIndicatorActivity(`Compacting context${event.reason ? ` (${event.reason})` : ""}…`);
|
|
10983
12307
|
addEvent(`compaction started (${event.reason})`);
|
|
12308
|
+
renderStatus();
|
|
10984
12309
|
break;
|
|
10985
12310
|
case "compaction_end":
|
|
10986
12311
|
if (currentState) currentState = { ...currentState, isCompacting: false };
|
|
12312
|
+
if (event.aborted) clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
12313
|
+
else markContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
10987
12314
|
addEvent(`compaction ${event.aborted ? "aborted" : "finished"}`);
|
|
10988
12315
|
if (!currentState?.isStreaming) clearRunIndicatorActivity();
|
|
10989
12316
|
markTabOutputSeen();
|
|
12317
|
+
renderStatus();
|
|
12318
|
+
scheduleRefreshState();
|
|
10990
12319
|
scheduleRefreshMessages();
|
|
12320
|
+
scheduleRefreshFooter();
|
|
10991
12321
|
break;
|
|
10992
12322
|
case "auto_retry_start": {
|
|
10993
12323
|
const seconds = Math.max(0, Math.ceil(Number(event.delayMs || 0) / 1000));
|
|
@@ -11031,6 +12361,7 @@ function handleEvent(event) {
|
|
|
11031
12361
|
applyOptimisticThinkingSelection(event.data, tabContext);
|
|
11032
12362
|
} else if (event.command === "new_session") {
|
|
11033
12363
|
const tabId = event.tabId || activeTabId;
|
|
12364
|
+
clearContextUsageUnknownAfterCompaction(tabId);
|
|
11034
12365
|
forgetLastUserPrompt(tabId);
|
|
11035
12366
|
resetGitWorkflowForTab(tabId);
|
|
11036
12367
|
}
|
|
@@ -11087,6 +12418,23 @@ elements.promptListSaveButton?.addEventListener("click", saveDisplayedPromptList
|
|
|
11087
12418
|
elements.promptListRunListButton?.addEventListener("click", () => runDisplayedPromptList());
|
|
11088
12419
|
elements.promptListCloseButton?.addEventListener("click", () => elements.promptListDialog?.close());
|
|
11089
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());
|
|
11090
12438
|
elements.sendFeedbackButton.addEventListener("click", () => submitQueuedActionFeedback());
|
|
11091
12439
|
elements.composer.addEventListener("submit", (event) => {
|
|
11092
12440
|
event.preventDefault();
|
|
@@ -11149,17 +12497,20 @@ elements.gitWorkflowButton.addEventListener("click", () => {
|
|
|
11149
12497
|
const publishMenuContainer = elements.publishButton.parentElement;
|
|
11150
12498
|
elements.publishButton.addEventListener("click", () => {
|
|
11151
12499
|
setNativeCommandMenuOpen(false);
|
|
12500
|
+
setAppRunnerMenuOpen(false);
|
|
11152
12501
|
setOptionsMenuOpen(false);
|
|
11153
12502
|
setPublishMenuOpen(true);
|
|
11154
12503
|
});
|
|
11155
12504
|
publishMenuContainer?.addEventListener("pointerenter", () => {
|
|
11156
12505
|
setNativeCommandMenuOpen(false);
|
|
12506
|
+
setAppRunnerMenuOpen(false);
|
|
11157
12507
|
setOptionsMenuOpen(false);
|
|
11158
12508
|
setPublishMenuOpen(true);
|
|
11159
12509
|
});
|
|
11160
12510
|
publishMenuContainer?.addEventListener("pointerleave", () => setPublishMenuOpen(false));
|
|
11161
12511
|
publishMenuContainer?.addEventListener("focusin", () => {
|
|
11162
12512
|
setNativeCommandMenuOpen(false);
|
|
12513
|
+
setAppRunnerMenuOpen(false);
|
|
11163
12514
|
setOptionsMenuOpen(false);
|
|
11164
12515
|
setPublishMenuOpen(true);
|
|
11165
12516
|
});
|
|
@@ -11171,17 +12522,20 @@ publishMenuContainer?.addEventListener("focusout", () => {
|
|
|
11171
12522
|
const nativeCommandMenuContainer = elements.nativeCommandMenuButton.parentElement;
|
|
11172
12523
|
elements.nativeCommandMenuButton.addEventListener("click", () => {
|
|
11173
12524
|
setPublishMenuOpen(false);
|
|
12525
|
+
setAppRunnerMenuOpen(false);
|
|
11174
12526
|
setOptionsMenuOpen(false);
|
|
11175
12527
|
setNativeCommandMenuOpen(true);
|
|
11176
12528
|
});
|
|
11177
12529
|
nativeCommandMenuContainer?.addEventListener("pointerenter", () => {
|
|
11178
12530
|
setPublishMenuOpen(false);
|
|
12531
|
+
setAppRunnerMenuOpen(false);
|
|
11179
12532
|
setOptionsMenuOpen(false);
|
|
11180
12533
|
setNativeCommandMenuOpen(true);
|
|
11181
12534
|
});
|
|
11182
12535
|
nativeCommandMenuContainer?.addEventListener("pointerleave", () => setNativeCommandMenuOpen(false));
|
|
11183
12536
|
nativeCommandMenuContainer?.addEventListener("focusin", () => {
|
|
11184
12537
|
setPublishMenuOpen(false);
|
|
12538
|
+
setAppRunnerMenuOpen(false);
|
|
11185
12539
|
setOptionsMenuOpen(false);
|
|
11186
12540
|
setNativeCommandMenuOpen(true);
|
|
11187
12541
|
});
|
|
@@ -11190,21 +12544,66 @@ nativeCommandMenuContainer?.addEventListener("focusout", () => {
|
|
|
11190
12544
|
if (!nativeCommandMenuContainer?.contains(document.activeElement)) setNativeCommandMenuOpen(false);
|
|
11191
12545
|
}, 0);
|
|
11192
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
|
+
});
|
|
11193
12589
|
const optionsMenuContainer = elements.optionsMenuButton.parentElement;
|
|
11194
12590
|
elements.optionsMenuButton.addEventListener("click", () => {
|
|
11195
12591
|
setPublishMenuOpen(false);
|
|
11196
12592
|
setNativeCommandMenuOpen(false);
|
|
12593
|
+
setAppRunnerMenuOpen(false);
|
|
11197
12594
|
setOptionsMenuOpen(true);
|
|
11198
12595
|
});
|
|
11199
12596
|
optionsMenuContainer?.addEventListener("pointerenter", () => {
|
|
11200
12597
|
setPublishMenuOpen(false);
|
|
11201
12598
|
setNativeCommandMenuOpen(false);
|
|
12599
|
+
setAppRunnerMenuOpen(false);
|
|
11202
12600
|
setOptionsMenuOpen(true);
|
|
11203
12601
|
});
|
|
11204
12602
|
optionsMenuContainer?.addEventListener("pointerleave", () => setOptionsMenuOpen(false));
|
|
11205
12603
|
optionsMenuContainer?.addEventListener("focusin", () => {
|
|
11206
12604
|
setPublishMenuOpen(false);
|
|
11207
12605
|
setNativeCommandMenuOpen(false);
|
|
12606
|
+
setAppRunnerMenuOpen(false);
|
|
11208
12607
|
setOptionsMenuOpen(true);
|
|
11209
12608
|
});
|
|
11210
12609
|
optionsMenuContainer?.addEventListener("focusout", () => {
|
|
@@ -11224,7 +12623,32 @@ elements.optionsSettingsButton.addEventListener("click", () => runNativeCommandM
|
|
|
11224
12623
|
elements.optionsExportButton.addEventListener("click", () => runNativeCommandMenu("/export"));
|
|
11225
12624
|
elements.optionsForkButton.addEventListener("click", () => runNativeCommandMenu("/fork"));
|
|
11226
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
|
+
});
|
|
11227
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
|
+
});
|
|
11228
12652
|
elements.nativeCommandDialog.addEventListener("close", () => {
|
|
11229
12653
|
elements.nativeCommandSearch.oninput = null;
|
|
11230
12654
|
nativeCommandTabId = null;
|
|
@@ -11320,6 +12744,8 @@ elements.compactButton.addEventListener("click", async () => {
|
|
|
11320
12744
|
elements.compactButton.textContent = "Compacting…";
|
|
11321
12745
|
setRunIndicatorActivity("Requesting context compaction…");
|
|
11322
12746
|
scrollChatToBottom({ force: true });
|
|
12747
|
+
markContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
12748
|
+
renderFooter();
|
|
11323
12749
|
addEvent("manual compaction requested");
|
|
11324
12750
|
await api("/api/compact", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
11325
12751
|
if (!isCurrentTabContext(tabContext)) return;
|
|
@@ -11328,7 +12754,9 @@ elements.compactButton.addEventListener("click", async () => {
|
|
|
11328
12754
|
scheduleRefreshFooter(600, tabContext);
|
|
11329
12755
|
} catch (error) {
|
|
11330
12756
|
if (isCurrentTabContext(tabContext)) {
|
|
12757
|
+
clearContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
11331
12758
|
clearRunIndicatorActivity();
|
|
12759
|
+
renderFooter();
|
|
11332
12760
|
addEvent(error.message, "error");
|
|
11333
12761
|
}
|
|
11334
12762
|
} finally {
|
|
@@ -11440,6 +12868,9 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
11440
12868
|
if (nativeCommandMenuOpen && !event.target?.closest?.(".composer-native-command-menu")) {
|
|
11441
12869
|
setNativeCommandMenuOpen(false);
|
|
11442
12870
|
}
|
|
12871
|
+
if (appRunnerMenuOpen && !event.target?.closest?.(".composer-app-runner-menu")) {
|
|
12872
|
+
setAppRunnerMenuOpen(false);
|
|
12873
|
+
}
|
|
11443
12874
|
if (optionsMenuOpen && !event.target?.closest?.(".composer-options-menu")) {
|
|
11444
12875
|
setOptionsMenuOpen(false);
|
|
11445
12876
|
}
|
|
@@ -11467,7 +12898,7 @@ function isTextEntryTarget(target) {
|
|
|
11467
12898
|
|
|
11468
12899
|
function shouldHandleNativeAppShortcut(event) {
|
|
11469
12900
|
if (event.defaultPrevented) return false;
|
|
11470
|
-
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;
|
|
11471
12902
|
return event.target === elements.promptInput || !isTextEntryTarget(event.target);
|
|
11472
12903
|
}
|
|
11473
12904
|
|
|
@@ -11535,6 +12966,10 @@ window.addEventListener("keydown", (event) => {
|
|
|
11535
12966
|
setNativeCommandMenuOpen(false);
|
|
11536
12967
|
return;
|
|
11537
12968
|
}
|
|
12969
|
+
if (appRunnerMenuOpen) {
|
|
12970
|
+
setAppRunnerMenuOpen(false);
|
|
12971
|
+
return;
|
|
12972
|
+
}
|
|
11538
12973
|
if (optionsMenuOpen) {
|
|
11539
12974
|
setOptionsMenuOpen(false);
|
|
11540
12975
|
return;
|
|
@@ -11571,7 +13006,7 @@ window.addEventListener("keydown", (event) => {
|
|
|
11571
13006
|
}
|
|
11572
13007
|
lastEmptyPromptEscapeTime = now;
|
|
11573
13008
|
}
|
|
11574
|
-
if (
|
|
13009
|
+
if (isSidePanelOverlayView() && !document.body.classList.contains("side-panel-collapsed")) {
|
|
11575
13010
|
setSidePanelCollapsed(true);
|
|
11576
13011
|
return;
|
|
11577
13012
|
}
|
|
@@ -11664,6 +13099,7 @@ elements.promptInput.addEventListener("keydown", (event) => {
|
|
|
11664
13099
|
|
|
11665
13100
|
elements.promptInput.addEventListener("input", () => {
|
|
11666
13101
|
resetPromptHistoryNavigation();
|
|
13102
|
+
if (moveLongPromptInputToAttachment()) return;
|
|
11667
13103
|
resizePromptInput();
|
|
11668
13104
|
renderCommandSuggestions();
|
|
11669
13105
|
});
|
|
@@ -11692,6 +13128,7 @@ resizePromptInput();
|
|
|
11692
13128
|
focusPromptInput({ defer: true });
|
|
11693
13129
|
updateComposerModeButtons();
|
|
11694
13130
|
updateOptionalFeatureAvailability();
|
|
13131
|
+
renderAppRunnerControls();
|
|
11695
13132
|
renderLoadedPromptListPreview();
|
|
11696
13133
|
loadLastUserPromptCache();
|
|
11697
13134
|
loadPromptHistoryCache();
|
|
@@ -11711,6 +13148,7 @@ bindSidePanelSectionToggles();
|
|
|
11711
13148
|
restoreSidePanelState();
|
|
11712
13149
|
initializeCodexUsage();
|
|
11713
13150
|
bindMobileViewChanges();
|
|
13151
|
+
bindSidePanelOverlayViewChanges();
|
|
11714
13152
|
registerPwaServiceWorker();
|
|
11715
13153
|
renderServerOfflinePanel();
|
|
11716
13154
|
initializeTabs().catch((error) => addEvent(error.message, "error"));
|