@pellux/goodvibes-tui 1.19.3 → 1.19.4

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,15 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.19.4] - 2026-07-17
8
+
9
+ ### Changes
10
+
11
+ Fixed: selecting a custom-provider model no longer crashes the app at the next startup — boot now waits for custom providers to load before resolving the current model, and falls back to a selectable model (with a logged warning) if the configured provider's file was removed.
12
+ Fixed: startup failures now print a clear "goodvibes failed to start" message with the real error and exit, instead of hanging on a blank screen while the log recorded an empty error object.
13
+ Fixed: after an automatic context compaction, the transcript shows a compact expandable "compaction handoff" block instead of re-printing the entire re-injected instruction text every time.
14
+ Updated: @pellux/goodvibes-sdk and @pellux/goodvibes-terminal-shell to 1.11.3 (quieter startup when no hooks file exists; error summaries no longer mangle redacted paths).
15
+
7
16
  ## [1.19.3] - 2026-07-17
8
17
 
9
18
  ### 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-1.19.3-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-1.19.4-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
 
@@ -3,7 +3,7 @@
3
3
  "product": {
4
4
  "id": "goodvibes",
5
5
  "surface": "operator",
6
- "version": "1.11.2"
6
+ "version": "1.11.3"
7
7
  },
8
8
  "auth": {
9
9
  "modes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "1.19.3",
3
+ "version": "1.19.4",
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",
@@ -104,8 +104,8 @@
104
104
  "@anthropic-ai/vertex-sdk": "^0.16.0",
105
105
  "@ast-grep/napi": "^0.42.0",
106
106
  "@aws/bedrock-token-generator": "^1.1.0",
107
- "@pellux/goodvibes-sdk": "1.11.2",
108
- "@pellux/goodvibes-terminal-shell": "1.11.2",
107
+ "@pellux/goodvibes-sdk": "1.11.3",
108
+ "@pellux/goodvibes-terminal-shell": "1.11.3",
109
109
  "bash-language-server": "^5.6.0",
110
110
  "fuse.js": "^7.1.0",
111
111
  "graphql": "^16.13.2",
@@ -131,10 +131,10 @@
131
131
  "typescript": "^5.9.3"
132
132
  },
133
133
  "optionalDependencies": {
134
- "@pellux/goodvibes-tui-linux-x64": "1.19.3",
135
- "@pellux/goodvibes-tui-linux-arm64": "1.19.3",
136
- "@pellux/goodvibes-tui-darwin-x64": "1.19.3",
137
- "@pellux/goodvibes-tui-darwin-arm64": "1.19.3"
134
+ "@pellux/goodvibes-tui-linux-x64": "1.19.4",
135
+ "@pellux/goodvibes-tui-linux-arm64": "1.19.4",
136
+ "@pellux/goodvibes-tui-darwin-x64": "1.19.4",
137
+ "@pellux/goodvibes-tui-darwin-arm64": "1.19.4"
138
138
  },
139
139
  "trustedDependencies": [],
