@pellux/goodvibes-tui 1.1.0 → 1.9.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 +85 -21
- package/README.md +28 -18
- package/docs/foundation-artifacts/operator-contract.json +4045 -1763
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +12 -2
- package/src/audio/spoken-turn-wiring.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/cli/status.ts +2 -1
- package/src/cli/surface-command.ts +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +90 -14
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/stream-event-wiring.ts +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +104 -51
- package/src/core/turn-cancellation.ts +25 -0
- package/src/core/turn-event-wiring.ts +72 -3
- package/src/daemon/cli.ts +29 -2
- package/src/daemon/handlers/register.ts +8 -1
- package/src/daemon/service-commands.ts +329 -0
- package/src/export/cost-utils.ts +36 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +84 -5
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +232 -0
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +10 -2
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +73 -35
- package/src/input/commands/operator-panel-runtime.ts +71 -31
- package/src/input/commands/planning-runtime.ts +116 -4
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +26 -6
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +20 -9
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +16 -4
- package/src/input/commands/shell-core.ts +25 -8
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +488 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +175 -0
- package/src/input/config-modal.ts +592 -0
- package/src/input/feed-context-factory.ts +10 -8
- package/src/input/handler-command-route.ts +37 -20
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +74 -114
- package/src/input/handler-feed.ts +79 -27
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +136 -6
- package/src/input/handler-modal-stack.ts +14 -7
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
- package/src/input/handler-onboarding.ts +71 -59
- package/src/input/handler-picker-routes.ts +26 -102
- package/src/input/handler-shortcuts.ts +74 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +12 -29
- package/src/input/keybindings.ts +23 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +77 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/panel-mouse-geometry.ts +97 -0
- package/src/input/panel-paste-flood-guard.ts +86 -0
- package/src/input/selection-modal.ts +11 -0
- package/src/input/session-picker-modal.ts +44 -4
- package/src/input/settings-modal-data.ts +146 -2
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +49 -51
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +18 -46
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-deep-link.ts +31 -0
- package/src/panels/fleet-panel-format.ts +62 -0
- package/src/panels/fleet-panel-worktree-detail.ts +48 -0
- package/src/panels/fleet-panel.ts +758 -0
- package/src/panels/fleet-read-model.ts +520 -0
- package/src/panels/fleet-steer.ts +125 -0
- package/src/panels/fleet-stop.ts +153 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +166 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +42 -0
- package/src/panels/modals/pairing-modal.ts +163 -0
- package/src/panels/modals/planning-modal.ts +304 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +177 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +123 -9
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -11
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +37 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +216 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +95 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +15 -3
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +43 -1
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/layout.ts +0 -4
- package/src/renderer/markdown.ts +7 -3
- package/src/renderer/modal-factory.ts +25 -20
- package/src/renderer/model-workspace.ts +62 -4
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/process-indicator.ts +14 -3
- package/src/renderer/selection-modal-overlay.ts +6 -1
- package/src/renderer/session-picker-modal.ts +196 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +7 -0
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/status-glyphs.ts +14 -15
- package/src/renderer/system-message.ts +15 -3
- package/src/renderer/terminal-bg-probe.ts +339 -0
- package/src/renderer/terminal-escapes.ts +20 -0
- package/src/renderer/theme-mode-config.ts +67 -0
- package/src/renderer/theme.ts +91 -1
- package/src/renderer/thinking.ts +11 -3
- package/src/renderer/tool-call.ts +15 -9
- package/src/renderer/tool-result-summary.ts +148 -0
- package/src/renderer/turn-injection.ts +133 -0
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +27 -1
- package/src/runtime/bootstrap-command-parts.ts +41 -10
- package/src/runtime/bootstrap-core.ts +47 -15
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +48 -17
- package/src/runtime/bootstrap.ts +129 -22
- package/src/runtime/code-index-services.ts +135 -0
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +26 -0
- package/src/runtime/onboarding/derivation.ts +7 -2
- package/src/runtime/onboarding/snapshot.ts +27 -1
- package/src/runtime/onboarding/types.ts +31 -1
- package/src/runtime/operator-token-cleanup.ts +82 -1
- package/src/runtime/orchestrator-core-services.ts +59 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +98 -46
- package/src/runtime/session-inbound-inputs.ts +252 -0
- package/src/runtime/session-spine-transport.ts +64 -0
- package/src/runtime/terminal-output-guard.ts +15 -8
- package/src/runtime/ui-services.ts +27 -3
- package/src/runtime/workstream-services.ts +327 -0
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +68 -4
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +171 -26
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/utils/terminal-width.ts +52 -0
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { UIFactory } from '../renderer/ui-factory.ts';
|
|
2
2
|
import { renderMarkdownTracked } from '../renderer/markdown.ts';
|
|
3
3
|
import { renderDiffView } from '../renderer/diff-view.ts';
|
|
4
|
-
import {
|
|
4
|
+
import { activeTheme } from '../renderer/theme.ts';
|
|
5
5
|
import { renderToolCallBlock } from '../renderer/tool-call.ts';
|
|
6
|
+
import { summarizeToolResult } from '../renderer/tool-result-summary.ts';
|
|
7
|
+
import type { ToolCall } from '@pellux/goodvibes-sdk/platform/types';
|
|
6
8
|
import { renderThinkingBlock } from '../renderer/thinking.ts';
|
|
7
9
|
import { renderSystemMessage } from '../renderer/system-message.ts';
|
|
8
10
|
import { createEmptyLine, type Line, type Cell } from '../types/grid.ts';
|
|
@@ -20,7 +22,9 @@ import { extractUserDisplayText } from '@pellux/goodvibes-sdk/platform/core';
|
|
|
20
22
|
// conversation-rendering.ts → system-message-router.ts → conversation.ts → conversation-rendering.ts
|
|
21
23
|
import type { SystemMessageKind } from '@/runtime/index.ts';
|
|
22
24
|
|
|
23
|
-
const T =
|
|
25
|
+
// Transcript tokens are read live per render (const T = activeTheme() at the top
|
|
26
|
+
// of each render function that styles content) so a dark→light repaint
|
|
27
|
+
// re-resolves with no module reload. See theme.ts's active-mode runtime note.
|
|
24
28
|
|
|
25
29
|
/**
|
|
26
30
|
* Navigable system message kinds for error-navigation (nextErrorLine/prevErrorLine).
|
|
@@ -38,6 +42,20 @@ const NAVIGABLE_KINDS: ReadonlySet<SystemMessageKind> = new Set(['system', 'wrfc
|
|
|
38
42
|
|
|
39
43
|
type Message = ConversationMessageSnapshot;
|
|
40
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Collect the ids of tool calls that have a matching tool-result message in the
|
|
47
|
+
* given slice — i.e. the tools that actually ran. Used to decide whether an
|
|
48
|
+
* assistant tool call still shows as pending (awaiting approval) or done.
|
|
49
|
+
* (UX-B item 2c.)
|
|
50
|
+
*/
|
|
51
|
+
export function collectCompletedToolCallIds(messages: readonly Message[]): Set<string> {
|
|
52
|
+
const ids = new Set<string>();
|
|
53
|
+
for (const message of messages) {
|
|
54
|
+
if (message.role === 'tool' && message.callId) ids.add(message.callId);
|
|
55
|
+
}
|
|
56
|
+
return ids;
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
function summarizeCallId(callId: string, maxLength = 24): string {
|
|
42
60
|
return callId.length <= maxLength ? callId : `${callId.slice(0, maxLength - 1)}…`;
|
|
43
61
|
}
|
|
@@ -55,6 +73,15 @@ export interface ConversationRenderContext {
|
|
|
55
73
|
readonly messageKindRegistry: ReadonlyMap<number, SystemMessageKind>;
|
|
56
74
|
readonly configManager: ConfigManager | null;
|
|
57
75
|
readonly splashOptions: SplashOptions;
|
|
76
|
+
/**
|
|
77
|
+
* Tool-call ids that have a corresponding tool-result message (i.e. the tool
|
|
78
|
+
* actually ran). An assistant tool call whose id is NOT in this set is still
|
|
79
|
+
* awaiting a decision (e.g. an approval prompt) and renders with a pending
|
|
80
|
+
* glyph instead of the completed ✓. When undefined (single-message callers
|
|
81
|
+
* without sibling context), every tool call renders as done — the prior
|
|
82
|
+
* behaviour. (UX-B item 2c.)
|
|
83
|
+
*/
|
|
84
|
+
readonly completedToolCallIds?: ReadonlySet<string>;
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
export function renderConversationUserMessage(
|
|
@@ -62,6 +89,7 @@ export function renderConversationUserMessage(
|
|
|
62
89
|
message: Extract<Message, { role: 'user' }>,
|
|
63
90
|
width: number,
|
|
64
91
|
): void {
|
|
92
|
+
const T = activeTheme();
|
|
65
93
|
const displayText = extractUserDisplayText(message.content);
|
|
66
94
|
if (message.cancelled) {
|
|
67
95
|
context.history.addLines(UIFactory.createMessageBar(width, displayText, T.errorBarBg, '196', ' x ', true));
|
|
@@ -78,6 +106,7 @@ export function renderConversationAssistantMessage(
|
|
|
78
106
|
collapseThreshold: number,
|
|
79
107
|
msgIdx: number,
|
|
80
108
|
): void {
|
|
109
|
+
const T = activeTheme();
|
|
81
110
|
const assistantHeaderDetails = [];
|
|
82
111
|
if (message.model) {
|
|
83
112
|
assistantHeaderDetails.push({ text: ` ${message.model}${message.provider ? ` (${message.provider})` : ''} `, fg: T.modelNameDim, dim: true });
|
|
@@ -195,7 +224,11 @@ export function renderConversationAssistantMessage(
|
|
|
195
224
|
|
|
196
225
|
if (message.toolCalls && message.toolCalls.length > 0) {
|
|
197
226
|
for (const tc of message.toolCalls) {
|
|
198
|
-
|
|
227
|
+
// A tool call with no result message yet is still awaiting a decision
|
|
228
|
+
// (e.g. approval) — show a pending glyph, not the completed ✓.
|
|
229
|
+
const ran = context.completedToolCallIds === undefined
|
|
230
|
+
|| (tc.id !== undefined && context.completedToolCallIds.has(tc.id));
|
|
231
|
+
context.history.addLines(renderToolCallBlock(tc, ran ? 'done' : 'pending', undefined, width));
|
|
199
232
|
}
|
|
200
233
|
}
|
|
201
234
|
}
|
|
@@ -223,6 +256,7 @@ export function renderConversationToolMessage(
|
|
|
223
256
|
width: number,
|
|
224
257
|
msgIdx: number,
|
|
225
258
|
): void {
|
|
259
|
+
const T = activeTheme();
|
|
226
260
|
const collapseKey = `msg_${msgIdx}`;
|
|
227
261
|
const blockIdx = context.blockRegistry.length;
|
|
228
262
|
const startLine = context.history.getLineCount();
|
|
@@ -236,7 +270,17 @@ export function renderConversationToolMessage(
|
|
|
236
270
|
// block-registry meta merge below regardless of collapsed/expanded state.
|
|
237
271
|
const diffParse = isDiff ? parseDiffForApply(message.content) : undefined;
|
|
238
272
|
|
|
239
|
-
|
|
273
|
+
// Human one-line summary for tool results (write/read/exec/edit): shown as the
|
|
274
|
+
// collapsed line so the transcript reads "wrote foo.txt (532 B)" instead of a
|
|
275
|
+
// raw JSON blob; the full payload stays behind the expand toggle. Only for
|
|
276
|
+
// 'tool' blocks (diffs render their own view). (UX-B item 3.)
|
|
277
|
+
const resultSummary = blockType === 'tool'
|
|
278
|
+
? summarizeToolResult(message.toolName, message.content)
|
|
279
|
+
: null;
|
|
280
|
+
|
|
281
|
+
// A summarizable result is kept collapsed-by-default even when short, so the
|
|
282
|
+
// one-line summary wins and the raw JSON is tucked behind the expand toggle.
|
|
283
|
+
const isShort = message.content.length <= 200 && resultSummary === null;
|
|
240
284
|
const isCollapsed = isShort
|
|
241
285
|
? false
|
|
242
286
|
: context.collapseState.has(collapseKey)
|
|
@@ -262,11 +306,20 @@ export function renderConversationToolMessage(
|
|
|
262
306
|
|
|
263
307
|
if (isCollapsed) {
|
|
264
308
|
const collapseSuffixReserve = 30;
|
|
265
|
-
const
|
|
309
|
+
const avail = Math.max(0, width - LAYOUT.LEFT_MARGIN - LAYOUT.RIGHT_MARGIN - collapseSuffixReserve);
|
|
266
310
|
const hiddenCount = lineCount - 1;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
311
|
+
let collapsedText: string;
|
|
312
|
+
if (resultSummary !== null) {
|
|
313
|
+
// Summary line — the header already shows "▸ N lines", so no hidden badge.
|
|
314
|
+
collapsedText = resultSummary.length > avail
|
|
315
|
+
? `${resultSummary.slice(0, Math.max(0, avail - 1))}…`
|
|
316
|
+
: resultSummary;
|
|
317
|
+
} else {
|
|
318
|
+
const preview = contentLines[0].slice(0, avail);
|
|
319
|
+
collapsedText = hiddenCount > 0
|
|
320
|
+
? `${preview}... [${GLYPHS.navigation.collapsed} ${hiddenCount} hidden]`
|
|
321
|
+
: preview;
|
|
322
|
+
}
|
|
270
323
|
const rendered = renderConversationCollapsedFragment(collapsedText, width, {
|
|
271
324
|
prefix: blockType === 'diff' ? ` ${GLYPHS.status.dualPane} ` : ` ${GLYPHS.navigation.collapsed} `,
|
|
272
325
|
prefixFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
|
|
@@ -325,23 +378,28 @@ export function appendConversationMessages(
|
|
|
325
378
|
): void {
|
|
326
379
|
const lineNumberMode = context.configManager?.get('display.lineNumbers') ?? 'off';
|
|
327
380
|
const collapseThreshold = context.configManager?.get('display.collapseThreshold') ?? 30;
|
|
381
|
+
// Derive pending vs done for tool calls from sibling tool-result messages,
|
|
382
|
+
// unless the caller already supplied the set. (UX-B item 2c.)
|
|
383
|
+
const renderContext: ConversationRenderContext = context.completedToolCallIds !== undefined
|
|
384
|
+
? context
|
|
385
|
+
: { ...context, completedToolCallIds: collectCompletedToolCallIds(messages) };
|
|
328
386
|
|
|
329
387
|
for (let msgIdx = 0; msgIdx < messages.length; msgIdx++) {
|
|
330
388
|
const message = messages[msgIdx];
|
|
331
389
|
// absoluteIdx aligns the slice-relative loop counter with the absolute
|
|
332
390
|
// message index used as the key in messageKindRegistry and messageLineRegistry.
|
|
333
391
|
const absoluteIdx = msgIndexOffset + msgIdx;
|
|
334
|
-
messageLineRegistry[absoluteIdx] =
|
|
392
|
+
messageLineRegistry[absoluteIdx] = renderContext.history.getLineCount();
|
|
335
393
|
if (message.role === 'user') {
|
|
336
|
-
renderConversationUserMessage(
|
|
394
|
+
renderConversationUserMessage(renderContext, message, width);
|
|
337
395
|
} else if (message.role === 'assistant') {
|
|
338
|
-
renderConversationAssistantMessage(
|
|
396
|
+
renderConversationAssistantMessage(renderContext, message, width, lineNumberMode, collapseThreshold, absoluteIdx);
|
|
339
397
|
} else if (message.role === 'system') {
|
|
340
|
-
renderConversationSystemMessage(
|
|
398
|
+
renderConversationSystemMessage(renderContext, message, width, absoluteIdx);
|
|
341
399
|
} else if (message.role === 'tool') {
|
|
342
|
-
renderConversationToolMessage(
|
|
400
|
+
renderConversationToolMessage(renderContext, message, width, absoluteIdx);
|
|
343
401
|
}
|
|
344
|
-
|
|
402
|
+
renderContext.history.addLine(createEmptyLine(width));
|
|
345
403
|
}
|
|
346
404
|
}
|
|
347
405
|
|
|
@@ -402,3 +460,21 @@ export function logConversationText(
|
|
|
402
460
|
const lines = text.split('\n').map((line) => UIFactory.stringToLine(indent + line, width, style));
|
|
403
461
|
context.history.addLines(lines);
|
|
404
462
|
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* logConversationToolResult - Append a single tool-call-style result line to the
|
|
466
|
+
* display history only, reusing renderToolCallBlock so a display-only render (e.g.
|
|
467
|
+
* a slash command's subprocess result) is visually indistinguishable from the
|
|
468
|
+
* model's own tool-call results. Display-only: never touches message history.
|
|
469
|
+
*/
|
|
470
|
+
export function logConversationToolResult(
|
|
471
|
+
context: Pick<ConversationRenderContext, 'history'>,
|
|
472
|
+
width: number,
|
|
473
|
+
toolCall: ToolCall,
|
|
474
|
+
status: 'done' | 'error',
|
|
475
|
+
resultSummary: string,
|
|
476
|
+
durationMs: number,
|
|
477
|
+
errorMsg?: string,
|
|
478
|
+
): void {
|
|
479
|
+
context.history.addLines(renderToolCallBlock(toolCall, status, resultSummary, width, durationMs, errorMsg));
|
|
480
|
+
}
|
package/src/core/conversation.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
addConversationSplashScreen,
|
|
19
19
|
conversationTextToLines,
|
|
20
20
|
logConversationText,
|
|
21
|
+
logConversationToolResult,
|
|
21
22
|
renderConversationAssistantMessage,
|
|
22
23
|
renderConversationSystemMessage,
|
|
23
24
|
renderConversationToolMessage,
|
|
@@ -727,6 +728,23 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
727
728
|
logConversationText(this.renderingContext(), this._getWidth(), text, style, indent);
|
|
728
729
|
}
|
|
729
730
|
|
|
731
|
+
/**
|
|
732
|
+
* logToolResultBlock - Append a single tool-call-styled result line to the display
|
|
733
|
+
* history only (never touches the LLM message list), so a slash command's subprocess
|
|
734
|
+
* result (e.g. /test) renders visually identical to a real tool-call result without
|
|
735
|
+
* leaking into model context.
|
|
736
|
+
*/
|
|
737
|
+
public logToolResultBlock(
|
|
738
|
+
toolCall: ToolCall,
|
|
739
|
+
status: 'done' | 'error',
|
|
740
|
+
resultSummary: string,
|
|
741
|
+
durationMs: number,
|
|
742
|
+
errorMsg?: string,
|
|
743
|
+
): void {
|
|
744
|
+
logConversationToolResult(this.renderingContext(), this._getWidth(), toolCall, status, resultSummary, durationMs, errorMsg);
|
|
745
|
+
this.markDirty();
|
|
746
|
+
}
|
|
747
|
+
|
|
730
748
|
/**
|
|
731
749
|
* clearDisplay - Clear the visual history buffer without touching the LLM context messages.
|
|
732
750
|
* The next render will show a blank conversation area. Subsequent message additions
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* focus-tracker — tracks whether the terminal window currently has OS-level
|
|
3
|
+
* input focus, using the terminal focus-reporting protocol (DECSET ?1004h,
|
|
4
|
+
* enabled at terminal setup in main.ts). The SDK's InputTokenizer already
|
|
5
|
+
* parses the resulting `\x1b[I` (focus-in) / `\x1b[O` (focus-out) sequences
|
|
6
|
+
* into `{ type: 'focus', action }` tokens (platform/core/tokenizer.ts); this
|
|
7
|
+
* module is the single place those tokens land (consumed in
|
|
8
|
+
* src/input/handler-feed.ts's feedInputTokens()).
|
|
9
|
+
*
|
|
10
|
+
* Honest degradation: a terminal that does not implement focus reporting
|
|
11
|
+
* (or a multiplexer that swallows the sequences) never sends focus tokens.
|
|
12
|
+
* In that case `isFocused()` stays `null` forever — "unknown", never a
|
|
13
|
+
* guessed `true`/`false`. Callers that gate a user-facing behavior on focus
|
|
14
|
+
* (e.g. the unfocused-alert notifiers in this directory) must treat `null`
|
|
15
|
+
* the same as "not focused" per the fallback rule: alerts fire when the
|
|
16
|
+
* terminal is definitely unfocused OR when focus state was never observed.
|
|
17
|
+
*/
|
|
18
|
+
export class FocusTracker {
|
|
19
|
+
private focused: boolean | null = null;
|
|
20
|
+
|
|
21
|
+
/** True/false once at least one focus token has arrived; null if none ever has. */
|
|
22
|
+
isFocused(): boolean | null {
|
|
23
|
+
return this.focused;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Called from the input pipeline on every focus token. */
|
|
27
|
+
setFocused(value: boolean): void {
|
|
28
|
+
this.focused = value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The alert-gating rule shared by every unfocused-alert notifier: fire
|
|
33
|
+
* when the terminal is known to be unfocused, or when focus was never
|
|
34
|
+
* observed (honest fallback — never silently suppress on an unknown
|
|
35
|
+
* terminal). Only `isFocused() === true` (a real, observed focus-in with
|
|
36
|
+
* no observed focus-out since) suppresses an alert.
|
|
37
|
+
*/
|
|
38
|
+
shouldAlertWhenUnfocused(): boolean {
|
|
39
|
+
return this.focused !== true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -18,15 +18,21 @@
|
|
|
18
18
|
* When neither target is available the function is an honest no-op (debug log
|
|
19
19
|
* only; no user-facing error spam).
|
|
20
20
|
*
|
|
21
|
-
* Focus tracking:
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* Focus tracking (W2.3): when `focusTracker` is supplied, notifications are
|
|
22
|
+
* gated the same way as the other unfocused-user alert classes (see
|
|
23
|
+
* alert-gating.ts) — they fire when the terminal is unfocused or focus state
|
|
24
|
+
* was never observed, and are suppressed when it's known to be focused,
|
|
25
|
+
* unless `behavior.notifyOnlyWhenUnfocused` is turned off. `focusTracker` is
|
|
26
|
+
* optional and defaults to "always fire" (the pre-W2.3 behavior) when
|
|
27
|
+
* omitted, so existing callers that don't have a FocusTracker in scope are
|
|
28
|
+
* unaffected.
|
|
25
29
|
*/
|
|
26
30
|
|
|
27
31
|
import { notifyCompletion } from '@pellux/goodvibes-sdk/platform/utils';
|
|
28
32
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
29
33
|
import type { WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
|
|
34
|
+
import type { FocusTracker } from './focus-tracker.ts';
|
|
35
|
+
import { readNotifyOnlyWhenUnfocused, type ConfigGet } from './alert-gating.ts';
|
|
30
36
|
|
|
31
37
|
/** Default threshold in seconds. Turns shorter than this do not notify. */
|
|
32
38
|
export const NOTIFY_AFTER_SECONDS_DEFAULT = 60;
|
|
@@ -74,6 +80,18 @@ export interface MaybeNotifyLongTaskOptions {
|
|
|
74
80
|
* skipped silently.
|
|
75
81
|
*/
|
|
76
82
|
readonly webhookNotifier?: WebhookNotifier | null;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Terminal focus tracker (W2.3). When supplied together with `configGet`,
|
|
86
|
+
* the notification additionally respects behavior.notifyOnlyWhenUnfocused
|
|
87
|
+
* (default on): suppressed when the terminal is known to be focused, fires
|
|
88
|
+
* when unfocused or when focus was never observed. Omit both to preserve
|
|
89
|
+
* pre-W2.3 behavior (always fire once the threshold is met).
|
|
90
|
+
*/
|
|
91
|
+
readonly focusTracker?: Pick<FocusTracker, 'shouldAlertWhenUnfocused'> | null;
|
|
92
|
+
|
|
93
|
+
/** Config reader used only for the notifyOnlyWhenUnfocused gate above. */
|
|
94
|
+
readonly configGet?: ConfigGet;
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
/**
|
|
@@ -81,13 +99,14 @@ export interface MaybeNotifyLongTaskOptions {
|
|
|
81
99
|
* exceeds the configured threshold.
|
|
82
100
|
*
|
|
83
101
|
* Returns true when at least one delivery was attempted, false when the
|
|
84
|
-
* call was a no-op (threshold not reached, or off
|
|
102
|
+
* call was a no-op (threshold not reached, off-state, or focus-gated off —
|
|
103
|
+
* see `focusTracker`/`configGet` above).
|
|
85
104
|
*
|
|
86
105
|
* PRIVACY: builds message text from structural metadata only (kind, elapsed,
|
|
87
106
|
* status, sessionId). Never includes conversation content.
|
|
88
107
|
*/
|
|
89
108
|
export function maybeNotifyLongTask(opts: MaybeNotifyLongTaskOptions): boolean {
|
|
90
|
-
const { elapsedMs, status, kind, sessionId, thresholdSeconds, webhookNotifier } = opts;
|
|
109
|
+
const { elapsedMs, status, kind, sessionId, thresholdSeconds, webhookNotifier, focusTracker, configGet } = opts;
|
|
91
110
|
|
|
92
111
|
// Off-state: 0 disables notifications entirely.
|
|
93
112
|
if (thresholdSeconds === NOTIFY_AFTER_SECONDS_OFF) {
|
|
@@ -102,6 +121,14 @@ export function maybeNotifyLongTask(opts: MaybeNotifyLongTaskOptions): boolean {
|
|
|
102
121
|
return false;
|
|
103
122
|
}
|
|
104
123
|
|
|
124
|
+
// Focus gate (W2.3): only applied when both a tracker and a config reader
|
|
125
|
+
// are supplied. Absent either one, behavior is unchanged from pre-W2.3
|
|
126
|
+
// (always fire once the threshold is met).
|
|
127
|
+
if (focusTracker && configGet && readNotifyOnlyWhenUnfocused(configGet) && !focusTracker.shouldAlertWhenUnfocused()) {
|
|
128
|
+
logger.debug('long-task-notifier: suppressed — terminal focused');
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
105
132
|
// Build concise, metadata-only message. No conversation text.
|
|
106
133
|
const statusLabel = status === 'ok' ? 'completed' : 'failed';
|
|
107
134
|
const title = `GoodVibes — ${kind} ${statusLabel}`;
|
|
@@ -155,6 +155,20 @@ export interface WireStreamEventMetricsOptions {
|
|
|
155
155
|
* unavailable for either model, the notice honestly states "cost data unavailable".
|
|
156
156
|
*/
|
|
157
157
|
readonly costLookup?: FailoverCostLookup;
|
|
158
|
+
/**
|
|
159
|
+
* Optional accessor for whether an approval card is currently waiting on the USER. When it
|
|
160
|
+
* returns true, the stall watchdog does NOT emit its "Still waiting on <provider>… Ctrl+C to
|
|
161
|
+
* cancel" hint: the stream is legitimately silent because we asked the user a question, so
|
|
162
|
+
* blaming the provider (or framing it as a stall) would be dishonest. The approval surface owns
|
|
163
|
+
* the honest "Waiting for your approval" label instead.
|
|
164
|
+
*/
|
|
165
|
+
readonly isApprovalPending?: (() => boolean) | undefined;
|
|
166
|
+
/**
|
|
167
|
+
* Stall watchdog threshold in ms. Defaults to the watchdog's own default (STALL_THRESHOLD_MS,
|
|
168
|
+
* 30 000). Exposed only so unit tests can drive the stall path without waiting 30s — production
|
|
169
|
+
* callers omit it.
|
|
170
|
+
*/
|
|
171
|
+
readonly stallThresholdMs?: number | undefined;
|
|
158
172
|
}
|
|
159
173
|
|
|
160
174
|
/** Result of wireStreamEventMetrics. */
|
|
@@ -237,6 +251,7 @@ export function wireStreamEventMetrics(
|
|
|
237
251
|
const {
|
|
238
252
|
events, metrics, orchestrator, providerRegistry,
|
|
239
253
|
systemMessageRouter, render, providerOptimizer, retryTurn, costLookup,
|
|
254
|
+
isApprovalPending, stallThresholdMs,
|
|
240
255
|
} = options;
|
|
241
256
|
|
|
242
257
|
const unsubs: Array<() => void> = [];
|
|
@@ -364,12 +379,16 @@ export function wireStreamEventMetrics(
|
|
|
364
379
|
const stallWatchdog = createStreamStallWatchdog({
|
|
365
380
|
events: events.turns,
|
|
366
381
|
onStall: (providerName, episode) => {
|
|
382
|
+
// Suppress the provider-blaming stall hint while an approval card is waiting on the user: the
|
|
383
|
+
// stream is silent because WE asked the user a question, not because the provider stalled.
|
|
384
|
+
if (isApprovalPending?.()) return;
|
|
367
385
|
metrics.stallEpisode = episode;
|
|
368
386
|
systemMessageRouter.low(`Still waiting on ${providerName}… Ctrl+C to cancel`);
|
|
369
387
|
render();
|
|
370
388
|
},
|
|
371
389
|
getProviderName: () => providerRegistry.getCurrentModel().provider,
|
|
372
|
-
// thresholdMs
|
|
390
|
+
// thresholdMs defaults to 30 000 in the watchdog; only unit tests override it.
|
|
391
|
+
...(stallThresholdMs !== undefined ? { thresholdMs: stallThresholdMs } : {}),
|
|
373
392
|
});
|
|
374
393
|
unsubs.push(() => stallWatchdog.dispose());
|
|
375
394
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System-message noise policy — decides whether an operational status message
|
|
3
|
+
* should reach the transcript unchanged, be dropped, or be folded. Every noisy
|
|
4
|
+
* source these rules catch is emitted (from the SDK) through
|
|
5
|
+
* SystemMessageRouter, so this one funnel is where first-run plumbing is kept
|
|
6
|
+
* out of the user's stream while the information stays reachable elsewhere
|
|
7
|
+
* (fleet panel, /health, /model, activity log). (UX-B item 1.)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface NoiseGateDeps {
|
|
11
|
+
/**
|
|
12
|
+
* True when a WRFC chain is terminal (passed/failed) or gone (killed/removed).
|
|
13
|
+
* Used to suppress stale "[Replay] … transitioned … waiting for action"
|
|
14
|
+
* re-notifications for chains that can no longer act. Absent → never suppress.
|
|
15
|
+
*/
|
|
16
|
+
readonly isChainTerminal?: (chainId: string) => boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type NoiseVerdict =
|
|
20
|
+
/** Pass through to the transcript unchanged. */
|
|
21
|
+
| { readonly action: 'emit' }
|
|
22
|
+
/** Suppress entirely — reachable via another live surface. */
|
|
23
|
+
| { readonly action: 'drop' }
|
|
24
|
+
/** Provider "from last session" replay line — buffer and fold to one line. */
|
|
25
|
+
| { readonly action: 'foldProviderReplay' };
|
|
26
|
+
|
|
27
|
+
const EMIT: NoiseVerdict = { action: 'emit' };
|
|
28
|
+
const DROP: NoiseVerdict = { action: 'drop' };
|
|
29
|
+
const FOLD: NoiseVerdict = { action: 'foldProviderReplay' };
|
|
30
|
+
|
|
31
|
+
/** Matches the SDK's per-provider persisted-replay line (1b). */
|
|
32
|
+
const PROVIDER_REPLAY_RE = / — from last session$/;
|
|
33
|
+
|
|
34
|
+
/** Matches the SDK's 30s periodic running-agents snapshot (1d). */
|
|
35
|
+
const AGENTS_RUNNING_SNAPSHOT_RE = /^\[Agents\] \d+ running:/;
|
|
36
|
+
|
|
37
|
+
/** Extracts the chain id from a "[Replay]… WRFC chain <id> transitioned …" line (1c). */
|
|
38
|
+
const REPLAY_CHAIN_RE = /WRFC chain (\S+) transitioned .* waiting for action/;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Classify one system message. Pure aside from the injected `isChainTerminal`
|
|
42
|
+
* lookup, so it is trivially testable.
|
|
43
|
+
*/
|
|
44
|
+
export function classifyNoise(message: string, deps: NoiseGateDeps): NoiseVerdict {
|
|
45
|
+
// 1b — provider-discovery replay burst ("[Local] … — from last session").
|
|
46
|
+
if (message.startsWith('[Local]') && PROVIDER_REPLAY_RE.test(message)) {
|
|
47
|
+
return FOLD;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 1d — periodic "[Agents] N running:" status snapshot. The same live detail
|
|
51
|
+
// is shown in the fleet panel (per-agent activity) and the footer count, so
|
|
52
|
+
// the 30-second transcript churn is dropped. Meaningful lifecycle lines
|
|
53
|
+
// ("[Agents] ✓ …", "[Agents] Cohort …", "[Agents] ✗ …") do not match.
|
|
54
|
+
if (AGENTS_RUNNING_SNAPSHOT_RE.test(message)) {
|
|
55
|
+
return DROP;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 1c — stale "[Replay] … waiting for action" for a terminal/killed chain.
|
|
59
|
+
if (message.startsWith('[Replay]') && deps.isChainTerminal) {
|
|
60
|
+
const match = REPLAY_CHAIN_RE.exec(message);
|
|
61
|
+
if (match && deps.isChainTerminal(match[1])) return DROP;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return EMIT;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Provider name from a "[Local] <name> at host:port (…) — from last session" line. */
|
|
68
|
+
export function providerNameFromReplay(message: string): string {
|
|
69
|
+
const match = /^\[Local\]\s+(.+?)\s+at\s/.exec(message);
|
|
70
|
+
if (match) return match[1];
|
|
71
|
+
// Fallback: first token after the tag.
|
|
72
|
+
const bare = /^\[Local\]\s+(\S+)/.exec(message);
|
|
73
|
+
return bare ? bare[1] : 'provider';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build the single folded summary line for a burst of provider-replay lines.
|
|
78
|
+
* Names a few providers; the full list stays reachable via /health and /model.
|
|
79
|
+
*/
|
|
80
|
+
export function foldProviderReplayLines(messages: readonly string[]): string {
|
|
81
|
+
const names = messages.map(providerNameFromReplay);
|
|
82
|
+
const count = names.length;
|
|
83
|
+
const noun = count === 1 ? 'provider' : 'providers';
|
|
84
|
+
const shown = names.slice(0, 3).join(', ');
|
|
85
|
+
const extra = count > 3 ? `, +${count - 3} more` : '';
|
|
86
|
+
return `[Local] Restored ${count} ${noun} from last session (${shown}${extra})`;
|
|
87
|
+
}
|