@pellux/goodvibes-tui 2.0.7 → 2.0.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "Terminal-native GoodVibes client for coding, operations, knowledge and channel workflows — a pure client of the GoodVibes daemon.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -95,8 +95,8 @@
95
95
  "@anthropic-ai/bedrock-sdk": "^0.29.1",
96
96
  "@anthropic-ai/sdk": "^0.92.0",
97
97
  "@ast-grep/napi": "^0.42.0",
98
- "@pellux/goodvibes-sdk": "2.0.7",
99
- "@pellux/goodvibes-terminal-shell": "2.0.7",
98
+ "@pellux/goodvibes-sdk": "2.0.9",
99
+ "@pellux/goodvibes-terminal-shell": "2.0.9",
100
100
  "bash-language-server": "^5.6.0",
101
101
  "fuse.js": "^7.1.0",
102
102
  "@pellux/goodvibes-daemon": "1.28.7",
@@ -354,7 +354,9 @@ export function toProfileProvenanceReport(value: unknown): Checked<ProfileProven
354
354
  if (provenance === MALFORMED) return MALFORMED;
355
355
  const superseded = checkedArray(record.superseded, toSuperseded);
356
356
  if (superseded === MALFORMED) return MALFORMED;
357
- return { fieldId, present, handEdited, ...(provenance ? { provenance } : {}), superseded };
357
+ // Platform runtime 2.0.8: the contract declares provenance nullable and
358
+ // always present — null IS the answer for a hand-edited field.
359
+ return { fieldId, present, handEdited, provenance: provenance ?? null, superseded };
358
360
  }
359
361
 
360
362
  function toChange(value: unknown): Checked<ProfileChangeView> {
@@ -58,6 +58,45 @@ export async function hydrateOnboardingWizardFromRuntimeForHandler(handler: Inpu
58
58
  }
59
59
  }
60
60
 
