opencode-subagent-magazine 1.6.0-beta.2 → 1.6.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,7 +56,8 @@
56
56
  - **TTL 自动清理**:可配置数据保留期限(3/7/14/30 天/无期限),每次访问自动续期,过期自动清除
57
57
  - **手动清除条目**:`/subagent-clear-entries` 清除当前会话所有记录,防止历史条目扫描重建
58
58
  - **语言适配**:支持 `/subagent-lang` 运行时切换中/英文,偏好持久化
59
- - **斜杠命令**:`/subagent-lang` `/subagent-max` `/subagent-order` `/subagent-scroll` `/subagent-ttl` `/subagent-clear-entries` `/subagent-session` `/subagent-version` `/subagent-clear-running` 动态配置
59
+ - **可切换边框**:`/subagent-border` 控制面板外边框显隐(默认隐藏),开启后与缓存统计面板风格统一
60
+ - **斜杠命令**:`/subagent-lang` `/subagent-max` `/subagent-order` `/subagent-scroll` `/subagent-ttl` `/subagent-clear-entries` `/subagent-session` `/subagent-version` `/subagent-clear-running` `/subagent-border` 动态配置
60
61
 
61
62
  ---
62
63
 
@@ -114,6 +115,7 @@ npm install -g opencode-subagent-magazine@latest
114
115
  | `/subagent-session` | 查看当前会话 ID | 弹出当前 OpenCode 会话 ID |
115
116
  | `/subagent-version` | 查看插件版本 | 弹出当前插件版本号 |
116
117
  | `/subagent-clear-running` | 批量清理僵尸条目 | 一键将所有运行中的条目标记为完成,清理卡住的旧数据 |
118
+ | `/subagent-border` | 开关面板边框 | 切换面板外边框显隐(默认隐藏),即时生效并持久化 |
117
119
 
118
120
  <div align="center">
119
121
  <img src="https://raw.githubusercontent.com/Hotakus/opencode-subagent-magazine/master/assets/slash_cmds.png" alt="斜杠命令" width="49%"></img>
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.6.0-beta.2";
1
+ export declare const PLUGIN_VERSION = "1.6.0-beta.4";
package/dist/_version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION = "1.6.0-beta.2";
2
+ export const PLUGIN_VERSION = "1.6.0-beta.4";
package/dist/core/kv.d.ts CHANGED
@@ -14,6 +14,7 @@ export declare const SETTING_KEYS: {
14
14
  readonly scrollMode: "subagent_magazine.scroll_mode";
15
15
  readonly open: "subagent_magazine.open";
16
16
  readonly ttlDays: "subagent_magazine.ttl_days";
17
+ readonly border: "subagent_magazine.border";
17
18
  };
18
19
  export declare function loadSessionData(kv: KVApi): Record<string, SessionRecord>;
19
20
  export declare function saveSessionData(kv: KVApi, data: Record<string, SessionRecord>): void;
package/dist/core/kv.js CHANGED
@@ -8,6 +8,7 @@ export const SETTING_KEYS = {
8
8
  scrollMode: `${KV_PREFIX}.scroll_mode`,
9
9
  open: `${KV_PREFIX}.open`,
10
10
  ttlDays: `${KV_PREFIX}.ttl_days`,
11
+ border: `${KV_PREFIX}.border`,
11
12
  };