140
140
  "overrides": {
@@ -5,6 +5,7 @@ import type { WorkspaceRegistrationManager } from '../runtime/trust/workspace-re
5
5
  import { hasResumableWizardProgress, readOnboardingCheckMarker, readWizardProgress } from '../runtime/onboarding/index.ts';
6
6
  import { startOnboardingFastPath } from '../runtime/onboarding/fast-path.ts';
7
7
  import { readLastSessionPointer } from '@/runtime/index.ts';
8
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
8
9
  import type { GoodVibesCliParseResult } from './types.ts';
9
10
 
10
11
  export type TuiStartupShellPaths = Parameters<typeof readOnboardingCheckMarker>[0] & {
@@ -173,3 +174,28 @@ export function applyInitialTuiCliState(options: {
173
174
  // request, not here — see main.ts's trustPromptRef wiring.
174
175
  }
175
176
  }
177
+
178
+ /**
179
+ * reportFatalStartupError — the main() catch handler. A bare Error
180
+ * JSON-serializes to {} in structured logs, and background timers keep the
181
+ * process alive after a boot failure — the historical result was a blank
182
+ * screen and a useless `Fatal error {"error": {}}` log line. Log real
183
+ * fields, tell the user what broke, and exit. Both writes are individually
184
+ * best-effort — a failing logger or torn-down stderr must never hide the
185
+ * original launch failure.
186
+ */
187
+ export function reportFatalStartupError(err: unknown): void {
188
+ const message = err instanceof Error ? err.message : String(err);
189
+ const stack = err instanceof Error ? err.stack : undefined;
190
+ try {
191
+ logger.error('Fatal error', { message, ...(stack ? { stack } : {}) });
192
+ } catch {
193
+ // Startup diagnostics must never hide the original launch failure.
194
+ }
195
+ try {
196
+ process.stderr.write(`goodvibes failed to start: ${message}\n${stack ? `${stack}\n` : ''}`);
197
+ } catch {
198
+ // Ignore secondary stderr failures during process teardown.
199
+ }
200
+ process.exit(1);
201
+ }
@@ -224,7 +224,7 @@ function renderOne(
224
224
  cfg: RenderConfig,
225
225
  ): void {
226
226
  if (message.role === 'user') {
227
- renderConversationUserMessage(ctx, message, width);
227
+ renderConversationUserMessage(ctx, message, width, absoluteIdx);
228
228
  } else if (message.role === 'assistant') {
229
229
  renderConversationAssistantMessage(ctx, message, width, cfg.lineNumberMode, cfg.collapseThreshold, absoluteIdx);
230
230
  } else if (message.role === 'system') {
@@ -17,7 +17,7 @@ import { GLYPHS } from '../renderer/ui-primitives.ts';
17
17
  import type { BlockMeta } from './conversation-types.ts';
18
18
  import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
19
19
  import { parseDiffForApply } from '@pellux/goodvibes-sdk/platform/core';
20
- import { extractUserDisplayText } from '@pellux/goodvibes-sdk/platform/core';
20
+ import { extractUserDisplayText, COMPACTION_HANDOFF_HEADER } from '@pellux/goodvibes-sdk/platform/core';
21
21
  // SystemMessageKind imported from runtime directly to avoid cycle:
22
22
  // conversation-rendering.ts → system-message-router.ts → conversation.ts → conversation-rendering.ts
23
23
  import type { SystemMessageKind } from '@/runtime/index.ts';
@@ -88,6 +88,7 @@ export function renderConversationUserMessage(
88
88
  context: ConversationRenderContext,
89
89
  message: Extract<Message, { role: 'user' }>,
90
90
  width: number,
91
+ msgIdx?: number,
91
92
  ): void {
92
93
  const T = activeTheme();
93
94
  const displayText = extractUserDisplayText(message.content);
@@ -95,9 +96,74 @@ export function renderConversationUserMessage(
95
96
  context.history.addLines(UIFactory.createMessageBar(width, displayText, T.errorBarBg, '196', ' x ', true));
96
97
  return;
97
98
  }
99
+ // Compaction-continuation handoff: a user-ROLE message the compactor
100
+ // authored, not something the user typed. Rendered in full it repeats the
101
+ // entire re-injected instruction block after every automatic compaction —
102
+ // a multi-kilobyte wall in the transcript. Fold it like a tool result; the
103
+ // full payload stays reachable through the normal expand toggle.
104
+ if (msgIdx !== undefined && displayText.startsWith(COMPACTION_HANDOFF_HEADER)) {
105
+ renderCompactionContinuationMessage(context, displayText, width, msgIdx);
106
+ return;
107
+ }
98
108
  context.history.addLines(UIFactory.createMessageBar(width, displayText));
99
109
  }
100
110
 
111
+ /** Fold a compaction-continuation user message to one header + preview line. */
112
+ function renderCompactionContinuationMessage(
113
+ context: ConversationRenderContext,
114
+ content: string,
115
+ width: number,
116
+ msgIdx: number,
117
+ ): void {
118
+ const T = activeTheme();
119
+ const collapseKey = `msg_${msgIdx}`;
120
+ const blockIdx = context.blockRegistry.length;
121
+ const startLine = context.history.getLineCount();
122
+ const lineCount = content.split('\n').length;
123
+ const isCollapsed = context.collapseState.has(collapseKey)
124
+ ? context.collapseState.get(collapseKey)!
125
+ : true;
126
+ if (!context.collapseState.has(collapseKey)) {
127
+ context.collapseState.set(collapseKey, true);
128
+ }
129
+
130
+ context.history.addLine(renderConversationEventLine(width, {
131
+ marker: GLYPHS.status.active,
132
+ markerFg: T.toolAccent,
133
+ label: 'compaction handoff',
134
+ labelFg: T.toolAccent,
135
+ detailFg: '244',
136
+ }, [
137
+ { text: ` ${isCollapsed ? GLYPHS.navigation.collapsed : GLYPHS.navigation.expanded} ${lineCount} line${lineCount === 1 ? '' : 's'} `, fg: '244', dim: true },
138
+ ]));
139
+
140
+ if (isCollapsed) {
141
+ const rendered = renderConversationCollapsedFragment(
142
+ 'compacted-context handoff (re-injected instructions + session summary)',
143
+ width,
144
+ {
145
+ prefix: ` ${GLYPHS.navigation.collapsed} `,
146
+ prefixFg: T.toolAccent,
147
+ text: '244',
148
+ bodyBg: T.collapsedBodyBg,
149
+ dim: true,
150
+ },
151
+ );
152
+ context.history.addLines(rendered);
153
+ } else {
154
+ context.history.addLines(renderMarkdownTracked(content, width).lines);
155
+ }
156
+
157
+ context.blockRegistry.push({
158
+ blockIndex: blockIdx,
159
+ collapseKey,
160
+ type: 'tool',
161
+ startLine,
162
+ lineCount: context.history.getLineCount() - startLine,
163
+ rawContent: content,
164
+ });
165
+ }
166
+
101
167
  export function renderConversationAssistantMessage(
102
168
  context: ConversationRenderContext,
103
169
  message: Extract<Message, { role: 'assistant' }>,
@@ -400,7 +466,7 @@ export function appendConversationMessages(
400
466
  const absoluteIdx = msgIndexOffset + msgIdx;
401
467
  messageLineRegistry[absoluteIdx] = renderContext.history.getLineCount();
402
468
  if (message.role === 'user') {
403
- renderConversationUserMessage(renderContext, message, width);
469
+ renderConversationUserMessage(renderContext, message, width, absoluteIdx);
404
470
  } else if (message.role === 'assistant') {
405
471
  renderConversationAssistantMessage(renderContext, message, width, lineNumberMode, collapseThreshold, absoluteIdx);
406
472
  } else if (message.role === 'system') {
package/src/main.ts CHANGED
@@ -45,7 +45,7 @@ import { deriveComposerState } from './core/composer-state.ts';
45
45
  import { buildPersistedSessionContext, getReturnContextMode, maybeAssistReturnContextSummary } from '@/runtime/index.ts';
46
46
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
47
47
  import { prepareShellCliRuntime } from './cli/entrypoint.ts';
48
- import { applyInitialTuiCliState } from './cli/tui-startup.ts';
48
+ import { applyInitialTuiCliState, reportFatalStartupError } from './cli/tui-startup.ts';
49
49
  import { applyConfiguredHitlMode, applyRuntimeConfigValue, applyTuiRuntimeConfigDefaults } from './cli/config-overrides.ts';
50
50
  import { renderToolCallBlock } from './renderer/tool-call.ts';
51
51
  import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
@@ -797,4 +797,4 @@ async function main() {
797
797
 
798
798
  }
799
799
 
800
- main().catch(err => logger.error('Fatal error', { error: err }));
800
+ main().catch(reportFatalStartupError);
@@ -37,12 +37,13 @@ import { pruneStaleOperatorTokens } from '@pellux/goodvibes-sdk/platform/pairing
37
37
  import { resolveDaemonCompanionToken, workspaceOperatorTokenCandidates } from './operator-token-cleanup.ts';
38
38
  import type { UiRuntimeServices } from './ui-services.ts';
39
39
  import { initializeBootstrapCore } from './bootstrap-core.ts';
40
+ import { ensureBootModelResolvable } from './provider-fallback.ts';
40
41
  import { createBootstrapShell } from './bootstrap-shell.ts';
41
42
  import { announceResumeState } from './resume-notice.ts';
42
43
  import { announceInstallSelfCheck } from './install-self-check-startup.ts';
43
44
  import { buildSharedOrchestratorCoreServices, refreshMemoryRecallSnapshot } from './orchestrator-core-services.ts';
44
45
  import { consumeDaemonAttachNotices, consumeExternalDaemonAttachNotices } from './daemon-attach-notices.ts';
45
- import { createContextAccountingSource } from './context-accounting-source.ts';
46
+ import { wireContextAccountingSource } from './context-accounting-source.ts';
46
47
  import { createEmbeddedServiceFactories } from './embedded-service-factories.ts';
47
48
  import { buildRelayExternalServiceMethods } from './relay-reachability-bridge.ts';
48
49
  import { startMcpConfigAutoReload } from '../mcp/runtime-reload.ts';
@@ -194,7 +195,8 @@ export async function bootstrapRuntime(
194
195
  panelManager,
195
196
  pluginManager,
196
197
  } = services;
197
-
198
+ // A saved custom-provider model must never crash boot — see provider-fallback.ts.
199
+ await ensureBootModelResolvable(providerRegistry, configManager);
198
200
  // A liveness flip is only PAINTED once something calls requestRender(); without
199
201
  // this, the footer's spine segment sat correct-but-undrawn until incidental activity redrew it (minutes, during an idle stretch).
200
202
  sessionUnionCache.setOnTransition(() => requestRender());
@@ -248,11 +250,9 @@ export async function bootstrapRuntime(
248
250
  conversation.setSessionLineageTracker(services.sessionLineageTracker);
249
251
 
250
252
  // Bind context_accounting's session source to the live Orchestrator (see context-accounting-source.ts).
251
- const contextAccountingSourceHandle = createContextAccountingSource({
253
+ wireContextAccountingSource({
252
254
  orchestrator, providerRegistry, sessionLineageTracker: services.sessionLineageTracker, runtimeBus, sessionId: runtime.sessionId,
253
- });
254
- services.contextAccountingHolder.setSource(contextAccountingSourceHandle.source);
255
- bootstrapUnsubs.push(contextAccountingSourceHandle.dispose);
255
+ }, services.contextAccountingHolder, bootstrapUnsubs);
256
256
 
257
257
  const acpManager = new AcpManager({
258
258
  requestPermission: (request) => permissionPromptRef.requestPermission(request),
@@ -101,3 +101,17 @@ export function createContextAccountingSource(deps: ContextAccountingSourceDeps)
101
101
  dispose: () => { for (const unsub of unsubs) unsub(); },
102
102
  };
103
103
  }
104
+
105
+ /**
106
+ * Create the source, install it on the holder, and register its dispose on
107
+ * the caller's unsubscribe list — the one-call bootstrap wiring.
108
+ */
109
+ export function wireContextAccountingSource(
110
+ deps: ContextAccountingSourceDeps,
111
+ holder: { setSource: (source: ContextAccountingSourceHandle['source']) => void },
112
+ unsubs: Array<() => void>,
113
+ ): void {
114
+ const handle = createContextAccountingSource(deps);
115
+ holder.setSource(handle.source);
116
+ unsubs.push(handle.dispose);
117
+ }
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
9
9
  import { inferFallbackContextWindow, type ModelDefinition, type ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
10
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
10
11
 
11
12
  function buildFallbackModelDefinition(provider: string, modelId: string): ModelDefinition {
12
13
  const providerLower = provider.toLowerCase();
@@ -53,3 +54,33 @@ export function ensureConfiguredModelIsRoutable(providerRegistry: ProviderRegist
53
54
  models: [buildFallbackModelDefinition(providerId, modelId)],
54
55
  });
55
56
  }
57
+
58
+ /**
59
+ * Boot-time custom-provider readiness. Custom providers register
60
+ * asynchronously (services.ts fires initCustomProviders() without awaiting),
61
+ * while the boot path resolves the current model synchronously — without
62
+ * waiting here, a saved provider.model that points at a custom provider
63
+ * throws "not in registry" before the first frame renders. The routability
64
+ * guard must re-run after ready(): its services-composition pass bails when
65
+ * the provider itself isn't registered yet, which is exactly the
66
+ * custom-provider case. As a last resort (the configured provider's file was
67
+ * deleted entirely), boot on a real selectable model with a warning instead
68
+ * of dying before the UI exists.
69
+ */
70
+ export async function ensureBootModelResolvable(
71
+ providerRegistry: ProviderRegistry,
72
+ configManager: ConfigManager,
73
+ ): Promise<void> {
74
+ await providerRegistry.ready();
75
+ ensureConfiguredModelIsRoutable(providerRegistry, configManager);
76
+ try {
77
+ providerRegistry.getCurrentModel();
78
+ } catch (err) {
79
+ const configured = String(configManager.get('provider.model') ?? '');
80
+ const replacement = providerRegistry.getSelectableModels()[0]?.registryKey;
81
+ if (!replacement) throw err;
82
+ providerRegistry.setCurrentModel(replacement);
83
+ configManager.set('provider.model', replacement);
84
+ logger.warn(`[bootstrap] Configured model '${configured}' is not resolvable (its provider is not registered); switched to '${replacement}'.`);
85
+ }
86
+ }
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 = '1.19.3';
9
+ let _version = '1.19.4';
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