61
+ /**
62
+ * handlePasteForHandler - Shared paste path for Ctrl+V and middle-click.
63
+ *
64
+ * Tries the clipboard's image first, then its text. The clipboard itself comes
65
+ * from `handler.clipboardSource`, so tests can hand in a clipboard without
66
+ * touching the real one.
67
+ *
68
+ * The prompt is edited on the HANDLER here, not on the shortcut route state
69
+ * that dispatched the key. feedInputTokens snapshots the prompt before
70
+ * dispatching and would write that snapshot back over this edit, erasing the
71
+ * paste — it does not, because it only restores a field the action left
72
+ * untouched (see the promptBefore guard in handler-feed.ts). That guard is the
73
+ * only reason Ctrl+V works, so it is pinned by test.
74
+ */
75
+ export function handlePasteForHandler(handler: InputHandler): ReturnType<typeof handleClipboardPaste> {
76
+ const result = handleClipboardPaste({
77
+ prompt: handler.prompt,
78
+ cursorPos: handler.cursorPos,
79
+ pasteRegistry: handler.pasteRegistry,
80
+ nextPasteId: handler.nextPasteId,
81
+ imageRegistry: handler.imageRegistry,
82
+ nextImageId: handler.nextImageId,
83
+ saveUndoState: () => handler.saveUndoState(),
84
+ ensureInputCursorVisible: () => handler.ensureInputCursorVisible(),
85
+ requestRender: handler.requestRender,
86
+ }, handler.uiServices.environment.shellPaths.workingDirectory, handler.clipboardSource);
87
+
88
+ handler.prompt = result.prompt;
89
+ handler.cursorPos = result.cursorPos;
90
+ handler.nextImageId = result.nextImageId;
91
+ handler.nextPasteId = result.nextPasteId;
92
+
93
+ if (!result.pasted) {
94
+ handler.conversationManager?.log('[Paste: clipboard does not contain supported text or image data]', { fg: '240' });
95
+ handler.requestRender();
96
+ }
97
+ return result;
98
+ }
99
+
61
100
  export function registerPasteForHandler(handler: InputHandler, content: string): string {
62
101
  const result = registerPaste({
63
102
  pasteRegistry: handler.pasteRegistry,
@@ -19,6 +19,7 @@ import type { UiRuntimeServices } from '../runtime/ui-services.ts';
19
19
  import type { CommandContext } from './command-registry.ts';
20
20
  import type { ConversationManager } from '../core/conversation';
21
21
  import type { ModelPickerModal, ModelPickerTarget } from './model-picker.ts';
22
+ import type { ClipboardPasteSource } from './handler-content-actions.ts';
22
23
  import type { SelectionManager } from '@pellux/goodvibes-terminal-shell';
23
24
  import type { InfiniteBuffer } from '@pellux/goodvibes-terminal-shell';
24
25
  import type { AutocompleteEngine } from './autocomplete.ts';
@@ -81,6 +82,11 @@ export interface InputHandlerLike {
81
82
  nextPasteId: number;
82
83
  imageRegistry: Map<string, { data: string; mediaType: string }>;
83
84
  nextImageId: number;
85
+ /**
86
+ * The clipboard this composer pastes from. Swappable so tests can supply a
87
+ * clipboard instead of reaching for the machine's real one.
88
+ */
89
+ clipboardSource: ClipboardPasteSource;
84
90
 
85
91
  // ── Timing ───────────────────────────────────────────────────────────────
86
92
  lastCopyTime: number;
@@ -124,6 +130,7 @@ export interface InputHandlerLike {
124
130
  // ── Methods: modal lifecycle ──────────────────────────────────────────────
125
131
  modalOpened(name: string): void;
126
132
  saveUndoState(): void;
133
+ ensureInputCursorVisible(contentWidth?: number): void;
127
134
 
128
135
  // ── Methods: block actions (dispatched in executeBlockAction) ────────────
129
136
  handleBlockCopy(): void;
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node
2
2
  import { dirname } from 'node:path';
3
3
  import { InputTokenizer } from '@pellux/goodvibes-sdk/platform/core';
4
4
  import { createOAuthLocalListener } from '@pellux/goodvibes-sdk/platform/config';
5
- import { clearModalStackForHandler, cleanupMarkerRegistryForHandler, executeBlockActionForHandler, expandPromptForHandler, findMarkerAtPosForHandler, getImageAttachmentsForHandler, handleBlockCopyForHandler, handleBlockSaveForHandler, handleBlockToggleForHandler, handleBookmarkForHandler, handleCopyForHandler, handleCtrlCForHandler, handleDiffApplyForHandler, handleEscapeForHandler, hydrateOnboardingWizardFromRuntimeForHandler, modalOpenedForHandler, openOnboardingWizardForHandler, registerPasteForHandler } from './handler-interactions.ts';
5
+ import { clearModalStackForHandler, cleanupMarkerRegistryForHandler, executeBlockActionForHandler, expandPromptForHandler, findMarkerAtPosForHandler, getImageAttachmentsForHandler, handleBlockCopyForHandler, handleBlockSaveForHandler, handleBlockToggleForHandler, handleBookmarkForHandler, handleCopyForHandler, handleCtrlCForHandler, handleDiffApplyForHandler, handleEscapeForHandler, handlePasteForHandler, hydrateOnboardingWizardFromRuntimeForHandler, modalOpenedForHandler, openOnboardingWizardForHandler, registerPasteForHandler } from './handler-interactions.ts';
6
6
  import { getViewportBottomLine } from '../renderer/conversation-layout.ts';
7
7
  import { clearOnboardingModelPickerCancelStateForHandler, clearOnboardingPendingModelPickerTargetForHandler, completeOpenAiSubscriptionFromListenerForHandler, getOnboardingConfigValueForHandler, getOnboardingRuntimePostureForHandler, handleModelPickerCommitForHandler, handleOnboardingActionForHandler, handleOpenAiSubscriptionFinishForHandler, handleOpenAiSubscriptionStartForHandler, openModelPickerWithTargetForHandler, openProviderModelPickerWithTargetForHandler, refreshOnboardingHydrationForHandler, restartOnboardingExternalServicesIfNeededForHandler, restoreOnboardingModelPickerCancelStateForHandler, saveWizardProgressForHandler, syncRuntimeFromOnboardingRequestForHandler, verifyOnboardingRuntimePostureForHandler, } from './handler-onboarding.ts';
8
8
  import type { OnboardingRuntimePosture } from './handler-types.ts';
@@ -53,12 +53,14 @@ import {
53
53
  handleBlockToggle,
54
54
  handleBookmark,
55
55
  handleClipboardPaste,
56
+ type ClipboardPasteSource,
56
57
  handleCopy,
57
58
  handleCtrlC,
58
59
  handleDiffApply,
59
60
  mediaTypeFromExt,
60
61
  registerPaste,
61
62
  } from './handler-content-actions.ts';
63
+ import { pasteFromClipboard, pasteImageFromClipboard } from '../utils/clipboard.ts';
62
64
  import {
63
65
  handleIndicatorFocusToken,
64
66
  handleMouseToken,
@@ -574,28 +576,15 @@ export class InputHandler implements InputHandlerLike {
574
576
  * Tries image clipboard first, falls back to text paste.
575
577
  */
576
578
  public handlePaste(): ReturnType<typeof handleClipboardPaste> {
577
- const result = handleClipboardPaste({
578
- prompt: this.prompt,
579
- cursorPos: this.cursorPos,
580
- pasteRegistry: this.pasteRegistry,
581
- nextPasteId: this.nextPasteId,
582
- imageRegistry: this.imageRegistry,
583
- nextImageId: this.nextImageId,
584
- saveUndoState: () => this.saveUndoState(),
585
- ensureInputCursorVisible: () => this.ensureInputCursorVisible(),
586
- requestRender: this.requestRender,
587
- }, this.uiServices.environment.shellPaths.workingDirectory);
588
- this.prompt = result.prompt;
589
- this.cursorPos = result.cursorPos;
590
- this.nextImageId = result.nextImageId;
591
- this.nextPasteId = result.nextPasteId;
592
- if (!result.pasted) {
593
- this.conversationManager?.log('[Paste: clipboard does not contain supported text or image data]', { fg: '240' });
594
- this.requestRender();
595
- }
596
- return result;
579
+ return handlePasteForHandler(this);
597
580
  }
598
581
 
582
+ /**
583
+ * The clipboard this composer pastes from. Swappable so tests can supply a
584
+ * clipboard instead of reaching for the machine's real one.
585
+ */
586
+ public clipboardSource: ClipboardPasteSource = { pasteImageFromClipboard, pasteFromClipboard };
587
+
599
588
  /** Content width for wrapping — set by main.ts via setContentWidth(). */
600
589
  public contentWidth = 76;
601
590
  public panelMouseLayout: PanelMouseLayout | null = null;
@@ -81,6 +81,16 @@ export type SettingsCategory =
81
81
  // hostedSessions.* key from the workspace, which is exactly what happened to
82
82
  // push.* and cluster.* before them.
83
83
  | 'hostedSessions'
84
+ // email.* / calendar.* / google.* — the connector keys the daemon really
85
+ // reads for its Google-backed mail and calendar services (platform runtime
86
+ // 2.0.8 registered them as described schema rows; before that they were
87
+ // cast onto the live config invisibly and answered "Unknown setting").
88
+ // Same mandatory-dual-membership rule as profile and hostedSessions above —
89
+ // without BOTH these entries and the SETTINGS_CATEGORY_GROUPS membership
90
+ // below, every one of the 22 connector keys is dropped from the workspace.
91
+ | 'email'
92
+ | 'calendar'
93
+ | 'google'
84
94
  | 'danger';
85
95
 
86
96
  export type SettingsFocusPane = 'categories' | 'settings';
@@ -96,7 +106,10 @@ export const SETTINGS_CATEGORY_GROUPS: ReadonlyArray<{
96
106
  // answer "where does this conversation actually run, and who can reach it" —
97
107
  // the detach policy is a fact about the daemon holding a session open, not
98
108
  // about this terminal's presentation of it.
99
- { label: 'Surfaces & Cloud', categories: ['surfaces', 'hostedSessions', 'device', 'conversationGate', 'integrations', 'connections', 'mcp', 'cloudflare'] },
109
+ // 'email', 'calendar' and 'google' sit beside 'surfaces': they configure
110
+ // the daemon's connections to the owner's mail and calendar — where those
111
+ // services reach the world, not how this terminal presents anything.
112
+ { label: 'Surfaces & Cloud', categories: ['surfaces', 'hostedSessions', 'device', 'conversationGate', 'integrations', 'connections', 'email', 'calendar', 'google', 'mcp', 'cloudflare'] },
100
113
  // 'occasions' sits beside 'checkin': both are proactive background loops
101
114
  // (a cadence, a judgment pass, a conditional channel delivery) rather than
102
115
  // facts held about the owner — the data occasions/plans declare lives in
@@ -115,6 +115,9 @@ export const CATEGORY_INFO: Record<SettingsCategory, string> = {
115
115
  relay: 'Outbound zero-knowledge relay reachability, for reaching this daemon from outside the LAN without opening an inbound port. Off by default — relay.enabled is the switch. The relay operator sees only ciphertext and connection metadata — self-host your own relay for full control.',
116
116
  learning: 'Idle-time memory consolidation: merging duplicate standing memory records and decaying/archiving stale ones. Off by default — nothing runs until enabled.',
117
117
  payments: 'Payment card on file for daemon-initiated purchases: card number, expiry, CVV, cardholder name, billing and shipping address, and whether the CVV is stored or requested at purchase time. Card fields are entered through /payments card (concealed input) and are secret-tier — never rendered in plaintext.',
118
+ email: 'The daemon\'s own mailbox connection — IMAP and SMTP hosts, ports and security, the mailbox and drafts folders, and the username. Password fields hold secret-store references, never values; /google connects a Gmail-backed setup in one action.',
119
+ calendar: 'Calendar connections the daemon reads and writes: the Google or Microsoft OAuth client for CalDAV-free access, or a read-only ICS URL. Secret fields hold secret-store references, never values.',
120
+ google: 'The Google OAuth connection record: project id, publishing status, and the refresh-token reference the mail and calendar services share. Established by /google in one action; a dead grant is diagnosed in plain words there.',
118
121
  profile: 'What the platform knows about you — your name, how to reach you, where you are, how you like answers written, the people and places you mention — kept as one Markdown file at daemon scope that you can open and edit by hand at any time; your edits win and are never rewritten. These rows decide whether it is loaded at all, whether the assistant records facts it learns from things you say directly to it, whether it tells you in one line each time it does, whether the harmless part (how you are addressed, your city, your timezone, your formatting preferences, your style) is put in the model\'s context so it stops guessing, whether using a private detail such as your address is announced in the reply, whether settings left unset read their value from the profile, and where the file lives. Everything private — your name, contact details, home address, billing and shipping details, quiet hours, and the People, Places, Work and Notes sections — is never put in context in bulk and is reachable only by a named lookup that is disclosed when it happens. Run /profile to read the whole thing, /profile where <field> to see the surface, date and your exact words behind any recorded line, and /profile forget <field> to delete one.',
119
122
  };
120
123
 
@@ -159,6 +162,9 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
159
162
  release: 'Release',
160
163
  power: 'Power',
161
164
  memory: 'Memory',
165
+ email: 'Email Connection',
166
+ calendar: 'Calendar Connections',
167
+ google: 'Google Connection',
162
168
  danger: 'Danger',
163
169
  tools: 'Tools',
164
170
  network: 'Network',
@@ -108,6 +108,8 @@ export interface RuntimeServicesOptions {
108
108
  export interface RuntimeServices {
109
109
  readonly workingDirectory: string;
110
110
  readonly homeDirectory: string;
111
+ /** The surface segment this runtime's stores live under (platform runtime 2.0.8 requires it — a blank segment resolved to the unscoped control-plane orphan). */
112
+ readonly surfaceRoot: string;
111
113
  /** The declare-once session-storage handle every session reader and writer threads through — see session-storage-services.ts. */
112
114
  readonly surface: SessionSurface;
113
115
  readonly shellPaths: ShellPathService;
@@ -559,6 +559,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
559
559
  const services: RuntimeServices = {
560
560
  workingDirectory,
561
561
  homeDirectory,
562
+ surfaceRoot: GOODVIBES_TUI_SURFACE_ROOT,
562
563
  surface,
563
564
  shellPaths,
564
565
  workspaceTrustManager,
@@ -438,12 +438,56 @@ export const PAYMENTS_LOCAL_SETTINGS = [
438
438
  'payments.notifyChannels',
439
439
  ] as const;
440
440
 
441
+ /**
442
+ * Connector settings — the daemon's Google-backed mail/calendar keys plus the
443
+ * connected-host dial switch and the hosted-turn routing switch, declared as
444
+ * described schema rows by platform runtime 2.0.8 (before that the connector
445
+ * keys were cast onto the live config invisibly and the settings surface
446
+ * answered "Unknown setting" for keys the daemon really reads). Counted for
447
+ * exactly the reason DAEMON_MAILBOX_LOCAL_SETTINGS is: the same four-part
448
+ * persistence contract runs over every one of them in
449
+ * src/test/verification/connector-settings-persistence.test.ts, and that test
450
+ * asserts this list is the complete schema set for the email, calendar and
451
+ * google prefixes
452
+ * plus the two switches. Arriving uncounted is what dropped
453
+ * localBehaviorPercent through its floor when the schema grew. The three
454
+ * secret-valued keys hold goodvibes://secrets/ references, never values —
455
+ * same note as the mailbox passwords above.
456
+ */
457
+ export const CONNECTOR_LOCAL_SETTINGS = [
458
+ 'email.enabled',
459
+ 'email.imapHost',
460
+ 'email.imapPort',
461
+ 'email.imapSecurity',
462
+ 'email.smtpHost',
463
+ 'email.smtpPort',
464
+ 'email.smtpSecurity',
465
+ 'email.username',
466
+ 'email.passwordRef',
467
+ 'email.smtpPasswordRef',
468
+ 'email.fromAddress',
469
+ 'email.mailbox',
470
+ 'email.draftsMailbox',
471
+ 'calendar.google.clientId',
472
+ 'calendar.google.clientSecretRef',
473
+ 'calendar.google.icsUrl',
474
+ 'calendar.microsoft.clientId',
475
+ 'calendar.microsoft.clientSecretRef',
476
+ 'google.oauth.projectId',
477
+ 'google.oauth.publishingStatus',
478
+ 'google.oauth.refreshToken',
479
+ 'google.credentials.migratedFrom',
480
+ 'daemon.connectedHost.enabled',
481
+ 'hostedSessions.routeConversationTurns',
482
+ ] as const;
483
+
441
484
  const SETTINGS_BEHAVIOR_VERIFIED = SETTINGS_BEHAVIOR_BASELINE
442
485
  + FEATURE_KNOB_LOCAL_SETTINGS.length
443
486
  + DEVICE_AND_TRIGGER_LOCAL_SETTINGS.length
444
487
  + CLUSTER_GROUP_LOCAL_SETTINGS.length
445
488
  + DAEMON_MAILBOX_LOCAL_SETTINGS.length
446
489
  + PAYMENTS_LOCAL_SETTINGS.length
490
+ + CONNECTOR_LOCAL_SETTINGS.length
447
491
  + ENABLEMENT_KEYS_BEHAVIOR_VERIFIED;
448
492
 
449
493
  const EXTERNAL_SLASH_COMMANDS = new Set([
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 = '2.0.7';
9
+ let _version = '2.0.9';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  // Only trust a version read from OUR OWN package.json. A Bun single-file