@brimveyn/aimux 1.18.3 → 1.18.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.18.3",
3
+ "version": "1.18.4",
4
4
  "description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
5
5
  "keywords": [
6
6
  "ai",
@@ -5,6 +5,16 @@ import { bracketedPaste, notationToBytes } from '../../chord'
5
5
  import { SHARED_FLAGS } from '../../flags'
6
6
  import { EXIT_OK, writeJson } from '../../output'
7
7
 
8
+ /**
9
+ * Gap between the bracketed-paste write and the trailing carriage return when
10
+ * `--enter` submits a pasted block. Claude Code (and other paste-aware TUIs)
11
+ * buffer every byte between the paste-start/paste-end markers; a `\r` that
12
+ * arrives in the same burst as the paste-end marker is folded into the paste
13
+ * buffer as literal content instead of being read as a submit keystroke. A
14
+ * short settle lets the receiver exit paste mode before the Enter lands.
15
+ */
16
+ const PASTE_SUBMIT_SETTLE_MS = 50
17
+
8
18
  /**
9
19
  * Lower a chord/paste buffer of bytes into the string the protocol expects.
10
20
  * Every byte we emit is < 0x80 (control chars or printable ASCII), so a
@@ -47,9 +57,18 @@ export const tabSend: CliCommand = {
47
57
  const appendEnter = ctx.args.flags.enter === true
48
58
 
49
59
  let data: string
60
+ // Whether `data` is a bracketed-paste block (multi-line text that
61
+ // `bracketedPaste` wrapped in start/end markers). A trailing `\r` must be
62
+ // sent as a *separate*, settled write for these — see PASTE_SUBMIT_SETTLE_MS.
63
+ let bracketed = false
50
64
  if (fromStdin) {
51
65
  const stdinText = await Bun.stdin.text()
52
- data = asKeys ? bytesToString(notationToBytes(stdinText)) : bracketedPaste(stdinText)
66
+ if (asKeys) {
67
+ data = bytesToString(notationToBytes(stdinText))
68
+ } else {
69
+ data = bracketedPaste(stdinText)
70
+ bracketed = data !== stdinText
71
+ }
53
72
  } else {
54
73
  const text = ctx.args.positionals[1] ?? ''
55
74
  if (asKeys) {
@@ -57,13 +76,10 @@ export const tabSend: CliCommand = {
57
76
  data = bytesToString(notationToBytes(text))
58
77
  } else {
59
78
  data = bracketedPaste(text)
79
+ bracketed = data !== text
60
80
  }
61
81
  }
62
82
 
63
- if (appendEnter) {
64
- data = `${data}\r`
65
- }
66
-
67
83
  const workspace = ctx.getWorkspace()
68
84
  const daemon = await ctx.getDaemon()
69
85
  if (!daemon.hasCapability(IPC_CAPABILITY_THIN_ATTACH)) {
@@ -75,7 +91,20 @@ export const tabSend: CliCommand = {
75
91
  await daemon.attach({ cols: 0, rows: 0, sessionId: workspace.id, thin: true })
76
92
  await daemon.expectOk('write', { data, tabId })
77
93
 
78
- writeJson({ bytesWritten: Buffer.byteLength(data, 'utf8'), ok: true })
94
+ let bytesWritten = Buffer.byteLength(data, 'utf8')
95
+ if (appendEnter) {
96
+ // A bracketed paste swallows a same-burst `\r`, so settle first, then
97
+ // submit as an independent write. Plain text / key chords carry no paste
98
+ // markers, so the Enter can follow immediately — but still as its own
99
+ // write so the two paths stay uniform.
100
+ if (bracketed) {
101
+ await Bun.sleep(PASTE_SUBMIT_SETTLE_MS)
102
+ }
103
+ await daemon.expectOk('write', { data: '\r', tabId })
104
+ bytesWritten += 1
105
+ }
106
+
107
+ writeJson({ bytesWritten, ok: true })
79
108
  return EXIT_OK
80
109
  },
81
110
  summary: 'Write text or a key chord to a tab',
@@ -111,7 +111,21 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
111
111
  const orderedSnapshotTabs = snapshot.tabs
112
112
  .map((persistedTab) => this.tabs.get(persistedTab.id))
113
113
  .filter((tab): tab is TabSession => tab !== undefined)
114
- const normalizedTabs = normalizeGroupedTabOrder(orderedSnapshotTabs, layoutTrees, tabGroupMap)
114
+ // The registry not the snapshot — is the source of truth for *which*
115
+ // tabs exist. A tab spawned by a sibling CLI after this workspace's
116
+ // snapshot was last persisted lives in `this.tabs` but is absent from
117
+ // `snapshot.tabs`; ordering by the snapshot alone would silently drop it
118
+ // from the attach result, so the UI would render an empty/stale workspace
119
+ // on switch. Append any live tab the snapshot doesn't mention (in
120
+ // registry order) so membership stays authoritative while the snapshot
121
+ // still supplies ordering + layout for the tabs it captured.
122
+ const snapshotIds = new Set(snapshot.tabs.map((persistedTab) => persistedTab.id))
123
+ const extraLiveTabs = tabs.filter((tab) => !snapshotIds.has(tab.id))
124
+ const normalizedTabs = normalizeGroupedTabOrder(
125
+ [...orderedSnapshotTabs, ...extraLiveTabs],
126
+ layoutTrees,
127
+ tabGroupMap
128
+ )
115
129
 
116
130
  return { activeTabId: this.activeTabId, tabs: normalizedTabs }
117
131
  }