@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,48 +1,115 @@
|
|
|
1
1
|
// Generated from pi-webui-plugins/workspace-tasks/tasksPanelElement.ts. Do not edit directly.
|
|
2
|
-
import { TASKS_CONFIG_PATH } from "./config.js";
|
|
2
|
+
import { TASKS_CONFIG_PATH, appendWorkspaceTask, emptyWorkspaceTasksConfig, removeWorkspaceTaskAt, replaceWorkspaceTaskAt, suggestWorkspaceTaskId, validateAndNormalizeDraft, } from "./config.js";
|
|
3
3
|
import { runWorkspaceTaskInTerminal } from "./taskRunner.js";
|
|
4
|
-
import {
|
|
4
|
+
import { ensureWorkspaceTasksConfig, getWorkspaceTasksCacheEntry, guardedWriteWorkspaceTasksConfig, refreshWorkspaceTasksConfig, subscribeWorkspaceTasksConfig, } from "./workspaceTasksClient.js";
|
|
5
5
|
export const tasksPanelTagName = "pi-webui-workspace-tasks-panel";
|
|
6
|
-
const configChangedEvent = "pi-webui-workspace-tasks-config-changed";
|
|
7
|
-
const configCache = new Map();
|
|
8
6
|
export function defineTasksPanelElement() {
|
|
9
7
|
if (!customElements.get(tasksPanelTagName))
|
|
10
8
|
customElements.define(tasksPanelTagName, PiWebUiTasksPanel);
|
|
11
9
|
}
|
|
12
10
|
export function tasksPanelBadge(context) {
|
|
13
|
-
const
|
|
14
|
-
return state
|
|
11
|
+
const entry = getWorkspaceTasksCacheEntry(cacheKeyForContext(context));
|
|
12
|
+
return entry !== undefined && (entry.state.kind === "unavailable" || entry.refreshRequired) ? "!" : undefined;
|
|
15
13
|
}
|
|
16
14
|
class PiWebUiTasksPanel extends HTMLElement {
|
|
17
15
|
contextValue;
|
|
18
16
|
runningTaskId;
|
|
19
17
|
status;
|
|
18
|
+
mode = "view";
|
|
19
|
+
operation;
|
|
20
|
+
editor;
|
|
21
|
+
deleteState;
|
|
22
|
+
resetState;
|
|
23
|
+
failure;
|
|
24
|
+
validationErrors;
|
|
25
|
+
idManuallyEdited = false;
|
|
26
|
+
pendingRefreshFocus;
|
|
27
|
+
operationGeneration = 0;
|
|
28
|
+
terminalRunGeneration = 0;
|
|
29
|
+
panelRefreshRequired = false;
|
|
30
|
+
selectionGeneration = 0;
|
|
31
|
+
connected = false;
|
|
32
|
+
unsubscribe;
|
|
20
33
|
root;
|
|
21
|
-
onConfigChanged = () => {
|
|
34
|
+
onConfigChanged = (workspaceKey) => {
|
|
35
|
+
const context = this.contextValue;
|
|
36
|
+
if (!this.connected || context === undefined || cacheKeyForContext(context) !== workspaceKey)
|
|
37
|
+
return;
|
|
38
|
+
context.host.requestRender();
|
|
22
39
|
this.render();
|
|
23
40
|
};
|
|
41
|
+
onKeyDown = (event) => {
|
|
42
|
+
if (event.key !== "Escape" || this.operation !== undefined)
|
|
43
|
+
return;
|
|
44
|
+
if (this.mode === "add" || this.mode === "edit" || (this.mode === "conflicted" && this.failure?.action === "editor") || (this.mode === "needs-refresh-after-write" && this.failure?.action === "editor")) {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
event.stopPropagation();
|
|
47
|
+
this.cancelEditor();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (this.mode === "delete-confirm" || (this.mode === "conflicted" && this.failure?.action === "delete") || (this.mode === "needs-refresh-after-write" && this.failure?.action === "delete")) {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
event.stopPropagation();
|
|
53
|
+
this.cancelDelete();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (this.mode === "reset-confirm" || (this.mode === "conflicted" && this.failure?.action === "reset") || (this.mode === "needs-refresh-after-write" && this.failure?.action === "reset")) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
event.stopPropagation();
|
|
59
|
+
this.cancelReset();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (this.mode === "refresh-discard-confirm") {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
event.stopPropagation();
|
|
65
|
+
this.cancelRefreshDiscard();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
24
68
|
constructor() {
|
|
25
69
|
super();
|
|
26
70
|
this.root = this.attachShadow({ mode: "open" });
|
|
71
|
+
this.root.addEventListener("keydown", (event) => {
|
|
72
|
+
if (event instanceof KeyboardEvent)
|
|
73
|
+
this.onKeyDown(event);
|
|
74
|
+
});
|
|
27
75
|
}
|
|
28
76
|
set context(value) {
|
|
29
77
|
const previousKey = this.contextValue === undefined ? undefined : cacheKeyForContext(this.contextValue);
|
|
30
78
|
const nextKey = value === undefined ? undefined : cacheKeyForContext(value);
|
|
31
79
|
this.contextValue = value;
|
|
32
|
-
// Parent app updates should not rebuild this shadow DOM for the same workspace:
|
|
33
|
-
// doing so resets the mobile scroll position and can replace buttons mid-click.
|
|
34
80
|
if (previousKey === nextKey)
|
|
35
81
|
return;
|
|
82
|
+
this.selectionGeneration += 1;
|
|
83
|
+
this.operationGeneration += 1;
|
|
84
|
+
this.terminalRunGeneration += 1;
|
|
85
|
+
this.operation = undefined;
|
|
36
86
|
this.runningTaskId = undefined;
|
|
87
|
+
this.mode = "view";
|
|
88
|
+
this.editor = undefined;
|
|
89
|
+
this.deleteState = undefined;
|
|
90
|
+
this.resetState = undefined;
|
|
91
|
+
this.failure = undefined;
|
|
92
|
+
this.validationErrors = undefined;
|
|
93
|
+
this.pendingRefreshFocus = undefined;
|
|
94
|
+
this.idManuallyEdited = false;
|
|
95
|
+
this.panelRefreshRequired = false;
|
|
37
96
|
this.status = undefined;
|
|
38
97
|
this.render();
|
|
39
98
|
}
|
|
40
99
|
connectedCallback() {
|
|
41
|
-
|
|
100
|
+
this.connected = true;
|
|
101
|
+
this.unsubscribe = subscribeWorkspaceTasksConfig(this.onConfigChanged);
|
|
42
102
|
this.render();
|
|
43
103
|
}
|
|
44
104
|
disconnectedCallback() {
|
|
45
|
-
|
|
105
|
+
this.connected = false;
|
|
106
|
+
this.selectionGeneration += 1;
|
|
107
|
+
this.operationGeneration += 1;
|
|
108
|
+
this.terminalRunGeneration += 1;
|
|
109
|
+
this.operation = undefined;
|
|
110
|
+
this.runningTaskId = undefined;
|
|
111
|
+
this.unsubscribe?.();
|
|
112
|
+
this.unsubscribe = undefined;
|
|
46
113
|
}
|
|
47
114
|
render() {
|
|
48
115
|
const context = this.contextValue;
|
|
@@ -50,118 +117,707 @@ class PiWebUiTasksPanel extends HTMLElement {
|
|
|
50
117
|
this.root.innerHTML = `${taskStyles()}<section class="empty">Select a workspace.</section>`;
|
|
51
118
|
return;
|
|
52
119
|
}
|
|
53
|
-
const
|
|
120
|
+
const entry = ensureWorkspaceTasksConfig(context.files, cacheKeyForContext(context));
|
|
121
|
+
const refreshDisabled = this.operation !== undefined || this.isConfirmationMode();
|
|
122
|
+
const showAdd = this.mode === "view"
|
|
123
|
+
&& !this.isRefreshRequired(entry)
|
|
124
|
+
&& (entry.state.kind === "loaded" || entry.state.kind === "missing");
|
|
125
|
+
const showReset = this.mode === "view" && !this.isRefreshRequired(entry) && entry.state.kind === "invalid";
|
|
54
126
|
this.root.innerHTML = `
|
|
55
127
|
${taskStyles()}
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
128
|
+
<main class="tasks-panel" data-panel-mode="${escapeAttr(this.mode)}">
|
|
129
|
+
<section class="toolbar">
|
|
130
|
+
<h2 data-panel-heading tabindex="-1">Workspace Tasks</h2>
|
|
131
|
+
<span class="toolbar-tasks">
|
|
132
|
+
${showAdd ? `<button type="button" class="secondary" data-add-task>Add Task</button>` : ""}
|
|
133
|
+
<button type="button" class="secondary" data-refresh-config ${refreshDisabled ? "disabled" : ""}>Refresh</button>
|
|
134
|
+
<button type="button" class="secondary" data-open-terminal>Open Terminal</button>
|
|
135
|
+
</span>
|
|
136
|
+
</section>
|
|
137
|
+
${this.renderStatus()}
|
|
138
|
+
<section class="viewer tasks-viewer">
|
|
139
|
+
${this.renderBody(entry, showReset)}
|
|
140
|
+
</section>
|
|
141
|
+
</main>
|
|
142
|
+
`;
|
|
143
|
+
this.bindHandlers(context);
|
|
144
|
+
}
|
|
145
|
+
isRefreshRequired(entry) {
|
|
146
|
+
return this.panelRefreshRequired || entry.refreshRequired;
|
|
147
|
+
}
|
|
148
|
+
isConfirmationMode() {
|
|
149
|
+
return this.mode === "delete-confirm" || this.mode === "reset-confirm" || this.mode === "refresh-discard-confirm";
|
|
150
|
+
}
|
|
151
|
+
renderBody(entry, showReset) {
|
|
152
|
+
if (this.mode === "add" || this.mode === "edit")
|
|
153
|
+
return this.renderEditor(entry, false);
|
|
154
|
+
if (this.mode === "delete-confirm")
|
|
155
|
+
return this.renderDeleteConfirmation(false);
|
|
156
|
+
if (this.mode === "reset-confirm")
|
|
157
|
+
return this.renderResetConfirmation(false);
|
|
158
|
+
if (this.mode === "refresh-discard-confirm")
|
|
159
|
+
return this.renderRefreshDiscardConfirmation();
|
|
160
|
+
if (this.mode === "conflicted" || this.mode === "needs-refresh-after-write") {
|
|
161
|
+
const action = this.failure?.action;
|
|
162
|
+
if (action === "editor")
|
|
163
|
+
return this.renderEditor(entry, true);
|
|
164
|
+
if (action === "delete")
|
|
165
|
+
return this.renderDeleteConfirmation(true);
|
|
166
|
+
if (action === "reset")
|
|
167
|
+
return this.renderResetConfirmation(true);
|
|
168
|
+
}
|
|
169
|
+
return this.renderConfigState(entry, showReset);
|
|
170
|
+
}
|
|
171
|
+
renderConfigState(entry, showReset) {
|
|
172
|
+
const state = entry.state;
|
|
173
|
+
if (state.kind === "loading")
|
|
174
|
+
return `<p class="muted" data-loading>Loading ${escapeHtml(TASKS_CONFIG_PATH)}...</p>`;
|
|
175
|
+
if (state.kind === "missing") {
|
|
176
|
+
return `${this.renderRefreshRequired(entry)}<div class="empty-state"><strong>${escapeHtml(state.message)}</strong><p>${escapeHtml(state.hint)}</p></div>`;
|
|
177
|
+
}
|
|
178
|
+
if (state.kind === "invalid") {
|
|
179
|
+
return `
|
|
180
|
+
${this.renderRefreshRequired(entry)}
|
|
181
|
+
<div class="status error" data-invalid-state>
|
|
182
|
+
<strong>${escapeHtml(state.message)}</strong>
|
|
183
|
+
<p>${escapeHtml(state.hint)}</p>
|
|
184
|
+
<pre class="diagnostic">${escapeHtml(state.detail)}</pre>
|
|
185
|
+
${showReset ? `<button type="button" class="danger-secondary" data-reset-tasks-file>Reset Tasks File</button>` : ""}
|
|
186
|
+
</div>
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
if (state.kind === "unavailable") {
|
|
190
|
+
return `
|
|
191
|
+
<div class="status error" data-unavailable-state>
|
|
192
|
+
<strong>${escapeHtml(state.message)}</strong>
|
|
193
|
+
<p>${escapeHtml(state.hint)}</p>
|
|
194
|
+
${state.detail === undefined ? "" : `<pre class="diagnostic">${escapeHtml(state.detail)}</pre>`}
|
|
195
|
+
</div>
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
if (state.config.tasks.length === 0) {
|
|
199
|
+
return `${this.renderRefreshRequired(entry)}<p class="muted">No tasks are defined in ${escapeHtml(state.path)}. Add tasks to the file, then click Refresh.</p>`;
|
|
200
|
+
}
|
|
201
|
+
return `
|
|
202
|
+
${this.renderRefreshRequired(entry)}
|
|
203
|
+
<p class="muted">Tasks run as one script in one dedicated workspace terminal. Use <code>set -e</code> or <code>&&</code> when the script should stop after a failure.</p>
|
|
204
|
+
${renderTaskGroups(state.config.tasks, this.runningTaskId, this.isRefreshRequired(entry) || this.operation !== undefined)}
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
207
|
+
renderRefreshRequired(entry) {
|
|
208
|
+
if (!this.isRefreshRequired(entry))
|
|
209
|
+
return "";
|
|
210
|
+
return `<div class="status warning" data-refresh-required role="status">Refresh the workspace tasks file before making another change.</div>`;
|
|
211
|
+
}
|
|
212
|
+
renderStatus() {
|
|
213
|
+
if (this.status === undefined)
|
|
214
|
+
return "";
|
|
215
|
+
const detail = this.status.detail === undefined ? "" : `<pre class="diagnostic">${escapeHtml(this.status.detail)}</pre>`;
|
|
216
|
+
return `<div class="status panel-status ${escapeAttr(this.status.kind)}" data-panel-status role="status" aria-live="polite" tabindex="-1">${escapeHtml(this.status.message)}${detail}</div>`;
|
|
217
|
+
}
|
|
218
|
+
renderEditor(entry, locked) {
|
|
219
|
+
const editor = this.editor;
|
|
220
|
+
if (editor === undefined)
|
|
221
|
+
return "";
|
|
222
|
+
const validation = this.validateEditor(entry);
|
|
223
|
+
const errors = this.validationErrors ?? {};
|
|
224
|
+
const titleError = errors.title;
|
|
225
|
+
const commandError = errors.command;
|
|
226
|
+
const idError = errors.id;
|
|
227
|
+
const saveDisabled = locked
|
|
228
|
+
|| this.operation !== undefined
|
|
229
|
+
|| this.isRefreshRequired(entry)
|
|
230
|
+
|| (entry.state.kind !== "loaded" && entry.state.kind !== "missing")
|
|
231
|
+
|| !validation.ok;
|
|
232
|
+
const titleDescribedBy = titleError === undefined ? "" : ` aria-describedby="task-title-error"`;
|
|
233
|
+
const idDescribedBy = idError === undefined ? "" : ` aria-describedby="task-id-error"`;
|
|
234
|
+
const commandDescribedBy = commandError === undefined ? "task-command-help" : "task-command-help task-command-error";
|
|
235
|
+
const heading = editor.originalIndex === undefined ? "Add Task" : "Edit Task";
|
|
236
|
+
const failureRefresh = locked ? `<button type="button" class="secondary" data-refresh-after-failure>Refresh</button>` : "";
|
|
237
|
+
return `
|
|
238
|
+
<section class="task-editor" data-task-editor>
|
|
239
|
+
<h3>${heading}</h3>
|
|
240
|
+
<form class="task-form" data-task-form>
|
|
241
|
+
<label for="task-title">Title <span class="required" aria-hidden="true">*</span></label>
|
|
242
|
+
<input id="task-title" name="title" type="text" value="${escapeAttr(editor.draft.title)}" placeholder="Build app" data-editor-title aria-required="true"${titleError === undefined ? "" : ` aria-invalid="true"`}${titleDescribedBy} ${locked ? "disabled" : ""}>
|
|
243
|
+
${renderFieldError("title", titleError)}
|
|
244
|
+
|
|
245
|
+
<label for="task-command">Command script <span class="required" aria-hidden="true">*</span></label>
|
|
246
|
+
<textarea id="task-command" name="command" data-editor-command aria-required="true" aria-describedby="${commandDescribedBy}"${commandError === undefined ? "" : ` aria-invalid="true"`}${locked ? " disabled" : ""}>${escapeHtml(editor.draft.command)}</textarea>
|
|
247
|
+
<p id="task-command-help" class="field-help">Runs once in one terminal through the server shell. Use <code>set -e</code> or <code>&&</code> for fail-fast behavior.</p>
|
|
248
|
+
${renderFieldError("command", commandError)}
|
|
249
|
+
|
|
250
|
+
<label for="task-id">ID <span class="required" aria-hidden="true">*</span></label>
|
|
251
|
+
<input id="task-id" name="id" type="text" value="${escapeAttr(editor.draft.id)}" placeholder="Auto-generated from title" data-editor-id aria-required="true"${idError === undefined ? "" : ` aria-invalid="true"`}${idDescribedBy} ${locked ? "disabled" : ""}>
|
|
252
|
+
${renderFieldError("id", idError)}
|
|
253
|
+
|
|
254
|
+
<label for="task-description">Description</label>
|
|
255
|
+
<input id="task-description" name="description" type="text" value="${escapeAttr(editor.draft.description)}" placeholder="Optional description" data-editor-description ${locked ? "disabled" : ""}>
|
|
256
|
+
|
|
257
|
+
<label for="task-group">Group</label>
|
|
258
|
+
<input id="task-group" name="group" type="text" value="${escapeAttr(editor.draft.group)}" placeholder="Optional group name" data-editor-group ${locked ? "disabled" : ""}>
|
|
259
|
+
|
|
260
|
+
<span class="checkbox-field">
|
|
261
|
+
<input id="task-confirm" name="confirm" type="checkbox" ${editor.draft.confirm ? "checked" : ""} data-editor-confirm ${locked ? "disabled" : ""}>
|
|
262
|
+
<label for="task-confirm">Require confirmation before running</label>
|
|
263
|
+
</span>
|
|
264
|
+
|
|
265
|
+
<div class="editor-actions">
|
|
266
|
+
<button type="button" class="secondary" data-cancel-editor>Cancel</button>
|
|
267
|
+
${failureRefresh}
|
|
268
|
+
<button type="button" class="primary" data-save-task ${saveDisabled ? "disabled" : ""}>Save Task</button>
|
|
269
|
+
</div>
|
|
270
|
+
</form>
|
|
62
271
|
</section>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
renderDeleteConfirmation(locked) {
|
|
275
|
+
const state = this.deleteState;
|
|
276
|
+
if (state === undefined)
|
|
277
|
+
return "";
|
|
278
|
+
return `
|
|
279
|
+
<section class="confirmation" data-delete-confirmation>
|
|
280
|
+
<h3>Delete Task</h3>
|
|
281
|
+
<p>Are you sure you want to delete <strong>${escapeHtml(state.task.title)}</strong>?</p>
|
|
282
|
+
<pre class="task-script">${escapeHtml(state.task.command)}</pre>
|
|
283
|
+
<div class="editor-actions">
|
|
284
|
+
<button type="button" class="secondary" data-cancel-delete>Cancel</button>
|
|
285
|
+
${locked ? `<button type="button" class="secondary" data-refresh-after-failure>Refresh</button>` : `<button type="button" class="danger" data-confirm-delete ${this.operation === undefined ? "" : "disabled"}>Delete Task</button>`}
|
|
286
|
+
</div>
|
|
287
|
+
</section>
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
renderResetConfirmation(locked) {
|
|
291
|
+
if (this.resetState === undefined)
|
|
292
|
+
return "";
|
|
293
|
+
return `
|
|
294
|
+
<section class="confirmation" data-reset-confirmation>
|
|
295
|
+
<h3>Reset Tasks File</h3>
|
|
296
|
+
<p>This replaces the invalid contents of <code>${escapeHtml(TASKS_CONFIG_PATH)}</code> with an empty version 1 tasks file.</p>
|
|
297
|
+
<div class="editor-actions">
|
|
298
|
+
<button type="button" class="secondary" data-cancel-reset>Cancel</button>
|
|
299
|
+
${locked ? `<button type="button" class="secondary" data-refresh-after-failure>Refresh</button>` : `<button type="button" class="danger" data-confirm-reset ${this.operation === undefined ? "" : "disabled"}>Reset Tasks File</button>`}
|
|
300
|
+
</div>
|
|
301
|
+
</section>
|
|
302
|
+
`;
|
|
303
|
+
}
|
|
304
|
+
renderRefreshDiscardConfirmation() {
|
|
305
|
+
return `
|
|
306
|
+
<section class="confirmation" data-refresh-discard-confirmation>
|
|
307
|
+
<h3>Discard draft and refresh?</h3>
|
|
308
|
+
<p>Your unsaved task draft will be discarded. Refresh loads the authoritative workspace file.</p>
|
|
309
|
+
<div class="editor-actions">
|
|
310
|
+
<button type="button" class="secondary" data-cancel-refresh-discard>Cancel</button>
|
|
311
|
+
<button type="button" class="primary" data-confirm-refresh-discard>Discard & Refresh</button>
|
|
312
|
+
</div>
|
|
66
313
|
</section>
|
|
67
314
|
`;
|
|
315
|
+
}
|
|
316
|
+
bindHandlers(context) {
|
|
317
|
+
this.root.querySelector("button[data-add-task]")?.addEventListener("click", () => {
|
|
318
|
+
this.openAddTaskEditor();
|
|
319
|
+
});
|
|
68
320
|
this.root.querySelector("button[data-refresh-config]")?.addEventListener("click", () => {
|
|
69
|
-
void this.
|
|
321
|
+
void this.requestRefresh(context);
|
|
322
|
+
});
|
|
323
|
+
this.root.querySelector("button[data-open-terminal]")?.addEventListener("click", () => {
|
|
324
|
+
this.openWorkspaceTerminal();
|
|
325
|
+
});
|
|
326
|
+
this.root.querySelector("button[data-reset-tasks-file]")?.addEventListener("click", () => {
|
|
327
|
+
this.openResetConfirmation(context);
|
|
328
|
+
});
|
|
329
|
+
this.root.querySelector("button[data-cancel-editor]")?.addEventListener("click", () => {
|
|
330
|
+
this.cancelEditor();
|
|
331
|
+
});
|
|
332
|
+
this.root.querySelector("button[data-save-task]")?.addEventListener("click", () => {
|
|
333
|
+
void this.saveTask(context);
|
|
334
|
+
});
|
|
335
|
+
this.root.querySelector("button[data-cancel-delete]")?.addEventListener("click", () => {
|
|
336
|
+
this.cancelDelete();
|
|
337
|
+
});
|
|
338
|
+
this.root.querySelector("button[data-confirm-delete]")?.addEventListener("click", () => {
|
|
339
|
+
void this.confirmDelete(context);
|
|
340
|
+
});
|
|
341
|
+
this.root.querySelector("button[data-cancel-reset]")?.addEventListener("click", () => {
|
|
342
|
+
this.cancelReset();
|
|
343
|
+
});
|
|
344
|
+
this.root.querySelector("button[data-confirm-reset]")?.addEventListener("click", () => {
|
|
345
|
+
void this.confirmReset(context);
|
|
346
|
+
});
|
|
347
|
+
this.root.querySelector("button[data-cancel-refresh-discard]")?.addEventListener("click", () => {
|
|
348
|
+
this.cancelRefreshDiscard();
|
|
349
|
+
});
|
|
350
|
+
this.root.querySelector("button[data-confirm-refresh-discard]")?.addEventListener("click", () => {
|
|
351
|
+
void this.confirmRefreshDiscard(context);
|
|
352
|
+
});
|
|
353
|
+
this.root.querySelector("button[data-refresh-after-failure]")?.addEventListener("click", () => {
|
|
354
|
+
void this.requestRefresh(context, true);
|
|
70
355
|
});
|
|
71
356
|
for (const button of this.root.querySelectorAll("button[data-task-id]")) {
|
|
72
357
|
button.addEventListener("click", () => {
|
|
73
358
|
void this.dispatchTaskById(context, button.getAttribute("data-task-id"));
|
|
74
359
|
});
|
|
75
360
|
}
|
|
76
|
-
this.root.
|
|
77
|
-
|
|
361
|
+
for (const button of this.root.querySelectorAll("button[data-edit-task]")) {
|
|
362
|
+
button.addEventListener("click", () => {
|
|
363
|
+
this.openEditTaskEditor(context, button.getAttribute("data-edit-task"));
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
for (const button of this.root.querySelectorAll("button[data-delete-task]")) {
|
|
367
|
+
button.addEventListener("click", () => {
|
|
368
|
+
this.openDeleteConfirmation(context, button.getAttribute("data-delete-task"));
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
const title = this.root.querySelector("input[data-editor-title]");
|
|
372
|
+
title?.addEventListener("input", (event) => {
|
|
373
|
+
if (!(event.target instanceof HTMLInputElement) || this.editor === undefined)
|
|
374
|
+
return;
|
|
375
|
+
this.editor.draft.title = event.target.value;
|
|
376
|
+
if (!this.idManuallyEdited) {
|
|
377
|
+
this.editor.draft.id = suggestWorkspaceTaskId(event.target.value);
|
|
378
|
+
const id = this.root.querySelector("input[data-editor-id]");
|
|
379
|
+
if (id !== null)
|
|
380
|
+
id.value = this.editor.draft.id;
|
|
381
|
+
}
|
|
382
|
+
this.updateValidationInPlace(context);
|
|
383
|
+
});
|
|
384
|
+
const id = this.root.querySelector("input[data-editor-id]");
|
|
385
|
+
id?.addEventListener("input", (event) => {
|
|
386
|
+
if (!(event.target instanceof HTMLInputElement) || this.editor === undefined)
|
|
387
|
+
return;
|
|
388
|
+
this.editor.draft.id = event.target.value;
|
|
389
|
+
this.idManuallyEdited = true;
|
|
390
|
+
this.updateValidationInPlace(context);
|
|
391
|
+
});
|
|
392
|
+
const command = this.root.querySelector("textarea[data-editor-command]");
|
|
393
|
+
command?.addEventListener("input", (event) => {
|
|
394
|
+
if (!(event.target instanceof HTMLTextAreaElement) || this.editor === undefined)
|
|
395
|
+
return;
|
|
396
|
+
this.editor.draft.command = event.target.value;
|
|
397
|
+
this.updateValidationInPlace(context);
|
|
398
|
+
});
|
|
399
|
+
const description = this.root.querySelector("input[data-editor-description]");
|
|
400
|
+
description?.addEventListener("input", (event) => {
|
|
401
|
+
if (!(event.target instanceof HTMLInputElement) || this.editor === undefined)
|
|
402
|
+
return;
|
|
403
|
+
this.editor.draft.description = event.target.value;
|
|
404
|
+
this.updateValidationInPlace(context);
|
|
405
|
+
});
|
|
406
|
+
const group = this.root.querySelector("input[data-editor-group]");
|
|
407
|
+
group?.addEventListener("input", (event) => {
|
|
408
|
+
if (!(event.target instanceof HTMLInputElement) || this.editor === undefined)
|
|
409
|
+
return;
|
|
410
|
+
this.editor.draft.group = event.target.value;
|
|
411
|
+
this.updateValidationInPlace(context);
|
|
412
|
+
});
|
|
413
|
+
const confirm = this.root.querySelector("input[data-editor-confirm]");
|
|
414
|
+
confirm?.addEventListener("change", (event) => {
|
|
415
|
+
if (!(event.target instanceof HTMLInputElement) || this.editor === undefined)
|
|
416
|
+
return;
|
|
417
|
+
this.editor.draft.confirm = event.target.checked;
|
|
418
|
+
this.updateValidationInPlace(context);
|
|
419
|
+
});
|
|
420
|
+
this.root.querySelector("form[data-task-form]")?.addEventListener("submit", (event) => {
|
|
421
|
+
event.preventDefault();
|
|
422
|
+
void this.saveTask(context);
|
|
78
423
|
});
|
|
79
424
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
425
|
+
openAddTaskEditor() {
|
|
426
|
+
const context = this.contextValue;
|
|
427
|
+
if (context === undefined || this.operation !== undefined)
|
|
428
|
+
return;
|
|
429
|
+
const entry = getWorkspaceTasksCacheEntry(cacheKeyForContext(context));
|
|
430
|
+
if (entry === undefined || this.isRefreshRequired(entry) || (entry.state.kind !== "loaded" && entry.state.kind !== "missing"))
|
|
431
|
+
return;
|
|
432
|
+
const draft = emptyDraft();
|
|
433
|
+
this.editor = {
|
|
434
|
+
draft,
|
|
435
|
+
initialDraft: cloneDraft(draft),
|
|
436
|
+
sourceSnapshot: entry.state.snapshot,
|
|
437
|
+
originalIndex: undefined,
|
|
438
|
+
focusReturn: { kind: "add" },
|
|
439
|
+
};
|
|
440
|
+
this.mode = "add";
|
|
441
|
+
this.failure = undefined;
|
|
442
|
+
this.validationErrors = undefined;
|
|
443
|
+
this.idManuallyEdited = false;
|
|
444
|
+
this.status = undefined;
|
|
445
|
+
this.render();
|
|
446
|
+
this.focusSelector("input[data-editor-title]");
|
|
447
|
+
}
|
|
448
|
+
openEditTaskEditor(context, taskId) {
|
|
449
|
+
if (!this.isCurrentContext(context) || this.operation !== undefined || taskId === null)
|
|
450
|
+
return;
|
|
451
|
+
const entry = getWorkspaceTasksCacheEntry(cacheKeyForContext(context));
|
|
452
|
+
if (entry === undefined || this.isRefreshRequired(entry) || entry.state.kind !== "loaded")
|
|
453
|
+
return;
|
|
454
|
+
const index = entry.state.config.tasks.findIndex((task) => task.id === taskId);
|
|
455
|
+
const task = index < 0 ? undefined : entry.state.config.tasks[index];
|
|
84
456
|
if (task === undefined) {
|
|
85
|
-
this.
|
|
86
|
-
|
|
87
|
-
return Promise.resolve();
|
|
457
|
+
this.setError("That task is no longer available. Click Refresh, then try again.");
|
|
458
|
+
return;
|
|
88
459
|
}
|
|
89
|
-
|
|
460
|
+
const draft = {
|
|
461
|
+
id: task.id,
|
|
462
|
+
title: task.title,
|
|
463
|
+
command: task.command,
|
|
464
|
+
description: task.description ?? "",
|
|
465
|
+
group: task.group ?? "",
|
|
466
|
+
confirm: task.confirm,
|
|
467
|
+
};
|
|
468
|
+
this.editor = {
|
|
469
|
+
draft,
|
|
470
|
+
initialDraft: cloneDraft(draft),
|
|
471
|
+
sourceSnapshot: entry.state.snapshot,
|
|
472
|
+
originalIndex: index,
|
|
473
|
+
focusReturn: { kind: "edit", id: task.id },
|
|
474
|
+
};
|
|
475
|
+
this.mode = "edit";
|
|
476
|
+
this.failure = undefined;
|
|
477
|
+
this.validationErrors = undefined;
|
|
478
|
+
this.idManuallyEdited = true;
|
|
479
|
+
this.status = undefined;
|
|
480
|
+
this.render();
|
|
481
|
+
this.focusSelector("input[data-editor-title]");
|
|
90
482
|
}
|
|
91
|
-
|
|
92
|
-
|
|
483
|
+
openDeleteConfirmation(context, taskId) {
|
|
484
|
+
if (!this.isCurrentContext(context) || this.operation !== undefined || taskId === null)
|
|
485
|
+
return;
|
|
486
|
+
const entry = getWorkspaceTasksCacheEntry(cacheKeyForContext(context));
|
|
487
|
+
if (entry === undefined || this.isRefreshRequired(entry) || entry.state.kind !== "loaded")
|
|
488
|
+
return;
|
|
489
|
+
const index = entry.state.config.tasks.findIndex((task) => task.id === taskId);
|
|
490
|
+
const task = index < 0 ? undefined : entry.state.config.tasks[index];
|
|
491
|
+
if (task === undefined) {
|
|
492
|
+
this.setError("That task is no longer available. Click Refresh, then try again.");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
this.deleteState = {
|
|
496
|
+
task,
|
|
497
|
+
index,
|
|
498
|
+
sourceSnapshot: entry.state.snapshot,
|
|
499
|
+
focusReturn: { kind: "delete", id: task.id },
|
|
500
|
+
};
|
|
501
|
+
this.mode = "delete-confirm";
|
|
502
|
+
this.failure = undefined;
|
|
503
|
+
this.status = undefined;
|
|
504
|
+
this.render();
|
|
505
|
+
this.focusSelector("button[data-cancel-delete]");
|
|
93
506
|
}
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`;
|
|
507
|
+
openResetConfirmation(context) {
|
|
508
|
+
if (!this.isCurrentContext(context) || this.operation !== undefined)
|
|
509
|
+
return;
|
|
510
|
+
const entry = getWorkspaceTasksCacheEntry(cacheKeyForContext(context));
|
|
511
|
+
if (entry === undefined || this.isRefreshRequired(entry) || entry.state.kind !== "invalid")
|
|
512
|
+
return;
|
|
513
|
+
this.resetState = { sourceSnapshot: entry.state.snapshot, focusReturn: { kind: "reset" } };
|
|
514
|
+
this.mode = "reset-confirm";
|
|
515
|
+
this.failure = undefined;
|
|
516
|
+
this.status = undefined;
|
|
517
|
+
this.render();
|
|
518
|
+
this.focusSelector("button[data-cancel-reset]");
|
|
107
519
|
}
|
|
108
|
-
|
|
109
|
-
if (this.
|
|
110
|
-
return
|
|
111
|
-
const
|
|
112
|
-
|
|
520
|
+
cancelEditor() {
|
|
521
|
+
if (this.operation !== undefined)
|
|
522
|
+
return;
|
|
523
|
+
const target = this.editor?.focusReturn;
|
|
524
|
+
this.editor = undefined;
|
|
525
|
+
this.validationErrors = undefined;
|
|
526
|
+
this.idManuallyEdited = false;
|
|
527
|
+
this.failure = undefined;
|
|
528
|
+
this.mode = "view";
|
|
529
|
+
this.status = undefined;
|
|
530
|
+
this.render();
|
|
531
|
+
this.focusTarget(target);
|
|
532
|
+
}
|
|
533
|
+
cancelDelete() {
|
|
534
|
+
if (this.operation !== undefined)
|
|
535
|
+
return;
|
|
536
|
+
const target = this.deleteState?.focusReturn;
|
|
537
|
+
this.deleteState = undefined;
|
|
538
|
+
this.failure = undefined;
|
|
539
|
+
this.mode = "view";
|
|
540
|
+
this.status = undefined;
|
|
541
|
+
this.render();
|
|
542
|
+
this.focusTarget(target);
|
|
113
543
|
}
|
|
114
|
-
|
|
115
|
-
this.
|
|
116
|
-
|
|
544
|
+
cancelReset() {
|
|
545
|
+
if (this.operation !== undefined)
|
|
546
|
+
return;
|
|
547
|
+
const target = this.resetState?.focusReturn;
|
|
548
|
+
this.resetState = undefined;
|
|
549
|
+
this.failure = undefined;
|
|
550
|
+
this.mode = "view";
|
|
551
|
+
this.status = undefined;
|
|
117
552
|
this.render();
|
|
118
|
-
|
|
119
|
-
|
|
553
|
+
this.focusTarget(target);
|
|
554
|
+
}
|
|
555
|
+
cancelRefreshDiscard() {
|
|
556
|
+
if (this.operation !== undefined)
|
|
120
557
|
return;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
558
|
+
const target = this.editor?.focusReturn;
|
|
559
|
+
this.mode = this.editor === undefined ? "view" : this.editor.originalIndex === undefined ? "add" : "edit";
|
|
560
|
+
this.pendingRefreshFocus = undefined;
|
|
124
561
|
this.render();
|
|
562
|
+
this.focusTarget(target);
|
|
125
563
|
}
|
|
126
|
-
async
|
|
127
|
-
if (this.
|
|
128
|
-
|
|
564
|
+
async confirmRefreshDiscard(context) {
|
|
565
|
+
if (this.mode !== "refresh-discard-confirm")
|
|
566
|
+
return;
|
|
567
|
+
const target = this.pendingRefreshFocus ?? { kind: "refresh" };
|
|
568
|
+
this.editor = undefined;
|
|
569
|
+
this.deleteState = undefined;
|
|
570
|
+
this.resetState = undefined;
|
|
571
|
+
this.failure = undefined;
|
|
572
|
+
this.validationErrors = undefined;
|
|
573
|
+
this.idManuallyEdited = false;
|
|
574
|
+
this.pendingRefreshFocus = target;
|
|
575
|
+
await this.requestRefresh(context, true);
|
|
576
|
+
}
|
|
577
|
+
async requestRefresh(context, force = false) {
|
|
578
|
+
if (!this.isCurrentContext(context) || this.operation !== undefined)
|
|
579
|
+
return;
|
|
580
|
+
if (!force && this.isConfirmationMode())
|
|
581
|
+
return;
|
|
582
|
+
if (!force && (this.mode === "add" || this.mode === "edit") && this.editor !== undefined && this.isEditorDirty()) {
|
|
583
|
+
this.pendingRefreshFocus = this.editor.focusReturn;
|
|
584
|
+
this.mode = "refresh-discard-confirm";
|
|
585
|
+
this.status = undefined;
|
|
129
586
|
this.render();
|
|
587
|
+
this.focusSelector("button[data-cancel-refresh-discard]");
|
|
130
588
|
return;
|
|
131
589
|
}
|
|
590
|
+
const target = this.pendingRefreshFocus ?? this.editor?.focusReturn ?? { kind: "refresh" };
|
|
591
|
+
this.pendingRefreshFocus = undefined;
|
|
592
|
+
this.editor = undefined;
|
|
593
|
+
this.deleteState = undefined;
|
|
594
|
+
this.resetState = undefined;
|
|
595
|
+
this.failure = undefined;
|
|
596
|
+
this.validationErrors = undefined;
|
|
597
|
+
this.idManuallyEdited = false;
|
|
598
|
+
this.mode = "view";
|
|
599
|
+
this.operation = "refresh";
|
|
600
|
+
const operationGeneration = ++this.operationGeneration;
|
|
601
|
+
const selectionGeneration = this.selectionGeneration;
|
|
602
|
+
this.status = { kind: "info", message: `Refreshing ${TASKS_CONFIG_PATH}...` };
|
|
603
|
+
this.render();
|
|
604
|
+
try {
|
|
605
|
+
const state = await refreshWorkspaceTasksConfig(context.files, cacheKeyForContext(context));
|
|
606
|
+
if (!this.ownsOperation(context, selectionGeneration, operationGeneration))
|
|
607
|
+
return;
|
|
608
|
+
const entry = getWorkspaceTasksCacheEntry(cacheKeyForContext(context));
|
|
609
|
+
if (entry?.state !== state) {
|
|
610
|
+
this.operation = undefined;
|
|
611
|
+
this.status = undefined;
|
|
612
|
+
this.render();
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
this.panelRefreshRequired = false;
|
|
616
|
+
this.operation = undefined;
|
|
617
|
+
this.status = refreshStatus(state);
|
|
618
|
+
this.render();
|
|
619
|
+
this.focusTarget(target);
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
if (!this.ownsOperation(context, selectionGeneration, operationGeneration))
|
|
623
|
+
return;
|
|
624
|
+
this.operation = undefined;
|
|
625
|
+
this.status = { kind: "error", message: `Could not refresh ${TASKS_CONFIG_PATH}.`, detail: formatError(error) };
|
|
626
|
+
this.render();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
async saveTask(context) {
|
|
630
|
+
if (!this.isCurrentContext(context) || this.operation !== undefined || this.editor === undefined || (this.mode !== "add" && this.mode !== "edit"))
|
|
631
|
+
return;
|
|
632
|
+
const entry = getWorkspaceTasksCacheEntry(cacheKeyForContext(context));
|
|
633
|
+
if (entry === undefined)
|
|
634
|
+
return;
|
|
635
|
+
const validation = this.validateEditor(entry);
|
|
636
|
+
if (!validation.ok) {
|
|
637
|
+
this.validationErrors = validation.errors;
|
|
638
|
+
this.status = { kind: "error", message: "Fix the highlighted fields before saving." };
|
|
639
|
+
this.render();
|
|
640
|
+
this.focusFirstInvalid(validation.errors);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (entry.state.kind !== "loaded" && entry.state.kind !== "missing") {
|
|
644
|
+
this.enterFailure("preflight-unavailable", "The current tasks file could not be verified. Refresh before trying again.", "editor");
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const editor = this.editor;
|
|
648
|
+
const nextConfig = this.mode === "add"
|
|
649
|
+
? appendWorkspaceTask(entry.state.kind === "loaded" ? entry.state.config : emptyWorkspaceTasksConfig, validation.task)
|
|
650
|
+
: this.buildEditedConfig(entry, validation.task);
|
|
651
|
+
if (nextConfig === undefined)
|
|
652
|
+
return;
|
|
653
|
+
const operationGeneration = ++this.operationGeneration;
|
|
654
|
+
const selectionGeneration = this.selectionGeneration;
|
|
655
|
+
this.operation = "mutation";
|
|
656
|
+
this.status = { kind: "info", message: "Saving workspace task..." };
|
|
657
|
+
this.render();
|
|
658
|
+
try {
|
|
659
|
+
const result = await guardedWriteWorkspaceTasksConfig(context.files, cacheKeyForContext(context), editor.sourceSnapshot, nextConfig);
|
|
660
|
+
if (!this.ownsOperation(context, selectionGeneration, operationGeneration))
|
|
661
|
+
return;
|
|
662
|
+
this.operation = undefined;
|
|
663
|
+
if (result.kind === "written") {
|
|
664
|
+
const title = validation.task.title;
|
|
665
|
+
this.mode = "view";
|
|
666
|
+
this.editor = undefined;
|
|
667
|
+
this.validationErrors = undefined;
|
|
668
|
+
this.idManuallyEdited = false;
|
|
669
|
+
this.failure = undefined;
|
|
670
|
+
this.status = { kind: "success", message: `Saved task "${title}".` };
|
|
671
|
+
this.render();
|
|
672
|
+
this.focusTarget(editor.focusReturn);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
this.applyWriteFailure(result, "editor");
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
if (!this.ownsOperation(context, selectionGeneration, operationGeneration))
|
|
679
|
+
return;
|
|
680
|
+
this.operation = undefined;
|
|
681
|
+
this.applyWriteFailure({ kind: "write-failed", detail: `Unable to write ${TASKS_CONFIG_PATH}: ${formatError(error)}` }, "editor");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
buildEditedConfig(entry, task) {
|
|
685
|
+
const editor = this.editor;
|
|
686
|
+
if (entry.state.kind !== "loaded" || editor?.originalIndex === undefined)
|
|
687
|
+
return undefined;
|
|
688
|
+
const original = entry.state.config.tasks[editor.originalIndex];
|
|
689
|
+
if (original?.id !== editor.initialDraft.id) {
|
|
690
|
+
this.enterFailure("conflict", "The task list changed outside this panel. Refresh before trying again.", "editor");
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
return replaceWorkspaceTaskAt(entry.state.config, editor.originalIndex, task);
|
|
694
|
+
}
|
|
695
|
+
async confirmDelete(context) {
|
|
696
|
+
if (!this.isCurrentContext(context) || this.operation !== undefined || this.deleteState === undefined || this.mode !== "delete-confirm")
|
|
697
|
+
return;
|
|
698
|
+
const entry = getWorkspaceTasksCacheEntry(cacheKeyForContext(context));
|
|
699
|
+
const pending = this.deleteState;
|
|
700
|
+
if (entry?.state.kind !== "loaded" || this.isRefreshRequired(entry) || entry.state.config.tasks[pending.index]?.id !== pending.task.id) {
|
|
701
|
+
this.enterFailure("conflict", "The task list changed outside this panel. Refresh before trying again.", "delete");
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
let nextConfig;
|
|
705
|
+
try {
|
|
706
|
+
nextConfig = removeWorkspaceTaskAt(entry.state.config, pending.index);
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
this.enterFailure("conflict", formatError(error), "delete");
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
await this.performMutation(context, pending.sourceSnapshot, nextConfig, "delete", pending.task.title, pending.focusReturn);
|
|
713
|
+
}
|
|
714
|
+
async confirmReset(context) {
|
|
715
|
+
if (!this.isCurrentContext(context) || this.operation !== undefined || this.resetState === undefined || this.mode !== "reset-confirm")
|
|
716
|
+
return;
|
|
717
|
+
const pending = this.resetState;
|
|
718
|
+
await this.performMutation(context, pending.sourceSnapshot, emptyWorkspaceTasksConfig, "reset", undefined, pending.focusReturn);
|
|
719
|
+
}
|
|
720
|
+
async performMutation(context, sourceSnapshot, nextConfig, action, title, focusReturn) {
|
|
721
|
+
const operationGeneration = ++this.operationGeneration;
|
|
722
|
+
const selectionGeneration = this.selectionGeneration;
|
|
723
|
+
this.operation = "mutation";
|
|
724
|
+
this.status = { kind: "info", message: action === "delete" ? "Deleting workspace task..." : action === "reset" ? "Resetting workspace tasks file..." : "Saving workspace task..." };
|
|
725
|
+
this.render();
|
|
726
|
+
try {
|
|
727
|
+
const result = await guardedWriteWorkspaceTasksConfig(context.files, cacheKeyForContext(context), sourceSnapshot, nextConfig);
|
|
728
|
+
if (!this.ownsOperation(context, selectionGeneration, operationGeneration))
|
|
729
|
+
return;
|
|
730
|
+
this.operation = undefined;
|
|
731
|
+
if (result.kind === "written") {
|
|
732
|
+
this.mode = "view";
|
|
733
|
+
this.editor = undefined;
|
|
734
|
+
this.deleteState = undefined;
|
|
735
|
+
this.resetState = undefined;
|
|
736
|
+
this.failure = undefined;
|
|
737
|
+
this.validationErrors = undefined;
|
|
738
|
+
this.idManuallyEdited = false;
|
|
739
|
+
this.status = action === "delete" && title !== undefined
|
|
740
|
+
? { kind: "success", message: `Deleted task "${title}".` }
|
|
741
|
+
: action === "reset"
|
|
742
|
+
? { kind: "success", message: "Reset workspace tasks file." }
|
|
743
|
+
: { kind: "success", message: "Saved workspace task." };
|
|
744
|
+
this.render();
|
|
745
|
+
this.focusTarget(focusReturn);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
this.applyWriteFailure(result, action === "delete" ? "delete" : "reset");
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
if (!this.ownsOperation(context, selectionGeneration, operationGeneration))
|
|
752
|
+
return;
|
|
753
|
+
this.operation = undefined;
|
|
754
|
+
this.applyWriteFailure({ kind: "write-failed", detail: `Unable to write ${TASKS_CONFIG_PATH}: ${formatError(error)}` }, action === "delete" ? "delete" : "reset");
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
applyWriteFailure(result, action) {
|
|
758
|
+
this.panelRefreshRequired = true;
|
|
759
|
+
this.failure = { kind: result.kind, detail: result.detail, action };
|
|
760
|
+
this.mode = result.kind === "conflict" || result.kind === "preflight-unavailable" ? "conflicted" : "needs-refresh-after-write";
|
|
761
|
+
this.status = { kind: "error", message: failureMessage(result.kind), detail: result.detail };
|
|
762
|
+
this.render();
|
|
763
|
+
}
|
|
764
|
+
enterFailure(kind, detail, action) {
|
|
765
|
+
this.panelRefreshRequired = true;
|
|
766
|
+
this.failure = { kind, detail, action };
|
|
767
|
+
this.mode = kind === "conflict" || kind === "preflight-unavailable" ? "conflicted" : "needs-refresh-after-write";
|
|
768
|
+
this.status = { kind: "error", message: failureMessage(kind), detail };
|
|
769
|
+
this.render();
|
|
770
|
+
}
|
|
771
|
+
dispatchTaskById(context, taskId) {
|
|
772
|
+
if (!this.isCurrentContext(context) || taskId === null)
|
|
773
|
+
return Promise.resolve();
|
|
774
|
+
const entry = getWorkspaceTasksCacheEntry(cacheKeyForContext(context));
|
|
775
|
+
const task = entry?.state.kind === "loaded" ? entry.state.config.tasks.find((candidate) => candidate.id === taskId) : undefined;
|
|
776
|
+
if (task === undefined) {
|
|
777
|
+
this.setError("That task is no longer available. Click Refresh, then try again.");
|
|
778
|
+
return Promise.resolve();
|
|
779
|
+
}
|
|
780
|
+
return this.dispatchTask(context, task);
|
|
781
|
+
}
|
|
782
|
+
async dispatchTask(context, task) {
|
|
783
|
+
if (this.runningTaskId !== undefined || this.operation !== undefined)
|
|
784
|
+
return;
|
|
132
785
|
if (task.confirm && !window.confirm(`Run ${task.title}?\n\n${task.command}`)) {
|
|
133
786
|
this.status = { kind: "info", message: `Cancelled ${task.title}.` };
|
|
134
787
|
this.render();
|
|
135
788
|
return;
|
|
136
789
|
}
|
|
790
|
+
const selectionGeneration = this.selectionGeneration;
|
|
791
|
+
const terminalRunGeneration = ++this.terminalRunGeneration;
|
|
137
792
|
this.runningTaskId = task.id;
|
|
138
|
-
this.status = { kind: "info", message: `Starting ${task.title}
|
|
793
|
+
this.status = { kind: "info", message: `Starting ${task.title}...` };
|
|
139
794
|
this.render();
|
|
140
795
|
try {
|
|
141
796
|
const handle = await runWorkspaceTaskInTerminal(context.terminal, task);
|
|
142
|
-
if (!this.
|
|
797
|
+
if (!this.ownsTerminalRun(context, selectionGeneration, terminalRunGeneration))
|
|
143
798
|
return;
|
|
144
|
-
this.status = {
|
|
145
|
-
kind: "success",
|
|
146
|
-
message: `Started terminal command “${handle.run.title}”.`,
|
|
147
|
-
detail: task.command,
|
|
148
|
-
};
|
|
149
799
|
this.runningTaskId = undefined;
|
|
150
|
-
this.
|
|
800
|
+
this.renderTerminalStatus({ kind: "success", message: `Started terminal command "${handle.run.title}".`, detail: task.command });
|
|
151
801
|
}
|
|
152
802
|
catch (error) {
|
|
153
|
-
if (!this.
|
|
803
|
+
if (!this.ownsTerminalRun(context, selectionGeneration, terminalRunGeneration))
|
|
154
804
|
return;
|
|
155
805
|
this.runningTaskId = undefined;
|
|
156
|
-
this.
|
|
806
|
+
this.renderTerminalStatus({ kind: "error", message: formatError(error) });
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
renderTerminalStatus(status) {
|
|
810
|
+
if (this.operation !== undefined) {
|
|
157
811
|
this.render();
|
|
812
|
+
return;
|
|
158
813
|
}
|
|
814
|
+
this.status = status;
|
|
815
|
+
this.render();
|
|
159
816
|
}
|
|
160
817
|
openWorkspaceTerminal(terminalId) {
|
|
161
818
|
const context = this.contextValue;
|
|
162
819
|
if (context === undefined) {
|
|
163
|
-
this.
|
|
164
|
-
this.render();
|
|
820
|
+
this.setError("Select a workspace before opening a terminal.");
|
|
165
821
|
return;
|
|
166
822
|
}
|
|
167
823
|
if (terminalId === undefined)
|
|
@@ -169,102 +825,234 @@ class PiWebUiTasksPanel extends HTMLElement {
|
|
|
169
825
|
else
|
|
170
826
|
context.terminal.open({ terminalId });
|
|
171
827
|
}
|
|
828
|
+
validateEditor(entry) {
|
|
829
|
+
const editor = this.editor;
|
|
830
|
+
if (editor === undefined)
|
|
831
|
+
return { ok: false, errors: { title: "Title is required." } };
|
|
832
|
+
const existingTasks = entry.state.kind === "loaded" ? entry.state.config.tasks : [];
|
|
833
|
+
return validateAndNormalizeDraft(editor.draft, existingTasks, editor.originalIndex);
|
|
834
|
+
}
|
|
835
|
+
updateValidationInPlace(context) {
|
|
836
|
+
const entry = getWorkspaceTasksCacheEntry(cacheKeyForContext(context));
|
|
837
|
+
if (entry === undefined || this.editor === undefined)
|
|
838
|
+
return;
|
|
839
|
+
const result = this.validateEditor(entry);
|
|
840
|
+
this.validationErrors = result.ok ? undefined : result.errors;
|
|
841
|
+
const errors = this.validationErrors ?? {};
|
|
842
|
+
const controls = [["title", "task-title"], ["command", "task-command"], ["id", "task-id"]];
|
|
843
|
+
for (const [field, controlId] of controls) {
|
|
844
|
+
const control = this.root.querySelector(`#${controlId}`);
|
|
845
|
+
const error = errors[field];
|
|
846
|
+
const errorElement = this.root.querySelector(`[data-field-error="${field}"]`);
|
|
847
|
+
if (control === null || errorElement === null)
|
|
848
|
+
continue;
|
|
849
|
+
if (error === undefined) {
|
|
850
|
+
control.removeAttribute("aria-invalid");
|
|
851
|
+
if (field === "command")
|
|
852
|
+
control.setAttribute("aria-describedby", "task-command-help");
|
|
853
|
+
else
|
|
854
|
+
control.removeAttribute("aria-describedby");
|
|
855
|
+
errorElement.textContent = "";
|
|
856
|
+
errorElement.hidden = true;
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
control.setAttribute("aria-invalid", "true");
|
|
860
|
+
control.setAttribute("aria-describedby", field === "command" ? `task-command-help task-command-error` : `task-${field}-error`);
|
|
861
|
+
errorElement.textContent = error;
|
|
862
|
+
errorElement.hidden = false;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const save = this.root.querySelector("button[data-save-task]");
|
|
866
|
+
if (save !== null)
|
|
867
|
+
save.disabled = this.operation !== undefined || this.isRefreshRequired(entry) || !result.ok || this.failure !== undefined;
|
|
868
|
+
}
|
|
869
|
+
isEditorDirty() {
|
|
870
|
+
const editor = this.editor;
|
|
871
|
+
if (editor === undefined)
|
|
872
|
+
return false;
|
|
873
|
+
return editor.draft.id !== editor.initialDraft.id
|
|
874
|
+
|| editor.draft.title !== editor.initialDraft.title
|
|
875
|
+
|| editor.draft.command !== editor.initialDraft.command
|
|
876
|
+
|| editor.draft.description !== editor.initialDraft.description
|
|
877
|
+
|| editor.draft.group !== editor.initialDraft.group
|
|
878
|
+
|| editor.draft.confirm !== editor.initialDraft.confirm;
|
|
879
|
+
}
|
|
880
|
+
focusFirstInvalid(errors) {
|
|
881
|
+
const field = errors.title === undefined ? errors.command === undefined ? "id" : "command" : "title";
|
|
882
|
+
this.focusSelector(`input[data-editor-${field}], textarea[data-editor-${field}]`);
|
|
883
|
+
}
|
|
884
|
+
focusSelector(selector) {
|
|
885
|
+
if (!this.connected)
|
|
886
|
+
return;
|
|
887
|
+
this.root.querySelector(selector)?.focus();
|
|
888
|
+
}
|
|
889
|
+
focusTarget(target) {
|
|
890
|
+
if (!this.connected)
|
|
891
|
+
return;
|
|
892
|
+
let element;
|
|
893
|
+
if (target?.kind === "add")
|
|
894
|
+
element = this.root.querySelector("button[data-add-task]") ?? undefined;
|
|
895
|
+
if (target?.kind === "edit")
|
|
896
|
+
element = findButtonByValue(this.root, "data-edit-task", target.id);
|
|
897
|
+
if (target?.kind === "delete")
|
|
898
|
+
element = findButtonByValue(this.root, "data-delete-task", target.id);
|
|
899
|
+
if (target?.kind === "reset")
|
|
900
|
+
element = this.root.querySelector("button[data-reset-tasks-file]") ?? undefined;
|
|
901
|
+
if (target?.kind === "refresh")
|
|
902
|
+
element = this.root.querySelector("button[data-refresh-config]") ?? undefined;
|
|
903
|
+
if (target?.kind === "heading" || element === undefined)
|
|
904
|
+
element = this.root.querySelector("[data-panel-heading]") ?? undefined;
|
|
905
|
+
element?.focus();
|
|
906
|
+
}
|
|
907
|
+
isCurrentContext(context) {
|
|
908
|
+
return this.connected && this.contextValue !== undefined && cacheKeyForContext(this.contextValue) === cacheKeyForContext(context);
|
|
909
|
+
}
|
|
910
|
+
ownsTerminalRun(context, selectionGeneration, terminalRunGeneration) {
|
|
911
|
+
return this.isCurrentContext(context)
|
|
912
|
+
&& this.selectionGeneration === selectionGeneration
|
|
913
|
+
&& this.terminalRunGeneration === terminalRunGeneration;
|
|
914
|
+
}
|
|
915
|
+
ownsOperation(context, selectionGeneration, operationGeneration) {
|
|
916
|
+
return this.isCurrentContext(context)
|
|
917
|
+
&& this.selectionGeneration === selectionGeneration
|
|
918
|
+
&& this.operationGeneration === operationGeneration;
|
|
919
|
+
}
|
|
920
|
+
setError(message) {
|
|
921
|
+
this.status = { kind: "error", message };
|
|
922
|
+
this.render();
|
|
923
|
+
}
|
|
172
924
|
}
|
|
173
|
-
function
|
|
174
|
-
return
|
|
175
|
-
}
|
|
176
|
-
function getOrLoadWorkspaceConfig(context) {
|
|
177
|
-
const cached = getCachedWorkspaceConfig(context);
|
|
178
|
-
if (cached !== undefined)
|
|
179
|
-
return cached;
|
|
180
|
-
const loading = { kind: "loading" };
|
|
181
|
-
configCache.set(cacheKeyForContext(context), loading);
|
|
182
|
-
void refreshWorkspaceConfig(context);
|
|
183
|
-
return loading;
|
|
184
|
-
}
|
|
185
|
-
async function refreshWorkspaceConfig(context) {
|
|
186
|
-
const key = cacheKeyForContext(context);
|
|
187
|
-
const state = await loadWorkspaceTasksConfig(context.files).catch((error) => ({
|
|
188
|
-
kind: "unavailable",
|
|
189
|
-
message: tasksConfigUnavailableMessage,
|
|
190
|
-
hint: tasksConfigRefreshHint,
|
|
191
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
192
|
-
}));
|
|
193
|
-
configCache.set(key, state);
|
|
194
|
-
context.host.requestRender();
|
|
195
|
-
window.dispatchEvent(new Event(configChangedEvent));
|
|
196
|
-
return state;
|
|
197
|
-
}
|
|
198
|
-
function cacheKeyForContext(context) {
|
|
199
|
-
return `${context.machine.id}:${context.workspace.projectId}:${context.workspace.id}`;
|
|
200
|
-
}
|
|
201
|
-
function renderMissingState(state) {
|
|
202
|
-
return `<div class="empty-state"><strong>${escapeHtml(state.message)}</strong><p>${escapeHtml(state.hint)}</p></div>`;
|
|
203
|
-
}
|
|
204
|
-
function renderUnavailableState(state) {
|
|
205
|
-
const detail = state.detail === undefined ? "" : `<pre>${escapeHtml(state.detail)}</pre>`;
|
|
206
|
-
return `<div class="status error"><strong>${escapeHtml(state.message)}</strong><p>${escapeHtml(state.hint)}</p>${detail}</div>`;
|
|
925
|
+
function renderFieldError(field, error) {
|
|
926
|
+
return `<p id="task-${field}-error" data-field-error="${field}" class="field-error"${error === undefined ? " hidden" : ""}>${error === undefined ? "" : escapeHtml(error)}</p>`;
|
|
207
927
|
}
|
|
208
|
-
function renderTaskGroups(tasks, runningTaskId) {
|
|
209
|
-
return `<div class="tasks">${groupTasks(tasks).map((group) => renderTaskGroup(group, runningTaskId)).join("")}</div>`;
|
|
928
|
+
function renderTaskGroups(tasks, runningTaskId, actionsDisabled) {
|
|
929
|
+
return `<div class="tasks">${groupTasks(tasks).map((group) => renderTaskGroup(group, runningTaskId, actionsDisabled)).join("")}</div>`;
|
|
210
930
|
}
|
|
211
931
|
function groupTasks(tasks) {
|
|
212
932
|
const groups = [];
|
|
213
933
|
for (const task of tasks) {
|
|
214
|
-
|
|
215
|
-
let group = groups.find((candidate) => candidate.title === title);
|
|
934
|
+
let group = groups.find((candidate) => candidate.title === task.group);
|
|
216
935
|
if (group === undefined) {
|
|
217
|
-
group = { title, tasks: [] };
|
|
936
|
+
group = { title: task.group, tasks: [] };
|
|
218
937
|
groups.push(group);
|
|
219
938
|
}
|
|
220
939
|
group.tasks.push(task);
|
|
221
940
|
}
|
|
222
941
|
return groups;
|
|
223
942
|
}
|
|
224
|
-
function renderTaskGroup(group, runningTaskId) {
|
|
943
|
+
function renderTaskGroup(group, runningTaskId, actionsDisabled) {
|
|
225
944
|
const title = group.title === undefined ? "" : `<h3>${escapeHtml(group.title)}</h3>`;
|
|
226
|
-
return `<section class="task-group">${title}${group.tasks.map((task) => renderTask(task, runningTaskId)).join("")}</section>`;
|
|
945
|
+
return `<section class="task-group">${title}${group.tasks.map((task) => renderTask(task, runningTaskId, actionsDisabled)).join("")}</section>`;
|
|
227
946
|
}
|
|
228
|
-
function renderTask(task, runningTaskId) {
|
|
947
|
+
function renderTask(task, runningTaskId, actionsDisabled) {
|
|
229
948
|
const running = runningTaskId === task.id;
|
|
230
|
-
const
|
|
949
|
+
const runDisabled = runningTaskId !== undefined || actionsDisabled;
|
|
950
|
+
const mutationDisabled = runningTaskId !== undefined || actionsDisabled;
|
|
231
951
|
const description = task.description === undefined ? "" : `<span>${escapeHtml(task.description)}</span>`;
|
|
232
952
|
return `
|
|
233
953
|
<article class="task-card">
|
|
234
954
|
<div class="task-copy">
|
|
235
955
|
<strong>${escapeHtml(task.title)}</strong>
|
|
236
956
|
${description}
|
|
237
|
-
<
|
|
957
|
+
<pre class="task-script" data-task-script>${escapeHtml(task.command)}</pre>
|
|
958
|
+
</div>
|
|
959
|
+
<div class="task-actions">
|
|
960
|
+
<button type="button" class="secondary" data-edit-task="${escapeAttr(task.id)}" ${mutationDisabled ? "disabled" : ""}>Edit</button>
|
|
961
|
+
<button type="button" class="secondary danger-secondary" data-delete-task="${escapeAttr(task.id)}" ${mutationDisabled ? "disabled" : ""}>Delete</button>
|
|
962
|
+
<button type="button" data-task-id="${escapeAttr(task.id)}" ${runDisabled ? "disabled" : ""}>${running ? "Dispatching..." : "Run"}</button>
|
|
238
963
|
</div>
|
|
239
|
-
<button data-task-id="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${running ? "Dispatching…" : "Run"}</button>
|
|
240
964
|
</article>
|
|
241
965
|
`;
|
|
242
966
|
}
|
|
243
|
-
function
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
967
|
+
function findButtonByValue(root, attribute, value) {
|
|
968
|
+
for (const button of root.querySelectorAll(`button[${attribute}]`)) {
|
|
969
|
+
if (button.getAttribute(attribute) === value)
|
|
970
|
+
return button;
|
|
971
|
+
}
|
|
972
|
+
return undefined;
|
|
973
|
+
}
|
|
974
|
+
function emptyDraft() {
|
|
975
|
+
return { id: "", title: "", command: "", description: "", group: "", confirm: false };
|
|
976
|
+
}
|
|
977
|
+
function cloneDraft(draft) {
|
|
978
|
+
return { ...draft };
|
|
979
|
+
}
|
|
980
|
+
function cacheKeyForContext(context) {
|
|
981
|
+
return `${context.machine.id}:${context.workspace.projectId}:${context.workspace.id}`;
|
|
982
|
+
}
|
|
983
|
+
function refreshStatus(state) {
|
|
984
|
+
if (state.kind === "loaded")
|
|
985
|
+
return { kind: "success", message: `Loaded ${String(state.config.tasks.length)} task${state.config.tasks.length === 1 ? "" : "s"}.` };
|
|
986
|
+
if (state.kind === "missing")
|
|
987
|
+
return { kind: "info", message: "No workspace tasks file is configured." };
|
|
988
|
+
if (state.kind === "invalid")
|
|
989
|
+
return { kind: "error", message: "Workspace tasks configuration is invalid.", detail: state.detail };
|
|
990
|
+
const detail = state.detail;
|
|
991
|
+
return detail === undefined
|
|
992
|
+
? { kind: "error", message: state.message }
|
|
993
|
+
: { kind: "error", message: state.message, detail };
|
|
994
|
+
}
|
|
995
|
+
function failureMessage(kind) {
|
|
996
|
+
if (kind === "conflict")
|
|
997
|
+
return "The tasks file changed outside this panel. Refresh before trying again.";
|
|
998
|
+
if (kind === "preflight-unavailable")
|
|
999
|
+
return "The current tasks file could not be verified. Refresh before trying again.";
|
|
1000
|
+
if (kind === "write-failed")
|
|
1001
|
+
return "The task write failed. Refresh before trying again; the result may be unknown.";
|
|
1002
|
+
return "The task was written, but the new file could not be verified. Refresh before trying again.";
|
|
1003
|
+
}
|
|
1004
|
+
function formatError(error) {
|
|
1005
|
+
return error instanceof Error ? error.message : String(error);
|
|
1006
|
+
}
|
|
1007
|
+
function escapeHtml(value) {
|
|
1008
|
+
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
1009
|
+
}
|
|
1010
|
+
function escapeAttr(value) {
|
|
1011
|
+
return escapeHtml(value).replaceAll('"', """).replaceAll("'", "'");
|
|
247
1012
|
}
|
|
248
1013
|
function taskStyles() {
|
|
249
1014
|
return `
|
|
250
1015
|
<style>
|
|
251
|
-
:host { display:
|
|
1016
|
+
:host { display: block; min-width: 0; container-type: inline-size; }
|
|
1017
|
+
.tasks-panel { min-width: 0; container-type: inline-size; color: var(--pi-text); }
|
|
252
1018
|
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--pi-border-muted); }
|
|
1019
|
+
.toolbar h2 { margin: 0; font-size: 15px; }
|
|
253
1020
|
.toolbar-tasks { display: inline-flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
|
254
|
-
.viewer { box-sizing: border-box; min-height: 0; overflow: auto; padding: 12px; }
|
|
1021
|
+
.viewer { box-sizing: border-box; min-width: 0; min-height: 0; overflow: auto; padding: 12px; }
|
|
255
1022
|
.tasks-viewer { display: grid; align-content: start; gap: 12px; }
|
|
256
|
-
.tasks { display: grid; gap: 14px; }
|
|
257
|
-
.task-group { display: grid; gap: 10px; }
|
|
1023
|
+
.tasks { display: grid; gap: 14px; min-width: 0; }
|
|
1024
|
+
.task-group { display: grid; gap: 10px; min-width: 0; }
|
|
258
1025
|
.task-group h3 { margin: 4px 0 0; color: var(--pi-text-secondary); font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
259
|
-
.task-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items:
|
|
1026
|
+
.task-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: start; min-width: 0; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); padding: 12px; }
|
|
260
1027
|
.task-copy { display: grid; min-width: 0; gap: 5px; }
|
|
261
|
-
.task-copy span, .muted { color: var(--pi-muted); }
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
1028
|
+
.task-copy span, .muted, .field-help { color: var(--pi-muted); }
|
|
1029
|
+
.task-script, .diagnostic { box-sizing: border-box; max-width: 100%; margin: 0; border: 1px solid var(--pi-border-muted); border-radius: 6px; background: var(--pi-bg); color: var(--pi-text-secondary); font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1030
|
+
.task-script { max-height: 12rem; overflow-y: auto; padding: 8px; }
|
|
1031
|
+
.diagnostic { max-height: 12rem; overflow: auto; padding: 8px; }
|
|
1032
|
+
code { font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
265
1033
|
button { border: 1px solid var(--pi-accent-border); border-radius: 7px; background: var(--pi-accent); color: var(--pi-bg); cursor: pointer; padding: 6px 10px; font: inherit; }
|
|
266
1034
|
button.secondary { border-color: var(--pi-border); background: var(--pi-surface); color: var(--pi-text); }
|
|
1035
|
+
button.danger, button.danger-secondary { border-color: var(--pi-danger); }
|
|
1036
|
+
button.danger { background: var(--pi-danger); color: var(--pi-bg); }
|
|
1037
|
+
button.danger-secondary { background: var(--pi-surface); color: var(--pi-danger); }
|
|
267
1038
|
button:disabled { cursor: wait; opacity: 0.65; }
|
|
1039
|
+
button:focus-visible, input:focus-visible, textarea:focus-visible, [tabindex="-1"]:focus-visible { outline: 2px solid var(--pi-accent-border); outline-offset: 2px; }
|
|
1040
|
+
.task-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
|
1041
|
+
.task-editor, .confirmation { width: min(100%, 620px); box-sizing: border-box; padding: 16px; background: var(--pi-surface); border: 1px solid var(--pi-border); border-radius: 8px; }
|
|
1042
|
+
.task-editor h3, .confirmation h3 { margin: 0 0 16px; font-size: 18px; }
|
|
1043
|
+
.confirmation p { margin: 0 0 10px; line-height: 1.5; }
|
|
1044
|
+
.confirmation .task-script { margin: 10px 0 16px; }
|
|
1045
|
+
.task-form { display: grid; gap: 6px; }
|
|
1046
|
+
.task-form > label, .checkbox-field > label { font-size: 14px; font-weight: 500; }
|
|
1047
|
+
.task-form .required { color: var(--pi-danger); }
|
|
1048
|
+
.task-form input[type="text"], .task-form textarea { box-sizing: border-box; width: 100%; padding: 8px 10px; background: var(--pi-bg); border: 1px solid var(--pi-border); border-radius: 6px; font-size: 14px; color: var(--pi-text); font-family: inherit; }
|
|
1049
|
+
.task-form textarea { min-height: 9rem; max-height: 24rem; resize: vertical; line-height: 1.45; }
|
|
1050
|
+
.task-form input[type="text"]::placeholder, .task-form textarea::placeholder { color: var(--pi-muted); }
|
|
1051
|
+
.checkbox-field { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 8px; margin-top: 8px; }
|
|
1052
|
+
.checkbox-field input { width: 18px; height: 18px; }
|
|
1053
|
+
.field-help { margin: 0 0 8px; font-size: 12px; line-height: 1.4; }
|
|
1054
|
+
.field-error { margin: 0 0 8px; color: var(--pi-danger); font-size: 12px; }
|
|
1055
|
+
.editor-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 10px; }
|
|
268
1056
|
.empty-state { border: 1px dashed var(--pi-border-muted); border-radius: 8px; color: var(--pi-muted); padding: 12px; }
|
|
269
1057
|
.empty-state p { margin: 6px 0 0; }
|
|
270
1058
|
.panel-status { margin: 12px 12px 0; }
|
|
@@ -272,17 +1060,13 @@ function taskStyles() {
|
|
|
272
1060
|
.status.info { border-color: var(--pi-accent-border); background: var(--pi-bg-overlay-soft); }
|
|
273
1061
|
.status.success { border-color: var(--pi-success-border); background: var(--pi-success-surface); color: var(--pi-success); }
|
|
274
1062
|
.status.error { border-color: var(--pi-danger); color: var(--pi-danger); }
|
|
1063
|
+
.status.warning { border-color: var(--pi-warning-border); background: var(--pi-warning-surface); color: var(--pi-text); }
|
|
1064
|
+
.status p { margin: 6px 0; }
|
|
275
1065
|
.empty { padding: 16px; color: var(--pi-muted); }
|
|
276
|
-
@
|
|
277
|
-
.task-card { grid-template-columns: 1fr; }
|
|
278
|
-
.task-
|
|
1066
|
+
@container (max-width: 600px) {
|
|
1067
|
+
.task-card { grid-template-columns: minmax(0, 1fr); }
|
|
1068
|
+
.task-actions { justify-content: flex-start; }
|
|
279
1069
|
}
|
|
280
1070
|
</style>
|
|
281
1071
|
`;
|
|
282
1072
|
}
|
|
283
|
-
function escapeHtml(value) {
|
|
284
|
-
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
285
|
-
}
|
|
286
|
-
function escapeAttr(value) {
|
|
287
|
-
return escapeHtml(value).replaceAll('"', """);
|
|
288
|
-
}
|