@pellux/goodvibes-tui 0.19.96 → 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 +10 -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 +1 -0
- package/src/input/handler-command-route.ts +27 -0
- package/src/input/handler-content-actions.ts +36 -4
- package/src/input/handler-feed.ts +10 -0
- package/src/input/handler.ts +7 -1
- package/src/main.ts +1 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ 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
|
+
|
|
7
17
|
## [0.19.96] — 2026-05-11
|
|
8
18
|
|
|
9
19
|
### 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
|
|
|
@@ -98,6 +98,7 @@ export interface FeedContextStableRefs {
|
|
|
98
98
|
selection: SelectionManager;
|
|
99
99
|
pasteRegistry: Map<string, string>;
|
|
100
100
|
imageRegistry: Map<string, { data: string; mediaType: string }>;
|
|
101
|
+
projectRoot: string;
|
|
101
102
|
selectionModal: SelectionModal;
|
|
102
103
|
bookmarkModal: BookmarkModal;
|
|
103
104
|
settingsModal: SettingsModal;
|
|
@@ -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
|
}
|
|
@@ -100,6 +100,7 @@ export interface InputFeedContext {
|
|
|
100
100
|
contentWidth: number;
|
|
101
101
|
readonly pasteRegistry: Map<string, string>;
|
|
102
102
|
readonly imageRegistry: Map<string, { data: string; mediaType: string }>;
|
|
103
|
+
readonly projectRoot: string;
|
|
103
104
|
readonly selection: SelectionManager;
|
|
104
105
|
readonly selectionModal: SelectionModal;
|
|
105
106
|
selectionCallback: ((result: SelectionResult | null) => void) | null;
|
|
@@ -354,12 +355,21 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
354
355
|
conversationManager: context.conversationManager,
|
|
355
356
|
requestRender: context.requestRender,
|
|
356
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(),
|
|
357
365
|
};
|
|
358
366
|
if (handleCommandModeToken(commandState, token)) {
|
|
359
367
|
context.commandMode = commandState.commandMode;
|
|
360
368
|
context.prompt = commandState.prompt;
|
|
361
369
|
context.cursorPos = commandState.cursorPos;
|
|
362
370
|
context.panelFocused = commandState.panelFocused;
|
|
371
|
+
context.nextPasteId = commandState.nextPasteId;
|
|
372
|
+
context.nextImageId = commandState.nextImageId;
|
|
363
373
|
continue;
|
|
364
374
|
}
|
|
365
375
|
|
package/src/input/handler.ts
CHANGED
|
@@ -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,
|
|
@@ -502,7 +503,7 @@ export class InputHandler {
|
|
|
502
503
|
* handlePaste - Shared paste logic for Ctrl+V and middle-click.
|
|
503
504
|
* Tries image clipboard first, falls back to text paste.
|
|
504
505
|
*/
|
|
505
|
-
public handlePaste():
|
|
506
|
+
public handlePaste(): ReturnType<typeof handleClipboardPaste> {
|
|
506
507
|
const result = handleClipboardPaste({
|
|
507
508
|
prompt: this.prompt,
|
|
508
509
|
cursorPos: this.cursorPos,
|
|
@@ -518,6 +519,11 @@ export class InputHandler {
|
|
|
518
519
|
this.cursorPos = result.cursorPos;
|
|
519
520
|
this.nextImageId = result.nextImageId;
|
|
520
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;
|
|
521
527
|
}
|
|
522
528
|
|
|
523
529
|
/** Content width for wrapping — set by main.ts via setContentWidth(). */
|
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;
|