@ai-setting/roy-plugin-task-show 0.6.11 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -1
- package/dist/cli-tasks-adapter.d.ts +142 -0
- package/dist/cli-tasks-adapter.d.ts.map +1 -0
- package/dist/cli-tasks-adapter.js +379 -0
- package/dist/cli-tasks-adapter.js.map +1 -0
- package/dist/operations-cache.d.ts +44 -0
- package/dist/operations-cache.d.ts.map +1 -0
- package/dist/operations-cache.js +103 -0
- package/dist/operations-cache.js.map +1 -0
- package/dist/plugin.d.ts +12 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +58 -0
- package/dist/plugin.js.map +1 -1
- package/dist/server.d.ts +11 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +78 -0
- package/dist/server.js.map +1 -1
- package/dist/task-operations-html.d.ts +50 -0
- package/dist/task-operations-html.d.ts.map +1 -0
- package/dist/task-operations-html.js +164 -0
- package/dist/task-operations-html.js.map +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -1
- package/plugin.json +2 -2
- package/public/style.css +195 -1
- package/public/task-operations.js +263 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Client-side controller for the "Task lifecycle pipeline" panel.
|
|
3
|
+
*
|
|
4
|
+
* - Fetches /api/tasks/:id/operations on load.
|
|
5
|
+
* - Replaces the server-rendered placeholder with the real timeline.
|
|
6
|
+
* - Polls every `POLL_MS` while the task is not yet in a terminal state.
|
|
7
|
+
* Stops polling once we see status: completed/failed/cancelled.
|
|
8
|
+
* - Deduplicates nodes by `data-op-id` so incremental updates don't
|
|
9
|
+
* double-render existing operations.
|
|
10
|
+
* - On error, shows the retry button and stops polling until the user
|
|
11
|
+
* clicks it.
|
|
12
|
+
* - Never modifies the rest of the page (Mermaid section, etc.).
|
|
13
|
+
*
|
|
14
|
+
* Vanilla JS — no framework dependency. Runs in any modern browser.
|
|
15
|
+
*/
|
|
16
|
+
(function () {
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
/** @typedef {{id:number, sequence:number, milestoneType:string, title:string, description:string, timestamp:string, sessionShort:string}} OperationViewModel */
|
|
20
|
+
|
|
21
|
+
/** Status values for which polling stops. */
|
|
22
|
+
var TERMINAL_STATUSES = { completed: 1, failed: 1, cancelled: 1 };
|
|
23
|
+
var POLL_MS = 5000;
|
|
24
|
+
var MAX_BACKOFF_MS = 30000;
|
|
25
|
+
|
|
26
|
+
/** HTML escape — keep in sync with server-side `htmlEscape`. */
|
|
27
|
+
function esc(s) {
|
|
28
|
+
return String(s == null ? "" : s)
|
|
29
|
+
.replace(/&/g, "&")
|
|
30
|
+
.replace(/</g, "<")
|
|
31
|
+
.replace(/>/g, ">")
|
|
32
|
+
.replace(/"/g, """)
|
|
33
|
+
.replace(/'/g, "'");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fmtTs(ts) {
|
|
37
|
+
if (!ts) return "—";
|
|
38
|
+
var d = new Date(ts);
|
|
39
|
+
if (isNaN(d.getTime())) return ts;
|
|
40
|
+
return d.toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function statusLabel(t) {
|
|
44
|
+
switch (t) {
|
|
45
|
+
case "create": return "Created";
|
|
46
|
+
case "progress": return "In progress";
|
|
47
|
+
case "milestone": return "Milestone";
|
|
48
|
+
case "problem": return "Problem";
|
|
49
|
+
case "solution": return "Solution";
|
|
50
|
+
case "decision": return "Decision";
|
|
51
|
+
case "review": return "Review";
|
|
52
|
+
case "completed": return "Completed";
|
|
53
|
+
default: return "Unknown";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function statusClass(t) {
|
|
58
|
+
var known = ["create","progress","milestone","problem","solution","decision","review","completed"];
|
|
59
|
+
return "op-status-" + (known.indexOf(t) >= 0 ? t : "unknown");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Build the section HTML from operations. Pure function. */
|
|
63
|
+
function renderPipelineHtml(ops, stale, taskId) {
|
|
64
|
+
var nodes = (ops || []).map(function (op) {
|
|
65
|
+
var label = statusLabel(op.milestoneType);
|
|
66
|
+
var klass = statusClass(op.milestoneType);
|
|
67
|
+
var ts = fmtTs(op.timestamp);
|
|
68
|
+
var title = op.title || label;
|
|
69
|
+
var hasDesc = !!(op.description);
|
|
70
|
+
return (
|
|
71
|
+
'<li class="op-node ' + klass + '" data-op-id="' + esc(op.id) + '" data-sequence="' + esc(op.sequence) + '">' +
|
|
72
|
+
'<div class="op-bullet" aria-hidden="true"></div>' +
|
|
73
|
+
'<div class="op-body">' +
|
|
74
|
+
'<div class="op-row1">' +
|
|
75
|
+
'<span class="op-seq">#' + esc(op.sequence) + '</span>' +
|
|
76
|
+
'<span class="op-badge ' + klass + '">' + esc(label) + '</span>' +
|
|
77
|
+
'<span class="op-title">' + esc(title) + '</span>' +
|
|
78
|
+
'<span class="op-time" title="' + esc(op.timestamp) + '">' + esc(ts) + '</span>' +
|
|
79
|
+
'</div>' +
|
|
80
|
+
(hasDesc ? '<details class="op-details"><summary>Details</summary><div class="op-desc">' + esc(op.description) + '</div></details>' : '') +
|
|
81
|
+
'</div>' +
|
|
82
|
+
'</li>'
|
|
83
|
+
);
|
|
84
|
+
}).join("");
|
|
85
|
+
|
|
86
|
+
var staleBadge = stale ? ' <span class="pipeline-stale" data-pipeline-stale>stale</span>' : "";
|
|
87
|
+
var idAttr = taskId ? ' data-task-id="' + esc(taskId) + '"' : "";
|
|
88
|
+
if (!ops || ops.length === 0) {
|
|
89
|
+
return (
|
|
90
|
+
'<section class="panel panel-pipeline"' + idAttr + ' data-pipeline-root>' +
|
|
91
|
+
'<h2>Task lifecycle pipeline' + staleBadge + '</h2>' +
|
|
92
|
+
'<p class="empty">No operations recorded yet.</p>' +
|
|
93
|
+
'</section>'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return (
|
|
97
|
+
'<section class="panel panel-pipeline"' + idAttr + ' data-pipeline-root>' +
|
|
98
|
+
'<h2>Task lifecycle pipeline' + staleBadge + '</h2>' +
|
|
99
|
+
'<ol class="op-timeline" role="list">' + nodes + '</ol>' +
|
|
100
|
+
'</section>'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function renderErrorHtml(code, taskId) {
|
|
105
|
+
var idAttr = taskId ? ' data-task-id="' + esc(taskId) + '"' : "";
|
|
106
|
+
return (
|
|
107
|
+
'<section class="panel panel-pipeline"' + idAttr + ' data-pipeline-error="' + esc(code) + '" data-pipeline-root>' +
|
|
108
|
+
'<h2>Task lifecycle pipeline</h2>' +
|
|
109
|
+
'<div class="pipeline-error">' +
|
|
110
|
+
'<p>Could not load operations: <code>' + esc(code) + '</code></p>' +
|
|
111
|
+
'<button type="button" class="btn btn-retry" data-pipeline-retry>Retry</button>' +
|
|
112
|
+
'</div>' +
|
|
113
|
+
'</section>'
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Find the placeholder section by its data attribute. */
|
|
118
|
+
function findRoot() {
|
|
119
|
+
return document.querySelector('[data-pipeline-root]');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Replace the placeholder with new HTML, preserving the scroll position. */
|
|
123
|
+
function swapIn(newHtml) {
|
|
124
|
+
var root = findRoot();
|
|
125
|
+
if (!root) return;
|
|
126
|
+
var scrollY = window.scrollY;
|
|
127
|
+
var tmp = document.createElement("div");
|
|
128
|
+
tmp.innerHTML = newHtml;
|
|
129
|
+
var newEl = tmp.firstElementChild;
|
|
130
|
+
if (!newEl) return;
|
|
131
|
+
root.parentNode.replaceChild(newEl, root);
|
|
132
|
+
// Don't restore scrollY unless the user has actually scrolled
|
|
133
|
+
if (scrollY > 0) window.scrollTo(0, scrollY);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Idempotent node merge: keep existing nodes, append new ones. */
|
|
137
|
+
function mergeNodes(prevOps, nextOps) {
|
|
138
|
+
var seen = Object.create(null);
|
|
139
|
+
var merged = [];
|
|
140
|
+
for (var i = 0; i < prevOps.length; i++) {
|
|
141
|
+
seen[prevOps[i].id] = 1;
|
|
142
|
+
merged.push(prevOps[i]);
|
|
143
|
+
}
|
|
144
|
+
for (var j = 0; j < nextOps.length; j++) {
|
|
145
|
+
if (!seen[nextOps[j].id]) merged.push(nextOps[j]);
|
|
146
|
+
}
|
|
147
|
+
return merged;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Fetch + update once. Resolves to { ok: boolean, terminal: boolean, ops }. */
|
|
151
|
+
function fetchOnce(taskId) {
|
|
152
|
+
return fetch("/api/tasks/" + encodeURIComponent(taskId) + "/operations", {
|
|
153
|
+
headers: { Accept: "application/json" },
|
|
154
|
+
credentials: "same-origin",
|
|
155
|
+
}).then(function (resp) {
|
|
156
|
+
if (resp.status === 404) {
|
|
157
|
+
return { ok: false, code: "not_found", terminal: true, ops: null };
|
|
158
|
+
}
|
|
159
|
+
if (resp.status === 503) {
|
|
160
|
+
return { ok: false, code: "cli_failed", terminal: false, ops: null };
|
|
161
|
+
}
|
|
162
|
+
if (!resp.ok) {
|
|
163
|
+
return { ok: false, code: "http_" + resp.status, terminal: false, ops: null };
|
|
164
|
+
}
|
|
165
|
+
return resp.json().then(function (body) {
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
terminal: !!(body.task && TERMINAL_STATUSES[body.task.status]),
|
|
169
|
+
ops: body.operations || [],
|
|
170
|
+
stale: !!body.stale,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The controller — one per page. */
|
|
177
|
+
function PipelineController(rootEl) {
|
|
178
|
+
this.taskId = rootEl.getAttribute("data-task-id");
|
|
179
|
+
this.rootEl = rootEl;
|
|
180
|
+
this.lastOps = [];
|
|
181
|
+
this.timer = null;
|
|
182
|
+
this.backoff = POLL_MS;
|
|
183
|
+
this.stopped = false;
|
|
184
|
+
this.bindRetry();
|
|
185
|
+
this.update();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
PipelineController.prototype.bindRetry = function () {
|
|
189
|
+
var self = this;
|
|
190
|
+
document.addEventListener("click", function (e) {
|
|
191
|
+
var t = e.target;
|
|
192
|
+
if (t && t.matches && t.matches("[data-pipeline-retry]")) {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
self.backoff = POLL_MS;
|
|
195
|
+
self.update();
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
PipelineController.prototype.update = function () {
|
|
201
|
+
if (this.stopped) return;
|
|
202
|
+
var self = this;
|
|
203
|
+
return fetchOnce(this.taskId).then(function (r) {
|
|
204
|
+
if (r.ok) {
|
|
205
|
+
var merged = mergeNodes(self.lastOps, r.ops);
|
|
206
|
+
self.lastOps = merged;
|
|
207
|
+
swapIn(renderPipelineHtml(merged, r.stale, self.taskId));
|
|
208
|
+
if (r.terminal) {
|
|
209
|
+
self.stopped = true;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
self.scheduleNext();
|
|
213
|
+
} else {
|
|
214
|
+
swapIn(renderErrorHtml(r.code || "error", self.taskId));
|
|
215
|
+
if (r.terminal) {
|
|
216
|
+
self.stopped = true;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
self.scheduleNext(true);
|
|
220
|
+
}
|
|
221
|
+
}).catch(function (err) {
|
|
222
|
+
// Network failure — exponential-ish backoff, retry
|
|
223
|
+
swapIn(renderErrorHtml("network", self.taskId));
|
|
224
|
+
self.scheduleNext(true);
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
PipelineController.prototype.scheduleNext = function (failed) {
|
|
229
|
+
var self = this;
|
|
230
|
+
if (this.timer) clearTimeout(this.timer);
|
|
231
|
+
var delay = failed ? this.backoff : POLL_MS;
|
|
232
|
+
this.backoff = Math.min(MAX_BACKOFF_MS, this.backoff * (failed ? 1.5 : 1));
|
|
233
|
+
this.timer = setTimeout(function () {
|
|
234
|
+
self.update();
|
|
235
|
+
}, delay);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
PipelineController.prototype.destroy = function () {
|
|
239
|
+
this.stopped = true;
|
|
240
|
+
if (this.timer) clearTimeout(this.timer);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
function boot() {
|
|
244
|
+
var root = findRoot();
|
|
245
|
+
if (!root) return;
|
|
246
|
+
if (!root.hasAttribute("data-task-id")) {
|
|
247
|
+
// index page — nothing to do, unless the body carries a task-id
|
|
248
|
+
// (some embedded viewports re-use the placeholder inside a frame
|
|
249
|
+
// whose body holds the canonical task id).
|
|
250
|
+
var bodyId = document.body && document.body.dataset ? document.body.dataset.taskId : "";
|
|
251
|
+
if (!bodyId) return;
|
|
252
|
+
root.setAttribute("data-task-id", bodyId);
|
|
253
|
+
}
|
|
254
|
+
// Expose for tests
|
|
255
|
+
window.__pipelineController = new PipelineController(root);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (document.readyState === "loading") {
|
|
259
|
+
document.addEventListener("DOMContentLoaded", boot);
|
|
260
|
+
} else {
|
|
261
|
+
boot();
|
|
262
|
+
}
|
|
263
|
+
})();
|