12
13
  export function loadSessionData(kv) {
13
14
  try {
@@ -47,5 +47,7 @@ export interface SharedSignals {
47
47
  setSortOrder: (o: SortOrder) => void;
48
48
  scrollMode: () => ScrollMode;
49
49
  setScrollMode: (m: ScrollMode) => void;
50
+ borderVisible: () => boolean;
51
+ setBorderVisible: (v: boolean) => void;
50
52
  sessionId: string;
51
53
  }
package/dist/i18n.d.ts CHANGED
@@ -49,6 +49,8 @@ declare const ZH_T: {
49
49
  readonly "cancel.status_error": "无法查询会话状态";
50
50
  readonly "cancel.sent": "已发送取消指令";
51
51
  readonly "cancel.failed": "取消失败";
52
+ readonly borderShown: "面板边框 已显示";
53
+ readonly borderHidden: "面板边框 已隐藏";
52
54
  };
53
55
  /** 结构约束:值放宽为 string,键集合来自中文表(新增语言缺 key 会编译报错)。 */
54
56
  export type Translation = {
package/dist/i18n.js CHANGED
@@ -53,6 +53,8 @@ const ZH_T = {
53
53
  "cancel.status_error": "无法查询会话状态",
54
54
  "cancel.sent": "已发送取消指令",
55
55
  "cancel.failed": "取消失败",
56
+ "borderShown": "面板边框 已显示",
57
+ "borderHidden": "面板边框 已隐藏",
56
58
  };
57
59
  const EN_T = {
58
60
  "panel.title": "SubAgent",
@@ -104,6 +106,8 @@ const EN_T = {
104
106
  "cancel.status_error": "Cannot query session status",
105
107
  "cancel.sent": "Cancel instruction sent",
106
108
  "cancel.failed": "Cancellation failed",
109
+ "borderShown": "Panel border shown",
110
+ "borderHidden": "Panel border hidden",
107
111
  };
108
112
  const JA_T = {
109
113
  "panel.title": "サブエージェント",
@@ -155,6 +159,8 @@ const JA_T = {
155
159
  "cancel.status_error": "セッション状態を照会できません",
156
160
  "cancel.sent": "キャンセル指示を送信しました",
157
161
  "cancel.failed": "キャンセルに失敗しました",
162
+ "borderShown": "パネル枠線 表示",
163
+ "borderHidden": "パネル枠線 非表示",
158
164
  };
159
165
  const KO_T = {
160
166
  "panel.title": "서브 에이전트",
@@ -206,6 +212,8 @@ const KO_T = {
206
212
  "cancel.status_error": "세션 상태를 조회할 수 없습니다",
207
213
  "cancel.sent": "취소 지시를 보냈습니다",
208
214
  "cancel.failed": "취소에 실패했습니다",
215
+ "borderShown": "패널 테두리 표시",
216
+ "borderHidden": "패널 테두리 숨김",
209
217
  };
210
218
  export const LANGS = { zh: ZH_T, en: EN_T, ja: JA_T, ko: KO_T };
211
219
  /** 语言元数据:/subagent-lang 选项与自动检测共用。 */
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ function createSidebarSlot(api, panelApi, sig) {
13
13
  slots: {
14
14
  sidebar_content(ctx, input) {
15
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 }));
16
+ return (_jsx(SubAgentPanel, { api: panelApi, theme: ctx.theme.current, lang: sig.lang, maxEntries: sig.maxEntries, sortOrder: sig.sortOrder, scrollMode: sig.scrollMode, borderVisible: sig.borderVisible, sessionId: input.session_id }));
17
17
  },
18
18
  },
19
19
  };
@@ -26,7 +26,8 @@ const tui = async (api) => {
26
26
  const [maxEntries, setMaxEntries] = createSignal(parseInt(String(api.kv.get(`${KV_PREFIX}.max_entries`, "10")), 10) || 10);
27
27
  const [sortOrder, setSortOrder] = createSignal(String(api.kv.get(`${KV_PREFIX}.order`, "desc")) === "asc" ? "asc" : "desc");
28
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: "" };
29
+ const [borderVisible, setBorderVisible] = createSignal(api.kv.get(`${KV_PREFIX}.border`, false) === true);
30
+ const signals = { lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, borderVisible, setBorderVisible, sessionId: "" };
30
31
  // ── V1 PanelApi adapter: wraps the V1 host API into the shared panel contract ──
31
32
  const v1Api = {
32
33
  kv: api.kv,
@@ -416,6 +417,20 @@ const tui = async (api) => {
416
417
  } })));
417
418
  },
418
419
  },
