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.
Files changed (60) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +192 -192
  3. package/dist/_version.d.ts +1 -1
  4. package/dist/_version.js +1 -1
  5. package/dist/core/color.d.ts +20 -0
  6. package/dist/core/color.js +68 -0
  7. package/dist/core/format.d.ts +5 -0
  8. package/dist/core/format.js +68 -0
  9. package/dist/core/index.d.ts +6 -0
  10. package/dist/core/index.js +6 -0
  11. package/dist/core/kv.d.ts +20 -0
  12. package/dist/core/kv.js +30 -0
  13. package/dist/core/state-machine.d.ts +17 -0
  14. package/dist/core/state-machine.js +27 -0
  15. package/dist/core/types.d.ts +51 -0
  16. package/dist/core/types.js +2 -0
  17. package/dist/core/usage.d.ts +15 -0
  18. package/dist/core/usage.js +1 -0
  19. package/dist/index.js +148 -1484
  20. package/dist/panel/SubAgentPanel.d.ts +13 -0
  21. package/dist/panel/SubAgentPanel.js +1232 -0
  22. package/dist/panel/panel-api.d.ts +67 -0
  23. package/dist/panel/panel-api.js +1 -0
  24. package/dist/panel/store.d.ts +5 -0
  25. package/dist/panel/store.js +5 -0
  26. package/dist/tui.js +383 -289
  27. package/dist/v2/commands.d.ts +7 -0
  28. package/dist/v2/commands.js +263 -0
  29. package/dist/v2/index.d.ts +8 -0
  30. package/dist/v2/index.js +62 -0
  31. package/dist/v2/theme.d.ts +3 -0
  32. package/dist/v2/theme.js +12 -0
  33. package/dist/v2/types.d.ts +231 -0
  34. package/dist/v2/types.js +6 -0
  35. package/dist/v2/v2-panel-api.d.ts +12 -0
  36. package/dist/v2/v2-panel-api.js +345 -0
  37. package/dist/v2.js +3003 -0
  38. package/install.mjs +103 -103
  39. package/package.json +64 -63
  40. package/src/_version.ts +1 -1
  41. package/src/clipboard.ts +134 -134
  42. package/src/core/color.ts +70 -0
  43. package/src/core/format.ts +61 -0
  44. package/src/core/index.ts +6 -0
  45. package/src/core/kv.ts +36 -0
  46. package/src/core/state-machine.ts +46 -0
  47. package/src/core/types.ts +58 -0
  48. package/src/core/usage.ts +16 -0
  49. package/src/i18n.ts +261 -261
  50. package/src/index.tsx +124 -1750
  51. package/src/panel/SubAgentPanel.tsx +1460 -0
  52. package/src/panel/panel-api.ts +72 -0
  53. package/src/panel/store.ts +8 -0
  54. package/src/server.ts +10 -10
  55. package/src/v2/commands.ts +251 -0
  56. package/src/v2/index.tsx +98 -0
  57. package/src/v2/theme.ts +14 -0
  58. package/src/v2/types.ts +165 -0
  59. package/src/v2/v2-panel-api.ts +301 -0
  60. package/tui/index.js +11 -0
