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.
@@ -22,6 +22,7 @@ import { KV_PREFIX } from "../core/kv"
22
22
  import type { PanelApi, PanelEvent } from "./panel-api"
23
23
  import { globalEntryCache, clearTick } from "./store"
24
24
  import { isDirectChildSession } from "./session-routing"
25
+ import { findSubEntryKey, upsertSubEntry } from "./entry-map"
25
26
 
26
27
  /** Entry line left prefix: icon + space + status dot + space */
27
28
  const LEFT_PAD = 4
@@ -38,6 +39,7 @@ export function SubAgentPanel(props: {
38
39
  maxEntries: () => number
39
40
  sortOrder: () => SortOrder
40
41
  scrollMode: () => ScrollMode
42
+ borderVisible: () => boolean
41
43
  sessionId: string
42
44
  }): JSX.Element {
43
45
  const t = createT(() => props.lang())
@@ -87,14 +89,16 @@ export function SubAgentPanel(props: {
87
89
  }
88
90
 
89
91
  const loadEntries = (sid: string): Map<string, SubEntry> => {
90
- const m = new Map<string, SubEntry>()
92
+ let m = new Map<string, SubEntry>()
91
93
  try {
92
94
  const { parentSid, isChild } = resolveParent(sid)
93
95
  const rec = loadSessionData()[parentSid]
94
96
  if (rec) {
95
97
  const source = isChild ? rec.children?.[sid]?.entries : rec.entries
96
98
  if (source) {
97
- for (const e of source) m.set(e.id, e)
99
+ for (const e of source) {
100
+ m = upsertSubEntry(m, e)
101
+ }
98
102
  }
99
103
  }
100
104
  } catch {}
@@ -256,20 +260,7 @@ export function SubAgentPanel(props: {
256
260
  const upsertEntry = (
257
261
  partial: Omit<SubEntry, "startedAt" | "endedAt"> & { startedAt?: number }
258
262
  ) => {
259
- setEntryMap((prev) => {
260
- const existing = prev.get(partial.id)
261
- const next = new Map(prev)
262
- const nowTs = Date.now()
263
- const e = partial.status
264
- const ended = e === "done" || e === "error" || e === "cancelled"
265
- next.set(partial.id, {
266
- ...(existing ?? { startedAt: nowTs }),
267
- ...partial,
268
- startedAt: existing?.startedAt || partial.startedAt || nowTs,
269
- endedAt: ended ? (existing?.endedAt || nowTs) : undefined,
270
- })
271
- return next
272
- })
263
+ setEntryMap((prev) => upsertSubEntry(prev, partial))
273
264
  }
274
265
 
275
266
  // ── cancel helpers ──
@@ -397,9 +388,21 @@ export function SubAgentPanel(props: {
397
388
  if (rawStatus === "error") {
398
389
  const id = `tool:${String(part.id ?? "")}`
399
390
  if (!part.id) return
400
- const existing = entryMap().get(id)
391
+ const stMeta = st?.metadata as Record<string, unknown> | undefined
392
+ const errorSid = stMeta?.session_id !== undefined ? String(stMeta.session_id)
393
+ : stMeta?.sessionId !== undefined ? String(stMeta.sessionId)
394
+ : undefined
395
+ const key = findSubEntryKey(entryMap(), id, errorSid)
396
+ const existing = key ? entryMap().get(key) : undefined
401
397
  if (existing) {
402
- upsertEntry({ id, title: existing.title, agent: existing.agent, prompt: existing.prompt, status: "error" })
398
+ upsertEntry({
399
+ id: existing.id,
400
+ title: existing.title,
401
+ agent: existing.agent,
402
+ prompt: existing.prompt,
403
+ sessionId: existing.sessionId ?? errorSid,
404
+ status: "error",
405
+ })
403
406
  }
404
407
  return
405
408
  }
@@ -791,7 +794,12 @@ export function SubAgentPanel(props: {
791
794
 
792
795
  const st = (part as any).state as Record<string, unknown> | undefined
793
796
  const rawStatus = String(st?.status ?? "")
794
- const exists = next.get(id)
797
+ const scanStMeta = st?.metadata as Record<string, unknown> | undefined
798
+ const scanSubSid = scanStMeta?.session_id !== undefined ? String(scanStMeta.session_id)
799
+ : scanStMeta?.sessionId !== undefined ? String(scanStMeta.sessionId)
800
+ : undefined
801
+ const existingKey = findSubEntryKey(next, id, scanSubSid)
802
+ const exists = existingKey ? next.get(existingKey) : undefined
795
803
 
796
804
  // 已手动清除的条目:scan 发现但不在内存 → 跳过重建
797
805
  if (!exists && clearedIds.has(id)) continue
@@ -803,7 +811,7 @@ export function SubAgentPanel(props: {
803
811
  // "error": only update existing, never create a new entry
804
812
  if (rawStatus === "error") {
805
813
  if (exists && exists.status === "running") {
806
- next.set(id, { ...exists, status: "error", endedAt: Date.now() })
814
+ next.set(existingKey ?? id, { ...exists, status: "error", endedAt: Date.now() })
807
815
  }
808
816
  continue
809
817
  }
@@ -819,7 +827,7 @@ export function SubAgentPanel(props: {
819
827
  }
820
828
 
821
829
  // Already settled → skip
822
- if (exists && exists.status !== "running" && exists.status !== "cancel_requested") continue
830
+ if (exists && existingKey === id && exists.status !== "running" && exists.status !== "cancel_requested") continue
823
831
  // Running entry with no explicit status improvement from part:
824
832
  // try message-level heuristics first, then time-based fallback.
825
833
  if (exists && status === "running") {
@@ -847,15 +855,12 @@ export function SubAgentPanel(props: {
847
855
  const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40)
848
856
 
849
857
  let tokens: number | undefined
850
- const scanStMeta2 = st?.metadata as Record<string, unknown> | undefined
851
- const scanSubSid = scanStMeta2?.session_id !== undefined ? String(scanStMeta2.session_id)
852
- : scanStMeta2?.sessionId !== undefined ? String(scanStMeta2.sessionId)
853
- : undefined
854
858
  if (scanSubSid) tokens = props.api.usage.readSessionTokens(scanSubSid)
855
859
 
856
860
  const ended = status === "done" // "error" handled above, never reaches here
857
- next.set(id, {
858
- id, title, agent, prompt,
861
+ const entryKey = existingKey ?? id
862
+ next.set(entryKey, {
863
+ id: exists?.id ?? id, title, agent, prompt,
859
864
  // Preserve existing values (from handleSessionEnd / KV) — scan must not overwrite
860
865
  tokens: exists?.tokens ?? tokens,
861
866
  sessionId: exists?.sessionId ?? scanSubSid,
@@ -863,6 +868,7 @@ export function SubAgentPanel(props: {
863
868
  startedAt: exists?.startedAt || Date.now(),
864
869
  endedAt: ended ? (exists?.endedAt || Date.now()) : undefined,
865
870
  })
871
+ if (entryKey !== id) next.delete(id)
866
872
  }
867
873
  }
868
874
  }
@@ -1012,7 +1018,21 @@ export function SubAgentPanel(props: {
1012
1018
  })
1013
1019
  }
1014
1020
 
1015
- const sep = () => "\u2500".repeat(Math.max(1, panelWidth()))
1021
+ /** Horizontal space eaten by the panel border (1+1) and its padding (2+2). */
1022
+ const gutter = createMemo(() => (props.borderVisible() ? 6 : 0))
1023
+
1024
+ const sep = () => "\u2500".repeat(Math.max(1, panelWidth() - gutter()))
1025
+
1026
+ // The border toggling shifts the box dimensions, which may not reliably
1027
+ // re-fire onSizeChange across (re)mount cycles; re-sync panelWidth with the
1028
+ // live box so the content width stays correct.
1029
+ createEffect(() => {
1030
+ props.borderVisible()
1031
+ if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
1032
+ const w = Math.max(20, boxEl.width)
1033
+ setPanelWidth((prev) => (prev === w ? prev : w))
1034
+ }
1035
+ })
1016
1036
 
1017
1037
  // ── expanded detail right-align ──
1018
1038
  const expandedMaxLabelW = createMemo(() => {
@@ -1025,7 +1045,7 @@ export function SubAgentPanel(props: {
1025
1045
 
1026
1046
  const expandedPad = (label: string) => Math.max(0, expandedMaxLabelW() - visualWidth(label + ": "))
1027
1047
 
1028
- const expandedValAvail = () => Math.max(6, panelWidth() - INDENT - expandedMaxLabelW())
1048
+ const expandedValAvail = () => Math.max(6, panelWidth() - gutter() - INDENT - expandedMaxLabelW())
1029
1049
 
1030
1050
  // ── header parts for colored spans ──
1031
1051
  const summaryParts = createMemo(() => {
@@ -1059,7 +1079,7 @@ export function SubAgentPanel(props: {
1059
1079
  if (!open()) return false
1060
1080
  const icon = "\u25bc"
1061
1081
  const need = visualWidth(icon) + 1 + visualWidth(t("panel.title")) + versionW + summaryCols()
1062
- return need <= panelWidth()
1082
+ return need <= panelWidth() - gutter()
1063
1083
  })
1064
1084
 
1065
1085
  const leftCols = createMemo(() => {
@@ -1071,17 +1091,19 @@ export function SubAgentPanel(props: {
1071
1091
 
1072
1092
  const spacerCols = createMemo(() => {
1073
1093
  if (!anyEntry()) return 0
1074
- return Math.max(0, panelWidth() - leftCols() - summaryCols())
1094
+ return Math.max(0, panelWidth() - gutter() - leftCols() - summaryCols())
1075
1095
  })
1076
1096
 
1077
1097
  const valueCols = (label: string) =>
1078
- Math.max(4, panelWidth() - INDENT - visualWidth(label + ": "))
1098
+ Math.max(4, panelWidth() - gutter() - INDENT - visualWidth(label + ": "))
1079
1099
 
1080
1100
  // ── render ──
1081
1101
  return (
1082
1102
  <box
1083
- border={false}
1084
- paddingTop={0} paddingBottom={0} paddingLeft={0} paddingRight={0}
1103
+ border={props.borderVisible()}
1104
+ {...(props.borderVisible() ? { borderColor: pal().border } : {})}
1105
+ paddingTop={0} paddingBottom={0}
1106
+ paddingLeft={props.borderVisible() ? 2 : 0} paddingRight={props.borderVisible() ? 2 : 0}
1085
1107
  flexDirection="column" gap={0}
1086
1108
  ref={boxEl}
1087
1109
  onSizeChange={() => {
@@ -1218,7 +1240,7 @@ export function SubAgentPanel(props: {
1218
1240
  if (tk) w += visualWidth(tk)
1219
1241
  return w
1220
1242
  }
1221
- const labelAvail = () => Math.max(6, panelWidth() - LEFT_PAD - suffixW())
1243
+ const labelAvail = () => Math.max(6, panelWidth() - gutter() - LEFT_PAD - suffixW())
1222
1244
  const labelText = () => {
1223
1245
  const max = labelAvail()
1224
1246
  const text = entry.title || entry.agent
@@ -1360,7 +1382,7 @@ export function SubAgentPanel(props: {
1360
1382
  const cancelLabel = () => ` ${t("cancel.label")}`
1361
1383
  const dismissLabel = () => ` ${t("dismiss.label")}`
1362
1384
  const rightW = (isRunning ? visualWidth(dismissLabel()) : 0) + (isRunning && entry.sessionId ? visualWidth(cancelLabel()) : 0)
1363
- const spacerW = () => Math.max(1, panelWidth() - openW() - rightW - 2)
1385
+ const spacerW = () => Math.max(1, panelWidth() - gutter() - openW() - rightW - 2)
1364
1386
  return (
1365
1387
  <box flexDirection="row">
1366
1388
  <Show when={entry.sessionId}>
@@ -1417,7 +1439,7 @@ export function SubAgentPanel(props: {
1417
1439
  const right = props.sortOrder() === "desc"
1418
1440
  ? `\u2191 ${t("scroll.top")}`
1419
1441
  : `\u2193 ${t("scroll.bottom")}`
1420
- const pad = showTop ? Math.max(1, panelWidth() - visualWidth(left) - visualWidth(right)) : 0
1442
+ const pad = showTop ? Math.max(1, panelWidth() - gutter() - visualWidth(left) - visualWidth(right)) : 0
1421
1443
  return (
1422
1444
  <box flexDirection="row">
1423
1445
  <text
@@ -0,0 +1,39 @@
1
+ import type { SubEntry } from "../core/types"
2
+
3
+ export type SubEntryPatch = Omit<SubEntry, "startedAt" | "endedAt"> & {
4
+ startedAt?: number
5
+ }
6
+
7
+ export function findSubEntryKey(
8
+ entries: ReadonlyMap<string, SubEntry>,
9
+ id: string,
10
+ sessionId?: string,
11
+ ) {
12
+ if (sessionId) {
13
+ for (const [key, entry] of entries) {
14
+ if (entry.sessionId === sessionId) return key
15
+ }
16
+ }
17
+ return entries.has(id) ? id : undefined
18
+ }
19
+
20
+ export function upsertSubEntry(
21
+ entries: ReadonlyMap<string, SubEntry>,
22
+ partial: SubEntryPatch,
23
+ now = Date.now(),
24
+ ) {
25
+ const key = findSubEntryKey(entries, partial.id, partial.sessionId) ?? partial.id
26
+ const existing = entries.get(key)
27
+ const next = new Map(entries)
28
+ if (key !== partial.id) next.delete(partial.id)
29
+
30
+ const ended = partial.status === "done" || partial.status === "error" || partial.status === "cancelled"
31
+ next.set(key, {
32
+ ...(existing ?? { startedAt: now }),
33
+ ...partial,
34
+ id: existing?.id ?? partial.id,
35
+ startedAt: existing?.startedAt || partial.startedAt || now,
36
+ endedAt: ended ? (existing?.endedAt || now) : undefined,
37
+ })
38
+ return next
39
+ }
@@ -1,8 +1,8 @@
1
- import { createSignal } from "solid-js"
2
- import type { SubEntry } from "../core/types"
3
-
4
- /** 模块级缓存:各 session 的 entry 状态独立存储,不随当前视图切换而清除。 */
5
- export const globalEntryCache = new Map<string, Map<string, SubEntry>>()
6
-
7
- /** 模块级刷新信号:外部(如斜杠命令)触发清除后 +1,组件 scan 依赖它以重扫。 */
8
- export const [clearTick, setClearTick] = createSignal(0)
1
+ import { createSignal } from "solid-js"
2
+ import type { SubEntry } from "../core/types"
3
+
4
+ /** 模块级缓存:各 session 的 entry 状态独立存储,不随当前视图切换而清除。 */
5
+ export const globalEntryCache = new Map<string, Map<string, SubEntry>>()
6
+
7
+ /** 模块级刷新信号:外部(如斜杠命令)触发清除后 +1,组件 scan 依赖它以重扫。 */
8
+ export const [clearTick, setClearTick] = createSignal(0)