@hyperdreamer/pi-webui 1.14.0 → 1.15.0
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/dist/client/assets/{CodeViewer-BEbclxBf.js → CodeViewer-BGClPFoI.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-DXWhrIST.js → UnifiedDiffViewer-I9Fc0TRx.js} +1 -1
- package/dist/client/assets/{index-2EIg2vwJ.js → index-DEhtRZU8.js} +56 -56
- package/dist/client/index.html +1 -1
- package/dist/pi-webui-plugins/workspace-tasks/config.js +79 -0
- package/dist/pi-webui-plugins/workspace-tasks/tasksPanelElement.js +926 -142
- package/dist/pi-webui-plugins/workspace-tasks/workspaceTasksClient.js +204 -9
- package/dist/server/sessions/piSessionService.js +22 -12
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionCommandService.js +38 -0
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/docs/plugins.md +42 -2
- package/package.json +1 -1
|
@@ -1,17 +1,103 @@
|
|
|
1
1
|
// Generated from pi-webui-plugins/workspace-tasks/workspaceTasksClient.ts. Do not edit directly.
|
|
2
|
-
import { TASKS_CONFIG_PATH, parseTasksConfigText } from "./config.js";
|
|
2
|
+
import { TASKS_CONFIG_PATH, parseTasksConfigText, serializeWorkspaceTasksConfig, } from "./config.js";
|
|
3
3
|
export const tasksConfigMissingMessage = "No workspace tasks configured here.";
|
|
4
4
|
export const tasksConfigMissingHint = `${TASKS_CONFIG_PATH} is optional. Create it in this workspace if you want custom tasks.`;
|
|
5
|
+
export const tasksConfigInvalidMessage = "Workspace tasks configuration is invalid.";
|
|
6
|
+
export const tasksConfigInvalidHint = `Fix ${TASKS_CONFIG_PATH}, then click Refresh.`;
|
|
5
7
|
export const tasksConfigUnavailableMessage = "Could not load workspace tasks.";
|
|
6
8
|
export const tasksConfigRefreshHint = `Fix ${TASKS_CONFIG_PATH}, then click Refresh.`;
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
+
const configCache = new Map();
|
|
10
|
+
const workspaceRuntimes = new Map();
|
|
11
|
+
const subscribers = new Set();
|
|
12
|
+
let stateEpoch = 0;
|
|
13
|
+
export function loadWorkspaceTasksConfig(files) {
|
|
14
|
+
return readCurrentWorkspaceTasksConfig(files);
|
|
15
|
+
}
|
|
16
|
+
export function getWorkspaceTasksCacheEntry(workspaceKey) {
|
|
17
|
+
return configCache.get(workspaceKey);
|
|
18
|
+
}
|
|
19
|
+
export function ensureWorkspaceTasksConfig(files, workspaceKey) {
|
|
20
|
+
const cached = configCache.get(workspaceKey);
|
|
21
|
+
if (cached !== undefined)
|
|
22
|
+
return cached;
|
|
23
|
+
const runtime = getWorkspaceRuntime(workspaceKey);
|
|
24
|
+
const loadingEntry = {
|
|
25
|
+
state: { kind: "loading" },
|
|
26
|
+
refreshRequired: runtime.refreshRequired,
|
|
27
|
+
};
|
|
28
|
+
configCache.set(workspaceKey, loadingEntry);
|
|
29
|
+
void enqueueWorkspaceOperation(workspaceKey, async (operation) => {
|
|
30
|
+
const state = await loadWorkspaceTasksConfigSafely(files);
|
|
31
|
+
if (state.kind === "unavailable")
|
|
32
|
+
operation.runtime.refreshRequired = true;
|
|
33
|
+
publishState(operation, state, operation.runtime.refreshRequired);
|
|
34
|
+
}).catch(() => undefined);
|
|
35
|
+
return loadingEntry;
|
|
36
|
+
}
|
|
37
|
+
export function refreshWorkspaceTasksConfig(files, workspaceKey) {
|
|
38
|
+
return enqueueWorkspaceOperation(workspaceKey, async (operation) => {
|
|
39
|
+
const state = await loadWorkspaceTasksConfigSafely(files);
|
|
40
|
+
operation.runtime.refreshRequired = state.kind === "unavailable";
|
|
41
|
+
publishState(operation, state, operation.runtime.refreshRequired);
|
|
42
|
+
return state;
|
|
43
|
+
}, (operation) => {
|
|
44
|
+
if (!isCurrentOperation(operation))
|
|
45
|
+
return;
|
|
46
|
+
configCache.set(workspaceKey, {
|
|
47
|
+
state: { kind: "loading" },
|
|
48
|
+
refreshRequired: operation.runtime.refreshRequired,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function guardedWriteWorkspaceTasksConfig(files, workspaceKey, sourceSnapshot, nextConfig) {
|
|
53
|
+
return enqueueWorkspaceOperation(workspaceKey, async (operation) => {
|
|
54
|
+
if (operation.runtime.refreshRequired) {
|
|
55
|
+
return blockMutation(operation, "A successful Refresh is required before another workspace task mutation.");
|
|
56
|
+
}
|
|
57
|
+
const preflight = await loadWorkspaceTasksConfigSafely(files);
|
|
58
|
+
if (preflight.kind === "unavailable") {
|
|
59
|
+
return blockMutation(operation, unavailableDetail(preflight));
|
|
60
|
+
}
|
|
61
|
+
const currentSnapshot = preflight.snapshot;
|
|
62
|
+
if (!snapshotsEqual(sourceSnapshot, currentSnapshot)) {
|
|
63
|
+
return blockMutation(operation, `The ${TASKS_CONFIG_PATH} file changed outside this panel. Refresh before trying again.`, "conflict");
|
|
64
|
+
}
|
|
65
|
+
const payload = serializeWorkspaceTasksConfig(nextConfig);
|
|
66
|
+
try {
|
|
67
|
+
await files.writeFile(TASKS_CONFIG_PATH, payload);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return blockMutation(operation, `Unable to write ${TASKS_CONFIG_PATH}: ${formatUnknownError(error)}`, "write-failed");
|
|
71
|
+
}
|
|
72
|
+
const postWrite = await loadWorkspaceTasksConfigSafely(files);
|
|
73
|
+
if (postWrite.kind === "loaded" && postWrite.snapshot.content === payload) {
|
|
74
|
+
operation.runtime.refreshRequired = false;
|
|
75
|
+
publishState(operation, postWrite, false);
|
|
76
|
+
return { kind: "written", state: postWrite };
|
|
77
|
+
}
|
|
78
|
+
const detail = postWriteReloadDetail(postWrite);
|
|
79
|
+
blockMutation(operation, detail, undefined, postWrite);
|
|
80
|
+
return { kind: "written-but-unreloaded", detail };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export function subscribeWorkspaceTasksConfig(listener) {
|
|
84
|
+
subscribers.add(listener);
|
|
85
|
+
return () => {
|
|
86
|
+
subscribers.delete(listener);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function clearWorkspaceTasksStateForTesting() {
|
|
90
|
+
stateEpoch += 1;
|
|
91
|
+
configCache.clear();
|
|
92
|
+
workspaceRuntimes.clear();
|
|
93
|
+
}
|
|
94
|
+
async function readCurrentWorkspaceTasksConfig(files) {
|
|
9
95
|
let file;
|
|
10
96
|
try {
|
|
11
97
|
file = await files.readFile(TASKS_CONFIG_PATH);
|
|
12
98
|
}
|
|
13
99
|
catch (error) {
|
|
14
|
-
if (
|
|
100
|
+
if (isMissingWorkspaceFileError(error))
|
|
15
101
|
return missing();
|
|
16
102
|
return unavailable(`Unable to read ${TASKS_CONFIG_PATH}: ${formatUnknownError(error)}`);
|
|
17
103
|
}
|
|
@@ -20,15 +106,107 @@ export async function loadWorkspaceTasksConfig(files) {
|
|
|
20
106
|
if (file.truncated)
|
|
21
107
|
return unavailable(`${TASKS_CONFIG_PATH} is too large and was truncated`);
|
|
22
108
|
const result = parseTasksConfigText(file.content);
|
|
23
|
-
if (!result.ok)
|
|
24
|
-
return
|
|
25
|
-
|
|
109
|
+
if (!result.ok) {
|
|
110
|
+
return {
|
|
111
|
+
kind: "invalid",
|
|
112
|
+
message: tasksConfigInvalidMessage,
|
|
113
|
+
hint: tasksConfigInvalidHint,
|
|
114
|
+
detail: result.error,
|
|
115
|
+
snapshot: { kind: "text", content: file.content },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
kind: "loaded",
|
|
120
|
+
config: result.config,
|
|
121
|
+
path: TASKS_CONFIG_PATH,
|
|
122
|
+
snapshot: { kind: "text", content: file.content },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async function loadWorkspaceTasksConfigSafely(files) {
|
|
126
|
+
try {
|
|
127
|
+
return await readCurrentWorkspaceTasksConfig(files);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return unavailable(`Unable to read ${TASKS_CONFIG_PATH}: ${formatUnknownError(error)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function enqueueWorkspaceOperation(workspaceKey, operation, onRequest) {
|
|
134
|
+
const runtime = getWorkspaceRuntime(workspaceKey);
|
|
135
|
+
const context = {
|
|
136
|
+
key: workspaceKey,
|
|
137
|
+
runtime,
|
|
138
|
+
generation: runtime.requestGeneration + 1,
|
|
139
|
+
epoch: stateEpoch,
|
|
140
|
+
};
|
|
141
|
+
runtime.requestGeneration = context.generation;
|
|
142
|
+
onRequest?.(context);
|
|
143
|
+
const result = runtime.tail.then(() => operation(context));
|
|
144
|
+
runtime.tail = result.then(() => undefined, () => undefined);
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
function getWorkspaceRuntime(workspaceKey) {
|
|
148
|
+
const existing = workspaceRuntimes.get(workspaceKey);
|
|
149
|
+
if (existing !== undefined)
|
|
150
|
+
return existing;
|
|
151
|
+
const runtime = {
|
|
152
|
+
requestGeneration: 0,
|
|
153
|
+
refreshRequired: false,
|
|
154
|
+
tail: Promise.resolve(),
|
|
155
|
+
};
|
|
156
|
+
workspaceRuntimes.set(workspaceKey, runtime);
|
|
157
|
+
return runtime;
|
|
158
|
+
}
|
|
159
|
+
function publishState(operation, state, refreshRequired) {
|
|
160
|
+
if (!isCurrentOperation(operation))
|
|
161
|
+
return;
|
|
162
|
+
configCache.set(operation.key, { state, refreshRequired });
|
|
163
|
+
notifySubscribers(operation.key);
|
|
164
|
+
}
|
|
165
|
+
function blockMutation(operation, detail, kind = "preflight-unavailable", postWriteState) {
|
|
166
|
+
operation.runtime.refreshRequired = true;
|
|
167
|
+
if (isCurrentOperation(operation)) {
|
|
168
|
+
const existing = configCache.get(operation.key);
|
|
169
|
+
if (postWriteState !== undefined) {
|
|
170
|
+
configCache.set(operation.key, { state: postWriteState, refreshRequired: true });
|
|
171
|
+
}
|
|
172
|
+
else if (existing !== undefined) {
|
|
173
|
+
configCache.set(operation.key, { ...existing, refreshRequired: true });
|
|
174
|
+
}
|
|
175
|
+
notifySubscribers(operation.key);
|
|
176
|
+
}
|
|
177
|
+
return { kind, detail };
|
|
178
|
+
}
|
|
179
|
+
function isCurrentOperation(operation) {
|
|
180
|
+
return operation.epoch === stateEpoch
|
|
181
|
+
&& workspaceRuntimes.get(operation.key) === operation.runtime
|
|
182
|
+
&& operation.runtime.requestGeneration === operation.generation;
|
|
183
|
+
}
|
|
184
|
+
function notifySubscribers(workspaceKey) {
|
|
185
|
+
for (const listener of subscribers)
|
|
186
|
+
listener(workspaceKey);
|
|
187
|
+
}
|
|
188
|
+
function snapshotsEqual(left, right) {
|
|
189
|
+
if (left.kind !== right.kind)
|
|
190
|
+
return false;
|
|
191
|
+
if (left.kind === "missing" || right.kind === "missing")
|
|
192
|
+
return true;
|
|
193
|
+
return left.content === right.content;
|
|
194
|
+
}
|
|
195
|
+
function postWriteReloadDetail(result) {
|
|
196
|
+
if (result.kind === "loaded")
|
|
197
|
+
return `Unable to reload ${TASKS_CONFIG_PATH}: the post-write snapshot did not match the canonical payload.`;
|
|
198
|
+
if (result.kind === "missing")
|
|
199
|
+
return `Unable to reload ${TASKS_CONFIG_PATH}: the file is missing after the write.`;
|
|
200
|
+
if (result.kind === "invalid")
|
|
201
|
+
return `Unable to reload ${TASKS_CONFIG_PATH}: ${result.detail}`;
|
|
202
|
+
return `Unable to reload ${TASKS_CONFIG_PATH}: ${unavailableDetail(result)}`;
|
|
26
203
|
}
|
|
27
204
|
function missing() {
|
|
28
205
|
return {
|
|
29
206
|
kind: "missing",
|
|
30
207
|
message: tasksConfigMissingMessage,
|
|
31
208
|
hint: tasksConfigMissingHint,
|
|
209
|
+
snapshot: { kind: "missing" },
|
|
32
210
|
};
|
|
33
211
|
}
|
|
34
212
|
function unavailable(detail) {
|
|
@@ -39,8 +217,25 @@ function unavailable(detail) {
|
|
|
39
217
|
detail,
|
|
40
218
|
};
|
|
41
219
|
}
|
|
42
|
-
function
|
|
43
|
-
return
|
|
220
|
+
function unavailableDetail(result) {
|
|
221
|
+
return result.detail ?? tasksConfigUnavailableMessage;
|
|
222
|
+
}
|
|
223
|
+
function isMissingWorkspaceFileError(error) {
|
|
224
|
+
const message = errorField(error, "message");
|
|
225
|
+
const code = errorField(error, "code");
|
|
226
|
+
return message === "Path does not exist"
|
|
227
|
+
|| code?.toLowerCase() === "enoent"
|
|
228
|
+
|| message?.toLowerCase() === "enoent"
|
|
229
|
+
|| message?.toLowerCase().includes("no such file or directory") === true
|
|
230
|
+
|| code?.toLowerCase().includes("no such file or directory") === true;
|
|
231
|
+
}
|
|
232
|
+
function errorField(error, field) {
|
|
233
|
+
if (typeof error === "object" && error !== null && hasStringField(error, field))
|
|
234
|
+
return error[field];
|
|
235
|
+
return error instanceof Error && field === "message" ? error.message : undefined;
|
|
236
|
+
}
|
|
237
|
+
function hasStringField(value, field) {
|
|
238
|
+
return field in value && typeof Reflect.get(value, field) === "string";
|
|
44
239
|
}
|
|
45
240
|
function formatUnknownError(error) {
|
|
46
241
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -564,7 +564,12 @@ export class PiSessionService {
|
|
|
564
564
|
},
|
|
565
565
|
onCompactionEnd: (session, result, detail) => {
|
|
566
566
|
this.endSessionEntryMutation(session);
|
|
567
|
-
|
|
567
|
+
const activity = result === "success"
|
|
568
|
+
? { label: "compaction complete", phase: "idle" }
|
|
569
|
+
: result === "cancelled"
|
|
570
|
+
? { label: "compaction cancelled", phase: "idle" }
|
|
571
|
+
: { label: "compaction failed", phase: "error" };
|
|
572
|
+
this.publishActivity(session, activity.label, activity.phase, detail);
|
|
568
573
|
this.publishStatus(session);
|
|
569
574
|
},
|
|
570
575
|
reloadSession: (session) => this.reloadSessionRuntime(session),
|
|
@@ -2508,26 +2513,31 @@ export class PiSessionService {
|
|
|
2508
2513
|
}
|
|
2509
2514
|
}
|
|
2510
2515
|
async abortSessionOperations(session) {
|
|
2511
|
-
|
|
2512
|
-
|
|
2516
|
+
const failures = [];
|
|
2517
|
+
try {
|
|
2518
|
+
session.abortCompaction();
|
|
2519
|
+
this.commandService.cancelManualCompaction(session.sessionId);
|
|
2520
|
+
}
|
|
2521
|
+
catch (error) {
|
|
2522
|
+
failures.push(error);
|
|
2523
|
+
}
|
|
2513
2524
|
try {
|
|
2514
2525
|
session.abortBranchSummary?.();
|
|
2515
2526
|
}
|
|
2516
2527
|
catch (error) {
|
|
2517
|
-
|
|
2518
|
-
branchSummaryAbortError = error;
|
|
2528
|
+
failures.push(error);
|
|
2519
2529
|
}
|
|
2520
2530
|
try {
|
|
2521
2531
|
await session.abort();
|
|
2522
2532
|
}
|
|
2523
|
-
catch (
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
throw
|
|
2533
|
+
catch (error) {
|
|
2534
|
+
failures.push(error);
|
|
2535
|
+
}
|
|
2536
|
+
if (failures.length === 1)
|
|
2537
|
+
throw failures[0];
|
|
2538
|
+
if (failures.length > 1) {
|
|
2539
|
+
throw new AggregateError(failures, "Failed to abort session operations", { cause: failures[failures.length - 1] });
|
|
2528
2540
|
}
|
|
2529
|
-
if (branchSummaryAbortFailed)
|
|
2530
|
-
throw branchSummaryAbortError;
|
|
2531
2541
|
}
|
|
2532
2542
|
async assertWritable(ref) {
|
|
2533
2543
|
if ((await this.getArchived(ref)) !== undefined)
|