@ai-setting/roy-plugin-task-show 0.6.10 → 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 +141 -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 +80 -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 +5 -7
- package/plugin.json +2 -2
- package/public/app.js +53 -6
- package/public/mermaid-renderer.js +321 -0
- package/public/style.css +240 -1
- package/public/task-operations.js +263 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/* ------------------------------------------------------------------------- */
|
|
2
|
+
/* roy-plugin-task-show — Mermaid renderer controller (v0.6.11) */
|
|
3
|
+
/* ------------------------------------------------------------------------- */
|
|
4
|
+
/**
|
|
5
|
+
* Self-contained controller that wraps Mermaid rendering for the per-task
|
|
6
|
+
* visualization page. Replaces the brittle `mermaid.run({nodes})` approach
|
|
7
|
+
* that caused the v0.6.10 regression where the SVG was replaced with raw
|
|
8
|
+
* `flowchart TD` source on every async update.
|
|
9
|
+
*
|
|
10
|
+
* Public API (browser global `window.MermaidRenderer` and ES module export):
|
|
11
|
+
*
|
|
12
|
+
* createMermaidController({
|
|
13
|
+
* getMermaid?: () => any, // optional override for tests
|
|
14
|
+
* idPrefix?: string, // default "mmr"
|
|
15
|
+
* onStateChange?: (container, state) => void,
|
|
16
|
+
* }) -> {
|
|
17
|
+
* mount(container, source): Promise<void> // initial render (idempotent)
|
|
18
|
+
* update(container, source): Promise<void> // re-render with new source
|
|
19
|
+
* showError(container, source, error): void // user-visible error fallback
|
|
20
|
+
* getState(container): "rendered" | "error" | "idle" | "loading"
|
|
21
|
+
* destroy(): void // cancel all pending renders
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* Why this exists
|
|
25
|
+
* ---------------
|
|
26
|
+
* `mermaid.run({nodes})` does NOT reliably re-process an already-rendered
|
|
27
|
+
* node. Once Mermaid has replaced the `.mermaid` container with SVG, calling
|
|
28
|
+
* `run()` on that node again can silently no-op (or worse, leave the
|
|
29
|
+
* container holding the raw source we just wrote with `innerHTML`). The fix
|
|
30
|
+
* is to (1) call `mermaid.render(id, source)` per update with a unique id,
|
|
31
|
+
* (2) swap the *outer* container's child with the freshly produced SVG,
|
|
32
|
+
* (3) catch async rejections so the user sees an error block instead of raw
|
|
33
|
+
* text, and (4) drop the result of any render that completes after a newer
|
|
34
|
+
* update has superseded it.
|
|
35
|
+
*
|
|
36
|
+
* The controller is intentionally framework-free and is loaded by
|
|
37
|
+
* `public/app.js` via a regular `<script>` tag. It also exports its factory
|
|
38
|
+
* for unit tests.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
(function (root, factory) {
|
|
42
|
+
const api = factory();
|
|
43
|
+
if (typeof module === "object" && module.exports) {
|
|
44
|
+
module.exports = api;
|
|
45
|
+
}
|
|
46
|
+
if (typeof root === "object" && root) {
|
|
47
|
+
root.MermaidRenderer = api;
|
|
48
|
+
}
|
|
49
|
+
// ESM export path — Bun / Rollup / esbuild pick this up.
|
|
50
|
+
if (typeof exports !== "undefined") {
|
|
51
|
+
exports.createMermaidController = api.createMermaidController;
|
|
52
|
+
}
|
|
53
|
+
})(typeof window !== "undefined" ? window : typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
54
|
+
"use strict";
|
|
55
|
+
|
|
56
|
+
const DEFAULT_ID_PREFIX = "mmr";
|
|
57
|
+
|
|
58
|
+
function escapeHtml(s) {
|
|
59
|
+
return String(s)
|
|
60
|
+
.replace(/&/g, "&")
|
|
61
|
+
.replace(/</g, "<")
|
|
62
|
+
.replace(/>/g, ">")
|
|
63
|
+
.replace(/"/g, """)
|
|
64
|
+
.replace(/'/g, "'");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getMermaidFromGlobal() {
|
|
68
|
+
if (typeof window !== "undefined" && window.mermaid) return window.mermaid;
|
|
69
|
+
if (typeof globalThis !== "undefined" && globalThis.mermaid) return globalThis.mermaid;
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function setState(container, state) {
|
|
74
|
+
if (!container) return;
|
|
75
|
+
if (container.__mermaidRendererState !== state) {
|
|
76
|
+
container.__mermaidRendererState = state;
|
|
77
|
+
container.setAttribute("data-mermaid-state", state);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getState(container) {
|
|
82
|
+
return (container && container.__mermaidRendererState) || "idle";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function clearChildren(container) {
|
|
86
|
+
// happy-dom and jsdom both support `replaceChildren()`; fall back to
|
|
87
|
+
// manual removal for older runtimes.
|
|
88
|
+
if (typeof container.replaceChildren === "function") {
|
|
89
|
+
container.replaceChildren();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
while (container.firstChild) container.removeChild(container.firstChild);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renderErrorBlock(container, source, message) {
|
|
96
|
+
clearChildren(container);
|
|
97
|
+
const wrap = container.ownerDocument.createElement("div");
|
|
98
|
+
wrap.className = "mermaid-error";
|
|
99
|
+
wrap.setAttribute("role", "alert");
|
|
100
|
+
wrap.innerHTML =
|
|
101
|
+
`<div class="mermaid-error-header">⚠️ Mermaid render failed</div>` +
|
|
102
|
+
`<div class="mermaid-error-msg">${escapeHtml(message || "Unknown error")}</div>` +
|
|
103
|
+
`<details class="mermaid-error-source">` +
|
|
104
|
+
`<summary>Show source</summary>` +
|
|
105
|
+
`<pre>${escapeHtml(source || "")}</pre>` +
|
|
106
|
+
`</details>`;
|
|
107
|
+
container.appendChild(wrap);
|
|
108
|
+
setState(container, "error");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderUnavailable(container, source) {
|
|
112
|
+
renderErrorBlock(
|
|
113
|
+
container,
|
|
114
|
+
source,
|
|
115
|
+
"Mermaid library not loaded (window.mermaid is undefined).",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build the controller.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} [opts]
|
|
123
|
+
* @param {() => any} [opts.getMermaid] - returns the mermaid library or undefined.
|
|
124
|
+
* @param {string} [opts.idPrefix] - prefix used to make render IDs unique.
|
|
125
|
+
* @param {(container, state) => void} [opts.onStateChange]
|
|
126
|
+
*/
|
|
127
|
+
function createMermaidController(opts) {
|
|
128
|
+
const cfg = opts || {};
|
|
129
|
+
const idPrefix = cfg.idPrefix || DEFAULT_ID_PREFIX;
|
|
130
|
+
const getMermaid = cfg.getMermaid || getMermaidFromGlobal;
|
|
131
|
+
const onStateChange = typeof cfg.onStateChange === "function" ? cfg.onStateChange : null;
|
|
132
|
+
|
|
133
|
+
// Track every container this controller has touched so destroy() can
|
|
134
|
+
// iterate and mark each as destroyed. We use a Set so containers can be
|
|
135
|
+
// garbage-collected after they leave the DOM (we also drop them on
|
|
136
|
+
// destroy()).
|
|
137
|
+
const containers = new Set();
|
|
138
|
+
let controllerDestroyed = false;
|
|
139
|
+
|
|
140
|
+
// Per-container state: latest requested source, in-flight render token,
|
|
141
|
+
// sequence counter for unique IDs. We keep this in a WeakMap so we don't
|
|
142
|
+
// leak references after the container is removed.
|
|
143
|
+
const inflight = new WeakMap(); // container -> { token: number, source: string, destroyed: boolean, lastRenderedSource: string }
|
|
144
|
+
const seq = new WeakMap(); // container -> number
|
|
145
|
+
|
|
146
|
+
function nextSeq(container) {
|
|
147
|
+
const cur = seq.get(container) || 0;
|
|
148
|
+
const next = cur + 1;
|
|
149
|
+
seq.set(container, next);
|
|
150
|
+
return next;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function beginRender(container) {
|
|
154
|
+
const cur = inflight.get(container) || { token: 0, source: "", destroyed: false, lastRenderedSource: null };
|
|
155
|
+
const token = cur.token + 1;
|
|
156
|
+
inflight.set(container, { token, source: cur.source, destroyed: cur.destroyed, lastRenderedSource: cur.lastRenderedSource });
|
|
157
|
+
return token;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getToken(container) {
|
|
161
|
+
const cur = inflight.get(container);
|
|
162
|
+
return cur ? cur.token : 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function markDestroyed(container) {
|
|
166
|
+
const cur = inflight.get(container);
|
|
167
|
+
if (cur) inflight.set(container, { token: cur.token, source: cur.source, destroyed: true, lastRenderedSource: cur.lastRenderedSource });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isDestroyed(container) {
|
|
171
|
+
const cur = inflight.get(container);
|
|
172
|
+
return !!(cur && cur.destroyed);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function rememberRenderedSource(container, source) {
|
|
176
|
+
const cur = inflight.get(container);
|
|
177
|
+
if (cur) inflight.set(container, { token: cur.token, source: cur.source, destroyed: cur.destroyed, lastRenderedSource: source });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getLastRenderedSource(container) {
|
|
181
|
+
const cur = inflight.get(container);
|
|
182
|
+
return cur ? cur.lastRenderedSource : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Render `source` into `container`. Returns a Promise that resolves when
|
|
187
|
+
* the container reflects the latest source (either an SVG or an error
|
|
188
|
+
* block). If a newer call supersedes this one before the mermaid.render
|
|
189
|
+
* Promise resolves, the result is dropped (no DOM mutation).
|
|
190
|
+
*/
|
|
191
|
+
async function renderInternal(container, source) {
|
|
192
|
+
// Idempotency: if the controller was destroyed or the container has
|
|
193
|
+
// already successfully rendered this exact source, skip work entirely.
|
|
194
|
+
if (controllerDestroyed) return;
|
|
195
|
+
if (isDestroyed(container)) return;
|
|
196
|
+
if (typeof source !== "string") source = String(source == null ? "" : source);
|
|
197
|
+
if (getLastRenderedSource(container) === source && getState(container) === "rendered") {
|
|
198
|
+
// Same source, already rendered — nothing to do.
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
containers.add(container);
|
|
202
|
+
|
|
203
|
+
const mermaid = getMermaid();
|
|
204
|
+
if (!mermaid || typeof mermaid.render !== "function") {
|
|
205
|
+
renderUnavailable(container, source);
|
|
206
|
+
if (onStateChange) onStateChange(container, "error");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const myToken = beginRender(container);
|
|
211
|
+
setState(container, "loading");
|
|
212
|
+
const seqNum = nextSeq(container);
|
|
213
|
+
const id = `${idPrefix}-${seqNum}`;
|
|
214
|
+
let result;
|
|
215
|
+
try {
|
|
216
|
+
result = await mermaid.render(id, source);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
// Only surface this error if the user hasn't superseded us.
|
|
219
|
+
if (controllerDestroyed || isDestroyed(container) || getToken(container) !== myToken) return;
|
|
220
|
+
renderErrorBlock(
|
|
221
|
+
container,
|
|
222
|
+
source,
|
|
223
|
+
(err && err.message) || String(err),
|
|
224
|
+
);
|
|
225
|
+
if (onStateChange) onStateChange(container, "error");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Drop stale results: a newer render has already happened, the
|
|
229
|
+
// container was destroyed, or the controller was torn down.
|
|
230
|
+
if (controllerDestroyed || isDestroyed(container) || getToken(container) !== myToken) return;
|
|
231
|
+
const svg = result && result.svg;
|
|
232
|
+
if (!svg) {
|
|
233
|
+
renderErrorBlock(container, source, "mermaid.render returned no svg");
|
|
234
|
+
if (onStateChange) onStateChange(container, "error");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// Insert the new SVG. We use a wrapper so happy-dom/jsdom see a single
|
|
238
|
+
// child swap and so subsequent state checks (querySelector("svg"))
|
|
239
|
+
// remain correct.
|
|
240
|
+
clearChildren(container);
|
|
241
|
+
const doc = container.ownerDocument;
|
|
242
|
+
const range = doc.createRange
|
|
243
|
+
? doc.createRange()
|
|
244
|
+
: null;
|
|
245
|
+
let frag;
|
|
246
|
+
try {
|
|
247
|
+
if (range) {
|
|
248
|
+
range.selectNodeContents(doc.createElement("div"));
|
|
249
|
+
frag = range.createContextualFragment(svg);
|
|
250
|
+
} else {
|
|
251
|
+
// Last resort: parse as HTML via DOMParser.
|
|
252
|
+
const parser = new (doc.defaultView.DOMParser || (doc.defaultView && doc.defaultView.DOMParser))(
|
|
253
|
+
"text/html",
|
|
254
|
+
);
|
|
255
|
+
const doc2 = parser.parseFromString(`<body>${svg}</body>`, "text/html");
|
|
256
|
+
frag = doc.createDocumentFragment
|
|
257
|
+
? doc.createDocumentFragment()
|
|
258
|
+
: null;
|
|
259
|
+
if (frag && doc2 && doc2.body) {
|
|
260
|
+
while (doc2.body.firstChild) frag.appendChild(doc2.body.firstChild);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch (_) {
|
|
264
|
+
frag = null;
|
|
265
|
+
}
|
|
266
|
+
if (frag && frag.childNodes && frag.childNodes.length > 0) {
|
|
267
|
+
// Defensive: cap to 4 nodes to avoid pathological growth.
|
|
268
|
+
const nodes = Array.from(frag.childNodes).slice(0, 4);
|
|
269
|
+
for (const n of nodes) container.appendChild(n);
|
|
270
|
+
} else {
|
|
271
|
+
// Fallback: write innerHTML directly (acceptable in real browsers).
|
|
272
|
+
container.innerHTML = svg;
|
|
273
|
+
}
|
|
274
|
+
setState(container, "rendered");
|
|
275
|
+
rememberRenderedSource(container, source);
|
|
276
|
+
if (onStateChange) onStateChange(container, "rendered");
|
|
277
|
+
// Bind functions if mermaid provided any (e.g., for click handlers).
|
|
278
|
+
if (typeof result.bindFunctions === "function") {
|
|
279
|
+
try {
|
|
280
|
+
result.bindFunctions(container);
|
|
281
|
+
} catch (_) {
|
|
282
|
+
// ignore — bind failures are non-fatal
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
mount(container, source) {
|
|
289
|
+
// Idempotent: calling mount twice on the same container should not
|
|
290
|
+
// duplicate SVG. We treat mount and update identically from the
|
|
291
|
+
// rendering pipeline's perspective; the contract difference is only
|
|
292
|
+
// intent (mount for first paint, update for re-renders).
|
|
293
|
+
return renderInternal(container, source);
|
|
294
|
+
},
|
|
295
|
+
update(container, source) {
|
|
296
|
+
return renderInternal(container, source);
|
|
297
|
+
},
|
|
298
|
+
showError(container, source, error) {
|
|
299
|
+
renderErrorBlock(container, source, error && error.message ? error.message : String(error));
|
|
300
|
+
if (onStateChange) onStateChange(container, "error");
|
|
301
|
+
},
|
|
302
|
+
getState,
|
|
303
|
+
destroy() {
|
|
304
|
+
controllerDestroyed = true;
|
|
305
|
+
// Mark every known container as destroyed so any in-flight render
|
|
306
|
+
// for them is dropped. We can't iterate WeakMaps, but we *can*
|
|
307
|
+
// iterate the Set we maintain.
|
|
308
|
+
const list = Array.from(containers);
|
|
309
|
+
for (const c of list) markDestroyed(c);
|
|
310
|
+
containers.clear();
|
|
311
|
+
},
|
|
312
|
+
/** Destroy state for a single container. */
|
|
313
|
+
destroyContainer(container) {
|
|
314
|
+
markDestroyed(container);
|
|
315
|
+
containers.delete(container);
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { createMermaidController };
|
|
321
|
+
});
|
package/public/style.css
CHANGED
|
@@ -252,6 +252,51 @@ table.toolcalls .result {
|
|
|
252
252
|
overflow-x: auto;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/* v0.6.11: error fallback when Mermaid rendering fails (or Mermaid library
|
|
256
|
+
is unavailable). The error block replaces the bare raw `flowchart TD`
|
|
257
|
+
source so the user always sees a clear, actionable state. */
|
|
258
|
+
.mermaid-error {
|
|
259
|
+
background: #fef2f2;
|
|
260
|
+
border: 1px solid #fca5a5;
|
|
261
|
+
border-radius: 6px;
|
|
262
|
+
padding: 12px 14px;
|
|
263
|
+
color: #7f1d1d;
|
|
264
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.mermaid-error-header {
|
|
268
|
+
font-weight: 600;
|
|
269
|
+
margin-bottom: 6px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.mermaid-error-msg {
|
|
273
|
+
font-size: 13px;
|
|
274
|
+
margin-bottom: 8px;
|
|
275
|
+
word-break: break-word;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.mermaid-error-source {
|
|
279
|
+
margin-top: 6px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.mermaid-error-source summary {
|
|
283
|
+
cursor: pointer;
|
|
284
|
+
font-size: 12px;
|
|
285
|
+
color: #991b1b;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.mermaid-error-source pre {
|
|
289
|
+
background: #fff;
|
|
290
|
+
border: 1px solid #fca5a5;
|
|
291
|
+
border-radius: 4px;
|
|
292
|
+
padding: 8px;
|
|
293
|
+
font-size: 11px;
|
|
294
|
+
color: #1f2937;
|
|
295
|
+
max-height: 240px;
|
|
296
|
+
overflow: auto;
|
|
297
|
+
white-space: pre-wrap;
|
|
298
|
+
}
|
|
299
|
+
|
|
255
300
|
.hint {
|
|
256
301
|
margin-top: 8px;
|
|
257
302
|
font-size: 12px;
|
|
@@ -285,4 +330,198 @@ details summary {
|
|
|
285
330
|
padding: 14px 14px;
|
|
286
331
|
margin: 14px 10px;
|
|
287
332
|
}
|
|
288
|
-
}
|
|
333
|
+
}
|
|
334
|
+
/* ------------------------------------------------------------------------- */
|
|
335
|
+
/* Task lifecycle pipeline (v0.7.0+) */
|
|
336
|
+
/* ------------------------------------------------------------------------- */
|
|
337
|
+
|
|
338
|
+
.panel-pipeline {
|
|
339
|
+
/* keeps the existing panel rhythm */
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.op-timeline {
|
|
343
|
+
list-style: none;
|
|
344
|
+
margin: 0;
|
|
345
|
+
padding: 0;
|
|
346
|
+
position: relative;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.op-timeline::before {
|
|
350
|
+
content: "";
|
|
351
|
+
position: absolute;
|
|
352
|
+
left: 11px;
|
|
353
|
+
top: 6px;
|
|
354
|
+
bottom: 6px;
|
|
355
|
+
width: 2px;
|
|
356
|
+
background: var(--border);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.op-node {
|
|
360
|
+
position: relative;
|
|
361
|
+
padding: 6px 0 14px 36px;
|
|
362
|
+
list-style: none;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.op-bullet {
|
|
366
|
+
position: absolute;
|
|
367
|
+
left: 4px;
|
|
368
|
+
top: 12px;
|
|
369
|
+
width: 16px;
|
|
370
|
+
height: 16px;
|
|
371
|
+
border-radius: 50%;
|
|
372
|
+
background: var(--panel-2);
|
|
373
|
+
border: 2px solid var(--border);
|
|
374
|
+
box-shadow: 0 0 0 3px var(--bg);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.op-body {
|
|
378
|
+
display: flex;
|
|
379
|
+
flex-direction: column;
|
|
380
|
+
gap: 6px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.op-row1 {
|
|
384
|
+
display: flex;
|
|
385
|
+
align-items: center;
|
|
386
|
+
gap: 10px;
|
|
387
|
+
flex-wrap: wrap;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.op-seq {
|
|
391
|
+
color: var(--muted);
|
|
392
|
+
font-variant-numeric: tabular-nums;
|
|
393
|
+
min-width: 2.5em;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.op-time {
|
|
397
|
+
margin-left: auto;
|
|
398
|
+
color: var(--muted);
|
|
399
|
+
font-size: 12px;
|
|
400
|
+
font-variant-numeric: tabular-nums;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.op-title {
|
|
404
|
+
font-weight: 600;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.op-badge {
|
|
408
|
+
display: inline-block;
|
|
409
|
+
font-size: 11px;
|
|
410
|
+
padding: 2px 8px;
|
|
411
|
+
border-radius: 10px;
|
|
412
|
+
border: 1px solid var(--border);
|
|
413
|
+
background: var(--panel-2);
|
|
414
|
+
color: var(--text);
|
|
415
|
+
text-transform: uppercase;
|
|
416
|
+
letter-spacing: 0.04em;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.op-details {
|
|
420
|
+
background: var(--panel-2);
|
|
421
|
+
border: 1px solid var(--border);
|
|
422
|
+
border-radius: 6px;
|
|
423
|
+
padding: 8px 10px;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.op-details summary {
|
|
427
|
+
cursor: pointer;
|
|
428
|
+
color: var(--muted);
|
|
429
|
+
font-size: 12px;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.op-desc {
|
|
433
|
+
white-space: pre-wrap;
|
|
434
|
+
word-break: break-word;
|
|
435
|
+
font-size: 13px;
|
|
436
|
+
line-height: 1.5;
|
|
437
|
+
margin-top: 6px;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.op-proc {
|
|
441
|
+
margin-top: 6px;
|
|
442
|
+
font-size: 12px;
|
|
443
|
+
color: var(--muted);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.op-proc-label {
|
|
447
|
+
display: inline-block;
|
|
448
|
+
background: var(--panel);
|
|
449
|
+
border: 1px solid var(--border);
|
|
450
|
+
padding: 0 6px;
|
|
451
|
+
border-radius: 4px;
|
|
452
|
+
margin-right: 4px;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* Status colors per canonical milestone type */
|
|
456
|
+
.op-status-create .op-bullet { background: #1e40af; border-color: #3b82f6; }
|
|
457
|
+
.op-status-create .op-badge { background: #1e3a8a; color: #dbeafe; border-color: #3b82f6; }
|
|
458
|
+
|
|
459
|
+
.op-status-progress .op-bullet { background: #0e7490; border-color: #06b6d4; }
|
|
460
|
+
.op-status-progress .op-badge { background: #164e63; color: #cffafe; border-color: #06b6d4; }
|
|
461
|
+
|
|
462
|
+
.op-status-milestone .op-bullet { background: #15803d; border-color: #22c55e; }
|
|
463
|
+
.op-status-milestone .op-badge { background: #14532d; color: #dcfce7; border-color: #22c55e; }
|
|
464
|
+
|
|
465
|
+
.op-status-problem .op-bullet { background: #b91c1c; border-color: #ef4444; }
|
|
466
|
+
.op-status-problem .op-badge { background: #7f1d1d; color: #fee2e2; border-color: #ef4444; }
|
|
467
|
+
|
|
468
|
+
.op-status-solution .op-bullet { background: #a16207; border-color: #f59e0b; }
|
|
469
|
+
.op-status-solution .op-badge { background: #78350f; color: #fef3c7; border-color: #f59e0b; }
|
|
470
|
+
|
|
471
|
+
.op-status-decision .op-bullet { background: #6d28d9; border-color: #8b5cf6; }
|
|
472
|
+
.op-status-decision .op-badge { background: #4c1d95; color: #ede9fe; border-color: #8b5cf6; }
|
|
473
|
+
|
|
474
|
+
.op-status-review .op-bullet { background: #0f766e; border-color: #14b8a6; }
|
|
475
|
+
.op-status-review .op-badge { background: #134e4a; color: #ccfbf1; border-color: #14b8a6; }
|
|
476
|
+
|
|
477
|
+
.op-status-completed .op-bullet { background: #166534; border-color: #22c55e; }
|
|
478
|
+
.op-status-completed .op-badge { background: #14532d; color: #dcfce7; border-color: #22c55e; }
|
|
479
|
+
|
|
480
|
+
.op-status-unknown .op-bullet { background: #475569; border-color: #94a3b8; }
|
|
481
|
+
.op-status-unknown .op-badge { background: #334155; color: #e2e8f0; border-color: #94a3b8; }
|
|
482
|
+
|
|
483
|
+
.pipeline-stale {
|
|
484
|
+
display: inline-block;
|
|
485
|
+
background: var(--warn);
|
|
486
|
+
color: #1f2937;
|
|
487
|
+
padding: 1px 8px;
|
|
488
|
+
border-radius: 8px;
|
|
489
|
+
font-size: 11px;
|
|
490
|
+
margin-left: 8px;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.pipeline-error {
|
|
494
|
+
background: rgba(239, 68, 68, 0.08);
|
|
495
|
+
border: 1px solid var(--bad);
|
|
496
|
+
border-radius: 6px;
|
|
497
|
+
padding: 10px 12px;
|
|
498
|
+
display: flex;
|
|
499
|
+
align-items: center;
|
|
500
|
+
gap: 12px;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.btn {
|
|
504
|
+
background: var(--panel-2);
|
|
505
|
+
color: var(--text);
|
|
506
|
+
border: 1px solid var(--border);
|
|
507
|
+
border-radius: 6px;
|
|
508
|
+
padding: 6px 12px;
|
|
509
|
+
cursor: pointer;
|
|
510
|
+
font-size: 13px;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.btn-retry:hover {
|
|
514
|
+
border-color: var(--accent);
|
|
515
|
+
color: var(--accent);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
@media (max-width: 800px) {
|
|
519
|
+
.op-row1 {
|
|
520
|
+
flex-direction: column;
|
|
521
|
+
align-items: flex-start;
|
|
522
|
+
gap: 4px;
|
|
523
|
+
}
|
|
524
|
+
.op-time {
|
|
525
|
+
margin-left: 0;
|
|
526
|
+
}
|
|
527
|
+
}
|