@@ -0,0 +1,70 @@
1
+ /** Morandi palette helpers — desaturate any theme color toward a muted fallback. */
2
+
3
+ export function rgb(raw: unknown): { r: number; g: number; b: number } | null {
4
+ if (typeof raw === "string" && raw.startsWith("#")) {
5
+ const h = raw.slice(1)
6
+ return {
7
+ r: parseInt(h.slice(0, 2), 16),
8
+ g: parseInt(h.slice(2, 4), 16),
9
+ b: parseInt(h.slice(4, 6), 16),
10
+ }
11
+ }
12
+ if (raw && typeof raw === "object") {
13
+ const o = raw as Record<string, unknown>
14
+ if (typeof o.r === "number" && typeof o.g === "number" && typeof o.b === "number") {
15
+ const scale = o.r > 1 || o.g > 1 || o.b > 1 ? 1 : 255
16
+ return { r: Math.round(o.r * scale), g: Math.round(o.g * scale), b: Math.round(o.b * scale) }
17
+ }
18
+ }
19
+ return null
20
+ }
21
+
22
+ export function saturation(r: number, g: number, b: number): number {
23
+ const max = Math.max(r, g, b) / 255
24
+ const min = Math.min(r, g, b) / 255
25
+ const delta = max - min
26
+ if (delta === 0) return 0
27
+ const L = (max + min) / 2
28
+ return L <= 0.5 ? delta / (max + min) : delta / (2 - max - min)
29
+ }
30
+
31
+ function desaturateTo(raw: unknown, maxSat: number, fallback: string): string {
32
+ const c = rgb(raw)
33
+ if (!c) return fallback
34
+ const sat = saturation(c.r, c.g, c.b)
35
+ if (sat <= maxSat) {
36
+ return "#" + [c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")
37
+ }
38
+ const luma = c.r * 0.299 + c.g * 0.587 + c.b * 0.114
39
+ let lo = 0, hi = 1
40
+ for (let i = 0; i < 12; i++) {
41
+ const mid = (lo + hi) / 2
42
+ const nr = Math.round(c.r + (luma - c.r) * mid)
43
+ const ng = Math.round(c.g + (luma - c.g) * mid)
44
+ const nb = Math.round(c.b + (luma - c.b) * mid)
45
+ if (saturation(nr, ng, nb) > maxSat) lo = mid
46
+ else hi = mid
47
+ }
48
+ const nr = Math.round(c.r + (luma - c.r) * hi)
49
+ const ng = Math.round(c.g + (luma - c.g) * hi)
50
+ const nb = Math.round(c.b + (luma - c.b) * hi)
51
+ return "#" + [nr, ng, nb].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")
52
+ }
53
+
54
+ function dimColor(hex: string, factor = 0.5): string {
55
+ const c = rgb(hex)
56
+ if (!c) return hex
57
+ const r = Math.round(c.r * factor)
58
+ const g = Math.round(c.g * factor)
59
+ const b = Math.round(c.b * factor)
60
+ return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")
61
+ }
62
+
63
+ export const FALLBACK = {
64
+ primary: "#8B9DAF", text: "#C5C5BB", muted: "#7A7A72",
65
+ success: "#9CAF8B", warning: "#C5B88D", error: "#B08A8A", border: "#6B6B63",
66
+ } as const
67
+
68
+ export const MAX_SAT = 0.28
69
+
70
+ export { desaturateTo, dimColor }
@@ -0,0 +1,61 @@
1
+ /** Visual width of a single character (CJK/wide chars count as 2). */
2
+ function charColumns(c: string): number {
3
+ const code = c.codePointAt(0) ?? 0
4
+ if (code < 0x20) return 0
5
+ if (code < 0x7f) return 1
6
+ if (code < 0xa0) return 0
7
+ if (
8
+ (code >= 0x1100 && code <= 0x115f) ||
9
+ (code >= 0x2e80 && code <= 0xa4cf) ||
10
+ (code >= 0xac00 && code <= 0xd7a3) ||
11
+ (code >= 0xf900 && code <= 0xfaff) ||
12
+ (code >= 0xfe10 && code <= 0xfe6f) ||
13
+ (code >= 0xff01 && code <= 0xff60) ||
14
+ (code >= 0xffe0 && code <= 0xffe6) ||
15
+ (code >= 0x1f300 && code <= 0x1f64f) ||
16
+ (code >= 0x20000 && code <= 0x3fffd)
17
+ )
18
+ return 2
19
+ return 1
20
+ }
21
+
22
+ export function visualWidth(s: string): number {
23
+ let w = 0
24
+ for (const c of s) w += charColumns(c)
25
+ return w
26
+ }
27
+
28
+ export function truncate(text: string, maxCols: number): string {
29
+ if (visualWidth(text) <= maxCols) return text
30
+ let cols = 0
31
+ let i = 0
32
+ for (const c of text) {
33
+ const w = charColumns(c)
34
+ if (cols + w > maxCols - 1) break
35
+ cols += w
36
+ i += c.length
37
+ }
38
+ return text.slice(0, i) + "\u2026"
39
+ }
40
+
41
+ export function fmtDurationShort(ms: number, running: boolean): string {
42
+ if (running && ms < 2000) return ""
43
+ if (ms < 1000) return (ms / 1000).toFixed(2) + "s"
44
+ if (ms < 60000) return (ms / 1000).toFixed(2) + "s"
45
+ const m = Math.floor(ms / 60000)
46
+ const s = Math.round((ms % 60000) / 1000)
47
+ return `${m}m${s}s`
48
+ }
49
+
50
+ export function fmtTokens(n: number): string {
51
+ if (n < 1000) return `${n}`
52
+ if (n < 1000000) return `${(n / 1000).toFixed(1)}k`
53
+ return `${(n / 1000000).toFixed(1)}M`
54
+ }
55
+
56
+ export function safeErrorMsg(err: unknown): string {
57
+ if (!err) return ""
58
+ if (typeof err === "string") return err
59
+ if (typeof err === "object") return String((err as any).message || (err as any).code || "")
60
+ return ""
61
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./types"
2
+ export * from "./format"
3
+ export * from "./color"
4
+ export * from "./kv"
5
+ export * from "./usage"
6
+ export * from "./state-machine"
package/src/core/kv.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { SessionRecord } from "./types"
2
+
3
+ export const KV_PREFIX = "subagent_magazine"
4
+ export const SESSION_DATA_KEY = `${KV_PREFIX}.session_data`
5
+
6
+ /** Minimal KV surface used by the magazine's persistence layer. */
7
+ export interface KVApi {
8
+ get(key: string, fallback?: unknown): unknown
9
+ set(key: string, value: unknown): void
10
+ }
11
+
12
+ /** Setting keys shared by V1/V2 (values are raw strings in KV). */
13
+ export const SETTING_KEYS = {
14
+ lang: `${KV_PREFIX}.lang`,
15
+ maxEntries: `${KV_PREFIX}.max_entries`,
16
+ order: `${KV_PREFIX}.order`,
17
+ scrollMode: `${KV_PREFIX}.scroll_mode`,
18
+ open: `${KV_PREFIX}.open`,
19
+ ttlDays: `${KV_PREFIX}.ttl_days`,
20
+ } as const
21
+
22
+ export function loadSessionData(kv: KVApi): Record<string, SessionRecord> {
23
+ try {
24
+ const raw = kv.get(SESSION_DATA_KEY, "{}")
25
+ return JSON.parse(String(raw))
26
+ } catch { return {} }
27
+ }
28
+
29
+ export function saveSessionData(kv: KVApi, data: Record<string, SessionRecord>) {
30
+ try { kv.set(SESSION_DATA_KEY, JSON.stringify(data)) } catch {}
31
+ }
32
+
33
+ export function readTTLDays(kv: KVApi): number {
34
+ const ttlDaysRaw = parseInt(String(kv.get(SETTING_KEYS.ttlDays, "3")), 10)
35
+ return Number.isNaN(ttlDaysRaw) ? 3 : ttlDaysRaw
36
+ }
@@ -0,0 +1,46 @@
1
+ import type { SubEntry, SubStatus } from "./types"
2
+ import type { TodoStats } from "./usage"
3
+
4
+ /** Final status decision when a child session settles (V1 `session.idle` semantics). */
5
+ export function settleOnIdle(entry: SubEntry): SubStatus {
6
+ if (entry.status === "cancel_requested" && entry.abortAccepted) return "cancelled"
7
+ return "done"
8
+ }
9
+
10
+ /** Agent-name normalization used by the fallback matching chain. */
11
+ export function normalizeAgent(s: string): string {
12
+ return s.toLowerCase().replace(/[^a-z0-9-]/g, "")
13
+ }
14
+
15
+ /** Snapshot of usage values captured when a child session settles. */
16
+ export interface SettleSnapshot {
17
+ tokens?: number
18
+ cost?: number
19
+ model?: string
20
+ todo?: TodoStats
21
+ }
22
+
23
+ /** Apply a settled status + usage backfill onto an entry (no-op preserving already-settled). */
24
+ export function settleEntry(
25
+ entry: SubEntry,
26
+ targetStatus: SubStatus,
27
+ nowTs: number,
28
+ snap: SettleSnapshot,
29
+ errorMsg?: string,
30
+ ): SubEntry {
31
+ const alreadySettled = entry.status !== "running" && entry.status !== "cancel_requested"
32
+ const finalStatus = targetStatus === "error" ? "error" : settleOnIdle(entry)
33
+ return {
34
+ ...entry,
35
+ ...(alreadySettled ? {} : { status: finalStatus, endedAt: nowTs }),
36
+ tokens: entry.tokens ?? snap.tokens,
37
+ cost: entry.cost ?? snap.cost,
38
+ model: entry.model ?? snap.model,
39
+ todoTotal: entry.todoTotal ?? snap.todo?.total,
40
+ todoDone: entry.todoDone ?? snap.todo?.done,
41
+ error: errorMsg || entry.error,
42
+ }
43
+ }
44
+
45
+ /** Time-based scan heuristic: how old must a running entry be before assuming completion. */
46
+ export const STALE_RUNNING_MS = 30 * 60 * 1000
@@ -0,0 +1,58 @@
1
+ import type { LangCode } from "../i18n"
2
+
3
+ export type SubStatus = "running" | "done" | "error" | "cancel_requested" | "cancelled"
4
+
5
+ export interface SubEntry {
6
+ id: string
7
+ title: string
8
+ agent: string
9
+ prompt: string
10
+ error?: string
11
+ tokens?: number
12
+ cost?: number
13
+ status: SubStatus
14
+ sessionId?: string
15
+ startedAt: number
16
+ endedAt?: number
17
+ model?: string
18
+ todoTotal?: number
19
+ todoDone?: number
20
+ cancelRequestedAt?: number
21
+ abortAccepted?: boolean
22
+ cancelReason?: "manual"
23
+ }
24
+
25
+ export type Lang = LangCode
26
+ export type SortOrder = "desc" | "asc"
27
+ export type ScrollMode = "wheel" | "click"
28
+
29
+ /** OpenCode built-in tool names that spawn sub-agents or delegate tasks. */
30
+ export const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"])
31
+
32
+ export interface ChildRecord {
33
+ scroll: number
34
+ expanded: string
35
+ entries: SubEntry[]
36
+ clearedIds?: string[]
37
+ }
38
+
39
+ export interface SessionRecord {
40
+ ts: number
41
+ entries: SubEntry[]
42
+ scroll: number
43
+ expanded: string
44
+ children: Record<string, ChildRecord>
45
+ clearedIds?: string[]
46
+ }
47
+
48
+ export interface SharedSignals {
49
+ lang: () => Lang
50
+ setLang: (l: Lang) => void
51
+ maxEntries: () => number
52
+ setMaxEntries: (n: number) => void
53
+ sortOrder: () => SortOrder
54
+ setSortOrder: (o: SortOrder) => void
55
+ scrollMode: () => ScrollMode
56
+ setScrollMode: (m: ScrollMode) => void
57
+ sessionId: string
58
+ }
@@ -0,0 +1,16 @@
1
+ export interface TodoStats {
2
+ total: number
3
+ done: number
4
+ }
5
+
6
+ /**
7
+ * Data-source abstraction for per-session usage reads.
8
+ * V1 and V2 provide their own implementations backed by their respective
9
+ * client APIs; the state machine consumes only this surface.
10
+ */
11
+ export interface UsageReader {
12
+ readSessionTokens(sid: string): number | undefined
13
+ readSessionCost(sid: string): number | undefined
14
+ readSessionModel(sid: string): string | undefined
15
+ readSessionTodo(sid: string): TodoStats | undefined
16
+ }