420
+ {
421
+ title: "SubAgent Magazine: Border",
422
+ value: "subagent-border",
423
+ description: "Show or hide the panel border",
424
+ slash: { name: "subagent-border" },
425
+ onSelect: (dialog) => {
426
+ const t = createT(() => signals.lang());
427
+ const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, false));
428
+ api.kv.set(`${KV_PREFIX}.border`, !cur);
429
+ signals.setBorderVisible(!cur);
430
+ api.ui.toast({ message: !cur ? t("borderShown") : t("borderHidden") });
431
+ dialog?.clear();
432
+ },
433
+ },
419
434
  ]);
420
435
  };
421
436
  const mod = {
@@ -9,5 +9,6 @@ export declare function SubAgentPanel(props: {
9
9
  maxEntries: () => number;
10
10
  sortOrder: () => SortOrder;
11
11
  scrollMode: () => ScrollMode;
12
+ borderVisible: () => boolean;
12
13
  sessionId: string;
13
14
  }): JSX.Element;
@@ -9,6 +9,7 @@ import { rgb, desaturateTo, dimColor, FALLBACK, MAX_SAT } from "../core/color";
9
9
  import { KV_PREFIX } from "../core/kv";
10
10
  import { globalEntryCache, clearTick } from "./store";
11
11
  import { isDirectChildSession } from "./session-routing";
12
+ import { findSubEntryKey, upsertSubEntry } from "./entry-map";
12
13
  /** Entry line left prefix: icon + space + status dot + space */
13
14
  const LEFT_PAD = 4;
14
15
  /** Detail row indent: two spaces */
@@ -51,15 +52,16 @@ export function SubAgentPanel(props) {
51
52
  return { parentSid: sid, isChild: false };
52
53
  };
53
54
  const loadEntries = (sid) => {
54
- const m = new Map();
55
+ let m = new Map();
55
56
  try {
56
57
  const { parentSid, isChild } = resolveParent(sid);
57
58
  const rec = loadSessionData()[parentSid];
58
59
  if (rec) {
59
60
  const source = isChild ? rec.children?.[sid]?.entries : rec.entries;
60
61
  if (source) {
61
- for (const e of source)
62
- m.set(e.id, e);
62
+ for (const e of source) {
63
+ m = upsertSubEntry(m, e);
64
+ }
63
65
  }
64
66
  }
65
67
  }
@@ -232,20 +234,7 @@ export function SubAgentPanel(props) {
232
234
  let disposed = false;
233
235
  // ── upsert ──
234
236
  const upsertEntry = (partial) => {
235
- setEntryMap((prev) => {
236
- const existing = prev.get(partial.id);
237
- const next = new Map(prev);
238
- const nowTs = Date.now();
239
- const e = partial.status;
240
- const ended = e === "done" || e === "error" || e === "cancelled";
241
- next.set(partial.id, {
242
- ...(existing ?? { startedAt: nowTs }),
243
- ...partial,
244
- startedAt: existing?.startedAt || partial.startedAt || nowTs,
245
- endedAt: ended ? (existing?.endedAt || nowTs) : undefined,
246
- });
247
- return next;
248
- });
237
+ setEntryMap((prev) => upsertSubEntry(prev, partial));
249
238
  };
250
239
  // ── cancel helpers ──
251
240
  const isDescendantOf = (childId, rootId) => {
@@ -370,9 +359,21 @@ export function SubAgentPanel(props) {
370
359
  const id = `tool:${String(part.id ?? "")}`;
371
360
  if (!part.id)
372
361
  return;
373
- const existing = entryMap().get(id);
362
+ const stMeta = st?.metadata;
363
+ const errorSid = stMeta?.session_id !== undefined ? String(stMeta.session_id)
364
+ : stMeta?.sessionId !== undefined ? String(stMeta.sessionId)
365
+ : undefined;
366
+ const key = findSubEntryKey(entryMap(), id, errorSid);
367
+ const existing = key ? entryMap().get(key) : undefined;
374
368
  if (existing) {
375
- upsertEntry({ id, title: existing.title, agent: existing.agent, prompt: existing.prompt, status: "error" });
369
+ upsertEntry({
370
+ id: existing.id,
371
+ title: existing.title,
372
+ agent: existing.agent,
373
+ prompt: existing.prompt,
374
+ sessionId: existing.sessionId ?? errorSid,
375
+ status: "error",
376
+ });
376
377
  }
377
378
  return;
378
379
  }
@@ -778,7 +779,12 @@ export function SubAgentPanel(props) {
778
779
  continue;
779
780
  const st = part.state;
780
781
  const rawStatus = String(st?.status ?? "");
781
- const exists = next.get(id);
782
+ const scanStMeta = st?.metadata;
783
+ const scanSubSid = scanStMeta?.session_id !== undefined ? String(scanStMeta.session_id)
784
+ : scanStMeta?.sessionId !== undefined ? String(scanStMeta.sessionId)
785
+ : undefined;
786
+ const existingKey = findSubEntryKey(next, id, scanSubSid);
787
+ const exists = existingKey ? next.get(existingKey) : undefined;
782
788
  // 已手动清除的条目:scan 发现但不在内存 → 跳过重建
783
789
  if (!exists && clearedIds.has(id))
784
790
  continue;
@@ -789,7 +795,7 @@ export function SubAgentPanel(props) {
789
795
  // "error": only update existing, never create a new entry
790
796
  if (rawStatus === "error") {
791
797
  if (exists && exists.status === "running") {
792
- next.set(id, { ...exists, status: "error", endedAt: Date.now() });
798
+ next.set(existingKey ?? id, { ...exists, status: "error", endedAt: Date.now() });
793
799
  }
794
800
  continue;
795
801
  }
@@ -805,7 +811,7 @@ export function SubAgentPanel(props) {
805
811
  status = "running";
806
812
  }
807
813
  // Already settled → skip
808
- if (exists && exists.status !== "running" && exists.status !== "cancel_requested")
814
+ if (exists && existingKey === id && exists.status !== "running" && exists.status !== "cancel_requested")
809
815
  continue;
810
816
  // Running entry with no explicit status improvement from part:
811
817
  // try message-level heuristics first, then time-based fallback.
@@ -834,15 +840,12 @@ export function SubAgentPanel(props) {
834
840
  const desc = input?.description !== undefined ? String(input.description) : "";
835
841
  const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40);
836
842
  let tokens;
837
- const scanStMeta2 = st?.metadata;
838
- const scanSubSid = scanStMeta2?.session_id !== undefined ? String(scanStMeta2.session_id)
839
- : scanStMeta2?.sessionId !== undefined ? String(scanStMeta2.sessionId)
840
- : undefined;
841
843
  if (scanSubSid)
842
844
  tokens = props.api.usage.readSessionTokens(scanSubSid);
843
845
  const ended = status === "done"; // "error" handled above, never reaches here
844
- next.set(id, {
845
- id, title, agent, prompt,
846
+ const entryKey = existingKey ?? id;
847
+ next.set(entryKey, {
848
+ id: exists?.id ?? id, title, agent, prompt,
846
849
  // Preserve existing values (from handleSessionEnd / KV) — scan must not overwrite
847
850
  tokens: exists?.tokens ?? tokens,
848
851
  sessionId: exists?.sessionId ?? scanSubSid,
@@ -850,6 +853,8 @@ export function SubAgentPanel(props) {
850
853
  startedAt: exists?.startedAt || Date.now(),
851
854
  endedAt: ended ? (exists?.endedAt || Date.now()) : undefined,
852
855
  });
856
+ if (entryKey !== id)
857
+ next.delete(id);
853
858
  }
854
859
  }
855
860
  }
@@ -1012,7 +1017,19 @@ export function SubAgentPanel(props) {
1012
1017
  return next;
1013
1018
  });
1014
1019
  };
1015
- const sep = () => "\u2500".repeat(Math.max(1, panelWidth()));
1020
+ /** Horizontal space eaten by the panel border (1+1) and its padding (2+2). */
1021
+ const gutter = createMemo(() => (props.borderVisible() ? 6 : 0));
1022
+ const sep = () => "\u2500".repeat(Math.max(1, panelWidth() - gutter()));
1023
+ // The border toggling shifts the box dimensions, which may not reliably
1024
+ // re-fire onSizeChange across (re)mount cycles; re-sync panelWidth with the
1025
+ // live box so the content width stays correct.
1026
+ createEffect(() => {
1027
+ props.borderVisible();
1028
+ if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
1029
+ const w = Math.max(20, boxEl.width);
1030
+ setPanelWidth((prev) => (prev === w ? prev : w));
1031
+ }
1032
+ });
1016
1033
  // ── expanded detail right-align ──
1017
1034
  const expandedMaxLabelW = createMemo(() => {
1018
1035
  const labels = [
@@ -1022,7 +1039,7 @@ export function SubAgentPanel(props) {
1022
1039
  return Math.max(...labels.map(l => visualWidth(l + ": ")));
1023
1040
  });
1024
1041
  const expandedPad = (label) => Math.max(0, expandedMaxLabelW() - visualWidth(label + ": "));
1025
- const expandedValAvail = () => Math.max(6, panelWidth() - INDENT - expandedMaxLabelW());
1042
+ const expandedValAvail = () => Math.max(6, panelWidth() - gutter() - INDENT - expandedMaxLabelW());
1026
1043
  // ── header parts for colored spans ──
1027
1044
  const summaryParts = createMemo(() => {
1028
1045
  if (!anyEntry())
@@ -1057,7 +1074,7 @@ export function SubAgentPanel(props) {
1057
1074
  return false;
1058
1075
  const icon = "\u25bc";
1059
1076
  const need = visualWidth(icon) + 1 + visualWidth(t("panel.title")) + versionW + summaryCols();
1060
- return need <= panelWidth();
1077
+ return need <= panelWidth() - gutter();
1061
1078
  });
1062
1079
  const leftCols = createMemo(() => {
1063
1080
  const icon = open() ? "\u25bc" : "\u25b6";
@@ -1069,11 +1086,11 @@ export function SubAgentPanel(props) {
1069
1086
  const spacerCols = createMemo(() => {
1070
1087
  if (!anyEntry())
1071
1088
  return 0;
1072
- return Math.max(0, panelWidth() - leftCols() - summaryCols());
1089
+ return Math.max(0, panelWidth() - gutter() - leftCols() - summaryCols());
1073
1090
  });
1074
- const valueCols = (label) => Math.max(4, panelWidth() - INDENT - visualWidth(label + ": "));
1091
+ const valueCols = (label) => Math.max(4, panelWidth() - gutter() - INDENT - visualWidth(label + ": "));
1075
1092
  // ── render ──
1076
- return (_jsxs("box", { border: false, paddingTop: 0, paddingBottom: 0, paddingLeft: 0, paddingRight: 0, flexDirection: "column", gap: 0, ref: boxEl, onSizeChange: () => {
1093
+ return (_jsxs("box", { border: props.borderVisible(), ...(props.borderVisible() ? { borderColor: pal().border } : {}), paddingTop: 0, paddingBottom: 0, paddingLeft: props.borderVisible() ? 2 : 0, paddingRight: props.borderVisible() ? 2 : 0, flexDirection: "column", gap: 0, ref: boxEl, onSizeChange: () => {
1077
1094
  const w = boxEl ? Math.max(20, boxEl.width ?? 0) : 28;
1078
1095
  setPanelWidth((prev) => (prev === w ? prev : w));
1079
1096
  }, children: [_jsxs("text", { onMouseUp: () => {
@@ -1165,7 +1182,7 @@ export function SubAgentPanel(props) {
1165
1182
  w += visualWidth(tk);
1166
1183
  return w;
1167
1184
  };
1168
- const labelAvail = () => Math.max(6, panelWidth() - LEFT_PAD - suffixW());
1185
+ const labelAvail = () => Math.max(6, panelWidth() - gutter() - LEFT_PAD - suffixW());
1169
1186
  const labelText = () => {
1170
1187
  const max = labelAvail();
1171
1188
  const text = entry.title || entry.agent;
@@ -1201,7 +1218,7 @@ export function SubAgentPanel(props) {
1201
1218
  const cancelLabel = () => ` ${t("cancel.label")}`;
1202
1219
  const dismissLabel = () => ` ${t("dismiss.label")}`;
1203
1220
  const rightW = (isRunning ? visualWidth(dismissLabel()) : 0) + (isRunning && entry.sessionId ? visualWidth(cancelLabel()) : 0);
1204
- const spacerW = () => Math.max(1, panelWidth() - openW() - rightW - 2);
1221
+ const spacerW = () => Math.max(1, panelWidth() - gutter() - openW() - rightW - 2);
1205
1222
  return (_jsxs("box", { flexDirection: "row", children: [_jsx(Show, { when: entry.sessionId, children: _jsxs("text", { onMouseOver: () => setHoveredOpen(entry.id), onMouseOut: () => setHoveredOpen(undefined), onMouseUp: () => {
1206
1223
  if (entry.sessionId) {
1207
1224
  props.api.route.navigateSession(entry.sessionId);
@@ -1219,7 +1236,7 @@ export function SubAgentPanel(props) {
1219
1236
  const right = props.sortOrder() === "desc"
1220
1237
  ? `\u2191 ${t("scroll.top")}`
1221
1238
  : `\u2193 ${t("scroll.bottom")}`;
1222
- const pad = showTop ? Math.max(1, panelWidth() - visualWidth(left) - visualWidth(right)) : 0;
1239
+ const pad = showTop ? Math.max(1, panelWidth() - gutter() - visualWidth(left) - visualWidth(right)) : 0;
1223
1240
  return (_jsxs("box", { flexDirection: "row", children: [_jsx("text", { onMouseOver: () => showMore && setHoveredMoreBelow(true), onMouseOut: () => setHoveredMoreBelow(false), onMouseUp: () => {
1224
1241
  if (!showMore)
1225
1242
  return;
@@ -0,0 +1,6 @@
1
+ import type { SubEntry } from "../core/types";
2
+ export type SubEntryPatch = Omit<SubEntry, "startedAt" | "endedAt"> & {
3
+ startedAt?: number;
4
+ };
5
+ export declare function findSubEntryKey(entries: ReadonlyMap<string, SubEntry>, id: string, sessionId?: string): string | undefined;
6
+ export declare function upsertSubEntry(entries: ReadonlyMap<string, SubEntry>, partial: SubEntryPatch, now?: number): Map<string, SubEntry>;
@@ -0,0 +1,25 @@
1
+ export function findSubEntryKey(entries, id, sessionId) {
2
+ if (sessionId) {
3
+ for (const [key, entry] of entries) {
4
+ if (entry.sessionId === sessionId)
5
+ return key;
6
+ }
7
+ }
8
+ return entries.has(id) ? id : undefined;
9
+ }
10
+ export function upsertSubEntry(entries, partial, now = Date.now()) {
11
+ const key = findSubEntryKey(entries, partial.id, partial.sessionId) ?? partial.id;
12
+ const existing = entries.get(key);
13
+ const next = new Map(entries);
14
+ if (key !== partial.id)
15
+ next.delete(partial.id);
16
+ const ended = partial.status === "done" || partial.status === "error" || partial.status === "cancelled";
17
+ next.set(key, {
18
+ ...(existing ?? { startedAt: now }),
19
+ ...partial,
20
+ id: existing?.id ?? partial.id,
21
+ startedAt: existing?.startedAt || partial.startedAt || now,
22
+ endedAt: ended ? (existing?.endedAt || now) : undefined,
23
+ });
24
+ return next;
25
+ }