opencode-subagent-magazine 1.7.0 → 1.7.2

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 (59) hide show
  1. package/dist/_version.d.ts +1 -1
  2. package/dist/core/kv.d.ts +1 -0
  3. package/dist/core/types.d.ts +8 -0
  4. package/dist/i18n.d.ts +5 -0
  5. package/dist/panel/SubAgentPanel.d.ts +1 -0
  6. package/dist/panel/entry-map.d.ts +1 -0
  7. package/dist/panel/panel-api.d.ts +16 -0
  8. package/dist/tui.js +385 -218
  9. package/dist/v2/db.d.ts +14 -1
  10. package/dist/v2.js +473 -223
  11. package/package.json +17 -4
  12. package/dist/_version.js +0 -2
  13. package/dist/clipboard.js +0 -85
  14. package/dist/core/color.js +0 -68
  15. package/dist/core/format.js +0 -113
  16. package/dist/core/index.js +0 -6
  17. package/dist/core/kv.js +0 -63
  18. package/dist/core/state-machine.js +0 -27
  19. package/dist/core/types.js +0 -2
  20. package/dist/core/usage.js +0 -1
  21. package/dist/i18n.js +0 -333
  22. package/dist/index.js +0 -511
  23. package/dist/panel/SubAgentPanel.js +0 -1572
  24. package/dist/panel/entry-map.js +0 -99
  25. package/dist/panel/panel-api.js +0 -1
  26. package/dist/panel/session-routing.js +0 -4
  27. package/dist/panel/store.js +0 -5
  28. package/dist/server.js +0 -14
  29. package/dist/v2/commands.js +0 -420
  30. package/dist/v2/db.js +0 -267
  31. package/dist/v2/index.js +0 -83
  32. package/dist/v2/settings-menu.js +0 -225
  33. package/dist/v2/theme.js +0 -12
  34. package/dist/v2/types.js +0 -6
  35. package/dist/v2/v2-panel-api.js +0 -612
  36. package/src/_version.ts +0 -2
  37. package/src/clipboard.ts +0 -134
  38. package/src/core/color.ts +0 -70
  39. package/src/core/format.ts +0 -105
  40. package/src/core/index.ts +0 -6
  41. package/src/core/kv.ts +0 -77
  42. package/src/core/state-machine.ts +0 -46
  43. package/src/core/types.ts +0 -72
  44. package/src/core/usage.ts +0 -16
  45. package/src/i18n.ts +0 -345
  46. package/src/index.tsx +0 -564
  47. package/src/panel/SubAgentPanel.tsx +0 -1802
  48. package/src/panel/entry-map.ts +0 -115
  49. package/src/panel/panel-api.ts +0 -83
  50. package/src/panel/session-routing.ts +0 -10
  51. package/src/panel/store.ts +0 -8
  52. package/src/server.ts +0 -19
  53. package/src/v2/commands.ts +0 -408
  54. package/src/v2/db.ts +0 -274
  55. package/src/v2/index.tsx +0 -144
  56. package/src/v2/settings-menu.ts +0 -241
  57. package/src/v2/theme.ts +0 -14
  58. package/src/v2/types.ts +0 -167
  59. package/src/v2/v2-panel-api.ts +0 -515
