@firstpick/pi-package-webui 0.1.7 โ 0.1.9
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 +14 -24
- package/bin/pi-webui.mjs +311 -1
- package/images/Guardrails_v0.1.7.png +0 -0
- package/images/Guided_GitWorkflow_v0.1.7.png +0 -0
- package/images/Main_Window_v0.1.7.png +0 -0
- package/images/Matrix_Theme_v0.1.7.png +0 -0
- package/package.json +3 -1
- package/public/app.js +452 -97
- package/public/index.html +14 -0
- package/public/styles.css +77 -0
- package/tests/mobile-static.test.mjs +12 -0
package/public/app.js
CHANGED
|
@@ -63,6 +63,8 @@ const elements = {
|
|
|
63
63
|
agentDoneNotificationsToggle: $("#agentDoneNotificationsToggle"),
|
|
64
64
|
agentDoneNotificationsStatus: $("#agentDoneNotificationsStatus"),
|
|
65
65
|
optionalFeaturesBox: $("#optionalFeaturesBox"),
|
|
66
|
+
codexUsageBox: $("#codexUsageBox"),
|
|
67
|
+
refreshCodexUsageButton: $("#refreshCodexUsageButton"),
|
|
66
68
|
toggleSidePanelButton: $("#toggleSidePanelButton"),
|
|
67
69
|
sidePanelExpandButton: $("#sidePanelExpandButton"),
|
|
68
70
|
sidePanelBackdrop: $("#sidePanelBackdrop"),
|
|
@@ -146,6 +148,11 @@ let pathSuggestAbortController = null;
|
|
|
146
148
|
let latestStats = null;
|
|
147
149
|
let latestWorkspace = null;
|
|
148
150
|
let latestNetwork = null;
|
|
151
|
+
let latestCodexUsage = null;
|
|
152
|
+
let codexUsageError = null;
|
|
153
|
+
let codexUsageLoading = false;
|
|
154
|
+
let refreshCodexUsageTimer = null;
|
|
155
|
+
let codexUsageRenderTimer = null;
|
|
149
156
|
let backendOffline = false;
|
|
150
157
|
let backendOfflineNoticeShown = false;
|
|
151
158
|
let latestMessages = [];
|
|
@@ -220,6 +227,8 @@ const STICKY_USER_PROMPT_TOP_GAP_PX = 12;
|
|
|
220
227
|
const CHAT_FOLLOW_SETTLE_DELAY_MS = 80;
|
|
221
228
|
const CHAT_PROGRAMMATIC_SCROLL_GRACE_MS = 500;
|
|
222
229
|
const CHAT_USER_SCROLL_INTENT_MS = 700;
|
|
230
|
+
const CODEX_USAGE_REFRESH_MS = 5 * 60 * 1000;
|
|
231
|
+
const CODEX_USAGE_RENDER_TICK_MS = 30 * 1000;
|
|
223
232
|
const RUN_INDICATOR_TICK_MS = 1000;
|
|
224
233
|
const RUN_INDICATOR_START_GRACE_MS = 2500;
|
|
225
234
|
const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
|
|
@@ -319,16 +328,56 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
|
319
328
|
const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate"]);
|
|
320
329
|
const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models"]);
|
|
321
330
|
const optionalFeatureInstallInProgress = new Set();
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
331
|
+
|
|
332
|
+
function createGitWorkflowState() {
|
|
333
|
+
return {
|
|
334
|
+
active: false,
|
|
335
|
+
step: "idle",
|
|
336
|
+
busy: false,
|
|
337
|
+
runId: 0,
|
|
338
|
+
output: "",
|
|
339
|
+
error: "",
|
|
340
|
+
message: null,
|
|
341
|
+
messageRequestedAt: 0,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const gitWorkflowsByTab = new Map();
|
|
346
|
+
let gitWorkflow = createGitWorkflowState();
|
|
347
|
+
|
|
348
|
+
function gitWorkflowForTab(tabId = activeTabId, { create = true } = {}) {
|
|
349
|
+
if (!tabId) return null;
|
|
350
|
+
let workflow = gitWorkflowsByTab.get(tabId);
|
|
351
|
+
if (!workflow && create) {
|
|
352
|
+
workflow = createGitWorkflowState();
|
|
353
|
+
gitWorkflowsByTab.set(tabId, workflow);
|
|
354
|
+
}
|
|
355
|
+
return workflow || null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function bindGitWorkflowToActiveTab() {
|
|
359
|
+
gitWorkflow = gitWorkflowForTab(activeTabId) || createGitWorkflowState();
|
|
360
|
+
return gitWorkflow;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function resetGitWorkflowForTab(tabId = activeTabId) {
|
|
364
|
+
if (!tabId) return;
|
|
365
|
+
gitWorkflowsByTab.set(tabId, createGitWorkflowState());
|
|
366
|
+
if (tabId === activeTabId) {
|
|
367
|
+
bindGitWorkflowToActiveTab();
|
|
368
|
+
renderGitWorkflow();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function clearGitWorkflowForTab(tabId) {
|
|
373
|
+
if (!tabId) return;
|
|
374
|
+
gitWorkflowsByTab.delete(tabId);
|
|
375
|
+
if (tabId === activeTabId) {
|
|
376
|
+
bindGitWorkflowToActiveTab();
|
|
377
|
+
renderGitWorkflow();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
332
381
|
const GIT_WORKFLOW_STEPS = ["Stage", "Message", "Commit", "Push"];
|
|
333
382
|
const ACTION_FEEDBACK_REACTIONS = {
|
|
334
383
|
up: { icon: "๐", label: "Good job", title: "Good job!" },
|
|
@@ -1838,6 +1887,7 @@ function setActiveTabId(tabId, { remember = false } = {}) {
|
|
|
1838
1887
|
const nextTabId = tabId || null;
|
|
1839
1888
|
if (nextTabId !== activeTabId) activeTabGeneration += 1;
|
|
1840
1889
|
activeTabId = nextTabId;
|
|
1890
|
+
bindGitWorkflowToActiveTab();
|
|
1841
1891
|
if (remember) rememberActiveTab();
|
|
1842
1892
|
return activeTabContext(nextTabId);
|
|
1843
1893
|
}
|
|
@@ -1914,6 +1964,7 @@ function syncTabMetadata(nextTabs = []) {
|
|
|
1914
1964
|
tabActivities.delete(tabId);
|
|
1915
1965
|
tabSeenCompletionSerials.delete(tabId);
|
|
1916
1966
|
actionFeedbackByTab.delete(tabId);
|
|
1967
|
+
clearGitWorkflowForTab(tabId);
|
|
1917
1968
|
}
|
|
1918
1969
|
}
|
|
1919
1970
|
}
|
|
@@ -2094,6 +2145,15 @@ function restoreStoredTabId() {
|
|
|
2094
2145
|
}
|
|
2095
2146
|
}
|
|
2096
2147
|
|
|
2148
|
+
function requestedTabIdFromUrl() {
|
|
2149
|
+
try {
|
|
2150
|
+
const params = new URLSearchParams(window.location.search);
|
|
2151
|
+
return params.get("tab") || params.get("tabId") || null;
|
|
2152
|
+
} catch {
|
|
2153
|
+
return null;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2097
2157
|
function updateDocumentTitle() {
|
|
2098
2158
|
const tab = activeTab();
|
|
2099
2159
|
document.title = tab ? `Pi Web UI ยท ${tab.title}` : "Pi Web UI";
|
|
@@ -2179,15 +2239,7 @@ function resetActiveTabUi() {
|
|
|
2179
2239
|
cancelPendingDialogs();
|
|
2180
2240
|
if (elements.nativeCommandDialog.open) closeNativeCommandDialog();
|
|
2181
2241
|
if (pathPickerState) closePathPicker(null);
|
|
2182
|
-
|
|
2183
|
-
active: false,
|
|
2184
|
-
step: "idle",
|
|
2185
|
-
busy: false,
|
|
2186
|
-
output: "",
|
|
2187
|
-
error: "",
|
|
2188
|
-
message: null,
|
|
2189
|
-
messageRequestedAt: 0,
|
|
2190
|
-
});
|
|
2242
|
+
bindGitWorkflowToActiveTab();
|
|
2191
2243
|
resetChatOutput();
|
|
2192
2244
|
elements.stateDetails.replaceChildren();
|
|
2193
2245
|
elements.eventLog.replaceChildren();
|
|
@@ -2438,9 +2490,10 @@ async function refreshTabs({ selectStored = false } = {}) {
|
|
|
2438
2490
|
syncTabMetadata(tabs);
|
|
2439
2491
|
syncBlockedTabNotificationsFromTabs(tabs, previousTabs);
|
|
2440
2492
|
syncAgentDoneNotificationsFromTabs(tabs, previousTabs);
|
|
2493
|
+
const requested = selectStored ? requestedTabIdFromUrl() : null;
|
|
2441
2494
|
const stored = selectStored ? restoreStoredTabId() : null;
|
|
2442
2495
|
if (!activeTabId || !tabs.some((tab) => tab.id === activeTabId)) {
|
|
2443
|
-
setActiveTabId((stored && tabs.some((tab) => tab.id === stored) ? stored : tabs[0]?.id) || null, { remember: true });
|
|
2496
|
+
setActiveTabId((requested && tabs.some((tab) => tab.id === requested) ? requested : stored && tabs.some((tab) => tab.id === stored) ? stored : tabs[0]?.id) || null, { remember: true });
|
|
2444
2497
|
}
|
|
2445
2498
|
rememberServerStartCwd(tabs.find((tab) => tab.id === activeTabId)?.cwd || tabs[0]?.cwd);
|
|
2446
2499
|
renderTabs();
|
|
@@ -2532,6 +2585,7 @@ async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } =
|
|
|
2532
2585
|
for (const id of closedIds) {
|
|
2533
2586
|
tabDrafts.delete(id);
|
|
2534
2587
|
clearAttachments(id);
|
|
2588
|
+
clearGitWorkflowForTab(id);
|
|
2535
2589
|
}
|
|
2536
2590
|
clearOpenTerminalTabGroup(null, { force: true });
|
|
2537
2591
|
|
|
@@ -3436,6 +3490,7 @@ async function changeActiveTabCwd() {
|
|
|
3436
3490
|
return;
|
|
3437
3491
|
}
|
|
3438
3492
|
const nextContext = setActiveTabId(response.data?.tab?.id || activeTabId);
|
|
3493
|
+
resetGitWorkflowForTab(nextContext.tabId);
|
|
3439
3494
|
resetActiveTabUi();
|
|
3440
3495
|
renderTabs();
|
|
3441
3496
|
restoreActiveDraft();
|
|
@@ -3538,6 +3593,197 @@ function scheduleRefreshFooter(delay = 300, tabContext = activeTabContext()) {
|
|
|
3538
3593
|
}, delay);
|
|
3539
3594
|
}
|
|
3540
3595
|
|
|
3596
|
+
function formatCodexPlanType(value) {
|
|
3597
|
+
const text = String(value || "").trim();
|
|
3598
|
+
if (!text) return "unknown plan";
|
|
3599
|
+
return text.replace(/[_-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
function formatCodexPercent(value) {
|
|
3603
|
+
const number = Number(value);
|
|
3604
|
+
return Number.isFinite(number) ? `${Math.max(0, Math.min(100, Math.round(number)))}%` : "โ";
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
function codexWindowDurationMinutes(window) {
|
|
3608
|
+
const minutes = Number(window?.windowDurationMins);
|
|
3609
|
+
if (Number.isFinite(minutes) && minutes > 0) return minutes;
|
|
3610
|
+
const seconds = Number(window?.windowDurationSeconds);
|
|
3611
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds / 60 : null;
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
function formatCodexWindowDuration(window) {
|
|
3615
|
+
const minutes = codexWindowDurationMinutes(window);
|
|
3616
|
+
if (!minutes) return "window";
|
|
3617
|
+
if (minutes >= 280 && minutes <= 320) return "5h window";
|
|
3618
|
+
if (minutes >= 9500 && minutes <= 10550) return "weekly window";
|
|
3619
|
+
if (minutes >= 60 * 24) {
|
|
3620
|
+
const days = minutes / (60 * 24);
|
|
3621
|
+
return `${days >= 10 ? Math.round(days) : Number(days.toFixed(1))}d window`;
|
|
3622
|
+
}
|
|
3623
|
+
if (minutes >= 60) {
|
|
3624
|
+
const hours = minutes / 60;
|
|
3625
|
+
return `${Number.isInteger(hours) ? hours : Number(hours.toFixed(1))}h window`;
|
|
3626
|
+
}
|
|
3627
|
+
return `${Math.round(minutes)}m window`;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
function formatDurationParts(milliseconds) {
|
|
3631
|
+
if (!Number.isFinite(Number(milliseconds))) return "now";
|
|
3632
|
+
const totalMinutes = Math.max(0, Math.ceil(Number(milliseconds) / 60000));
|
|
3633
|
+
if (totalMinutes <= 1) return "<1m";
|
|
3634
|
+
if (totalMinutes < 60) return `${totalMinutes}m`;
|
|
3635
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
3636
|
+
const minutes = totalMinutes % 60;
|
|
3637
|
+
if (hours < 48) return minutes ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
3638
|
+
const days = Math.floor(hours / 24);
|
|
3639
|
+
const remHours = hours % 24;
|
|
3640
|
+
return remHours ? `${days}d ${remHours}h` : `${days}d`;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
function codexWindowResetDate(window) {
|
|
3644
|
+
const resetAt = window?.resetsAt ? new Date(window.resetsAt) : null;
|
|
3645
|
+
if (resetAt && Number.isFinite(resetAt.getTime())) return resetAt;
|
|
3646
|
+
const resetAfterSeconds = Number(window?.resetAfterSeconds);
|
|
3647
|
+
if (Number.isFinite(resetAfterSeconds) && resetAfterSeconds >= 0) return new Date(Date.now() + resetAfterSeconds * 1000);
|
|
3648
|
+
return null;
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
function formatCodexReset(window) {
|
|
3652
|
+
const resetDate = codexWindowResetDate(window);
|
|
3653
|
+
if (!resetDate) return "reset unknown";
|
|
3654
|
+
const diff = resetDate.getTime() - Date.now();
|
|
3655
|
+
if (diff <= 0) return "resetting now";
|
|
3656
|
+
return `resets in ${formatDurationParts(diff)}`;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
function codexSnapshotName(snapshot) {
|
|
3660
|
+
return snapshot?.limitName || snapshot?.limitId || "codex";
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
function codexUsageBuckets(data) {
|
|
3664
|
+
const buckets = [];
|
|
3665
|
+
const selected = data?.selected || data?.rateLimits || null;
|
|
3666
|
+
const snapshots = Array.isArray(data?.snapshots) ? data.snapshots : selected ? [selected] : [];
|
|
3667
|
+
const selectedKey = selected?.limitId || selected?.limitName || "codex";
|
|
3668
|
+
const pushWindow = (snapshot, kind, window, { prefix } = {}) => {
|
|
3669
|
+
if (!window) return;
|
|
3670
|
+
const durationLabel = formatCodexWindowDuration(window);
|
|
3671
|
+
const baseLabel = kind === "secondary" && durationLabel === "window" ? "secondary window" : durationLabel;
|
|
3672
|
+
buckets.push({
|
|
3673
|
+
key: `${snapshot?.limitId || snapshot?.limitName || buckets.length}-${kind}`,
|
|
3674
|
+
label: prefix ? `${prefix} ยท ${baseLabel}` : baseLabel,
|
|
3675
|
+
window,
|
|
3676
|
+
});
|
|
3677
|
+
};
|
|
3678
|
+
|
|
3679
|
+
if (selected) {
|
|
3680
|
+
pushWindow(selected, "primary", selected.primary);
|
|
3681
|
+
pushWindow(selected, "secondary", selected.secondary);
|
|
3682
|
+
}
|
|
3683
|
+
for (const snapshot of snapshots) {
|
|
3684
|
+
const key = snapshot?.limitId || snapshot?.limitName;
|
|
3685
|
+
if (!snapshot || snapshot === selected || key === selectedKey) continue;
|
|
3686
|
+
const name = codexSnapshotName(snapshot);
|
|
3687
|
+
pushWindow(snapshot, "primary", snapshot.primary, { prefix: name });
|
|
3688
|
+
pushWindow(snapshot, "secondary", snapshot.secondary, { prefix: name });
|
|
3689
|
+
}
|
|
3690
|
+
return buckets.slice(0, 6);
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
function renderCodexUsage() {
|
|
3694
|
+
const box = elements.codexUsageBox;
|
|
3695
|
+
if (!box) return;
|
|
3696
|
+
if (elements.refreshCodexUsageButton) {
|
|
3697
|
+
elements.refreshCodexUsageButton.disabled = codexUsageLoading;
|
|
3698
|
+
elements.refreshCodexUsageButton.textContent = codexUsageLoading ? "Refreshingโฆ" : "Refresh usage";
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
box.replaceChildren();
|
|
3702
|
+
box.classList.toggle("muted", !latestCodexUsage);
|
|
3703
|
+
|
|
3704
|
+
if (!latestCodexUsage && codexUsageLoading) {
|
|
3705
|
+
box.textContent = "Checking Codex usageโฆ";
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
if (!latestCodexUsage && codexUsageError) {
|
|
3709
|
+
const title = make("div", "codex-usage-unavailable", "Usage unavailable");
|
|
3710
|
+
const detail = make("div", "codex-usage-detail", codexUsageError.message || String(codexUsageError));
|
|
3711
|
+
box.append(title, detail);
|
|
3712
|
+
return;
|
|
3713
|
+
}
|
|
3714
|
+
if (!latestCodexUsage) {
|
|
3715
|
+
box.textContent = "Codex usage has not loaded yet.";
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
const header = make("div", "codex-usage-summary");
|
|
3720
|
+
header.append(
|
|
3721
|
+
make("span", "codex-usage-plan", formatCodexPlanType(latestCodexUsage.planType)),
|
|
3722
|
+
make("span", "codex-usage-fetched", latestCodexUsage.fetchedAt ? `updated ${formatDurationParts(Date.now() - new Date(latestCodexUsage.fetchedAt).getTime())} ago` : "updated now"),
|
|
3723
|
+
);
|
|
3724
|
+
box.append(header);
|
|
3725
|
+
|
|
3726
|
+
const buckets = codexUsageBuckets(latestCodexUsage);
|
|
3727
|
+
if (buckets.length === 0) {
|
|
3728
|
+
box.append(make("div", "codex-usage-detail", "No Codex rate-limit windows were returned."));
|
|
3729
|
+
} else {
|
|
3730
|
+
for (const bucket of buckets) {
|
|
3731
|
+
const usedPercent = Number(bucket.window?.usedPercent);
|
|
3732
|
+
const fillPercent = Number.isFinite(usedPercent) ? Math.max(0, Math.min(100, usedPercent)) : 0;
|
|
3733
|
+
const item = make("div", "codex-usage-bucket");
|
|
3734
|
+
const row = make("div", "codex-usage-row");
|
|
3735
|
+
row.append(
|
|
3736
|
+
make("span", "codex-usage-label", bucket.label),
|
|
3737
|
+
make("strong", "codex-usage-percent", formatCodexPercent(bucket.window?.usedPercent)),
|
|
3738
|
+
);
|
|
3739
|
+
const meter = make("div", "codex-usage-meter");
|
|
3740
|
+
const fill = make("span", "codex-usage-meter-fill");
|
|
3741
|
+
fill.style.width = `${fillPercent}%`;
|
|
3742
|
+
meter.append(fill);
|
|
3743
|
+
item.append(row, meter, make("div", "codex-usage-reset", formatCodexReset(bucket.window)));
|
|
3744
|
+
box.append(item);
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
if (latestCodexUsage.rateLimitReachedType) {
|
|
3749
|
+
box.append(make("div", "codex-usage-warning", `Limit status: ${latestCodexUsage.rateLimitReachedType}`));
|
|
3750
|
+
}
|
|
3751
|
+
if (codexUsageError) {
|
|
3752
|
+
box.append(make("div", "codex-usage-detail", `Latest refresh failed: ${codexUsageError.message || codexUsageError}`));
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
async function refreshCodexUsage({ forceAuthRefresh = false } = {}) {
|
|
3757
|
+
if (codexUsageLoading) return;
|
|
3758
|
+
codexUsageLoading = true;
|
|
3759
|
+
renderCodexUsage();
|
|
3760
|
+
try {
|
|
3761
|
+
const suffix = forceAuthRefresh ? "?refresh=1" : "";
|
|
3762
|
+
const response = await api(`/api/codex-usage${suffix}`, { scoped: false });
|
|
3763
|
+
latestCodexUsage = response.data || null;
|
|
3764
|
+
codexUsageError = null;
|
|
3765
|
+
} catch (error) {
|
|
3766
|
+
codexUsageError = error;
|
|
3767
|
+
} finally {
|
|
3768
|
+
codexUsageLoading = false;
|
|
3769
|
+
renderCodexUsage();
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
function scheduleRefreshCodexUsage(delay = CODEX_USAGE_REFRESH_MS) {
|
|
3774
|
+
clearTimeout(refreshCodexUsageTimer);
|
|
3775
|
+
refreshCodexUsageTimer = setTimeout(() => {
|
|
3776
|
+
refreshCodexUsage().finally(() => scheduleRefreshCodexUsage());
|
|
3777
|
+
}, delay);
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
function initializeCodexUsage() {
|
|
3781
|
+
renderCodexUsage();
|
|
3782
|
+
refreshCodexUsage().finally(() => scheduleRefreshCodexUsage());
|
|
3783
|
+
clearInterval(codexUsageRenderTimer);
|
|
3784
|
+
codexUsageRenderTimer = setInterval(renderCodexUsage, CODEX_USAGE_RENDER_TICK_MS);
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3541
3787
|
function renderStatus() {
|
|
3542
3788
|
const state = currentState;
|
|
3543
3789
|
updateComposerModeButtons();
|
|
@@ -3964,18 +4210,27 @@ function renderWidgets() {
|
|
|
3964
4210
|
}
|
|
3965
4211
|
}
|
|
3966
4212
|
|
|
3967
|
-
function setGitWorkflow(patch) {
|
|
3968
|
-
|
|
3969
|
-
|
|
4213
|
+
function setGitWorkflow(patch, { tabId = activeTabId } = {}) {
|
|
4214
|
+
const workflow = gitWorkflowForTab(tabId);
|
|
4215
|
+
if (!workflow) return null;
|
|
4216
|
+
Object.assign(workflow, patch);
|
|
4217
|
+
if (tabId === activeTabId) {
|
|
4218
|
+
gitWorkflow = workflow;
|
|
4219
|
+
renderGitWorkflow();
|
|
4220
|
+
}
|
|
4221
|
+
return workflow;
|
|
3970
4222
|
}
|
|
3971
4223
|
|
|
3972
|
-
function isCurrentGitWorkflowRun(runId) {
|
|
3973
|
-
|
|
4224
|
+
function isCurrentGitWorkflowRun(runId, tabId = activeTabId) {
|
|
4225
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4226
|
+
return !!workflow?.active && workflow.runId === runId;
|
|
3974
4227
|
}
|
|
3975
4228
|
|
|
3976
|
-
function appendGitWorkflowOutput(text) {
|
|
3977
|
-
const
|
|
3978
|
-
|
|
4229
|
+
function appendGitWorkflowOutput(text, { tabId = activeTabId } = {}) {
|
|
4230
|
+
const workflow = gitWorkflowForTab(tabId);
|
|
4231
|
+
if (!workflow) return;
|
|
4232
|
+
const next = `${workflow.output || ""}${workflow.output ? "\n" : ""}${text}`;
|
|
4233
|
+
setGitWorkflow({ output: next.slice(-60000) }, { tabId });
|
|
3979
4234
|
}
|
|
3980
4235
|
|
|
3981
4236
|
function formatGitCommandResult(result) {
|
|
@@ -4078,9 +4333,11 @@ function renderGitWorkflow() {
|
|
|
4078
4333
|
}
|
|
4079
4334
|
}
|
|
4080
4335
|
|
|
4081
|
-
async function gitWorkflowRequest(path, { method = "POST", body = {}, runId =
|
|
4082
|
-
const
|
|
4083
|
-
|
|
4336
|
+
async function gitWorkflowRequest(path, { method = "POST", body = {}, runId, tabId = activeTabId } = {}) {
|
|
4337
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4338
|
+
const expectedRunId = runId ?? workflow?.runId;
|
|
4339
|
+
const response = await api(path, method === "GET" ? { method, tabId } : { method, body, tabId });
|
|
4340
|
+
if (expectedRunId !== undefined && !isCurrentGitWorkflowRun(expectedRunId, tabId)) return null;
|
|
4084
4341
|
if (!response.ok) {
|
|
4085
4342
|
const detail = response.data ? `\n\n${formatGitCommandResult(response.data)}` : "";
|
|
4086
4343
|
throw new Error(`${response.error || "Git workflow request failed"}${detail}`);
|
|
@@ -4088,27 +4345,32 @@ async function gitWorkflowRequest(path, { method = "POST", body = {}, runId = gi
|
|
|
4088
4345
|
return response.data;
|
|
4089
4346
|
}
|
|
4090
4347
|
|
|
4091
|
-
function failGitWorkflow(error, step =
|
|
4348
|
+
function failGitWorkflow(error, step, { tabId = activeTabId } = {}) {
|
|
4349
|
+
const workflow = gitWorkflowForTab(tabId);
|
|
4350
|
+
if (!workflow) return;
|
|
4092
4351
|
const message = error?.message || String(error);
|
|
4093
4352
|
setGitWorkflow({
|
|
4094
|
-
step,
|
|
4353
|
+
step: step || workflow.step || "error",
|
|
4095
4354
|
busy: false,
|
|
4096
4355
|
error: message,
|
|
4097
|
-
output: `${
|
|
4098
|
-
});
|
|
4356
|
+
output: `${workflow.output || ""}${workflow.output ? "\n\n" : ""}ERROR: ${message}`.slice(-60000),
|
|
4357
|
+
}, { tabId });
|
|
4099
4358
|
}
|
|
4100
4359
|
|
|
4101
4360
|
function startGitWorkflow() {
|
|
4361
|
+
const tabId = activeTabId;
|
|
4362
|
+
if (!tabId) return;
|
|
4102
4363
|
if (!isOptionalFeatureEnabled("gitWorkflow")) {
|
|
4103
|
-
const tabContext = activeTabContext();
|
|
4364
|
+
const tabContext = activeTabContext(tabId);
|
|
4104
4365
|
addEvent(commandUnavailableMessage("git-staged-msg"), "warn");
|
|
4105
4366
|
refreshCommands(tabContext).catch((error) => {
|
|
4106
4367
|
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
4107
4368
|
});
|
|
4108
4369
|
return;
|
|
4109
4370
|
}
|
|
4110
|
-
|
|
4111
|
-
|
|
4371
|
+
const workflow = gitWorkflowForTab(tabId);
|
|
4372
|
+
if (workflow.active && !["done", "cancelled", "error"].includes(workflow.step) && !confirm("Restart the active git workflow?")) return;
|
|
4373
|
+
workflow.runId += 1;
|
|
4112
4374
|
setGitWorkflow({
|
|
4113
4375
|
active: true,
|
|
4114
4376
|
step: "add",
|
|
@@ -4117,40 +4379,52 @@ function startGitWorkflow() {
|
|
|
4117
4379
|
error: "",
|
|
4118
4380
|
message: null,
|
|
4119
4381
|
messageRequestedAt: 0,
|
|
4120
|
-
});
|
|
4382
|
+
}, { tabId });
|
|
4121
4383
|
}
|
|
4122
4384
|
|
|
4123
4385
|
async function cancelGitWorkflow() {
|
|
4124
|
-
const
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
if (
|
|
4386
|
+
const tabId = activeTabId;
|
|
4387
|
+
const tabContext = activeTabContext(tabId);
|
|
4388
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4389
|
+
if (!workflow?.active) return;
|
|
4390
|
+
const shouldAbortPi = workflow.step === "generating";
|
|
4391
|
+
workflow.runId += 1;
|
|
4392
|
+
setGitWorkflow({ step: "cancelled", busy: false, error: "", output: `${workflow.output || ""}${workflow.output ? "\n\n" : ""}Cancelled by user.` }, { tabId });
|
|
4393
|
+
if (shouldAbortPi && isCurrentTabContext(tabContext)) setRunIndicatorActivity("Abort requested; checking whether Pi stoppedโฆ");
|
|
4128
4394
|
await Promise.allSettled([
|
|
4129
|
-
api("/api/git-workflow/cancel", { method: "POST", body: {} }),
|
|
4130
|
-
shouldAbortPi ? api("/api/abort", { method: "POST", body: {} }) : Promise.resolve(),
|
|
4395
|
+
api("/api/git-workflow/cancel", { method: "POST", body: {}, tabId }),
|
|
4396
|
+
shouldAbortPi ? api("/api/abort", { method: "POST", body: {}, tabId }) : Promise.resolve(),
|
|
4131
4397
|
]);
|
|
4132
|
-
if (shouldAbortPi) scheduleAbortStateChecks();
|
|
4398
|
+
if (shouldAbortPi && isCurrentTabContext(tabContext)) scheduleAbortStateChecks();
|
|
4133
4399
|
}
|
|
4134
4400
|
|
|
4135
4401
|
async function runGitAdd() {
|
|
4136
|
-
const
|
|
4137
|
-
|
|
4402
|
+
const tabId = activeTabId;
|
|
4403
|
+
const tabContext = activeTabContext(tabId);
|
|
4404
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4405
|
+
if (!workflow) return;
|
|
4406
|
+
const runId = workflow.runId;
|
|
4407
|
+
setGitWorkflow({ step: "add", busy: true, error: "", output: "Running git add ." }, { tabId });
|
|
4138
4408
|
try {
|
|
4139
|
-
const result = await gitWorkflowRequest("/api/git-workflow/add", { runId });
|
|
4409
|
+
const result = await gitWorkflowRequest("/api/git-workflow/add", { runId, tabId });
|
|
4140
4410
|
if (!result) return;
|
|
4141
|
-
setGitWorkflow({ step: "generate", busy: false, output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` });
|
|
4142
|
-
scheduleRefreshFooter();
|
|
4411
|
+
setGitWorkflow({ step: "generate", busy: false, output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` }, { tabId });
|
|
4412
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
4143
4413
|
} catch (error) {
|
|
4144
|
-
if (isCurrentGitWorkflowRun(runId)) failGitWorkflow(error, "add");
|
|
4414
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "add", { tabId });
|
|
4145
4415
|
}
|
|
4146
4416
|
}
|
|
4147
4417
|
|
|
4148
4418
|
async function runGitMessagePrompt() {
|
|
4419
|
+
const tabId = activeTabId;
|
|
4420
|
+
const tabContext = activeTabContext(tabId);
|
|
4149
4421
|
if (currentState?.isStreaming) {
|
|
4150
|
-
failGitWorkflow(new Error("Pi is currently running. Wait for it to finish or abort before generating a staged commit message."), "generate");
|
|
4422
|
+
failGitWorkflow(new Error("Pi is currently running. Wait for it to finish or abort before generating a staged commit message."), "generate", { tabId });
|
|
4151
4423
|
return;
|
|
4152
4424
|
}
|
|
4153
|
-
const
|
|
4425
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4426
|
+
if (!workflow) return;
|
|
4427
|
+
const runId = workflow.runId;
|
|
4154
4428
|
const requestedAt = Date.now();
|
|
4155
4429
|
setGitWorkflow({
|
|
4156
4430
|
step: "generating",
|
|
@@ -4158,32 +4432,37 @@ async function runGitMessagePrompt() {
|
|
|
4158
4432
|
error: "",
|
|
4159
4433
|
messageRequestedAt: requestedAt,
|
|
4160
4434
|
output: "Sending /git-staged-msg to Pi.\n\nCancel will request Pi abort.",
|
|
4161
|
-
});
|
|
4162
|
-
setRunIndicatorActivity("Sending /git-staged-msg to Piโฆ");
|
|
4435
|
+
}, { tabId });
|
|
4436
|
+
if (isCurrentTabContext(tabContext)) setRunIndicatorActivity("Sending /git-staged-msg to Piโฆ");
|
|
4163
4437
|
try {
|
|
4164
|
-
await api("/api/prompt", { method: "POST", body: { message: "/git-staged-msg" } });
|
|
4165
|
-
if (!isCurrentGitWorkflowRun(runId)) return;
|
|
4166
|
-
appendGitWorkflowOutput("/git-staged-msg accepted. Waiting for agent_end, then the message files will be loaded.");
|
|
4167
|
-
scheduleRefreshState();
|
|
4438
|
+
await api("/api/prompt", { method: "POST", body: { message: "/git-staged-msg" }, tabId });
|
|
4439
|
+
if (!isCurrentGitWorkflowRun(runId, tabId)) return;
|
|
4440
|
+
appendGitWorkflowOutput("/git-staged-msg accepted. Waiting for agent_end, then the message files will be loaded.", { tabId });
|
|
4441
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshState(120, tabContext);
|
|
4168
4442
|
setTimeout(() => {
|
|
4169
|
-
|
|
4170
|
-
|
|
4443
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
4444
|
+
if (isCurrentTabContext(tabContext) && isCurrentGitWorkflowRun(runId, tabId) && currentWorkflow?.step === "generating" && !currentState?.isStreaming) {
|
|
4445
|
+
loadGitWorkflowMessage({ requireFresh: true, retries: 1, runId, tabId });
|
|
4171
4446
|
}
|
|
4172
4447
|
}, 2500);
|
|
4173
4448
|
} catch (error) {
|
|
4174
|
-
if (isCurrentGitWorkflowRun(runId)) {
|
|
4175
|
-
clearRunIndicatorActivity();
|
|
4176
|
-
failGitWorkflow(error, "generate");
|
|
4449
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) {
|
|
4450
|
+
if (isCurrentTabContext(tabContext)) clearRunIndicatorActivity();
|
|
4451
|
+
failGitWorkflow(error, "generate", { tabId });
|
|
4177
4452
|
}
|
|
4178
4453
|
}
|
|
4179
4454
|
}
|
|
4180
4455
|
|
|
4181
|
-
async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId =
|
|
4456
|
+
async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId, tabId = activeTabId } = {}) {
|
|
4457
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4458
|
+
const expectedRunId = runId ?? workflow?.runId;
|
|
4182
4459
|
try {
|
|
4183
|
-
const message = await gitWorkflowRequest("/api/git-workflow/message", { method: "GET", runId });
|
|
4460
|
+
const message = await gitWorkflowRequest("/api/git-workflow/message", { method: "GET", runId: expectedRunId, tabId });
|
|
4184
4461
|
if (!message) return;
|
|
4462
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
4463
|
+
if (!currentWorkflow) return;
|
|
4185
4464
|
const newestMtime = Math.max(message.shortMtimeMs || 0, message.longMtimeMs || 0);
|
|
4186
|
-
if (requireFresh &&
|
|
4465
|
+
if (requireFresh && currentWorkflow.messageRequestedAt && newestMtime + 10000 < currentWorkflow.messageRequestedAt) {
|
|
4187
4466
|
throw new Error("Generated message files have not refreshed yet.");
|
|
4188
4467
|
}
|
|
4189
4468
|
setGitWorkflow({
|
|
@@ -4192,40 +4471,63 @@ async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId
|
|
|
4192
4471
|
error: "",
|
|
4193
4472
|
message,
|
|
4194
4473
|
output: formatCommitMessagePreview(message),
|
|
4195
|
-
});
|
|
4474
|
+
}, { tabId });
|
|
4196
4475
|
} catch (error) {
|
|
4197
|
-
if (!isCurrentGitWorkflowRun(
|
|
4476
|
+
if (!isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
4198
4477
|
if (retries > 0) {
|
|
4199
|
-
setTimeout(() => loadGitWorkflowMessage({ requireFresh, retries: retries - 1, runId }), 1400);
|
|
4478
|
+
setTimeout(() => loadGitWorkflowMessage({ requireFresh, retries: retries - 1, runId: expectedRunId, tabId }), 1400);
|
|
4200
4479
|
return;
|
|
4201
4480
|
}
|
|
4202
|
-
|
|
4481
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
4482
|
+
failGitWorkflow(error, currentWorkflow?.step === "generating" ? "generate" : currentWorkflow?.step, { tabId });
|
|
4203
4483
|
}
|
|
4204
4484
|
}
|
|
4205
4485
|
|
|
4206
4486
|
async function commitGitWorkflow(variant) {
|
|
4207
|
-
const
|
|
4208
|
-
|
|
4487
|
+
const tabId = activeTabId;
|
|
4488
|
+
const tabContext = activeTabContext(tabId);
|
|
4489
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4490
|
+
if (!workflow) return;
|
|
4491
|
+
const runId = workflow.runId;
|
|
4492
|
+
setGitWorkflow({ step: "committing", busy: true, error: "", output: `${formatCommitMessagePreview(workflow.message)}\n\nRunning native ${variant} commitโฆ` }, { tabId });
|
|
4209
4493
|
try {
|
|
4210
|
-
const result = await gitWorkflowRequest("/api/git-workflow/commit", { body: { variant }, runId });
|
|
4494
|
+
const result = await gitWorkflowRequest("/api/git-workflow/commit", { body: { variant }, runId, tabId });
|
|
4211
4495
|
if (!result) return;
|
|
4212
|
-
setGitWorkflow({ step: "push", busy: false, output: `${formatGitCommandResult(result)}\n\nCommit created. Next: git push.` });
|
|
4213
|
-
scheduleRefreshFooter();
|
|
4496
|
+
setGitWorkflow({ step: "push", busy: false, output: `${formatGitCommandResult(result)}\n\nCommit created. Next: git push.` }, { tabId });
|
|
4497
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
4214
4498
|
} catch (error) {
|
|
4215
|
-
if (isCurrentGitWorkflowRun(runId)) failGitWorkflow(error, "message");
|
|
4499
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "message", { tabId });
|
|
4216
4500
|
}
|
|
4217
4501
|
}
|
|
4218
4502
|
|
|
4219
4503
|
async function pushGitWorkflow() {
|
|
4220
|
-
const
|
|
4221
|
-
|
|
4504
|
+
const tabId = activeTabId;
|
|
4505
|
+
const tabContext = activeTabContext(tabId);
|
|
4506
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4507
|
+
if (!workflow) return;
|
|
4508
|
+
const runId = workflow.runId;
|
|
4509
|
+
setGitWorkflow({ step: "pushing", busy: true, error: "", output: "Running git pushโฆ" }, { tabId });
|
|
4222
4510
|
try {
|
|
4223
|
-
const result = await gitWorkflowRequest("/api/git-workflow/push", { runId });
|
|
4511
|
+
const result = await gitWorkflowRequest("/api/git-workflow/push", { runId, tabId });
|
|
4224
4512
|
if (!result) return;
|
|
4225
|
-
setGitWorkflow({ step: "done", busy: false, output: formatGitCommandResult(result) || "git push finished." });
|
|
4226
|
-
scheduleRefreshFooter();
|
|
4513
|
+
setGitWorkflow({ step: "done", busy: false, output: formatGitCommandResult(result) || "git push finished." }, { tabId });
|
|
4514
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
4227
4515
|
} catch (error) {
|
|
4228
|
-
if (isCurrentGitWorkflowRun(runId)) failGitWorkflow(error, "push");
|
|
4516
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "push", { tabId });
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
|
|
4520
|
+
function resumeGitWorkflowForActiveTab(tabContext = activeTabContext()) {
|
|
4521
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4522
|
+
bindGitWorkflowToActiveTab();
|
|
4523
|
+
renderGitWorkflow();
|
|
4524
|
+
if (gitWorkflow.active && gitWorkflow.step === "generating" && !currentState?.isStreaming) {
|
|
4525
|
+
const retryDelayMs = Math.max(0, 2500 - (Date.now() - (gitWorkflow.messageRequestedAt || 0)));
|
|
4526
|
+
if (retryDelayMs > 0) {
|
|
4527
|
+
setTimeout(() => resumeGitWorkflowForActiveTab(tabContext), retryDelayMs);
|
|
4528
|
+
return;
|
|
4529
|
+
}
|
|
4530
|
+
loadGitWorkflowMessage({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId: tabContext.tabId });
|
|
4229
4531
|
}
|
|
4230
4532
|
}
|
|
4231
4533
|
|
|
@@ -5486,8 +5788,39 @@ function restoreToolDetailsOpenState(root, state) {
|
|
|
5486
5788
|
}
|
|
5487
5789
|
}
|
|
5488
5790
|
|
|
5791
|
+
function captureReusableToolCards() {
|
|
5792
|
+
const cards = new Map();
|
|
5793
|
+
for (const bubble of elements.chat.querySelectorAll(".message.toolExecution[data-tool-call-id]")) {
|
|
5794
|
+
const id = bubble.dataset.toolCallId;
|
|
5795
|
+
if (id) cards.set(id, bubble);
|
|
5796
|
+
}
|
|
5797
|
+
return cards;
|
|
5798
|
+
}
|
|
5799
|
+
|
|
5800
|
+
function reuseToolExecutionBubble(reusableToolCards, message, { streaming = false, messageIndex = -1, transient = false } = {}) {
|
|
5801
|
+
if (streaming || message?.role !== "toolExecution" || !message.toolCallId || !reusableToolCards) return null;
|
|
5802
|
+
const id = String(message.toolCallId);
|
|
5803
|
+
const bubble = reusableToolCards.get(id);
|
|
5804
|
+
if (!bubble) return null;
|
|
5805
|
+
reusableToolCards.delete(id);
|
|
5806
|
+
const body = bubble.querySelector(":scope > .message-body");
|
|
5807
|
+
if (!body || !updateLiveToolCard(bubble, message)) return null;
|
|
5808
|
+
bubble.classList.remove("action-enter", "streaming", "has-action-feedback");
|
|
5809
|
+
bubble.querySelector(":scope > .action-feedback-controls")?.remove();
|
|
5810
|
+
if (!transient && messageIndex >= 0) {
|
|
5811
|
+
bubble.dataset.messageIndex = String(messageIndex);
|
|
5812
|
+
bubble.removeAttribute("data-user-prompt");
|
|
5813
|
+
} else {
|
|
5814
|
+
bubble.removeAttribute("data-message-index");
|
|
5815
|
+
bubble.removeAttribute("data-user-prompt");
|
|
5816
|
+
}
|
|
5817
|
+
if (!streaming && !transient) renderActionFeedbackControls(bubble, message, messageIndex);
|
|
5818
|
+
elements.chat.append(bubble);
|
|
5819
|
+
return { bubble, body };
|
|
5820
|
+
}
|
|
5821
|
+
|
|
5489
5822
|
function updateLiveToolCard(bubble, message) {
|
|
5490
|
-
if (!bubble
|
|
5823
|
+
if (!bubble) return false;
|
|
5491
5824
|
const header = bubble.querySelector(":scope > .message-header");
|
|
5492
5825
|
const body = bubble.querySelector(":scope > .message-body");
|
|
5493
5826
|
if (!body) return false;
|
|
@@ -5544,6 +5877,7 @@ function renderLiveToolRun(run, { scroll = true } = {}) {
|
|
|
5544
5877
|
const existingConnected = !!(existing?.isConnected && existing.parentElement === elements.chat);
|
|
5545
5878
|
const shouldFollow = scroll && (autoFollowChat || isChatNearBottom());
|
|
5546
5879
|
const message = liveToolRunMessage(run);
|
|
5880
|
+
rememberActionEntries([{ message, messageIndex: -1, transient: true }]);
|
|
5547
5881
|
if (existingConnected && updateLiveToolCard(existing, message)) {
|
|
5548
5882
|
renderRunIndicator({ scroll: false });
|
|
5549
5883
|
if (shouldFollow) scrollChatToBottom();
|
|
@@ -5616,7 +5950,9 @@ function jumpToStickyUserPrompt() {
|
|
|
5616
5950
|
requestAnimationFrame(updateStickyUserPromptButton);
|
|
5617
5951
|
}
|
|
5618
5952
|
|
|
5619
|
-
function appendMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false } = {}) {
|
|
5953
|
+
function appendMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false, reusableToolCards = null } = {}) {
|
|
5954
|
+
const reused = reuseToolExecutionBubble(reusableToolCards, message, { streaming, messageIndex, transient });
|
|
5955
|
+
if (reused) return reused;
|
|
5620
5956
|
const role = String(message.role || "message");
|
|
5621
5957
|
const safeRole = role.replace(/[^a-z0-9_-]/gi, "");
|
|
5622
5958
|
const bubble = make("article", `message ${safeRole}${message.level ? ` ${message.level}` : ""}${streaming ? " streaming" : ""}${animateEntry ? " action-enter" : ""}`);
|
|
@@ -5674,9 +6010,9 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
5674
6010
|
return { bubble, body };
|
|
5675
6011
|
}
|
|
5676
6012
|
|
|
5677
|
-
function appendTranscriptMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false } = {}) {
|
|
6013
|
+
function appendTranscriptMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false, reusableToolCards = null } = {}) {
|
|
5678
6014
|
if (streaming || transient || message?.role !== "assistant") {
|
|
5679
|
-
return appendMessage(message, { streaming, messageIndex, transient, animateEntry });
|
|
6015
|
+
return appendMessage(message, { streaming, messageIndex, transient, animateEntry, reusableToolCards });
|
|
5680
6016
|
}
|
|
5681
6017
|
|
|
5682
6018
|
let finalOutput = null;
|
|
@@ -5705,6 +6041,7 @@ function appendTranscriptMessage(message, { streaming = false, messageIndex = -1
|
|
|
5705
6041
|
messageIndex: ["assistant", "toolExecution"].includes(transcriptMessage.role) ? messageIndex : -1,
|
|
5706
6042
|
transient: false,
|
|
5707
6043
|
animateEntry: animateEntry && isActionTranscriptMessage(transcriptMessage),
|
|
6044
|
+
reusableToolCards,
|
|
5708
6045
|
});
|
|
5709
6046
|
if (transcriptMessage.role === "assistant") finalOutput = created;
|
|
5710
6047
|
});
|
|
@@ -5923,16 +6260,17 @@ function actionEntrySeenKeys(tabId = activeTabId) {
|
|
|
5923
6260
|
|
|
5924
6261
|
function actionEntryKey(item) {
|
|
5925
6262
|
const message = item?.message || {};
|
|
6263
|
+
const keyedToolExecution = message.role === "toolExecution" && message.toolCallId;
|
|
5926
6264
|
return [
|
|
5927
|
-
item?.transient ? "transient" : "message",
|
|
5928
|
-
item?.messageIndex ?? -1,
|
|
6265
|
+
keyedToolExecution ? "toolExecution" : item?.transient ? "transient" : "message",
|
|
6266
|
+
keyedToolExecution ? "" : (item?.messageIndex ?? -1),
|
|
5929
6267
|
message.role || "message",
|
|
5930
6268
|
message.toolName || "",
|
|
5931
6269
|
message.toolCallId || "",
|
|
5932
|
-
message.command || "",
|
|
5933
|
-
message.title || "",
|
|
5934
|
-
message.timestamp || "",
|
|
5935
|
-
textFromContent(message.content).slice(0, 240),
|
|
6270
|
+
keyedToolExecution ? "" : message.command || "",
|
|
6271
|
+
keyedToolExecution ? "" : message.title || "",
|
|
6272
|
+
keyedToolExecution ? "" : message.timestamp || "",
|
|
6273
|
+
keyedToolExecution ? "" : textFromContent(message.content).slice(0, 240),
|
|
5936
6274
|
].join("|");
|
|
5937
6275
|
}
|
|
5938
6276
|
|
|
@@ -5974,6 +6312,7 @@ function orderedTranscriptItems() {
|
|
|
5974
6312
|
function renderAllMessages({ preserveScroll = false } = {}) {
|
|
5975
6313
|
const shouldFollow = !preserveScroll && (autoFollowChat || isChatNearBottom());
|
|
5976
6314
|
const previousScrollTop = elements.chat.scrollTop;
|
|
6315
|
+
const reusableToolCards = captureReusableToolCards();
|
|
5977
6316
|
resetChatOutput();
|
|
5978
6317
|
const transcriptItems = orderedTranscriptItems();
|
|
5979
6318
|
for (const item of transcriptItems) {
|
|
@@ -5981,6 +6320,7 @@ function renderAllMessages({ preserveScroll = false } = {}) {
|
|
|
5981
6320
|
messageIndex: item.messageIndex,
|
|
5982
6321
|
transient: item.transient,
|
|
5983
6322
|
animateEntry: shouldAnimateActionEntry(item),
|
|
6323
|
+
reusableToolCards,
|
|
5984
6324
|
});
|
|
5985
6325
|
}
|
|
5986
6326
|
rememberActionEntries(transcriptItems);
|
|
@@ -7589,6 +7929,7 @@ async function refreshAll(tabContext = activeTabContext()) {
|
|
|
7589
7929
|
for (const result of results) {
|
|
7590
7930
|
if (result.status === "rejected") addEvent(result.reason.message || String(result.reason), "error");
|
|
7591
7931
|
}
|
|
7932
|
+
resumeGitWorkflowForActiveTab(tabContext);
|
|
7592
7933
|
}
|
|
7593
7934
|
|
|
7594
7935
|
async function openToNetwork() {
|
|
@@ -8025,9 +8366,14 @@ function handleEvent(event) {
|
|
|
8025
8366
|
scheduleRefreshState();
|
|
8026
8367
|
scheduleRefreshMessages();
|
|
8027
8368
|
scheduleRefreshFooter();
|
|
8369
|
+
scheduleRefreshCodexUsage(2200);
|
|
8028
8370
|
renderFeedbackTray();
|
|
8029
|
-
|
|
8030
|
-
|
|
8371
|
+
{
|
|
8372
|
+
const workflowTabId = event.tabId || activeTabId;
|
|
8373
|
+
const workflow = gitWorkflowForTab(workflowTabId, { create: false });
|
|
8374
|
+
if (workflow?.active && workflow.step === "generating") {
|
|
8375
|
+
loadGitWorkflowMessage({ requireFresh: true, retries: 3, runId: workflow.runId, tabId: workflowTabId });
|
|
8376
|
+
}
|
|
8031
8377
|
}
|
|
8032
8378
|
break;
|
|
8033
8379
|
case "message_start":
|
|
@@ -8114,7 +8460,11 @@ function handleEvent(event) {
|
|
|
8114
8460
|
syncRunIndicatorFromState(currentState);
|
|
8115
8461
|
renderStatus();
|
|
8116
8462
|
} else if (["set_model", "set_thinking_level", "new_session", "compact"].includes(event.command)) {
|
|
8117
|
-
if (event.command === "new_session")
|
|
8463
|
+
if (event.command === "new_session") {
|
|
8464
|
+
const tabId = event.tabId || activeTabId;
|
|
8465
|
+
forgetLastUserPrompt(tabId);
|
|
8466
|
+
resetGitWorkflowForTab(tabId);
|
|
8467
|
+
}
|
|
8118
8468
|
scheduleRefreshState();
|
|
8119
8469
|
scheduleRefreshMessages();
|
|
8120
8470
|
scheduleRefreshFooter();
|
|
@@ -8252,6 +8602,7 @@ elements.newSessionButton.addEventListener("click", async () => {
|
|
|
8252
8602
|
const response = await api("/api/new-session", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
8253
8603
|
applyResponseTab(response);
|
|
8254
8604
|
forgetLastUserPrompt(tabContext.tabId);
|
|
8605
|
+
resetGitWorkflowForTab(tabContext.tabId);
|
|
8255
8606
|
if (!isCurrentTabContext(tabContext)) return;
|
|
8256
8607
|
await refreshAll(tabContext);
|
|
8257
8608
|
if (isCurrentTabContext(tabContext)) focusPromptInput({ defer: true });
|
|
@@ -8411,6 +8762,9 @@ window.addEventListener("keydown", (event) => {
|
|
|
8411
8762
|
}
|
|
8412
8763
|
});
|
|
8413
8764
|
|
|
8765
|
+
elements.refreshCodexUsageButton?.addEventListener("click", () => {
|
|
8766
|
+
refreshCodexUsage({ forceAuthRefresh: true }).finally(() => scheduleRefreshCodexUsage());
|
|
8767
|
+
});
|
|
8414
8768
|
elements.pathPickerAddFastPickButton.addEventListener("click", () => addCurrentFastPick().catch((error) => addEvent(error.message, "error")));
|
|
8415
8769
|
elements.pathPickerCancelButton.addEventListener("click", () => closePathPicker(null));
|
|
8416
8770
|
elements.pathPickerChooseButton.addEventListener("click", () => closePathPicker(pathPickerState?.cwd || null));
|
|
@@ -8507,6 +8861,7 @@ restoreThinkingVisibilitySetting();
|
|
|
8507
8861
|
restoreSidePanelSectionState();
|
|
8508
8862
|
bindSidePanelSectionToggles();
|
|
8509
8863
|
restoreSidePanelState();
|
|
8864
|
+
initializeCodexUsage();
|
|
8510
8865
|
bindMobileViewChanges();
|
|
8511
8866
|
registerPwaServiceWorker();
|
|
8512
8867
|
renderServerOfflinePanel();
|