opencode-subagent-magazine 1.1.0 → 1.1.1

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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Hotakus
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hotakus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.1.0";
1
+ export declare const PLUGIN_VERSION = "1.1.1";
package/dist/_version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION = "1.1.0";
2
+ export const PLUGIN_VERSION = "1.1.1";
package/dist/index.js CHANGED
@@ -188,6 +188,9 @@ function safeErrorMsg(err) {
188
188
  // ===================================================================
189
189
  // Sidebar component
190
190
  // ===================================================================
191
+ // 模块级缓存:保留各 session 的最新 entryMap,不随组件内 session 切换而丢失。
192
+ // 当用户在子 session 内部时,handleSessionEnd 可通过此缓存找到父 session 的 entries。
193
+ const globalEntryCache = new Map();
191
194
  function SubAgentPanel(props) {
192
195
  const t = (key) => I18N[props.lang()][key] ?? key;
193
196
  // ── session data (single-key, true deletion on cleanup) ──
@@ -270,7 +273,30 @@ function SubAgentPanel(props) {
270
273
  const setEntryMap = (arg) => {
271
274
  setEntryMapRaw((prev) => {
272
275
  const next = typeof arg === "function" ? arg(prev) : arg;
273
- persistEntries(props.sessionId, next);
276
+ // 检测是否有 entry 从 running 变为 done/error → 立即写 KV
277
+ // 避免 session 切换时 KV 仍是旧状态,导致 scan 用 Date.now() 覆盖正确的 endedAt
278
+ let needsImmediateFlush = false;
279
+ for (const [id, entry] of next) {
280
+ const prevEntry = prev.get(id);
281
+ if (prevEntry?.status === "running" && (entry.status === "done" || entry.status === "error")) {
282
+ needsImmediateFlush = true;
283
+ break;
284
+ }
285
+ }
286
+ if (needsImmediateFlush) {
287
+ clearTimeout(persistTimer);
288
+ try {
289
+ const data = loadSessionData();
290
+ data[props.sessionId] = { ...data[props.sessionId], ts: Date.now(), entries: [...next.values()] };
291
+ saveSessionData(data);
292
+ }
293
+ catch { }
294
+ }
295
+ else {
296
+ persistEntries(props.sessionId, next);
297
+ }
298
+ // 同步到全局缓存,确保跨 session 查找时数据可用
299
+ globalEntryCache.set(props.sessionId, new Map(next));
274
300
  return next;
275
301
  });
276
302
  };
@@ -511,6 +537,66 @@ function SubAgentPanel(props) {
511
537
  }
512
538
  }
513
539
  catch { }
540
+ // 在给定的 entries Map 中查找并更新匹配的子代理 entry。
541
+ // 返回 true 表示找到并更新了,false 表示未找到。
542
+ const tryMatchAndUpdate = (entriesMap, targetSid, targetStatus, nowTs) => {
543
+ // 精确匹配:sessionId 对得上 + 状态为 running
544
+ for (const [, entry] of entriesMap) {
545
+ if (entry.sessionId === targetSid && entry.status === "running") {
546
+ entry.status = targetStatus;
547
+ entry.endedAt = nowTs;
548
+ entry.tokens = entry.tokens ?? sessionTokens;
549
+ entry.cost = entry.cost ?? sessionCost;
550
+ entry.model = entry.model ?? sessionModel;
551
+ entry.todoTotal = entry.todoTotal ?? sessionTodo?.total;
552
+ entry.todoDone = entry.todoDone ?? sessionTodo?.done;
553
+ entry.error = errorMsg || entry.error;
554
+ return true;
555
+ }
556
+ }
557
+ // 回退:sessionId 未关联但 agent 名匹配 + 状态为 running
558
+ if (sessionAgent) {
559
+ const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9-]/g, "");
560
+ const saNorm = normalize(sessionAgent);
561
+ let best = null;
562
+ for (const [, entry] of entriesMap) {
563
+ if (entry.status !== "running")
564
+ continue;
565
+ const eaNorm = normalize(entry.agent);
566
+ if (!eaNorm || !saNorm)
567
+ continue;
568
+ if (!eaNorm.includes(saNorm) && !saNorm.includes(eaNorm))
569
+ continue;
570
+ const gap = nowTs - (entry.startedAt || 0);
571
+ if (!best || gap > best.gap)
572
+ best = { entry, gap };
573
+ }
574
+ if (!best) {
575
+ for (const [, entry] of entriesMap) {
576
+ if (entry.status !== "running")
577
+ continue;
578
+ if (entry.sessionId)
579
+ continue;
580
+ const gap = nowTs - (entry.startedAt || 0);
581
+ if (!best || gap > best.gap)
582
+ best = { entry, gap };
583
+ }
584
+ }
585
+ if (best) {
586
+ best.entry.status = targetStatus;
587
+ best.entry.endedAt = nowTs;
588
+ best.entry.tokens = best.entry.tokens ?? sessionTokens;
589
+ best.entry.cost = best.entry.cost ?? sessionCost;
590
+ best.entry.model = best.entry.model ?? sessionModel;
591
+ best.entry.todoTotal = best.entry.todoTotal ?? sessionTodo?.total;
592
+ best.entry.todoDone = best.entry.todoDone ?? sessionTodo?.done;
593
+ best.entry.sessionId = targetSid;
594
+ best.entry.error = errorMsg || best.entry.error;
595
+ return true;
596
+ }
597
+ }
598
+ return false;
599
+ };
514
600
  setEntryMap((prev) => {
515
601
  let changed = false;
516
602
  const next = new Map(prev);
@@ -581,6 +667,44 @@ function SubAgentPanel(props) {
581
667
  }
582
668
  return changed ? next : prev;
583
669
  });
