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
package/install.mjs CHANGED
@@ -1,103 +1,103 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Install script for opencode-subagent-magazine.
5
- *
6
- * Creates or updates `~/.config/opencode/tui.jsonc` so OpenCode loads
7
- * the TUI sidebar plugin.
8
- *
9
- * Usage:
10
- * node install.mjs
11
- * npm explore opencode-subagent-magazine -- node install.mjs
12
- */
13
-
14
- import { readFile, writeFile, mkdir, access } from "node:fs/promises"
15
- import { constants } from "node:fs"
16
- import { homedir, platform } from "node:os"
17
- import { join, dirname } from "node:path"
18
-
19
- const PLUGIN_SPEC = "opencode-subagent-magazine"
20
-
21
- function configDir() {
22
- if (platform() === "win32") {
23
- return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "opencode")
24
- }
25
- return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "opencode")
26
- }
27
-
28
- async function exists(p) {
29
- try { await access(p, constants.F_OK); return true }
30
- catch { return false }
31
- }
32
-
33
- async function readJSONC(p) {
34
- const raw = await readFile(p, "utf-8")
35
- const stripped = raw.replace(/^\s*\/\/.*$/gm, "")
36
- return JSON.parse(stripped)
37
- }
38
-
39
- function formatJSONC(obj) {
40
- return JSON.stringify(obj, null, 2) + "\n"
41
- }
42
-
43
- function mergePlugin(existing, spec) {
44
- const plugins = existing.plugin ?? []
45
- if (plugins.some((p) => (typeof p === "string" ? p : p[0]) === spec)) {
46
- return false
47
- }
48
- existing.plugin = [...plugins, spec]
49
- return true
50
- }
51
-
52
- async function main() {
53
- const dir = configDir()
54
- await mkdir(dir, { recursive: true })
55
-
56
- // ---- tui.jsonc ----
57
- const tuiPath = join(dir, "tui.jsonc")
58
- let tuiChanged = false
59
-
60
- if (await exists(tuiPath)) {
61
- const cfg = await readJSONC(tuiPath)
62
- tuiChanged = mergePlugin(cfg, PLUGIN_SPEC)
63
- if (tuiChanged) {
64
- await writeFile(tuiPath, formatJSONC(cfg))
65
- console.log(`[opencode-subagent-magazine] Added to ${tuiPath}`)
66
- } else {
67
- console.log(`[opencode-subagent-magazine] Already in ${tuiPath}`)
68
- }
69
- } else {
70
- const cfg = {
71
- $schema: "https://opencode.ai/tui.json",
72
- plugin: [PLUGIN_SPEC],
73
- }
74
- await writeFile(tuiPath, formatJSONC(cfg))
75
- console.log(`[opencode-subagent-magazine] Created ${tuiPath}`)
76
- tuiChanged = true
77
- }
78
-
79
- // ---- opencode.jsonc (forward compat) ----
80
- const ocPath = join(dir, "opencode.jsonc")
81
- let ocChanged = false
82
-
83
- if (await exists(ocPath)) {
84
- const cfg = await readJSONC(ocPath)
85
- ocChanged = mergePlugin(cfg, PLUGIN_SPEC)
86
- if (ocChanged) {
87
- await writeFile(ocPath, formatJSONC(cfg))
88
- console.log(`[opencode-subagent-magazine] Also added to ${ocPath}`)
89
- }
90
- }
91
-
92
- // ---- done ----
93
- if (tuiChanged || ocChanged) {
94
- console.log("\nDone! Restart OpenCode to see the SubAgent Monitor sidebar panel.")
95
- } else {
96
- console.log("\nAlready installed. Restart OpenCode if you haven't yet.")
97
- }
98
- }
99
-
100
- main().catch((err) => {
101
- console.error("Install failed:", err.message)
102
- process.exit(1)
103
- })
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Install script for opencode-subagent-magazine.
5
+ *
6
+ * Creates or updates `~/.config/opencode/tui.jsonc` so OpenCode loads
7
+ * the TUI sidebar plugin.
8
+ *
9
+ * Usage:
10
+ * node install.mjs
11
+ * npm explore opencode-subagent-magazine -- node install.mjs
12
+ */
13
+
14
+ import { readFile, writeFile, mkdir, access } from "node:fs/promises"
15
+ import { constants } from "node:fs"
16
+ import { homedir, platform } from "node:os"
17
+ import { join, dirname } from "node:path"
18
+
19
+ const PLUGIN_SPEC = "opencode-subagent-magazine"
20
+
21
+ function configDir() {
22
+ if (platform() === "win32") {
23
+ return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "opencode")
24
+ }
25
+ return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "opencode")
26
+ }
27
+
28
+ async function exists(p) {
29
+ try { await access(p, constants.F_OK); return true }
30
+ catch { return false }
31
+ }
32
+
33
+ async function readJSONC(p) {
34
+ const raw = await readFile(p, "utf-8")
35
+ const stripped = raw.replace(/^\s*\/\/.*$/gm, "")
36
+ return JSON.parse(stripped)
37
+ }
38
+
39
+ function formatJSONC(obj) {
40
+ return JSON.stringify(obj, null, 2) + "\n"
41
+ }
42
+
43
+ function mergePlugin(existing, spec) {
44
+ const plugins = existing.plugin ?? []
45
+ if (plugins.some((p) => (typeof p === "string" ? p : p[0]) === spec)) {
46
+ return false
47
+ }
48
+ existing.plugin = [...plugins, spec]
49
+ return true
50
+ }
51
+
52
+ async function main() {
53
+ const dir = configDir()
54
+ await mkdir(dir, { recursive: true })
55
+
56
+ // ---- tui.jsonc ----
57
+ const tuiPath = join(dir, "tui.jsonc")
58
+ let tuiChanged = false
59
+
60
+ if (await exists(tuiPath)) {
61
+ const cfg = await readJSONC(tuiPath)
62
+ tuiChanged = mergePlugin(cfg, PLUGIN_SPEC)
63
+ if (tuiChanged) {
64
+ await writeFile(tuiPath, formatJSONC(cfg))
65
+ console.log(`[opencode-subagent-magazine] Added to ${tuiPath}`)
66
+ } else {
67
+ console.log(`[opencode-subagent-magazine] Already in ${tuiPath}`)
68
+ }
69
+ } else {
70
+ const cfg = {
71
+ $schema: "https://opencode.ai/tui.json",
72
+ plugin: [PLUGIN_SPEC],
73
+ }
74
+ await writeFile(tuiPath, formatJSONC(cfg))
75
+ console.log(`[opencode-subagent-magazine] Created ${tuiPath}`)
76
+ tuiChanged = true
77
+ }
78
+
79
+ // ---- opencode.jsonc (forward compat) ----
80
+ const ocPath = join(dir, "opencode.jsonc")
81
+ let ocChanged = false
82
+
83
+ if (await exists(ocPath)) {
84
+ const cfg = await readJSONC(ocPath)
85
+ ocChanged = mergePlugin(cfg, PLUGIN_SPEC)
86
+ if (ocChanged) {
87
+ await writeFile(ocPath, formatJSONC(cfg))
88
+ console.log(`[opencode-subagent-magazine] Also added to ${ocPath}`)
89
+ }
90
+ }
91
+
92
+ // ---- done ----
93
+ if (tuiChanged || ocChanged) {
94
+ console.log("\nDone! Restart OpenCode to see the SubAgent Monitor sidebar panel.")
95
+ } else {
96
+ console.log("\nAlready installed. Restart OpenCode if you haven't yet.")
97
+ }
98
+ }
99
+
100
+ main().catch((err) => {
101
+ console.error("Install failed:", err.message)
102
+ process.exit(1)
103
+ })
package/package.json CHANGED
@@ -1,63 +1,64 @@
1
- {
2
- "name": "opencode-subagent-magazine",
3
- "version": "1.5.2",
4
- "description": "OpenCode TUI plugin monitoring sub-agent invocation status in real time",
5
- "type": "module",
6
- "types": "dist/index.d.ts",
7
- "exports": {
8
- "./server": {
9
- "import": "./dist/server.js",
10
- "types": "./dist/server.d.ts"
11
- },
12
- "./tui": {
13
- "import": "./dist/tui.js",
14
- "config": {
15
- "enabled": true
16
- }
17
- }
18
- },
19
- "files": [
20
- "dist",
21
- "src",
22
- "install.mjs",
23
- "README.md"
24
- ],
25
- "bin": {
26
- "opencode-subagent-magazine": "install.mjs"
27
- },
28
- "scripts": {
29
- "build": "tsc && node build.tui.mjs",
30
- "build:tui": "node build.tui.mjs",
31
- "typecheck": "tsc --noEmit",
32
- "version": "node -e \"require('fs').writeFileSync('src/_version.ts','// auto-generated\\nexport const PLUGIN_VERSION='+JSON.stringify(require('./package.json').version)+';\\n')\"",
33
- "prepublishOnly": "tsc"
34
- },
35
- "keywords": [
36
- "opencode",
37
- "opencode-plugin",
38
- "tui",
39
- "subagent",
40
- "subtask"
41
- ],
42
- "repository": {
43
- "type": "git",
44
- "url": "git+https://github.com/Hotakus/opencode-subagent-magazine.git"
45
- },
46
- "license": "MIT",
47
- "peerDependencies": {
48
- "@opencode-ai/plugin": ">=1.14.0",
49
- "@opencode-ai/sdk": ">=1.14.0",
50
- "@opentui/core": ">=0.2.0",
51
- "@opentui/solid": ">=0.2.0",
52
- "solid-js": ">=1.9.0"
53
- },
54
- "devDependencies": {
55
- "@opencode-ai/plugin": "^1.14.50",
56
- "@opencode-ai/sdk": "^1.14.50",
57
- "@types/node": "^22.0.0",
58
- "esbuild": "^0.25.0",
59
- "esbuild-plugin-solid": "^0.5.0",
60
- "tsx": "^4.22.3",
61
- "typescript": "^5.8.0"
62
- }
63
- }
1
+ {
2
+ "name": "opencode-subagent-magazine",
3
+ "version": "1.6.0-beta.0",
4
+ "description": "OpenCode TUI plugin monitoring sub-agent invocation status in real time",
5
+ "type": "module",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ "./server": {
9
+ "import": "./dist/server.js",
10
+ "types": "./dist/server.d.ts"
11
+ },
12
+ "./tui": {
13
+ "import": "./tui/index.js",
14
+ "config": {
15
+ "enabled": true
16
+ }
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "src",
22
+ "tui",
23
+ "install.mjs",
24
+ "README.md"
25
+ ],
26
+ "bin": {
27
+ "opencode-subagent-magazine": "install.mjs"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc && node build.tui.mjs",
31
+ "build:tui": "node build.tui.mjs",
32
+ "typecheck": "tsc --noEmit",
33
+ "version": "node -e \"require('fs').writeFileSync('src/_version.ts','// auto-generated\\nexport const PLUGIN_VERSION='+JSON.stringify(require('./package.json').version)+';\\n')\"",
34
+ "prepublishOnly": "tsc"
35
+ },
36
+ "keywords": [
37
+ "opencode",
38
+ "opencode-plugin",
39
+ "tui",
40
+ "subagent",
41
+ "subtask"
42
+ ],
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/Hotakus/opencode-subagent-magazine.git"
46
+ },
47
+ "license": "MIT",
48
+ "peerDependencies": {
49
+ "@opencode-ai/plugin": ">=1.14.0",
50
+ "@opencode-ai/sdk": ">=1.14.0",
51
+ "@opentui/core": ">=0.2.0",
52
+ "@opentui/solid": ">=0.2.0",
53
+ "solid-js": ">=1.9.0"
54
+ },
55
+ "devDependencies": {
56
+ "@opencode-ai/plugin": "^1.14.50",
57
+ "@opencode-ai/sdk": "^1.14.50",
58
+ "@types/node": "^22.0.0",
59
+ "esbuild": "^0.25.0",
60
+ "esbuild-plugin-solid": "^0.5.0",
61
+ "tsx": "^4.22.3",
62
+ "typescript": "^5.8.0"
63
+ }
64
+ }
package/src/_version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION="1.5.2";
2
+ export const PLUGIN_VERSION="1.6.0-beta.0";
package/src/clipboard.ts CHANGED
@@ -1,134 +1,134 @@
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
- }
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
+ }