@pellux/goodvibes-tui 0.19.95 → 0.19.98
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/CHANGELOG.md +15 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/input/command-registry.ts +5 -0
- package/src/input/commands/shell-core.ts +18 -1
- package/src/input/feed-context-factory.ts +3 -0
- package/src/input/handler-command-route.ts +27 -0
- package/src/input/handler-content-actions.ts +36 -4
- package/src/input/handler-feed-routes.ts +55 -35
- package/src/input/handler-feed.ts +14 -0
- package/src/input/handler.ts +11 -3
- package/src/input/input-history.ts +8 -19
- package/src/main.ts +1 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.19.98] — 2026-05-11
|
|
8
|
+
|
|
9
|
+
### Changes
|
|
10
|
+
- 6fa4c84b fix: pass owned project root to command paste
|
|
11
|
+
|
|
12
|
+
## [0.19.97] — 2026-05-11
|
|
13
|
+
|
|
14
|
+
### Changes
|
|
15
|
+
- 256454c2 fix: support explicit clipboard image paste
|
|
16
|
+
|
|
17
|
+
## [0.19.96] — 2026-05-11
|
|
18
|
+
|
|
19
|
+
### Changes
|
|
20
|
+
- 77abb7be fix: navigate multiline prompt history
|
|
21
|
+
|
|
7
22
|
## [0.19.95] — 2026-05-11
|
|
8
23
|
|
|
9
24
|
### Changes
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/mgd34msu/goodvibes-tui)
|
|
6
6
|
|
|
7
7
|
A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
|
|
8
8
|
|
|
@@ -1278,6 +1278,7 @@ Those pieces cover conversation-noise routing, panel-health/performance budgets,
|
|
|
1278
1278
|
| `/keybindings` | `/kb` | List current keyboard bindings and their config file path |
|
|
1279
1279
|
| `/schedule [action]` | `/sched` | Manage scheduled agent tasks (cron): add, list, remove, enable, disable, run |
|
|
1280
1280
|
| `/image <path>` | `/img` | Attach an image file to the next message |
|
|
1281
|
+
| `/paste` | `/clip` | Pull supported text or image data directly from the system clipboard into the prompt |
|
|
1281
1282
|
| `/refresh-models` | — | Refresh model catalog, benchmarks, and token limits |
|
|
1282
1283
|
| `/notify [action]` | `/ntf` | Manage webhook notifications (ntfy.sh): add, remove, list, clear, test |
|
|
1283
1284
|
| `/voice [action]` | — | Review optional voice posture and export/inspect voice bundles |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.98",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -61,6 +61,11 @@ export interface CommandUiActions {
|
|
|
61
61
|
submitInput?: (text: string, content?: import('@pellux/goodvibes-sdk/platform/providers').ContentPart[]) => void;
|
|
62
62
|
submitSpokenInput?: (text: string, content?: import('@pellux/goodvibes-sdk/platform/providers').ContentPart[]) => void;
|
|
63
63
|
stopSpokenOutput?: () => void;
|
|
64
|
+
pasteFromClipboard?: () => {
|
|
65
|
+
pasted: boolean;
|
|
66
|
+
kind: 'image' | 'text' | 'none';
|
|
67
|
+
marker?: string;
|
|
68
|
+
};
|
|
64
69
|
executeCommand?: (name: string, args: string[]) => Promise<boolean>;
|
|
65
70
|
cancelGeneration?: () => void;
|
|
66
71
|
completeModelSelection?: (selection: {
|
|
@@ -91,6 +91,22 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
91
91
|
},
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
registry.register({
|
|
95
|
+
name: 'paste',
|
|
96
|
+
aliases: ['clip'],
|
|
97
|
+
description: 'Insert clipboard text or image into the prompt',
|
|
98
|
+
handler(_args, ctx) {
|
|
99
|
+
if (!ctx.pasteFromClipboard) {
|
|
100
|
+
ctx.print('Paste is not available in this context.');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const result = ctx.pasteFromClipboard();
|
|
104
|
+
if (!result.pasted) {
|
|
105
|
+
ctx.print('Clipboard does not contain supported text or image data.');
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
94
110
|
registry.register({
|
|
95
111
|
name: 'help',
|
|
96
112
|
aliases: ['h', '?'],
|
|
@@ -135,6 +151,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
135
151
|
{ id: '/template save', label: '/template save <name>', detail: 'Save prompt as template', category: 'Templates' },
|
|
136
152
|
{ id: '/template use', label: '/template use <name>', detail: 'Execute template', category: 'Templates' },
|
|
137
153
|
{ id: '/tools', label: '/tools', detail: 'List available tools', category: 'Tools & System' },
|
|
154
|
+
{ id: '/paste', label: '/paste', detail: 'Insert clipboard text or image into the prompt', category: 'Tools & System' },
|
|
138
155
|
{ id: '/shortcuts', label: '/shortcuts', detail: 'View keyboard shortcuts reference', category: 'Tools & System' },
|
|
139
156
|
{ id: '/commands', label: '/commands', detail: 'Browse all commands in a scrollable list', category: 'Tools & System' },
|
|
140
157
|
{ id: '/secrets', label: '/secrets set|link|get|test|list|delete', detail: 'Manage encrypted and provider-backed secrets', category: 'Tools & System' },
|
|
@@ -154,7 +171,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
154
171
|
});
|
|
155
172
|
return;
|
|
156
173
|
}
|
|
157
|
-
ctx.print('Use /help to open the help modal. Commands: /model, /provider, /config, /template, /tools, /sessions, /bookmarks, /save, /load, /undo, /redo, /retry, /clear, /reset, /compact, /export, /title, /effort, /expand, /collapse, /debug, /quit, /wq');
|
|
174
|
+
ctx.print('Use /help to open the help modal. Commands: /model, /provider, /config, /template, /tools, /paste, /sessions, /bookmarks, /save, /load, /undo, /redo, /retry, /clear, /reset, /compact, /export, /title, /effort, /expand, /collapse, /debug, /quit, /wq');
|
|
158
175
|
},
|
|
159
176
|
});
|
|
160
177
|
|
|
@@ -56,6 +56,7 @@ import type { PanelMouseLayout } from './handler-feed-routes.ts';
|
|
|
56
56
|
export interface FeedContextMutableInit {
|
|
57
57
|
prompt: string;
|
|
58
58
|
cursorPos: number;
|
|
59
|
+
inputScrollTop: number;
|
|
59
60
|
commandMode: boolean;
|
|
60
61
|
panelFocused: boolean;
|
|
61
62
|
indicatorFocused: boolean;
|
|
@@ -97,6 +98,7 @@ export interface FeedContextStableRefs {
|
|
|
97
98
|
selection: SelectionManager;
|
|
98
99
|
pasteRegistry: Map<string, string>;
|
|
99
100
|
imageRegistry: Map<string, { data: string; mediaType: string }>;
|
|
101
|
+
projectRoot: string;
|
|
100
102
|
selectionModal: SelectionModal;
|
|
101
103
|
bookmarkModal: BookmarkModal;
|
|
102
104
|
settingsModal: SettingsModal;
|
|
@@ -230,6 +232,7 @@ export function syncFeedContextMutableFields(
|
|
|
230
232
|
): void {
|
|
231
233
|
ctx.prompt = fields.prompt;
|
|
232
234
|
ctx.cursorPos = fields.cursorPos;
|
|
235
|
+
ctx.inputScrollTop = fields.inputScrollTop;
|
|
233
236
|
ctx.commandMode = fields.commandMode;
|
|
234
237
|
ctx.panelFocused = fields.panelFocused;
|
|
235
238
|
ctx.indicatorFocused = fields.indicatorFocused;
|
|
@@ -4,6 +4,7 @@ import type { AutocompleteEngine } from './autocomplete.ts';
|
|
|
4
4
|
import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
|
|
5
5
|
import type { ConversationManager } from '../core/conversation';
|
|
6
6
|
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
7
|
+
import { handleClipboardPaste, type ClipboardPasteSource } from './handler-content-actions.ts';
|
|
7
8
|
|
|
8
9
|
export type CommandModeRouteState = {
|
|
9
10
|
commandMode: boolean;
|
|
@@ -18,6 +19,14 @@ export type CommandModeRouteState = {
|
|
|
18
19
|
conversationManager: ConversationManager | null;
|
|
19
20
|
requestRender: () => void;
|
|
20
21
|
handleEscape: () => void;
|
|
22
|
+
projectRoot: string;
|
|
23
|
+
pasteRegistry: Map<string, string>;
|
|
24
|
+
imageRegistry: Map<string, { data: string; mediaType: string }>;
|
|
25
|
+
nextPasteId: number;
|
|
26
|
+
nextImageId: number;
|
|
27
|
+
saveUndoState: () => void;
|
|
28
|
+
ensureInputCursorVisible: () => void;
|
|
29
|
+
clipboard?: ClipboardPasteSource;
|
|
21
30
|
};
|
|
22
31
|
|
|
23
32
|
export function handleCommandModeToken(state: CommandModeRouteState, token: InputToken): boolean {
|
|
@@ -141,6 +150,24 @@ function withPanelFocusSync(context: CommandContext, state: CommandModeRouteStat
|
|
|
141
150
|
state.panelFocused = false;
|
|
142
151
|
}
|
|
143
152
|
: undefined,
|
|
153
|
+
pasteFromClipboard: () => {
|
|
154
|
+
const result = handleClipboardPaste({
|
|
155
|
+
prompt: state.prompt,
|
|
156
|
+
cursorPos: state.cursorPos,
|
|
157
|
+
pasteRegistry: state.pasteRegistry,
|
|
158
|
+
nextPasteId: state.nextPasteId,
|
|
159
|
+
imageRegistry: state.imageRegistry,
|
|
160
|
+
nextImageId: state.nextImageId,
|
|
161
|
+
saveUndoState: state.saveUndoState,
|
|
162
|
+
ensureInputCursorVisible: state.ensureInputCursorVisible,
|
|
163
|
+
requestRender: state.requestRender,
|
|
164
|
+
}, context.workspace.shellPaths?.workingDirectory ?? state.projectRoot, state.clipboard);
|
|
165
|
+
state.prompt = result.prompt;
|
|
166
|
+
state.cursorPos = result.cursorPos;
|
|
167
|
+
state.nextImageId = result.nextImageId;
|
|
168
|
+
state.nextPasteId = result.nextPasteId;
|
|
169
|
+
return result;
|
|
170
|
+
},
|
|
144
171
|
executeCommand: async (name, args) => {
|
|
145
172
|
const wrapped = withPanelFocusSync(context, state);
|
|
146
173
|
const handled = state.commandRegistry?.get(name)
|
|
@@ -56,6 +56,23 @@ export type PasteRegistryState = {
|
|
|
56
56
|
nextImageId: number;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
export type ClipboardPasteKind = 'image' | 'text' | 'none';
|
|
60
|
+
|
|
61
|
+
export interface ClipboardPasteResult {
|
|
62
|
+
prompt: string;
|
|
63
|
+
cursorPos: number;
|
|
64
|
+
nextImageId: number;
|
|
65
|
+
nextPasteId: number;
|
|
66
|
+
pasted: boolean;
|
|
67
|
+
kind: ClipboardPasteKind;
|
|
68
|
+
marker?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ClipboardPasteSource {
|
|
72
|
+
pasteImageFromClipboard: typeof pasteImageFromClipboard;
|
|
73
|
+
pasteFromClipboard: typeof pasteFromClipboard;
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
export function registerPaste(
|
|
60
77
|
state: PasteRegistryState,
|
|
61
78
|
content: string,
|
|
@@ -436,22 +453,34 @@ export function handleClipboardPaste(
|
|
|
436
453
|
requestRender: () => void;
|
|
437
454
|
},
|
|
438
455
|
projectRoot: string,
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const img = pasteImageFromClipboard();
|
|
456
|
+
clipboard: ClipboardPasteSource = { pasteImageFromClipboard, pasteFromClipboard },
|
|
457
|
+
): ClipboardPasteResult {
|
|
458
|
+
const img = clipboard.pasteImageFromClipboard();
|
|
459
|
+
let pasted = false;
|
|
460
|
+
let kind: ClipboardPasteKind = 'none';
|
|
461
|
+
let insertedMarker: string | undefined;
|
|
462
|
+
|
|
442
463
|
if (img) {
|
|
464
|
+
state.saveUndoState();
|
|
443
465
|
const id = `img${state.nextImageId++}`;
|
|
444
466
|
const sizeKB = Math.round(img.data.length * 3 / 4 / 1024);
|
|
445
467
|
state.imageRegistry.set(id, img);
|
|
446
468
|
const marker = `[IMAGE: ${id}, clipboard, ${sizeKB}KB]`;
|
|
447
469
|
state.prompt = state.prompt.slice(0, state.cursorPos) + marker + state.prompt.slice(state.cursorPos);
|
|
448
470
|
state.cursorPos += marker.length;
|
|
471
|
+
pasted = true;
|
|
472
|
+
kind = 'image';
|
|
473
|
+
insertedMarker = marker;
|
|
449
474
|
} else {
|
|
450
|
-
const raw = pasteFromClipboard();
|
|
475
|
+
const raw = clipboard.pasteFromClipboard();
|
|
451
476
|
if (raw) {
|
|
477
|
+
state.saveUndoState();
|
|
452
478
|
const { marker } = registerPaste(state, raw, projectRoot);
|
|
453
479
|
state.prompt = state.prompt.slice(0, state.cursorPos) + marker + state.prompt.slice(state.cursorPos);
|
|
454
480
|
state.cursorPos += marker.length;
|
|
481
|
+
pasted = true;
|
|
482
|
+
kind = marker.startsWith('[IMAGE:') ? 'image' : 'text';
|
|
483
|
+
insertedMarker = marker;
|
|
455
484
|
}
|
|
456
485
|
}
|
|
457
486
|
state.ensureInputCursorVisible();
|
|
@@ -461,5 +490,8 @@ export function handleClipboardPaste(
|
|
|
461
490
|
cursorPos: state.cursorPos,
|
|
462
491
|
nextImageId: state.nextImageId,
|
|
463
492
|
nextPasteId: state.nextPasteId,
|
|
493
|
+
pasted,
|
|
494
|
+
kind,
|
|
495
|
+
marker: insertedMarker,
|
|
464
496
|
};
|
|
465
497
|
}
|
|
@@ -6,6 +6,11 @@ import type { CommandRegistry, CommandContext } from './command-registry.ts';
|
|
|
6
6
|
import type { AutocompleteEngine } from './autocomplete.ts';
|
|
7
7
|
import type { SelectionManager } from './selection.ts';
|
|
8
8
|
import type { WrappedPromptInfo } from './handler-prompt-buffer.ts';
|
|
9
|
+
import {
|
|
10
|
+
ensureInputCursorVisible as computeInputScrollTop,
|
|
11
|
+
getWrappedPromptInfo as computeWrappedPromptInfo,
|
|
12
|
+
moveCursorVertical as computeCursorVerticalMove,
|
|
13
|
+
} from './handler-prompt-buffer.ts';
|
|
9
14
|
import { cleanupMarkerRegistry, expandPrompt, findMarkerAtPos, registerPaste } from './handler-content-actions.ts';
|
|
10
15
|
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
11
16
|
import type { KeybindingsManager } from './keybindings.ts';
|
|
@@ -243,8 +248,10 @@ export function handlePromptTextToken(state: TextRouteState, token: InputToken):
|
|
|
243
248
|
export type KeyRouteState = {
|
|
244
249
|
prompt: string;
|
|
245
250
|
cursorPos: number;
|
|
251
|
+
inputScrollTop: number;
|
|
246
252
|
commandMode: boolean;
|
|
247
253
|
contentWidth: number;
|
|
254
|
+
maxInputRows: number;
|
|
248
255
|
inputHistory: InputHistory | null;
|
|
249
256
|
indicatorFocused: boolean;
|
|
250
257
|
conversationManager: ConversationManager | null;
|
|
@@ -271,6 +278,7 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
|
|
|
271
278
|
handled: boolean;
|
|
272
279
|
prompt: string;
|
|
273
280
|
cursorPos: number;
|
|
281
|
+
inputScrollTop: number;
|
|
274
282
|
commandMode: boolean;
|
|
275
283
|
indicatorFocused: boolean;
|
|
276
284
|
} {
|
|
@@ -279,6 +287,7 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
|
|
|
279
287
|
handled: false,
|
|
280
288
|
prompt: state.prompt,
|
|
281
289
|
cursorPos: state.cursorPos,
|
|
290
|
+
inputScrollTop: state.inputScrollTop,
|
|
282
291
|
commandMode: state.commandMode,
|
|
283
292
|
indicatorFocused: state.indicatorFocused,
|
|
284
293
|
};
|
|
@@ -286,8 +295,12 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
|
|
|
286
295
|
|
|
287
296
|
let prompt = state.prompt;
|
|
288
297
|
let cursorPos = state.cursorPos;
|
|
298
|
+
let inputScrollTop = state.inputScrollTop;
|
|
289
299
|
let commandMode = state.commandMode;
|
|
290
300
|
let indicatorFocused = state.indicatorFocused;
|
|
301
|
+
const ensureLocalInputCursorVisible = () => {
|
|
302
|
+
inputScrollTop = computeInputScrollTop(prompt, cursorPos, inputScrollTop, state.contentWidth, state.maxInputRows);
|
|
303
|
+
};
|
|
291
304
|
const runQuitShortcut = (commandName: 'quit' | 'wq') => {
|
|
292
305
|
if (state.commandContext?.executeCommand) {
|
|
293
306
|
void state.commandContext.executeCommand(commandName, []).catch((error) => {
|
|
@@ -304,15 +317,15 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
|
|
|
304
317
|
if (!state.handlePathCompletion()) {
|
|
305
318
|
state.handleBlockToggle();
|
|
306
319
|
}
|
|
307
|
-
return { handled: true, prompt, cursorPos, commandMode, indicatorFocused };
|
|
320
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
308
321
|
}
|
|
309
322
|
|
|
310
323
|
if (token.logicalName === 'enter') {
|
|
311
324
|
if (token.shift) {
|
|
312
325
|
prompt = prompt.slice(0, cursorPos) + '\n' + prompt.slice(cursorPos);
|
|
313
326
|
cursorPos++;
|
|
314
|
-
|
|
315
|
-
return { handled: true, prompt, cursorPos, commandMode, indicatorFocused };
|
|
327
|
+
ensureLocalInputCursorVisible();
|
|
328
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
316
329
|
}
|
|
317
330
|
|
|
318
331
|
const text = prompt.trim();
|
|
@@ -323,7 +336,7 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
|
|
|
323
336
|
state.modalOpened('blockActions');
|
|
324
337
|
state.blockActionsMenu.open(nearest);
|
|
325
338
|
state.requestRender();
|
|
326
|
-
return { handled: true, prompt, cursorPos, commandMode, indicatorFocused };
|
|
339
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
327
340
|
}
|
|
328
341
|
}
|
|
329
342
|
if (text === ':q' || text === ':wq') {
|
|
@@ -331,7 +344,7 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
|
|
|
331
344
|
cursorPos = 0;
|
|
332
345
|
runQuitShortcut(text === ':wq' ? 'wq' : 'quit');
|
|
333
346
|
state.requestRender();
|
|
334
|
-
return { handled: true, prompt, cursorPos, commandMode, indicatorFocused };
|
|
347
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
335
348
|
}
|
|
336
349
|
if (text) {
|
|
337
350
|
const expanded = state.expandPrompt(text);
|
|
@@ -354,7 +367,7 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
|
|
|
354
367
|
state.commandContext?.submitInput?.(textOnly, expanded);
|
|
355
368
|
}
|
|
356
369
|
}
|
|
357
|
-
return { handled: true, prompt, cursorPos, commandMode, indicatorFocused };
|
|
370
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
358
371
|
}
|
|
359
372
|
|
|
360
373
|
if (token.logicalName === 'backspace') {
|
|
@@ -376,9 +389,9 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
|
|
|
376
389
|
prompt = prompt.slice(0, cursorPos - 1) + prompt.slice(cursorPos);
|
|
377
390
|
cursorPos--;
|
|
378
391
|
}
|
|
379
|
-
|
|
392
|
+
ensureLocalInputCursorVisible();
|
|
380
393
|
}
|
|
381
|
-
return { handled: true, prompt, cursorPos, commandMode, indicatorFocused };
|
|
394
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
382
395
|
}
|
|
383
396
|
|
|
384
397
|
if (token.logicalName === 'delete') {
|
|
@@ -392,52 +405,58 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
|
|
|
392
405
|
} else {
|
|
393
406
|
prompt = prompt.slice(0, cursorPos) + prompt.slice(cursorPos + 1);
|
|
394
407
|
}
|
|
395
|
-
|
|
408
|
+
ensureLocalInputCursorVisible();
|
|
396
409
|
}
|
|
397
410
|
state.requestRender();
|
|
398
|
-
return { handled: true, prompt, cursorPos, commandMode, indicatorFocused };
|
|
411
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
399
412
|
}
|
|
400
413
|
|
|
401
414
|
if (token.logicalName === 'left') {
|
|
402
415
|
if (cursorPos > 0) {
|
|
403
416
|
const marker = state.findMarkerAtPos(cursorPos);
|
|
404
417
|
cursorPos = marker ? marker.start : cursorPos - 1;
|
|
405
|
-
|
|
418
|
+
ensureLocalInputCursorVisible();
|
|
406
419
|
}
|
|
407
420
|
state.requestRender();
|
|
408
|
-
return { handled: true, prompt, cursorPos, commandMode, indicatorFocused };
|
|
421
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
409
422
|
}
|
|
410
423
|
|
|
411
424
|
if (token.logicalName === 'right') {
|
|
412
425
|
if (cursorPos < prompt.length) {
|
|
413
426
|
const marker = state.findMarkerAtPos(cursorPos + 1);
|
|
414
427
|
cursorPos = marker ? marker.end : cursorPos + 1;
|
|
415
|
-
|
|
428
|
+
ensureLocalInputCursorVisible();
|
|
416
429
|
}
|
|
417
430
|
state.requestRender();
|
|
418
|
-
return { handled: true, prompt, cursorPos, commandMode, indicatorFocused };
|
|
431
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
419
432
|
}
|
|
420
433
|
|
|
421
434
|
if (token.logicalName === 'home') {
|
|
422
435
|
cursorPos = 0;
|
|
423
|
-
|
|
436
|
+
ensureLocalInputCursorVisible();
|
|
437
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
424
438
|
}
|
|
425
439
|
|
|
426
440
|
if (token.logicalName === 'end') {
|
|
427
441
|
cursorPos = prompt.length;
|
|
428
|
-
|
|
442
|
+
ensureLocalInputCursorVisible();
|
|
443
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
429
444
|
}
|
|
430
445
|
|
|
431
446
|
if (token.logicalName === 'up') {
|
|
432
|
-
|
|
433
|
-
|
|
447
|
+
const move = computeCursorVerticalMove(prompt, cursorPos, inputScrollTop, state.contentWidth, state.maxInputRows, -1);
|
|
448
|
+
if (move.moved) {
|
|
449
|
+
cursorPos = move.cursorPos;
|
|
450
|
+
inputScrollTop = move.inputScrollTop;
|
|
451
|
+
} else {
|
|
452
|
+
const info = computeWrappedPromptInfo(prompt, cursorPos, inputScrollTop, state.contentWidth, state.maxInputRows);
|
|
434
453
|
if (info.cursorWrappedLine === 0) {
|
|
435
454
|
if (state.inputHistory) {
|
|
436
455
|
const recalled = state.inputHistory.up(prompt);
|
|
437
456
|
if (recalled !== null) {
|
|
438
457
|
prompt = recalled;
|
|
439
458
|
cursorPos = recalled.length;
|
|
440
|
-
|
|
459
|
+
ensureLocalInputCursorVisible();
|
|
441
460
|
} else {
|
|
442
461
|
state.scroll(-3);
|
|
443
462
|
}
|
|
@@ -446,22 +465,23 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
|
|
|
446
465
|
}
|
|
447
466
|
}
|
|
448
467
|
}
|
|
449
|
-
return { handled: true, prompt, cursorPos, commandMode, indicatorFocused };
|
|
468
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
450
469
|
}
|
|
451
470
|
|
|
452
471
|
if (token.logicalName === 'down') {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
472
|
+
const move = computeCursorVerticalMove(prompt, cursorPos, inputScrollTop, state.contentWidth, state.maxInputRows, 1);
|
|
473
|
+
if (move.moved) {
|
|
474
|
+
cursorPos = move.cursorPos;
|
|
475
|
+
inputScrollTop = move.inputScrollTop;
|
|
476
|
+
} else {
|
|
477
|
+
const info = computeWrappedPromptInfo(prompt, cursorPos, inputScrollTop, state.contentWidth, state.maxInputRows);
|
|
478
|
+
const atBottom = info.cursorWrappedLine >= info.wrappedLines.length - 1;
|
|
479
|
+
if (atBottom && state.inputHistory?.isBrowsing) {
|
|
480
|
+
const recalled = state.inputHistory.down();
|
|
481
|
+
if (recalled !== null) {
|
|
482
|
+
prompt = recalled;
|
|
483
|
+
cursorPos = recalled.length;
|
|
484
|
+
ensureLocalInputCursorVisible();
|
|
465
485
|
} else {
|
|
466
486
|
indicatorFocused = true;
|
|
467
487
|
}
|
|
@@ -469,17 +489,17 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
|
|
|
469
489
|
indicatorFocused = true;
|
|
470
490
|
}
|
|
471
491
|
}
|
|
472
|
-
return { handled: true, prompt, cursorPos, commandMode, indicatorFocused };
|
|
492
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
473
493
|
}
|
|
474
494
|
|
|
475
495
|
if (token.logicalName === 'f2') {
|
|
476
496
|
indicatorFocused = false;
|
|
477
497
|
state.modalOpened('process');
|
|
478
498
|
state.processModal.open();
|
|
479
|
-
return { handled: true, prompt, cursorPos, commandMode, indicatorFocused };
|
|
499
|
+
return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
480
500
|
}
|
|
481
501
|
|
|
482
|
-
return { handled: false, prompt, cursorPos, commandMode, indicatorFocused };
|
|
502
|
+
return { handled: false, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
|
|
483
503
|
}
|
|
484
504
|
|
|
485
505
|
export type MouseRouteState = {
|
|
@@ -85,6 +85,7 @@ import type { ModelPickerTarget } from './model-picker.ts';
|
|
|
85
85
|
export interface InputFeedContext {
|
|
86
86
|
prompt: string;
|
|
87
87
|
cursorPos: number;
|
|
88
|
+
inputScrollTop: number;
|
|
88
89
|
commandMode: boolean;
|
|
89
90
|
panelFocused: boolean;
|
|
90
91
|
indicatorFocused: boolean;
|
|
@@ -99,6 +100,7 @@ export interface InputFeedContext {
|
|
|
99
100
|
contentWidth: number;
|
|
100
101
|
readonly pasteRegistry: Map<string, string>;
|
|
101
102
|
readonly imageRegistry: Map<string, { data: string; mediaType: string }>;
|
|
103
|
+
readonly projectRoot: string;
|
|
102
104
|
readonly selection: SelectionManager;
|
|
103
105
|
readonly selectionModal: SelectionModal;
|
|
104
106
|
selectionCallback: ((result: SelectionResult | null) => void) | null;
|
|
@@ -353,20 +355,31 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
353
355
|
conversationManager: context.conversationManager,
|
|
354
356
|
requestRender: context.requestRender,
|
|
355
357
|
handleEscape: context.handleEscape,
|
|
358
|
+
projectRoot: context.projectRoot,
|
|
359
|
+
pasteRegistry: context.pasteRegistry,
|
|
360
|
+
imageRegistry: context.imageRegistry,
|
|
361
|
+
nextPasteId: context.nextPasteId,
|
|
362
|
+
nextImageId: context.nextImageId,
|
|
363
|
+
saveUndoState: context.saveUndoState,
|
|
364
|
+
ensureInputCursorVisible: () => context.ensureInputCursorVisible(),
|
|
356
365
|
};
|
|
357
366
|
if (handleCommandModeToken(commandState, token)) {
|
|
358
367
|
context.commandMode = commandState.commandMode;
|
|
359
368
|
context.prompt = commandState.prompt;
|
|
360
369
|
context.cursorPos = commandState.cursorPos;
|
|
361
370
|
context.panelFocused = commandState.panelFocused;
|
|
371
|
+
context.nextPasteId = commandState.nextPasteId;
|
|
372
|
+
context.nextImageId = commandState.nextImageId;
|
|
362
373
|
continue;
|
|
363
374
|
}
|
|
364
375
|
|
|
365
376
|
const keyRoute = handlePromptKeyToken({
|
|
366
377
|
prompt: context.prompt,
|
|
367
378
|
cursorPos: context.cursorPos,
|
|
379
|
+
inputScrollTop: context.inputScrollTop,
|
|
368
380
|
commandMode: context.commandMode,
|
|
369
381
|
contentWidth: context.contentWidth,
|
|
382
|
+
maxInputRows: 8,
|
|
370
383
|
inputHistory: context.inputHistory,
|
|
371
384
|
indicatorFocused: context.indicatorFocused,
|
|
372
385
|
conversationManager: context.conversationManager,
|
|
@@ -391,6 +404,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
391
404
|
if (keyRoute.handled) {
|
|
392
405
|
context.prompt = keyRoute.prompt;
|
|
393
406
|
context.cursorPos = keyRoute.cursorPos;
|
|
407
|
+
context.inputScrollTop = keyRoute.inputScrollTop;
|
|
394
408
|
context.commandMode = keyRoute.commandMode;
|
|
395
409
|
context.indicatorFocused = keyRoute.indicatorFocused;
|
|
396
410
|
continue;
|
package/src/input/handler.ts
CHANGED
|
@@ -258,7 +258,7 @@ export class InputHandler {
|
|
|
258
258
|
public initFeedContext(): void {
|
|
259
259
|
this.feedContext = buildInitialFeedContext(
|
|
260
260
|
{
|
|
261
|
-
prompt: this.prompt, cursorPos: this.cursorPos, commandMode: this.commandMode,
|
|
261
|
+
prompt: this.prompt, cursorPos: this.cursorPos, inputScrollTop: this.inputScrollTop, commandMode: this.commandMode,
|
|
262
262
|
panelFocused: this.panelFocused, indicatorFocused: this.indicatorFocused,
|
|
263
263
|
helpOverlayActive: this.helpOverlayActive, helpScrollOffset: this.helpScrollOffset,
|
|
264
264
|
shortcutsOverlayActive: this.shortcutsOverlayActive, shortcutsScrollOffset: this.shortcutsScrollOffset,
|
|
@@ -271,6 +271,7 @@ export class InputHandler {
|
|
|
271
271
|
selection: this.selection,
|
|
272
272
|
pasteRegistry: this.pasteRegistry,
|
|
273
273
|
imageRegistry: this.imageRegistry,
|
|
274
|
+
projectRoot: this.uiServices.environment.shellPaths.workingDirectory,
|
|
274
275
|
selectionModal: this.selectionModal,
|
|
275
276
|
bookmarkModal: this.bookmarkModal,
|
|
276
277
|
settingsModal: this.settingsModal,
|
|
@@ -338,7 +339,7 @@ export class InputHandler {
|
|
|
338
339
|
/** Sync mutable handler fields back into feedContext after in-feed mutations. */
|
|
339
340
|
public syncFeedContextMutableFields(): void {
|
|
340
341
|
const h = this;
|
|
341
|
-
syncFeedContextMutableFields({ prompt: h.prompt, cursorPos: h.cursorPos, commandMode: h.commandMode,
|
|
342
|
+
syncFeedContextMutableFields({ prompt: h.prompt, cursorPos: h.cursorPos, inputScrollTop: h.inputScrollTop, commandMode: h.commandMode,
|
|
342
343
|
panelFocused: h.panelFocused, indicatorFocused: h.indicatorFocused, helpOverlayActive: h.helpOverlayActive,
|
|
343
344
|
helpScrollOffset: h.helpScrollOffset, shortcutsOverlayActive: h.shortcutsOverlayActive,
|
|
344
345
|
shortcutsScrollOffset: h.shortcutsScrollOffset, selectionCallback: h.selectionCallback,
|
|
@@ -445,6 +446,7 @@ export class InputHandler {
|
|
|
445
446
|
// Sync mutable scalars from handler into the reused context.
|
|
446
447
|
context.prompt = this.prompt;
|
|
447
448
|
context.cursorPos = this.cursorPos;
|
|
449
|
+
context.inputScrollTop = this.inputScrollTop;
|
|
448
450
|
context.commandMode = this.commandMode;
|
|
449
451
|
context.panelFocused = this.panelFocused;
|
|
450
452
|
context.indicatorFocused = this.indicatorFocused;
|
|
@@ -473,6 +475,7 @@ export class InputHandler {
|
|
|
473
475
|
feedInputTokens(context, this.tokenizer.feed(data));
|
|
474
476
|
this.prompt = context.prompt;
|
|
475
477
|
this.cursorPos = context.cursorPos;
|
|
478
|
+
this.inputScrollTop = context.inputScrollTop;
|
|
476
479
|
this.commandMode = context.commandMode;
|
|
477
480
|
this.panelFocused = context.panelFocused;
|
|
478
481
|
this.indicatorFocused = context.indicatorFocused;
|
|
@@ -500,7 +503,7 @@ export class InputHandler {
|
|
|
500
503
|
* handlePaste - Shared paste logic for Ctrl+V and middle-click.
|
|
501
504
|
* Tries image clipboard first, falls back to text paste.
|
|
502
505
|
*/
|
|
503
|
-
public handlePaste():
|
|
506
|
+
public handlePaste(): ReturnType<typeof handleClipboardPaste> {
|
|
504
507
|
const result = handleClipboardPaste({
|
|
505
508
|
prompt: this.prompt,
|
|
506
509
|
cursorPos: this.cursorPos,
|
|
@@ -516,6 +519,11 @@ export class InputHandler {
|
|
|
516
519
|
this.cursorPos = result.cursorPos;
|
|
517
520
|
this.nextImageId = result.nextImageId;
|
|
518
521
|
this.nextPasteId = result.nextPasteId;
|
|
522
|
+
if (!result.pasted) {
|
|
523
|
+
this.conversationManager?.log('[Paste: clipboard does not contain supported text or image data]', { fg: '240' });
|
|
524
|
+
this.requestRender();
|
|
525
|
+
}
|
|
526
|
+
return result;
|
|
519
527
|
}
|
|
520
528
|
|
|
521
529
|
/** Content width for wrapping — set by main.ts via setContentWidth(). */
|
|
@@ -175,7 +175,6 @@ export class InputHistory {
|
|
|
175
175
|
* Navigate up (older entry).
|
|
176
176
|
* On first call, saves currentInput as draft.
|
|
177
177
|
* Returns the entry to display, or null if at boundary.
|
|
178
|
-
* Only single-line entries are returned (multiline stored but skipped).
|
|
179
178
|
*/
|
|
180
179
|
up(currentInput: string): string | null {
|
|
181
180
|
if (this.entries.length === 0) return null;
|
|
@@ -185,15 +184,10 @@ export class InputHistory {
|
|
|
185
184
|
this.draft = currentInput;
|
|
186
185
|
}
|
|
187
186
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (!this.getDisplayText(entry).includes('\n')) {
|
|
193
|
-
this.position = next;
|
|
194
|
-
return this.getRecallText(this.entries[this.position]!);
|
|
195
|
-
}
|
|
196
|
-
next++;
|
|
187
|
+
const next = this.position + 1;
|
|
188
|
+
if (next < this.entries.length) {
|
|
189
|
+
this.position = next;
|
|
190
|
+
return this.getRecallText(this.entries[this.position]!);
|
|
197
191
|
}
|
|
198
192
|
|
|
199
193
|
// At oldest boundary
|
|
@@ -208,15 +202,10 @@ export class InputHistory {
|
|
|
208
202
|
down(): string | null {
|
|
209
203
|
if (this.position === -1) return null;
|
|
210
204
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (!this.getDisplayText(entry).includes('\n')) {
|
|
216
|
-
this.position = prev;
|
|
217
|
-
return this.getRecallText(this.entries[this.position]!);
|
|
218
|
-
}
|
|
219
|
-
prev--;
|
|
205
|
+
const prev = this.position - 1;
|
|
206
|
+
if (prev >= 0) {
|
|
207
|
+
this.position = prev;
|
|
208
|
+
return this.getRecallText(this.entries[this.position]!);
|
|
220
209
|
}
|
|
221
210
|
|
|
222
211
|
// Back to draft
|
package/src/main.ts
CHANGED
|
@@ -376,6 +376,7 @@ async function main() {
|
|
|
376
376
|
commandContext.submitInput = submitInput;
|
|
377
377
|
commandContext.submitSpokenInput = (text, content) => submitInput(text, content, { spokenOutput: true });
|
|
378
378
|
commandContext.stopSpokenOutput = () => spokenTurns.stop();
|
|
379
|
+
commandContext.pasteFromClipboard = () => input.handlePaste();
|
|
379
380
|
commandContext.executeCommand = (name, args) => commandRegistry.execute(name, args, commandContext);
|
|
380
381
|
commandContext.cancelGeneration = cancelGeneration;
|
|
381
382
|
commandContext.jumpToBookmark = jumpToBookmark;
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '0.19.
|
|
9
|
+
let _version = '0.19.98';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|