@hyperdreamer/pi-webui 1.15.2 → 1.16.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-CHZziPur.js → CodeViewer-CCHq9DpL.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-j1-AFZ8f.js → UnifiedDiffViewer-JFQMCnzP.js} +1 -1
- package/dist/client/assets/{index-2RMTbgnM.js → index-WjewjFVt.js} +469 -469
- package/dist/client/index.html +1 -1
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -1
- package/dist/configMutationCoordinator.js +10 -1
- package/dist/configMutationCoordinator.js.map +1 -1
- package/dist/pi-webui-plugins/workspace-tasks/config.js +3 -132
- package/dist/pi-webui-plugins/workspace-tasks/pi-webui-plugin.js +14 -2
- package/dist/pi-webui-plugins/workspace-tasks/taskDomain.js +203 -0
- package/dist/pi-webui-plugins/workspace-tasks/taskRunner.js +3 -2
- package/dist/pi-webui-plugins/workspace-tasks/tasksPanelElement.js +947 -673
- package/dist/server/app.js +20 -4
- package/dist/server/app.js.map +1 -1
- package/dist/server/configRoutes.js +5 -0
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +4 -4
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/workspaceExplorerRoutes.js +64 -4
- package/dist/server/workspaceExplorerRoutes.js.map +1 -1
- package/dist/server/workspaceTasks/workspaceTasksCatalogService.js +318 -0
- package/dist/server/workspaceTasks/workspaceTasksCatalogService.js.map +1 -0
- package/dist/server/workspaceTasks/workspaceTasksComposition.js +67 -0
- package/dist/server/workspaceTasks/workspaceTasksComposition.js.map +1 -0
- package/dist/server/workspaceTasks/workspaceTasksErrors.js +34 -0
- package/dist/server/workspaceTasks/workspaceTasksErrors.js.map +1 -0
- package/dist/server/workspaceTasks/workspaceTasksGlobalCatalogAdapter.js +169 -0
- package/dist/server/workspaceTasks/workspaceTasksGlobalCatalogAdapter.js.map +1 -0
- package/dist/server/workspaceTasks/workspaceTasksGlobalMutationGate.js +75 -0
- package/dist/server/workspaceTasks/workspaceTasksGlobalMutationGate.js.map +1 -0
- package/dist/server/workspaceTasks/workspaceTasksMoveProtocol.js +313 -0
- package/dist/server/workspaceTasks/workspaceTasksMoveProtocol.js.map +1 -0
- package/dist/server/workspaceTasks/workspaceTasksMoveRegistry.js +352 -0
- package/dist/server/workspaceTasks/workspaceTasksMoveRegistry.js.map +1 -0
- package/dist/server/workspaceTasks/workspaceTasksProxyRoutes.js +63 -0
- package/dist/server/workspaceTasks/workspaceTasksProxyRoutes.js.map +1 -0
- package/dist/server/workspaceTasks/workspaceTasksRoutes.js +153 -0
- package/dist/server/workspaceTasks/workspaceTasksRoutes.js.map +1 -0
- package/dist/server/workspaceTasks/workspaceTasksWorkspaceCatalogAdapter.js +171 -0
- package/dist/server/workspaceTasks/workspaceTasksWorkspaceCatalogAdapter.js.map +1 -0
- package/dist/server/workspaceTasks/workspaceTasksWorkspaceFile.js +227 -0
- package/dist/server/workspaceTasks/workspaceTasksWorkspaceFile.js.map +1 -0
- package/dist/server/workspaceTasks/workspaceTasksWorkspacePathGate.js +42 -0
- package/dist/server/workspaceTasks/workspaceTasksWorkspacePathGate.js.map +1 -0
- package/dist/server/workspaces/fileContentService.js +116 -6
- package/dist/server/workspaces/fileContentService.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +132 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/federatedRoutes.js +15 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/workspaceFiles.js +2 -0
- package/dist/shared/workspaceFiles.js.map +1 -1
- package/dist/shared/workspaceTasks.d.ts +53 -0
- package/dist/shared/workspaceTasks.js +203 -0
- package/dist/shared/workspaceTasks.js.map +1 -0
- package/dist/shared/workspaceTasksApi.js +379 -0
- package/dist/shared/workspaceTasksApi.js.map +1 -0
- package/docs/config.md +22 -4
- package/docs/plugins.md +24 -13
- package/package.json +1 -1
- package/dist/pi-webui-plugins/workspace-tasks/workspaceTasksClient.js +0 -242
|
@@ -1,255 +1,279 @@
|
|
|
1
1
|
// Generated from pi-webui-plugins/workspace-tasks/tasksPanelElement.ts. Do not edit directly.
|
|
2
|
-
import { TASKS_CONFIG_PATH,
|
|
2
|
+
import { TASKS_CONFIG_PATH, parseWorkspaceTaskRefKey, workspaceTaskGroupKey, workspaceTaskRefKey, } from "./taskDomain.js?v=b6542ca5f17b";
|
|
3
|
+
import { validateAndNormalizeDraft, suggestWorkspaceTaskId } from "./config.js";
|
|
3
4
|
import { runWorkspaceTaskInTerminal } from "./taskRunner.js";
|
|
4
|
-
import { ensureWorkspaceTasksConfig, getWorkspaceTasksCacheEntry, guardedWriteWorkspaceTasksConfig, refreshWorkspaceTasksConfig, subscribeWorkspaceTasksConfig, } from "./workspaceTasksClient.js";
|
|
5
5
|
export const tasksPanelTagName = "pi-webui-workspace-tasks-panel";
|
|
6
6
|
export function defineTasksPanelElement() {
|
|
7
|
-
if (!customElements.get(tasksPanelTagName))
|
|
7
|
+
if (typeof customElements !== "undefined" && !customElements.get(tasksPanelTagName)) {
|
|
8
8
|
customElements.define(tasksPanelTagName, PiWebUiTasksPanel);
|
|
9
|
+
}
|
|
9
10
|
}
|
|
10
11
|
export function tasksPanelBadge(context) {
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
// The bundled contribution receives this bridge only from the core app.
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
14
|
+
const bridge = context.workspaceTasks;
|
|
15
|
+
if (bridge === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
const state = bridge.state;
|
|
18
|
+
if (state.move !== undefined || state.moveError !== undefined || state.mutationGate !== undefined)
|
|
19
|
+
return "!";
|
|
20
|
+
if (catalogNeedsAttention(state.workspace) || catalogNeedsAttention(state.global))
|
|
21
|
+
return "!";
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
// Non-browser plugin activation tests still import this module, so keep the
|
|
25
|
+
// custom element base safe when DOM globals are not installed.
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
27
|
+
class NoopElement {
|
|
13
28
|
}
|
|
14
|
-
|
|
29
|
+
function noopElementConstructor() {
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
31
|
+
return NoopElement;
|
|
32
|
+
}
|
|
33
|
+
const BaseElement = typeof HTMLElement === "undefined" ? noopElementConstructor() : HTMLElement;
|
|
34
|
+
class PiWebUiTasksPanel extends BaseElement {
|
|
15
35
|
contextValue;
|
|
16
|
-
|
|
17
|
-
|
|
36
|
+
stateValue = emptyPanelState();
|
|
37
|
+
actionsValue = noOpActions();
|
|
38
|
+
filter = "all";
|
|
18
39
|
mode = "view";
|
|
19
40
|
operation;
|
|
20
41
|
editor;
|
|
21
42
|
deleteState;
|
|
22
|
-
|
|
23
|
-
failure;
|
|
43
|
+
pendingRefreshFocus;
|
|
24
44
|
validationErrors;
|
|
45
|
+
status;
|
|
46
|
+
runningTaskKey;
|
|
47
|
+
pendingAction;
|
|
25
48
|
idManuallyEdited = false;
|
|
26
|
-
pendingRefreshFocus;
|
|
27
|
-
operationGeneration = 0;
|
|
28
|
-
terminalRunGeneration = 0;
|
|
29
|
-
panelRefreshRequired = false;
|
|
30
|
-
selectionGeneration = 0;
|
|
31
49
|
connected = false;
|
|
32
|
-
|
|
50
|
+
selectionGeneration = 0;
|
|
51
|
+
terminalGeneration = 0;
|
|
52
|
+
expandedGroupKeys = new Set();
|
|
53
|
+
moveRecoveryObserved = false;
|
|
33
54
|
root;
|
|
34
|
-
onConfigChanged = (workspaceKey) => {
|
|
35
|
-
const context = this.contextValue;
|
|
36
|
-
if (!this.connected || context === undefined || cacheKeyForContext(context) !== workspaceKey)
|
|
37
|
-
return;
|
|
38
|
-
context.host.requestRender();
|
|
39
|
-
this.render();
|
|
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
|
-
};
|
|
68
55
|
constructor() {
|
|
69
56
|
super();
|
|
70
57
|
this.root = this.attachShadow({ mode: "open" });
|
|
71
58
|
this.root.addEventListener("keydown", (event) => {
|
|
72
59
|
if (event instanceof KeyboardEvent)
|
|
73
|
-
this.
|
|
60
|
+
this.handleKeyDown(event);
|
|
74
61
|
});
|
|
75
62
|
}
|
|
76
63
|
set context(value) {
|
|
77
|
-
const previousKey =
|
|
78
|
-
const nextKey =
|
|
64
|
+
const previousKey = contextKey(this.contextValue);
|
|
65
|
+
const nextKey = contextKey(value);
|
|
79
66
|
this.contextValue = value;
|
|
80
|
-
if (previousKey === nextKey)
|
|
67
|
+
if (previousKey === nextKey) {
|
|
68
|
+
this.render();
|
|
81
69
|
return;
|
|
70
|
+
}
|
|
82
71
|
this.selectionGeneration += 1;
|
|
83
|
-
this.
|
|
84
|
-
this.terminalRunGeneration += 1;
|
|
72
|
+
this.terminalGeneration += 1;
|
|
85
73
|
this.operation = undefined;
|
|
86
|
-
this.
|
|
87
|
-
this.mode = "view";
|
|
74
|
+
this.pendingAction = undefined;
|
|
88
75
|
this.editor = undefined;
|
|
89
76
|
this.deleteState = undefined;
|
|
90
|
-
this.resetState = undefined;
|
|
91
|
-
this.failure = undefined;
|
|
92
|
-
this.validationErrors = undefined;
|
|
93
77
|
this.pendingRefreshFocus = undefined;
|
|
94
|
-
this.
|
|
95
|
-
this.panelRefreshRequired = false;
|
|
78
|
+
this.validationErrors = undefined;
|
|
96
79
|
this.status = undefined;
|
|
80
|
+
this.runningTaskKey = undefined;
|
|
81
|
+
this.mode = "view";
|
|
82
|
+
this.filter = "all";
|
|
83
|
+
this.idManuallyEdited = false;
|
|
84
|
+
this.moveRecoveryObserved = false;
|
|
97
85
|
this.render();
|
|
98
86
|
}
|
|
87
|
+
set workspaceTasksState(value) {
|
|
88
|
+
const previous = this.stateValue;
|
|
89
|
+
this.stateValue = value;
|
|
90
|
+
this.recordPendingSourceObservations(value);
|
|
91
|
+
if (value.move !== undefined)
|
|
92
|
+
this.moveRecoveryObserved = true;
|
|
93
|
+
this.rememberOpenGroups();
|
|
94
|
+
this.pruneExpandedGroups();
|
|
95
|
+
if (this.reconcilePendingAction())
|
|
96
|
+
return;
|
|
97
|
+
if (previous.move !== undefined && value.move === undefined && this.moveRecoveryObserved && this.editor !== undefined) {
|
|
98
|
+
if (this.isMoveComplete(this.editor, value)) {
|
|
99
|
+
const target = this.editor.focusReturn;
|
|
100
|
+
this.editor = undefined;
|
|
101
|
+
this.mode = "view";
|
|
102
|
+
this.validationErrors = undefined;
|
|
103
|
+
this.moveRecoveryObserved = false;
|
|
104
|
+
this.status = { kind: "success", message: "Task move completed." };
|
|
105
|
+
this.render();
|
|
106
|
+
this.focusTarget(target);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.status = { kind: "info", message: "Move was not completed. Confirm the move again after reviewing the catalogs." };
|
|
110
|
+
}
|
|
111
|
+
this.render();
|
|
112
|
+
}
|
|
113
|
+
get workspaceTasksState() {
|
|
114
|
+
return this.stateValue;
|
|
115
|
+
}
|
|
116
|
+
set workspaceTasksActions(value) {
|
|
117
|
+
this.actionsValue = value;
|
|
118
|
+
this.render();
|
|
119
|
+
}
|
|
120
|
+
get workspaceTasksActions() {
|
|
121
|
+
return this.actionsValue;
|
|
122
|
+
}
|
|
99
123
|
connectedCallback() {
|
|
100
124
|
this.connected = true;
|
|
101
|
-
this.unsubscribe = subscribeWorkspaceTasksConfig(this.onConfigChanged);
|
|
102
125
|
this.render();
|
|
103
126
|
}
|
|
104
127
|
disconnectedCallback() {
|
|
105
128
|
this.connected = false;
|
|
106
129
|
this.selectionGeneration += 1;
|
|
107
|
-
this.
|
|
108
|
-
this.terminalRunGeneration += 1;
|
|
130
|
+
this.terminalGeneration += 1;
|
|
109
131
|
this.operation = undefined;
|
|
110
|
-
this.
|
|
111
|
-
this.
|
|
112
|
-
this.unsubscribe = undefined;
|
|
132
|
+
this.pendingAction = undefined;
|
|
133
|
+
this.runningTaskKey = undefined;
|
|
113
134
|
}
|
|
114
135
|
render() {
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
this.rememberOpenGroups();
|
|
137
|
+
this.pruneExpandedGroups();
|
|
138
|
+
if (this.contextValue === undefined) {
|
|
117
139
|
this.root.innerHTML = `${taskStyles()}<section class="empty">Select a workspace.</section>`;
|
|
118
140
|
return;
|
|
119
141
|
}
|
|
120
|
-
const
|
|
121
|
-
const
|
|
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";
|
|
142
|
+
const canAdd = this.canWriteScope("workspace") || this.canWriteScope("global");
|
|
143
|
+
const toolbarDisabled = this.operation !== undefined || this.mode === "refresh-discard-confirm";
|
|
126
144
|
this.root.innerHTML = `
|
|
127
145
|
${taskStyles()}
|
|
128
146
|
<main class="tasks-panel" data-panel-mode="${escapeAttr(this.mode)}">
|
|
129
147
|
<section class="toolbar">
|
|
130
148
|
<h2 data-panel-heading tabindex="-1">Workspace Tasks</h2>
|
|
131
149
|
<span class="toolbar-tasks">
|
|
132
|
-
${
|
|
133
|
-
<button type="button" class="secondary" data-refresh
|
|
150
|
+
${canAdd ? `<button type="button" class="secondary" data-add-task>Add Task</button>` : ""}
|
|
151
|
+
<button type="button" class="secondary" data-refresh ${toolbarDisabled ? "disabled" : ""}>Refresh</button>
|
|
134
152
|
<button type="button" class="secondary" data-open-terminal>Open Terminal</button>
|
|
135
153
|
</span>
|
|
136
154
|
</section>
|
|
137
155
|
${this.renderStatus()}
|
|
138
156
|
<section class="viewer tasks-viewer">
|
|
139
|
-
${this.renderBody(
|
|
157
|
+
${this.renderBody()}
|
|
140
158
|
</section>
|
|
141
159
|
</main>
|
|
142
160
|
`;
|
|
143
|
-
this.bindHandlers(
|
|
161
|
+
this.bindHandlers();
|
|
144
162
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
163
|
+
renderStatus() {
|
|
164
|
+
const parts = [];
|
|
165
|
+
const gate = this.stateValue.mutationGate;
|
|
166
|
+
if (gate !== undefined) {
|
|
167
|
+
parts.push(`<div class="status warning" data-mutation-gate role="status">${escapeHtml(gate.message)}</div>`);
|
|
168
|
+
}
|
|
169
|
+
const move = this.stateValue.move;
|
|
170
|
+
if (move !== undefined) {
|
|
171
|
+
const retryDisabled = !move.retryAllowed || this.operation !== undefined;
|
|
172
|
+
parts.push(`<div class="status warning" data-move-state role="status">
|
|
173
|
+
<strong>${escapeHtml(move.kind === "partial" ? "Move recovery pending." : move.kind === "conflict" ? "Manual resolution required." : "Move outcome needs verification.")}</strong>
|
|
174
|
+
<p>${escapeHtml(move.message)}</p>
|
|
175
|
+
${move.kind === "conflict" ? `<p data-manual-resolution>Refresh the catalogs and resolve the definitions manually.</p>` : ""}
|
|
176
|
+
<button type="button" class="secondary" data-retry-move ${retryDisabled ? "disabled" : ""}>Retry move</button>
|
|
177
|
+
</div>`);
|
|
178
|
+
}
|
|
179
|
+
const moveError = this.stateValue.moveError;
|
|
180
|
+
if (moveError !== undefined) {
|
|
181
|
+
parts.push(`<div class="status error" data-move-error role="status"><strong>Move was not applied.</strong><p>${escapeHtml(moveError.message)}</p></div>`);
|
|
182
|
+
}
|
|
183
|
+
if (this.status !== undefined) {
|
|
184
|
+
const detail = this.status.detail === undefined ? "" : `<pre class="diagnostic">${escapeHtml(this.status.detail)}</pre>`;
|
|
185
|
+
parts.push(`<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>`);
|
|
186
|
+
}
|
|
187
|
+
return parts.join("");
|
|
150
188
|
}
|
|
151
|
-
renderBody(
|
|
189
|
+
renderBody() {
|
|
152
190
|
if (this.mode === "add" || this.mode === "edit")
|
|
153
|
-
return this.renderEditor(
|
|
191
|
+
return this.renderEditor();
|
|
154
192
|
if (this.mode === "delete-confirm")
|
|
155
|
-
return this.renderDeleteConfirmation(
|
|
156
|
-
if (this.mode === "
|
|
157
|
-
return this.
|
|
193
|
+
return this.renderDeleteConfirmation();
|
|
194
|
+
if (this.mode === "move-confirm")
|
|
195
|
+
return this.renderMoveConfirmation();
|
|
158
196
|
if (this.mode === "refresh-discard-confirm")
|
|
159
197
|
return this.renderRefreshDiscardConfirmation();
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
198
|
+
return this.renderView();
|
|
199
|
+
}
|
|
200
|
+
renderView() {
|
|
201
|
+
const workspaceCount = loadedCount(this.stateValue.workspace);
|
|
202
|
+
const globalCount = loadedCount(this.stateValue.global);
|
|
203
|
+
return `
|
|
204
|
+
<section class="scope-filters" role="group" aria-label="Task scope">
|
|
205
|
+
${this.renderFilterButton("all", "All", workspaceCount + globalCount)}
|
|
206
|
+
${this.renderFilterButton("global", "Global", globalCount)}
|
|
207
|
+
${this.renderFilterButton("workspace", "Project", workspaceCount)}
|
|
208
|
+
</section>
|
|
209
|
+
<p class="muted helper">Tasks run as one script in one dedicated terminal rooted at the selected workspace. Use <code>set -e</code> or <code>&&</code> when the script should stop after a failure.</p>
|
|
210
|
+
<div class="catalog-list">
|
|
211
|
+
${this.filter === "all" || this.filter === "global" ? this.renderCatalog("global", "Global tasks", this.stateValue.global) : ""}
|
|
212
|
+
${this.filter === "all" || this.filter === "workspace" ? this.renderCatalog("workspace", "Project tasks", this.stateValue.workspace) : ""}
|
|
213
|
+
</div>
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
renderFilterButton(filter, label, count) {
|
|
217
|
+
return `<button type="button" class="scope-filter ${this.filter === filter ? "selected" : ""}" data-filter="${filter}" aria-pressed="${this.filter === filter ? "true" : "false"}">${escapeHtml(label)} <span class="count">${String(count)}</span></button>`;
|
|
218
|
+
}
|
|
219
|
+
renderCatalog(scope, heading, catalog) {
|
|
220
|
+
const label = scopeLabel(scope);
|
|
221
|
+
const warning = catalog.kind === "loaded" || catalog.kind === "missing"
|
|
222
|
+
? catalog.refreshError === undefined ? "" : `<p class="status warning" data-refresh-error>${escapeHtml(catalog.refreshError)}</p>`
|
|
223
|
+
: "";
|
|
224
|
+
let body;
|
|
225
|
+
if (catalog.kind === "loading") {
|
|
226
|
+
body = `<p class="muted" data-catalog-loading>Loading ${escapeHtml(label)} tasks...</p>`;
|
|
168
227
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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>`;
|
|
228
|
+
else if (catalog.kind === "invalid" || catalog.kind === "unavailable" || catalog.kind === "error") {
|
|
229
|
+
const hint = catalog.kind === "error" ? "" : `<p>${escapeHtml(catalog.hint)}</p>`;
|
|
230
|
+
const detail = catalog.kind === "invalid" || catalog.kind === "unavailable" && catalog.detail !== undefined
|
|
231
|
+
? `<pre class="diagnostic">${escapeHtml(catalog.detail)}</pre>`
|
|
232
|
+
: "";
|
|
233
|
+
body = `<div class="status error" data-catalog-error><strong>${escapeHtml(catalog.message)}</strong>${hint}${detail}</div>`;
|
|
177
234
|
}
|
|
178
|
-
if (
|
|
179
|
-
|
|
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
|
-
`;
|
|
235
|
+
else if (catalog.kind === "missing") {
|
|
236
|
+
body = `${warning}<p class="empty-state"><strong>No Project task catalog is configured.</strong><span>Create ${escapeHtml(TASKS_CONFIG_PATH)} in this workspace or add a task here.</span></p>`;
|
|
188
237
|
}
|
|
189
|
-
if (
|
|
190
|
-
|
|
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
|
-
`;
|
|
238
|
+
else if (catalog.config.tasks.length === 0) {
|
|
239
|
+
body = `${warning}<p class="empty-state">No ${escapeHtml(label.toLowerCase())} tasks are defined.</p>`;
|
|
197
240
|
}
|
|
198
|
-
|
|
199
|
-
|
|
241
|
+
else {
|
|
242
|
+
body = `${warning}${renderTaskGroups(scope, catalog.config.tasks, this.runningTaskKey, this.isScopeDisabled(scope), this.expandedGroupKeys)}`;
|
|
200
243
|
}
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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>`;
|
|
244
|
+
return `<section class="scope-catalog" data-catalog-scope="${scope}">
|
|
245
|
+
<h3>${escapeHtml(heading)} <span class="catalog-count">${String(loadedCount(catalog))}</span></h3>
|
|
246
|
+
${body}
|
|
247
|
+
</section>`;
|
|
211
248
|
}
|
|
212
|
-
|
|
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) {
|
|
249
|
+
renderEditor() {
|
|
219
250
|
const editor = this.editor;
|
|
220
251
|
if (editor === undefined)
|
|
221
252
|
return "";
|
|
222
|
-
const
|
|
253
|
+
const targetScope = editor.draft.global ? "global" : "workspace";
|
|
254
|
+
const validation = this.validateEditor();
|
|
223
255
|
const errors = this.validationErrors ?? {};
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
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>` : "";
|
|
256
|
+
const locked = this.operation !== undefined || this.stateValue.move !== undefined || this.isScopeDisabled(targetScope);
|
|
257
|
+
const canWrite = this.canWriteScope(targetScope);
|
|
258
|
+
const saveDisabled = locked || !canWrite || !validation.ok;
|
|
259
|
+
const heading = editor.sourceRef === undefined ? "Add Task" : "Edit Task";
|
|
260
|
+
const sourceLabel = editor.sourceRef === undefined ? scopeLabel(targetScope) : scopeLabel(editor.sourceRef.scope);
|
|
237
261
|
return `
|
|
238
262
|
<section class="task-editor" data-task-editor>
|
|
239
|
-
<h3>${heading}</h3>
|
|
263
|
+
<div class="editor-heading"><h3>${heading}</h3><span class="scope-badge" data-scope-badge>${escapeHtml(`${sourceLabel} task`)}</span></div>
|
|
240
264
|
<form class="task-form" data-task-form>
|
|
241
265
|
<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"${
|
|
243
|
-
${renderFieldError("title",
|
|
266
|
+
<input id="task-title" name="title" type="text" value="${escapeAttr(editor.draft.title)}" placeholder="Build app" data-editor-title aria-required="true"${fieldAttrs("title", errors.title, locked)}>
|
|
267
|
+
${renderFieldError("title", errors.title)}
|
|
244
268
|
|
|
245
269
|
<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="${
|
|
270
|
+
<textarea id="task-command" name="command" data-editor-command aria-required="true" aria-describedby="task-command-help${errors.command === undefined ? "" : " task-command-error"}"${errors.command === undefined ? "" : " aria-invalid=\"true\""}${locked ? " disabled" : ""}>${escapeHtml(editor.draft.command)}</textarea>
|
|
247
271
|
<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",
|
|
272
|
+
${renderFieldError("command", errors.command)}
|
|
249
273
|
|
|
250
274
|
<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"${
|
|
252
|
-
${renderFieldError("id",
|
|
275
|
+
<input id="task-id" name="id" type="text" value="${escapeAttr(editor.draft.id)}" placeholder="Auto-generated from title" data-editor-id aria-required="true"${fieldAttrs("id", errors.id, locked)}>
|
|
276
|
+
${renderFieldError("id", errors.id)}
|
|
253
277
|
|
|
254
278
|
<label for="task-description">Description</label>
|
|
255
279
|
<input id="task-description" name="description" type="text" value="${escapeAttr(editor.draft.description)}" placeholder="Optional description" data-editor-description ${locked ? "disabled" : ""}>
|
|
@@ -257,6 +281,12 @@ class PiWebUiTasksPanel extends HTMLElement {
|
|
|
257
281
|
<label for="task-group">Group</label>
|
|
258
282
|
<input id="task-group" name="group" type="text" value="${escapeAttr(editor.draft.group)}" placeholder="Optional group name" data-editor-group ${locked ? "disabled" : ""}>
|
|
259
283
|
|
|
284
|
+
<span class="checkbox-field">
|
|
285
|
+
<input id="task-global" name="global" type="checkbox" data-editor-global ${editor.draft.global ? "checked" : ""} ${locked ? "disabled" : ""}>
|
|
286
|
+
<label for="task-global">Available in all projects on this machine</label>
|
|
287
|
+
</span>
|
|
288
|
+
<p class="field-help" data-scope-help>Global definitions run in the currently selected workspace root. Project definitions are stored in ${escapeHtml(TASKS_CONFIG_PATH)} for this workspace.</p>
|
|
289
|
+
|
|
260
290
|
<span class="checkbox-field">
|
|
261
291
|
<input id="task-confirm" name="confirm" type="checkbox" ${editor.draft.confirm ? "checked" : ""} data-editor-confirm ${locked ? "disabled" : ""}>
|
|
262
292
|
<label for="task-confirm">Require confirmation before running</label>
|
|
@@ -264,258 +294,219 @@ class PiWebUiTasksPanel extends HTMLElement {
|
|
|
264
294
|
|
|
265
295
|
<div class="editor-actions">
|
|
266
296
|
<button type="button" class="secondary" data-cancel-editor>Cancel</button>
|
|
267
|
-
${failureRefresh}
|
|
268
297
|
<button type="button" class="primary" data-save-task ${saveDisabled ? "disabled" : ""}>Save Task</button>
|
|
269
298
|
</div>
|
|
270
299
|
</form>
|
|
271
300
|
</section>
|
|
272
301
|
`;
|
|
273
302
|
}
|
|
274
|
-
renderDeleteConfirmation(
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
303
|
+
renderDeleteConfirmation() {
|
|
304
|
+
const pending = this.deleteState;
|
|
305
|
+
if (pending === undefined)
|
|
277
306
|
return "";
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
`;
|
|
307
|
+
const label = scopeLabel(pending.ref.scope);
|
|
308
|
+
return `<section class="confirmation" data-delete-confirmation>
|
|
309
|
+
<div class="editor-heading"><h3>Delete Task</h3><span class="scope-badge">${escapeHtml(`${label} task`)}</span></div>
|
|
310
|
+
<p>Are you sure you want to delete <strong>${escapeHtml(pending.task.title)}</strong> (${escapeHtml(label)})?</p>
|
|
311
|
+
<pre class="task-script">${escapeHtml(pending.task.command)}</pre>
|
|
312
|
+
<div class="editor-actions">
|
|
313
|
+
<button type="button" class="secondary" data-cancel-delete>Cancel</button>
|
|
314
|
+
<button type="button" class="danger" data-confirm-delete aria-label="Delete ${escapeAttr(pending.task.title)} (${escapeAttr(label)})" ${this.operation === undefined && !this.isScopeDisabled(pending.ref.scope) ? "" : "disabled"}>Delete Task</button>
|
|
315
|
+
</div>
|
|
316
|
+
</section>`;
|
|
289
317
|
}
|
|
290
|
-
|
|
291
|
-
|
|
318
|
+
renderMoveConfirmation() {
|
|
319
|
+
const editor = this.editor;
|
|
320
|
+
if (editor?.sourceRef === undefined)
|
|
292
321
|
return "";
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
322
|
+
const source = scopeLabel(editor.sourceRef.scope);
|
|
323
|
+
const destination = scopeLabel(editor.draft.global ? "global" : "workspace");
|
|
324
|
+
const validation = this.validateEditor();
|
|
325
|
+
return `<section class="confirmation" data-move-confirmation>
|
|
326
|
+
<div class="editor-heading"><h3>Change Task Scope</h3><span class="scope-badge">${escapeHtml(`${source} to ${destination}`)}</span></div>
|
|
327
|
+
<p>Move <strong>${escapeHtml(editor.draft.title)}</strong> from ${escapeHtml(source)} to ${escapeHtml(destination)}? The source definition will be removed.</p>
|
|
328
|
+
<pre class="task-script">${escapeHtml(validation.ok ? validation.task.command : editor.draft.command)}</pre>
|
|
329
|
+
<div class="editor-actions">
|
|
330
|
+
<button type="button" class="secondary" data-cancel-move>Cancel</button>
|
|
331
|
+
<button type="button" class="primary" data-confirm-move ${this.operation === undefined && validation.ok ? "" : "disabled"}>Confirm move</button>
|
|
332
|
+
</div>
|
|
333
|
+
</section>`;
|
|
303
334
|
}
|
|
304
335
|
renderRefreshDiscardConfirmation() {
|
|
305
|
-
return
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
<
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
</section>
|
|
314
|
-
`;
|
|
336
|
+
return `<section class="confirmation" data-refresh-discard-confirmation>
|
|
337
|
+
<h3>Discard draft and refresh?</h3>
|
|
338
|
+
<p>Your unsaved task draft will be discarded. Refresh loads the authoritative catalogs.</p>
|
|
339
|
+
<div class="editor-actions">
|
|
340
|
+
<button type="button" class="secondary" data-cancel-refresh-discard>Cancel</button>
|
|
341
|
+
<button type="button" class="primary" data-confirm-refresh-discard>Discard & Refresh</button>
|
|
342
|
+
</div>
|
|
343
|
+
</section>`;
|
|
315
344
|
}
|
|
316
|
-
bindHandlers(
|
|
317
|
-
this.root.querySelector("
|
|
318
|
-
|
|
319
|
-
});
|
|
320
|
-
this.root.querySelector("
|
|
321
|
-
|
|
322
|
-
});
|
|
323
|
-
this.root.querySelector("
|
|
324
|
-
|
|
325
|
-
});
|
|
326
|
-
this.root.querySelector("
|
|
327
|
-
|
|
328
|
-
});
|
|
329
|
-
this.root.
|
|
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);
|
|
355
|
-
});
|
|
356
|
-
for (const button of this.root.querySelectorAll("button[data-task-id]")) {
|
|
345
|
+
bindHandlers() {
|
|
346
|
+
this.root.querySelector("[data-add-task]")?.addEventListener("click", () => { this.openAddTaskEditor(); });
|
|
347
|
+
this.root.querySelector("[data-refresh]")?.addEventListener("click", () => { void this.requestRefresh(); });
|
|
348
|
+
this.root.querySelector("[data-open-terminal]")?.addEventListener("click", () => { this.openWorkspaceTerminal(); });
|
|
349
|
+
this.root.querySelector("[data-retry-move]")?.addEventListener("click", () => { void this.retryMove(); });
|
|
350
|
+
this.root.querySelector("[data-cancel-editor]")?.addEventListener("click", () => { this.cancelEditor(); });
|
|
351
|
+
this.root.querySelector("[data-save-task]")?.addEventListener("click", () => { void this.saveTask(); });
|
|
352
|
+
this.root.querySelector("[data-cancel-delete]")?.addEventListener("click", () => { this.cancelDelete(); });
|
|
353
|
+
this.root.querySelector("[data-confirm-delete]")?.addEventListener("click", () => { void this.confirmDelete(); });
|
|
354
|
+
this.root.querySelector("[data-cancel-move]")?.addEventListener("click", () => { this.cancelMove(); });
|
|
355
|
+
this.root.querySelector("[data-confirm-move]")?.addEventListener("click", () => { void this.confirmMove(); });
|
|
356
|
+
this.root.querySelector("[data-cancel-refresh-discard]")?.addEventListener("click", () => { this.cancelRefreshDiscard(); });
|
|
357
|
+
this.root.querySelector("[data-confirm-refresh-discard]")?.addEventListener("click", () => { void this.confirmRefreshDiscard(); });
|
|
358
|
+
for (const button of this.root.querySelectorAll("button[data-filter]")) {
|
|
357
359
|
button.addEventListener("click", () => {
|
|
358
|
-
|
|
360
|
+
const value = button.getAttribute("data-filter");
|
|
361
|
+
if (value === "all" || value === "global" || value === "workspace") {
|
|
362
|
+
this.filter = value;
|
|
363
|
+
this.render();
|
|
364
|
+
}
|
|
359
365
|
});
|
|
360
366
|
}
|
|
361
367
|
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
|
-
});
|
|
368
|
+
button.addEventListener("click", () => { this.openEditTaskEditor(button.getAttribute("data-edit-task")); });
|
|
365
369
|
}
|
|
366
370
|
for (const button of this.root.querySelectorAll("button[data-delete-task]")) {
|
|
367
|
-
button.addEventListener("click", () => {
|
|
368
|
-
|
|
371
|
+
button.addEventListener("click", () => { this.openDeleteConfirmation(button.getAttribute("data-delete-task")); });
|
|
372
|
+
}
|
|
373
|
+
for (const button of this.root.querySelectorAll("button[data-run-task]")) {
|
|
374
|
+
button.addEventListener("click", () => { void this.dispatchTask(button.getAttribute("data-run-task")); });
|
|
375
|
+
}
|
|
376
|
+
for (const details of this.root.querySelectorAll("details[data-group-key]")) {
|
|
377
|
+
details.addEventListener("toggle", () => {
|
|
378
|
+
const key = details.getAttribute("data-group-key");
|
|
379
|
+
if (key === null)
|
|
380
|
+
return;
|
|
381
|
+
if (details.open)
|
|
382
|
+
this.expandedGroupKeys.add(key);
|
|
383
|
+
else
|
|
384
|
+
this.expandedGroupKeys.delete(key);
|
|
369
385
|
});
|
|
370
386
|
}
|
|
371
|
-
|
|
387
|
+
this.bindEditorInputs();
|
|
388
|
+
}
|
|
389
|
+
bindEditorInputs() {
|
|
390
|
+
const title = this.root.querySelector("[data-editor-title]");
|
|
372
391
|
title?.addEventListener("input", (event) => {
|
|
373
392
|
if (!(event.target instanceof HTMLInputElement) || this.editor === undefined)
|
|
374
393
|
return;
|
|
375
394
|
this.editor.draft.title = event.target.value;
|
|
376
395
|
if (!this.idManuallyEdited) {
|
|
377
396
|
this.editor.draft.id = suggestWorkspaceTaskId(event.target.value);
|
|
378
|
-
const id = this.root.querySelector("
|
|
397
|
+
const id = this.root.querySelector("[data-editor-id]");
|
|
379
398
|
if (id !== null)
|
|
380
399
|
id.value = this.editor.draft.id;
|
|
381
400
|
}
|
|
382
|
-
this.updateValidationInPlace(
|
|
401
|
+
this.updateValidationInPlace();
|
|
383
402
|
});
|
|
384
|
-
const id = this.root.querySelector("
|
|
403
|
+
const id = this.root.querySelector("[data-editor-id]");
|
|
385
404
|
id?.addEventListener("input", (event) => {
|
|
386
405
|
if (!(event.target instanceof HTMLInputElement) || this.editor === undefined)
|
|
387
406
|
return;
|
|
388
407
|
this.editor.draft.id = event.target.value;
|
|
389
408
|
this.idManuallyEdited = true;
|
|
390
|
-
this.updateValidationInPlace(
|
|
409
|
+
this.updateValidationInPlace();
|
|
391
410
|
});
|
|
392
|
-
const command = this.root.querySelector("
|
|
411
|
+
const command = this.root.querySelector("[data-editor-command]");
|
|
393
412
|
command?.addEventListener("input", (event) => {
|
|
394
413
|
if (!(event.target instanceof HTMLTextAreaElement) || this.editor === undefined)
|
|
395
414
|
return;
|
|
396
415
|
this.editor.draft.command = event.target.value;
|
|
397
|
-
this.updateValidationInPlace(
|
|
416
|
+
this.updateValidationInPlace();
|
|
398
417
|
});
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
418
|
+
for (const [selector, field] of [["[data-editor-description]", "description"], ["[data-editor-group]", "group"]]) {
|
|
419
|
+
this.root.querySelector(selector)?.addEventListener("input", (event) => {
|
|
420
|
+
if (!(event.target instanceof HTMLInputElement) || this.editor === undefined)
|
|
421
|
+
return;
|
|
422
|
+
this.editor.draft[field] = event.target.value;
|
|
423
|
+
this.updateValidationInPlace();
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
this.root.querySelector("[data-editor-global]")?.addEventListener("change", (event) => {
|
|
408
427
|
if (!(event.target instanceof HTMLInputElement) || this.editor === undefined)
|
|
409
428
|
return;
|
|
410
|
-
this.editor.draft.
|
|
411
|
-
this.updateValidationInPlace(
|
|
429
|
+
this.editor.draft.global = event.target.checked;
|
|
430
|
+
this.updateValidationInPlace();
|
|
431
|
+
const badge = this.root.querySelector("[data-scope-badge]");
|
|
432
|
+
if (badge !== null)
|
|
433
|
+
badge.textContent = `${scopeLabel(this.editor.draft.global ? "global" : "workspace")} task`;
|
|
412
434
|
});
|
|
413
|
-
|
|
414
|
-
confirm?.addEventListener("change", (event) => {
|
|
435
|
+
this.root.querySelector("[data-editor-confirm]")?.addEventListener("change", (event) => {
|
|
415
436
|
if (!(event.target instanceof HTMLInputElement) || this.editor === undefined)
|
|
416
437
|
return;
|
|
417
438
|
this.editor.draft.confirm = event.target.checked;
|
|
418
|
-
this.updateValidationInPlace(
|
|
439
|
+
this.updateValidationInPlace();
|
|
419
440
|
});
|
|
420
|
-
this.root.querySelector("
|
|
441
|
+
this.root.querySelector("[data-task-form]")?.addEventListener("submit", (event) => {
|
|
421
442
|
event.preventDefault();
|
|
422
|
-
void this.saveTask(
|
|
443
|
+
void this.saveTask();
|
|
423
444
|
});
|
|
424
445
|
}
|
|
425
446
|
openAddTaskEditor() {
|
|
426
|
-
|
|
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"))
|
|
447
|
+
if (this.contextValue === undefined || this.operation !== undefined || !this.canWriteScope("workspace") && !this.canWriteScope("global"))
|
|
431
448
|
return;
|
|
432
449
|
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
|
-
};
|
|
450
|
+
this.editor = { draft, initialDraft: cloneDraft(draft), sourceRef: undefined, originalIndex: undefined, focusReturn: { kind: "add" } };
|
|
440
451
|
this.mode = "add";
|
|
441
|
-
this.
|
|
452
|
+
this.status = undefined;
|
|
442
453
|
this.validationErrors = undefined;
|
|
443
454
|
this.idManuallyEdited = false;
|
|
444
|
-
this.
|
|
455
|
+
this.moveRecoveryObserved = false;
|
|
445
456
|
this.render();
|
|
446
|
-
this.focusSelector("
|
|
457
|
+
this.focusSelector("[data-editor-title]");
|
|
447
458
|
}
|
|
448
|
-
openEditTaskEditor(
|
|
449
|
-
if (
|
|
459
|
+
openEditTaskEditor(key) {
|
|
460
|
+
if (this.operation !== undefined || key === null)
|
|
450
461
|
return;
|
|
451
|
-
const
|
|
452
|
-
if (
|
|
462
|
+
const ref = parseRef(key);
|
|
463
|
+
if (ref === undefined)
|
|
453
464
|
return;
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
this.setError("That task is no longer available. Click Refresh, then try again.");
|
|
465
|
+
const found = this.findTask(ref);
|
|
466
|
+
if (found === undefined) {
|
|
467
|
+
this.setError("That task is no longer available. Refresh, then try again.");
|
|
458
468
|
return;
|
|
459
469
|
}
|
|
460
470
|
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,
|
|
471
|
+
id: found.task.id,
|
|
472
|
+
title: found.task.title,
|
|
473
|
+
command: found.task.command,
|
|
474
|
+
description: found.task.description ?? "",
|
|
475
|
+
group: found.task.group ?? "",
|
|
476
|
+
confirm: found.task.confirm,
|
|
477
|
+
global: ref.scope === "global",
|
|
467
478
|
};
|
|
468
479
|
this.editor = {
|
|
469
480
|
draft,
|
|
470
481
|
initialDraft: cloneDraft(draft),
|
|
471
|
-
|
|
472
|
-
originalIndex: index,
|
|
473
|
-
focusReturn: { kind: "edit",
|
|
482
|
+
sourceRef: ref,
|
|
483
|
+
originalIndex: found.index,
|
|
484
|
+
focusReturn: { kind: "edit", ref },
|
|
474
485
|
};
|
|
475
486
|
this.mode = "edit";
|
|
476
|
-
this.
|
|
487
|
+
this.status = undefined;
|
|
477
488
|
this.validationErrors = undefined;
|
|
478
489
|
this.idManuallyEdited = true;
|
|
479
|
-
this.
|
|
490
|
+
this.moveRecoveryObserved = false;
|
|
480
491
|
this.render();
|
|
481
|
-
this.focusSelector("
|
|
492
|
+
this.focusSelector("[data-editor-title]");
|
|
482
493
|
}
|
|
483
|
-
openDeleteConfirmation(
|
|
484
|
-
if (
|
|
494
|
+
openDeleteConfirmation(key) {
|
|
495
|
+
if (this.operation !== undefined || key === null)
|
|
485
496
|
return;
|
|
486
|
-
const
|
|
487
|
-
if (
|
|
497
|
+
const ref = parseRef(key);
|
|
498
|
+
if (ref === undefined)
|
|
488
499
|
return;
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
this.setError("That task is no longer available. Click Refresh, then try again.");
|
|
500
|
+
const found = this.findTask(ref);
|
|
501
|
+
if (found === undefined) {
|
|
502
|
+
this.setError("That task is no longer available. Refresh, then try again.");
|
|
493
503
|
return;
|
|
494
504
|
}
|
|
495
|
-
this.deleteState = {
|
|
496
|
-
task,
|
|
497
|
-
index,
|
|
498
|
-
sourceSnapshot: entry.state.snapshot,
|
|
499
|
-
focusReturn: { kind: "delete", id: task.id },
|
|
500
|
-
};
|
|
505
|
+
this.deleteState = { ref, task: cloneTask(found.task), focusReturn: { kind: "delete", ref } };
|
|
501
506
|
this.mode = "delete-confirm";
|
|
502
|
-
this.failure = undefined;
|
|
503
|
-
this.status = undefined;
|
|
504
|
-
this.render();
|
|
505
|
-
this.focusSelector("button[data-cancel-delete]");
|
|
506
|
-
}
|
|
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
507
|
this.status = undefined;
|
|
517
508
|
this.render();
|
|
518
|
-
this.focusSelector("
|
|
509
|
+
this.focusSelector("[data-cancel-delete]");
|
|
519
510
|
}
|
|
520
511
|
cancelEditor() {
|
|
521
512
|
if (this.operation !== undefined)
|
|
@@ -524,7 +515,7 @@ class PiWebUiTasksPanel extends HTMLElement {
|
|
|
524
515
|
this.editor = undefined;
|
|
525
516
|
this.validationErrors = undefined;
|
|
526
517
|
this.idManuallyEdited = false;
|
|
527
|
-
this.
|
|
518
|
+
this.moveRecoveryObserved = false;
|
|
528
519
|
this.mode = "view";
|
|
529
520
|
this.status = undefined;
|
|
530
521
|
this.render();
|
|
@@ -535,104 +526,94 @@ class PiWebUiTasksPanel extends HTMLElement {
|
|
|
535
526
|
return;
|
|
536
527
|
const target = this.deleteState?.focusReturn;
|
|
537
528
|
this.deleteState = undefined;
|
|
538
|
-
this.failure = undefined;
|
|
539
529
|
this.mode = "view";
|
|
540
530
|
this.status = undefined;
|
|
541
531
|
this.render();
|
|
542
532
|
this.focusTarget(target);
|
|
543
533
|
}
|
|
544
|
-
|
|
545
|
-
if (this.operation !== undefined)
|
|
534
|
+
cancelMove() {
|
|
535
|
+
if (this.operation !== undefined || this.editor === undefined)
|
|
546
536
|
return;
|
|
547
|
-
|
|
548
|
-
this.resetState = undefined;
|
|
549
|
-
this.failure = undefined;
|
|
550
|
-
this.mode = "view";
|
|
551
|
-
this.status = undefined;
|
|
537
|
+
this.mode = this.editor.sourceRef === undefined ? "add" : "edit";
|
|
552
538
|
this.render();
|
|
553
|
-
this.
|
|
539
|
+
this.focusSelector("[data-editor-title]");
|
|
554
540
|
}
|
|
555
541
|
cancelRefreshDiscard() {
|
|
556
542
|
if (this.operation !== undefined)
|
|
557
543
|
return;
|
|
558
544
|
const target = this.editor?.focusReturn;
|
|
559
|
-
this.mode = this.editor === undefined ? "
|
|
545
|
+
this.mode = this.editor?.sourceRef === undefined ? "add" : "edit";
|
|
560
546
|
this.pendingRefreshFocus = undefined;
|
|
561
547
|
this.render();
|
|
562
548
|
this.focusTarget(target);
|
|
563
549
|
}
|
|
564
|
-
async confirmRefreshDiscard(
|
|
550
|
+
async confirmRefreshDiscard() {
|
|
565
551
|
if (this.mode !== "refresh-discard-confirm")
|
|
566
552
|
return;
|
|
567
553
|
const target = this.pendingRefreshFocus ?? { kind: "refresh" };
|
|
568
554
|
this.editor = undefined;
|
|
569
555
|
this.deleteState = undefined;
|
|
570
|
-
this.resetState = undefined;
|
|
571
|
-
this.failure = undefined;
|
|
572
556
|
this.validationErrors = undefined;
|
|
573
557
|
this.idManuallyEdited = false;
|
|
558
|
+
this.moveRecoveryObserved = false;
|
|
559
|
+
this.mode = "view";
|
|
574
560
|
this.pendingRefreshFocus = target;
|
|
575
|
-
await this.requestRefresh(
|
|
561
|
+
await this.requestRefresh(true);
|
|
576
562
|
}
|
|
577
|
-
async requestRefresh(
|
|
578
|
-
if (
|
|
579
|
-
return;
|
|
580
|
-
if (!force && this.isConfirmationMode())
|
|
563
|
+
async requestRefresh(force = false) {
|
|
564
|
+
if (this.operation !== undefined)
|
|
581
565
|
return;
|
|
582
|
-
if (!force &&
|
|
583
|
-
this.pendingRefreshFocus = this.editor
|
|
566
|
+
if (!force && this.isEditorDirty()) {
|
|
567
|
+
this.pendingRefreshFocus = this.editor?.focusReturn;
|
|
584
568
|
this.mode = "refresh-discard-confirm";
|
|
585
569
|
this.status = undefined;
|
|
586
570
|
this.render();
|
|
587
|
-
this.focusSelector("
|
|
571
|
+
this.focusSelector("[data-cancel-refresh-discard]");
|
|
588
572
|
return;
|
|
589
573
|
}
|
|
590
574
|
const target = this.pendingRefreshFocus ?? this.editor?.focusReturn ?? { kind: "refresh" };
|
|
591
575
|
this.pendingRefreshFocus = undefined;
|
|
592
576
|
this.editor = undefined;
|
|
593
577
|
this.deleteState = undefined;
|
|
594
|
-
this.resetState = undefined;
|
|
595
|
-
this.failure = undefined;
|
|
596
578
|
this.validationErrors = undefined;
|
|
597
579
|
this.idManuallyEdited = false;
|
|
580
|
+
this.moveRecoveryObserved = false;
|
|
598
581
|
this.mode = "view";
|
|
599
|
-
this.operation = "refresh";
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
|
|
582
|
+
this.operation = { kind: "refresh", scopes: [] };
|
|
583
|
+
const observation = this.captureActionState();
|
|
584
|
+
const generation = this.selectionGeneration;
|
|
585
|
+
const pending = {
|
|
586
|
+
kind: "refresh",
|
|
587
|
+
generation,
|
|
588
|
+
scopes: ["workspace", "global"],
|
|
589
|
+
observation,
|
|
590
|
+
focusReturn: target,
|
|
591
|
+
settled: false,
|
|
592
|
+
};
|
|
593
|
+
this.pendingAction = pending;
|
|
594
|
+
this.status = { kind: "info", message: "Refreshing workspace task catalogs..." };
|
|
603
595
|
this.render();
|
|
604
596
|
try {
|
|
605
|
-
|
|
606
|
-
if (!this.
|
|
597
|
+
await this.actionsValue.refresh();
|
|
598
|
+
if (!this.ownsSelection(generation) || this.pendingAction !== pending)
|
|
607
599
|
return;
|
|
608
|
-
|
|
609
|
-
|
|
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);
|
|
600
|
+
pending.settled = true;
|
|
601
|
+
this.reconcilePendingAction();
|
|
620
602
|
}
|
|
621
603
|
catch (error) {
|
|
622
|
-
if (!this.
|
|
604
|
+
if (!this.ownsSelection(generation) || this.pendingAction !== pending)
|
|
623
605
|
return;
|
|
606
|
+
this.pendingAction = undefined;
|
|
624
607
|
this.operation = undefined;
|
|
625
|
-
this.status = { kind: "error", message:
|
|
608
|
+
this.status = { kind: "error", message: "Could not refresh workspace task catalogs.", detail: formatError(error) };
|
|
626
609
|
this.render();
|
|
627
610
|
}
|
|
628
611
|
}
|
|
629
|
-
async saveTask(
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const entry = getWorkspaceTasksCacheEntry(cacheKeyForContext(context));
|
|
633
|
-
if (entry === undefined)
|
|
612
|
+
async saveTask() {
|
|
613
|
+
const editor = this.editor;
|
|
614
|
+
if (editor === undefined || this.operation !== undefined || (this.mode !== "add" && this.mode !== "edit"))
|
|
634
615
|
return;
|
|
635
|
-
const validation = this.validateEditor(
|
|
616
|
+
const validation = this.validateEditor();
|
|
636
617
|
if (!validation.ok) {
|
|
637
618
|
this.validationErrors = validation.errors;
|
|
638
619
|
this.status = { kind: "error", message: "Fix the highlighted fields before saving." };
|
|
@@ -640,231 +621,249 @@ class PiWebUiTasksPanel extends HTMLElement {
|
|
|
640
621
|
this.focusFirstInvalid(validation.errors);
|
|
641
622
|
return;
|
|
642
623
|
}
|
|
643
|
-
|
|
644
|
-
|
|
624
|
+
const destinationScope = editor.draft.global ? "global" : "workspace";
|
|
625
|
+
if (!this.canWriteScope(destinationScope))
|
|
626
|
+
return;
|
|
627
|
+
if (editor.sourceRef !== undefined && editor.sourceRef.scope !== destinationScope) {
|
|
628
|
+
this.mode = "move-confirm";
|
|
629
|
+
this.status = undefined;
|
|
630
|
+
this.render();
|
|
631
|
+
this.focusSelector("[data-cancel-move]");
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (editor.sourceRef !== undefined && !this.isEditorDirty()) {
|
|
635
|
+
const target = editor.focusReturn;
|
|
636
|
+
this.editor = undefined;
|
|
637
|
+
this.mode = "view";
|
|
638
|
+
this.validationErrors = undefined;
|
|
639
|
+
this.idManuallyEdited = false;
|
|
640
|
+
this.status = { kind: "info", message: "No changes to save." };
|
|
641
|
+
this.render();
|
|
642
|
+
this.focusTarget(target);
|
|
645
643
|
return;
|
|
646
644
|
}
|
|
645
|
+
const generation = this.selectionGeneration;
|
|
646
|
+
const observation = this.captureActionState();
|
|
647
|
+
const pending = {
|
|
648
|
+
kind: "save",
|
|
649
|
+
generation,
|
|
650
|
+
scopes: [destinationScope],
|
|
651
|
+
observation,
|
|
652
|
+
focusReturn: editor.focusReturn,
|
|
653
|
+
scope: destinationScope,
|
|
654
|
+
task: cloneTask(validation.task),
|
|
655
|
+
settled: false,
|
|
656
|
+
};
|
|
657
|
+
this.pendingAction = pending;
|
|
658
|
+
this.operation = { kind: "mutation", scopes: [destinationScope] };
|
|
659
|
+
this.status = { kind: "info", message: editor.sourceRef === undefined ? "Creating workspace task..." : "Saving workspace task..." };
|
|
660
|
+
this.render();
|
|
661
|
+
try {
|
|
662
|
+
if (editor.sourceRef === undefined)
|
|
663
|
+
await this.actionsValue.create(destinationScope, validation.task);
|
|
664
|
+
else
|
|
665
|
+
await this.actionsValue.update(editor.sourceRef, validation.task);
|
|
666
|
+
if (!this.ownsSelection(generation) || this.pendingAction !== pending)
|
|
667
|
+
return;
|
|
668
|
+
pending.settled = true;
|
|
669
|
+
this.reconcilePendingAction();
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
if (!this.ownsSelection(generation) || this.pendingAction !== pending)
|
|
673
|
+
return;
|
|
674
|
+
this.pendingAction = undefined;
|
|
675
|
+
this.operation = undefined;
|
|
676
|
+
this.status = { kind: "error", message: "Could not save workspace task.", detail: formatError(error) };
|
|
677
|
+
this.render();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async confirmMove() {
|
|
647
681
|
const editor = this.editor;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
if (
|
|
682
|
+
if (editor?.sourceRef === undefined || this.operation !== undefined || this.mode !== "move-confirm")
|
|
683
|
+
return;
|
|
684
|
+
const validation = this.validateEditor();
|
|
685
|
+
if (!validation.ok)
|
|
652
686
|
return;
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
this.
|
|
656
|
-
this.status = { kind: "info", message: "Saving workspace task..." };
|
|
687
|
+
const generation = this.selectionGeneration;
|
|
688
|
+
this.operation = { kind: "mutation", scopes: ["workspace", "global"] };
|
|
689
|
+
this.status = { kind: "info", message: "Moving workspace task..." };
|
|
657
690
|
this.render();
|
|
658
691
|
try {
|
|
659
|
-
|
|
660
|
-
if (!this.
|
|
692
|
+
await this.actionsValue.move(editor.sourceRef, validation.task);
|
|
693
|
+
if (!this.ownsSelection(generation))
|
|
661
694
|
return;
|
|
662
695
|
this.operation = undefined;
|
|
663
|
-
if (
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
this.validationErrors = undefined;
|
|
668
|
-
this.idManuallyEdited = false;
|
|
669
|
-
this.failure = undefined;
|
|
670
|
-
this.status = { kind: "success", message: `Saved task "${title}".` };
|
|
696
|
+
if (this.stateValue.move !== undefined || this.stateValue.moveError !== undefined || this.mutationBlocked("workspace") || this.mutationBlocked("global")) {
|
|
697
|
+
if (this.stateValue.moveError === undefined) {
|
|
698
|
+
this.status = this.stateValue.move === undefined ? { kind: "error", message: this.stateValue.mutationGate?.message ?? "Refresh before trying another task change." } : undefined;
|
|
699
|
+
}
|
|
671
700
|
this.render();
|
|
672
|
-
this.focusTarget(editor.focusReturn);
|
|
673
701
|
return;
|
|
674
702
|
}
|
|
675
|
-
this.
|
|
703
|
+
if (this.moveRecoveryObserved && !this.isMoveComplete(editor, this.stateValue)) {
|
|
704
|
+
this.status = { kind: "info", message: "Move was not completed. Confirm the move again after reviewing the catalogs." };
|
|
705
|
+
this.render();
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const target = editor.focusReturn;
|
|
709
|
+
this.editor = undefined;
|
|
710
|
+
this.mode = "view";
|
|
711
|
+
this.validationErrors = undefined;
|
|
712
|
+
this.moveRecoveryObserved = false;
|
|
713
|
+
this.status = { kind: "success", message: `Moved task "${validation.task.title}".` };
|
|
714
|
+
this.render();
|
|
715
|
+
this.focusTarget(target);
|
|
676
716
|
}
|
|
677
717
|
catch (error) {
|
|
678
|
-
if (!this.
|
|
718
|
+
if (!this.ownsSelection(generation))
|
|
679
719
|
return;
|
|
680
720
|
this.operation = undefined;
|
|
681
|
-
this.
|
|
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;
|
|
721
|
+
this.status = { kind: "error", message: "Could not move workspace task.", detail: formatError(error) };
|
|
722
|
+
this.render();
|
|
692
723
|
}
|
|
693
|
-
return replaceWorkspaceTaskAt(entry.state.config, editor.originalIndex, task);
|
|
694
724
|
}
|
|
695
|
-
async confirmDelete(
|
|
696
|
-
if (!this.isCurrentContext(context) || this.operation !== undefined || this.deleteState === undefined || this.mode !== "delete-confirm")
|
|
697
|
-
return;
|
|
698
|
-
const entry = getWorkspaceTasksCacheEntry(cacheKeyForContext(context));
|
|
725
|
+
async confirmDelete() {
|
|
699
726
|
const pending = this.deleteState;
|
|
700
|
-
if (
|
|
701
|
-
this.enterFailure("conflict", "The task list changed outside this panel. Refresh before trying again.", "delete");
|
|
727
|
+
if (pending === undefined || this.operation !== undefined || this.isScopeDisabled(pending.ref.scope))
|
|
702
728
|
return;
|
|
703
|
-
|
|
704
|
-
|
|
729
|
+
const generation = this.selectionGeneration;
|
|
730
|
+
const observation = this.captureActionState();
|
|
731
|
+
const pendingAction = {
|
|
732
|
+
kind: "delete",
|
|
733
|
+
generation,
|
|
734
|
+
scopes: [pending.ref.scope],
|
|
735
|
+
observation,
|
|
736
|
+
focusReturn: pending.focusReturn,
|
|
737
|
+
ref: { ...pending.ref },
|
|
738
|
+
task: cloneTask(pending.task),
|
|
739
|
+
settled: false,
|
|
740
|
+
};
|
|
741
|
+
this.pendingAction = pendingAction;
|
|
742
|
+
this.operation = { kind: "mutation", scopes: [pending.ref.scope] };
|
|
743
|
+
this.status = { kind: "info", message: "Deleting workspace task..." };
|
|
744
|
+
this.render();
|
|
705
745
|
try {
|
|
706
|
-
|
|
746
|
+
await this.actionsValue.remove(pending.ref);
|
|
747
|
+
if (!this.ownsSelection(generation) || this.pendingAction !== pendingAction)
|
|
748
|
+
return;
|
|
749
|
+
pendingAction.settled = true;
|
|
750
|
+
this.reconcilePendingAction();
|
|
707
751
|
}
|
|
708
752
|
catch (error) {
|
|
709
|
-
this.
|
|
710
|
-
|
|
753
|
+
if (!this.ownsSelection(generation) || this.pendingAction !== pendingAction)
|
|
754
|
+
return;
|
|
755
|
+
this.pendingAction = undefined;
|
|
756
|
+
this.operation = undefined;
|
|
757
|
+
this.status = { kind: "error", message: "Could not delete workspace task.", detail: formatError(error) };
|
|
758
|
+
this.render();
|
|
711
759
|
}
|
|
712
|
-
await this.performMutation(context, pending.sourceSnapshot, nextConfig, "delete", pending.task.title, pending.focusReturn);
|
|
713
760
|
}
|
|
714
|
-
async
|
|
715
|
-
if (
|
|
761
|
+
async retryMove() {
|
|
762
|
+
if (this.operation !== undefined || this.stateValue.move?.retryAllowed !== true)
|
|
716
763
|
return;
|
|
717
|
-
const
|
|
718
|
-
|
|
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..." };
|
|
764
|
+
const generation = this.selectionGeneration;
|
|
765
|
+
this.operation = { kind: "mutation", scopes: ["workspace", "global"] };
|
|
766
|
+
this.status = { kind: "info", message: "Retrying workspace task move..." };
|
|
725
767
|
this.render();
|
|
726
768
|
try {
|
|
727
|
-
|
|
728
|
-
if (!this.
|
|
769
|
+
await this.actionsValue.retryMove();
|
|
770
|
+
if (!this.ownsSelection(generation))
|
|
729
771
|
return;
|
|
730
772
|
this.operation = undefined;
|
|
731
|
-
|
|
732
|
-
|
|
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");
|
|
773
|
+
this.status = undefined;
|
|
774
|
+
this.render();
|
|
749
775
|
}
|
|
750
776
|
catch (error) {
|
|
751
|
-
if (!this.
|
|
777
|
+
if (!this.ownsSelection(generation))
|
|
752
778
|
return;
|
|
753
779
|
this.operation = undefined;
|
|
754
|
-
this.
|
|
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();
|
|
780
|
+
this.status = { kind: "error", message: "Could not retry workspace task move.", detail: formatError(error) };
|
|
781
|
+
this.render();
|
|
779
782
|
}
|
|
780
|
-
return this.dispatchTask(context, task);
|
|
781
783
|
}
|
|
782
|
-
async dispatchTask(
|
|
783
|
-
if (this.
|
|
784
|
+
async dispatchTask(key) {
|
|
785
|
+
if (this.contextValue === undefined || key === null || this.runningTaskKey !== undefined)
|
|
786
|
+
return;
|
|
787
|
+
const ref = parseRef(key);
|
|
788
|
+
if (ref === undefined)
|
|
789
|
+
return;
|
|
790
|
+
const found = this.findTask(ref);
|
|
791
|
+
if (found === undefined) {
|
|
792
|
+
this.setError("That task is no longer available. Refresh, then try again.");
|
|
784
793
|
return;
|
|
785
|
-
|
|
786
|
-
|
|
794
|
+
}
|
|
795
|
+
if (found.task.confirm && !window.confirm(`Run ${found.task.title} (${scopeLabel(ref.scope)})?\n\n${found.task.command}`)) {
|
|
796
|
+
this.status = { kind: "info", message: `Cancelled ${found.task.title}.` };
|
|
787
797
|
this.render();
|
|
788
798
|
return;
|
|
789
799
|
}
|
|
790
|
-
const
|
|
791
|
-
const
|
|
792
|
-
this.
|
|
793
|
-
this.status = { kind: "info", message: `Starting ${task.title}...` };
|
|
800
|
+
const selection = this.selectionGeneration;
|
|
801
|
+
const terminal = ++this.terminalGeneration;
|
|
802
|
+
this.runningTaskKey = workspaceTaskRefKey(ref);
|
|
803
|
+
this.status = { kind: "info", message: `Starting ${found.task.title}...` };
|
|
794
804
|
this.render();
|
|
795
805
|
try {
|
|
796
|
-
const handle = await runWorkspaceTaskInTerminal(
|
|
797
|
-
if (!this.
|
|
806
|
+
const handle = await runWorkspaceTaskInTerminal(this.contextValue.terminal, ref, found.task);
|
|
807
|
+
if (!this.ownsTerminal(selection, terminal))
|
|
798
808
|
return;
|
|
799
|
-
this.
|
|
800
|
-
this.
|
|
809
|
+
this.runningTaskKey = undefined;
|
|
810
|
+
this.status = { kind: "success", message: `Started terminal command "${handle.run.title}".`, detail: found.task.command };
|
|
811
|
+
this.render();
|
|
801
812
|
}
|
|
802
813
|
catch (error) {
|
|
803
|
-
if (!this.
|
|
814
|
+
if (!this.ownsTerminal(selection, terminal))
|
|
804
815
|
return;
|
|
805
|
-
this.
|
|
806
|
-
this.
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
renderTerminalStatus(status) {
|
|
810
|
-
if (this.operation !== undefined) {
|
|
816
|
+
this.runningTaskKey = undefined;
|
|
817
|
+
this.status = { kind: "error", message: formatError(error) };
|
|
811
818
|
this.render();
|
|
812
|
-
return;
|
|
813
819
|
}
|
|
814
|
-
this.status = status;
|
|
815
|
-
this.render();
|
|
816
820
|
}
|
|
817
|
-
openWorkspaceTerminal(
|
|
818
|
-
|
|
819
|
-
if (context === undefined) {
|
|
821
|
+
openWorkspaceTerminal() {
|
|
822
|
+
if (this.contextValue === undefined) {
|
|
820
823
|
this.setError("Select a workspace before opening a terminal.");
|
|
821
824
|
return;
|
|
822
825
|
}
|
|
823
|
-
|
|
824
|
-
context.terminal.open();
|
|
825
|
-
else
|
|
826
|
-
context.terminal.open({ terminalId });
|
|
826
|
+
this.contextValue.terminal.open();
|
|
827
827
|
}
|
|
828
|
-
validateEditor(
|
|
828
|
+
validateEditor() {
|
|
829
829
|
const editor = this.editor;
|
|
830
830
|
if (editor === undefined)
|
|
831
831
|
return { ok: false, errors: { title: "Title is required." } };
|
|
832
|
-
const
|
|
833
|
-
|
|
832
|
+
const scope = editor.draft.global ? "global" : "workspace";
|
|
833
|
+
const catalog = this.catalog(scope);
|
|
834
|
+
const existing = catalog.kind === "loaded" ? catalog.config.tasks : [];
|
|
835
|
+
const originalIndex = editor.sourceRef?.scope === scope ? editor.originalIndex : undefined;
|
|
836
|
+
return validateAndNormalizeDraft(editor.draft, existing, originalIndex);
|
|
834
837
|
}
|
|
835
|
-
updateValidationInPlace(
|
|
836
|
-
|
|
837
|
-
if (entry === undefined || this.editor === undefined)
|
|
838
|
+
updateValidationInPlace() {
|
|
839
|
+
if (this.editor === undefined)
|
|
838
840
|
return;
|
|
839
|
-
const
|
|
840
|
-
this.validationErrors =
|
|
841
|
+
const validation = this.validateEditor();
|
|
842
|
+
this.validationErrors = validation.ok ? undefined : validation.errors;
|
|
841
843
|
const errors = this.validationErrors ?? {};
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
const control = this.root.querySelector(`#${controlId}`);
|
|
845
|
-
const error = errors[field];
|
|
844
|
+
for (const [field, id] of [["title", "task-title"], ["command", "task-command"], ["id", "task-id"]]) {
|
|
845
|
+
const control = this.root.querySelector(`#${id}`);
|
|
846
846
|
const errorElement = this.root.querySelector(`[data-field-error="${field}"]`);
|
|
847
847
|
if (control === null || errorElement === null)
|
|
848
848
|
continue;
|
|
849
|
+
const error = errors[field];
|
|
849
850
|
if (error === undefined) {
|
|
850
851
|
control.removeAttribute("aria-invalid");
|
|
851
|
-
if (field
|
|
852
|
-
control.setAttribute("aria-describedby", "task-command-help");
|
|
853
|
-
else
|
|
852
|
+
if (field !== "command")
|
|
854
853
|
control.removeAttribute("aria-describedby");
|
|
855
854
|
errorElement.textContent = "";
|
|
856
855
|
errorElement.hidden = true;
|
|
857
856
|
}
|
|
858
857
|
else {
|
|
859
858
|
control.setAttribute("aria-invalid", "true");
|
|
860
|
-
control.setAttribute("aria-describedby", field === "command" ?
|
|
859
|
+
control.setAttribute("aria-describedby", field === "command" ? "task-command-help task-command-error" : `task-${field}-error`);
|
|
861
860
|
errorElement.textContent = error;
|
|
862
861
|
errorElement.hidden = false;
|
|
863
862
|
}
|
|
864
863
|
}
|
|
865
|
-
const save = this.root.querySelector("
|
|
864
|
+
const save = this.root.querySelector("[data-save-task]");
|
|
866
865
|
if (save !== null)
|
|
867
|
-
save.disabled = this.operation !== undefined || this.
|
|
866
|
+
save.disabled = this.operation !== undefined || !this.canWriteScope(this.editor.draft.global ? "global" : "workspace") || !validation.ok || this.isScopeDisabled(this.editor.draft.global ? "global" : "workspace");
|
|
868
867
|
}
|
|
869
868
|
isEditorDirty() {
|
|
870
869
|
const editor = this.editor;
|
|
@@ -875,11 +874,12 @@ class PiWebUiTasksPanel extends HTMLElement {
|
|
|
875
874
|
|| editor.draft.command !== editor.initialDraft.command
|
|
876
875
|
|| editor.draft.description !== editor.initialDraft.description
|
|
877
876
|
|| editor.draft.group !== editor.initialDraft.group
|
|
878
|
-
|| editor.draft.confirm !== editor.initialDraft.confirm
|
|
877
|
+
|| editor.draft.confirm !== editor.initialDraft.confirm
|
|
878
|
+
|| editor.draft.global !== editor.initialDraft.global;
|
|
879
879
|
}
|
|
880
880
|
focusFirstInvalid(errors) {
|
|
881
881
|
const field = errors.title === undefined ? errors.command === undefined ? "id" : "command" : "title";
|
|
882
|
-
this.focusSelector(`
|
|
882
|
+
this.focusSelector(`[data-editor-${field}]`);
|
|
883
883
|
}
|
|
884
884
|
focusSelector(selector) {
|
|
885
885
|
if (!this.connected)
|
|
@@ -889,117 +889,377 @@ class PiWebUiTasksPanel extends HTMLElement {
|
|
|
889
889
|
focusTarget(target) {
|
|
890
890
|
if (!this.connected)
|
|
891
891
|
return;
|
|
892
|
-
let element;
|
|
892
|
+
let element = null;
|
|
893
893
|
if (target?.kind === "add")
|
|
894
|
-
element = this.root.querySelector("
|
|
894
|
+
element = this.root.querySelector("[data-add-task]");
|
|
895
895
|
if (target?.kind === "edit")
|
|
896
|
-
element = findButtonByValue(this.root, "data-edit-task", target.
|
|
896
|
+
element = findButtonByValue(this.root, "data-edit-task", workspaceTaskRefKey(target.ref));
|
|
897
897
|
if (target?.kind === "delete")
|
|
898
|
-
element = findButtonByValue(this.root, "data-delete-task", target.
|
|
899
|
-
if (target?.kind === "reset")
|
|
900
|
-
element = this.root.querySelector("button[data-reset-tasks-file]") ?? undefined;
|
|
898
|
+
element = findButtonByValue(this.root, "data-delete-task", workspaceTaskRefKey(target.ref));
|
|
901
899
|
if (target?.kind === "refresh")
|
|
902
|
-
element = this.root.querySelector("
|
|
903
|
-
if (target?.kind === "heading" || element ===
|
|
904
|
-
element = this.root.querySelector("[data-panel-heading]")
|
|
900
|
+
element = this.root.querySelector("[data-refresh]");
|
|
901
|
+
if (target?.kind === "heading" || element === null || element instanceof HTMLButtonElement && element.disabled)
|
|
902
|
+
element = this.root.querySelector("[data-panel-heading]");
|
|
905
903
|
element?.focus();
|
|
906
904
|
}
|
|
907
|
-
|
|
908
|
-
|
|
905
|
+
handleKeyDown(event) {
|
|
906
|
+
if (event.key !== "Escape" || this.operation !== undefined)
|
|
907
|
+
return;
|
|
908
|
+
if (this.mode === "add" || this.mode === "edit") {
|
|
909
|
+
event.preventDefault();
|
|
910
|
+
event.stopPropagation();
|
|
911
|
+
this.cancelEditor();
|
|
912
|
+
}
|
|
913
|
+
else if (this.mode === "delete-confirm") {
|
|
914
|
+
event.preventDefault();
|
|
915
|
+
event.stopPropagation();
|
|
916
|
+
this.cancelDelete();
|
|
917
|
+
}
|
|
918
|
+
else if (this.mode === "move-confirm") {
|
|
919
|
+
event.preventDefault();
|
|
920
|
+
event.stopPropagation();
|
|
921
|
+
this.cancelMove();
|
|
922
|
+
}
|
|
923
|
+
else if (this.mode === "refresh-discard-confirm") {
|
|
924
|
+
event.preventDefault();
|
|
925
|
+
event.stopPropagation();
|
|
926
|
+
this.cancelRefreshDiscard();
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
findTask(ref) {
|
|
930
|
+
const catalog = this.catalog(ref.scope);
|
|
931
|
+
if (catalog.kind !== "loaded")
|
|
932
|
+
return undefined;
|
|
933
|
+
const index = catalog.config.tasks.findIndex((task) => task.id === ref.id);
|
|
934
|
+
const task = index < 0 ? undefined : catalog.config.tasks[index];
|
|
935
|
+
return task === undefined ? undefined : { task: cloneTask(task), index };
|
|
936
|
+
}
|
|
937
|
+
catalog(scope) {
|
|
938
|
+
return scope === "workspace" ? this.stateValue.workspace : this.stateValue.global;
|
|
939
|
+
}
|
|
940
|
+
captureActionState() {
|
|
941
|
+
return {
|
|
942
|
+
sources: {
|
|
943
|
+
workspace: {
|
|
944
|
+
lastKey: catalogStateKey(this.stateValue.workspace),
|
|
945
|
+
generation: sourceGeneration(this.stateValue, "workspace"),
|
|
946
|
+
delivered: false,
|
|
947
|
+
},
|
|
948
|
+
global: {
|
|
949
|
+
lastKey: catalogStateKey(this.stateValue.global),
|
|
950
|
+
generation: sourceGeneration(this.stateValue, "global"),
|
|
951
|
+
delivered: false,
|
|
952
|
+
},
|
|
953
|
+
},
|
|
954
|
+
mutationGateKey: mutationGateKey(this.stateValue.mutationGate),
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
recordPendingSourceObservations(next) {
|
|
958
|
+
const pending = this.pendingAction;
|
|
959
|
+
if (pending === undefined || !this.ownsSelection(pending.generation))
|
|
960
|
+
return;
|
|
961
|
+
for (const scope of ["workspace", "global"]) {
|
|
962
|
+
if (!pending.scopes.includes(scope))
|
|
963
|
+
continue;
|
|
964
|
+
const nextCatalog = scope === "workspace" ? next.workspace : next.global;
|
|
965
|
+
const source = pending.observation.sources[scope];
|
|
966
|
+
const nextKey = catalogStateKey(nextCatalog);
|
|
967
|
+
if (source.generation < sourceGeneration(next, scope)
|
|
968
|
+
|| source.lastKey !== nextKey && hasAuthoritativeCatalogState(nextCatalog)) {
|
|
969
|
+
source.delivered = true;
|
|
970
|
+
}
|
|
971
|
+
source.lastKey = nextKey;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
reconcilePendingAction() {
|
|
975
|
+
const pending = this.pendingAction;
|
|
976
|
+
if (pending === undefined || !pending.settled || !this.ownsSelection(pending.generation))
|
|
977
|
+
return false;
|
|
978
|
+
if (pending.kind === "refresh") {
|
|
979
|
+
if (!pending.scopes.every((scope) => this.sourceSnapshotDelivered(scope, pending.observation)))
|
|
980
|
+
return false;
|
|
981
|
+
if (pending.scopes.some((scope) => !hasAuthoritativeCatalogState(this.catalog(scope))))
|
|
982
|
+
return false;
|
|
983
|
+
for (const scope of pending.scopes) {
|
|
984
|
+
const failure = catalogFailure(this.catalog(scope));
|
|
985
|
+
if (failure !== undefined)
|
|
986
|
+
return this.finishPendingFailure(pending, failure.message);
|
|
987
|
+
}
|
|
988
|
+
this.pendingAction = undefined;
|
|
989
|
+
this.operation = undefined;
|
|
990
|
+
this.status = { kind: "success", message: "Workspace task catalogs refreshed." };
|
|
991
|
+
this.render();
|
|
992
|
+
this.focusTarget(pending.focusReturn);
|
|
993
|
+
return true;
|
|
994
|
+
}
|
|
995
|
+
const scope = pending.kind === "save" ? pending.scope : pending.ref.scope;
|
|
996
|
+
const delivered = this.sourceSnapshotDelivered(scope, pending.observation);
|
|
997
|
+
const gateChanged = this.mutationGateChanged(scope, pending.observation);
|
|
998
|
+
if (!delivered && !gateChanged)
|
|
999
|
+
return false;
|
|
1000
|
+
if (!hasAuthoritativeCatalogState(this.catalog(scope)) && !gateChanged)
|
|
1001
|
+
return false;
|
|
1002
|
+
const failure = catalogFailure(this.catalog(scope));
|
|
1003
|
+
if (failure !== undefined)
|
|
1004
|
+
return this.finishPendingFailure(pending, failure.message);
|
|
1005
|
+
if (gateChanged || this.mutationBlocked(scope)) {
|
|
1006
|
+
return this.finishPendingFailure(pending, this.stateValue.mutationGate?.message ?? "Refresh before trying another task change.");
|
|
1007
|
+
}
|
|
1008
|
+
const confirmed = pending.kind === "save"
|
|
1009
|
+
? this.catalogContainsTask(scope, pending.task)
|
|
1010
|
+
: this.catalogDoesNotContainTask(pending.ref);
|
|
1011
|
+
if (!confirmed)
|
|
1012
|
+
return false;
|
|
1013
|
+
this.pendingAction = undefined;
|
|
1014
|
+
this.operation = undefined;
|
|
1015
|
+
if (pending.kind === "save") {
|
|
1016
|
+
this.editor = undefined;
|
|
1017
|
+
this.mode = "view";
|
|
1018
|
+
this.validationErrors = undefined;
|
|
1019
|
+
this.idManuallyEdited = false;
|
|
1020
|
+
this.status = { kind: "success", message: `Saved task "${pending.task.title}".` };
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
this.deleteState = undefined;
|
|
1024
|
+
this.mode = "view";
|
|
1025
|
+
this.status = { kind: "success", message: `Deleted task "${pending.task.title}".` };
|
|
1026
|
+
}
|
|
1027
|
+
this.render();
|
|
1028
|
+
this.focusTarget(pending.focusReturn);
|
|
1029
|
+
return true;
|
|
1030
|
+
}
|
|
1031
|
+
sourceSnapshotDelivered(scope, observation) {
|
|
1032
|
+
return observation.sources[scope].delivered;
|
|
1033
|
+
}
|
|
1034
|
+
mutationGateChanged(scope, observation) {
|
|
1035
|
+
const gate = this.stateValue.mutationGate;
|
|
1036
|
+
return mutationGateKey(gate) !== observation.mutationGateKey && gate?.scopes.includes(scope) === true;
|
|
1037
|
+
}
|
|
1038
|
+
finishPendingFailure(pending, detail) {
|
|
1039
|
+
if (this.pendingAction !== pending)
|
|
1040
|
+
return false;
|
|
1041
|
+
this.pendingAction = undefined;
|
|
1042
|
+
this.operation = undefined;
|
|
1043
|
+
const message = pending.kind === "refresh"
|
|
1044
|
+
? "Could not refresh workspace task catalogs."
|
|
1045
|
+
: pending.kind === "save"
|
|
1046
|
+
? "Could not save workspace task."
|
|
1047
|
+
: "Could not delete workspace task.";
|
|
1048
|
+
this.status = { kind: "error", message, detail };
|
|
1049
|
+
this.render();
|
|
1050
|
+
if (pending.kind === "refresh")
|
|
1051
|
+
this.focusTarget(pending.focusReturn);
|
|
1052
|
+
return true;
|
|
909
1053
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
&& this.terminalRunGeneration === terminalRunGeneration;
|
|
1054
|
+
catalogContainsTask(scope, expected) {
|
|
1055
|
+
const catalog = this.catalog(scope);
|
|
1056
|
+
return catalog.kind === "loaded" && catalog.config.tasks.some((task) => sameTask(task, expected));
|
|
914
1057
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1058
|
+
catalogDoesNotContainTask(ref) {
|
|
1059
|
+
const catalog = this.catalog(ref.scope);
|
|
1060
|
+
return catalog.kind === "loaded" && !catalog.config.tasks.some((task) => task.id === ref.id);
|
|
1061
|
+
}
|
|
1062
|
+
canWriteScope(scope) {
|
|
1063
|
+
const catalog = this.catalog(scope);
|
|
1064
|
+
return catalog.kind === "loaded" || scope === "workspace" && catalog.kind === "missing";
|
|
1065
|
+
}
|
|
1066
|
+
isScopeDisabled(scope) {
|
|
1067
|
+
return this.operation?.scopes.includes(scope) === true
|
|
1068
|
+
|| this.stateValue.mutationGate?.scopes.includes(scope) === true
|
|
1069
|
+
|| this.stateValue.move !== undefined;
|
|
1070
|
+
}
|
|
1071
|
+
mutationBlocked(scope) {
|
|
1072
|
+
return this.stateValue.mutationGate?.scopes.includes(scope) === true || this.stateValue.move !== undefined;
|
|
1073
|
+
}
|
|
1074
|
+
ownsSelection(generation) {
|
|
1075
|
+
return this.connected && generation === this.selectionGeneration;
|
|
1076
|
+
}
|
|
1077
|
+
ownsTerminal(selection, terminal) {
|
|
1078
|
+
return this.ownsSelection(selection) && terminal === this.terminalGeneration;
|
|
919
1079
|
}
|
|
920
1080
|
setError(message) {
|
|
921
1081
|
this.status = { kind: "error", message };
|
|
922
1082
|
this.render();
|
|
923
1083
|
}
|
|
1084
|
+
rememberOpenGroups() {
|
|
1085
|
+
for (const details of this.root.querySelectorAll("details[data-group-key]")) {
|
|
1086
|
+
const key = details.getAttribute("data-group-key");
|
|
1087
|
+
if (key === null)
|
|
1088
|
+
continue;
|
|
1089
|
+
if (details.open)
|
|
1090
|
+
this.expandedGroupKeys.add(key);
|
|
1091
|
+
else
|
|
1092
|
+
this.expandedGroupKeys.delete(key);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
pruneExpandedGroups() {
|
|
1096
|
+
const valid = new Set();
|
|
1097
|
+
for (const scope of ["global", "workspace"]) {
|
|
1098
|
+
const catalog = this.catalog(scope);
|
|
1099
|
+
if (catalog.kind !== "loaded")
|
|
1100
|
+
continue;
|
|
1101
|
+
for (const task of catalog.config.tasks) {
|
|
1102
|
+
if (task.group !== undefined)
|
|
1103
|
+
valid.add(workspaceTaskGroupKey(scope, task.group));
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
for (const key of this.expandedGroupKeys)
|
|
1107
|
+
if (!valid.has(key))
|
|
1108
|
+
this.expandedGroupKeys.delete(key);
|
|
1109
|
+
}
|
|
1110
|
+
isMoveComplete(editor, state) {
|
|
1111
|
+
const source = editor.sourceRef;
|
|
1112
|
+
if (source === undefined)
|
|
1113
|
+
return false;
|
|
1114
|
+
const destination = { scope: editor.draft.global ? "global" : "workspace", id: editor.draft.id.trim() };
|
|
1115
|
+
return !this.catalogHasRef(state, source) && this.catalogHasRef(state, destination);
|
|
1116
|
+
}
|
|
1117
|
+
catalogHasRef(state, ref) {
|
|
1118
|
+
const catalog = ref.scope === "workspace" ? state.workspace : state.global;
|
|
1119
|
+
return catalog.kind === "loaded" && catalog.config.tasks.some((task) => task.id === ref.id);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function renderTaskGroups(scope, tasks, runningTaskKey, scopeDisabled, expanded) {
|
|
1123
|
+
const groups = new Map();
|
|
1124
|
+
for (const sourceTask of tasks) {
|
|
1125
|
+
const task = cloneTask(sourceTask);
|
|
1126
|
+
const group = groups.get(task.group);
|
|
1127
|
+
if (group === undefined)
|
|
1128
|
+
groups.set(task.group, [task]);
|
|
1129
|
+
else
|
|
1130
|
+
group.push(task);
|
|
1131
|
+
}
|
|
1132
|
+
return `<div class="tasks">${[...groups.entries()].map(([group, groupedTasks]) => {
|
|
1133
|
+
if (group === undefined)
|
|
1134
|
+
return `<section class="task-group ungrouped">${groupedTasks.map((task) => renderTask(scope, task, runningTaskKey, scopeDisabled, true)).join("")}</section>`;
|
|
1135
|
+
const key = workspaceTaskGroupKey(scope, group);
|
|
1136
|
+
const open = expanded.has(key);
|
|
1137
|
+
return `<details class="task-group" data-group-key="${escapeAttr(key)}" data-group-scope="${scope}"${open ? " open" : ""}>
|
|
1138
|
+
<summary><span class="disclosure-chevron" aria-hidden="true">›</span><span class="group-title">${escapeHtml(group)}</span><span class="group-count">${String(groupedTasks.length)} ${groupedTasks.length === 1 ? "task" : "tasks"}</span></summary>
|
|
1139
|
+
<div class="task-group-body">${groupedTasks.map((task) => renderTask(scope, task, runningTaskKey, scopeDisabled, true)).join("")}</div>
|
|
1140
|
+
</details>`;
|
|
1141
|
+
}).join("")}</div>`;
|
|
1142
|
+
}
|
|
1143
|
+
function renderTask(scope, task, runningTaskKey, scopeDisabled, showScopeBadge) {
|
|
1144
|
+
const ref = { scope, id: task.id };
|
|
1145
|
+
const key = workspaceTaskRefKey(ref);
|
|
1146
|
+
const label = scopeLabel(scope);
|
|
1147
|
+
const running = runningTaskKey === key;
|
|
1148
|
+
const description = task.description === undefined ? "" : `<span class="task-description">${escapeHtml(task.description)}</span>`;
|
|
1149
|
+
return `<article class="task-card" data-task-ref="${escapeAttr(key)}">
|
|
1150
|
+
<div class="task-copy">
|
|
1151
|
+
<div class="task-title-line"><strong>${escapeHtml(task.title)}</strong>${showScopeBadge ? `<span class="scope-badge">${escapeHtml(label)}</span>` : ""}</div>
|
|
1152
|
+
${description}
|
|
1153
|
+
<pre class="task-script" data-task-script>${escapeHtml(task.command)}</pre>
|
|
1154
|
+
</div>
|
|
1155
|
+
<div class="task-actions">
|
|
1156
|
+
<button type="button" class="secondary" data-edit-task="${escapeAttr(key)}" aria-label="Edit ${escapeAttr(task.title)} (${escapeAttr(label)})" ${scopeDisabled ? "disabled" : ""}>Edit</button>
|
|
1157
|
+
<button type="button" class="secondary danger-secondary" data-delete-task="${escapeAttr(key)}" aria-label="Delete ${escapeAttr(task.title)} (${escapeAttr(label)})" ${scopeDisabled ? "disabled" : ""}>Delete</button>
|
|
1158
|
+
<button type="button" data-run-task="${escapeAttr(key)}" aria-label="Run ${escapeAttr(task.title)} (${escapeAttr(label)})" ${runningTaskKey !== undefined ? "disabled" : ""}>${running ? "Dispatching..." : "Run"}</button>
|
|
1159
|
+
</div>
|
|
1160
|
+
</article>`;
|
|
1161
|
+
}
|
|
1162
|
+
function fieldAttrs(field, error, locked) {
|
|
1163
|
+
return `${error === undefined ? "" : " aria-invalid=\"true\" aria-describedby=\"task-${field}-error\""}${locked ? " disabled" : ""}`;
|
|
924
1164
|
}
|
|
925
1165
|
function renderFieldError(field, error) {
|
|
926
1166
|
return `<p id="task-${field}-error" data-field-error="${field}" class="field-error"${error === undefined ? " hidden" : ""}>${error === undefined ? "" : escapeHtml(error)}</p>`;
|
|
927
1167
|
}
|
|
928
|
-
function
|
|
929
|
-
return
|
|
1168
|
+
function loadedCount(catalog) {
|
|
1169
|
+
return catalog.kind === "loaded" ? catalog.config.tasks.length : 0;
|
|
930
1170
|
}
|
|
931
|
-
function
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
groups.push(group);
|
|
938
|
-
}
|
|
939
|
-
group.tasks.push(task);
|
|
1171
|
+
function catalogNeedsAttention(catalog) {
|
|
1172
|
+
return catalog.kind === "invalid" || catalog.kind === "unavailable" || catalog.kind === "error" || catalog.kind === "loaded" && catalog.refreshError !== undefined || catalog.kind === "missing" && catalog.refreshError !== undefined;
|
|
1173
|
+
}
|
|
1174
|
+
function catalogFailure(catalog) {
|
|
1175
|
+
if ((catalog.kind === "loaded" || catalog.kind === "missing") && catalog.refreshError !== undefined) {
|
|
1176
|
+
return { message: catalog.refreshError };
|
|
940
1177
|
}
|
|
941
|
-
|
|
1178
|
+
if (catalog.kind === "invalid" || catalog.kind === "unavailable" || catalog.kind === "error") {
|
|
1179
|
+
return { message: catalog.message };
|
|
1180
|
+
}
|
|
1181
|
+
return undefined;
|
|
942
1182
|
}
|
|
943
|
-
function
|
|
944
|
-
const
|
|
945
|
-
return
|
|
1183
|
+
function sourceGeneration(state, scope) {
|
|
1184
|
+
const generation = state.sourceGenerations?.[scope];
|
|
1185
|
+
return typeof generation === "number" && Number.isFinite(generation) ? generation : 0;
|
|
946
1186
|
}
|
|
947
|
-
function
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1187
|
+
function catalogStateKey(catalog) {
|
|
1188
|
+
return JSON.stringify(catalog);
|
|
1189
|
+
}
|
|
1190
|
+
function mutationGateKey(gate) {
|
|
1191
|
+
return gate === undefined ? undefined : JSON.stringify({ scopes: [...gate.scopes], message: gate.message });
|
|
1192
|
+
}
|
|
1193
|
+
function hasAuthoritativeCatalogState(catalog) {
|
|
1194
|
+
if (catalog.kind === "loading")
|
|
1195
|
+
return false;
|
|
1196
|
+
if (catalog.kind === "loaded" || catalog.kind === "missing")
|
|
1197
|
+
return !catalog.refreshing;
|
|
1198
|
+
return true;
|
|
1199
|
+
}
|
|
1200
|
+
function sameTask(left, right) {
|
|
1201
|
+
return left.id === right.id
|
|
1202
|
+
&& left.title === right.title
|
|
1203
|
+
&& left.command === right.command
|
|
1204
|
+
&& left.description === right.description
|
|
1205
|
+
&& left.group === right.group
|
|
1206
|
+
&& left.confirm === right.confirm;
|
|
1207
|
+
}
|
|
1208
|
+
function scopeLabel(scope) {
|
|
1209
|
+
return scope === "global" ? "Global" : "Project";
|
|
1210
|
+
}
|
|
1211
|
+
function parseRef(value) {
|
|
1212
|
+
try {
|
|
1213
|
+
return parseWorkspaceTaskRefKey(value);
|
|
1214
|
+
}
|
|
1215
|
+
catch {
|
|
1216
|
+
return undefined;
|
|
1217
|
+
}
|
|
966
1218
|
}
|
|
967
1219
|
function findButtonByValue(root, attribute, value) {
|
|
968
1220
|
for (const button of root.querySelectorAll(`button[${attribute}]`)) {
|
|
969
1221
|
if (button.getAttribute(attribute) === value)
|
|
970
1222
|
return button;
|
|
971
1223
|
}
|
|
972
|
-
return
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
function contextKey(context) {
|
|
1227
|
+
if (context === undefined)
|
|
1228
|
+
return undefined;
|
|
1229
|
+
return JSON.stringify([context.machine.id, context.workspace.projectId, context.workspace.id, context.workspace.path]);
|
|
973
1230
|
}
|
|
974
1231
|
function emptyDraft() {
|
|
975
|
-
return { id: "", title: "", command: "", description: "", group: "", confirm: false };
|
|
1232
|
+
return { id: "", title: "", command: "", description: "", group: "", confirm: false, global: false };
|
|
976
1233
|
}
|
|
977
1234
|
function cloneDraft(draft) {
|
|
978
1235
|
return { ...draft };
|
|
979
1236
|
}
|
|
980
|
-
function
|
|
981
|
-
return
|
|
1237
|
+
function cloneTask(task) {
|
|
1238
|
+
return {
|
|
1239
|
+
id: task.id,
|
|
1240
|
+
title: task.title,
|
|
1241
|
+
command: task.command,
|
|
1242
|
+
...(task.description === undefined ? {} : { description: task.description }),
|
|
1243
|
+
...(task.group === undefined ? {} : { group: task.group }),
|
|
1244
|
+
confirm: task.confirm,
|
|
1245
|
+
};
|
|
982
1246
|
}
|
|
983
|
-
function
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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 };
|
|
1247
|
+
function emptyPanelState() {
|
|
1248
|
+
return {
|
|
1249
|
+
workspace: { kind: "loading" },
|
|
1250
|
+
global: { kind: "loading" },
|
|
1251
|
+
sourceGenerations: { workspace: 0, global: 0 },
|
|
1252
|
+
};
|
|
994
1253
|
}
|
|
995
|
-
function
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1254
|
+
function noOpActions() {
|
|
1255
|
+
return {
|
|
1256
|
+
create: () => Promise.resolve(),
|
|
1257
|
+
update: () => Promise.resolve(),
|
|
1258
|
+
remove: () => Promise.resolve(),
|
|
1259
|
+
move: () => Promise.resolve(),
|
|
1260
|
+
retryMove: () => Promise.resolve(),
|
|
1261
|
+
refresh: () => Promise.resolve(),
|
|
1262
|
+
};
|
|
1003
1263
|
}
|
|
1004
1264
|
function formatError(error) {
|
|
1005
1265
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1011,62 +1271,76 @@ function escapeAttr(value) {
|
|
|
1011
1271
|
return escapeHtml(value).replaceAll('"', """).replaceAll("'", "'");
|
|
1012
1272
|
}
|
|
1013
1273
|
function taskStyles() {
|
|
1014
|
-
return
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1274
|
+
return `<style>
|
|
1275
|
+
:host { display: block; min-width: 0; container-type: inline-size; }
|
|
1276
|
+
.tasks-panel { min-width: 0; container-type: inline-size; color: var(--pi-text); }
|
|
1277
|
+
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--pi-border-muted); }
|
|
1278
|
+
.toolbar h2 { margin: 0; font-size: 15px; }
|
|
1279
|
+
.toolbar-tasks { display: inline-flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
|
1280
|
+
.viewer { box-sizing: border-box; min-width: 0; min-height: 0; overflow: auto; padding: 12px; }
|
|
1281
|
+
.tasks-viewer { display: grid; align-content: start; gap: 12px; }
|
|
1282
|
+
.scope-filters { display: inline-flex; flex-wrap: wrap; gap: 6px; }
|
|
1283
|
+
.scope-filter { border-color: var(--pi-border); background: var(--pi-surface); color: var(--pi-text); }
|
|
1284
|
+
.scope-filter.selected { border-color: var(--pi-accent-border); background: var(--pi-accent); color: var(--pi-bg); }
|
|
1285
|
+
.count, .catalog-count, .group-count { color: var(--pi-muted); font-size: 12px; white-space: nowrap; }
|
|
1286
|
+
.scope-filter.selected .count { color: inherit; }
|
|
1287
|
+
.helper, .muted, .field-help { color: var(--pi-muted); }
|
|
1288
|
+
.helper { margin: 0; line-height: 1.45; }
|
|
1289
|
+
.catalog-list { display: grid; gap: 16px; min-width: 0; }
|
|
1290
|
+
.scope-catalog { display: grid; gap: 10px; min-width: 0; }
|
|
1291
|
+
.scope-catalog > h3 { margin: 0; color: var(--pi-text-secondary); font-size: 13px; }
|
|
1292
|
+
.tasks { display: grid; gap: 10px; min-width: 0; }
|
|
1293
|
+
.task-group { min-width: 0; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); }
|
|
1294
|
+
.task-group[open] { background: var(--pi-surface); }
|
|
1295
|
+
.task-group summary { display: flex; align-items: center; gap: 8px; padding: 10px 12px; cursor: pointer; list-style: none; }
|
|
1296
|
+
.task-group summary::-webkit-details-marker { display: none; }
|
|
1297
|
+
.task-group summary::marker { content: ""; }
|
|
1298
|
+
.disclosure-chevron { color: var(--pi-muted); font-size: 18px; line-height: 1; transition: transform .15s ease; }
|
|
1299
|
+
.task-group[open] .disclosure-chevron { transform: rotate(90deg); }
|
|
1300
|
+
.group-title { flex: 1 1 auto; min-width: 0; font-weight: 600; }
|
|
1301
|
+
.task-group-body { display: grid; gap: 10px; padding: 0 10px 10px; }
|
|
1302
|
+
.ungrouped { display: grid; gap: 10px; }
|
|
1303
|
+
.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; }
|
|
1304
|
+
.task-copy { display: grid; min-width: 0; gap: 5px; }
|
|
1305
|
+
.task-title-line { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
|
1306
|
+
.task-description { color: var(--pi-muted); }
|
|
1307
|
+
.scope-badge { display: inline-block; border: 1px solid var(--pi-border-muted); border-radius: 4px; padding: 2px 6px; color: var(--pi-text-secondary); font-size: 11px; white-space: nowrap; }
|
|
1308
|
+
.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; }
|
|
1309
|
+
.task-script { max-height: 12rem; overflow-y: auto; padding: 8px; }
|
|
1310
|
+
.diagnostic { max-height: 12rem; overflow: auto; padding: 8px; }
|
|
1311
|
+
.task-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
|
1312
|
+
.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; }
|
|
1313
|
+
.editor-heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
|
|
1314
|
+
.task-editor h3, .confirmation h3 { margin: 0 0 16px; font-size: 18px; }
|
|
1315
|
+
.confirmation p { margin: 0 0 10px; line-height: 1.5; }
|
|
1316
|
+
.confirmation .task-script { margin: 10px 0 16px; }
|
|
1317
|
+
.task-form { display: grid; gap: 6px; }
|
|
1318
|
+
.task-form > label, .checkbox-field > label { font-size: 14px; font-weight: 500; }
|
|
1319
|
+
.task-form .required { color: var(--pi-danger); }
|
|
1320
|
+
.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; }
|
|
1321
|
+
.task-form textarea { min-height: 9rem; max-height: 24rem; resize: vertical; line-height: 1.45; }
|
|
1322
|
+
.task-form input[type="text"]::placeholder, .task-form textarea::placeholder { color: var(--pi-muted); }
|
|
1323
|
+
.checkbox-field { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 8px; margin-top: 8px; }
|
|
1324
|
+
.checkbox-field input { width: 18px; height: 18px; }
|
|
1325
|
+
.field-help { margin: 0 0 8px; font-size: 12px; line-height: 1.4; }
|
|
1326
|
+
.field-error { margin: 0 0 8px; color: var(--pi-danger); font-size: 12px; }
|
|
1327
|
+
.editor-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 10px; }
|
|
1328
|
+
.empty-state { display: grid; gap: 6px; border: 1px dashed var(--pi-border-muted); border-radius: 8px; color: var(--pi-muted); padding: 12px; }
|
|
1329
|
+
.panel-status { margin: 12px 12px 0; }
|
|
1330
|
+
.status { border: 1px solid var(--pi-border); border-radius: 8px; padding: 10px; }
|
|
1331
|
+
.status.info { border-color: var(--pi-accent-border); background: var(--pi-bg-overlay-soft); }
|
|
1332
|
+
.status.success { border-color: var(--pi-success-border); background: var(--pi-success-surface); color: var(--pi-success); }
|
|
1333
|
+
.status.error { border-color: var(--pi-danger); color: var(--pi-danger); }
|
|
1334
|
+
.status.warning { border-color: var(--pi-warning-border); background: var(--pi-warning-surface); color: var(--pi-text); }
|
|
1335
|
+
.status p { margin: 6px 0; }
|
|
1336
|
+
.empty { padding: 16px; color: var(--pi-muted); }
|
|
1337
|
+
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; }
|
|
1338
|
+
button.secondary { border-color: var(--pi-border); background: var(--pi-surface); color: var(--pi-text); }
|
|
1339
|
+
button.danger, button.danger-secondary { border-color: var(--pi-danger); }
|
|
1340
|
+
button.danger { background: var(--pi-danger); color: var(--pi-bg); }
|
|
1341
|
+
button.danger-secondary { background: var(--pi-surface); color: var(--pi-danger); }
|
|
1342
|
+
button:disabled { cursor: wait; opacity: .65; }
|
|
1343
|
+
button:focus-visible, input:focus-visible, textarea:focus-visible, [tabindex="-1"]:focus-visible { outline: 2px solid var(--pi-accent-border); outline-offset: 2px; }
|
|
1344
|
+
@container (max-width: 600px) { .task-card { grid-template-columns: minmax(0, 1fr); } .task-actions { justify-content: flex-start; } }
|
|
1345
|
+
</style>`;
|
|
1072
1346
|
}
|