@firstpick/pi-package-webui 0.1.7 → 0.1.8
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 +2 -0
- 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 +247 -97
- package/tests/mobile-static.test.mjs +4 -0
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Local browser companion for [Pi coding agent](https://www.npmjs.com/package/@earendil-works/pi-coding-agent).
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
This package provides:
|
|
6
8
|
|
|
7
9
|
- `pi-webui`: a local HTTP/SSE server that starts `pi --mode rpc`, serves the static browser UI, and proxies browser actions to Pi RPC commands.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firstpick/pi-package-webui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Pi Web UI companion package with a local browser UI CLI plus /webui-start and /webui-status commands.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"extension"
|
|
15
15
|
],
|
|
16
16
|
"pi": {
|
|
17
|
+
"image": "https://unpkg.com/@firstpick/pi-package-webui/images/Main_Window_v0.1.7.png",
|
|
17
18
|
"extensions": [
|
|
18
19
|
"./index.ts",
|
|
19
20
|
"../pi-extension-git-footer-status/index.ts",
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
"index.ts",
|
|
64
65
|
"bin",
|
|
65
66
|
"public",
|
|
67
|
+
"images",
|
|
66
68
|
"tests",
|
|
67
69
|
"README.md",
|
|
68
70
|
"LICENSE"
|
package/public/app.js
CHANGED
|
@@ -319,16 +319,56 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
|
319
319
|
const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate"]);
|
|
320
320
|
const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models"]);
|
|
321
321
|
const optionalFeatureInstallInProgress = new Set();
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
322
|
+
|
|
323
|
+
function createGitWorkflowState() {
|
|
324
|
+
return {
|
|
325
|
+
active: false,
|
|
326
|
+
step: "idle",
|
|
327
|
+
busy: false,
|
|
328
|
+
runId: 0,
|
|
329
|
+
output: "",
|
|
330
|
+
error: "",
|
|
331
|
+
message: null,
|
|
332
|
+
messageRequestedAt: 0,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const gitWorkflowsByTab = new Map();
|
|
337
|
+
let gitWorkflow = createGitWorkflowState();
|
|
338
|
+
|
|
339
|
+
function gitWorkflowForTab(tabId = activeTabId, { create = true } = {}) {
|
|
340
|
+
if (!tabId) return null;
|
|
341
|
+
let workflow = gitWorkflowsByTab.get(tabId);
|
|
342
|
+
if (!workflow && create) {
|
|
343
|
+
workflow = createGitWorkflowState();
|
|
344
|
+
gitWorkflowsByTab.set(tabId, workflow);
|
|
345
|
+
}
|
|
346
|
+
return workflow || null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function bindGitWorkflowToActiveTab() {
|
|
350
|
+
gitWorkflow = gitWorkflowForTab(activeTabId) || createGitWorkflowState();
|
|
351
|
+
return gitWorkflow;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function resetGitWorkflowForTab(tabId = activeTabId) {
|
|
355
|
+
if (!tabId) return;
|
|
356
|
+
gitWorkflowsByTab.set(tabId, createGitWorkflowState());
|
|
357
|
+
if (tabId === activeTabId) {
|
|
358
|
+
bindGitWorkflowToActiveTab();
|
|
359
|
+
renderGitWorkflow();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function clearGitWorkflowForTab(tabId) {
|
|
364
|
+
if (!tabId) return;
|
|
365
|
+
gitWorkflowsByTab.delete(tabId);
|
|
366
|
+
if (tabId === activeTabId) {
|
|
367
|
+
bindGitWorkflowToActiveTab();
|
|
368
|
+
renderGitWorkflow();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
332
372
|
const GIT_WORKFLOW_STEPS = ["Stage", "Message", "Commit", "Push"];
|
|
333
373
|
const ACTION_FEEDBACK_REACTIONS = {
|
|
334
374
|
up: { icon: "👍", label: "Good job", title: "Good job!" },
|
|
@@ -1838,6 +1878,7 @@ function setActiveTabId(tabId, { remember = false } = {}) {
|
|
|
1838
1878
|
const nextTabId = tabId || null;
|
|
1839
1879
|
if (nextTabId !== activeTabId) activeTabGeneration += 1;
|
|
1840
1880
|
activeTabId = nextTabId;
|
|
1881
|
+
bindGitWorkflowToActiveTab();
|
|
1841
1882
|
if (remember) rememberActiveTab();
|
|
1842
1883
|
return activeTabContext(nextTabId);
|
|
1843
1884
|
}
|
|
@@ -1914,6 +1955,7 @@ function syncTabMetadata(nextTabs = []) {
|
|
|
1914
1955
|
tabActivities.delete(tabId);
|
|
1915
1956
|
tabSeenCompletionSerials.delete(tabId);
|
|
1916
1957
|
actionFeedbackByTab.delete(tabId);
|
|
1958
|
+
clearGitWorkflowForTab(tabId);
|
|
1917
1959
|
}
|
|
1918
1960
|
}
|
|
1919
1961
|
}
|
|
@@ -2094,6 +2136,15 @@ function restoreStoredTabId() {
|
|
|
2094
2136
|
}
|
|
2095
2137
|
}
|
|
2096
2138
|
|
|
2139
|
+
function requestedTabIdFromUrl() {
|
|
2140
|
+
try {
|
|
2141
|
+
const params = new URLSearchParams(window.location.search);
|
|
2142
|
+
return params.get("tab") || params.get("tabId") || null;
|
|
2143
|
+
} catch {
|
|
2144
|
+
return null;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2097
2148
|
function updateDocumentTitle() {
|
|
2098
2149
|
const tab = activeTab();
|
|
2099
2150
|
document.title = tab ? `Pi Web UI · ${tab.title}` : "Pi Web UI";
|
|
@@ -2179,15 +2230,7 @@ function resetActiveTabUi() {
|
|
|
2179
2230
|
cancelPendingDialogs();
|
|
2180
2231
|
if (elements.nativeCommandDialog.open) closeNativeCommandDialog();
|
|
2181
2232
|
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
|
-
});
|
|
2233
|
+
bindGitWorkflowToActiveTab();
|
|
2191
2234
|
resetChatOutput();
|
|
2192
2235
|
elements.stateDetails.replaceChildren();
|
|
2193
2236
|
elements.eventLog.replaceChildren();
|
|
@@ -2438,9 +2481,10 @@ async function refreshTabs({ selectStored = false } = {}) {
|
|
|
2438
2481
|
syncTabMetadata(tabs);
|
|
2439
2482
|
syncBlockedTabNotificationsFromTabs(tabs, previousTabs);
|
|
2440
2483
|
syncAgentDoneNotificationsFromTabs(tabs, previousTabs);
|
|
2484
|
+
const requested = selectStored ? requestedTabIdFromUrl() : null;
|
|
2441
2485
|
const stored = selectStored ? restoreStoredTabId() : null;
|
|
2442
2486
|
if (!activeTabId || !tabs.some((tab) => tab.id === activeTabId)) {
|
|
2443
|
-
setActiveTabId((stored && tabs.some((tab) => tab.id === stored) ? stored : tabs[0]?.id) || null, { remember: true });
|
|
2487
|
+
setActiveTabId((requested && tabs.some((tab) => tab.id === requested) ? requested : stored && tabs.some((tab) => tab.id === stored) ? stored : tabs[0]?.id) || null, { remember: true });
|
|
2444
2488
|
}
|
|
2445
2489
|
rememberServerStartCwd(tabs.find((tab) => tab.id === activeTabId)?.cwd || tabs[0]?.cwd);
|
|
2446
2490
|
renderTabs();
|
|
@@ -2532,6 +2576,7 @@ async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } =
|
|
|
2532
2576
|
for (const id of closedIds) {
|
|
2533
2577
|
tabDrafts.delete(id);
|
|
2534
2578
|
clearAttachments(id);
|
|
2579
|
+
clearGitWorkflowForTab(id);
|
|
2535
2580
|
}
|
|
2536
2581
|
clearOpenTerminalTabGroup(null, { force: true });
|
|
2537
2582
|
|
|
@@ -3436,6 +3481,7 @@ async function changeActiveTabCwd() {
|
|
|
3436
3481
|
return;
|
|
3437
3482
|
}
|
|
3438
3483
|
const nextContext = setActiveTabId(response.data?.tab?.id || activeTabId);
|
|
3484
|
+
resetGitWorkflowForTab(nextContext.tabId);
|
|
3439
3485
|
resetActiveTabUi();
|
|
3440
3486
|
renderTabs();
|
|
3441
3487
|
restoreActiveDraft();
|
|
@@ -3964,18 +4010,27 @@ function renderWidgets() {
|
|
|
3964
4010
|
}
|
|
3965
4011
|
}
|
|
3966
4012
|
|
|
3967
|
-
function setGitWorkflow(patch) {
|
|
3968
|
-
|
|
3969
|
-
|
|
4013
|
+
function setGitWorkflow(patch, { tabId = activeTabId } = {}) {
|
|
4014
|
+
const workflow = gitWorkflowForTab(tabId);
|
|
4015
|
+
if (!workflow) return null;
|
|
4016
|
+
Object.assign(workflow, patch);
|
|
4017
|
+
if (tabId === activeTabId) {
|
|
4018
|
+
gitWorkflow = workflow;
|
|
4019
|
+
renderGitWorkflow();
|
|
4020
|
+
}
|
|
4021
|
+
return workflow;
|
|
3970
4022
|
}
|
|
3971
4023
|
|
|
3972
|
-
function isCurrentGitWorkflowRun(runId) {
|
|
3973
|
-
|
|
4024
|
+
function isCurrentGitWorkflowRun(runId, tabId = activeTabId) {
|
|
4025
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4026
|
+
return !!workflow?.active && workflow.runId === runId;
|
|
3974
4027
|
}
|
|
3975
4028
|
|
|
3976
|
-
function appendGitWorkflowOutput(text) {
|
|
3977
|
-
const
|
|
3978
|
-
|
|
4029
|
+
function appendGitWorkflowOutput(text, { tabId = activeTabId } = {}) {
|
|
4030
|
+
const workflow = gitWorkflowForTab(tabId);
|
|
4031
|
+
if (!workflow) return;
|
|
4032
|
+
const next = `${workflow.output || ""}${workflow.output ? "\n" : ""}${text}`;
|
|
4033
|
+
setGitWorkflow({ output: next.slice(-60000) }, { tabId });
|
|
3979
4034
|
}
|
|
3980
4035
|
|
|
3981
4036
|
function formatGitCommandResult(result) {
|
|
@@ -4078,9 +4133,11 @@ function renderGitWorkflow() {
|
|
|
4078
4133
|
}
|
|
4079
4134
|
}
|
|
4080
4135
|
|
|
4081
|
-
async function gitWorkflowRequest(path, { method = "POST", body = {}, runId =
|
|
4082
|
-
const
|
|
4083
|
-
|
|
4136
|
+
async function gitWorkflowRequest(path, { method = "POST", body = {}, runId, tabId = activeTabId } = {}) {
|
|
4137
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4138
|
+
const expectedRunId = runId ?? workflow?.runId;
|
|
4139
|
+
const response = await api(path, method === "GET" ? { method, tabId } : { method, body, tabId });
|
|
4140
|
+
if (expectedRunId !== undefined && !isCurrentGitWorkflowRun(expectedRunId, tabId)) return null;
|
|
4084
4141
|
if (!response.ok) {
|
|
4085
4142
|
const detail = response.data ? `\n\n${formatGitCommandResult(response.data)}` : "";
|
|
4086
4143
|
throw new Error(`${response.error || "Git workflow request failed"}${detail}`);
|
|
@@ -4088,27 +4145,32 @@ async function gitWorkflowRequest(path, { method = "POST", body = {}, runId = gi
|
|
|
4088
4145
|
return response.data;
|
|
4089
4146
|
}
|
|
4090
4147
|
|
|
4091
|
-
function failGitWorkflow(error, step =
|
|
4148
|
+
function failGitWorkflow(error, step, { tabId = activeTabId } = {}) {
|
|
4149
|
+
const workflow = gitWorkflowForTab(tabId);
|
|
4150
|
+
if (!workflow) return;
|
|
4092
4151
|
const message = error?.message || String(error);
|
|
4093
4152
|
setGitWorkflow({
|
|
4094
|
-
step,
|
|
4153
|
+
step: step || workflow.step || "error",
|
|
4095
4154
|
busy: false,
|
|
4096
4155
|
error: message,
|
|
4097
|
-
output: `${
|
|
4098
|
-
});
|
|
4156
|
+
output: `${workflow.output || ""}${workflow.output ? "\n\n" : ""}ERROR: ${message}`.slice(-60000),
|
|
4157
|
+
}, { tabId });
|
|
4099
4158
|
}
|
|
4100
4159
|
|
|
4101
4160
|
function startGitWorkflow() {
|
|
4161
|
+
const tabId = activeTabId;
|
|
4162
|
+
if (!tabId) return;
|
|
4102
4163
|
if (!isOptionalFeatureEnabled("gitWorkflow")) {
|
|
4103
|
-
const tabContext = activeTabContext();
|
|
4164
|
+
const tabContext = activeTabContext(tabId);
|
|
4104
4165
|
addEvent(commandUnavailableMessage("git-staged-msg"), "warn");
|
|
4105
4166
|
refreshCommands(tabContext).catch((error) => {
|
|
4106
4167
|
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
4107
4168
|
});
|
|
4108
4169
|
return;
|
|
4109
4170
|
}
|
|
4110
|
-
|
|
4111
|
-
|
|
4171
|
+
const workflow = gitWorkflowForTab(tabId);
|
|
4172
|
+
if (workflow.active && !["done", "cancelled", "error"].includes(workflow.step) && !confirm("Restart the active git workflow?")) return;
|
|
4173
|
+
workflow.runId += 1;
|
|
4112
4174
|
setGitWorkflow({
|
|
4113
4175
|
active: true,
|
|
4114
4176
|
step: "add",
|
|
@@ -4117,40 +4179,52 @@ function startGitWorkflow() {
|
|
|
4117
4179
|
error: "",
|
|
4118
4180
|
message: null,
|
|
4119
4181
|
messageRequestedAt: 0,
|
|
4120
|
-
});
|
|
4182
|
+
}, { tabId });
|
|
4121
4183
|
}
|
|
4122
4184
|
|
|
4123
4185
|
async function cancelGitWorkflow() {
|
|
4124
|
-
const
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
if (
|
|
4186
|
+
const tabId = activeTabId;
|
|
4187
|
+
const tabContext = activeTabContext(tabId);
|
|
4188
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4189
|
+
if (!workflow?.active) return;
|
|
4190
|
+
const shouldAbortPi = workflow.step === "generating";
|
|
4191
|
+
workflow.runId += 1;
|
|
4192
|
+
setGitWorkflow({ step: "cancelled", busy: false, error: "", output: `${workflow.output || ""}${workflow.output ? "\n\n" : ""}Cancelled by user.` }, { tabId });
|
|
4193
|
+
if (shouldAbortPi && isCurrentTabContext(tabContext)) setRunIndicatorActivity("Abort requested; checking whether Pi stopped…");
|
|
4128
4194
|
await Promise.allSettled([
|
|
4129
|
-
api("/api/git-workflow/cancel", { method: "POST", body: {} }),
|
|
4130
|
-
shouldAbortPi ? api("/api/abort", { method: "POST", body: {} }) : Promise.resolve(),
|
|
4195
|
+
api("/api/git-workflow/cancel", { method: "POST", body: {}, tabId }),
|
|
4196
|
+
shouldAbortPi ? api("/api/abort", { method: "POST", body: {}, tabId }) : Promise.resolve(),
|
|
4131
4197
|
]);
|
|
4132
|
-
if (shouldAbortPi) scheduleAbortStateChecks();
|
|
4198
|
+
if (shouldAbortPi && isCurrentTabContext(tabContext)) scheduleAbortStateChecks();
|
|
4133
4199
|
}
|
|
4134
4200
|
|
|
4135
4201
|
async function runGitAdd() {
|
|
4136
|
-
const
|
|
4137
|
-
|
|
4202
|
+
const tabId = activeTabId;
|
|
4203
|
+
const tabContext = activeTabContext(tabId);
|
|
4204
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4205
|
+
if (!workflow) return;
|
|
4206
|
+
const runId = workflow.runId;
|
|
4207
|
+
setGitWorkflow({ step: "add", busy: true, error: "", output: "Running git add ." }, { tabId });
|
|
4138
4208
|
try {
|
|
4139
|
-
const result = await gitWorkflowRequest("/api/git-workflow/add", { runId });
|
|
4209
|
+
const result = await gitWorkflowRequest("/api/git-workflow/add", { runId, tabId });
|
|
4140
4210
|
if (!result) return;
|
|
4141
|
-
setGitWorkflow({ step: "generate", busy: false, output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` });
|
|
4142
|
-
scheduleRefreshFooter();
|
|
4211
|
+
setGitWorkflow({ step: "generate", busy: false, output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` }, { tabId });
|
|
4212
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
4143
4213
|
} catch (error) {
|
|
4144
|
-
if (isCurrentGitWorkflowRun(runId)) failGitWorkflow(error, "add");
|
|
4214
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "add", { tabId });
|
|
4145
4215
|
}
|
|
4146
4216
|
}
|
|
4147
4217
|
|
|
4148
4218
|
async function runGitMessagePrompt() {
|
|
4219
|
+
const tabId = activeTabId;
|
|
4220
|
+
const tabContext = activeTabContext(tabId);
|
|
4149
4221
|
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");
|
|
4222
|
+
failGitWorkflow(new Error("Pi is currently running. Wait for it to finish or abort before generating a staged commit message."), "generate", { tabId });
|
|
4151
4223
|
return;
|
|
4152
4224
|
}
|
|
4153
|
-
const
|
|
4225
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4226
|
+
if (!workflow) return;
|
|
4227
|
+
const runId = workflow.runId;
|
|
4154
4228
|
const requestedAt = Date.now();
|
|
4155
4229
|
setGitWorkflow({
|
|
4156
4230
|
step: "generating",
|
|
@@ -4158,32 +4232,37 @@ async function runGitMessagePrompt() {
|
|
|
4158
4232
|
error: "",
|
|
4159
4233
|
messageRequestedAt: requestedAt,
|
|
4160
4234
|
output: "Sending /git-staged-msg to Pi.\n\nCancel will request Pi abort.",
|
|
4161
|
-
});
|
|
4162
|
-
setRunIndicatorActivity("Sending /git-staged-msg to Pi…");
|
|
4235
|
+
}, { tabId });
|
|
4236
|
+
if (isCurrentTabContext(tabContext)) setRunIndicatorActivity("Sending /git-staged-msg to Pi…");
|
|
4163
4237
|
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();
|
|
4238
|
+
await api("/api/prompt", { method: "POST", body: { message: "/git-staged-msg" }, tabId });
|
|
4239
|
+
if (!isCurrentGitWorkflowRun(runId, tabId)) return;
|
|
4240
|
+
appendGitWorkflowOutput("/git-staged-msg accepted. Waiting for agent_end, then the message files will be loaded.", { tabId });
|
|
4241
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshState(120, tabContext);
|
|
4168
4242
|
setTimeout(() => {
|
|
4169
|
-
|
|
4170
|
-
|
|
4243
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
4244
|
+
if (isCurrentTabContext(tabContext) && isCurrentGitWorkflowRun(runId, tabId) && currentWorkflow?.step === "generating" && !currentState?.isStreaming) {
|
|
4245
|
+
loadGitWorkflowMessage({ requireFresh: true, retries: 1, runId, tabId });
|
|
4171
4246
|
}
|
|
4172
4247
|
}, 2500);
|
|
4173
4248
|
} catch (error) {
|
|
4174
|
-
if (isCurrentGitWorkflowRun(runId)) {
|
|
4175
|
-
clearRunIndicatorActivity();
|
|
4176
|
-
failGitWorkflow(error, "generate");
|
|
4249
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) {
|
|
4250
|
+
if (isCurrentTabContext(tabContext)) clearRunIndicatorActivity();
|
|
4251
|
+
failGitWorkflow(error, "generate", { tabId });
|
|
4177
4252
|
}
|
|
4178
4253
|
}
|
|
4179
4254
|
}
|
|
4180
4255
|
|
|
4181
|
-
async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId =
|
|
4256
|
+
async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId, tabId = activeTabId } = {}) {
|
|
4257
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4258
|
+
const expectedRunId = runId ?? workflow?.runId;
|
|
4182
4259
|
try {
|
|
4183
|
-
const message = await gitWorkflowRequest("/api/git-workflow/message", { method: "GET", runId });
|
|
4260
|
+
const message = await gitWorkflowRequest("/api/git-workflow/message", { method: "GET", runId: expectedRunId, tabId });
|
|
4184
4261
|
if (!message) return;
|
|
4262
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
4263
|
+
if (!currentWorkflow) return;
|
|
4185
4264
|
const newestMtime = Math.max(message.shortMtimeMs || 0, message.longMtimeMs || 0);
|
|
4186
|
-
if (requireFresh &&
|
|
4265
|
+
if (requireFresh && currentWorkflow.messageRequestedAt && newestMtime + 10000 < currentWorkflow.messageRequestedAt) {
|
|
4187
4266
|
throw new Error("Generated message files have not refreshed yet.");
|
|
4188
4267
|
}
|
|
4189
4268
|
setGitWorkflow({
|
|
@@ -4192,40 +4271,63 @@ async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId
|
|
|
4192
4271
|
error: "",
|
|
4193
4272
|
message,
|
|
4194
4273
|
output: formatCommitMessagePreview(message),
|
|
4195
|
-
});
|
|
4274
|
+
}, { tabId });
|
|
4196
4275
|
} catch (error) {
|
|
4197
|
-
if (!isCurrentGitWorkflowRun(
|
|
4276
|
+
if (!isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
4198
4277
|
if (retries > 0) {
|
|
4199
|
-
setTimeout(() => loadGitWorkflowMessage({ requireFresh, retries: retries - 1, runId }), 1400);
|
|
4278
|
+
setTimeout(() => loadGitWorkflowMessage({ requireFresh, retries: retries - 1, runId: expectedRunId, tabId }), 1400);
|
|
4200
4279
|
return;
|
|
4201
4280
|
}
|
|
4202
|
-
|
|
4281
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
4282
|
+
failGitWorkflow(error, currentWorkflow?.step === "generating" ? "generate" : currentWorkflow?.step, { tabId });
|
|
4203
4283
|
}
|
|
4204
4284
|
}
|
|
4205
4285
|
|
|
4206
4286
|
async function commitGitWorkflow(variant) {
|
|
4207
|
-
const
|
|
4208
|
-
|
|
4287
|
+
const tabId = activeTabId;
|
|
4288
|
+
const tabContext = activeTabContext(tabId);
|
|
4289
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4290
|
+
if (!workflow) return;
|
|
4291
|
+
const runId = workflow.runId;
|
|
4292
|
+
setGitWorkflow({ step: "committing", busy: true, error: "", output: `${formatCommitMessagePreview(workflow.message)}\n\nRunning native ${variant} commit…` }, { tabId });
|
|
4209
4293
|
try {
|
|
4210
|
-
const result = await gitWorkflowRequest("/api/git-workflow/commit", { body: { variant }, runId });
|
|
4294
|
+
const result = await gitWorkflowRequest("/api/git-workflow/commit", { body: { variant }, runId, tabId });
|
|
4211
4295
|
if (!result) return;
|
|
4212
|
-
setGitWorkflow({ step: "push", busy: false, output: `${formatGitCommandResult(result)}\n\nCommit created. Next: git push.` });
|
|
4213
|
-
scheduleRefreshFooter();
|
|
4296
|
+
setGitWorkflow({ step: "push", busy: false, output: `${formatGitCommandResult(result)}\n\nCommit created. Next: git push.` }, { tabId });
|
|
4297
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
4214
4298
|
} catch (error) {
|
|
4215
|
-
if (isCurrentGitWorkflowRun(runId)) failGitWorkflow(error, "message");
|
|
4299
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "message", { tabId });
|
|
4216
4300
|
}
|
|
4217
4301
|
}
|
|
4218
4302
|
|
|
4219
4303
|
async function pushGitWorkflow() {
|
|
4220
|
-
const
|
|
4221
|
-
|
|
4304
|
+
const tabId = activeTabId;
|
|
4305
|
+
const tabContext = activeTabContext(tabId);
|
|
4306
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4307
|
+
if (!workflow) return;
|
|
4308
|
+
const runId = workflow.runId;
|
|
4309
|
+
setGitWorkflow({ step: "pushing", busy: true, error: "", output: "Running git push…" }, { tabId });
|
|
4222
4310
|
try {
|
|
4223
|
-
const result = await gitWorkflowRequest("/api/git-workflow/push", { runId });
|
|
4311
|
+
const result = await gitWorkflowRequest("/api/git-workflow/push", { runId, tabId });
|
|
4224
4312
|
if (!result) return;
|
|
4225
|
-
setGitWorkflow({ step: "done", busy: false, output: formatGitCommandResult(result) || "git push finished." });
|
|
4226
|
-
scheduleRefreshFooter();
|
|
4313
|
+
setGitWorkflow({ step: "done", busy: false, output: formatGitCommandResult(result) || "git push finished." }, { tabId });
|
|
4314
|
+
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
4227
4315
|
} catch (error) {
|
|
4228
|
-
if (isCurrentGitWorkflowRun(runId)) failGitWorkflow(error, "push");
|
|
4316
|
+
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "push", { tabId });
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
function resumeGitWorkflowForActiveTab(tabContext = activeTabContext()) {
|
|
4321
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4322
|
+
bindGitWorkflowToActiveTab();
|
|
4323
|
+
renderGitWorkflow();
|
|
4324
|
+
if (gitWorkflow.active && gitWorkflow.step === "generating" && !currentState?.isStreaming) {
|
|
4325
|
+
const retryDelayMs = Math.max(0, 2500 - (Date.now() - (gitWorkflow.messageRequestedAt || 0)));
|
|
4326
|
+
if (retryDelayMs > 0) {
|
|
4327
|
+
setTimeout(() => resumeGitWorkflowForActiveTab(tabContext), retryDelayMs);
|
|
4328
|
+
return;
|
|
4329
|
+
}
|
|
4330
|
+
loadGitWorkflowMessage({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId: tabContext.tabId });
|
|
4229
4331
|
}
|
|
4230
4332
|
}
|
|
4231
4333
|
|
|
@@ -5486,8 +5588,39 @@ function restoreToolDetailsOpenState(root, state) {
|
|
|
5486
5588
|
}
|
|
5487
5589
|
}
|
|
5488
5590
|
|
|
5591
|
+
function captureReusableToolCards() {
|
|
5592
|
+
const cards = new Map();
|
|
5593
|
+
for (const bubble of elements.chat.querySelectorAll(".message.toolExecution[data-tool-call-id]")) {
|
|
5594
|
+
const id = bubble.dataset.toolCallId;
|
|
5595
|
+
if (id) cards.set(id, bubble);
|
|
5596
|
+
}
|
|
5597
|
+
return cards;
|
|
5598
|
+
}
|
|
5599
|
+
|
|
5600
|
+
function reuseToolExecutionBubble(reusableToolCards, message, { streaming = false, messageIndex = -1, transient = false } = {}) {
|
|
5601
|
+
if (streaming || message?.role !== "toolExecution" || !message.toolCallId || !reusableToolCards) return null;
|
|
5602
|
+
const id = String(message.toolCallId);
|
|
5603
|
+
const bubble = reusableToolCards.get(id);
|
|
5604
|
+
if (!bubble) return null;
|
|
5605
|
+
reusableToolCards.delete(id);
|
|
5606
|
+
const body = bubble.querySelector(":scope > .message-body");
|
|
5607
|
+
if (!body || !updateLiveToolCard(bubble, message)) return null;
|
|
5608
|
+
bubble.classList.remove("action-enter", "streaming", "has-action-feedback");
|
|
5609
|
+
bubble.querySelector(":scope > .action-feedback-controls")?.remove();
|
|
5610
|
+
if (!transient && messageIndex >= 0) {
|
|
5611
|
+
bubble.dataset.messageIndex = String(messageIndex);
|
|
5612
|
+
bubble.removeAttribute("data-user-prompt");
|
|
5613
|
+
} else {
|
|
5614
|
+
bubble.removeAttribute("data-message-index");
|
|
5615
|
+
bubble.removeAttribute("data-user-prompt");
|
|
5616
|
+
}
|
|
5617
|
+
if (!streaming && !transient) renderActionFeedbackControls(bubble, message, messageIndex);
|
|
5618
|
+
elements.chat.append(bubble);
|
|
5619
|
+
return { bubble, body };
|
|
5620
|
+
}
|
|
5621
|
+
|
|
5489
5622
|
function updateLiveToolCard(bubble, message) {
|
|
5490
|
-
if (!bubble
|
|
5623
|
+
if (!bubble) return false;
|
|
5491
5624
|
const header = bubble.querySelector(":scope > .message-header");
|
|
5492
5625
|
const body = bubble.querySelector(":scope > .message-body");
|
|
5493
5626
|
if (!body) return false;
|
|
@@ -5544,6 +5677,7 @@ function renderLiveToolRun(run, { scroll = true } = {}) {
|
|
|
5544
5677
|
const existingConnected = !!(existing?.isConnected && existing.parentElement === elements.chat);
|
|
5545
5678
|
const shouldFollow = scroll && (autoFollowChat || isChatNearBottom());
|
|
5546
5679
|
const message = liveToolRunMessage(run);
|
|
5680
|
+
rememberActionEntries([{ message, messageIndex: -1, transient: true }]);
|
|
5547
5681
|
if (existingConnected && updateLiveToolCard(existing, message)) {
|
|
5548
5682
|
renderRunIndicator({ scroll: false });
|
|
5549
5683
|
if (shouldFollow) scrollChatToBottom();
|
|
@@ -5616,7 +5750,9 @@ function jumpToStickyUserPrompt() {
|
|
|
5616
5750
|
requestAnimationFrame(updateStickyUserPromptButton);
|
|
5617
5751
|
}
|
|
5618
5752
|
|
|
5619
|
-
function appendMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false } = {}) {
|
|
5753
|
+
function appendMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false, reusableToolCards = null } = {}) {
|
|
5754
|
+
const reused = reuseToolExecutionBubble(reusableToolCards, message, { streaming, messageIndex, transient });
|
|
5755
|
+
if (reused) return reused;
|
|
5620
5756
|
const role = String(message.role || "message");
|
|
5621
5757
|
const safeRole = role.replace(/[^a-z0-9_-]/gi, "");
|
|
5622
5758
|
const bubble = make("article", `message ${safeRole}${message.level ? ` ${message.level}` : ""}${streaming ? " streaming" : ""}${animateEntry ? " action-enter" : ""}`);
|
|
@@ -5674,9 +5810,9 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
5674
5810
|
return { bubble, body };
|
|
5675
5811
|
}
|
|
5676
5812
|
|
|
5677
|
-
function appendTranscriptMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false } = {}) {
|
|
5813
|
+
function appendTranscriptMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false, reusableToolCards = null } = {}) {
|
|
5678
5814
|
if (streaming || transient || message?.role !== "assistant") {
|
|
5679
|
-
return appendMessage(message, { streaming, messageIndex, transient, animateEntry });
|
|
5815
|
+
return appendMessage(message, { streaming, messageIndex, transient, animateEntry, reusableToolCards });
|
|
5680
5816
|
}
|
|
5681
5817
|
|
|
5682
5818
|
let finalOutput = null;
|
|
@@ -5705,6 +5841,7 @@ function appendTranscriptMessage(message, { streaming = false, messageIndex = -1
|
|
|
5705
5841
|
messageIndex: ["assistant", "toolExecution"].includes(transcriptMessage.role) ? messageIndex : -1,
|
|
5706
5842
|
transient: false,
|
|
5707
5843
|
animateEntry: animateEntry && isActionTranscriptMessage(transcriptMessage),
|
|
5844
|
+
reusableToolCards,
|
|
5708
5845
|
});
|
|
5709
5846
|
if (transcriptMessage.role === "assistant") finalOutput = created;
|
|
5710
5847
|
});
|
|
@@ -5923,16 +6060,17 @@ function actionEntrySeenKeys(tabId = activeTabId) {
|
|
|
5923
6060
|
|
|
5924
6061
|
function actionEntryKey(item) {
|
|
5925
6062
|
const message = item?.message || {};
|
|
6063
|
+
const keyedToolExecution = message.role === "toolExecution" && message.toolCallId;
|
|
5926
6064
|
return [
|
|
5927
|
-
item?.transient ? "transient" : "message",
|
|
5928
|
-
item?.messageIndex ?? -1,
|
|
6065
|
+
keyedToolExecution ? "toolExecution" : item?.transient ? "transient" : "message",
|
|
6066
|
+
keyedToolExecution ? "" : (item?.messageIndex ?? -1),
|
|
5929
6067
|
message.role || "message",
|
|
5930
6068
|
message.toolName || "",
|
|
5931
6069
|
message.toolCallId || "",
|
|
5932
|
-
message.command || "",
|
|
5933
|
-
message.title || "",
|
|
5934
|
-
message.timestamp || "",
|
|
5935
|
-
textFromContent(message.content).slice(0, 240),
|
|
6070
|
+
keyedToolExecution ? "" : message.command || "",
|
|
6071
|
+
keyedToolExecution ? "" : message.title || "",
|
|
6072
|
+
keyedToolExecution ? "" : message.timestamp || "",
|
|
6073
|
+
keyedToolExecution ? "" : textFromContent(message.content).slice(0, 240),
|
|
5936
6074
|
].join("|");
|
|
5937
6075
|
}
|
|
5938
6076
|
|
|
@@ -5974,6 +6112,7 @@ function orderedTranscriptItems() {
|
|
|
5974
6112
|
function renderAllMessages({ preserveScroll = false } = {}) {
|
|
5975
6113
|
const shouldFollow = !preserveScroll && (autoFollowChat || isChatNearBottom());
|
|
5976
6114
|
const previousScrollTop = elements.chat.scrollTop;
|
|
6115
|
+
const reusableToolCards = captureReusableToolCards();
|
|
5977
6116
|
resetChatOutput();
|
|
5978
6117
|
const transcriptItems = orderedTranscriptItems();
|
|
5979
6118
|
for (const item of transcriptItems) {
|
|
@@ -5981,6 +6120,7 @@ function renderAllMessages({ preserveScroll = false } = {}) {
|
|
|
5981
6120
|
messageIndex: item.messageIndex,
|
|
5982
6121
|
transient: item.transient,
|
|
5983
6122
|
animateEntry: shouldAnimateActionEntry(item),
|
|
6123
|
+
reusableToolCards,
|
|
5984
6124
|
});
|
|
5985
6125
|
}
|
|
5986
6126
|
rememberActionEntries(transcriptItems);
|
|
@@ -7589,6 +7729,7 @@ async function refreshAll(tabContext = activeTabContext()) {
|
|
|
7589
7729
|
for (const result of results) {
|
|
7590
7730
|
if (result.status === "rejected") addEvent(result.reason.message || String(result.reason), "error");
|
|
7591
7731
|
}
|
|
7732
|
+
resumeGitWorkflowForActiveTab(tabContext);
|
|
7592
7733
|
}
|
|
7593
7734
|
|
|
7594
7735
|
async function openToNetwork() {
|
|
@@ -8026,8 +8167,12 @@ function handleEvent(event) {
|
|
|
8026
8167
|
scheduleRefreshMessages();
|
|
8027
8168
|
scheduleRefreshFooter();
|
|
8028
8169
|
renderFeedbackTray();
|
|
8029
|
-
|
|
8030
|
-
|
|
8170
|
+
{
|
|
8171
|
+
const workflowTabId = event.tabId || activeTabId;
|
|
8172
|
+
const workflow = gitWorkflowForTab(workflowTabId, { create: false });
|
|
8173
|
+
if (workflow?.active && workflow.step === "generating") {
|
|
8174
|
+
loadGitWorkflowMessage({ requireFresh: true, retries: 3, runId: workflow.runId, tabId: workflowTabId });
|
|
8175
|
+
}
|
|
8031
8176
|
}
|
|
8032
8177
|
break;
|
|
8033
8178
|
case "message_start":
|
|
@@ -8114,7 +8259,11 @@ function handleEvent(event) {
|
|
|
8114
8259
|
syncRunIndicatorFromState(currentState);
|
|
8115
8260
|
renderStatus();
|
|
8116
8261
|
} else if (["set_model", "set_thinking_level", "new_session", "compact"].includes(event.command)) {
|
|
8117
|
-
if (event.command === "new_session")
|
|
8262
|
+
if (event.command === "new_session") {
|
|
8263
|
+
const tabId = event.tabId || activeTabId;
|
|
8264
|
+
forgetLastUserPrompt(tabId);
|
|
8265
|
+
resetGitWorkflowForTab(tabId);
|
|
8266
|
+
}
|
|
8118
8267
|
scheduleRefreshState();
|
|
8119
8268
|
scheduleRefreshMessages();
|
|
8120
8269
|
scheduleRefreshFooter();
|
|
@@ -8252,6 +8401,7 @@ elements.newSessionButton.addEventListener("click", async () => {
|
|
|
8252
8401
|
const response = await api("/api/new-session", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
8253
8402
|
applyResponseTab(response);
|
|
8254
8403
|
forgetLastUserPrompt(tabContext.tabId);
|
|
8404
|
+
resetGitWorkflowForTab(tabContext.tabId);
|
|
8255
8405
|
if (!isCurrentTabContext(tabContext)) return;
|
|
8256
8406
|
await refreshAll(tabContext);
|
|
8257
8407
|
if (isCurrentTabContext(tabContext)) focusPromptInput({ defer: true });
|
|
@@ -291,6 +291,10 @@ assert.match(app, /const TOOL_LIVE_UPDATE_THROTTLE_MS = 80/, "live tool cards sh
|
|
|
291
291
|
assert.match(app, /function updateLiveToolCard\(bubble, message\)[\s\S]*?body\.replaceChildren\(\);[\s\S]*?renderToolExecution\(body, message\);/, "live tool card updates should re-render the existing card body in place");
|
|
292
292
|
assert.match(app, /function scheduleLiveToolRunRender\(run[\s\S]*?liveToolRenderQueue\.set[\s\S]*?TOOL_LIVE_UPDATE_THROTTLE_MS/, "live tool update events should be queued and throttled for smoother browser output");
|
|
293
293
|
assert.match(app, /function handleToolExecutionUpdate\(event\)[\s\S]*?event\.partialResult[\s\S]*?scheduleLiveToolRunRender\(run, \{ scroll: false \}\)/, "live tool_execution_update events should update transcript-visible tool cards without replacing them per event");
|
|
294
|
+
assert.match(app, /function captureReusableToolCards\(\)[\s\S]*?\.message\.toolExecution\[data-tool-call-id\]/, "full transcript re-renders should capture existing tool cards before clearing the chat");
|
|
295
|
+
assert.match(app, /function appendMessage\(message,[\s\S]*?reusableToolCards = null[\s\S]*?reuseToolExecutionBubble\(reusableToolCards, message/, "message rendering should reuse matching tool cards instead of replacing them during refreshes");
|
|
296
|
+
assert.match(app, /function renderAllMessages\(\{ preserveScroll = false \} = \{\}\)[\s\S]*?const reusableToolCards = captureReusableToolCards\(\);[\s\S]*?appendTranscriptMessage\(item\.message,[\s\S]*?reusableToolCards,/, "transcript refreshes should pass reusable tool cards through to item rendering");
|
|
297
|
+
assert.match(app, /const keyedToolExecution = message\.role === "toolExecution" && message\.toolCallId[\s\S]*?keyedToolExecution \? "toolExecution"[\s\S]*?keyedToolExecution \? "" : message\.title[\s\S]*?keyedToolExecution \? "" : message\.timestamp/, "tool action entry identity should stay stable when live transient cards become persisted transcript cards");
|
|
294
298
|
assert.match(app, /appendText\(preview, toolResultPreviewText\(message, 10\), "code-block tool-result-preview-text"\)/, "collapsed tool results should render the first ten preview lines by default");
|
|
295
299
|
assert.match(app, /function assistantDisplayMessages\(message\)/, "assistant history should split thinking and tool-call parts out of the final Assistant output card");
|
|
296
300
|
assert.match(app, /function assistantHasToolCallAfter\(content, index\)/, "assistant text that precedes a tool call should be detectable and suppressible");
|