@brimveyn/aimux 1.20.3 → 1.20.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.20.3",
3
+ "version": "1.20.4",
4
4
  "description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode, Kimi side-by-side with tabbed navigation, split panes, and persistent sessions.",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,13 +1,135 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+
1
3
  import { logDebug } from '../debug/input-log'
2
4
  import { toast } from '../state/toast-store'
3
5
 
6
+ export interface ClipboardCandidate {
7
+ argv: string[]
8
+ // powershell's Get-Clipboard emits CRLF line endings and appends a trailing
9
+ // newline of its own; both have to be undone to get the copied text back.
10
+ normalizeWindowsOutput?: boolean
11
+ }
12
+
13
+ export interface ClipboardPlatform {
14
+ env: Record<string, string | undefined>
15
+ isWsl: boolean
16
+ platform: string
17
+ }
18
+
19
+ const POWERSHELL_PASTE: ClipboardCandidate = {
20
+ argv: ['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', 'Get-Clipboard'],
21
+ normalizeWindowsOutput: true,
22
+ }
23
+
24
+ function isSet(value: string | undefined): boolean {
25
+ return value !== undefined && value !== ''
26
+ }
27
+
28
+ export function detectClipboardPlatform(): ClipboardPlatform {
29
+ return {
30
+ env: process.env,
31
+ isWsl: detectWsl(),
32
+ platform: process.platform,
33
+ }
34
+ }
35
+
36
+ function detectWsl(): boolean {
37
+ if (process.platform !== 'linux') return false
38
+ if (isSet(process.env.WSL_DISTRO_NAME) || isSet(process.env.WSL_INTEROP)) return true
39
+ try {
40
+ return readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft')
41
+ } catch {
42
+ return false
43
+ }
44
+ }
45
+
46
+ export function copyCandidates({ env, isWsl, platform }: ClipboardPlatform): ClipboardCandidate[] {
47
+ if (platform === 'darwin') return [{ argv: ['pbcopy'] }]
48
+ if (platform === 'win32') return [{ argv: ['clip'] }]
49
+
50
+ const candidates: ClipboardCandidate[] = []
51
+ // On WSL the Windows clipboard is the one the user pastes from, and clip.exe
52
+ // always reaches it. The X/Wayland bridges only exist under WSLg.
53
+ if (isWsl) {
54
+ candidates.push({ argv: ['clip.exe'] }, { argv: ['/mnt/c/Windows/System32/clip.exe'] })
55
+ }
56
+ const wayland: ClipboardCandidate = { argv: ['wl-copy'] }
57
+ const xorg: ClipboardCandidate[] = [
58
+ { argv: ['xclip', '-selection', 'clipboard'] },
59
+ { argv: ['xsel', '--clipboard', '--input'] },
60
+ ]
61
+ candidates.push(...(isSet(env.WAYLAND_DISPLAY) ? [wayland, ...xorg] : [...xorg, wayland]))
62
+ return candidates
63
+ }
64
+
65
+ export function pasteCandidates({ env, isWsl, platform }: ClipboardPlatform): ClipboardCandidate[] {
66
+ if (platform === 'darwin') return [{ argv: ['pbpaste'] }]
67
+ if (platform === 'win32') return [POWERSHELL_PASTE]
68
+
69
+ const candidates: ClipboardCandidate[] = []
70
+ if (isWsl) candidates.push(POWERSHELL_PASTE)
71
+ const wayland: ClipboardCandidate = { argv: ['wl-paste', '--no-newline'] }
72
+ const xorg: ClipboardCandidate[] = [
73
+ { argv: ['xclip', '-selection', 'clipboard', '-o'] },
74
+ { argv: ['xsel', '--clipboard', '--output'] },
75
+ ]
76
+ candidates.push(...(isSet(env.WAYLAND_DISPLAY) ? [wayland, ...xorg] : [...xorg, wayland]))
77
+ return candidates
78
+ }
79
+
80
+ function resolveCandidate(candidates: ClipboardCandidate[]): ClipboardCandidate | null {
81
+ for (const candidate of candidates) {
82
+ const [bin, ...args] = candidate.argv
83
+ if (bin === undefined) continue
84
+ let resolved: string | null
85
+ if (bin.includes('/')) {
86
+ resolved = existsSync(bin) ? bin : null
87
+ } else {
88
+ resolved = Bun.which(bin)
89
+ }
90
+ if (resolved !== null) return { ...candidate, argv: [resolved, ...args] }
91
+ }
92
+ return null
93
+ }
94
+
95
+ let cachedCopy: ClipboardCandidate | null | undefined
96
+ let cachedPaste: ClipboardCandidate | null | undefined
97
+
98
+ function copyCommand(): ClipboardCandidate | null {
99
+ cachedCopy ??= resolveCandidate(copyCandidates(detectClipboardPlatform()))
100
+ return cachedCopy
101
+ }
102
+
103
+ function pasteCommand(): ClipboardCandidate | null {
104
+ cachedPaste ??= resolveCandidate(pasteCandidates(detectClipboardPlatform()))
105
+ return cachedPaste
106
+ }
107
+
108
+ const MISSING_TOOL_MESSAGE =
109
+ process.platform === 'darwin'
110
+ ? 'Copy failed: pbcopy not found'
111
+ : 'Copy failed: install xclip, wl-clipboard, or xsel'
112
+
4
113
  export function copyToSystemClipboard(text: string): void {
114
+ const command = copyCommand()
115
+ if (!command) {
116
+ logDebug('platform.clipboard.noCopyCommand', { platform: process.platform })
117
+ toast.error(MISSING_TOOL_MESSAGE)
118
+ return
119
+ }
5
120
  try {
6
- const proc = Bun.spawn(['pbcopy'], { stdin: 'pipe' })
121
+ const proc = Bun.spawn(command.argv, { stderr: 'pipe', stdin: 'pipe' })
7
122
  void proc.stdin.write(text)
8
123
  void proc.stdin.end()
124
+ void (async () => {
125
+ const code = await proc.exited
126
+ if (code === 0) return
127
+ logDebug('platform.clipboard.copyExit', { argv: command.argv, code })
128
+ toast.error('Copy failed')
129
+ })()
9
130
  } catch (error) {
10
131
  logDebug('platform.clipboard.copyError', {
132
+ argv: command.argv,
11
133
  error: error instanceof Error ? error.message : String(error),
12
134
  })
13
135
  toast.error('Copy failed')
@@ -15,15 +137,29 @@ export function copyToSystemClipboard(text: string): void {
15
137
  }
16
138
 
17
139
  export async function readFromSystemClipboard(): Promise<string> {
140
+ const command = pasteCommand()
141
+ if (!command) {
142
+ logDebug('platform.clipboard.noPasteCommand', { platform: process.platform })
143
+ return ''
144
+ }
18
145
  try {
19
- const proc = Bun.spawn(['pbpaste'], { stdout: 'pipe' })
146
+ const proc = Bun.spawn(command.argv, { stderr: 'pipe', stdout: 'pipe' })
20
147
  const text = await new Response(proc.stdout).text()
21
- await proc.exited
22
- return text
148
+ const code = await proc.exited
149
+ if (code !== 0) {
150
+ logDebug('platform.clipboard.readExit', { argv: command.argv, code })
151
+ return ''
152
+ }
153
+ return command.normalizeWindowsOutput === true ? normalizeWindowsClipboardText(text) : text
23
154
  } catch (error) {
24
155
  logDebug('platform.clipboard.readError', {
156
+ argv: command.argv,
25
157
  error: error instanceof Error ? error.message : String(error),
26
158
  })
27
159
  return ''
28
160
  }
29
161
  }
162
+
163
+ export function normalizeWindowsClipboardText(text: string): string {
164
+ return text.replaceAll('\r\n', '\n').replace(/\n$/, '')
165
+ }