package/src/clipboard.ts DELETED
@@ -1,134 +0,0 @@
1
- import { spawn } from "node:child_process"
2
- import { platform, release } from "node:os"
3
-
4
- export type ClipboardMethod =
5
- | "pbcopy"
6
- | "osascript"
7
- | "wl-copy"
8
- | "xclip"
9
- | "xsel"
10
- | "powershell"
11
- | "osc52"
12
- | "none"
13
-
14
- export interface CopyTextResult {
15
- copied: boolean
16
- method: ClipboardMethod
17
- error?: string
18
- }
19
-
20
- function runWithInput(command: string, args: string[], input: string): Promise<void> {
21
- return new Promise((resolve, reject) => {
22
- const child = spawn(command, args, {
23
- stdio: ["pipe", "ignore", "ignore"],
24
- windowsHide: true,
25
- })
26
-
27
- child.once("error", reject)
28
- child.once("close", (code) => {
29
- if (code === 0) resolve()
30
- else reject(new Error(`${command} exited with code ${code}`))
31
- })
32
-
33
- child.stdin?.end(input)
34
- })
35
- }
36
-
37
- function writeOsc52(text: string): boolean {
38
- if (!process.stdout.isTTY) return false
39
-
40
- const payload = Buffer.from(text, "utf8").toString("base64")
41
- const sequence = `\x1b]52;c;${payload}\x07`
42
- const wrapped = process.env.TMUX || process.env.STY
43
- ? `\x1bPtmux;\x1b${sequence}\x1b\\`
44
- : sequence
45
-
46
- process.stdout.write(wrapped)
47
- return true
48
- }
49
-
50
- async function tryCommand(
51
- method: ClipboardMethod,
52
- command: string,
53
- args: string[],
54
- text: string,
55
- ): Promise<CopyTextResult | undefined> {
56
- try {
57
- await runWithInput(command, args, text)
58
- return { copied: true, method }
59
- } catch {
60
- return undefined
61
- }
62
- }
63
-
64
- export async function copyText(text: string): Promise<CopyTextResult> {
65
- if (!text) return { copied: false, method: "none", error: "empty_text" }
66
-
67
- const os = platform()
68
- const isWsl = release().toLowerCase().includes("microsoft")
69
-
70
- const attempts: Array<() => Promise<CopyTextResult | undefined>> = []
71
-
72
- if (os === "darwin") {
73
- attempts.push(() => tryCommand("pbcopy", "pbcopy", [], text))
74
- attempts.push(() =>
75
- tryCommand(
76
- "osascript",
77
- "osascript",
78
- ["-e", "set the clipboard to (read (POSIX file \"/dev/stdin\") as text)"],
79
- text,
80
- ),
81
- )
82
- }
83
-
84
- if (os === "linux" && isWsl) {
85
- attempts.push(() =>
86
- tryCommand(
87
- "powershell",
88
- "powershell.exe",
89
- [
90
- "-NonInteractive",
91
- "-NoProfile",
92
- "-Command",
93
- "[Console]::InputEncoding=[Text.Encoding]::UTF8; Set-Clipboard ([Console]::In.ReadToEnd())",
94
- ],
95
- text,
96
- ),
97
- )
98
- } else if (os === "linux") {
99
- if (process.env.WAYLAND_DISPLAY) {
100
- attempts.push(() => tryCommand("wl-copy", "wl-copy", [], text))
101
- }
102
- attempts.push(() => tryCommand("xclip", "xclip", ["-selection", "clipboard"], text))
103
- attempts.push(() => tryCommand("xsel", "xsel", ["--clipboard", "--input"], text))
104
- }
105
-
106
- if (os === "win32") {
107
- attempts.push(() =>
108
- tryCommand(
109
- "powershell",
110
- "powershell.exe",
111
- [
112
- "-NonInteractive",
113
- "-NoProfile",
114
- "-Command",
115
- "[Console]::InputEncoding=[Text.Encoding]::UTF8; Set-Clipboard ([Console]::In.ReadToEnd())",
116
- ],
117
- text,
118
- ),
119
- )
120
- }
121
-
122
- for (const attempt of attempts) {
123
- const result = await attempt()
124
- if (result) return result
125
- }
126
-
127
- try {
128
- if (writeOsc52(text)) return { copied: true, method: "osc52" }
129
- } catch {
130
- // Continue to the explicit failure result.
131
- }
132
-
133
- return { copied: false, method: "none", error: "clipboard_unavailable" }
134
- }
package/src/core/color.ts DELETED
@@ -1,70 +0,0 @@
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 }
@@ -1,105 +0,0 @@
1
- import type { TimeFormat } from "./types"
2
-
3
- /** Visual width of a single character (CJK/wide chars count as 2). */
4
- function charColumns(c: string): number {
5
- const code = c.codePointAt(0) ?? 0
6
- if (code < 0x20) return 0
7
- if (code < 0x7f) return 1
8
- if (code < 0xa0) return 0
9
- if (
10
- (code >= 0x1100 && code <= 0x115f) ||
11
- (code >= 0x2e80 && code <= 0xa4cf) ||
12
- (code >= 0xac00 && code <= 0xd7a3) ||
13
- (code >= 0xf900 && code <= 0xfaff) ||
14
- (code >= 0xfe10 && code <= 0xfe6f) ||
15
- (code >= 0xff01 && code <= 0xff60) ||
16
- (code >= 0xffe0 && code <= 0xffe6) ||
17
- (code >= 0x1f300 && code <= 0x1f64f) ||
18
- (code >= 0x20000 && code <= 0x3fffd)
19
- )
20
- return 2
21
- return 1
22
- }
23
-
24
- export function visualWidth(s: string): number {
25
- let w = 0
26
- for (const c of s) w += charColumns(c)
27
- return w
28
- }
29
-
30
- export function truncate(text: string, maxCols: number): string {
31
- if (visualWidth(text) <= maxCols) return text
32
- let cols = 0
33
- let i = 0
34
- for (const c of text) {
35
- const w = charColumns(c)
36
- if (cols + w > maxCols - 1) break
37
- cols += w
38
- i += c.length
39
- }
40
- return text.slice(0, i) + "\u2026"
41
- }
42
-
43
- export function fmtDurationShort(ms: number, running: boolean): string {
44
- if (running && ms < 2000) return ""
45
- if (ms < 1000) return (ms / 1000).toFixed(2) + "s"
46
- if (ms < 60000) return (ms / 1000).toFixed(2) + "s"
47
- const m = Math.floor(ms / 60000)
48
- const s = Math.round((ms % 60000) / 1000)
49
- return `${m}m${s}s`
50
- }
51
-
52
- /** All supported elapsed-time display modes, in picker order. */
53
- export const TIME_FORMATS: readonly TimeFormat[] = ["short", "decimal", "clock", "compact", "seconds"]
54
-
55
- /** Neutral samples shown in the time-format picker (language independent). */
56
- export const TIME_FORMAT_SAMPLES: Record<TimeFormat, string> = {
57
- decimal: "45.3s · 9.5m · 1.9h",
58
- short: "45.32s · 112m35s",
59
- clock: "0:45 · 1:52:35",
60
- compact: "45s · 1h52m",
61
- seconds: "45s · 6755s",
62
- }
63
-
64
- /** Formats elapsed milliseconds using the configured display mode. */
65
- export function fmtDuration(ms: number, running: boolean, mode: TimeFormat = "short"): string {
66
- if (running && ms < 2000) return ""
67
- const t = Number.isFinite(ms) && ms > 0 ? ms : 0
68
- switch (mode) {
69
- case "short":
70
- return fmtDurationShort(t, false)
71
- case "compact": {
72
- if (t < 60000) return `${Math.floor(t / 1000)}s`
73
- if (t < 3600000) return `${Math.floor(t / 60000)}m${Math.floor((t % 60000) / 1000)}s`
74
- return `${Math.floor(t / 3600000)}h${Math.floor((t % 3600000) / 60000)}m`
75
- }
76
- case "clock": {
77
- const s = Math.floor((t % 60000) / 1000)
78
- if (t < 3600000) return `${Math.floor(t / 60000)}:${String(s).padStart(2, "0")}`
79
- const m = Math.floor((t % 3600000) / 60000)
80
- return `${Math.floor(t / 3600000)}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`
81
- }
82
- case "seconds":
83
- return `${Math.floor(t / 1000)}s`
84
- case "decimal":
85
- default: {
86
- // Promote units when rounding would show "60.0s" / "60.0m".
87
- if (t < 59950) return `${(t / 1000).toFixed(1)}s`
88
- if (t < 3597000) return `${(t / 60000).toFixed(1)}m`
89
- return `${(t / 3600000).toFixed(1)}h`
90
- }
91
- }
92
- }
93
-
94
- export function fmtTokens(n: number): string {
95
- if (n < 1000) return `${n}`
96
- if (n < 1000000) return `${(n / 1000).toFixed(1)}k`
97
- return `${(n / 1000000).toFixed(1)}M`
98
- }
99
-
100
- export function safeErrorMsg(err: unknown): string {
101
- if (!err) return ""
102
- if (typeof err === "string") return err
103
- if (typeof err === "object") return String((err as any).message || (err as any).code || "")
104
- return ""
105
- }
package/src/core/index.ts DELETED
@@ -1,6 +0,0 @@
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 DELETED
@@ -1,77 +0,0 @@
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
- * Optional atomic read-modify-write. When the host exposes it (V2), the
12
- * updater runs under the host storage lock against the latest on-disk value,
13
- * which is what makes concurrent TUI instances safe: no instance can
14
- * overwrite another one's fresh writes with a stale in-memory snapshot.
15
- */
16
- update?(key: string, updater: (current: unknown) => unknown): void | Promise<void>
17
- }
18
-
19
- /** Setting keys shared by V1/V2 (values are raw strings in KV). */
20
- export const SETTING_KEYS = {
21
- lang: `${KV_PREFIX}.lang`,
22
- maxEntries: `${KV_PREFIX}.max_entries`,
23
- order: `${KV_PREFIX}.order`,
24
- scrollMode: `${KV_PREFIX}.scroll_mode`,
25
- open: `${KV_PREFIX}.open`,
26
- ttlDays: `${KV_PREFIX}.ttl_days`,
27
- border: `${KV_PREFIX}.border`,
28
- showEntryCost: `${KV_PREFIX}.show_entry_cost`,
29
- showEntryTime: `${KV_PREFIX}.show_entry_time`,
30
- showEntryTokens: `${KV_PREFIX}.show_entry_tokens`,
31
- timeFormat: `${KV_PREFIX}.time_format`,
32
- dbSync: `${KV_PREFIX}.db_sync`,
33
- } as const
34
-
35
- export function loadSessionData(kv: KVApi): Record<string, SessionRecord> {
36
- try {
37
- const raw = kv.get(SESSION_DATA_KEY, "{}")
38
- return JSON.parse(String(raw))
39
- } catch { return {} }
40
- }
41
-
42
- /**
43
- * Atomic read-merge-write of the session store.
44
- *
45
- * `mutator` receives the latest persisted map (read under the host lock when
46
- * `kv.update` is available) and may mutate it freely; the result is written
47
- * back. Every writer must go through here: writing a whole in-memory snapshot
48
- * directly would silently drop entries created by the other TUIs.
49
- */
50
- export function updateSessionData(
51
- kv: KVApi,
52
- mutator: (data: Record<string, SessionRecord>) => void,
53
- ): void | Promise<void> {
54
- try {
55
- if (typeof kv.update === "function") {
56
- return kv.update(SESSION_DATA_KEY, (raw) => {
57
- const data = parseSessionData(raw)
58
- mutator(data)
59
- return JSON.stringify(data)
60
- })
61
- }
62
- // Fallback (V1 hosts without atomic update): read-modify-write. The window
63
- // is small and the mutation is merged against the value we just read.
64
- const data = loadSessionData(kv)
65
- mutator(data)
66
- kv.set(SESSION_DATA_KEY, JSON.stringify(data))
67
- } catch {}
68
- }
69
-
70
- function parseSessionData(raw: unknown): Record<string, SessionRecord> {
71
- try { return JSON.parse(String(raw ?? "{}")) } catch { return {} }
72
- }
73
-
74
- export function readTTLDays(kv: KVApi): number {
75
- const ttlDaysRaw = parseInt(String(kv.get(SETTING_KEYS.ttlDays, "3")), 10)
76
- return Number.isNaN(ttlDaysRaw) ? 3 : ttlDaysRaw
77
- }
@@ -1,46 +0,0 @@
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
package/src/core/types.ts DELETED
@@ -1,72 +0,0 @@
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
- /** How elapsed time is rendered in the sidebar (see TIME_FORMAT_SAMPLES). */
29
- export type TimeFormat = "decimal" | "short" | "clock" | "compact" | "seconds"
30
-
31
- /** OpenCode built-in tool names that spawn sub-agents or delegate tasks. */
32
- export const SUBAGENT_TOOLS = new Set(["task", "subagent", "delegate", "call_omo_agent"])
33
-
34
- export interface ChildRecord {
35
- scroll: number
36
- expanded: string
37
- entries: SubEntry[]
38
- clearedIds?: string[]
39
- }
40
-
41
- export interface SessionRecord {
42
- ts: number
43
- entries: SubEntry[]
44
- scroll: number
45
- expanded: string
46
- children: Record<string, ChildRecord>
47
- clearedIds?: string[]
48
- }
49
-
50
- export interface SharedSignals {
51
- lang: () => Lang
52
- setLang: (l: Lang) => void
53
- maxEntries: () => number
54
- setMaxEntries: (n: number) => void
55
- sortOrder: () => SortOrder
56
- setSortOrder: (o: SortOrder) => void
57
- scrollMode: () => ScrollMode
58
- setScrollMode: (m: ScrollMode) => void
59
- borderVisible: () => boolean
60
- setBorderVisible: (v: boolean) => void
61
- showEntryCost: () => boolean
62
- setShowEntryCost: (v: boolean) => void
63
- showEntryTime: () => boolean
64
- setShowEntryTime: (v: boolean) => void
65
- showEntryTokens: () => boolean
66
- setShowEntryTokens: (v: boolean) => void
67
- timeFormat: () => TimeFormat
68
- setTimeFormat: (f: TimeFormat) => void
69
- dbSync: () => boolean
70
- setDbSync: (v: boolean) => void
71
- sessionId: string
72
- }
package/src/core/usage.ts DELETED
@@ -1,16 +0,0 @@
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
- }