@brimveyn/aimux 1.15.1 → 1.16.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.
- package/package.json +1 -1
- package/src/app-runtime/selection-clipboard-dedup.ts +21 -0
- package/src/app-runtime/selection-scroll.ts +5 -1
- package/src/app-runtime/use-mouse-handlers.ts +5 -2
- package/src/app-runtime/use-renderer-bindings.ts +16 -2
- package/src/app-runtime/use-terminal-resize.ts +19 -1
- package/src/pty/pty-manager.ts +19 -0
package/package.json
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// opentui re-emits 'finishSelection' on re-renders, focus changes, and
|
|
2
|
+
// viewport refreshes. Each of those would call copyToSystemClipboard with the
|
|
3
|
+
// *same* text, blindly clobbering whatever the user just copied in another
|
|
4
|
+
// app. We dedupe by content: aimux never writes the same selection twice in
|
|
5
|
+
// a row to the system clipboard, so an external copy survives any phantom
|
|
6
|
+
// re-emit of the previous in-aimux selection.
|
|
7
|
+
//
|
|
8
|
+
// The dedup state is reset when the renderer reports an empty selection
|
|
9
|
+
// (deselect), so a user who deliberately reselects the same text after
|
|
10
|
+
// clicking elsewhere still gets it copied.
|
|
11
|
+
let lastWrittenText: string | null = null
|
|
12
|
+
|
|
13
|
+
export function shouldWriteSelectionToClipboard(text: string): boolean {
|
|
14
|
+
if (text === lastWrittenText) return false
|
|
15
|
+
lastWrittenText = text
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resetSelectionClipboardDedup(): void {
|
|
20
|
+
lastWrittenText = null
|
|
21
|
+
}
|
|
@@ -126,11 +126,15 @@ export function applyViewportObservation(
|
|
|
126
126
|
next: ViewportObservation | null
|
|
127
127
|
): ViewportObservation | null {
|
|
128
128
|
if (!next) {
|
|
129
|
-
if (prior !== null)
|
|
129
|
+
if (prior !== null) {
|
|
130
|
+
renderer.clearSelection()
|
|
131
|
+
resetSelectionShiftState(renderer)
|
|
132
|
+
}
|
|
130
133
|
return null
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
if (!prior || prior.tabId !== next.tabId) {
|
|
137
|
+
if (prior && prior.tabId !== next.tabId) renderer.clearSelection()
|
|
134
138
|
resetSelectionShiftState(renderer)
|
|
135
139
|
return next
|
|
136
140
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from './click-selection-resolver'
|
|
22
22
|
import { recordMultiClickClipboardWrite } from './multi-click-clipboard-guard'
|
|
23
23
|
import { requestRenderUpTree } from './render-invalidation'
|
|
24
|
+
import { shouldWriteSelectionToClipboard } from './selection-clipboard-dedup'
|
|
24
25
|
import {
|
|
25
26
|
type AnchoredRatioDragState,
|
|
26
27
|
type AxisDragState,
|
|
@@ -146,7 +147,9 @@ function applyMultiClickInitialSelection(
|
|
|
146
147
|
finishDragging: false,
|
|
147
148
|
})
|
|
148
149
|
requestRenderUpTree(selection.target)
|
|
149
|
-
|
|
150
|
+
if (shouldWriteSelectionToClipboard(selection.selectedText)) {
|
|
151
|
+
copyToSystemClipboard(selection.selectedText)
|
|
152
|
+
}
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
export function useMouseHandlers({
|
|
@@ -573,7 +576,7 @@ export function useMouseHandlers({
|
|
|
573
576
|
const anchorIdx = drag.anchorAbsRow - startAbs
|
|
574
577
|
const focusIdx = drag.focusAbsRow - startAbs
|
|
575
578
|
const text = extractStreamText(lines, anchorIdx, anchorCol, focusIdx, drag.focusCol)
|
|
576
|
-
if (text.length > 0) {
|
|
579
|
+
if (text.length > 0 && shouldWriteSelectionToClipboard(text)) {
|
|
577
580
|
copyToSystemClipboard(text)
|
|
578
581
|
recordMultiClickClipboardWrite()
|
|
579
582
|
}
|
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
import { shouldSuppressSelectionCopy } from './multi-click-clipboard-guard'
|
|
24
24
|
import { writeMacroExpansionToTab, writePasteToTab, writeToTab } from './pty-write'
|
|
25
25
|
import { type OtuiSelection, resolveSelectionClipboardText } from './selection-clipboard'
|
|
26
|
+
import {
|
|
27
|
+
resetSelectionClipboardDedup,
|
|
28
|
+
shouldWriteSelectionToClipboard,
|
|
29
|
+
} from './selection-clipboard-dedup'
|
|
26
30
|
import { applyViewportObservation, type ViewportObservation } from './selection-scroll'
|
|
27
31
|
|
|
28
32
|
const BRACKETED_PASTE_ENABLE_SEQUENCE = '\x1b[?2004h'
|
|
@@ -239,7 +243,13 @@ export function useRendererBindings({
|
|
|
239
243
|
textLength: selectedText.length,
|
|
240
244
|
})
|
|
241
245
|
|
|
242
|
-
if (selection.isDragging === true
|
|
246
|
+
if (selection.isDragging === true) {
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
if (selectedText.length === 0) {
|
|
250
|
+
// Deselect → forget the last-written text so a deliberate reselection
|
|
251
|
+
// of the same text writes it again.
|
|
252
|
+
resetSelectionClipboardDedup()
|
|
243
253
|
return
|
|
244
254
|
}
|
|
245
255
|
|
|
@@ -254,7 +264,11 @@ export function useRendererBindings({
|
|
|
254
264
|
}
|
|
255
265
|
|
|
256
266
|
renderer.copyToClipboardOSC52(selectedText)
|
|
257
|
-
|
|
267
|
+
if (shouldWriteSelectionToClipboard(selectedText)) {
|
|
268
|
+
copyToSystemClipboard(selectedText)
|
|
269
|
+
} else {
|
|
270
|
+
logInputDebug('app.selection.dedup', { textLength: selectedText.length })
|
|
271
|
+
}
|
|
258
272
|
}
|
|
259
273
|
|
|
260
274
|
renderer.prependInputHandler(handler)
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
forEachSplitPaneRect,
|
|
20
20
|
toTerminalContentSize,
|
|
21
21
|
} from '../state/layout-resize'
|
|
22
|
+
import { getTreeForTab } from '../state/layout-tree'
|
|
22
23
|
|
|
23
24
|
const MAIN_AREA_HORIZONTAL_CHROME = 2
|
|
24
25
|
const MAIN_AREA_VERTICAL_PADDING = 0
|
|
@@ -161,6 +162,19 @@ export function useTerminalResize({
|
|
|
161
162
|
|
|
162
163
|
const activeTabIdRef = useRef(state.activeTabId)
|
|
163
164
|
activeTabIdRef.current = state.activeTabId
|
|
165
|
+
// Whether the active tab is part of a split. contentOriginRef must hold the
|
|
166
|
+
// whole terminal-area origin (root.tsx derives splitContentOrigin from it and
|
|
167
|
+
// SplitLayout re-adds each pane's bounds offset). When split, the active
|
|
168
|
+
// pane's measured box is pane-relative, so adopting it as the area origin
|
|
169
|
+
// would double-count the pane offset and push the hardware cursor off along
|
|
170
|
+
// the split axis. Track it so handleMeasure can skip the x/y overwrite.
|
|
171
|
+
const activeIsSplitRef = useRef(false)
|
|
172
|
+
{
|
|
173
|
+
const id = state.activeTabId
|
|
174
|
+
const tree =
|
|
175
|
+
id != null && id !== '' ? getTreeForTab(state.layoutTrees, state.tabGroupMap, id) : null
|
|
176
|
+
activeIsSplitRef.current = tree?.type === 'split'
|
|
177
|
+
}
|
|
164
178
|
// Last size we pushed to the backend per tab, used to dedupe the measurement
|
|
165
179
|
// loop so an unchanged box never re-triggers a resize.
|
|
166
180
|
const measuredRef = useRef(new Map<string, { cols: number; rows: number }>())
|
|
@@ -187,7 +201,11 @@ export function useTerminalResize({
|
|
|
187
201
|
const cols = Math.max(PTY_MIN_COLS, rect.cols)
|
|
188
202
|
const rows = Math.max(PTY_MIN_ROWS, rect.rows)
|
|
189
203
|
const isActive = tabId === activeTabIdRef.current
|
|
190
|
-
|
|
204
|
+
// Only adopt the measured box as the terminal-area origin when the active
|
|
205
|
+
// pane spans the whole area (no split). In a split the box is pane-local;
|
|
206
|
+
// keep the model-derived area origin from the terminalSize memo below so
|
|
207
|
+
// the hardware cursor isn't offset by the pane's bounds (see ref comment).
|
|
208
|
+
if (isActive && !activeIsSplitRef.current) {
|
|
191
209
|
contentOriginRef.current = { cols, rows, x: rect.x, y: rect.y }
|
|
192
210
|
}
|
|
193
211
|
const prev = measuredRef.current.get(tabId)
|
package/src/pty/pty-manager.ts
CHANGED
|
@@ -341,6 +341,25 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
341
341
|
return false
|
|
342
342
|
})
|
|
343
343
|
|
|
344
|
+
// Wire the emulator's reply channel back to the pty. When a program
|
|
345
|
+
// writes a query sequence — CPR (ESC[6n), Device Attributes (ESC[c),
|
|
346
|
+
// Device Status (ESC[5n), DECRQM, OSC color queries — xterm generates
|
|
347
|
+
// the answer and emits it via onData. Without forwarding it the program
|
|
348
|
+
// waits forever for a response it never gets (e.g. AWS CLI / Python
|
|
349
|
+
// prompt_toolkit warns "terminal doesn't support cursor position
|
|
350
|
+
// requests (CPR)"). These replies are produced synchronously inside the
|
|
351
|
+
// emulator.write() below, so write straight to the pty rather than
|
|
352
|
+
// queueing through the render path. The listener is disposed together
|
|
353
|
+
// with the emulator in finalize/dispose, mirroring registerCsiHandler.
|
|
354
|
+
//
|
|
355
|
+
// onBinary (legacy non-UTF-8 mouse reports) is intentionally not wired:
|
|
356
|
+
// bun-pty.write() only accepts a UTF-8 string so byte values >127 can't
|
|
357
|
+
// be transmitted faithfully, and the headless emulator is never fed raw
|
|
358
|
+
// mouse input, so it never generates those reports in the first place.
|
|
359
|
+
emulator.onData((reply) => {
|
|
360
|
+
session.pty.write(reply)
|
|
361
|
+
})
|
|
362
|
+
|
|
344
363
|
pty.onData((data) => {
|
|
345
364
|
logDebug('ptyManager.data', {
|
|
346
365
|
byteLength: Buffer.byteLength(data, 'utf8'),
|