670
+ // 跨 session 完成事件:用户正在查看子 session 内部时,其他子代理 done。
671
+ // 上面的 setEntryMap 在子 session 的 entries 中找不到父 session 的 entry。
672
+ // 通过全局缓存(切换前保留的父 session entries)查找并更新,再同步回 KV。
673
+ try {
674
+ const sessionObj = props.api.state.session.get(sid);
675
+ const parentSid = sessionObj?.parentID;
676
+ if (parentSid && parentSid !== props.sessionId) {
677
+ // 优先全局缓存——切换 session 时不会被替换,始终保留父 session 的最新 entries
678
+ const parentCache = globalEntryCache.get(parentSid);
679
+ const nowTs = Date.now();
680
+ let found = false;
681
+ if (parentCache) {
682
+ found = tryMatchAndUpdate(parentCache, sid, status, nowTs);
683
+ }
684
+ // 缓存未命中时回退到 KV(极端情况:session 切换前缓存未建立)
685
+ if (!found) {
686
+ const data = loadSessionData();
687
+ const rec = data[parentSid];
688
+ if (rec?.entries) {
689
+ const fallbackMap = new Map(rec.entries.map((e) => [e.id, e]));
690
+ found = tryMatchAndUpdate(fallbackMap, sid, status, nowTs);
691
+ if (found) {
692
+ // 回退命中了也要写回 KV,并回填缓存
693
+ data[parentSid] = { ...rec, ts: nowTs, entries: [...fallbackMap.values()] };
694
+ saveSessionData(data);
695
+ globalEntryCache.set(parentSid, fallbackMap);
696
+ }
697
+ }
698
+ }
699
+ // KV 同步:以全局缓存(最新的内存状态)为准写回 KV
700
+ if (found && parentCache) {
701
+ const data = loadSessionData();
702
+ data[parentSid] = { ...data[parentSid], ts: nowTs, entries: [...parentCache.values()] };
703
+ saveSessionData(data);
704
+ }
705
+ }
706
+ }
707
+ catch { }
584
708
  // Delayed backfill: re-read data after state sync catches up, to capture the final
585
709
  // token/cost values that may not have been available when session.idle fired.
586
710
  setTimeout(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-subagent-magazine",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "OpenCode TUI plugin monitoring sub-agent invocation status in real time",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -39,6 +39,10 @@
39
39
  "subagent",
40
40
  "subtask"
41
41
  ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/Hotakus/opencode-subagent-magazine.git"
45
+ },
42
46
  "license": "MIT",
43
47
  "peerDependencies": {
44
48
  "@opencode-ai/plugin": ">=1.14.0",
package/src/_version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION="1.1.0";
2
+ export const PLUGIN_VERSION="1.1.1";