@pellux/goodvibes-tui 0.20.3 → 0.21.0
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 +27 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +3 -2
- package/src/audio/spoken-turn-controller.ts +31 -1
- package/src/audio/spoken-turn-wiring.ts +26 -4
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/completions/generate.ts +662 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management.ts +1 -8
- package/src/cli/parser.ts +14 -18
- package/src/cli/service-command.ts +1 -1
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/tui-startup.ts +72 -10
- package/src/cli/types.ts +12 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/conversation-rendering.ts +49 -15
- package/src/core/conversation.ts +101 -16
- package/src/core/format-user-error.ts +192 -0
- package/src/core/stream-event-wiring.ts +144 -0
- package/src/core/stream-stall-watchdog.ts +103 -0
- package/src/core/system-message-router.ts +5 -1
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +31 -1
- package/src/input/commands/control-room-runtime.ts +5 -5
- package/src/input/commands/experience-runtime.ts +5 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +27 -5
- package/src/input/commands/local-setup.ts +4 -6
- package/src/input/commands/memory-product-runtime.ts +8 -6
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +3 -10
- package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +3 -3
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -2
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +2 -15
- package/src/input/handler-modal-routes.ts +91 -12
- package/src/input/handler-modal-token-routes.ts +3 -0
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +55 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +5 -2
- package/src/input/input-history.ts +76 -6
- package/src/input/model-picker-filter.ts +265 -0
- package/src/input/model-picker-items.ts +208 -0
- package/src/input/model-picker.ts +92 -325
- package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
- package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +4 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
- package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
- package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
- package/src/input/onboarding/onboarding-wizard-steps.ts +18 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-data.ts +304 -0
- package/src/input/settings-modal-mutations.ts +154 -0
- package/src/input/settings-modal.ts +182 -220
- package/src/main.ts +57 -57
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +4 -1
- package/src/panels/confirm-state.ts +27 -12
- package/src/panels/cost-tracker-panel.ts +23 -67
- package/src/panels/eval-panel.ts +10 -9
- package/src/panels/knowledge-panel.ts +3 -5
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/subscription-panel.ts +33 -25
- package/src/panels/types.ts +28 -1
- package/src/panels/wrfc-panel.ts +224 -41
- package/src/renderer/agent-detail-modal.ts +11 -10
- package/src/renderer/code-block.ts +10 -2
- package/src/renderer/compositor.ts +18 -4
- package/src/renderer/context-inspector.ts +1 -5
- package/src/renderer/diff.ts +94 -21
- package/src/renderer/markdown.ts +29 -13
- package/src/renderer/settings-modal-helpers.ts +1 -1
- package/src/renderer/settings-modal.ts +77 -8
- package/src/renderer/syntax-highlighter.ts +10 -3
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/theme.ts +158 -0
- package/src/renderer/tool-call.ts +12 -2
- package/src/renderer/ui-factory.ts +50 -6
- package/src/runtime/bootstrap-command-context.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +14 -0
- package/src/runtime/bootstrap-core.ts +121 -13
- package/src/runtime/bootstrap.ts +2 -0
- package/src/runtime/onboarding/apply.ts +4 -6
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/markers.ts +42 -49
- package/src/runtime/onboarding/progress.ts +148 -0
- package/src/runtime/onboarding/state.ts +133 -55
- package/src/runtime/onboarding/types.ts +20 -0
- package/src/runtime/services.ts +21 -0
- package/src/runtime/wrfc-persistence.ts +237 -0
- package/src/shell/blocking-input.ts +20 -5
- package/src/tools/wrfc-agent-guard.ts +64 -3
- package/src/utils/format-elapsed.ts +30 -0
- package/src/utils/terminal-width.ts +45 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +4 -6
- package/src/planning/project-planning-coordinator.ts +0 -543
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { UIFactory } from '../renderer/ui-factory.ts';
|
|
2
2
|
import { renderMarkdownTracked } from '../renderer/markdown.ts';
|
|
3
|
+
import { DARK_THEME } from '../renderer/theme.ts';
|
|
3
4
|
import { renderToolCallBlock } from '../renderer/tool-call.ts';
|
|
4
5
|
import { renderThinkingBlock } from '../renderer/thinking.ts';
|
|
5
6
|
import { renderSystemMessage } from '../renderer/system-message.ts';
|
|
@@ -13,6 +14,23 @@ import { GLYPHS } from '../renderer/ui-primitives.ts';
|
|
|
13
14
|
import type { BlockMeta, ConversationMessageSnapshot } from './conversation';
|
|
14
15
|
import { parseDiffForApply } from '@pellux/goodvibes-sdk/platform/core';
|
|
15
16
|
import { extractUserDisplayText } from '@pellux/goodvibes-sdk/platform/core';
|
|
17
|
+
import type { SystemMessageKind } from './system-message-router.ts';
|
|
18
|
+
|
|
19
|
+
const T = DARK_THEME;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Navigable system message kinds for error-navigation (nextErrorLine/prevErrorLine).
|
|
23
|
+
*
|
|
24
|
+
* Kind → navigable mapping:
|
|
25
|
+
* - 'system' YES — generic/catch-all messages (provider failures, session
|
|
26
|
+
* events, user-visible errors). Default for un-prefixed messages.
|
|
27
|
+
* - 'wrfc' YES — WRFC chain events are important and worth navigating to.
|
|
28
|
+
* - 'operational' NO — tool/scan/plugin/MCP status noise; not useful to jump to.
|
|
29
|
+
*
|
|
30
|
+
* When a message has no recorded kind (added via bare addSystemMessage), it
|
|
31
|
+
* defaults to 'system' and is therefore navigable.
|
|
32
|
+
*/
|
|
33
|
+
const NAVIGABLE_KINDS: ReadonlySet<SystemMessageKind> = new Set(['system', 'wrfc']);
|
|
16
34
|
|
|
17
35
|
type Message = ConversationMessageSnapshot;
|
|
18
36
|
|
|
@@ -29,6 +47,8 @@ interface ConversationRenderContext {
|
|
|
29
47
|
readonly blockRegistry: BlockMeta[];
|
|
30
48
|
readonly collapseState: Map<string, boolean>;
|
|
31
49
|
readonly errorLineRegistry: number[];
|
|
50
|
+
/** Maps message index → SystemMessageKind for typed system messages. */
|
|
51
|
+
readonly messageKindRegistry: ReadonlyMap<number, SystemMessageKind>;
|
|
32
52
|
readonly configManager: ConfigManager | null;
|
|
33
53
|
readonly splashOptions: SplashOptions;
|
|
34
54
|
}
|
|
@@ -40,7 +60,7 @@ export function renderConversationUserMessage(
|
|
|
40
60
|
): void {
|
|
41
61
|
const displayText = extractUserDisplayText(message.content);
|
|
42
62
|
if (message.cancelled) {
|
|
43
|
-
context.history.addLines(UIFactory.createMessageBar(width, displayText,
|
|
63
|
+
context.history.addLines(UIFactory.createMessageBar(width, displayText, T.errorBarBg, '196', ' x ', true));
|
|
44
64
|
return;
|
|
45
65
|
}
|
|
46
66
|
context.history.addLines(UIFactory.createMessageBar(width, displayText));
|
|
@@ -56,20 +76,20 @@ export function renderConversationAssistantMessage(
|
|
|
56
76
|
): void {
|
|
57
77
|
const assistantHeaderDetails = [];
|
|
58
78
|
if (message.model) {
|
|
59
|
-
assistantHeaderDetails.push({ text: ` ${message.model}${message.provider ? ` (${message.provider})` : ''} `, fg:
|
|
79
|
+
assistantHeaderDetails.push({ text: ` ${message.model}${message.provider ? ` (${message.provider})` : ''} `, fg: T.modelNameDim, dim: true });
|
|
60
80
|
}
|
|
61
81
|
if (message.toolCalls && message.toolCalls.length > 0) {
|
|
62
|
-
assistantHeaderDetails.push({ text: ` ${GLYPHS.status.pending} tools:${message.toolCalls.length} `, fg:
|
|
82
|
+
assistantHeaderDetails.push({ text: ` ${GLYPHS.status.pending} tools:${message.toolCalls.length} `, fg: T.toolAccent });
|
|
63
83
|
}
|
|
64
84
|
if (message.reasoningContent || message.reasoningSummary) {
|
|
65
|
-
assistantHeaderDetails.push({ text: ` ${GLYPHS.status.active} reasoning `, fg:
|
|
85
|
+
assistantHeaderDetails.push({ text: ` ${GLYPHS.status.active} reasoning `, fg: T.reasoningAccent, dim: true });
|
|
66
86
|
}
|
|
67
87
|
if (assistantHeaderDetails.length > 0) {
|
|
68
88
|
context.history.addLine(renderConversationEventLine(width, {
|
|
69
89
|
marker: GLYPHS.status.active,
|
|
70
|
-
markerFg:
|
|
90
|
+
markerFg: T.assistantHeader,
|
|
71
91
|
label: 'assistant',
|
|
72
|
-
labelFg:
|
|
92
|
+
labelFg: T.assistantHeader,
|
|
73
93
|
detailFg: '244',
|
|
74
94
|
}, assistantHeaderDetails));
|
|
75
95
|
}
|
|
@@ -180,11 +200,15 @@ export function renderConversationSystemMessage(
|
|
|
180
200
|
context: ConversationRenderContext,
|
|
181
201
|
message: Extract<Message, { role: 'system' }>,
|
|
182
202
|
width: number,
|
|
203
|
+
msgIdx: number,
|
|
183
204
|
): void {
|
|
184
205
|
const sysStartLine = context.history.getLineCount();
|
|
185
206
|
const sysLines = renderSystemMessage(message.content, width);
|
|
186
207
|
context.history.addLines(sysLines);
|
|
187
|
-
|
|
208
|
+
// Resolve navigability from the stored kind, defaulting to 'system'
|
|
209
|
+
// (navigable) for messages added without an explicit kind tag.
|
|
210
|
+
const kind: SystemMessageKind = context.messageKindRegistry.get(msgIdx) ?? 'system';
|
|
211
|
+
if (NAVIGABLE_KINDS.has(kind)) {
|
|
188
212
|
context.errorLineRegistry.push(sysStartLine);
|
|
189
213
|
}
|
|
190
214
|
}
|
|
@@ -218,13 +242,13 @@ export function renderConversationToolMessage(
|
|
|
218
242
|
|
|
219
243
|
context.history.addLine(renderConversationEventLine(width, {
|
|
220
244
|
marker: blockType === 'diff' ? GLYPHS.status.dualPane : GLYPHS.status.active,
|
|
221
|
-
markerFg: blockType === 'diff' ?
|
|
245
|
+
markerFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
|
|
222
246
|
label: blockType === 'diff' ? 'diff' : 'tool result',
|
|
223
|
-
labelFg: blockType === 'diff' ?
|
|
247
|
+
labelFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
|
|
224
248
|
detailFg: '244',
|
|
225
249
|
}, [
|
|
226
250
|
...(message.toolName
|
|
227
|
-
? [{ text: ` ${message.toolName} `, fg:
|
|
251
|
+
? [{ text: ` ${message.toolName} `, fg: T.toolNameFg }]
|
|
228
252
|
: [{ text: ` ${summarizeCallId(message.callId || 'standalone')} `, fg: '244' as const, dim: true }]),
|
|
229
253
|
{ text: ` ${isCollapsed ? GLYPHS.navigation.collapsed : GLYPHS.navigation.expanded} ${lineCount} line${lineCount === 1 ? '' : 's'} `, fg: '244', dim: true },
|
|
230
254
|
]));
|
|
@@ -238,9 +262,9 @@ export function renderConversationToolMessage(
|
|
|
238
262
|
: preview;
|
|
239
263
|
const rendered = renderConversationCollapsedFragment(collapsedText, width, {
|
|
240
264
|
prefix: blockType === 'diff' ? ` ${GLYPHS.status.dualPane} ` : ` ${GLYPHS.navigation.collapsed} `,
|
|
241
|
-
prefixFg: blockType === 'diff' ?
|
|
265
|
+
prefixFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
|
|
242
266
|
text: '244',
|
|
243
|
-
bodyBg:
|
|
267
|
+
bodyBg: T.collapsedBodyBg,
|
|
244
268
|
dim: true,
|
|
245
269
|
});
|
|
246
270
|
context.history.addLines(rendered);
|
|
@@ -280,6 +304,13 @@ export function appendConversationMessages(
|
|
|
280
304
|
messages: Message[],
|
|
281
305
|
width: number,
|
|
282
306
|
messageLineRegistry: number[],
|
|
307
|
+
/**
|
|
308
|
+
* Absolute index of messages[0] in the full (unsliced) conversation snapshot.
|
|
309
|
+
* Required to align slice-relative loop indices with the absolute keys stored
|
|
310
|
+
* in messageKindRegistry, which is keyed at add-time (before any slice).
|
|
311
|
+
* Defaults to 0 when the full snapshot is rendered (no clearDisplay in effect).
|
|
312
|
+
*/
|
|
313
|
+
msgIndexOffset = 0,
|
|
283
314
|
): void {
|
|
284
315
|
const lineNumberMode = context.configManager?.get('display.lineNumbers') ?? 'off';
|
|
285
316
|
const collapseThreshold = context.configManager?.get('display.collapseThreshold') ?? 30;
|
|
@@ -287,14 +318,17 @@ export function appendConversationMessages(
|
|
|
287
318
|
for (let msgIdx = 0; msgIdx < messages.length; msgIdx++) {
|
|
288
319
|
const message = messages[msgIdx];
|
|
289
320
|
messageLineRegistry[msgIdx] = context.history.getLineCount();
|
|
321
|
+
// absoluteIdx aligns the slice-relative loop counter with the absolute
|
|
322
|
+
// message index used as the key in messageKindRegistry.
|
|
323
|
+
const absoluteIdx = msgIndexOffset + msgIdx;
|
|
290
324
|
if (message.role === 'user') {
|
|
291
325
|
renderConversationUserMessage(context, message, width);
|
|
292
326
|
} else if (message.role === 'assistant') {
|
|
293
|
-
renderConversationAssistantMessage(context, message, width, lineNumberMode, collapseThreshold,
|
|
327
|
+
renderConversationAssistantMessage(context, message, width, lineNumberMode, collapseThreshold, absoluteIdx);
|
|
294
328
|
} else if (message.role === 'system') {
|
|
295
|
-
renderConversationSystemMessage(context, message, width);
|
|
329
|
+
renderConversationSystemMessage(context, message, width, absoluteIdx);
|
|
296
330
|
} else if (message.role === 'tool') {
|
|
297
|
-
renderConversationToolMessage(context, message, width,
|
|
331
|
+
renderConversationToolMessage(context, message, width, absoluteIdx);
|
|
298
332
|
}
|
|
299
333
|
context.history.addLine(createEmptyLine(width));
|
|
300
334
|
}
|
package/src/core/conversation.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { ToolCall, ToolResult } from '@pellux/goodvibes-sdk/platform/types'
|
|
|
5
5
|
import type { ProviderMessage, ContentPart } from '@pellux/goodvibes-sdk/platform/providers';
|
|
6
6
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
7
7
|
import type { TranscriptEventKind } from '@pellux/goodvibes-sdk/platform/core';
|
|
8
|
+
import type { SystemMessageKind } from './system-message-router.ts';
|
|
8
9
|
import {
|
|
9
10
|
ConversationManager as SdkConversationManager,
|
|
10
11
|
type BlockMeta as SdkBlockMeta,
|
|
@@ -70,10 +71,31 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
70
71
|
protected blockRegistry: BlockMeta[] = [];
|
|
71
72
|
/** Message index -> first rendered line index in the history buffer. */
|
|
72
73
|
private messageLineRegistry: number[] = [];
|
|
73
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Registry of rendered line indices for system messages whose kind is
|
|
76
|
+
* navigable (error-navigation worthy).
|
|
77
|
+
*
|
|
78
|
+
* Kind → navigable mapping:
|
|
79
|
+
* - 'system' YES — generic/catch-all messages (failures, provider errors,
|
|
80
|
+
* session events); the default for any un-prefixed message
|
|
81
|
+
* - 'wrfc' YES — WRFC chain events (failures matter for navigation)
|
|
82
|
+
* - 'operational' NO — noisy tool/scan/plugin/MCP status; not worth jumping to
|
|
83
|
+
*
|
|
84
|
+
* This replaces the old /error/i substring test, which missed failure phrases
|
|
85
|
+
* like "request failed" / "rate limited" and false-positived on benign info
|
|
86
|
+
* text that happened to contain the word "error".
|
|
87
|
+
*/
|
|
74
88
|
private errorLineRegistry: number[] = [];
|
|
89
|
+
/** Maps message index → SystemMessageKind for system-role messages. */
|
|
90
|
+
private messageKindRegistry: Map<number, SystemMessageKind> = new Map();
|
|
75
91
|
/** Streaming block start line in history buffer (for incremental streaming update). */
|
|
76
92
|
private streamingStartLine = -1;
|
|
93
|
+
/**
|
|
94
|
+
* Timestamp of the last renderMarkdown call during streaming.
|
|
95
|
+
* Used to throttle full re-parses to the 16ms render coalescer cadence.
|
|
96
|
+
* Reset to 0 on startStreamingBlock and finalizeStreamingBlock.
|
|
97
|
+
*/
|
|
98
|
+
private _lastStreamRenderMs = 0;
|
|
77
99
|
/**
|
|
78
100
|
* Message index at the time of the last clearDisplay() call.
|
|
79
101
|
* rebuildHistory() renders only messages at or after this index, so the
|
|
@@ -135,13 +157,49 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
public override addSystemMessage(content: string): void {
|
|
160
|
+
// Clear any stale kind entry at the index this message will occupy.
|
|
161
|
+
// undo() splices the tail of this.messages, freeing indices that ARE reused
|
|
162
|
+
// by subsequent adds. Without this delete, a recycled index could carry a
|
|
163
|
+
// stale kind (e.g. 'operational') and silently mis-classify the new message.
|
|
164
|
+
const nextIndex = this.getMessageSnapshot().length;
|
|
165
|
+
this.messageKindRegistry.delete(nextIndex);
|
|
166
|
+
super.addSystemMessage(content);
|
|
167
|
+
this.markDirty();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* addTypedSystemMessage - Add a system message with an explicit kind tag.
|
|
172
|
+
* The kind is stored in messageKindRegistry (keyed by the message index that
|
|
173
|
+
* will be assigned after the push) so that renderConversationSystemMessage
|
|
174
|
+
* can use it instead of text-based pattern matching.
|
|
175
|
+
*
|
|
176
|
+
* Called by SystemMessageRouter.routeTypedSystemMessage when routing to the
|
|
177
|
+
* conversation surface. Falls back to addSystemMessage for callers that do
|
|
178
|
+
* not have kind information.
|
|
179
|
+
*/
|
|
180
|
+
public addTypedSystemMessage(content: string, kind: SystemMessageKind): void {
|
|
181
|
+
// getMessageSnapshot().length is the index this message will receive after
|
|
182
|
+
// addSystemMessage appends it to the messages array.
|
|
183
|
+
const nextIndex = this.getMessageSnapshot().length;
|
|
184
|
+
this.messageKindRegistry.set(nextIndex, kind);
|
|
138
185
|
super.addSystemMessage(content);
|
|
139
186
|
this.markDirty();
|
|
140
187
|
}
|
|
141
188
|
|
|
142
189
|
public override undo(): boolean {
|
|
143
190
|
const result = super.undo();
|
|
144
|
-
if (result)
|
|
191
|
+
if (result) {
|
|
192
|
+
// undo() splices the messages tail at the last user-message index, meaning
|
|
193
|
+
// freed indices CAN be reused by subsequent adds. Purge all registry entries
|
|
194
|
+
// at or after the new message count so a recycled index cannot carry a stale
|
|
195
|
+
// kind from the evicted turn (e.g. 'operational' mis-classifying a bare add
|
|
196
|
+
// as non-navigable, or 'wrfc' wrongly making an operational message navigable).
|
|
197
|
+
const postUndoCount = this.getMessageSnapshot().length;
|
|
198
|
+
for (const key of this.messageKindRegistry.keys()) {
|
|
199
|
+
if (key >= postUndoCount) this.messageKindRegistry.delete(key);
|
|
200
|
+
}
|
|
201
|
+
this.markDirty();
|
|
202
|
+
}
|
|
145
203
|
return result;
|
|
146
204
|
}
|
|
147
205
|
|
|
@@ -172,9 +230,11 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
172
230
|
public override startStreamingBlock(): void {
|
|
173
231
|
super.startStreamingBlock();
|
|
174
232
|
this.markDirty();
|
|
175
|
-
// Record the line where the streaming block starts so updates can be incremental
|
|
233
|
+
// Record the line where the streaming block starts so updates can be incremental.
|
|
234
|
+
// Reset the render throttle so the first delta always renders immediately.
|
|
176
235
|
this.flushHistory();
|
|
177
236
|
this.streamingStartLine = this.history.getLineCount();
|
|
237
|
+
this._lastStreamRenderMs = 0;
|
|
178
238
|
}
|
|
179
239
|
|
|
180
240
|
/**
|
|
@@ -184,12 +244,18 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
184
244
|
*/
|
|
185
245
|
public override updateStreamingBlock(content: string): void {
|
|
186
246
|
super.updateStreamingBlock(content);
|
|
187
|
-
// Incrementally update the history buffer instead of full rebuild
|
|
247
|
+
// Incrementally update the history buffer instead of full rebuild.
|
|
248
|
+
// Throttle renderMarkdown to the 16ms render-coalescer cadence to avoid
|
|
249
|
+
// O(n) parse overhead on every delta token during streaming.
|
|
188
250
|
if (this.streamingStartLine >= 0) {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
if (now - this._lastStreamRenderMs >= 16) {
|
|
253
|
+
this._lastStreamRenderMs = now;
|
|
254
|
+
const width = this._getWidth();
|
|
255
|
+
this.history.truncateToLine(this.streamingStartLine);
|
|
256
|
+
const rendered = renderMarkdown(content, width, { isStreaming: true });
|
|
257
|
+
this.history.addLines(rendered);
|
|
258
|
+
}
|
|
193
259
|
}
|
|
194
260
|
}
|
|
195
261
|
|
|
@@ -200,6 +266,7 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
200
266
|
public override finalizeStreamingBlock(): void {
|
|
201
267
|
super.finalizeStreamingBlock();
|
|
202
268
|
this.streamingStartLine = -1;
|
|
269
|
+
this._lastStreamRenderMs = 0;
|
|
203
270
|
this.markDirty();
|
|
204
271
|
}
|
|
205
272
|
|
|
@@ -221,6 +288,7 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
221
288
|
this.blockRegistry = [];
|
|
222
289
|
this.messageLineRegistry = [];
|
|
223
290
|
this.errorLineRegistry = [];
|
|
291
|
+
this.messageKindRegistry = new Map();
|
|
224
292
|
this.streamingStartLine = -1;
|
|
225
293
|
this._displayFromMessageIndex = 0; // full reset — show everything on next render
|
|
226
294
|
}
|
|
@@ -297,6 +365,8 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
297
365
|
this.blockRegistry = [];
|
|
298
366
|
this.messageLineRegistry = [];
|
|
299
367
|
this.errorLineRegistry = [];
|
|
368
|
+
// messageKindRegistry is NOT cleared here: kind info is set at add-time
|
|
369
|
+
// and must survive width-change rebuilds.
|
|
300
370
|
const width = this._getWidth();
|
|
301
371
|
this.lastRenderedWidth = width;
|
|
302
372
|
this.dirty = false;
|
|
@@ -321,7 +391,7 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
321
391
|
return;
|
|
322
392
|
}
|
|
323
393
|
|
|
324
|
-
this.appendMessages(visibleSnapshot, width);
|
|
394
|
+
this.appendMessages(visibleSnapshot, width, displayStart);
|
|
325
395
|
this.appendedUpTo = snapshot.length;
|
|
326
396
|
}
|
|
327
397
|
|
|
@@ -345,6 +415,7 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
345
415
|
blockRegistry: this.blockRegistry,
|
|
346
416
|
collapseState: this.collapseState,
|
|
347
417
|
errorLineRegistry: this.errorLineRegistry,
|
|
418
|
+
messageKindRegistry: this.messageKindRegistry as ReadonlyMap<number, SystemMessageKind>,
|
|
348
419
|
configManager: this._configManager,
|
|
349
420
|
splashOptions: this.splashOptions,
|
|
350
421
|
};
|
|
@@ -364,17 +435,24 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
364
435
|
renderConversationAssistantMessage(this.renderingContext(), message, width, lineNumberMode, collapseThreshold, msgIdx);
|
|
365
436
|
}
|
|
366
437
|
|
|
367
|
-
private renderSystemMessage(message: Extract<Message, { role: 'system' }>, width: number): void {
|
|
368
|
-
renderConversationSystemMessage(this.renderingContext(), message, width);
|
|
438
|
+
private renderSystemMessage(message: Extract<Message, { role: 'system' }>, width: number, msgIdx: number): void {
|
|
439
|
+
renderConversationSystemMessage(this.renderingContext(), message, width, msgIdx);
|
|
369
440
|
}
|
|
370
441
|
|
|
371
442
|
private renderToolMessage(message: Extract<Message, { role: 'tool' }>, width: number, msgIdx: number): void {
|
|
372
443
|
renderConversationToolMessage(this.renderingContext(), message, width, msgIdx);
|
|
373
444
|
}
|
|
374
445
|
|
|
375
|
-
/**
|
|
376
|
-
|
|
377
|
-
|
|
446
|
+
/**
|
|
447
|
+
* Render a slice of messages into the history buffer.
|
|
448
|
+
*
|
|
449
|
+
* @param msgIndexOffset - Absolute index of messages[0] in the full snapshot.
|
|
450
|
+
* Must equal displayStart when rendering a post-clearDisplay slice so that
|
|
451
|
+
* the renderer can resolve messageKindRegistry keys (which are absolute)
|
|
452
|
+
* from its slice-relative loop counter.
|
|
453
|
+
*/
|
|
454
|
+
private appendMessages(messages: Message[], width: number, msgIndexOffset = 0): void {
|
|
455
|
+
appendConversationMessages(this.renderingContext(), messages, width, this.messageLineRegistry, msgIndexOffset);
|
|
378
456
|
}
|
|
379
457
|
|
|
380
458
|
/** Find the nearest block to a given line index, optionally filtered by type. */
|
|
@@ -446,8 +524,9 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
446
524
|
|
|
447
525
|
/**
|
|
448
526
|
* getErrorLines - Returns line indices in the rendered history buffer for
|
|
449
|
-
* system messages
|
|
450
|
-
*
|
|
527
|
+
* system messages whose kind is navigable (see NAVIGABLE_KINDS in
|
|
528
|
+
* conversation-rendering.ts: 'system' and 'wrfc'). Operational messages
|
|
529
|
+
* are excluded regardless of message text. Triggers a history flush if dirty.
|
|
451
530
|
*/
|
|
452
531
|
public getErrorLines(): number[] {
|
|
453
532
|
this.flushHistory();
|
|
@@ -538,6 +617,12 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
538
617
|
this.blockRegistry = [];
|
|
539
618
|
this.messageLineRegistry = [];
|
|
540
619
|
this.errorLineRegistry = [];
|
|
620
|
+
// messageKindRegistry is NOT cleared here. The underlying messages array is
|
|
621
|
+
// preserved by clearDisplay(); kind entries for pre-clear messages are harmless
|
|
622
|
+
// because those messages are hidden by _displayFromMessageIndex and never rendered.
|
|
623
|
+
// Clearing the registry would cause kind loss for pre-clear messages that become
|
|
624
|
+
// visible again after a subsequent width-change rebuild (which resets displayStart
|
|
625
|
+
// to 0), incorrectly making operational messages navigable.
|
|
541
626
|
// Advance _displayFromMessageIndex to exclude all current messages from display.
|
|
542
627
|
// rebuildHistory() will only render messages added AFTER this point.
|
|
543
628
|
this._displayFromMessageIndex = this.getMessageSnapshot().length;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* formatUserFacingError — classifies provider/network errors into plain-language
|
|
3
|
+
* one-liners with a suggested action.
|
|
4
|
+
*
|
|
5
|
+
* Classification order (most-specific first):
|
|
6
|
+
* auth → 401, 'invalid' + 'key'/'token', 'Unauthorized', 'forbidden'
|
|
7
|
+
* rate-limit → 429, 'rate limit', 'rate_limit', 'too many requests', 'quota'
|
|
8
|
+
* context-overflow → 'context length', 'maximum context', 'too many tokens',
|
|
9
|
+
* 'context window'
|
|
10
|
+
* network → ECONNREFUSED, ETIMEDOUT, 'fetch failed', 'socket hang up',
|
|
11
|
+
* 'network', ENOTFOUND
|
|
12
|
+
* generic → summarizeError fallback
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Types
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export type ErrorClass =
|
|
24
|
+
| 'auth'
|
|
25
|
+
| 'rate-limit'
|
|
26
|
+
| 'context-overflow'
|
|
27
|
+
| 'network'
|
|
28
|
+
| 'generic';
|
|
29
|
+
|
|
30
|
+
export interface UserFacingError {
|
|
31
|
+
/** Short, plain-language description of what went wrong. */
|
|
32
|
+
message: string;
|
|
33
|
+
/** Suggested recovery action (slash-command or brief instruction). */
|
|
34
|
+
action: string;
|
|
35
|
+
/** Which classifier matched. */
|
|
36
|
+
kind: ErrorClass;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Classifier
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract a canonical string probe from an unknown error value.
|
|
45
|
+
* Concatenates status code (if present), error name, and message so that a
|
|
46
|
+
* single regex can match across all shapes.
|
|
47
|
+
*/
|
|
48
|
+
function probeString(err: unknown): string {
|
|
49
|
+
if (err == null) return '';
|
|
50
|
+
const parts: string[] = [];
|
|
51
|
+
|
|
52
|
+
if (typeof err === 'object') {
|
|
53
|
+
// HTTP-style status fields
|
|
54
|
+
const asRecord = err as Record<string, unknown>;
|
|
55
|
+
const status = asRecord['status'] ?? asRecord['statusCode'] ?? asRecord['code'];
|
|
56
|
+
if (status != null) parts.push(String(status));
|
|
57
|
+
|
|
58
|
+
// name and message
|
|
59
|
+
if ('name' in asRecord && typeof asRecord['name'] === 'string') parts.push(asRecord['name']);
|
|
60
|
+
if ('message' in asRecord && typeof asRecord['message'] === 'string') parts.push(asRecord['message']);
|
|
61
|
+
|
|
62
|
+
// cause chain (one level)
|
|
63
|
+
if ('cause' in asRecord && asRecord['cause'] != null) {
|
|
64
|
+
const cause = asRecord['cause'] as Record<string, unknown>;
|
|
65
|
+
if (typeof cause['message'] === 'string') parts.push(cause['message']);
|
|
66
|
+
if (typeof cause['code'] === 'string') parts.push(cause['code']);
|
|
67
|
+
}
|
|
68
|
+
} else if (typeof err === 'string') {
|
|
69
|
+
parts.push(err);
|
|
70
|
+
} else {
|
|
71
|
+
parts.push(String(err));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return parts.join(' ').toLowerCase();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function classifyError(err: unknown): ErrorClass {
|
|
78
|
+
const probe = probeString(err);
|
|
79
|
+
|
|
80
|
+
// Auth: 401 status, key/token invalidity, Unauthorized
|
|
81
|
+
if (
|
|
82
|
+
/\b401\b/.test(probe) ||
|
|
83
|
+
/invalid.{0,10}(api.?key|key|token)/i.test(probe) ||
|
|
84
|
+
/unauthorized/i.test(probe) ||
|
|
85
|
+
/authentication/i.test(probe) ||
|
|
86
|
+
/api.?key.{0,20}(missing|not.?set|required|invalid)/i.test(probe)
|
|
87
|
+
) {
|
|
88
|
+
return 'auth';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Rate-limit: 429 status, quota exceeded
|
|
92
|
+
if (
|
|
93
|
+
/\b429\b/.test(probe) ||
|
|
94
|
+
/rate.?limit/i.test(probe) ||
|
|
95
|
+
/too many requests/i.test(probe) ||
|
|
96
|
+
/quota.{0,20}exceeded/i.test(probe) ||
|
|
97
|
+
/request.{0,20}limit/i.test(probe)
|
|
98
|
+
) {
|
|
99
|
+
return 'rate-limit';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Context overflow: provider context-window exceeded messages
|
|
103
|
+
if (
|
|
104
|
+
/context.{0,10}(length|window|limit)/i.test(probe) ||
|
|
105
|
+
/maximum context/i.test(probe) ||
|
|
106
|
+
/too many tokens/i.test(probe) ||
|
|
107
|
+
/token.{0,10}limit/i.test(probe) ||
|
|
108
|
+
/context.{0,20}exceeded/i.test(probe) ||
|
|
109
|
+
/input.{0,10}too.{0,10}long/i.test(probe)
|
|
110
|
+
) {
|
|
111
|
+
return 'context-overflow';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Network: connection/transport errors
|
|
115
|
+
if (
|
|
116
|
+
/econnrefused/i.test(probe) ||
|
|
117
|
+
/etimedout/i.test(probe) ||
|
|
118
|
+
/enotfound/i.test(probe) ||
|
|
119
|
+
/fetch failed/i.test(probe) ||
|
|
120
|
+
/socket hang up/i.test(probe) ||
|
|
121
|
+
/network.{0,20}(error|failure|timeout)/i.test(probe) ||
|
|
122
|
+
/connection.{0,20}(refused|reset|timeout|closed)/i.test(probe) ||
|
|
123
|
+
/timeout/i.test(probe)
|
|
124
|
+
) {
|
|
125
|
+
return 'network';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return 'generic';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Formatter
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Classifies `err` and returns a plain-language one-liner with a suggested
|
|
137
|
+
* recovery action.
|
|
138
|
+
*
|
|
139
|
+
* Uses `summarizeError` from the SDK as the detail fallback for generic errors
|
|
140
|
+
* so stack traces never leak into the UI.
|
|
141
|
+
*/
|
|
142
|
+
export function formatUserFacingError(err: unknown): UserFacingError {
|
|
143
|
+
const kind = classifyError(err);
|
|
144
|
+
|
|
145
|
+
switch (kind) {
|
|
146
|
+
case 'auth':
|
|
147
|
+
return {
|
|
148
|
+
kind,
|
|
149
|
+
message: 'Authentication failed — the provider rejected your API key.',
|
|
150
|
+
action: 'Run /login to re-authenticate or check your API key.',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
case 'rate-limit':
|
|
154
|
+
return {
|
|
155
|
+
kind,
|
|
156
|
+
message: 'Rate limit reached — the provider is throttling requests.',
|
|
157
|
+
action: 'Wait a moment and retry, or switch models with /model.',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
case 'context-overflow':
|
|
161
|
+
return {
|
|
162
|
+
kind,
|
|
163
|
+
message: 'Context window exceeded — the conversation is too long for this model.',
|
|
164
|
+
action: 'Run /compact to summarise the conversation and free context.',
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
case 'network':
|
|
168
|
+
return {
|
|
169
|
+
kind,
|
|
170
|
+
message: 'Network error — could not reach the provider.',
|
|
171
|
+
action: 'Check your connection and retry, or switch models with /model.',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
default: {
|
|
175
|
+
const detail = summarizeError(err);
|
|
176
|
+
return {
|
|
177
|
+
kind: 'generic',
|
|
178
|
+
message: `Provider error: ${detail}`,
|
|
179
|
+
action: 'Retry your last message, or switch models with /model.',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Convenience helper: returns the full formatted string for use in a single
|
|
187
|
+
* system message. Formats as "<message> <action>"
|
|
188
|
+
*/
|
|
189
|
+
export function formatUserFacingErrorLine(err: unknown): string {
|
|
190
|
+
const { message, action } = formatUserFacingError(err);
|
|
191
|
+
return `${message} ${action}`;
|
|
192
|
+
}
|