opencode-subagent-magazine 1.5.2 → 1.6.0-beta.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 +21 -21
- package/README.md +192 -192
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/core/color.d.ts +20 -0
- package/dist/core/color.js +68 -0
- package/dist/core/format.d.ts +5 -0
- package/dist/core/format.js +68 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +6 -0
- package/dist/core/kv.d.ts +20 -0
- package/dist/core/kv.js +30 -0
- package/dist/core/state-machine.d.ts +17 -0
- package/dist/core/state-machine.js +27 -0
- package/dist/core/types.d.ts +51 -0
- package/dist/core/types.js +2 -0
- package/dist/core/usage.d.ts +15 -0
- package/dist/core/usage.js +1 -0
- package/dist/index.js +148 -1484
- package/dist/panel/SubAgentPanel.d.ts +13 -0
- package/dist/panel/SubAgentPanel.js +1232 -0
- package/dist/panel/panel-api.d.ts +67 -0
- package/dist/panel/panel-api.js +1 -0
- package/dist/panel/store.d.ts +5 -0
- package/dist/panel/store.js +5 -0
- package/dist/tui.js +383 -289
- package/dist/v2/commands.d.ts +7 -0
- package/dist/v2/commands.js +263 -0
- package/dist/v2/index.d.ts +8 -0
- package/dist/v2/index.js +62 -0
- package/dist/v2/theme.d.ts +3 -0
- package/dist/v2/theme.js +12 -0
- package/dist/v2/types.d.ts +231 -0
- package/dist/v2/types.js +6 -0
- package/dist/v2/v2-panel-api.d.ts +12 -0
- package/dist/v2/v2-panel-api.js +345 -0
- package/dist/v2.js +3003 -0
- package/install.mjs +103 -103
- package/package.json +64 -63
- package/src/_version.ts +1 -1
- package/src/clipboard.ts +134 -134
- package/src/core/color.ts +70 -0
- package/src/core/format.ts +61 -0
- package/src/core/index.ts +6 -0
- package/src/core/kv.ts +36 -0
- package/src/core/state-machine.ts +46 -0
- package/src/core/types.ts +58 -0
- package/src/core/usage.ts +16 -0
- package/src/i18n.ts +261 -261
- package/src/index.tsx +124 -1750
- package/src/panel/SubAgentPanel.tsx +1460 -0
- package/src/panel/panel-api.ts +72 -0
- package/src/panel/store.ts +8 -0
- package/src/server.ts +10 -10
- package/src/v2/commands.ts +251 -0
- package/src/v2/index.tsx +98 -0
- package/src/v2/theme.ts +14 -0
- package/src/v2/types.ts +165 -0
- package/src/v2/v2-panel-api.ts +301 -0
- package/tui/index.js +11 -0
package/dist/index.js
CHANGED
|
@@ -1,1515 +1,179 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "@opentui/solid/jsx-runtime";
|
|
2
|
+
import { createSignal } from "solid-js";
|
|
3
3
|
import { PLUGIN_VERSION } from "./_version";
|
|
4
|
-
import { copyText } from "./clipboard";
|
|
5
4
|
import { LANG_META, createT, detectLang } from "./i18n";
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { KV_PREFIX } from "./core/kv";
|
|
6
|
+
import { SubAgentPanel } from "./panel/SubAgentPanel";
|
|
7
|
+
import { globalEntryCache, setClearTick } from "./panel/store";
|
|
8
|
+
// Plugin entry
|
|
8
9
|
// ===================================================================
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return 0;
|
|
19
|
-
if ((code >= 0x1100 && code <= 0x115f) ||
|
|
20
|
-
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
21
|
-
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
22
|
-
(code >= 0xf900 && code <= 0xfaff) ||
|
|
23
|
-
(code >= 0xfe10 && code <= 0xfe6f) ||
|
|
24
|
-
(code >= 0xff01 && code <= 0xff60) ||
|
|
25
|
-
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
26
|
-
(code >= 0x1f300 && code <= 0x1f64f) ||
|
|
27
|
-
(code >= 0x20000 && code <= 0x3fffd))
|
|
28
|
-
return 2;
|
|
29
|
-
return 1;
|
|
30
|
-
}
|
|
31
|
-
function visualWidth(s) {
|
|
32
|
-
let w = 0;
|
|
33
|
-
for (const c of s)
|
|
34
|
-
w += charColumns(c);
|
|
35
|
-
return w;
|
|
36
|
-
}
|
|
37
|
-
function truncate(text, maxCols) {
|
|
38
|
-
if (visualWidth(text) <= maxCols)
|
|
39
|
-
return text;
|
|
40
|
-
let cols = 0;
|
|
41
|
-
let i = 0;
|
|
42
|
-
for (const c of text) {
|
|
43
|
-
const w = charColumns(c);
|
|
44
|
-
if (cols + w > maxCols - 1)
|
|
45
|
-
break;
|
|
46
|
-
cols += w;
|
|
47
|
-
i += c.length;
|
|
48
|
-
}
|
|
49
|
-
return text.slice(0, i) + "\u2026";
|
|
50
|
-
}
|
|
51
|
-
function fmtDurationShort(ms, running) {
|
|
52
|
-
if (running && ms < 2000)
|
|
53
|
-
return "";
|
|
54
|
-
if (ms < 1000)
|
|
55
|
-
return (ms / 1000).toFixed(2) + "s";
|
|
56
|
-
if (ms < 60000)
|
|
57
|
-
return (ms / 1000).toFixed(2) + "s";
|
|
58
|
-
const m = Math.floor(ms / 60000);
|
|
59
|
-
const s = Math.round((ms % 60000) / 1000);
|
|
60
|
-
return `${m}m${s}s`;
|
|
61
|
-
}
|
|
62
|
-
function fmtTokens(n) {
|
|
63
|
-
if (n < 1000)
|
|
64
|
-
return `${n}`;
|
|
65
|
-
if (n < 1000000)
|
|
66
|
-
return `${(n / 1000).toFixed(1)}k`;
|
|
67
|
-
return `${(n / 1000000).toFixed(1)}M`;
|
|
68
|
-
}
|
|
69
|
-
// ===================================================================
|
|
70
|
-
// Color helpers — Morandi palette
|
|
71
|
-
// ===================================================================
|
|
72
|
-
function rgb(raw) {
|
|
73
|
-
if (typeof raw === "string" && raw.startsWith("#")) {
|
|
74
|
-
const h = raw.slice(1);
|
|
75
|
-
return {
|
|
76
|
-
r: parseInt(h.slice(0, 2), 16),
|
|
77
|
-
g: parseInt(h.slice(2, 4), 16),
|
|
78
|
-
b: parseInt(h.slice(4, 6), 16),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
if (raw && typeof raw === "object") {
|
|
82
|
-
const o = raw;
|
|
83
|
-
if (typeof o.r === "number" && typeof o.g === "number" && typeof o.b === "number") {
|
|
84
|
-
const scale = o.r > 1 || o.g > 1 || o.b > 1 ? 1 : 255;
|
|
85
|
-
return { r: Math.round(o.r * scale), g: Math.round(o.g * scale), b: Math.round(o.b * scale) };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
function saturation(r, g, b) {
|
|
91
|
-
const max = Math.max(r, g, b) / 255;
|
|
92
|
-
const min = Math.min(r, g, b) / 255;
|
|
93
|
-
const delta = max - min;
|
|
94
|
-
if (delta === 0)
|
|
95
|
-
return 0;
|
|
96
|
-
const L = (max + min) / 2;
|
|
97
|
-
return L <= 0.5 ? delta / (max + min) : delta / (2 - max - min);
|
|
98
|
-
}
|
|
99
|
-
function desaturateTo(raw, maxSat, fallback) {
|
|
100
|
-
const c = rgb(raw);
|
|
101
|
-
if (!c)
|
|
102
|
-
return fallback;
|
|
103
|
-
const sat = saturation(c.r, c.g, c.b);
|
|
104
|
-
if (sat <= maxSat) {
|
|
105
|
-
return "#" + [c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("");
|
|
106
|
-
}
|
|
107
|
-
const luma = c.r * 0.299 + c.g * 0.587 + c.b * 0.114;
|
|
108
|
-
let lo = 0, hi = 1;
|
|
109
|
-
for (let i = 0; i < 12; i++) {
|
|
110
|
-
const mid = (lo + hi) / 2;
|
|
111
|
-
const nr = Math.round(c.r + (luma - c.r) * mid);
|
|
112
|
-
const ng = Math.round(c.g + (luma - c.g) * mid);
|
|
113
|
-
const nb = Math.round(c.b + (luma - c.b) * mid);
|
|
114
|
-
if (saturation(nr, ng, nb) > maxSat)
|
|
115
|
-
lo = mid;
|
|
116
|
-
else
|
|
117
|
-
hi = mid;
|
|
118
|
-
}
|
|
119
|
-
const nr = Math.round(c.r + (luma - c.r) * hi);
|
|
120
|
-
const ng = Math.round(c.g + (luma - c.g) * hi);
|
|
121
|
-
const nb = Math.round(c.b + (luma - c.b) * hi);
|
|
122
|
-
return "#" + [nr, ng, nb].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
|
|
123
|
-
}
|
|
124
|
-
function dimColor(hex, factor = 0.5) {
|
|
125
|
-
const c = rgb(hex);
|
|
126
|
-
if (!c)
|
|
127
|
-
return hex;
|
|
128
|
-
const r = Math.round(c.r * factor);
|
|
129
|
-
const g = Math.round(c.g * factor);
|
|
130
|
-
const b = Math.round(c.b * factor);
|
|
131
|
-
return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
|
|
132
|
-
}
|
|
133
|
-
const FALLBACK = {
|
|
134
|
-
primary: "#8B9DAF", text: "#C5C5BB", muted: "#7A7A72",
|
|
135
|
-
success: "#9CAF8B", warning: "#C5B88D", error: "#B08A8A", border: "#6B6B63",
|
|
136
|
-
};
|
|
137
|
-
const MAX_SAT = 0.28;
|
|
138
|
-
/** Entry line left prefix: icon + space + status dot + space */
|
|
139
|
-
const LEFT_PAD = 4;
|
|
140
|
-
/** Detail row indent: two spaces */
|
|
141
|
-
const INDENT = 2;
|
|
142
|
-
function safeErrorMsg(err) {
|
|
143
|
-
if (!err)
|
|
144
|
-
return "";
|
|
145
|
-
if (typeof err === "string")
|
|
146
|
-
return err;
|
|
147
|
-
if (typeof err === "object")
|
|
148
|
-
return String(err.message || err.code || "");
|
|
149
|
-
return "";
|
|
150
|
-
}
|
|
151
|
-
// ===================================================================
|
|
152
|
-
// Sidebar component
|
|
153
|
-
// ===================================================================
|
|
154
|
-
// 模块级缓存:各 session 的 entry 状态独立存储,不随当前视图切换而清除。
|
|
155
|
-
const globalEntryCache = new Map();
|
|
156
|
-
// 模块级刷新信号:外部(如斜杠命令)触发清除后 +1,组件 scan 依赖它以重扫。
|
|
157
|
-
const [clearTick, setClearTick] = createSignal(0);
|
|
158
|
-
function SubAgentPanel(props) {
|
|
159
|
-
const t = createT(() => props.lang());
|
|
160
|
-
// ── session data (single-key, true deletion on cleanup) ──
|
|
161
|
-
const SESSION_DATA_KEY = `${KV_PREFIX}.session_data`;
|
|
162
|
-
const ttlDaysRaw = parseInt(String(props.api.kv.get(`${KV_PREFIX}.ttl_days`, "3")), 10);
|
|
163
|
-
const ttlDays = Number.isNaN(ttlDaysRaw) ? 3 : ttlDaysRaw;
|
|
164
|
-
const TTL_MS = ttlDays * 24 * 60 * 60 * 1000;
|
|
165
|
-
const loadSessionData = () => {
|
|
166
|
-
try {
|
|
167
|
-
const raw = props.api.kv.get(SESSION_DATA_KEY, "{}");
|
|
168
|
-
return JSON.parse(String(raw));
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
return {};
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
const saveSessionData = (data) => {
|
|
175
|
-
try {
|
|
176
|
-
props.api.kv.set(SESSION_DATA_KEY, JSON.stringify(data));
|
|
177
|
-
}
|
|
178
|
-
catch { }
|
|
179
|
-
};
|
|
180
|
-
/** 将任意 session ID 解析为父会话 ID + 是否子会话。
|
|
181
|
-
* 通过 SDK session.get(sid).parentID 判断,无 parentID 即为主会话。 */
|
|
182
|
-
const resolveParent = (sid) => {
|
|
183
|
-
try {
|
|
184
|
-
const session = props.api.state.session.get(sid);
|
|
185
|
-
const parentID = session?.parentID;
|
|
186
|
-
if (parentID)
|
|
187
|
-
return { parentSid: parentID, isChild: true };
|
|
188
|
-
}
|
|
189
|
-
catch { }
|
|
190
|
-
return { parentSid: sid, isChild: false };
|
|
191
|
-
};
|
|
192
|
-
const loadEntries = (sid) => {
|
|
193
|
-
const m = new Map();
|
|
194
|
-
try {
|
|
195
|
-
const { parentSid, isChild } = resolveParent(sid);
|
|
196
|
-
const rec = loadSessionData()[parentSid];
|
|
197
|
-
if (rec) {
|
|
198
|
-
const source = isChild ? rec.children?.[sid]?.entries : rec.entries;
|
|
199
|
-
if (source) {
|
|
200
|
-
for (const e of source)
|
|
201
|
-
m.set(e.id, e);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
catch { }
|
|
206
|
-
return m;
|
|
207
|
-
};
|
|
208
|
-
let persistTimer;
|
|
209
|
-
const persistEntries = (sid, entries) => {
|
|
210
|
-
clearTimeout(persistTimer);
|
|
211
|
-
persistTimer = setTimeout(() => {
|
|
212
|
-
try {
|
|
213
|
-
const data = loadSessionData();
|
|
214
|
-
const { parentSid, isChild } = resolveParent(sid);
|
|
215
|
-
if (isChild) {
|
|
216
|
-
if (!data[parentSid])
|
|
217
|
-
data[parentSid] = { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} };
|
|
218
|
-
if (!data[parentSid].children)
|
|
219
|
-
data[parentSid].children = {};
|
|
220
|
-
if (!data[parentSid].children[sid])
|
|
221
|
-
data[parentSid].children[sid] = { scroll: 0, expanded: "", entries: [] };
|
|
222
|
-
data[parentSid].children[sid] = { ...data[parentSid].children[sid], entries: [...entries.values()] };
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
data[sid] = { ...data[sid], ts: Date.now(), entries: [...entries.values()], children: data[sid]?.children ?? {} };
|
|
226
|
-
}
|
|
227
|
-
saveSessionData(data);
|
|
228
|
-
}
|
|
229
|
-
catch { }
|
|
230
|
-
}, 200);
|
|
231
|
-
};
|
|
232
|
-
const persistScroll = (sid, scroll) => {
|
|
233
|
-
try {
|
|
234
|
-
const data = loadSessionData();
|
|
235
|
-
const { parentSid, isChild } = resolveParent(sid);
|
|
236
|
-
if (isChild) {
|
|
237
|
-
if (!data[parentSid])
|
|
238
|
-
data[parentSid] = { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} };
|
|
239
|
-
if (!data[parentSid].children)
|
|
240
|
-
data[parentSid].children = {};
|
|
241
|
-
if (!data[parentSid].children[sid])
|
|
242
|
-
data[parentSid].children[sid] = { scroll: 0, expanded: "", entries: [] };
|
|
243
|
-
data[parentSid].children[sid] = { ...data[parentSid].children[sid], scroll };
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
data[sid] = { ...data[sid], ts: Date.now(), scroll, children: data[sid]?.children ?? {} };
|
|
247
|
-
}
|
|
248
|
-
saveSessionData(data);
|
|
249
|
-
}
|
|
250
|
-
catch { }
|
|
251
|
-
};
|
|
252
|
-
const persistExpanded = (sid, expanded) => {
|
|
253
|
-
try {
|
|
254
|
-
const data = loadSessionData();
|
|
255
|
-
const { parentSid, isChild } = resolveParent(sid);
|
|
256
|
-
if (isChild) {
|
|
257
|
-
if (!data[parentSid])
|
|
258
|
-
data[parentSid] = { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} };
|
|
259
|
-
if (!data[parentSid].children)
|
|
260
|
-
data[parentSid].children = {};
|
|
261
|
-
if (!data[parentSid].children[sid])
|
|
262
|
-
data[parentSid].children[sid] = { scroll: 0, expanded: "", entries: [] };
|
|
263
|
-
data[parentSid].children[sid] = { ...data[parentSid].children[sid], expanded };
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
data[sid] = { ...data[sid], ts: Date.now(), expanded, children: data[sid]?.children ?? {} };
|
|
267
|
-
}
|
|
268
|
-
saveSessionData(data);
|
|
269
|
-
}
|
|
270
|
-
catch { }
|
|
10
|
+
function createSidebarSlot(api, panelApi, sig) {
|
|
11
|
+
return {
|
|
12
|
+
order: 60,
|
|
13
|
+
slots: {
|
|
14
|
+
sidebar_content(ctx, input) {
|
|
15
|
+
sig.sessionId = input.session_id;
|
|
16
|
+
return (_jsx(SubAgentPanel, { api: panelApi, theme: ctx.theme.current, lang: sig.lang, maxEntries: sig.maxEntries, sortOrder: sig.sortOrder, scrollMode: sig.scrollMode, sessionId: input.session_id }));
|
|
17
|
+
},
|
|
18
|
+
},
|
|
271
19
|
};
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
20
|
+
}
|
|
21
|
+
const tui = async (api) => {
|
|
22
|
+
// ── language ──
|
|
23
|
+
const stored = String(api.kv.get(`${KV_PREFIX}.lang`, ""));
|
|
24
|
+
const initialLang = LANG_META.some((m) => m.code === stored) ? stored : detectLang();
|
|
25
|
+
const [lang, setLang] = createSignal(initialLang);
|
|
26
|
+
const [maxEntries, setMaxEntries] = createSignal(parseInt(String(api.kv.get(`${KV_PREFIX}.max_entries`, "10")), 10) || 10);
|
|
27
|
+
const [sortOrder, setSortOrder] = createSignal(String(api.kv.get(`${KV_PREFIX}.order`, "desc")) === "asc" ? "asc" : "desc");
|
|
28
|
+
const [scrollMode, setScrollMode] = createSignal(String(api.kv.get(`${KV_PREFIX}.scroll_mode`, "wheel")) === "click" ? "click" : "wheel");
|
|
29
|
+
const signals = { lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, sessionId: "" };
|
|
30
|
+
// ── V1 PanelApi adapter: wraps the V1 host API into the shared panel contract ──
|
|
31
|
+
const v1Api = {
|
|
32
|
+
kv: api.kv,
|
|
33
|
+
usage: {
|
|
34
|
+
readSessionTokens: (sid) => {
|
|
35
|
+
if (!sid)
|
|
36
|
+
return undefined;
|
|
37
|
+
try {
|
|
38
|
+
const msgs = api.state.session.messages(sid);
|
|
39
|
+
if (msgs) {
|
|
40
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
41
|
+
const m = msgs[i];
|
|
42
|
+
if (m.role !== "assistant")
|
|
43
|
+
continue;
|
|
44
|
+
const t = m.tokens;
|
|
45
|
+
if (!t || !(Number(t.output) > 0))
|
|
46
|
+
continue;
|
|
47
|
+
const ctx = (Number(t.input) || 0) +
|
|
48
|
+
(Number(t.output) || 0) +
|
|
49
|
+
(Number(t.reasoning) || 0) +
|
|
50
|
+
(Number(t.cache?.read) || 0) +
|
|
51
|
+
(Number(t.cache?.write) || 0);
|
|
52
|
+
if (ctx > 0)
|
|
53
|
+
return ctx;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
283
57
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
saveSessionData(data);
|
|
287
|
-
}
|
|
288
|
-
catch { }
|
|
289
|
-
};
|
|
290
|
-
cleanupOldSessions();
|
|
291
|
-
const [entryMap, setEntryMapRaw] = createSignal(loadEntries(props.sessionId));
|
|
292
|
-
// Wrapped setter — also persists to kv on every mutation
|
|
293
|
-
const setEntryMap = (arg) => {
|
|
294
|
-
setEntryMapRaw((prev) => {
|
|
295
|
-
const next = typeof arg === "function" ? arg(prev) : arg;
|
|
296
|
-
// entry 状态落定(done/error)时立即持久化到 KV,跳过常规 debounce,
|
|
297
|
-
// 确保跨视图的状态一致性。
|
|
298
|
-
let needsImmediateFlush = false;
|
|
299
|
-
for (const [id, entry] of next) {
|
|
300
|
-
const prevEntry = prev.get(id);
|
|
301
|
-
if (prevEntry?.status === "running" && (entry.status === "done" || entry.status === "error" || entry.status === "cancelled")) {
|
|
302
|
-
needsImmediateFlush = true;
|
|
303
|
-
break;
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
304
60
|
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
61
|
+
},
|
|
62
|
+
readSessionCost: (sid) => {
|
|
63
|
+
if (!sid)
|
|
64
|
+
return undefined;
|
|
308
65
|
try {
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
data[props.sessionId] = { ...data[props.sessionId], ts: Date.now(), entries: [...next.values()], children: data[props.sessionId]?.children ?? {} };
|
|
66
|
+
const session = api.state.session.get(sid);
|
|
67
|
+
if (session?.cost != null && session.cost > 0)
|
|
68
|
+
return session.cost;
|
|
69
|
+
const msgs = api.state.session.messages(sid);
|
|
70
|
+
if (!msgs)
|
|
71
|
+
return undefined;
|
|
72
|
+
let total = 0;
|
|
73
|
+
for (const m of msgs) {
|
|
74
|
+
if (m.role === "assistant" && typeof m.cost === "number")
|
|
75
|
+
total += m.cost;
|
|
322
76
|
}
|
|
323
|
-
|
|
77
|
+
return total > 0 ? total : undefined;
|
|
324
78
|
}
|
|
325
|
-
catch {
|
|
326
|
-
|
|
327
|
-
else {
|
|
328
|
-
persistEntries(props.sessionId, next);
|
|
329
|
-
}
|
|
330
|
-
// 同步到模块级缓存,供其他视图读取当前 session 的最新状态
|
|
331
|
-
globalEntryCache.set(props.sessionId, new Map(next));
|
|
332
|
-
return next;
|
|
333
|
-
});
|
|
334
|
-
};
|
|
335
|
-
const [panelWidth, setPanelWidth] = createSignal(28);
|
|
336
|
-
const [open, setOpen] = createSignal((() => { try {
|
|
337
|
-
return props.api.kv.get(`${KV_PREFIX}.open`, true);
|
|
338
|
-
}
|
|
339
|
-
catch {
|
|
340
|
-
return true;
|
|
341
|
-
} })());
|
|
342
|
-
const [expanded, setExpanded] = createSignal((() => {
|
|
343
|
-
try {
|
|
344
|
-
const { parentSid, isChild } = resolveParent(props.sessionId);
|
|
345
|
-
const rec = loadSessionData()[parentSid];
|
|
346
|
-
if (rec)
|
|
347
|
-
return isChild ? rec.children?.[props.sessionId]?.expanded || undefined : rec.expanded || undefined;
|
|
348
|
-
}
|
|
349
|
-
catch { }
|
|
350
|
-
return undefined;
|
|
351
|
-
})());
|
|
352
|
-
const [hoveredOpen, setHoveredOpen] = createSignal(undefined);
|
|
353
|
-
const [hoveredDismiss, setHoveredDismiss] = createSignal(undefined);
|
|
354
|
-
const [hoveredCancel, setHoveredCancel] = createSignal(undefined);
|
|
355
|
-
const [hoveredTop, setHoveredTop] = createSignal(false);
|
|
356
|
-
const [hoveredMoreAbove, setHoveredMoreAbove] = createSignal(false);
|
|
357
|
-
const [hoveredMoreBelow, setHoveredMoreBelow] = createSignal(false);
|
|
358
|
-
const [scrollOffset, setScrollOffset] = createSignal((() => {
|
|
359
|
-
try {
|
|
360
|
-
const { parentSid, isChild } = resolveParent(props.sessionId);
|
|
361
|
-
const rec = loadSessionData()[parentSid];
|
|
362
|
-
return isChild ? rec?.children?.[props.sessionId]?.scroll ?? 0 : rec?.scroll ?? 0;
|
|
363
|
-
}
|
|
364
|
-
catch {
|
|
365
|
-
return 0;
|
|
366
|
-
}
|
|
367
|
-
})());
|
|
368
|
-
const [now, setNow] = createSignal(Date.now());
|
|
369
|
-
const [renderTick, setRenderTick] = createSignal(0);
|
|
370
|
-
let boxEl;
|
|
371
|
-
let disposed = false;
|
|
372
|
-
/** Total usage tokens for a sub-agent session.
|
|
373
|
-
* Matches opencode's bottom status bar usage: last assistant message with
|
|
374
|
-
* output > 0, summing input + output + reasoning + cache read/write. */
|
|
375
|
-
const readSessionTokens = (sid) => {
|
|
376
|
-
if (!sid)
|
|
377
|
-
return undefined;
|
|
378
|
-
try {
|
|
379
|
-
const msgs = props.api.state.session.messages(sid);
|
|
380
|
-
if (msgs) {
|
|
381
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
382
|
-
const m = msgs[i];
|
|
383
|
-
if (m.role !== "assistant")
|
|
384
|
-
continue;
|
|
385
|
-
const t = m.tokens;
|
|
386
|
-
if (!t || !(Number(t.output) > 0))
|
|
387
|
-
continue;
|
|
388
|
-
const ctx = (Number(t.input) || 0) +
|
|
389
|
-
(Number(t.output) || 0) +
|
|
390
|
-
(Number(t.reasoning) || 0) +
|
|
391
|
-
(Number(t.cache?.read) || 0) +
|
|
392
|
-
(Number(t.cache?.write) || 0);
|
|
393
|
-
if (ctx > 0)
|
|
394
|
-
return ctx;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
return undefined;
|
|
398
|
-
}
|
|
399
|
-
catch {
|
|
400
|
-
return undefined;
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
/** Sum USD cost from a session's messages.
|
|
404
|
-
* Prefers the database-level aggregate (`session.cost`) which is not affected
|
|
405
|
-
* by the sync layer's `limit: 100` message window. Falls back to message
|
|
406
|
-
* traversal when the aggregate is unavailable (older SDK versions). */
|
|
407
|
-
const readSessionCost = (sid) => {
|
|
408
|
-
if (!sid)
|
|
409
|
-
return undefined;
|
|
410
|
-
try {
|
|
411
|
-
const session = props.api.state.session.get(sid);
|
|
412
|
-
if (session?.cost != null && session.cost > 0)
|
|
413
|
-
return session.cost;
|
|
414
|
-
const msgs = props.api.state.session.messages(sid);
|
|
415
|
-
if (!msgs)
|
|
416
|
-
return undefined;
|
|
417
|
-
let total = 0;
|
|
418
|
-
for (const m of msgs) {
|
|
419
|
-
if (m.role === "assistant" && typeof m.cost === "number")
|
|
420
|
-
total += m.cost;
|
|
421
|
-
}
|
|
422
|
-
return total > 0 ? total : undefined;
|
|
423
|
-
}
|
|
424
|
-
catch {
|
|
425
|
-
return undefined;
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
/** Last assistant message's modelID for a sub-agent session. */
|
|
429
|
-
const readSessionModel = (sid) => {
|
|
430
|
-
if (!sid)
|
|
431
|
-
return undefined;
|
|
432
|
-
try {
|
|
433
|
-
const msgs = props.api.state.session.messages(sid);
|
|
434
|
-
if (msgs) {
|
|
435
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
436
|
-
const m = msgs[i];
|
|
437
|
-
if (m.role === "assistant" && m.modelID)
|
|
438
|
-
return String(m.modelID);
|
|
79
|
+
catch {
|
|
80
|
+
return undefined;
|
|
439
81
|
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
};
|
|
447
|
-
/** Todo completion stats for a sub-agent session.
|
|
448
|
-
* `done` counts completed + cancelled items. */
|
|
449
|
-
const readSessionTodo = (sid) => {
|
|
450
|
-
if (!sid)
|
|
451
|
-
return undefined;
|
|
452
|
-
try {
|
|
453
|
-
const todos = props.api.state.session.todo(sid);
|
|
454
|
-
if (!todos || todos.length === 0)
|
|
455
|
-
return undefined;
|
|
456
|
-
let done = 0;
|
|
457
|
-
for (const t of todos) {
|
|
458
|
-
if (t.status === "completed" || t.status === "cancelled")
|
|
459
|
-
done++;
|
|
460
|
-
}
|
|
461
|
-
return { total: todos.length, done };
|
|
462
|
-
}
|
|
463
|
-
catch {
|
|
464
|
-
return undefined;
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
// ── upsert ──
|
|
468
|
-
const upsertEntry = (partial) => {
|
|
469
|
-
setEntryMap((prev) => {
|
|
470
|
-
const existing = prev.get(partial.id);
|
|
471
|
-
const next = new Map(prev);
|
|
472
|
-
const nowTs = Date.now();
|
|
473
|
-
const e = partial.status;
|
|
474
|
-
const ended = e === "done" || e === "error" || e === "cancelled";
|
|
475
|
-
next.set(partial.id, {
|
|
476
|
-
...(existing ?? { startedAt: nowTs }),
|
|
477
|
-
...partial,
|
|
478
|
-
startedAt: existing?.startedAt || partial.startedAt || nowTs,
|
|
479
|
-
endedAt: ended ? (existing?.endedAt || nowTs) : undefined,
|
|
480
|
-
});
|
|
481
|
-
return next;
|
|
482
|
-
});
|
|
483
|
-
};
|
|
484
|
-
// ── cancel helpers ──
|
|
485
|
-
const isDescendantOf = (childId, rootId) => {
|
|
486
|
-
const visited = new Set();
|
|
487
|
-
try {
|
|
488
|
-
let current = props.api.state.session.get(childId);
|
|
489
|
-
while (current?.parentID) {
|
|
490
|
-
if (visited.has(current.id))
|
|
491
|
-
return false;
|
|
492
|
-
visited.add(current.id);
|
|
493
|
-
if (current.parentID === rootId)
|
|
494
|
-
return true;
|
|
495
|
-
current = props.api.state.session.get(current.parentID);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
catch { }
|
|
499
|
-
return false;
|
|
500
|
-
};
|
|
501
|
-
const settleOnIdle = (entry) => {
|
|
502
|
-
if (entry.status === "cancel_requested" && entry.abortAccepted)
|
|
503
|
-
return "cancelled";
|
|
504
|
-
return "done";
|
|
505
|
-
};
|
|
506
|
-
const cancelEntry = async (entry) => {
|
|
507
|
-
const childId = entry.sessionId;
|
|
508
|
-
if (!childId) {
|
|
509
|
-
props.api.ui.toast({
|
|
510
|
-
title: entry.title || entry.agent,
|
|
511
|
-
message: t("cancel.label") + ": " + t("cancel.no_session"),
|
|
512
|
-
});
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
try {
|
|
516
|
-
const child = props.api.state.session.get(childId);
|
|
517
|
-
if (!child?.parentID) {
|
|
518
|
-
props.api.ui.toast({
|
|
519
|
-
title: entry.title || entry.agent,
|
|
520
|
-
message: t("cancel.label") + ": " + t("cancel.not_child"),
|
|
521
|
-
});
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
catch {
|
|
526
|
-
props.api.ui.toast({
|
|
527
|
-
title: entry.title || entry.agent,
|
|
528
|
-
message: t("cancel.label") + ": " + t("cancel.read_error"),
|
|
529
|
-
});
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
if (!isDescendantOf(childId, props.sessionId)) {
|
|
533
|
-
props.api.ui.toast({
|
|
534
|
-
title: entry.title || entry.agent,
|
|
535
|
-
message: t("cancel.label") + ": " + t("cancel.outside_tree"),
|
|
536
|
-
});
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
try {
|
|
540
|
-
const st = props.api.state.session.status(childId);
|
|
541
|
-
if (st?.type !== "busy") {
|
|
542
|
-
const tokens = readSessionTokens(childId);
|
|
543
|
-
const cost = readSessionCost(childId);
|
|
544
|
-
upsertEntry({
|
|
545
|
-
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
546
|
-
status: "done", sessionId: entry.sessionId,
|
|
547
|
-
tokens, cost,
|
|
548
|
-
});
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
catch {
|
|
553
|
-
props.api.ui.toast({
|
|
554
|
-
title: entry.title || entry.agent,
|
|
555
|
-
message: t("cancel.label") + ": " + t("cancel.status_error"),
|
|
556
|
-
});
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
upsertEntry({
|
|
560
|
-
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
561
|
-
status: "cancel_requested", sessionId: entry.sessionId,
|
|
562
|
-
cancelRequestedAt: Date.now(), abortAccepted: false, cancelReason: "manual",
|
|
563
|
-
});
|
|
564
|
-
try {
|
|
565
|
-
await props.api.client.session.abort({ sessionID: childId });
|
|
566
|
-
upsertEntry({
|
|
567
|
-
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
568
|
-
status: "cancel_requested", sessionId: entry.sessionId,
|
|
569
|
-
abortAccepted: true,
|
|
570
|
-
});
|
|
571
|
-
props.api.ui.toast({ message: t("cancel.label") + ": " + t("cancel.sent") });
|
|
572
|
-
}
|
|
573
|
-
catch (err) {
|
|
574
|
-
upsertEntry({
|
|
575
|
-
id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
|
|
576
|
-
status: "error", sessionId: entry.sessionId,
|
|
577
|
-
error: String(err),
|
|
578
|
-
});
|
|
579
|
-
props.api.ui.toast({
|
|
580
|
-
title: entry.title || entry.agent,
|
|
581
|
-
message: t("cancel.label") + ": " + t("cancel.failed"),
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
};
|
|
585
|
-
// ── event handlers ──
|
|
586
|
-
const handlePartUpdated = (event) => {
|
|
587
|
-
const e = event;
|
|
588
|
-
const props_ = e.properties;
|
|
589
|
-
const part = props_?.part;
|
|
590
|
-
if (!part)
|
|
591
|
-
return;
|
|
592
|
-
// SubtaskPart
|
|
593
|
-
if (part.type === "subtask") {
|
|
594
|
-
const agent = String(part.agent ?? "?");
|
|
595
|
-
const prompt = String(part.prompt ?? "");
|
|
596
|
-
const desc = String(part.description ?? "");
|
|
597
|
-
const title = desc || truncate(prompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim(), 40);
|
|
598
|
-
const id = `sub:${String(part.id ?? crypto.randomUUID())}`;
|
|
599
|
-
const subSid = part.sessionID !== undefined ? String(part.sessionID) : undefined;
|
|
600
|
-
const partModel = part.model;
|
|
601
|
-
const modelId = partModel?.modelID ? String(partModel.modelID) : undefined;
|
|
602
|
-
upsertEntry({ id, title, agent, prompt, sessionId: subSid, status: "running", model: modelId });
|
|
603
|
-
}
|
|
604
|
-
// ToolPart
|
|
605
|
-
if (part.type === "tool") {
|
|
606
|
-
const tool = String(part.tool ?? "");
|
|
607
|
-
if (!SUBAGENT_TOOLS.has(tool))
|
|
608
|
-
return;
|
|
609
|
-
const st = part.state;
|
|
610
|
-
const rawStatus = String(st?.status ?? "");
|
|
611
|
-
// Only create entries for tool calls that actually entered execution.
|
|
612
|
-
// "pending" / empty → state unknown yet, wait for next event
|
|
613
|
-
if (rawStatus === "pending" || rawStatus === "")
|
|
614
|
-
return;
|
|
615
|
-
// "error" → tool call failed, sub-agent never spawned.
|
|
616
|
-
// Only update an existing entry (e.g. previously running → now error),
|
|
617
|
-
// never create a new one.
|
|
618
|
-
if (rawStatus === "error") {
|
|
619
|
-
const id = `tool:${String(part.id ?? "")}`;
|
|
620
|
-
if (!part.id)
|
|
621
|
-
return;
|
|
622
|
-
const existing = entryMap().get(id);
|
|
623
|
-
if (existing) {
|
|
624
|
-
upsertEntry({ id, title: existing.title, agent: existing.agent, prompt: existing.prompt, status: "error" });
|
|
625
|
-
}
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
// rawStatus is "running" or "completed" — tool entered execution, track it.
|
|
629
|
-
const input = st?.input;
|
|
630
|
-
let status = "running";
|
|
631
|
-
if (rawStatus === "completed")
|
|
632
|
-
status = "done";
|
|
633
|
-
// Background tasks: tool completion ≠ agent completion — keep running until session.idle
|
|
634
|
-
// Only keep running if state metadata confirms a child session was spawned;
|
|
635
|
-
// otherwise (failed spawn, invalid agent) mark as done so the entry isn't stuck forever.
|
|
636
|
-
if ((input?.run_in_background === true || input?.background === true) && status === "done") {
|
|
637
|
-
const stMetaCheck = st?.metadata;
|
|
638
|
-
const hasChild = stMetaCheck?.session_id !== undefined || stMetaCheck?.sessionId !== undefined;
|
|
639
|
-
if (hasChild)
|
|
640
|
-
status = "running";
|
|
641
|
-
}
|
|
642
|
-
const agent = String(part.subagent_type ?? input?.subagent_type ?? input?.category ?? tool);
|
|
643
|
-
const prompt = String(input?.prompt ?? part.description ?? "");
|
|
644
|
-
const desc = input?.description !== undefined ? String(input.description) : "";
|
|
645
|
-
const title = desc || truncate(prompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim(), 40);
|
|
646
|
-
const id = `tool:${String(part.id ?? crypto.randomUUID())}`;
|
|
647
|
-
// Child session ID lives in state-level metadata (ToolStateCompleted.metadata),
|
|
648
|
-
// injected by the tool executor. ToolPart.sessionID is the parent session.
|
|
649
|
-
const stMeta = st?.metadata;
|
|
650
|
-
const subSid = stMeta?.session_id !== undefined ? String(stMeta.session_id)
|
|
651
|
-
: stMeta?.sessionId !== undefined ? String(stMeta.sessionId)
|
|
652
|
-
: undefined;
|
|
653
|
-
upsertEntry({ id, title, agent, prompt, sessionId: subSid, status });
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
|
-
const handleSessionEnd = (event, status) => {
|
|
657
|
-
const e = event;
|
|
658
|
-
const props_ = e.properties;
|
|
659
|
-
const sid = String(props_?.sessionID ?? "");
|
|
660
|
-
if (!sid)
|
|
661
|
-
return;
|
|
662
|
-
const sessionTokens = readSessionTokens(sid);
|
|
663
|
-
const sessionCost = readSessionCost(sid);
|
|
664
|
-
const sessionModel = readSessionModel(sid);
|
|
665
|
-
const sessionTodo = readSessionTodo(sid);
|
|
666
|
-
let sessionAgent;
|
|
667
|
-
let errorMsg;
|
|
668
|
-
try {
|
|
669
|
-
const s = props.api.state.session.get(sid);
|
|
670
|
-
sessionAgent = s?.agent;
|
|
671
|
-
if (status === "error") {
|
|
672
|
-
const evtErr = props_?.error;
|
|
673
|
-
errorMsg = safeErrorMsg(evtErr) || safeErrorMsg(props_?.message);
|
|
674
|
-
if (!errorMsg) {
|
|
675
|
-
const msgs = props.api.state.session.messages(sid);
|
|
82
|
+
},
|
|
83
|
+
readSessionModel: (sid) => {
|
|
84
|
+
if (!sid)
|
|
85
|
+
return undefined;
|
|
86
|
+
try {
|
|
87
|
+
const msgs = api.state.session.messages(sid);
|
|
676
88
|
if (msgs) {
|
|
677
89
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
678
90
|
const m = msgs[i];
|
|
679
|
-
if (m.role === "assistant" && m.
|
|
680
|
-
|
|
681
|
-
break;
|
|
682
|
-
}
|
|
91
|
+
if (m.role === "assistant" && m.modelID)
|
|
92
|
+
return String(m.modelID);
|
|
683
93
|
}
|
|
684
94
|
}
|
|
95
|
+
return undefined;
|
|
685
96
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
catch { }
|
|
689
|
-
// 在给定的 entries Map 中查找并更新匹配的子代理 entry。
|
|
690
|
-
// 返回 true 表示找到并更新了,false 表示未找到。
|
|
691
|
-
const tryMatchAndUpdate = (entriesMap, targetSid, targetStatus, nowTs) => {
|
|
692
|
-
// 精确匹配:sessionId 对得上 + 状态为 running / cancel_requested
|
|
693
|
-
for (const [, entry] of entriesMap) {
|
|
694
|
-
if (entry.sessionId === targetSid && (entry.status === "running" || entry.status === "cancel_requested")) {
|
|
695
|
-
const finalStatus = targetStatus === "error" ? "error" : settleOnIdle(entry);
|
|
696
|
-
entry.status = finalStatus;
|
|
697
|
-
entry.endedAt = nowTs;
|
|
698
|
-
entry.tokens = entry.tokens ?? sessionTokens;
|
|
699
|
-
entry.cost = entry.cost ?? sessionCost;
|
|
700
|
-
entry.model = entry.model ?? sessionModel;
|
|
701
|
-
entry.todoTotal = entry.todoTotal ?? sessionTodo?.total;
|
|
702
|
-
entry.todoDone = entry.todoDone ?? sessionTodo?.done;
|
|
703
|
-
entry.error = errorMsg || entry.error;
|
|
704
|
-
return true;
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
705
99
|
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if (!eaNorm.includes(saNorm) && !saNorm.includes(eaNorm))
|
|
719
|
-
continue;
|
|
720
|
-
const gap = nowTs - (entry.startedAt || 0);
|
|
721
|
-
if (!best || gap > best.gap)
|
|
722
|
-
best = { entry, gap };
|
|
723
|
-
}
|
|
724
|
-
if (!best) {
|
|
725
|
-
for (const [, entry] of entriesMap) {
|
|
726
|
-
if (entry.status !== "running" && entry.status !== "cancel_requested")
|
|
727
|
-
continue;
|
|
728
|
-
if (entry.sessionId)
|
|
729
|
-
continue;
|
|
730
|
-
const gap = nowTs - (entry.startedAt || 0);
|
|
731
|
-
if (!best || gap > best.gap)
|
|
732
|
-
best = { entry, gap };
|
|
100
|
+
},
|
|
101
|
+
readSessionTodo: (sid) => {
|
|
102
|
+
if (!sid)
|
|
103
|
+
return undefined;
|
|
104
|
+
try {
|
|
105
|
+
const todos = api.state.session.todo(sid);
|
|
106
|
+
if (!todos || todos.length === 0)
|
|
107
|
+
return undefined;
|
|
108
|
+
let done = 0;
|
|
109
|
+
for (const t of todos) {
|
|
110
|
+
if (t.status === "completed" || t.status === "cancelled")
|
|
111
|
+
done++;
|
|
733
112
|
}
|
|
113
|
+
return { total: todos.length, done };
|
|
734
114
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
best.entry.status = finalStatus;
|
|
738
|
-
best.entry.endedAt = nowTs;
|
|
739
|
-
best.entry.tokens = best.entry.tokens ?? sessionTokens;
|
|
740
|
-
best.entry.cost = best.entry.cost ?? sessionCost;
|
|
741
|
-
best.entry.model = best.entry.model ?? sessionModel;
|
|
742
|
-
best.entry.todoTotal = best.entry.todoTotal ?? sessionTodo?.total;
|
|
743
|
-
best.entry.todoDone = best.entry.todoDone ?? sessionTodo?.done;
|
|
744
|
-
best.entry.sessionId = targetSid;
|
|
745
|
-
best.entry.error = errorMsg || best.entry.error;
|
|
746
|
-
return true;
|
|
115
|
+
catch {
|
|
116
|
+
return undefined;
|
|
747
117
|
}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
session: {
|
|
121
|
+
get: (sid) => { try {
|
|
122
|
+
return api.state.session.get(sid);
|
|
748
123
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
for (const [id, entry] of next) {
|
|
755
|
-
if (entry.sessionId !== sid)
|
|
756
|
-
continue;
|
|
757
|
-
if (entry.status !== "running" && entry.status !== "done" && entry.status !== "cancel_requested")
|
|
758
|
-
continue;
|
|
759
|
-
// Skip parent session idle — subagent entries belong to child sessions only
|
|
760
|
-
if (sid === props.sessionId)
|
|
761
|
-
continue;
|
|
762
|
-
// For "done" entries (sync tasks completed before session.idle), only backfill tokens/cost
|
|
763
|
-
const alreadySettled = entry.status !== "running" && entry.status !== "cancel_requested";
|
|
764
|
-
const finalStatus = status === "error" ? "error" : settleOnIdle(entry);
|
|
765
|
-
next.set(id, {
|
|
766
|
-
...entry,
|
|
767
|
-
...(alreadySettled ? {} : { status: finalStatus, endedAt: Date.now() }),
|
|
768
|
-
tokens: entry.tokens ?? sessionTokens,
|
|
769
|
-
cost: entry.cost ?? sessionCost,
|
|
770
|
-
model: entry.model ?? sessionModel,
|
|
771
|
-
todoTotal: entry.todoTotal ?? sessionTodo?.total,
|
|
772
|
-
todoDone: entry.todoDone ?? sessionTodo?.done,
|
|
773
|
-
error: errorMsg || entry.error,
|
|
774
|
-
});
|
|
775
|
-
changed = true;
|
|
776
|
-
}
|
|
777
|
-
if (!changed && sessionAgent) {
|
|
778
|
-
const nowTs = Date.now();
|
|
779
|
-
const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
780
|
-
const saNorm = normalize(sessionAgent);
|
|
781
|
-
let best = null;
|
|
782
|
-
// Phase 1: try matching by agent name(agent 名有交集)
|
|
783
|
-
for (const [id, entry] of next) {
|
|
784
|
-
if (entry.status !== "running" && entry.status !== "cancel_requested")
|
|
785
|
-
continue;
|
|
786
|
-
const eaNorm = normalize(entry.agent);
|
|
787
|
-
if (!eaNorm || !saNorm)
|
|
788
|
-
continue;
|
|
789
|
-
if (!eaNorm.includes(saNorm) && !saNorm.includes(eaNorm))
|
|
790
|
-
continue;
|
|
791
|
-
const gap = nowTs - (entry.startedAt || 0);
|
|
792
|
-
if (!best || gap > best.gap)
|
|
793
|
-
best = { id, gap };
|
|
794
|
-
}
|
|
795
|
-
// Phase 2: if agent name has no overlap (e.g. category calls: agent="deep" vs sessionAgent="Sisyphus-Junior"),
|
|
796
|
-
// fall back to time proximity for entries that have no sessionId yet
|
|
797
|
-
if (!best) {
|
|
798
|
-
for (const [id, entry] of next) {
|
|
799
|
-
if (entry.status !== "running" && entry.status !== "cancel_requested")
|
|
800
|
-
continue;
|
|
801
|
-
if (entry.sessionId)
|
|
802
|
-
continue;
|
|
803
|
-
const gap = nowTs - (entry.startedAt || 0);
|
|
804
|
-
if (!best || gap > best.gap)
|
|
805
|
-
best = { id, gap };
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
if (best) {
|
|
809
|
-
const entry = next.get(best.id);
|
|
810
|
-
const finalStatus = status === "error" ? "error" : settleOnIdle(entry);
|
|
811
|
-
next.set(best.id, {
|
|
812
|
-
...entry, status: finalStatus, endedAt: nowTs,
|
|
813
|
-
tokens: sessionTokens || entry.tokens,
|
|
814
|
-
cost: sessionCost || entry.cost,
|
|
815
|
-
sessionId: sid,
|
|
816
|
-
error: errorMsg || entry.error,
|
|
817
|
-
});
|
|
818
|
-
changed = true;
|
|
819
|
-
}
|
|
124
|
+
catch {
|
|
125
|
+
return undefined;
|
|
126
|
+
} },
|
|
127
|
+
status: (sid) => { try {
|
|
128
|
+
return api.state.session.status(sid);
|
|
820
129
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
const sessionObj = props.api.state.session.get(sid);
|
|
827
|
-
const parentSid = sessionObj?.parentID;
|
|
828
|
-
if (parentSid && parentSid !== props.sessionId) {
|
|
829
|
-
// 优先从模块级缓存获取父 session 的 entries,不受当前视图切换影响
|
|
830
|
-
const parentCache = globalEntryCache.get(parentSid);
|
|
831
|
-
const nowTs = Date.now();
|
|
832
|
-
let found = false;
|
|
833
|
-
if (parentCache) {
|
|
834
|
-
found = tryMatchAndUpdate(parentCache, sid, status, nowTs);
|
|
835
|
-
}
|
|
836
|
-
// 缓存未命中时回退到 KV 读取
|
|
837
|
-
if (!found) {
|
|
838
|
-
const data = loadSessionData();
|
|
839
|
-
const rec = data[parentSid];
|
|
840
|
-
if (rec?.entries) {
|
|
841
|
-
const fallbackMap = new Map(rec.entries.map((e) => [e.id, e]));
|
|
842
|
-
found = tryMatchAndUpdate(fallbackMap, sid, status, nowTs);
|
|
843
|
-
if (found) {
|
|
844
|
-
// 回退命中后写入 KV 并回填缓存
|
|
845
|
-
data[parentSid] = { ...rec, ts: nowTs, entries: [...fallbackMap.values()] };
|
|
846
|
-
saveSessionData(data);
|
|
847
|
-
globalEntryCache.set(parentSid, fallbackMap);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
// 将模块级缓存中的最新状态同步到 KV
|
|
852
|
-
if (found && parentCache) {
|
|
853
|
-
const data = loadSessionData();
|
|
854
|
-
data[parentSid] = { ...data[parentSid], ts: nowTs, entries: [...parentCache.values()] };
|
|
855
|
-
saveSessionData(data);
|
|
856
|
-
}
|
|
130
|
+
catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
} },
|
|
133
|
+
messages: (sid) => { try {
|
|
134
|
+
return api.state.session.messages(sid);
|
|
857
135
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if (disposed)
|
|
864
|
-
return;
|
|
865
|
-
const finalTokens = readSessionTokens(sid);
|
|
866
|
-
const finalCost = readSessionCost(sid);
|
|
867
|
-
const finalModel = readSessionModel(sid);
|
|
868
|
-
const finalTodo = readSessionTodo(sid);
|
|
869
|
-
setEntryMap((prev) => {
|
|
870
|
-
let changed = false;
|
|
871
|
-
const next = new Map(prev);
|
|
872
|
-
for (const [id, entry] of next) {
|
|
873
|
-
if (entry.sessionId !== sid)
|
|
874
|
-
continue;
|
|
875
|
-
const t = finalTokens ?? entry.tokens;
|
|
876
|
-
const c = finalCost ?? entry.cost;
|
|
877
|
-
const m = finalModel ?? entry.model;
|
|
878
|
-
const tt = finalTodo?.total ?? entry.todoTotal;
|
|
879
|
-
const td = finalTodo?.done ?? entry.todoDone;
|
|
880
|
-
if (t !== entry.tokens || c !== entry.cost || m !== entry.model ||
|
|
881
|
-
tt !== entry.todoTotal || td !== entry.todoDone) {
|
|
882
|
-
next.set(id, { ...entry, tokens: t, cost: c, model: m, todoTotal: tt, todoDone: td });
|
|
883
|
-
changed = true;
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
return changed ? next : prev;
|
|
887
|
-
});
|
|
888
|
-
bump();
|
|
889
|
-
}, 150);
|
|
890
|
-
};
|
|
891
|
-
// ── bumpRenderTick: force re-render (visual-cache pattern) ──
|
|
892
|
-
const bump = () => setRenderTick((v) => v + 1);
|
|
893
|
-
onMount(() => {
|
|
894
|
-
// Fast clock for smooth time display, separate from token polling
|
|
895
|
-
const clock = setInterval(() => { setNow(Date.now()); bump(); }, 100);
|
|
896
|
-
// Token poll — runs every 500ms for running entries
|
|
897
|
-
const tokenTimer = setInterval(() => {
|
|
898
|
-
untrack(() => {
|
|
899
|
-
setEntryMapRaw((prev) => {
|
|
900
|
-
let changed = false;
|
|
901
|
-
const next = new Map(prev);
|
|
902
|
-
for (const [id, entry] of next) {
|
|
903
|
-
if (entry.status === "running" && entry.sessionId) {
|
|
904
|
-
// Only read from child sessions, never the parent
|
|
905
|
-
let isChild = false;
|
|
906
|
-
try {
|
|
907
|
-
const s = props.api.state.session.get(entry.sessionId);
|
|
908
|
-
isChild = s?.parentID === props.sessionId;
|
|
909
|
-
}
|
|
910
|
-
catch { }
|
|
911
|
-
if (!isChild)
|
|
912
|
-
continue;
|
|
913
|
-
const total = readSessionTokens(entry.sessionId);
|
|
914
|
-
const todo = readSessionTodo(entry.sessionId);
|
|
915
|
-
const model = entry.model ?? readSessionModel(entry.sessionId);
|
|
916
|
-
const nextEntry = { ...entry };
|
|
917
|
-
if (total !== undefined && total !== entry.tokens) {
|
|
918
|
-
nextEntry.tokens = total;
|
|
919
|
-
changed = true;
|
|
920
|
-
}
|
|
921
|
-
if (todo !== undefined) {
|
|
922
|
-
if (todo.total !== entry.todoTotal || todo.done !== entry.todoDone) {
|
|
923
|
-
nextEntry.todoTotal = todo.total;
|
|
924
|
-
nextEntry.todoDone = todo.done;
|
|
925
|
-
changed = true;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
if (model && !entry.model) {
|
|
929
|
-
nextEntry.model = model;
|
|
930
|
-
changed = true;
|
|
931
|
-
}
|
|
932
|
-
if (changed)
|
|
933
|
-
next.set(id, nextEntry);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
return changed ? next : prev;
|
|
937
|
-
});
|
|
938
|
-
});
|
|
939
|
-
bump();
|
|
940
|
-
}, 500);
|
|
941
|
-
bump();
|
|
942
|
-
const unsubPart = props.api.event.on("message.part.updated", (e) => {
|
|
943
|
-
handlePartUpdated(e);
|
|
944
|
-
bump();
|
|
945
|
-
});
|
|
946
|
-
const unsubMsg = props.api.event.on("message.updated", () => bump());
|
|
947
|
-
const unsubIdle = props.api.event.on("session.idle", (e) => {
|
|
948
|
-
handleSessionEnd(e, "done");
|
|
949
|
-
bump();
|
|
950
|
-
});
|
|
951
|
-
const unsubError = props.api.event.on("session.error", (e) => {
|
|
952
|
-
handleSessionEnd(e, "error");
|
|
953
|
-
bump();
|
|
954
|
-
});
|
|
955
|
-
onCleanup(() => {
|
|
956
|
-
disposed = true;
|
|
957
|
-
clearInterval(clock);
|
|
958
|
-
clearInterval(tokenTimer);
|
|
959
|
-
unsubPart();
|
|
960
|
-
unsubMsg();
|
|
961
|
-
unsubIdle();
|
|
962
|
-
unsubError();
|
|
963
|
-
});
|
|
964
|
-
});
|
|
965
|
-
// ── session‑switch & initial‑load scan ──
|
|
966
|
-
// On session change: load from kv (entries survive component unmount), then scan+merge.
|
|
967
|
-
// On same session: only scan+merge (keep event‑driven running entries).
|
|
968
|
-
let lastSid = props.sessionId;
|
|
969
|
-
let lastTick = 0;
|
|
970
|
-
createEffect(() => {
|
|
971
|
-
const sid = props.sessionId;
|
|
972
|
-
const switched = sid !== lastSid;
|
|
973
|
-
lastSid = sid;
|
|
974
|
-
const tick = clearTick(); // 外部触发清除时 +1,effect 重跑
|
|
975
|
-
const forceReload = tick !== lastTick && !switched;
|
|
976
|
-
lastTick = tick;
|
|
977
|
-
const t = setTimeout(() => {
|
|
978
|
-
untrack(() => {
|
|
979
|
-
if (switched) {
|
|
980
|
-
const { parentSid, isChild } = resolveParent(sid);
|
|
981
|
-
const data = loadSessionData();
|
|
982
|
-
const saved = isChild
|
|
983
|
-
? data[parentSid]?.children?.[sid]?.scroll ?? 0
|
|
984
|
-
: data[sid]?.scroll ?? 0;
|
|
985
|
-
setScrollOffset(saved);
|
|
986
|
-
// 刷新父会话的访问时间 TTL,防止活跃会话的数据过期
|
|
987
|
-
if (!isChild && data[sid]?.entries?.length) {
|
|
988
|
-
data[sid].ts = Date.now();
|
|
989
|
-
saveSessionData(data);
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
// scan uses setEntryMapRaw — ephemeral data, not persisted to kv.
|
|
993
|
-
// Only event-driven changes (handlePartUpdated, handleSessionEnd) persist.
|
|
994
|
-
setEntryMapRaw((prev) => {
|
|
995
|
-
// 优先从模块级缓存加载,KV 仅作缓存未命中时的回退
|
|
996
|
-
const next = (switched || forceReload)
|
|
997
|
-
? new Map(globalEntryCache.get(sid) ?? loadEntries(sid))
|
|
998
|
-
: new Map(prev);
|
|
999
|
-
// 从 KV 加载当前会话的清除名单,扫描时跳过被手动清除的历史条目
|
|
1000
|
-
const { parentSid: scanPSid, isChild: scanChild } = resolveParent(sid);
|
|
1001
|
-
const scanRec = loadSessionData()[scanPSid];
|
|
1002
|
-
const clearedIds = new Set(scanChild ? scanRec?.children?.[sid]?.clearedIds : scanRec?.clearedIds);
|
|
1003
|
-
try {
|
|
1004
|
-
const msgs = props.api.state.session.messages(sid);
|
|
1005
|
-
if (msgs && msgs.length) {
|
|
1006
|
-
for (const msg of msgs) {
|
|
1007
|
-
const parts = props.api.state.part(msg.id) ?? [];
|
|
1008
|
-
for (const partRaw of parts) {
|
|
1009
|
-
const part = partRaw;
|
|
1010
|
-
// Subtask entries are purely event-driven — never created by scan.
|
|
1011
|
-
// (SubtaskPart exists from spawn, not completion, so we cannot infer status.)
|
|
1012
|
-
if (part.type === "tool") {
|
|
1013
|
-
const tool = String(part.tool ?? "");
|
|
1014
|
-
if (!SUBAGENT_TOOLS.has(tool))
|
|
1015
|
-
continue;
|
|
1016
|
-
const id = `tool:${String(part.id ?? "")}`;
|
|
1017
|
-
if (!part.id)
|
|
1018
|
-
continue;
|
|
1019
|
-
const st = part.state;
|
|
1020
|
-
const rawStatus = String(st?.status ?? "");
|
|
1021
|
-
const exists = next.get(id);
|
|
1022
|
-
// 已手动清除的条目:scan 发现但不在内存 → 跳过重建
|
|
1023
|
-
if (!exists && clearedIds.has(id))
|
|
1024
|
-
continue;
|
|
1025
|
-
// Only create entries for tool calls that entered execution.
|
|
1026
|
-
// "pending" / empty: skip new entries; allow heuristics for existing ones below.
|
|
1027
|
-
if ((rawStatus === "pending" || rawStatus === "") && !exists)
|
|
1028
|
-
continue;
|
|
1029
|
-
// "error": only update existing, never create a new entry
|
|
1030
|
-
if (rawStatus === "error") {
|
|
1031
|
-
if (exists && exists.status === "running") {
|
|
1032
|
-
next.set(id, { ...exists, status: "error", endedAt: Date.now() });
|
|
1033
|
-
}
|
|
1034
|
-
continue;
|
|
1035
|
-
}
|
|
1036
|
-
let status = "running";
|
|
1037
|
-
if (rawStatus === "completed")
|
|
1038
|
-
status = "done";
|
|
1039
|
-
// Background tasks: tool completion ≠ agent completion — keep running until session.idle
|
|
1040
|
-
// Only keep running if state metadata confirms a child session was spawned.
|
|
1041
|
-
if ((st?.input?.run_in_background === true || st?.input?.background === true) && status === "done") {
|
|
1042
|
-
const scanStMeta = st?.metadata;
|
|
1043
|
-
const scanHasChild = scanStMeta?.session_id !== undefined || scanStMeta?.sessionId !== undefined;
|
|
1044
|
-
if (scanHasChild)
|
|
1045
|
-
status = "running";
|
|
1046
|
-
}
|
|
1047
|
-
// Already settled → skip
|
|
1048
|
-
if (exists && exists.status !== "running" && exists.status !== "cancel_requested")
|
|
1049
|
-
continue;
|
|
1050
|
-
// Running entry with no explicit status improvement from part:
|
|
1051
|
-
// try message-level heuristics first, then time-based fallback.
|
|
1052
|
-
if (exists && status === "running") {
|
|
1053
|
-
if (!rawStatus) {
|
|
1054
|
-
const msgTokens = msg?.tokens;
|
|
1055
|
-
if (msgTokens && (Number(msgTokens.input) > 0 || Number(msgTokens.output) > 0)) {
|
|
1056
|
-
status = "done"; // LLM returned tokens → agent completed
|
|
1057
|
-
}
|
|
1058
|
-
else if (Date.now() - exists.startedAt > 30 * 60 * 1000) {
|
|
1059
|
-
status = "done"; // >30 min idle → assume completed
|
|
1060
|
-
}
|
|
1061
|
-
else {
|
|
1062
|
-
continue;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
else {
|
|
1066
|
-
continue;
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
// If already tracked as running but tool state says completed/error → update
|
|
1070
|
-
// If not tracked → add fresh
|
|
1071
|
-
const input = st?.input;
|
|
1072
|
-
const agent = String(part.subagent_type ?? input?.subagent_type ?? tool);
|
|
1073
|
-
const prompt = String(input?.prompt ?? part.description ?? "");
|
|
1074
|
-
const desc = input?.description !== undefined ? String(input.description) : "";
|
|
1075
|
-
const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40);
|
|
1076
|
-
let tokens;
|
|
1077
|
-
const scanStMeta2 = st?.metadata;
|
|
1078
|
-
const scanSubSid = scanStMeta2?.session_id !== undefined ? String(scanStMeta2.session_id)
|
|
1079
|
-
: scanStMeta2?.sessionId !== undefined ? String(scanStMeta2.sessionId)
|
|
1080
|
-
: undefined;
|
|
1081
|
-
if (scanSubSid)
|
|
1082
|
-
tokens = readSessionTokens(scanSubSid);
|
|
1083
|
-
const ended = status === "done"; // "error" handled above, never reaches here
|
|
1084
|
-
next.set(id, {
|
|
1085
|
-
id, title, agent, prompt,
|
|
1086
|
-
// Preserve existing values (from handleSessionEnd / KV) — scan must not overwrite
|
|
1087
|
-
tokens: exists?.tokens ?? tokens,
|
|
1088
|
-
sessionId: exists?.sessionId ?? scanSubSid,
|
|
1089
|
-
status,
|
|
1090
|
-
startedAt: exists?.startedAt || Date.now(),
|
|
1091
|
-
endedAt: ended ? (exists?.endedAt || Date.now()) : undefined,
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
catch { }
|
|
1099
|
-
return next;
|
|
1100
|
-
});
|
|
1101
|
-
// Reconcile: check running entries against live child session status.
|
|
1102
|
-
// Covers session.idle events missed while user was inside a child session.
|
|
1103
|
-
setEntryMapRaw((prev) => {
|
|
1104
|
-
let changed = false;
|
|
1105
|
-
const next = new Map(prev);
|
|
1106
|
-
for (const [id, entry] of next) {
|
|
1107
|
-
if ((entry.status !== "running" && entry.status !== "cancel_requested") || !entry.sessionId)
|
|
1108
|
-
continue;
|
|
1109
|
-
try {
|
|
1110
|
-
const st = props.api.state.session.status(entry.sessionId);
|
|
1111
|
-
if (!st || st.type !== "idle")
|
|
1112
|
-
continue;
|
|
1113
|
-
const tokens = readSessionTokens(entry.sessionId);
|
|
1114
|
-
const cost = readSessionCost(entry.sessionId);
|
|
1115
|
-
const finalStatus = entry.status === "cancel_requested" && entry.abortAccepted
|
|
1116
|
-
? "cancelled"
|
|
1117
|
-
: "done";
|
|
1118
|
-
next.set(id, {
|
|
1119
|
-
...entry, status: finalStatus, endedAt: Date.now(),
|
|
1120
|
-
tokens: tokens ?? entry.tokens,
|
|
1121
|
-
cost: cost ?? entry.cost,
|
|
1122
|
-
});
|
|
1123
|
-
changed = true;
|
|
1124
|
-
}
|
|
1125
|
-
catch { }
|
|
1126
|
-
}
|
|
1127
|
-
return changed ? next : prev;
|
|
1128
|
-
});
|
|
1129
|
-
bump();
|
|
1130
|
-
});
|
|
1131
|
-
}, 150);
|
|
1132
|
-
onCleanup(() => clearTimeout(t));
|
|
1133
|
-
});
|
|
1134
|
-
// ── palette ──
|
|
1135
|
-
const pal = createMemo(() => {
|
|
1136
|
-
const th = props.theme;
|
|
1137
|
-
const sat = (k, fb) => desaturateTo(th[k], MAX_SAT, fb);
|
|
1138
|
-
return {
|
|
1139
|
-
primary: sat("primary", FALLBACK.primary),
|
|
1140
|
-
text: sat("text", FALLBACK.text),
|
|
1141
|
-
muted: sat("textMuted", FALLBACK.muted),
|
|
1142
|
-
success: sat("success", FALLBACK.success),
|
|
1143
|
-
warning: sat("warning", FALLBACK.warning),
|
|
1144
|
-
error: sat("error", FALLBACK.error),
|
|
1145
|
-
border: sat("border", FALLBACK.border),
|
|
1146
|
-
};
|
|
1147
|
-
});
|
|
1148
|
-
// ── derived signals ──
|
|
1149
|
-
// Stable list — only changes when entryMap changes
|
|
1150
|
-
const entryList = createMemo(() => {
|
|
1151
|
-
const entries = [...entryMap().values()];
|
|
1152
|
-
if (props.sortOrder() === "desc") {
|
|
1153
|
-
return entries.sort((a, b) => b.startedAt - a.startedAt);
|
|
1154
|
-
}
|
|
1155
|
-
return entries.sort((a, b) => a.startedAt - b.startedAt);
|
|
1156
|
-
});
|
|
1157
|
-
const max = props.maxEntries;
|
|
1158
|
-
const clampedOffset = createMemo(() => {
|
|
1159
|
-
const total = entryList().length;
|
|
1160
|
-
const m = max();
|
|
1161
|
-
if (total <= m)
|
|
1162
|
-
return 0;
|
|
1163
|
-
return Math.min(scrollOffset(), total - m);
|
|
1164
|
-
});
|
|
1165
|
-
const visibleList = createMemo(() => entryList().slice(clampedOffset(), clampedOffset() + max()));
|
|
1166
|
-
const hiddenAbove = createMemo(() => clampedOffset());
|
|
1167
|
-
const hiddenBelow = createMemo(() => Math.max(0, entryList().length - clampedOffset() - max()));
|
|
1168
|
-
// Drop hover state when ↑ more disappears (hiddenAbove hits zero)
|
|
1169
|
-
createEffect(() => {
|
|
1170
|
-
if (hiddenAbove() === 0)
|
|
1171
|
-
setHoveredMoreAbove(false);
|
|
1172
|
-
});
|
|
1173
|
-
// Reset scroll on sort order change: jump to newest in view
|
|
1174
|
-
let sortInitialized = false;
|
|
1175
|
-
createEffect(() => {
|
|
1176
|
-
props.sortOrder();
|
|
1177
|
-
if (!sortInitialized) {
|
|
1178
|
-
sortInitialized = true;
|
|
1179
|
-
return;
|
|
1180
|
-
}
|
|
1181
|
-
const total = untrack(() => entryList().length);
|
|
1182
|
-
const m = untrack(() => max());
|
|
1183
|
-
const target = props.sortOrder() === "desc" ? 0 : Math.max(0, total - m);
|
|
1184
|
-
setScrollOffset(target);
|
|
1185
|
-
setTimeout(() => {
|
|
1186
|
-
try {
|
|
1187
|
-
persistScroll(props.sessionId, target);
|
|
136
|
+
catch {
|
|
137
|
+
return undefined;
|
|
138
|
+
} },
|
|
139
|
+
part: (messageID) => { try {
|
|
140
|
+
return api.state.part(messageID);
|
|
1188
141
|
}
|
|
1189
|
-
catch {
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
? untrack(() => scrollOffset() === 0)
|
|
1205
|
-
: untrack(() => scrollOffset() >= prevEntryCount - m);
|
|
1206
|
-
prevEntryCount = total;
|
|
1207
|
-
if (wasAtNewest) {
|
|
1208
|
-
const target = props.sortOrder() === "desc" ? 0 : Math.max(0, total - m);
|
|
1209
|
-
setScrollOffset(target);
|
|
1210
|
-
setTimeout(() => {
|
|
1211
|
-
try {
|
|
1212
|
-
persistScroll(props.sessionId, target);
|
|
142
|
+
catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
} },
|
|
145
|
+
},
|
|
146
|
+
event: {
|
|
147
|
+
on: (type, cb) => {
|
|
148
|
+
switch (type) {
|
|
149
|
+
case "part.updated":
|
|
150
|
+
return api.event.on("message.part.updated", (e) => cb({ type, payload: { part: e.properties?.part } }));
|
|
151
|
+
case "message.updated":
|
|
152
|
+
return api.event.on("message.updated", () => cb({ type }));
|
|
153
|
+
case "session.idle":
|
|
154
|
+
return api.event.on("session.idle", (e) => cb({ type, payload: e.properties }));
|
|
155
|
+
case "session.error":
|
|
156
|
+
return api.event.on("session.error", (e) => cb({ type, payload: e.properties }));
|
|
1213
157
|
}
|
|
1214
|
-
catch { }
|
|
1215
|
-
}, 0);
|
|
1216
|
-
}
|
|
1217
|
-
});
|
|
1218
|
-
const entries = createMemo(() => {
|
|
1219
|
-
const nowVal = now();
|
|
1220
|
-
return entryList().map((e) => ({
|
|
1221
|
-
...e,
|
|
1222
|
-
elapsed: (e.endedAt ?? nowVal) - e.startedAt,
|
|
1223
|
-
}));
|
|
1224
|
-
});
|
|
1225
|
-
const doneCount = createMemo(() => entryList().filter((e) => e.status === "done" || e.status === "cancelled").length);
|
|
1226
|
-
const runningCount = createMemo(() => entryList().filter((e) => e.status === "running" || e.status === "cancel_requested").length);
|
|
1227
|
-
const errCount = createMemo(() => entryList().filter((e) => e.status === "error").length);
|
|
1228
|
-
const anyEntry = () => entryList().length > 0;
|
|
1229
|
-
const totalTokens = createMemo(() => {
|
|
1230
|
-
let sum = 0;
|
|
1231
|
-
for (const e of entryList()) {
|
|
1232
|
-
if (e.tokens)
|
|
1233
|
-
sum += e.tokens;
|
|
1234
|
-
}
|
|
1235
|
-
return sum;
|
|
1236
|
-
});
|
|
1237
|
-
const totalCost = createMemo(() => {
|
|
1238
|
-
let sum = 0;
|
|
1239
|
-
for (const e of entryList()) {
|
|
1240
|
-
if (e.cost)
|
|
1241
|
-
sum += e.cost;
|
|
1242
|
-
}
|
|
1243
|
-
return sum;
|
|
1244
|
-
});
|
|
1245
|
-
const toggleExpand = (id) => {
|
|
1246
|
-
setExpanded((prev) => {
|
|
1247
|
-
const next = prev === id ? undefined : id;
|
|
1248
|
-
try {
|
|
1249
|
-
persistExpanded(props.sessionId, next ?? "");
|
|
1250
|
-
}
|
|
1251
|
-
catch { }
|
|
1252
|
-
return next;
|
|
1253
|
-
});
|
|
1254
|
-
};
|
|
1255
|
-
const sep = () => "\u2500".repeat(Math.max(1, panelWidth()));
|
|
1256
|
-
// ── expanded detail right-align ──
|
|
1257
|
-
const expandedMaxLabelW = createMemo(() => {
|
|
1258
|
-
const labels = [
|
|
1259
|
-
t("agent.label"), t("status.label"), t("time.label"), t("tokens.label"),
|
|
1260
|
-
t("error.label"), t("cost.label"), t("model.label"), t("todo.label"), t("session.label"),
|
|
1261
|
-
];
|
|
1262
|
-
return Math.max(...labels.map(l => visualWidth(l + ": ")));
|
|
1263
|
-
});
|
|
1264
|
-
const expandedPad = (label) => Math.max(0, expandedMaxLabelW() - visualWidth(label + ": "));
|
|
1265
|
-
const expandedValAvail = () => Math.max(6, panelWidth() - INDENT - expandedMaxLabelW());
|
|
1266
|
-
// ── header parts for colored spans ──
|
|
1267
|
-
const summaryParts = createMemo(() => {
|
|
1268
|
-
if (!anyEntry())
|
|
1269
|
-
return null;
|
|
1270
|
-
const dot = "\u25cf";
|
|
1271
|
-
const cost = totalCost();
|
|
1272
|
-
return {
|
|
1273
|
-
done: `${dot}${doneCount()}`,
|
|
1274
|
-
running: runningCount() > 0 ? `${dot}${runningCount()}` : null,
|
|
1275
|
-
err: errCount() > 0 ? `${dot}${errCount()}` : null,
|
|
1276
|
-
duration: totalTokens() > 0 ? fmtTokens(totalTokens()) : "",
|
|
1277
|
-
cost: cost > 0 ? `$${cost.toFixed(2)}` : "",
|
|
1278
|
-
};
|
|
1279
|
-
});
|
|
1280
|
-
const summaryCols = createMemo(() => {
|
|
1281
|
-
const p = summaryParts();
|
|
1282
|
-
if (!p)
|
|
1283
|
-
return 0;
|
|
1284
|
-
let w = visualWidth(p.done);
|
|
1285
|
-
if (p.running)
|
|
1286
|
-
w += 1 + visualWidth(p.running);
|
|
1287
|
-
if (p.err)
|
|
1288
|
-
w += 1 + visualWidth(p.err);
|
|
1289
|
-
w += p.duration ? 1 + visualWidth(p.duration) : 0;
|
|
1290
|
-
w += p.cost ? 1 + visualWidth(p.cost) : 0;
|
|
1291
|
-
return w;
|
|
1292
|
-
});
|
|
1293
|
-
const versionText = ` v${PLUGIN_VERSION}`;
|
|
1294
|
-
const versionW = visualWidth(versionText);
|
|
1295
|
-
const showVersion = createMemo(() => {
|
|
1296
|
-
if (!open())
|
|
1297
|
-
return false;
|
|
1298
|
-
const icon = "\u25bc";
|
|
1299
|
-
const need = visualWidth(icon) + 1 + visualWidth(t("panel.title")) + versionW + summaryCols();
|
|
1300
|
-
return need <= panelWidth();
|
|
1301
|
-
});
|
|
1302
|
-
const leftCols = createMemo(() => {
|
|
1303
|
-
const icon = open() ? "\u25bc" : "\u25b6";
|
|
1304
|
-
let w = visualWidth(icon) + 1 + visualWidth(t("panel.title"));
|
|
1305
|
-
if (showVersion())
|
|
1306
|
-
w += versionW;
|
|
1307
|
-
return w;
|
|
1308
|
-
});
|
|
1309
|
-
const spacerCols = createMemo(() => {
|
|
1310
|
-
if (!anyEntry())
|
|
1311
|
-
return 0;
|
|
1312
|
-
return Math.max(0, panelWidth() - leftCols() - summaryCols());
|
|
1313
|
-
});
|
|
1314
|
-
const valueCols = (label) => Math.max(4, panelWidth() - INDENT - visualWidth(label + ": "));
|
|
1315
|
-
// ── render ──
|
|
1316
|
-
return (_jsxs("box", { border: false, paddingTop: 0, paddingBottom: 0, paddingLeft: 0, paddingRight: 0, flexDirection: "column", gap: 0, ref: boxEl, onSizeChange: () => {
|
|
1317
|
-
const w = boxEl ? Math.max(20, boxEl.width ?? 0) : 28;
|
|
1318
|
-
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
1319
|
-
}, children: [_jsxs("text", { onMouseUp: () => {
|
|
1320
|
-
setOpen((o) => {
|
|
1321
|
-
const n = !o;
|
|
1322
|
-
try {
|
|
1323
|
-
props.api.kv.set(`${KV_PREFIX}.open`, n);
|
|
1324
|
-
}
|
|
1325
|
-
catch { }
|
|
1326
|
-
return n;
|
|
1327
|
-
});
|
|
1328
|
-
bump();
|
|
1329
|
-
}, children: [_jsx("span", { style: { fg: pal().muted }, children: renderTick() >= 0 && open() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: t("panel.title") }), _jsx(Show, { when: showVersion(), children: _jsx("span", { style: { fg: dimColor(pal().muted, 0.75) }, children: versionText }) }), anyEntry() ? (_jsxs(_Fragment, { children: [_jsx("span", { style: { fg: pal().muted }, children: " ".repeat(spacerCols()) }), _jsx("span", { style: { fg: pal().success }, children: summaryParts().done }), runningCount() > 0 && (_jsxs("span", { style: { fg: pal().warning }, children: [" ", summaryParts().running] })), errCount() > 0 && (_jsxs("span", { style: { fg: pal().error }, children: [" ", summaryParts().err] })), summaryParts().duration ? (_jsxs("span", { style: { fg: pal().muted }, children: [" ", summaryParts().duration] })) : null, summaryParts().cost ? (_jsxs("span", { style: { fg: pal().warning }, children: [" ", summaryParts().cost] })) : null] })) : null] }), _jsxs(Show, { when: open(), children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsx(Show, { when: anyEntry(), fallback: _jsxs("text", { style: { fg: pal().muted }, children: [" ", "> ", t("status.none"), " "] }), children: _jsxs("box", { onMouseScroll: (e) => {
|
|
1330
|
-
if (props.scrollMode() === "click")
|
|
1331
|
-
return;
|
|
1332
|
-
const total = entryList().length;
|
|
1333
|
-
const m = max();
|
|
1334
|
-
if (total <= m)
|
|
1335
|
-
return;
|
|
1336
|
-
const dir = e.button === 0 ? 1 : -1;
|
|
1337
|
-
setScrollOffset((prev) => {
|
|
1338
|
-
const next = Math.max(0, Math.min(prev + dir, total - m));
|
|
1339
|
-
try {
|
|
1340
|
-
persistScroll(props.sessionId, next);
|
|
1341
|
-
}
|
|
1342
|
-
catch { }
|
|
1343
|
-
return next;
|
|
1344
|
-
});
|
|
1345
|
-
}, children: [_jsx(Show, { when: hiddenAbove() > 0, children: _jsx("text", { onMouseOver: () => setHoveredMoreAbove(true), onMouseOut: () => setHoveredMoreAbove(false), onMouseUp: () => {
|
|
1346
|
-
const total = entryList().length;
|
|
1347
|
-
const m = max();
|
|
1348
|
-
if (total <= m)
|
|
1349
|
-
return;
|
|
1350
|
-
const next = Math.max(0, scrollOffset() - m);
|
|
1351
|
-
if (next === 0) {
|
|
1352
|
-
setTimeout(() => {
|
|
1353
|
-
setScrollOffset(next);
|
|
1354
|
-
try {
|
|
1355
|
-
persistScroll(props.sessionId, next);
|
|
1356
|
-
}
|
|
1357
|
-
catch { }
|
|
1358
|
-
}, 0);
|
|
1359
|
-
}
|
|
1360
|
-
else {
|
|
1361
|
-
setScrollOffset(next);
|
|
1362
|
-
try {
|
|
1363
|
-
persistScroll(props.sessionId, next);
|
|
1364
|
-
}
|
|
1365
|
-
catch { }
|
|
1366
|
-
}
|
|
1367
|
-
}, children: _jsxs("span", { style: { fg: hoveredMoreAbove() ? pal().warning : pal().muted }, children: [" ", "\u2191 ", hiddenAbove(), " ", t("scroll.more")] }) }) }), _jsx(For, { each: visibleList(), children: (entry) => {
|
|
1368
|
-
const isExpanded = () => expanded() === entry.id;
|
|
1369
|
-
const isRunning = entry.status === "running";
|
|
1370
|
-
const isCancelRequested = entry.status === "cancel_requested";
|
|
1371
|
-
const isCancelled = entry.status === "cancelled";
|
|
1372
|
-
const isError = entry.status === "error";
|
|
1373
|
-
const isActiveRunning = isRunning || isCancelRequested;
|
|
1374
|
-
const elapsed = () => (entry.endedAt ?? now()) - entry.startedAt;
|
|
1375
|
-
const statusDot = () => "\u25cf";
|
|
1376
|
-
const statusColor = () => {
|
|
1377
|
-
if (isCancelled)
|
|
1378
|
-
return pal().muted;
|
|
1379
|
-
if (!isActiveRunning)
|
|
1380
|
-
return isError ? pal().error : pal().success;
|
|
1381
|
-
const t = (Math.sin(((now() % 2000) / 2000) * Math.PI * 2 - Math.PI / 2) + 1) / 2;
|
|
1382
|
-
const a = rgb(pal().muted), b = rgb(pal().warning);
|
|
1383
|
-
if (!a || !b)
|
|
1384
|
-
return pal().warning;
|
|
1385
|
-
const r = Math.round(a.r + (b.r - a.r) * t);
|
|
1386
|
-
const g = Math.round(a.g + (b.g - a.g) * t);
|
|
1387
|
-
const bl = Math.round(a.b + (b.b - a.b) * t);
|
|
1388
|
-
return "#" + [r, g, bl].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
|
|
1389
|
-
};
|
|
1390
|
-
const timeColor = () => isActiveRunning ? pal().warning : isError ? pal().error : pal().muted;
|
|
1391
|
-
// Entry label: collapsed shows title only, expanded shows title only too
|
|
1392
|
-
const tokenText = () => !isExpanded() && entry.tokens !== undefined && entry.tokens > 0
|
|
1393
|
-
? ` ${fmtTokens(entry.tokens)}`
|
|
1394
|
-
: "";
|
|
1395
|
-
const timeText = () => !isExpanded() && (elapsed() >= 2000 || entry.endedAt !== undefined)
|
|
1396
|
-
? fmtDurationShort(elapsed(), isActiveRunning)
|
|
1397
|
-
: "";
|
|
1398
|
-
const suffixW = () => {
|
|
1399
|
-
let w = 0;
|
|
1400
|
-
const t = timeText();
|
|
1401
|
-
if (t)
|
|
1402
|
-
w += 1 + visualWidth(t);
|
|
1403
|
-
const tk = tokenText();
|
|
1404
|
-
if (tk)
|
|
1405
|
-
w += visualWidth(tk);
|
|
1406
|
-
return w;
|
|
1407
|
-
};
|
|
1408
|
-
const labelAvail = () => Math.max(6, panelWidth() - LEFT_PAD - suffixW());
|
|
1409
|
-
const labelText = () => {
|
|
1410
|
-
const max = labelAvail();
|
|
1411
|
-
const text = entry.title || entry.agent;
|
|
1412
|
-
const truncated = truncate(text, max);
|
|
1413
|
-
const pad = Math.max(0, max - visualWidth(truncated));
|
|
1414
|
-
return truncated + " ".repeat(pad);
|
|
1415
|
-
};
|
|
1416
|
-
return (_jsxs(_Fragment, { children: [_jsxs("text", { onMouseUp: () => toggleExpand(entry.id), children: [_jsx("span", { style: { fg: pal().muted }, children: isExpanded() ? "\u25bc" : "\u25b6" }), " ", _jsx("span", { style: { fg: statusColor() }, children: statusDot() }), " ", _jsx("span", { style: { fg: pal().text }, children: labelText() }), timeText() ? (_jsxs(_Fragment, { children: [" ", _jsx("span", { style: { fg: timeColor() }, children: timeText() })] })) : null, tokenText() ? (_jsx("span", { style: { fg: pal().muted }, children: tokenText() })) : null] }), _jsxs(Show, { when: isExpanded(), children: [_jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("agent.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("agent.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: entry.agent })] }), _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("status.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("status.label"))) }), _jsx("span", { style: { fg: isActiveRunning ? pal().warning : isCancelled ? pal().muted : isError ? pal().error : pal().success }, children: isActiveRunning ? t("status.running") : isCancelled ? t("status.cancelled") : isError ? t("status.error") : t("status.done") })] }), _jsx(Show, { when: elapsed() >= 2000 || entry.endedAt !== undefined, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("time.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("time.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: fmtDurationShort(elapsed(), isActiveRunning) })] }) }), _jsx(Show, { when: entry.tokens !== undefined, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("tokens.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("tokens.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: fmtTokens(entry.tokens) })] }) }), _jsx(Show, { when: entry.error, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().error }, children: [t("error.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("error.label"))) }), _jsx("span", { style: { fg: pal().error }, children: truncate(String(entry.error), expandedValAvail()) })] }) }), _jsx(Show, { when: entry.cost !== undefined, children: (() => {
|
|
1417
|
-
const cost = entry.cost;
|
|
1418
|
-
return (_jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("cost.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("cost.label"))) }), _jsxs("span", { style: { fg: pal().muted }, children: ["$", cost.toFixed(4)] })] }));
|
|
1419
|
-
})() }), _jsx(Show, { when: entry.model, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("model.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("model.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: truncate(entry.model, expandedValAvail()) })] }) }), _jsx(Show, { when: entry.todoTotal !== undefined, children: _jsxs("text", { children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("todo.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("todo.label"))) }), _jsxs("span", { style: { fg: pal().muted }, children: [entry.todoDone, "/", entry.todoTotal] })] }) }), _jsx(Show, { when: entry.sessionId, children: _jsxs("text", { onMouseUp: async () => {
|
|
1420
|
-
const sessionId = entry.sessionId;
|
|
1421
|
-
if (!sessionId)
|
|
1422
|
-
return;
|
|
1423
|
-
const result = await copyText(sessionId);
|
|
1424
|
-
if (result.copied) {
|
|
1425
|
-
props.api.ui.toast({
|
|
1426
|
-
variant: "success",
|
|
1427
|
-
title: entry.title || entry.agent,
|
|
1428
|
-
message: t("session.toast.copied"),
|
|
1429
|
-
duration: 2500,
|
|
1430
|
-
});
|
|
1431
|
-
return;
|
|
1432
|
-
}
|
|
1433
|
-
props.api.ui.toast({
|
|
1434
|
-
variant: "warning",
|
|
1435
|
-
title: entry.title || entry.agent,
|
|
1436
|
-
message: `${sessionId}\n\n${t("session.toast.copy_failed")}`,
|
|
1437
|
-
duration: 8000,
|
|
1438
|
-
});
|
|
1439
|
-
}, children: [" ", _jsxs("span", { style: { fg: pal().primary }, children: [t("session.label"), ": "] }), _jsx("span", { style: { fg: pal().muted }, children: " ".repeat(expandedPad(t("session.label"))) }), _jsx("span", { style: { fg: pal().muted }, children: truncate(entry.sessionId, expandedValAvail() - visualWidth(" ⎘")) }), _jsx("span", { style: { fg: pal().warning }, children: " \u2398" })] }) }), _jsx(Show, { when: entry.sessionId || isRunning, children: (() => {
|
|
1440
|
-
const openPrefix = () => " \u2192 ";
|
|
1441
|
-
const openFull = () => entry.sessionId ? openPrefix() + t("open.label") : "";
|
|
1442
|
-
const openW = () => entry.sessionId ? visualWidth(openFull()) : 0;
|
|
1443
|
-
const cancelLabel = () => ` ${t("cancel.label")}`;
|
|
1444
|
-
const dismissLabel = () => ` ${t("dismiss.label")}`;
|
|
1445
|
-
const rightW = (isRunning ? visualWidth(dismissLabel()) : 0) + (isRunning && entry.sessionId ? visualWidth(cancelLabel()) : 0);
|
|
1446
|
-
const spacerW = () => Math.max(1, panelWidth() - openW() - rightW - 2);
|
|
1447
|
-
return (_jsxs("box", { flexDirection: "row", children: [_jsx(Show, { when: entry.sessionId, children: _jsxs("text", { onMouseOver: () => setHoveredOpen(entry.id), onMouseOut: () => setHoveredOpen(undefined), onMouseUp: () => {
|
|
1448
|
-
if (entry.sessionId) {
|
|
1449
|
-
props.api.route.navigate("session", { sessionID: entry.sessionId });
|
|
1450
|
-
}
|
|
1451
|
-
}, children: [_jsx("span", { style: { fg: hoveredOpen() === entry.id ? pal().warning : pal().primary }, children: openPrefix() }), _jsx("span", { style: { fg: hoveredOpen() === entry.id ? pal().warning : pal().primary }, children: t("open.label") })] }) }), _jsx("text", { style: { fg: pal().muted }, children: " ".repeat(spacerW()) }), _jsx(Show, { when: isRunning && entry.sessionId, children: _jsx("text", { onMouseOver: () => setHoveredCancel(entry.id), onMouseOut: () => setHoveredCancel(undefined), onMouseUp: () => cancelEntry(entry), children: _jsx("span", { style: { fg: hoveredCancel() === entry.id ? pal().warning : pal().error }, children: cancelLabel() }) }) }), _jsx(Show, { when: isRunning, children: _jsx("text", { onMouseOver: () => setHoveredDismiss(entry.id), onMouseOut: () => setHoveredDismiss(undefined), onMouseUp: () => {
|
|
1452
|
-
upsertEntry({ id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt, status: "done" });
|
|
1453
|
-
}, children: _jsx("span", { style: { fg: hoveredDismiss() === entry.id ? pal().warning : pal().muted }, children: dismissLabel() }) }) })] }));
|
|
1454
|
-
})() })] })] }));
|
|
1455
|
-
} }), _jsx(Show, { when: hiddenBelow() > 0 || (props.sortOrder() === "desc" ? scrollOffset() > 0 : entryList().length > max() && clampedOffset() < entryList().length - max()), children: (() => {
|
|
1456
|
-
const showMore = hiddenBelow() > 0;
|
|
1457
|
-
const showTop = props.sortOrder() === "desc"
|
|
1458
|
-
? scrollOffset() > 0
|
|
1459
|
-
: entryList().length > max() && clampedOffset() < entryList().length - max();
|
|
1460
|
-
const left = showMore ? ` \u2193 ${hiddenBelow()} ${t("scroll.more")}` : " ";
|
|
1461
|
-
const right = props.sortOrder() === "desc"
|
|
1462
|
-
? `\u2191 ${t("scroll.top")}`
|
|
1463
|
-
: `\u2193 ${t("scroll.bottom")}`;
|
|
1464
|
-
const pad = showTop ? Math.max(1, panelWidth() - visualWidth(left) - visualWidth(right)) : 0;
|
|
1465
|
-
return (_jsxs("box", { flexDirection: "row", children: [_jsx("text", { onMouseOver: () => showMore && setHoveredMoreBelow(true), onMouseOut: () => setHoveredMoreBelow(false), onMouseUp: () => {
|
|
1466
|
-
if (!showMore)
|
|
1467
|
-
return;
|
|
1468
|
-
const total = entryList().length;
|
|
1469
|
-
const m = max();
|
|
1470
|
-
if (total <= m)
|
|
1471
|
-
return;
|
|
1472
|
-
setScrollOffset((prev) => Math.min(total - m, prev + m));
|
|
1473
|
-
try {
|
|
1474
|
-
persistScroll(props.sessionId, scrollOffset());
|
|
1475
|
-
}
|
|
1476
|
-
catch { }
|
|
1477
|
-
setHoveredMoreBelow(false);
|
|
1478
|
-
}, children: _jsx("span", { style: { fg: showMore && hoveredMoreBelow() ? pal().warning : pal().muted }, children: left }) }), showTop ? (_jsxs(_Fragment, { children: [_jsx("text", { style: { fg: pal().muted }, children: " ".repeat(pad) }), _jsx("text", { onMouseOver: () => setHoveredTop(true), onMouseOut: () => setHoveredTop(false), onMouseUp: () => {
|
|
1479
|
-
const total = entryList().length;
|
|
1480
|
-
const m = max();
|
|
1481
|
-
if (props.sortOrder() === "desc") {
|
|
1482
|
-
setScrollOffset(0);
|
|
1483
|
-
}
|
|
1484
|
-
else {
|
|
1485
|
-
setScrollOffset(Math.max(0, total - m));
|
|
1486
|
-
}
|
|
1487
|
-
setHoveredTop(false);
|
|
1488
|
-
}, children: _jsx("span", { style: { fg: hoveredTop() ? pal().warning : pal().muted }, children: right }) })] })) : null] }));
|
|
1489
|
-
})() })] }) })] })] }));
|
|
1490
|
-
}
|
|
1491
|
-
function createSidebarSlot(api, sig) {
|
|
1492
|
-
return {
|
|
1493
|
-
order: 60,
|
|
1494
|
-
slots: {
|
|
1495
|
-
sidebar_content(ctx, input) {
|
|
1496
|
-
sig.sessionId = input.session_id;
|
|
1497
|
-
return (_jsx(SubAgentPanel, { theme: ctx.theme.current, api: api, lang: sig.lang, maxEntries: sig.maxEntries, sortOrder: sig.sortOrder, scrollMode: sig.scrollMode, sessionId: input.session_id }));
|
|
1498
158
|
},
|
|
1499
159
|
},
|
|
160
|
+
client: {
|
|
161
|
+
abort: (input) => api.client.session.abort(input).then(() => { }),
|
|
162
|
+
},
|
|
163
|
+
route: {
|
|
164
|
+
navigateSession: (sessionID) => api.route.navigate("session", { sessionID }),
|
|
165
|
+
},
|
|
166
|
+
ui: {
|
|
167
|
+
toast: (message, opts) => api.ui.toast({ ...opts, message }),
|
|
168
|
+
},
|
|
169
|
+
settings: {
|
|
170
|
+
lang: () => lang(),
|
|
171
|
+
maxEntries: () => maxEntries(),
|
|
172
|
+
sortOrder: () => sortOrder(),
|
|
173
|
+
scrollMode: () => scrollMode(),
|
|
174
|
+
},
|
|
1500
175
|
};
|
|
1501
|
-
|
|
1502
|
-
const KV_PREFIX = "subagent_magazine";
|
|
1503
|
-
const tui = async (api) => {
|
|
1504
|
-
// ── language ──
|
|
1505
|
-
const stored = String(api.kv.get(`${KV_PREFIX}.lang`, ""));
|
|
1506
|
-
const initialLang = LANG_META.some((m) => m.code === stored) ? stored : detectLang();
|
|
1507
|
-
const [lang, setLang] = createSignal(initialLang);
|
|
1508
|
-
const [maxEntries, setMaxEntries] = createSignal(parseInt(String(api.kv.get(`${KV_PREFIX}.max_entries`, "10")), 10) || 10);
|
|
1509
|
-
const [sortOrder, setSortOrder] = createSignal(String(api.kv.get(`${KV_PREFIX}.order`, "desc")) === "asc" ? "asc" : "desc");
|
|
1510
|
-
const [scrollMode, setScrollMode] = createSignal(String(api.kv.get(`${KV_PREFIX}.scroll_mode`, "wheel")) === "click" ? "click" : "wheel");
|
|
1511
|
-
const signals = { lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, sessionId: "" };
|
|
1512
|
-
api.slots.register(createSidebarSlot(api, signals));
|
|
176
|
+
api.slots.register(createSidebarSlot(api, v1Api, signals));
|
|
1513
177
|
// ── slash command: /subagent-lang ──
|
|
1514
178
|
api.command?.register(() => [
|
|
1515
179
|
{
|