@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 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
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.19.96-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.19.98-blue.svg)](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.96",
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
- ): { prompt: string; cursorPos: number; nextImageId: number; nextPasteId: number } {
440
- state.saveUndoState();
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
 
@@ -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(): void {
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.96';
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;