@hyperdreamer/pi-webui 1.14.1 → 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/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);
|
package/docs/plugins.md
CHANGED
|
@@ -301,8 +301,6 @@ Configure workspace tasks in `.pi-webui/tasks.json`:
|
|
|
301
301
|
}
|
|
302
302
|
```
|
|
303
303
|
|
|
304
|
-
Open a workspace, choose the **Tasks** tab, and click **Run** next to a task. Commands run in the workspace root because PI WEBUI creates the terminal for that workspace.
|
|
305
|
-
|
|
306
304
|
Task fields:
|
|
307
305
|
|
|
308
306
|
- `version`: must be `1`.
|
|
@@ -314,6 +312,48 @@ Task fields:
|
|
|
314
312
|
- `group`: optional group heading.
|
|
315
313
|
- `confirm`: optional boolean. When true, the browser asks before dispatching the command.
|
|
316
314
|
|
|
315
|
+
Open a workspace, choose the **Tasks** tab, and use the editor to manage tasks in the browser:
|
|
316
|
+
|
|
317
|
+
- **Add** creates a task and appends it to the task array.
|
|
318
|
+
- **Edit** updates the task at its existing position in the array.
|
|
319
|
+
- **Delete** removes the selected task without reordering the remaining tasks.
|
|
320
|
+
- **Reset** is offered only for a complete, readable task-file snapshot whose text is invalid, including parse- or schema-invalid text. It is unavailable for binary, truncated, or otherwise unavailable files. After confirmation, it replaces the file with the canonical empty version-1 configuration `{"version":1,"tasks":[]}`.
|
|
321
|
+
|
|
322
|
+
Browser saves write canonical JSON for the supported task fields. They preserve supported task values and task-array order, but canonicalize whitespace and key order and drop unsupported fields; original JSON formatting is not preserved.
|
|
323
|
+
|
|
324
|
+
For a multiline script, encode line feeds as `\n` in JSON:
|
|
325
|
+
|
|
326
|
+
```json
|
|
327
|
+
{
|
|
328
|
+
"version": 1,
|
|
329
|
+
"tasks": [
|
|
330
|
+
{
|
|
331
|
+
"id": "verify",
|
|
332
|
+
"title": "Build and test",
|
|
333
|
+
"command": "set -e\nnpm run build\nnpm test"
|
|
334
|
+
}
|
|
335
|
+
]
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Click **Run** next to a task to send one terminal request. The server starts one dedicated terminal in the workspace root with `$SHELL -lc`, so lines in a multiline script share shell state such as variables and the current directory. The shell's final exit status determines whether the run succeeds or fails. For POSIX-compatible fail-fast behavior, use syntax such as:
|
|
340
|
+
|
|
341
|
+
```sh
|
|
342
|
+
set -e
|
|
343
|
+
npm run build
|
|
344
|
+
npm test
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
or:
|
|
348
|
+
|
|
349
|
+
```sh
|
|
350
|
+
npm run build && npm test
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
These are POSIX-compatible examples, not shell-neutral guarantees.
|
|
354
|
+
|
|
355
|
+
If the task file changed after the browser loaded it, the editor refuses the stale save as a best-effort conflict guard instead of merging the changes. Use **Refresh** to load the latest file, review it, and retry. This protects against sequential stale browser edits; it is not an atomic cross-tab or cross-process compare-and-swap guarantee.
|
|
356
|
+
|
|
317
357
|
Review task configs before running them, especially in shared projects. Workspace Tasks runs trusted shell commands from your repositories.
|
|
318
358
|
|
|
319
359
|
## Discovery and packaging
|
package/package.json
CHANGED