codebee 0.1.104 → 0.1.106

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.
@@ -0,0 +1,199 @@
1
+ /* Activity model is dependency-free; browser integration uses the existing UI state. */
2
+ "use strict";
3
+ (function (root) {
4
+ const READ_KEY = "orch.activity.read.v1";
5
+ const VIEW_KEY = "orch.activity.view.v1";
6
+ const activeStatuses = new Set(["running", "queued"]);
7
+ const failures = new Set(["failed", "timeout", "cancelled"]);
8
+ const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
9
+ const fingerprint = (run) => [run.id, run.status, run.ended_at || ""].join("|");
10
+
11
+ function parseTime(value) {
12
+ // Server timestamps without a zone are local time, like the rest of the UI.
13
+ return new Date(String(value || "").replace(" ", "T"));
14
+ }
15
+
16
+ function dayGroup(value, now) {
17
+ const date = parseTime(value);
18
+ if (!Number.isFinite(date.getTime())) return "更早";
19
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
20
+ const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
21
+ return date >= today ? "今天" : date >= yesterday ? "昨天" : "更早";
22
+ }
23
+
24
+ function build(state, reads, now) {
25
+ state = state || {};
26
+ reads = reads || {};
27
+ now = now || new Date();
28
+ const tasks = new Map((state.tasks || []).filter((task) => !task.archived).map((task) => [task.id, task]));
29
+ const archived = new Set((state.archived_tasks || []).map((task) => task.id));
30
+ const latest = state.task_latest || {};
31
+ const runs = new Map();
32
+ // task_latest covers tasks outside the recent 40-run window.
33
+ (state.runs || []).forEach((run) => runs.set(run.id, run));
34
+ Object.values(latest).forEach((run) => runs.set(run.id, run));
35
+ const rows = [];
36
+ runs.forEach((run) => {
37
+ if (!run.id || (run.task_id && (!tasks.has(run.task_id) || archived.has(run.task_id)))) return;
38
+ const task = tasks.get(run.task_id) || {};
39
+ const isLatest = !run.task_id || !latest[run.task_id] || latest[run.task_id].id === run.id;
40
+ const active = activeStatuses.has(run.status);
41
+ const attention = (state.task_attention || {})[run.task_id] || {};
42
+ // A running isolated branch is being written; offer verdict only after it stops.
43
+ const reasons = [];
44
+ if (isLatest && task.git_state === "isolated" && !active) reasons.push("待裁决");
45
+ if (isLatest && attention.approval_pending) reasons.push("待审批");
46
+ if (isLatest && active && run.paused) reasons.push("已暂停");
47
+ if (isLatest && failures.has(run.status)) reasons.push(run.status === "timeout" ? "超时" : run.status === "cancelled" ? "已取消" : "执行失败");
48
+ const needs = reasons.length > 0;
49
+ if (!needs && !active && run.status !== "done") return;
50
+ const step = run.current_step || {};
51
+ const failedStep = run.failed_step || {};
52
+ const stepText = [step.n ? "#" + step.n : "", step.role, step.agent_label || step.agent].filter(Boolean).join(" · ");
53
+ const summary = compact(needs ? (run.error || run.summary || failedStep.summary || step.summary) :
54
+ active ? [stepText, step.summary].filter(Boolean).join(" — ") : (run.summary || step.summary));
55
+ const time = run.ended_at || run.started_at || run.created_at || task.created_at || "";
56
+ const unread = run.status === "done" && reads[run.id] !== fingerprint(run);
57
+ rows.push({ id: run.id, taskId: task.id || "", title: task.title || run.title || run.id,
58
+ status: run.status, time, workdir: task.workdir || "", summary,
59
+ reasons, needs, unread, fingerprint: fingerprint(run),
60
+ step: Number(failedStep.n) || 0,
61
+ target: failures.has(run.status) ? "steps" : reasons.includes("待裁决") ? "git" :
62
+ reasons.includes("待审批") ? "approval" : active ? "hive" : "result",
63
+ group: needs ? "需要你处理" : active ? "正在执行" : dayGroup(time, now) });
64
+ });
65
+ rows.sort((a, b) => (parseTime(b.time).getTime() || 0) - (parseTime(a.time).getTime() || 0) || a.id.localeCompare(b.id));
66
+ return { rows, pending: rows.filter((row) => row.needs).length,
67
+ unread: rows.filter((row) => row.unread && !row.needs).length };
68
+ }
69
+
70
+ const model = { build, fingerprint, dayGroup };
71
+ if (typeof module !== "undefined" && module.exports) module.exports = model;
72
+ if (typeof document === "undefined") return;
73
+
74
+ let reads = {};
75
+ let enabled = false;
76
+ let signature = "";
77
+ try {
78
+ const saved = JSON.parse(localStorage.getItem(READ_KEY) || "{}");
79
+ if (saved && typeof saved === "object" && !Array.isArray(saved)) reads = saved;
80
+ enabled = localStorage.getItem(VIEW_KEY) === "1";
81
+ } catch (error) { /* Storage may be unavailable; session state still works. */ }
82
+
83
+ function saveReads() {
84
+ // Bounded persistent index. Reading a rerun cannot acknowledge an older/newer run.
85
+ reads = Object.fromEntries(Object.entries(reads).slice(-1000));
86
+ try { localStorage.setItem(READ_KEY, JSON.stringify(reads)); } catch (error) { /* memory only */ }
87
+ }
88
+
89
+ function paintToggle() {
90
+ const button = $("btn-notify-toggle");
91
+ if (!button) return;
92
+ button.classList.toggle("on", enabled);
93
+ button.classList.remove("off");
94
+ button.setAttribute("aria-pressed", String(enabled));
95
+ button.title = t(enabled ? "关闭活动视图" : "打开活动视图") + " (Ctrl+Alt+U)";
96
+ button.setAttribute("aria-label", button.title);
97
+ $("side-projects").classList.toggle("hidden", enabled);
98
+ $("side-activity").classList.toggle("hidden", !enabled);
99
+ }
100
+
101
+ function markViewed(run) {
102
+ if (!run || run.status !== "done" || reads[run.id] === fingerprint(run)) return;
103
+ delete reads[run.id];
104
+ reads[run.id] = fingerprint(run);
105
+ saveReads();
106
+ render();
107
+ }
108
+
109
+ function rowHtml(row) {
110
+ const selected = S.detailRunId === row.id || (row.taskId && S.detailTaskKey === row.taskId);
111
+ const status = row.needs ? row.reasons.map((reason) => t(reason)).join(" · ") : browserStatusLabel(row.status);
112
+ return '<button type="button" class="activity-row' + (selected ? ' selected' : '') + '" data-activity-run="' + esc(row.id) + '"' +
113
+ (selected ? ' aria-current="true"' : '') + '>' +
114
+ '<span class="activity-title"><strong>' + esc(row.title) + '</strong>' +
115
+ (row.unread ? '<span class="activity-dot" title="' + esc(t("未读")) + '"><span class="sr-only">' + esc(t("未读")) + '</span></span>' : '') + '</span>' +
116
+ '<span class="activity-meta"><span class="activity-status ' + (row.needs ? 'needs ' : '') + esc(row.status) + '">' + esc(status) + '</span>' +
117
+ '<time title="' + esc(row.time) + '">' + esc(relTime(row.time)) + '</time></span>' +
118
+ (row.summary ? '<span class="activity-summary">' + esc(row.summary) + '</span>' : '') +
119
+ (row.workdir ? '<span class="activity-project" title="' + esc(row.workdir) + '">' + esc(dirName(row.workdir)) + '</span>' : '') + '</button>';
120
+ }
121
+
122
+ function render() {
123
+ if (!$("activity-list")) return;
124
+ const data = build(S.state, reads);
125
+ const badge = $("activity-badge");
126
+ const count = data.pending + data.unread;
127
+ badge.textContent = count > 99 ? "99+" : String(count);
128
+ badge.classList.toggle("hidden", !count);
129
+ badge.title = t("{0} 项待处理 · {1} 项完成未读", data.pending, data.unread);
130
+ $("activity-count").textContent = badge.title;
131
+ $("activity-read-all").disabled = !data.rows.some((row) => row.unread);
132
+ paintToggle();
133
+ if (!enabled) return;
134
+ const sig = JSON.stringify([data, S.detailRunId, S.detailTaskKey, document.documentElement.dataset.lang]);
135
+ if (sig === signature) return;
136
+ signature = sig;
137
+ const focused = document.activeElement && document.activeElement.dataset.activityRun;
138
+ $("activity-list").innerHTML = ["需要你处理", "正在执行", "今天", "昨天", "更早"].map((group) => {
139
+ const rows = data.rows.filter((row) => row.group === group);
140
+ if (!rows.length) return "";
141
+ return '<section class="activity-group' + (group === "需要你处理" ? ' needs' : '') + '"><h3>' + esc(t(group)) +
142
+ '<span>' + rows.length + '</span></h3>' + rows.map(rowHtml).join("") + '</section>';
143
+ }).join("") || '<div class="activity-empty">' + esc(t("暂无活动,开始一个任务后会在这里显示进展。")) + '</div>';
144
+ if (focused) {
145
+ const button = Array.from($("activity-list").querySelectorAll("[data-activity-run]")).find((item) => item.dataset.activityRun === focused);
146
+ if (button) button.focus({ preventScroll: true });
147
+ }
148
+ }
149
+
150
+ function toggle() {
151
+ enabled = !enabled;
152
+ if (document.body.classList.contains("files-mode")) closeFolderFiles();
153
+ if (document.body.classList.contains("settings-mode")) exitSettings();
154
+ document.body.classList.remove("side-collapsed");
155
+ try { localStorage.setItem(VIEW_KEY, enabled ? "1" : "0"); } catch (error) { /* memory only */ }
156
+ signature = "";
157
+ render();
158
+ }
159
+
160
+ function openRow(id) {
161
+ const row = build(S.state, reads).rows.find((item) => item.id === id);
162
+ if (!row) return;
163
+ if (row.target === "steps") sideOpenRun(row.id, row.step, "steps");
164
+ else if (row.taskId && ["git", "approval"].includes(row.target)) sideOpenTask(row.taskId, row.target === "approval" ? "hive" : row.target);
165
+ else sideOpenRun(row.id, 0, row.target);
166
+ if (row.target === "approval") setRdMetaOpen(true);
167
+ render();
168
+ }
169
+
170
+ root.CodeBeeActivity = { model, render, paintToggle, markViewed };
171
+ document.addEventListener("DOMContentLoaded", () => {
172
+ $("btn-notify-toggle").addEventListener("click", toggle);
173
+ $("activity-sound").addEventListener("click", toggleNotifySound);
174
+ $("activity-read-all").addEventListener("click", () => {
175
+ build(S.state, reads).rows.filter((row) => row.unread).forEach((row) => {
176
+ delete reads[row.id]; reads[row.id] = row.fingerprint;
177
+ });
178
+ saveReads(); render();
179
+ });
180
+ $("activity-list").addEventListener("click", (event) => {
181
+ const row = event.target.closest("[data-activity-run]");
182
+ if (row) openRow(row.dataset.activityRun);
183
+ });
184
+ document.addEventListener("keydown", (event) => {
185
+ if (event.ctrlKey && event.altKey && !event.metaKey && !event.shiftKey && !event.repeat && !event.isComposing && event.key.toLowerCase() === "u") {
186
+ event.preventDefault(); toggle();
187
+ }
188
+ });
189
+ window.addEventListener("storage", (event) => {
190
+ if (event.key !== READ_KEY) return;
191
+ try {
192
+ const saved = JSON.parse(event.newValue || "{}");
193
+ if (saved && typeof saved === "object" && !Array.isArray(saved)) reads = saved;
194
+ } catch (error) { return; }
195
+ render();
196
+ });
197
+ render();
198
+ });
199
+ })(typeof window !== "undefined" ? window : globalThis);