@tidegap/cli 0.0.0-stage → 0.18.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/LICENSE +201 -0
- package/README.md +125 -2
- package/dist/commands/env-file.d.ts +53 -0
- package/dist/commands/env-file.d.ts.map +1 -0
- package/dist/commands/env-file.js +127 -0
- package/dist/commands/env-file.js.map +1 -0
- package/dist/commands/install.d.ts +84 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +299 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/queue.d.ts +139 -0
- package/dist/commands/queue.d.ts.map +1 -0
- package/dist/commands/queue.js +491 -0
- package/dist/commands/queue.js.map +1 -0
- package/dist/commands/receipts.d.ts +15 -0
- package/dist/commands/receipts.d.ts.map +1 -0
- package/dist/commands/receipts.js +70 -0
- package/dist/commands/receipts.js.map +1 -0
- package/dist/commands/register-wake.d.ts +21 -0
- package/dist/commands/register-wake.d.ts.map +1 -0
- package/dist/commands/register-wake.js +76 -0
- package/dist/commands/register-wake.js.map +1 -0
- package/dist/commands/stats.d.ts +31 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +165 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/commands/tick.d.ts +73 -0
- package/dist/commands/tick.d.ts.map +1 -0
- package/dist/commands/tick.js +294 -0
- package/dist/commands/tick.js.map +1 -0
- package/dist/commands/ui.d.ts +25 -0
- package/dist/commands/ui.d.ts.map +1 -0
- package/dist/commands/ui.js +83 -0
- package/dist/commands/ui.js.map +1 -0
- package/dist/commands/verify.d.ts +32 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +135 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/env.d.ts +55 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +90 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +351 -0
- package/dist/index.js.map +1 -0
- package/dist/platform/index.d.ts +30 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +66 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/linux.d.ts +81 -0
- package/dist/platform/linux.d.ts.map +1 -0
- package/dist/platform/linux.js +158 -0
- package/dist/platform/linux.js.map +1 -0
- package/dist/platform/macos.d.ts +69 -0
- package/dist/platform/macos.d.ts.map +1 -0
- package/dist/platform/macos.js +151 -0
- package/dist/platform/macos.js.map +1 -0
- package/dist/platform/windows.d.ts +31 -0
- package/dist/platform/windows.d.ts.map +1 -0
- package/dist/platform/windows.js +58 -0
- package/dist/platform/windows.js.map +1 -0
- package/dist/ui/api.d.ts +59 -0
- package/dist/ui/api.d.ts.map +1 -0
- package/dist/ui/api.js +459 -0
- package/dist/ui/api.js.map +1 -0
- package/dist/ui/dispatcher.d.ts +84 -0
- package/dist/ui/dispatcher.d.ts.map +1 -0
- package/dist/ui/dispatcher.js +150 -0
- package/dist/ui/dispatcher.js.map +1 -0
- package/dist/ui/events.d.ts +27 -0
- package/dist/ui/events.d.ts.map +1 -0
- package/dist/ui/events.js +85 -0
- package/dist/ui/events.js.map +1 -0
- package/dist/ui/server.d.ts +69 -0
- package/dist/ui/server.d.ts.map +1 -0
- package/dist/ui/server.js +258 -0
- package/dist/ui/server.js.map +1 -0
- package/package.json +54 -3
- package/ui/agenda.js +256 -0
- package/ui/api.js +111 -0
- package/ui/app.css +707 -0
- package/ui/app.js +597 -0
- package/ui/detail.js +431 -0
- package/ui/dialog.js +279 -0
- package/ui/dnd.js +131 -0
- package/ui/dom.js +255 -0
- package/ui/index.html +37 -0
- package/ui/journal.js +207 -0
- package/ui/keys.js +92 -0
- package/ui/row.js +153 -0
- package/ui/state.js +412 -0
- package/ui/timeline.js +202 -0
- package/ui/toast.js +69 -0
- package/ui/tokens.css +106 -0
- package/ui/views.js +31 -0
package/ui/detail.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// Task detail drawer: everything about one task and the actions its state
|
|
2
|
+
// allows. Buttons carry data-action and the drawer carries data-task-id, so
|
|
3
|
+
// app.js handles them with the same delegated listener as the rows.
|
|
4
|
+
// Opening an unread finished task marks it read (v1 behaviour).
|
|
5
|
+
|
|
6
|
+
import { api, ApiError, SessionExpired } from "./api.js";
|
|
7
|
+
import {
|
|
8
|
+
bandChip,
|
|
9
|
+
bandOf,
|
|
10
|
+
grams,
|
|
11
|
+
h,
|
|
12
|
+
isFinished,
|
|
13
|
+
parse,
|
|
14
|
+
providerModel,
|
|
15
|
+
REPEAT_LABEL,
|
|
16
|
+
shortId,
|
|
17
|
+
swapChildren,
|
|
18
|
+
taskHref,
|
|
19
|
+
title,
|
|
20
|
+
when,
|
|
21
|
+
} from "./dom.js";
|
|
22
|
+
import { badges, joinMeta, statusTag } from "./row.js";
|
|
23
|
+
import { findTask, notify, state } from "./state.js";
|
|
24
|
+
import { errorToast, toast } from "./toast.js";
|
|
25
|
+
|
|
26
|
+
const drawerState = {
|
|
27
|
+
id: null,
|
|
28
|
+
data: null,
|
|
29
|
+
loading: false,
|
|
30
|
+
error: null,
|
|
31
|
+
draft: "",
|
|
32
|
+
rawOpen: false,
|
|
33
|
+
seq: 0,
|
|
34
|
+
returnFocus: null,
|
|
35
|
+
marked: new Set(),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function detailId() {
|
|
39
|
+
return drawerState.id;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function el() {
|
|
43
|
+
return document.getElementById("drawer");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function openDetail(id) {
|
|
47
|
+
const same = drawerState.id === id;
|
|
48
|
+
if (!same) {
|
|
49
|
+
if (!drawerState.id) drawerState.returnFocus = document.activeElement;
|
|
50
|
+
drawerState.id = id;
|
|
51
|
+
drawerState.data = null;
|
|
52
|
+
drawerState.error = null;
|
|
53
|
+
drawerState.draft = "";
|
|
54
|
+
drawerState.rawOpen = false;
|
|
55
|
+
}
|
|
56
|
+
drawerState.loading = !same || !drawerState.data;
|
|
57
|
+
renderDetail();
|
|
58
|
+
if (!same) focusDrawer();
|
|
59
|
+
await loadDetail();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function loadDetail() {
|
|
63
|
+
const id = drawerState.id;
|
|
64
|
+
if (!id) return;
|
|
65
|
+
const mine = ++drawerState.seq;
|
|
66
|
+
try {
|
|
67
|
+
const data = await api.task(id);
|
|
68
|
+
const series = data.series ? await api.series(data.series.series_id).catch(() => null) : null;
|
|
69
|
+
if (mine !== drawerState.seq || drawerState.id !== id) return;
|
|
70
|
+
drawerState.data = data;
|
|
71
|
+
drawerState.series = series;
|
|
72
|
+
drawerState.error = null;
|
|
73
|
+
drawerState.loading = false;
|
|
74
|
+
renderDetail();
|
|
75
|
+
maybeMarkRead(data);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (mine !== drawerState.seq || drawerState.id !== id) return;
|
|
78
|
+
if (err instanceof SessionExpired) {
|
|
79
|
+
state.expired = true;
|
|
80
|
+
notify();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
drawerState.loading = false;
|
|
84
|
+
drawerState.error = err;
|
|
85
|
+
renderDetail();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function maybeMarkRead(t) {
|
|
90
|
+
if ((t.status !== "completed" && t.status !== "failed") || t.read_at || drawerState.marked.has(t.task_id)) return;
|
|
91
|
+
drawerState.marked.add(t.task_id);
|
|
92
|
+
// A failed mark-read is ignored: the task just stays unread.
|
|
93
|
+
api.read(t.task_id).catch(() => {});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function closeDetail() {
|
|
97
|
+
if (!drawerState.id) return;
|
|
98
|
+
const id = drawerState.id;
|
|
99
|
+
drawerState.id = null;
|
|
100
|
+
drawerState.data = null;
|
|
101
|
+
drawerState.seq++;
|
|
102
|
+
el().hidden = true;
|
|
103
|
+
el().removeAttribute("data-task-id");
|
|
104
|
+
document.getElementById("scrim").hidden = true;
|
|
105
|
+
document.body.classList.remove("drawer-open");
|
|
106
|
+
const row = document.querySelector(`#view .row[data-task-id="${CSS.escape(id)}"]`);
|
|
107
|
+
const back = row || (drawerState.returnFocus?.isConnected ? drawerState.returnFocus : null);
|
|
108
|
+
(back || document.getElementById("view")).focus({ preventScroll: !!row });
|
|
109
|
+
drawerState.returnFocus = null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Re-fetch when the stream names this task, its parent or a child. */
|
|
113
|
+
export function onDetailEvent(ids) {
|
|
114
|
+
const d = drawerState.data;
|
|
115
|
+
if (!drawerState.id) return;
|
|
116
|
+
const related = new Set([drawerState.id]);
|
|
117
|
+
if (d?.parent?.task_id) related.add(d.parent.task_id);
|
|
118
|
+
for (const c of d?.children || []) related.add(c.task_id);
|
|
119
|
+
if (ids.length === 0 || ids.some((id) => related.has(id)) || d?.series) void loadDetail();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function focusDrawer() {
|
|
123
|
+
requestAnimationFrame(() => el().querySelector(".drawer-close")?.focus());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function focusReply() {
|
|
127
|
+
const box = el().querySelector("textarea.reply-input");
|
|
128
|
+
if (box) box.focus();
|
|
129
|
+
return !!box;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Keep Tab inside the open drawer. */
|
|
133
|
+
export function trapFocus(e) {
|
|
134
|
+
if (e.key !== "Tab" || el().hidden) return;
|
|
135
|
+
const items = [...el().querySelectorAll("a[href], button:not([disabled]), textarea, select, input, summary, [tabindex='0']")].filter((n) => n.offsetParent !== null);
|
|
136
|
+
if (items.length === 0) return;
|
|
137
|
+
const first = items[0], last = items[items.length - 1];
|
|
138
|
+
if (!el().contains(document.activeElement)) {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
first.focus();
|
|
141
|
+
} else if (e.shiftKey && document.activeElement === first) {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
last.focus();
|
|
144
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
first.focus();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── render ──────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
function section(name, ...body) {
|
|
153
|
+
return h("section", { class: "d-section" }, h("h3", { class: "d-h" }, name), body);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function kv(pairs) {
|
|
157
|
+
const dl = h("dl", { class: "kv" });
|
|
158
|
+
for (const [k, v] of pairs) {
|
|
159
|
+
if (v === undefined || v === null || v === "") continue;
|
|
160
|
+
dl.append(h("dt", {}, k), h("dd", {}, v));
|
|
161
|
+
}
|
|
162
|
+
return dl;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function renderDetail() {
|
|
166
|
+
const root = el();
|
|
167
|
+
if (!drawerState.id) return;
|
|
168
|
+
// Merge the optimistic view of this task (pending op) over the fetched one.
|
|
169
|
+
const live = findTask(drawerState.id);
|
|
170
|
+
const t = drawerState.data ? { ...drawerState.data, ...(live?._op ? { ...live._op.patch, _op: live._op } : {}) } : null;
|
|
171
|
+
const draftBox = root.querySelector("textarea.reply-input");
|
|
172
|
+
if (draftBox) drawerState.draft = draftBox.value;
|
|
173
|
+
const rawBox = root.querySelector("details.raw");
|
|
174
|
+
if (rawBox) drawerState.rawOpen = rawBox.open;
|
|
175
|
+
const focusedAction = root.contains(document.activeElement) ? document.activeElement?.dataset?.action || (document.activeElement?.classList.contains("reply-input") ? "reply-input" : null) : null;
|
|
176
|
+
|
|
177
|
+
root.setAttribute("data-task-id", drawerState.id);
|
|
178
|
+
const close = h("button", { type: "button", class: "icon-btn drawer-close", "data-action": "close", "aria-label": "Close (Esc)", title: "Close (Esc)" }, "×");
|
|
179
|
+
let body;
|
|
180
|
+
if (drawerState.error) {
|
|
181
|
+
const notFound = drawerState.error instanceof ApiError && drawerState.error.status === 404;
|
|
182
|
+
body = [
|
|
183
|
+
h("div", { class: "drawer-head" }, h("h2", { id: "drawer-title", class: "drawer-title", tabindex: "-1" }, notFound ? "Task not found" : "Could not load this task"), close),
|
|
184
|
+
h("p", { class: "muted" }, notFound ? "It may have been deleted, or the link is from another ledger." : "The page could not reach tidegap ui."),
|
|
185
|
+
h("p", {}, notFound ? h("a", { class: "btn", href: "#/agenda" }, "Back to the Agenda") : h("button", { type: "button", class: "btn", "data-action": "detail-retry" }, "Retry")),
|
|
186
|
+
];
|
|
187
|
+
} else if (!t) {
|
|
188
|
+
body = [
|
|
189
|
+
h("div", { class: "drawer-head" }, h("h2", { id: "drawer-title", class: "drawer-title" }, h("span", { class: "sk sk-title" })), close),
|
|
190
|
+
h("div", { class: "sk sk-block" }), h("div", { class: "sk sk-block" }), h("div", { class: "sk sk-block sk-short" }),
|
|
191
|
+
];
|
|
192
|
+
} else {
|
|
193
|
+
body = detailBody(t, close);
|
|
194
|
+
}
|
|
195
|
+
swapChildren(root, body);
|
|
196
|
+
root.hidden = false;
|
|
197
|
+
document.getElementById("scrim").hidden = false;
|
|
198
|
+
document.body.classList.add("drawer-open");
|
|
199
|
+
const box = root.querySelector("textarea.reply-input");
|
|
200
|
+
if (box) box.value = drawerState.draft;
|
|
201
|
+
if (focusedAction) {
|
|
202
|
+
const target = focusedAction === "reply-input" ? box : root.querySelector(`[data-action="${focusedAction}"]`);
|
|
203
|
+
target?.focus();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function headline(t) {
|
|
208
|
+
const parts = [statusTag(t)];
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const at = parse(t.scheduled_for);
|
|
211
|
+
const done = parse(t.completed_at);
|
|
212
|
+
if (isFinished(t) && Number.isFinite(done)) parts.push(`${t.status === "cancelled" ? "cancelled" : "ran"} ${when(done, now)}`);
|
|
213
|
+
else if (Number.isFinite(at)) parts.push(at <= now ? "due now · runs on the next tick" : `runs ${when(at, now)}`);
|
|
214
|
+
if (typeof t.carbon_g === "number") parts.push(h("span", { class: "num" }, `${grams(t.carbon_g)} CO2e${t.status === "completed" ? "" : " est."}`));
|
|
215
|
+
const band = bandOf(t.intensity_g_per_kwh);
|
|
216
|
+
if (band) parts.push(bandChip(band));
|
|
217
|
+
return parts;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function actionBar(t) {
|
|
221
|
+
const a = t.actions || {};
|
|
222
|
+
const pendingOp = !!t._op;
|
|
223
|
+
const btn = (action, label, cls = "", key) =>
|
|
224
|
+
h("button", { type: "button", class: `btn ${cls}`, "data-action": action, disabled: pendingOp ? true : undefined, title: key ? `${label} (${key})` : label }, label);
|
|
225
|
+
const out = [];
|
|
226
|
+
if (a.runNow) out.push(btn("run-now", "Run now", "btn-primary", "n"));
|
|
227
|
+
if (a.reschedule) out.push(btn("reschedule", "Reschedule…", "", "r"), btn("move", "Move to…", "", "m"));
|
|
228
|
+
if (a.reply) out.push(btn("reply-focus", "Reply", "btn-primary", "R"));
|
|
229
|
+
if (a.markRead) out.push(btn(t.read_at ? "unread" : "read", t.read_at ? "Mark unread" : "Mark read", "", "u"));
|
|
230
|
+
if (a.cancel) out.push(btn("cancel", "Cancel", "btn-danger", "c"));
|
|
231
|
+
if (a.stopSeries) out.push(btn("stop-series", "Stop series", "btn-danger", "C"));
|
|
232
|
+
if (pendingOp) out.push(h("span", { class: "row-op" }, h("span", { class: "spinner", "aria-hidden": "true" }), ` ${t._op.label || "Saving…"}`));
|
|
233
|
+
return out.length ? h("div", { class: "action-bar" }, out) : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function detailBody(t, close) {
|
|
237
|
+
const out = [
|
|
238
|
+
h("div", { class: "drawer-head" },
|
|
239
|
+
h("h2", { id: "drawer-title", class: "drawer-title", tabindex: "-1", title: title(t) }, title(t)),
|
|
240
|
+
close),
|
|
241
|
+
h("p", { class: "drawer-meta" }, joinMeta(headline(t))),
|
|
242
|
+
badges(t).length ? h("p", { class: "drawer-badges" }, badges(t), providerModel(t) ? h("span", { class: "muted" }, providerModel(t)) : null) : providerModel(t) ? h("p", { class: "drawer-badges muted" }, providerModel(t)) : null,
|
|
243
|
+
actionBar(t),
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
if (t.error) out.push(section("Error", h("pre", { class: "pre is-error" }, t.error)));
|
|
247
|
+
|
|
248
|
+
if (t.status === "completed") {
|
|
249
|
+
const text = t.result_text ?? (t.result !== null && t.result !== undefined ? JSON.stringify(t.result, null, 2) : null);
|
|
250
|
+
const blocks = [text !== null ? h("pre", { class: "pre result" }, text) : h("p", { class: "muted" }, "No result text.")];
|
|
251
|
+
if (t.raw !== null && t.raw !== undefined) {
|
|
252
|
+
const raw = h("details", { class: "raw" }, h("summary", {}, "Raw JSON"), h("pre", { class: "pre mono" }, JSON.stringify(t.raw, null, 2)));
|
|
253
|
+
raw.open = drawerState.rawOpen;
|
|
254
|
+
blocks.push(raw);
|
|
255
|
+
}
|
|
256
|
+
out.push(section("Result", blocks));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
out.push(conversation(t));
|
|
260
|
+
|
|
261
|
+
if (!isFinished(t)) {
|
|
262
|
+
out.push(section("Schedule", kv([
|
|
263
|
+
["runs", Number.isFinite(parse(t.scheduled_for)) ? when(parse(t.scheduled_for)) : "not planned yet"],
|
|
264
|
+
["deadline", Number.isFinite(parse(t.deadline)) ? `${when(parse(t.deadline))}${t.deadline_label ? ` (${t.deadline_label})` : ""}` : null],
|
|
265
|
+
["region", t.region],
|
|
266
|
+
["estimate", typeof t.estimated_carbon_g === "number" ? `${grams(t.estimated_carbon_g)} CO2e` : null],
|
|
267
|
+
])));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (t.receipt) out.push(receipt(t));
|
|
271
|
+
out.push(origin(t));
|
|
272
|
+
const chain = chainSection(t);
|
|
273
|
+
if (chain) out.push(chain);
|
|
274
|
+
out.push(section("Details", kv([
|
|
275
|
+
["task id", h("code", {}, t.task_id)],
|
|
276
|
+
["enqueued", Number.isFinite(parse(t.enqueued_at)) ? when(parse(t.enqueued_at)) : null],
|
|
277
|
+
["region", t.region],
|
|
278
|
+
["routing", t.routing?.chosen ? `${t.routing.chosen}${t.routing.reasoning ? ` — ${t.routing.reasoning}` : ""}` : null],
|
|
279
|
+
])));
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function conversation(t) {
|
|
284
|
+
const turns = Array.isArray(t.history) ? [...t.history] : [];
|
|
285
|
+
if (turns.length === 0 && t.prompt) turns.push({ role: "user", content: t.prompt });
|
|
286
|
+
// On a completed task the last assistant turn is the result shown above.
|
|
287
|
+
const shown = t.status === "completed" && turns.length && turns[turns.length - 1].role === "assistant" && t.result_text ? turns.slice(0, -1) : turns;
|
|
288
|
+
const items = shown.map((m) =>
|
|
289
|
+
h("li", { class: `turn turn-${m.role}` },
|
|
290
|
+
h("span", { class: "turn-role" }, m.role === "user" ? "you" : "model"),
|
|
291
|
+
h("pre", { class: "pre turn-text" }, m.content)));
|
|
292
|
+
const body = [];
|
|
293
|
+
if (items.length) body.push(h("ol", { class: "turns" }, items));
|
|
294
|
+
else body.push(h("p", { class: "muted" }, t.status === "running" ? "The prompt is hidden while the task runs." : "No stored prompt."));
|
|
295
|
+
if (t.actions?.reply) {
|
|
296
|
+
body.push(h("form", { class: "reply", "data-action-form": "reply" },
|
|
297
|
+
h("label", { class: "sr-only", for: "reply-input" }, "Reply"),
|
|
298
|
+
h("textarea", { id: "reply-input", class: "reply-input", rows: "3", maxlength: "32000", placeholder: "Continue the conversation…", "aria-describedby": "reply-hint" }),
|
|
299
|
+
h("div", { class: "reply-row" },
|
|
300
|
+
h("span", { id: "reply-hint", class: "muted" }, "Same provider and model, with this history. Runs on the next tick."),
|
|
301
|
+
h("button", { type: "submit", class: "btn btn-primary", "data-action": "reply-send" }, "Send"))));
|
|
302
|
+
} else if (t.status === "failed" || t.status === "cancelled") {
|
|
303
|
+
body.push(h("p", { class: "muted small" }, `A ${t.status} task cannot be replied to.`));
|
|
304
|
+
}
|
|
305
|
+
return section("Conversation", body);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function receipt(t) {
|
|
309
|
+
const r = t.receipt;
|
|
310
|
+
const est = r.estimatedCarbonGCo2;
|
|
311
|
+
const act = r.actualCarbonGCo2;
|
|
312
|
+
return section("Receipt",
|
|
313
|
+
kv([
|
|
314
|
+
["region", r.region],
|
|
315
|
+
["intensity", typeof r.intensityGCo2PerKwh === "number" ? h("span", {}, `${Math.round(r.intensityGCo2PerKwh)} gCO2/kWh `, bandChip(bandOf(r.intensityGCo2PerKwh))) : null],
|
|
316
|
+
["estimated", typeof est === "number" ? `${grams(est)} CO2e` : null],
|
|
317
|
+
["actual", typeof act === "number" ? `${grams(act)} CO2e${typeof r.deltaPct === "number" ? ` (${r.deltaPct > 0 ? "+" : ""}${r.deltaPct}%)` : ""}` : null],
|
|
318
|
+
["tokens", typeof r.totalTokens === "number" ? String(r.totalTokens) : null],
|
|
319
|
+
["grid", r.gridSource === "mock" ? "mock (synthetic, not a measurement)" : r.gridSource],
|
|
320
|
+
["signature", r.signature ? h("span", { class: "tag tag-completed" }, "signed ✓") : h("span", { class: "tag tag-muted" }, "unsigned")],
|
|
321
|
+
]),
|
|
322
|
+
r.signature ? h("p", { class: "muted small" }, "Verify: ", h("code", {}, `tidegap verify ${t.task_id}`)) : null);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function origin(t) {
|
|
326
|
+
const where = [t.origin_host, t.origin_cwd].filter(Boolean).join(" · ");
|
|
327
|
+
const body = [h("p", {}, where || h("span", { class: "muted" }, "Origin not recorded (older task)."))];
|
|
328
|
+
if (t.resume_command) {
|
|
329
|
+
body.push(h("div", { class: "cmd" },
|
|
330
|
+
h("code", { class: "cmd-text", id: "resume-cmd" }, `$ ${t.resume_command}`),
|
|
331
|
+
h("button", { type: "button", class: "btn btn-sm", "data-action": "copy-resume", "data-copy": t.resume_command }, "Copy")));
|
|
332
|
+
body.push(h("p", { class: "muted small" }, "Back to the Claude Code session that created this task."));
|
|
333
|
+
} else if (t.origin_session_id) {
|
|
334
|
+
body.push(h("p", { class: "muted small" }, `Session ${t.origin_session_id}`));
|
|
335
|
+
}
|
|
336
|
+
return section("Origin", body);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function chainSection(t) {
|
|
340
|
+
const items = [];
|
|
341
|
+
if (t.parent) {
|
|
342
|
+
items.push(h("p", {}, "Reply to ", t.parent.missing ? h("code", {}, shortId(t.parent.task_id)) : h("a", { href: taskHref(t.parent.task_id) }, title(t.parent))));
|
|
343
|
+
}
|
|
344
|
+
if (t.children?.length) {
|
|
345
|
+
items.push(h("p", { class: "muted small" }, "Replies"));
|
|
346
|
+
items.push(h("ul", { class: "links" }, t.children.map((c) => h("li", {}, h("a", { href: taskHref(c.task_id) }, title(c)), " ", statusTag(c)))));
|
|
347
|
+
}
|
|
348
|
+
const s = t.series;
|
|
349
|
+
if (s) {
|
|
350
|
+
const next = (s.next_deadlines || [])[0];
|
|
351
|
+
items.push(h("p", {},
|
|
352
|
+
s.ended ? "Series ended" : `Series: ${REPEAT_LABEL[s.repeat] || s.repeat}`,
|
|
353
|
+
` · ${s.occurrences} occurrence${s.occurrences === 1 ? "" : "s"}`,
|
|
354
|
+
next && !s.ended ? ` · next by ${when(parse(next))}` : ""));
|
|
355
|
+
const runs = drawerState.series?.occurrences || [];
|
|
356
|
+
if (runs.length > 1) {
|
|
357
|
+
items.push(h("ol", { class: "links series-runs" }, runs.slice(-8).reverse().map((o) =>
|
|
358
|
+
h("li", {},
|
|
359
|
+
o.task_id === t.task_id ? h("span", {}, "this run") : h("a", { href: taskHref(o.task_id) }, when(parse(o.completed_at || o.scheduled_for || o.deadline)) || o.task_id),
|
|
360
|
+
" ", statusTag(o)))));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return items.length ? section(s ? "Chain and series" : "Chain", items) : null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── actions owned by the drawer ─────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
let replying = false;
|
|
369
|
+
|
|
370
|
+
export async function sendReply() {
|
|
371
|
+
if (replying) return;
|
|
372
|
+
const id = drawerState.id;
|
|
373
|
+
const box = el().querySelector("textarea.reply-input");
|
|
374
|
+
const text = box?.value.trim();
|
|
375
|
+
if (!id || !text) {
|
|
376
|
+
box?.focus();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const send = el().querySelector('[data-action="reply-send"]');
|
|
380
|
+
if (send) send.disabled = true;
|
|
381
|
+
replying = true;
|
|
382
|
+
try {
|
|
383
|
+
const child = await api.reply(id, text);
|
|
384
|
+
drawerState.draft = "";
|
|
385
|
+
toast("Reply queued · runs on the next tick");
|
|
386
|
+
location.hash = taskHref(child.task_id);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
if (err instanceof SessionExpired) {
|
|
389
|
+
state.expired = true;
|
|
390
|
+
notify();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
errorToast(`Reply failed: ${err.message}`);
|
|
394
|
+
if (send) send.disabled = false;
|
|
395
|
+
} finally {
|
|
396
|
+
replying = false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function copyResume(button) {
|
|
401
|
+
const text = button.dataset.copy;
|
|
402
|
+
try {
|
|
403
|
+
await navigator.clipboard.writeText(text);
|
|
404
|
+
button.textContent = "Copied";
|
|
405
|
+
toast("Resume command copied");
|
|
406
|
+
setTimeout(() => (button.textContent = "Copy"), 2000);
|
|
407
|
+
} catch {
|
|
408
|
+
// No clipboard permission: select the text so the user can copy it.
|
|
409
|
+
const code = el().querySelector("#resume-cmd");
|
|
410
|
+
const range = document.createRange();
|
|
411
|
+
range.selectNodeContents(code);
|
|
412
|
+
const sel = getSelection();
|
|
413
|
+
sel.removeAllRanges();
|
|
414
|
+
sel.addRange(range);
|
|
415
|
+
toast("Selected. Press Ctrl+C (⌘C) to copy.");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function retryDetail() {
|
|
420
|
+
drawerState.error = null;
|
|
421
|
+
drawerState.loading = true;
|
|
422
|
+
renderDetail();
|
|
423
|
+
void loadDetail();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** The task the drawer shows, with the optimistic patch. */
|
|
427
|
+
export function detailTask() {
|
|
428
|
+
if (!drawerState.data) return null;
|
|
429
|
+
const live = findTask(drawerState.id);
|
|
430
|
+
return { ...drawerState.data, ...(live?._op ? live._op.patch : {}), actions: live?.actions ?? drawerState.data.actions };
|
|
431
|
+
}
|