@duetso/agent 0.1.56 → 0.1.58
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/dist/package.json +2 -2
- package/dist/src/cli/memory-db.d.ts +1 -1
- package/dist/src/cli/memory-db.js +3 -2
- package/dist/src/cli/memory-db.js.map +1 -1
- package/dist/src/memory/context-pack.d.ts +3 -3
- package/dist/src/memory/context-pack.d.ts.map +1 -1
- package/dist/src/memory/context-pack.js +2 -2
- package/dist/src/memory/loader.d.ts +12 -45
- package/dist/src/memory/loader.d.ts.map +1 -1
- package/dist/src/memory/loader.js +61 -77
- package/dist/src/memory/loader.js.map +1 -1
- package/dist/src/memory/migrations.d.ts.map +1 -1
- package/dist/src/memory/migrations.js +28 -0
- package/dist/src/memory/migrations.js.map +1 -1
- package/dist/src/memory/observational-prompts.d.ts.map +1 -1
- package/dist/src/memory/observational-prompts.js +3 -0
- package/dist/src/memory/observational-prompts.js.map +1 -1
- package/dist/src/memory/observational.d.ts +15 -3
- package/dist/src/memory/observational.d.ts.map +1 -1
- package/dist/src/memory/observational.js +117 -40
- package/dist/src/memory/observational.js.map +1 -1
- package/dist/src/memory/recall.js +2 -1
- package/dist/src/memory/recall.js.map +1 -1
- package/dist/src/memory/storage.d.ts +48 -27
- package/dist/src/memory/storage.d.ts.map +1 -1
- package/dist/src/memory/storage.js +65 -42
- package/dist/src/memory/storage.js.map +1 -1
- package/dist/src/memory/store.d.ts +20 -21
- package/dist/src/memory/store.d.ts.map +1 -1
- package/dist/src/memory/store.js +15 -99
- package/dist/src/memory/store.js.map +1 -1
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +146 -51
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/tui/clipboard.d.ts +16 -11
- package/dist/src/tui/clipboard.d.ts.map +1 -1
- package/dist/src/tui/clipboard.js +16 -11
- package/dist/src/tui/clipboard.js.map +1 -1
- package/dist/src/tui/theme.d.ts +12 -1
- package/dist/src/tui/theme.d.ts.map +1 -1
- package/dist/src/tui/theme.js +15 -1
- package/dist/src/tui/theme.js.map +1 -1
- package/dist/src/tui/transcript-log.d.ts +1 -1
- package/dist/src/tui/transcript-log.js +1 -1
- package/dist/src/turn-runner/tools.d.ts +1 -1
- package/dist/src/turn-runner/turn-runner.d.ts +2 -2
- package/dist/src/turn-runner/turn-runner.d.ts.map +1 -1
- package/dist/src/turn-runner/turn-runner.js +9 -5
- package/dist/src/turn-runner/turn-runner.js.map +1 -1
- package/dist/src/types/memory.d.ts +20 -36
- package/dist/src/types/memory.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/src/tui/app.js
CHANGED
|
@@ -9,7 +9,7 @@ import { DUET_BANNER_LINES_COMPACT, historyDisplayBlocks, limitHistoryDisplayMes
|
|
|
9
9
|
import { listRecentSessions } from "./recent-sessions.js";
|
|
10
10
|
import { createSidebar, SIDEBAR_WIDTH } from "./sidebar.js";
|
|
11
11
|
import { orderedSelectableStarters, selectStarters } from "./starters.js";
|
|
12
|
-
import { COLORS, HINT_IDLE, HINT_RUNNING } from "./theme.js";
|
|
12
|
+
import { COLORS, HINT_IDLE, HINT_RUNNING, HINT_SELECTION_COPY } from "./theme.js";
|
|
13
13
|
// Re-exports preserve the historical `tui/app.js` entry point used by tests
|
|
14
14
|
// and external callers; the implementations live in focused leaf modules.
|
|
15
15
|
export { activeFileAutocompleteToken, activeSkillAutocompleteToken, fileAutocompleteMatches, formatQuestionOptionDescription, formatSkillAutocompleteDescription, moveQuestionOptionSelection, moveSkillAutocompleteSelection, questionPickerAnswerPayload, replaceFileAutocompleteToken, replaceSkillAutocompleteToken, skillAutocompleteMatches, } from "./autocomplete.js";
|
|
@@ -30,11 +30,17 @@ const QUESTION_OPTION_LIMIT = AUTOCOMPLETE_LIMITS.questionOption;
|
|
|
30
30
|
export async function runTui(input) {
|
|
31
31
|
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
|
|
32
32
|
// useMouse: true so the scroll wheel reaches the transcript
|
|
33
|
-
// ScrollBoxRenderable
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
33
|
+
// ScrollBoxRenderable and so OpenTUI receives drag events for in-app
|
|
34
|
+
// text selection. Selected text is captured via the renderer's
|
|
35
|
+
// `selection` event below and copied to the clipboard via OSC 52 (or
|
|
36
|
+
// CLI fallback) on the platform-appropriate copy keystroke (Cmd+C on
|
|
37
|
+
// macOS, Ctrl+Shift+C elsewhere) or `/copy`. PageUp/PageDown and
|
|
38
|
+
// Shift+Up/Down keyboard bindings below cover terminals or sessions
|
|
39
|
+
// where the wheel does not reach us (e.g. tmux without mouse mode).
|
|
40
|
+
//
|
|
41
|
+
// Bare Ctrl+C remains the always-exit keystroke (handled via
|
|
42
|
+
// exitOnCtrlC) so the convention every other interactive Linux/Windows
|
|
43
|
+
// terminal app follows still works here.
|
|
38
44
|
const renderer = await createCliRenderer({
|
|
39
45
|
exitOnCtrlC: true,
|
|
40
46
|
useMouse: true,
|
|
@@ -42,6 +48,16 @@ export async function runTui(input) {
|
|
|
42
48
|
targetFps: 60,
|
|
43
49
|
});
|
|
44
50
|
restoreWindowGlobal(previousWindow);
|
|
51
|
+
// Most recent drag-selected text. OpenTUI emits the `selection` event
|
|
52
|
+
// whenever a drag finishes; we cache the resulting string so /copy and
|
|
53
|
+
// the copy keystroke can prefer the user's actual highlight over the
|
|
54
|
+
// last-message heuristic, and so the bottom hint can advertise the
|
|
55
|
+
// copy keystroke only while it actually does something.
|
|
56
|
+
let lastSelectionText = "";
|
|
57
|
+
renderer.on("selection", (selection) => {
|
|
58
|
+
lastSelectionText = selection.getSelectedText();
|
|
59
|
+
refreshHint();
|
|
60
|
+
});
|
|
45
61
|
// Outer row wraps the main column and a right-side sidebar that surfaces
|
|
46
62
|
// the runner's current todo list and state-machine progress.
|
|
47
63
|
const root = new BoxRenderable(renderer, {
|
|
@@ -71,17 +87,22 @@ export async function runTui(input) {
|
|
|
71
87
|
borderColor: COLORS.border,
|
|
72
88
|
padding: 1,
|
|
73
89
|
});
|
|
90
|
+
// Status and hint chrome are excluded from drag-select so a highlight that
|
|
91
|
+
// sweeps the bottom of the screen does not pull the spinner / hint text
|
|
92
|
+
// into the clipboard alongside the transcript content the user wanted.
|
|
74
93
|
const status = new TextRenderable(renderer, {
|
|
75
94
|
content: "",
|
|
76
95
|
fg: COLORS.status,
|
|
77
96
|
height: 1,
|
|
78
97
|
flexShrink: 0,
|
|
98
|
+
selectable: false,
|
|
79
99
|
});
|
|
80
100
|
const hint = new TextRenderable(renderer, {
|
|
81
101
|
content: HINT_IDLE,
|
|
82
102
|
fg: COLORS.hint,
|
|
83
103
|
height: 1,
|
|
84
104
|
flexShrink: 0,
|
|
105
|
+
selectable: false,
|
|
85
106
|
});
|
|
86
107
|
const skillAutocompletePanel = new BoxRenderable(renderer, {
|
|
87
108
|
flexDirection: "column",
|
|
@@ -99,11 +120,15 @@ export async function runTui(input) {
|
|
|
99
120
|
// length so a one-line description doesn't leave an empty trailing line
|
|
100
121
|
// beneath the name. The renderer sets `height` whenever it writes
|
|
101
122
|
// `content`.
|
|
123
|
+
// Autocomplete and panel chrome are not part of the transcript content,
|
|
124
|
+
// so exclude them from drag-select to keep the clipboard focused on
|
|
125
|
+
// assistant/user messages.
|
|
102
126
|
const makeItemRow = () => {
|
|
103
127
|
const row = new TextRenderable(renderer, {
|
|
104
128
|
content: "",
|
|
105
129
|
fg: COLORS.hint,
|
|
106
130
|
flexShrink: 0,
|
|
131
|
+
selectable: false,
|
|
107
132
|
});
|
|
108
133
|
row.visible = false;
|
|
109
134
|
return row;
|
|
@@ -113,6 +138,7 @@ export async function runTui(input) {
|
|
|
113
138
|
fg: COLORS.status,
|
|
114
139
|
height: 1,
|
|
115
140
|
flexShrink: 0,
|
|
141
|
+
selectable: false,
|
|
116
142
|
});
|
|
117
143
|
const commandHeader = makeHeaderRow("commands");
|
|
118
144
|
const commandRows = Array.from({ length: BUILT_IN_SLASH_COMMANDS.length }, makeItemRow);
|
|
@@ -140,6 +166,7 @@ export async function runTui(input) {
|
|
|
140
166
|
fg: COLORS.status,
|
|
141
167
|
height: 1,
|
|
142
168
|
flexShrink: 0,
|
|
169
|
+
selectable: false,
|
|
143
170
|
});
|
|
144
171
|
const fileAutocompleteRows = Array.from({ length: FILE_AUTOCOMPLETE_LIMIT }, () => {
|
|
145
172
|
const row = new TextRenderable(renderer, {
|
|
@@ -147,6 +174,7 @@ export async function runTui(input) {
|
|
|
147
174
|
fg: COLORS.hint,
|
|
148
175
|
height: 1,
|
|
149
176
|
flexShrink: 0,
|
|
177
|
+
selectable: false,
|
|
150
178
|
});
|
|
151
179
|
row.visible = false;
|
|
152
180
|
return row;
|
|
@@ -169,11 +197,13 @@ export async function runTui(input) {
|
|
|
169
197
|
fg: COLORS.agent,
|
|
170
198
|
wrapMode: "word",
|
|
171
199
|
flexShrink: 0,
|
|
200
|
+
selectable: false,
|
|
172
201
|
});
|
|
173
202
|
const questionSpacer = new TextRenderable(renderer, {
|
|
174
203
|
content: "",
|
|
175
204
|
height: 1,
|
|
176
205
|
flexShrink: 0,
|
|
206
|
+
selectable: false,
|
|
177
207
|
});
|
|
178
208
|
const questionRows = Array.from({ length: QUESTION_OPTION_LIMIT }, () => {
|
|
179
209
|
const row = new TextRenderable(renderer, {
|
|
@@ -181,6 +211,7 @@ export async function runTui(input) {
|
|
|
181
211
|
fg: COLORS.hint,
|
|
182
212
|
wrapMode: "word",
|
|
183
213
|
flexShrink: 0,
|
|
214
|
+
selectable: false,
|
|
184
215
|
});
|
|
185
216
|
row.visible = false;
|
|
186
217
|
return row;
|
|
@@ -198,10 +229,14 @@ export async function runTui(input) {
|
|
|
198
229
|
paddingRight: 1,
|
|
199
230
|
flexShrink: 0,
|
|
200
231
|
});
|
|
232
|
+
// The leading "> " sigil is decoration, not content; excluding it from
|
|
233
|
+
// selection means a drag that starts at the input row does not pull the
|
|
234
|
+
// sigil into the clipboard alongside the highlighted text.
|
|
201
235
|
const prompt = new TextRenderable(renderer, {
|
|
202
236
|
content: "> ",
|
|
203
237
|
fg: COLORS.user,
|
|
204
238
|
width: 2,
|
|
239
|
+
selectable: false,
|
|
205
240
|
});
|
|
206
241
|
// Textarea (rather than Input) so long messages soft-wrap visually. Enter
|
|
207
242
|
// is intercepted in onKeyDown below to submit instead of inserting a newline.
|
|
@@ -252,13 +287,21 @@ export async function runTui(input) {
|
|
|
252
287
|
}
|
|
253
288
|
function setHint(running) {
|
|
254
289
|
const base = running ? HINT_RUNNING : HINT_IDLE;
|
|
255
|
-
|
|
290
|
+
const segments = [];
|
|
291
|
+
if (pendingImages.length > 0)
|
|
292
|
+
segments.push(attachmentHint());
|
|
293
|
+
segments.push(base);
|
|
294
|
+
if (lastSelectionText.trim().length > 0)
|
|
295
|
+
segments.push(HINT_SELECTION_COPY);
|
|
296
|
+
hint.content = segments.join(" · ");
|
|
256
297
|
}
|
|
257
298
|
function attachmentHint() {
|
|
258
299
|
const n = pendingImages.length;
|
|
259
300
|
return n === 1 ? "📎 1 image attached" : `📎 ${n} images attached`;
|
|
260
301
|
}
|
|
261
|
-
|
|
302
|
+
// Single-channel hint refresh used by every input that affects what the
|
|
303
|
+
// bottom row should advertise (running state, attachments, selection).
|
|
304
|
+
function refreshHint() {
|
|
262
305
|
setHint(running);
|
|
263
306
|
}
|
|
264
307
|
// ---- runtime state ---------------------------------------------------------
|
|
@@ -272,7 +315,7 @@ export async function runTui(input) {
|
|
|
272
315
|
let nextImageId = 1;
|
|
273
316
|
// Parallel record of user/agent message bodies driven by the same code
|
|
274
317
|
// paths that render them into the transcript. The `/copy` slash command
|
|
275
|
-
// and
|
|
318
|
+
// and copy keystroke read from this log instead of trying to walk the
|
|
276
319
|
// ScrollBoxRenderable, which only stores presentation lines.
|
|
277
320
|
const transcriptLog = [];
|
|
278
321
|
function recordTranscriptEntry(kind, text) {
|
|
@@ -850,29 +893,6 @@ export async function runTui(input) {
|
|
|
850
893
|
let pendingQuestions = [];
|
|
851
894
|
let questionOptionSelectedIndex = 0;
|
|
852
895
|
let suppressNextEscapeExit = false;
|
|
853
|
-
let closingAfterInterrupt = false;
|
|
854
|
-
const requestExit = async () => {
|
|
855
|
-
if (running) {
|
|
856
|
-
if (closingAfterInterrupt)
|
|
857
|
-
return;
|
|
858
|
-
closingAfterInterrupt = true;
|
|
859
|
-
stopWorkingTicker();
|
|
860
|
-
setStatus("● interrupting…");
|
|
861
|
-
try {
|
|
862
|
-
await input.session.interrupt();
|
|
863
|
-
await input.session.waitForTerminal();
|
|
864
|
-
}
|
|
865
|
-
catch (error) {
|
|
866
|
-
reportError(error);
|
|
867
|
-
}
|
|
868
|
-
finally {
|
|
869
|
-
renderer.destroy();
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
else {
|
|
873
|
-
renderer.destroy();
|
|
874
|
-
}
|
|
875
|
-
};
|
|
876
896
|
function skillAutocompleteIsOpen() {
|
|
877
897
|
return Boolean(skillAutocompleteToken && skillAutocompleteItems.length > 0);
|
|
878
898
|
}
|
|
@@ -1149,7 +1169,7 @@ export async function runTui(input) {
|
|
|
1149
1169
|
return;
|
|
1150
1170
|
}
|
|
1151
1171
|
key.preventDefault();
|
|
1152
|
-
|
|
1172
|
+
handleEscape();
|
|
1153
1173
|
});
|
|
1154
1174
|
// Keyboard scroll bindings for the transcript. Mirrors the mouse wheel
|
|
1155
1175
|
// for terminals that swallow mouse events (tmux without mouse mode, ssh
|
|
@@ -1232,13 +1252,29 @@ export async function runTui(input) {
|
|
|
1232
1252
|
void triggerClipboardProbe("keystroke");
|
|
1233
1253
|
return;
|
|
1234
1254
|
}
|
|
1235
|
-
//
|
|
1236
|
-
//
|
|
1237
|
-
//
|
|
1238
|
-
//
|
|
1239
|
-
|
|
1255
|
+
// Copy keystrokes. The set is intentionally generous because each
|
|
1256
|
+
// mainstream terminal forwards a different subset:
|
|
1257
|
+
//
|
|
1258
|
+
// - Cmd+C: macOS muscle memory; many terminals (Terminal.app, Warp)
|
|
1259
|
+
// own this for their own selection UI and never forward it.
|
|
1260
|
+
// - Cmd+Shift+C: forwarded by Warp on macOS and by some configs of
|
|
1261
|
+
// iTerm2 / Ghostty where Cmd+C is reserved.
|
|
1262
|
+
// - Ctrl+Shift+C: Linux/Windows terminal-app convention that
|
|
1263
|
+
// leaves bare Ctrl+C free for "exit."
|
|
1264
|
+
//
|
|
1265
|
+
// The hint label surfaces only the OS-natural one so the bottom row
|
|
1266
|
+
// stays terse. No-op when nothing is selected so the keystroke still
|
|
1267
|
+
// falls through to whatever the terminal would do natively. We accept
|
|
1268
|
+
// both "c" and "C" because some kitty parsers report the shifted
|
|
1269
|
+
// letter while others report the base letter plus shift=true.
|
|
1270
|
+
const isCopyLetter = key.name === "c" || key.name === "C";
|
|
1271
|
+
const cmdHeld = key.super || key.meta;
|
|
1272
|
+
const isCmdC = isCopyLetter && cmdHeld && !key.shift && !key.ctrl;
|
|
1273
|
+
const isCmdShiftC = isCopyLetter && cmdHeld && key.shift && !key.ctrl;
|
|
1274
|
+
const isCtrlShiftC = isCopyLetter && key.ctrl && key.shift && !cmdHeld;
|
|
1275
|
+
if ((isCmdC || isCmdShiftC || isCtrlShiftC) && lastSelectionText.trim().length > 0) {
|
|
1240
1276
|
key.preventDefault();
|
|
1241
|
-
void
|
|
1277
|
+
void copyActiveSelection();
|
|
1242
1278
|
return;
|
|
1243
1279
|
}
|
|
1244
1280
|
if (skillAutocompleteIsOpen()) {
|
|
@@ -1335,10 +1371,20 @@ export async function runTui(input) {
|
|
|
1335
1371
|
return;
|
|
1336
1372
|
}
|
|
1337
1373
|
if (key.name === "escape") {
|
|
1338
|
-
|
|
1374
|
+
handleEscape();
|
|
1339
1375
|
return;
|
|
1340
1376
|
}
|
|
1341
1377
|
};
|
|
1378
|
+
// Esc interrupts the in-flight turn; when nothing is running it is a
|
|
1379
|
+
// no-op so muscle memory does not eject the user out of the session.
|
|
1380
|
+
// Quitting goes through Ctrl+C (renderer's exitOnCtrlC) or closing the
|
|
1381
|
+
// terminal — both paths drain through the `finally` block in
|
|
1382
|
+
// cli/run.ts that disposes the SessionManager and flushes PGlite.
|
|
1383
|
+
function handleEscape() {
|
|
1384
|
+
if (!running)
|
|
1385
|
+
return;
|
|
1386
|
+
void input.session.interrupt().catch(reportError);
|
|
1387
|
+
}
|
|
1342
1388
|
inputField.onContentChange = () => {
|
|
1343
1389
|
// First real keystroke into the input collapses the starter section
|
|
1344
1390
|
// — the user is composing their own prompt, so the suggestions get
|
|
@@ -1405,7 +1451,7 @@ export async function runTui(input) {
|
|
|
1405
1451
|
pendingImages.push(pending);
|
|
1406
1452
|
inputField.insertText(pending.label);
|
|
1407
1453
|
appendBlock("[paste]", `attached ${pending.label} from ${pending.path}`, COLORS.system);
|
|
1408
|
-
|
|
1454
|
+
refreshHint();
|
|
1409
1455
|
}
|
|
1410
1456
|
catch (error) {
|
|
1411
1457
|
// The clipboard looked like an image path but we could not load
|
|
@@ -1445,7 +1491,7 @@ export async function runTui(input) {
|
|
|
1445
1491
|
pendingImages.push(pending);
|
|
1446
1492
|
inputField.insertText(pending.label);
|
|
1447
1493
|
appendBlock("[paste]", `attached ${pending.label} (${mimeType}, ${formatBytes(bytes.length)})`, COLORS.system);
|
|
1448
|
-
|
|
1494
|
+
refreshHint();
|
|
1449
1495
|
}
|
|
1450
1496
|
catch (error) {
|
|
1451
1497
|
appendBlock("[paste]", error instanceof Error ? error.message : String(error), COLORS.error);
|
|
@@ -1456,7 +1502,7 @@ export async function runTui(input) {
|
|
|
1456
1502
|
return;
|
|
1457
1503
|
pendingImages = [];
|
|
1458
1504
|
nextImageId = 1;
|
|
1459
|
-
|
|
1505
|
+
refreshHint();
|
|
1460
1506
|
}
|
|
1461
1507
|
// Manual clipboard probe. Read the OS clipboard for an image right now and
|
|
1462
1508
|
// attach it if found; otherwise emit a useful diagnostic line. Used both by
|
|
@@ -1541,27 +1587,63 @@ export async function runTui(input) {
|
|
|
1541
1587
|
markRunning();
|
|
1542
1588
|
}
|
|
1543
1589
|
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Copy the active drag-selection to the clipboard and clear the highlight
|
|
1592
|
+
* so the user gets visual confirmation the action happened. Used by the
|
|
1593
|
+
* platform copy keystroke (Cmd+C on macOS, Ctrl+Shift+C elsewhere); the
|
|
1594
|
+
* slash command path goes through `handleCopySlashCommand` so it can
|
|
1595
|
+
* also serve `/copy last|all|<N>`.
|
|
1596
|
+
*/
|
|
1597
|
+
async function copyActiveSelection() {
|
|
1598
|
+
const text = lastSelectionText;
|
|
1599
|
+
if (text.trim().length === 0)
|
|
1600
|
+
return;
|
|
1601
|
+
const result = await copyTextToClipboard(text);
|
|
1602
|
+
renderer.clearSelection();
|
|
1603
|
+
lastSelectionText = "";
|
|
1604
|
+
refreshHint();
|
|
1605
|
+
if (result.ok) {
|
|
1606
|
+
appendBlock("[copy]", `copied selection (${text.length} char${text.length === 1 ? "" : "s"}) to clipboard via ${result.via}`, COLORS.system);
|
|
1607
|
+
}
|
|
1608
|
+
else {
|
|
1609
|
+
appendBlock("[copy]", `clipboard write failed: ${result.error ?? "unknown error"}` +
|
|
1610
|
+
(process.platform === "linux" ? "\nInstall one of: wl-clipboard, xclip, xsel" : ""), COLORS.error);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1544
1613
|
/**
|
|
1545
1614
|
* Resolve a `/copy ...` invocation to clipboard text and pipe it to the
|
|
1546
|
-
* OS clipboard.
|
|
1547
|
-
*
|
|
1548
|
-
*
|
|
1615
|
+
* OS clipboard. When the user has an active drag-selection and ran a bare
|
|
1616
|
+
* `/copy`, copy that highlight verbatim — it matches what they actually
|
|
1617
|
+
* have on screen. Otherwise fall back to the transcript-log heuristic
|
|
1618
|
+
* (`last` / `all` / `<N>`).
|
|
1619
|
+
*
|
|
1620
|
+
* Failures are surfaced in the transcript so users on minimal Linux
|
|
1621
|
+
* installs see exactly which writer is missing.
|
|
1549
1622
|
*/
|
|
1550
1623
|
async function handleCopySlashCommand(raw) {
|
|
1551
1624
|
const argumentRaw = raw === "/copy" ? "" : raw.slice("/copy ".length);
|
|
1552
1625
|
const argument = parseCopyArgument(argumentRaw);
|
|
1553
1626
|
if (argument === undefined) {
|
|
1554
|
-
appendBlock("[copy]", "Usage: /copy [last|all|<N>] — last (default) copies the most recent agent reply
|
|
1627
|
+
appendBlock("[copy]", "Usage: /copy [last|all|<N>] — last (default) copies the most recent agent reply, " +
|
|
1628
|
+
"or copies the active drag-selection when one is present", COLORS.system);
|
|
1555
1629
|
return;
|
|
1556
1630
|
}
|
|
1557
|
-
|
|
1631
|
+
// A bare `/copy` (or the copy keystroke while a selection is active)
|
|
1632
|
+
// prefers the drag-selection so the clipboard matches what the user
|
|
1633
|
+
// has highlighted on screen; an explicit `/copy last|all|<N>` always
|
|
1634
|
+
// uses the transcript log instead.
|
|
1635
|
+
const explicitArgument = argumentRaw.trim().length > 0;
|
|
1636
|
+
const useSelection = !explicitArgument && lastSelectionText.trim().length > 0;
|
|
1637
|
+
const text = useSelection ? lastSelectionText : selectCopyText(transcriptLog, argument);
|
|
1558
1638
|
if (!text) {
|
|
1559
1639
|
appendBlock("[copy]", "nothing to copy yet", COLORS.system);
|
|
1560
1640
|
return;
|
|
1561
1641
|
}
|
|
1562
|
-
const result = await
|
|
1642
|
+
const result = await copyTextToClipboard(text);
|
|
1563
1643
|
if (result.ok) {
|
|
1564
|
-
const summary =
|
|
1644
|
+
const summary = useSelection
|
|
1645
|
+
? `selection (${text.length} char${text.length === 1 ? "" : "s"})`
|
|
1646
|
+
: describeCopySelection(argument, text.length);
|
|
1565
1647
|
appendBlock("[copy]", `copied ${summary} to clipboard via ${result.via}`, COLORS.system);
|
|
1566
1648
|
}
|
|
1567
1649
|
else {
|
|
@@ -1569,6 +1651,19 @@ export async function runTui(input) {
|
|
|
1569
1651
|
(process.platform === "linux" ? "\nInstall one of: wl-clipboard, xclip, xsel" : ""), COLORS.error);
|
|
1570
1652
|
}
|
|
1571
1653
|
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Two-stage clipboard write. OpenTUI's OSC 52 path is preferred because it
|
|
1656
|
+
* lands in the user's clipboard even when the TUI is running over SSH or
|
|
1657
|
+
* inside tmux — no host CLI required. When the terminal does not
|
|
1658
|
+
* advertise OSC 52 support (or stdout is gone), fall back to the
|
|
1659
|
+
* platform-native CLI writers in `clipboard.ts`.
|
|
1660
|
+
*/
|
|
1661
|
+
async function copyTextToClipboard(text) {
|
|
1662
|
+
if (renderer.isOsc52Supported() && renderer.copyToClipboardOSC52(text)) {
|
|
1663
|
+
return { ok: true, via: "OSC 52" };
|
|
1664
|
+
}
|
|
1665
|
+
return writeClipboardText(text);
|
|
1666
|
+
}
|
|
1572
1667
|
function describeCopySelection(argument, length) {
|
|
1573
1668
|
const chars = `${length} char${length === 1 ? "" : "s"}`;
|
|
1574
1669
|
if (argument === "last")
|
|
@@ -1595,7 +1690,7 @@ export async function runTui(input) {
|
|
|
1595
1690
|
// can keep typing their prompt with the image already attached.
|
|
1596
1691
|
inputField.insertText(pending.label);
|
|
1597
1692
|
appendBlock("[paste]", `attached ${pending.label} from ${pending.path}`, COLORS.system);
|
|
1598
|
-
|
|
1693
|
+
refreshHint();
|
|
1599
1694
|
}
|
|
1600
1695
|
catch (error) {
|
|
1601
1696
|
appendBlock("[paste]", error instanceof Error ? error.message : String(error), COLORS.error);
|