@pellux/goodvibes-tui 1.0.0 → 1.7.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 +130 -127
- package/README.md +32 -14
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
- package/src/cli/entrypoint.ts +2 -0
- 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/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +58 -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 +104 -2
- package/src/core/stream-stall-watchdog.ts +41 -17
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +126 -8
- package/src/input/command-registry.ts +54 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/diff-runtime.ts +61 -30
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- 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 +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- 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 +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- 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 +12 -0
- package/src/input/commands/session-workflow.ts +19 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/share-runtime.ts +9 -2
- package/src/input/commands/shell-core.ts +15 -7
- 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 +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-content-actions.ts +22 -8
- package/src/input/handler-feed-routes.ts +107 -7
- package/src/input/handler-feed.ts +49 -13
- package/src/input/handler-interactions.ts +3 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +3 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +10 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +41 -44
- package/src/panels/agent-inspector-shared.ts +35 -11
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +16 -62
- 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 +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +63 -14
- package/src/panels/diff-panel.ts +123 -60
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/git-panel.ts +9 -9
- package/src/panels/index.ts +7 -31
- package/src/panels/local-auth-panel.ts +10 -0
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -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 +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -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 +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/scrollable-list-panel.ts +9 -0
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/token-budget-panel.ts +5 -3
- package/src/panels/types.ts +26 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- 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/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/turn-injection.ts +114 -0
- package/src/renderer/ui-factory.ts +91 -7
- package/src/runtime/bootstrap-command-context.ts +24 -1
- package/src/runtime/bootstrap-command-parts.ts +36 -7
- package/src/runtime/bootstrap-core.ts +19 -4
- package/src/runtime/bootstrap-hook-bridge.ts +5 -0
- package/src/runtime/bootstrap-shell.ts +41 -17
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- 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 -233
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -609
- package/src/panels/docs-panel.ts +0 -375
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -664
- 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 -506
- 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 -557
- 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 -721
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -306
- package/src/panels/provider-health-views.ts +0 -560
- 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 -487
- 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 -530
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -465
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -656
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
} from '../command-registry.ts';
|
|
10
10
|
import { compactSmallWindow, getLastCompactionEvent, SMALL_WINDOW_THRESHOLD } from '@pellux/goodvibes-sdk/platform/core';
|
|
11
11
|
import type { CompactionContext, CompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
|
|
12
|
+
import { recordCompactionQualityScore, scoreCompactionRun } from '../../renderer/compaction-quality.ts';
|
|
13
|
+
import type { CompactionQualityScore } from '../../renderer/compaction-quality.ts';
|
|
12
14
|
import type { UiReadModels } from '../../runtime/ui-read-models.ts';
|
|
13
15
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
14
16
|
import type { EcosystemCatalogPathOptions } from '@/runtime/index.ts';
|
|
@@ -101,6 +103,21 @@ export function openCommandPanel(
|
|
|
101
103
|
showPanel(panelId, pane);
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
/**
|
|
107
|
+
* W6.1 (group-B migration): open a config-modal surface by name via the
|
|
108
|
+
* ctx.openModal seam (ui-openers wires it; the WO-A host replaces the interim
|
|
109
|
+
* "not available yet" implementation with real dispatch). Front-doors for
|
|
110
|
+
* panels that migrated to modals call this instead of openCommandPanel — the
|
|
111
|
+
* modal is the surface's new home. Stubbed in tests by setting ctx.openModal.
|
|
112
|
+
*/
|
|
113
|
+
export function openModalCommand(
|
|
114
|
+
context: Pick<CommandContext, 'openModal'>,
|
|
115
|
+
modalName: string,
|
|
116
|
+
): void {
|
|
117
|
+
const openModal = requireContextValue(context.openModal, 'openModal');
|
|
118
|
+
openModal(modalName);
|
|
119
|
+
}
|
|
120
|
+
|
|
104
121
|
export function openOnboardingWizard(
|
|
105
122
|
context: Pick<CommandContext, 'openOnboardingWizard'>,
|
|
106
123
|
modeOrOptions?: import('../onboarding/onboarding-wizard.ts').OnboardingWizardMode
|
|
@@ -238,12 +255,19 @@ export function requireProviderApi(context: CommandContext): ProviderApi {
|
|
|
238
255
|
return requireContextValue(context.clients?.providerApi, 'clients.providerApi');
|
|
239
256
|
}
|
|
240
257
|
|
|
258
|
+
/** Result of a manual /compact run: the SDK's CompactionEvent, plus an
|
|
259
|
+
* out-of-band quality score (W5.4/B28) when one could be computed. */
|
|
260
|
+
export interface CompactionRunResult {
|
|
261
|
+
readonly event: CompactionEvent;
|
|
262
|
+
readonly qualityScore: CompactionQualityScore | null;
|
|
263
|
+
}
|
|
264
|
+
|
|
241
265
|
/**
|
|
242
|
-
* Compact the conversation and return the CompactionEvent recorded by the SDK
|
|
243
|
-
* or null if no event was recorded (e.g.
|
|
244
|
-
* change).
|
|
266
|
+
* Compact the conversation and return the CompactionEvent recorded by the SDK
|
|
267
|
+
* (plus an out-of-band quality score), or null if no event was recorded (e.g.
|
|
268
|
+
* compaction was skipped or produced no change).
|
|
245
269
|
*/
|
|
246
|
-
export async function compactConversation(context: CommandContext): Promise<
|
|
270
|
+
export async function compactConversation(context: CommandContext): Promise<CompactionRunResult | null> {
|
|
247
271
|
const conversationManager = context.session.conversationManager;
|
|
248
272
|
const providerRegistry = context.provider.providerRegistry;
|
|
249
273
|
// Resolve the live context window for the current model so manual /compact
|
|
@@ -263,20 +287,20 @@ export async function compactConversation(context: CommandContext): Promise<Comp
|
|
|
263
287
|
}
|
|
264
288
|
|
|
265
289
|
const eventBefore = getLastCompactionEvent();
|
|
290
|
+
const messagesBefore = conversationManager.getMessagesForLLM();
|
|
266
291
|
const sessionMemories = context.session.sessionMemoryStore?.list() ?? [];
|
|
267
292
|
const lineageTracker = context.session.sessionLineageTracker;
|
|
268
293
|
const agentManager = context.ops.agentManager;
|
|
269
294
|
const planManager = context.ops.planManager;
|
|
270
295
|
const compactionCtx: CompactionContext = {
|
|
271
|
-
messages:
|
|
296
|
+
messages: messagesBefore,
|
|
272
297
|
sessionMemories,
|
|
273
|
-
// Mirror buildAutoCompactionContext: pass
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
.filter((agent) => agent.status === 'running' || agent.status === 'pending') ?? [],
|
|
298
|
+
// Mirror buildAutoCompactionContext: pass ALL agent records (not just
|
|
299
|
+
// running/pending) so completed/failed subagent work reaches the
|
|
300
|
+
// "Completed Agent Work" / "Currently Running" compaction sections, plus
|
|
301
|
+
// the active execution plan and the session-lineage log so the handoff
|
|
302
|
+
// summary and lineage section are populated correctly (not "compaction #0").
|
|
303
|
+
agents: agentManager?.exportState() ?? [],
|
|
280
304
|
// Mirror buildAutoCompactionContext: include the live WRFC chains (wired
|
|
281
305
|
// through context.session.wrfcController) so the handoff summary's
|
|
282
306
|
// orchestration section matches the SDK auto-compaction path.
|
|
@@ -284,6 +308,11 @@ export async function compactConversation(context: CommandContext): Promise<Comp
|
|
|
284
308
|
activePlan: planManager?.getActive(context.session.runtime.sessionId) ?? null,
|
|
285
309
|
lineageEntries: lineageTracker?.getEntries() ?? [],
|
|
286
310
|
compactionCount: lineageTracker?.getCompactionCount() ?? 0,
|
|
311
|
+
// Mirror buildAutoCompactionContext: pass the recorded original task so
|
|
312
|
+
// the "Original task" lineage line reflects the session's actual first
|
|
313
|
+
// task instead of falling back to the current message on every manual
|
|
314
|
+
// compaction after the first.
|
|
315
|
+
originalTask: lineageTracker?.getOriginalTask() ?? undefined,
|
|
287
316
|
contextWindow,
|
|
288
317
|
trigger: 'manual',
|
|
289
318
|
extractionModelId: context.session.runtime.model,
|
|
@@ -299,7 +328,19 @@ export async function compactConversation(context: CommandContext): Promise<Comp
|
|
|
299
328
|
const eventAfter = getLastCompactionEvent();
|
|
300
329
|
// Return the new event only if it differs from the one recorded before the call.
|
|
301
330
|
if (eventAfter !== null && eventAfter !== eventBefore) {
|
|
302
|
-
|
|
331
|
+
// W5.4/B28: score this run out-of-band (see compaction-quality.ts for why
|
|
332
|
+
// this can't just subscribe to the SDK's own CompactionManager pipeline)
|
|
333
|
+
// and keep the score keyed by this event's timestamp for /compact-history.
|
|
334
|
+
const qualityScore = scoreCompactionRun({
|
|
335
|
+
sessionId: context.session.runtime.sessionId,
|
|
336
|
+
contextWindow,
|
|
337
|
+
messagesBefore,
|
|
338
|
+
messagesAfter: conversationManager.getMessagesForLLM(),
|
|
339
|
+
tokensBefore: eventAfter.tokensBeforeEstimate,
|
|
340
|
+
tokensAfter: eventAfter.tokensAfterEstimate,
|
|
341
|
+
});
|
|
342
|
+
recordCompactionQualityScore(eventAfter.timestamp, qualityScore);
|
|
343
|
+
return { event: eventAfter, qualityScore };
|
|
303
344
|
}
|
|
304
345
|
return null;
|
|
305
346
|
}
|
|
@@ -16,7 +16,7 @@ export function registerServicesRuntimeCommands(registry: CommandRegistry): void
|
|
|
16
16
|
const sub = args[0] ?? 'open';
|
|
17
17
|
const shellPaths = requireShellPaths(ctx);
|
|
18
18
|
if (sub === 'open' || sub === 'panel') {
|
|
19
|
-
|
|
19
|
+
ctx.openModal?.('services-modal'); // W6.1: services panel -> config modal
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
const svcRegistry = requireServiceRegistry(ctx);
|
|
@@ -7,6 +7,7 @@ import { exportToMarkdown } from '@pellux/goodvibes-sdk/platform/export';
|
|
|
7
7
|
import { TemplateManager, parseTemplateArgs } from '@pellux/goodvibes-sdk/platform/templates';
|
|
8
8
|
import { requireSessionManager, requireSessionMemoryStore, requireShellPaths } from './runtime-services.ts';
|
|
9
9
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
10
|
+
import { sessionCommand } from './session.ts';
|
|
10
11
|
|
|
11
12
|
export function registerSessionContentCommands(registry: CommandRegistry): void {
|
|
12
13
|
registry.register({
|
|
@@ -238,7 +239,18 @@ export function registerSessionContentCommands(registry: CommandRegistry): void
|
|
|
238
239
|
registry.register({
|
|
239
240
|
name: 'sessions',
|
|
240
241
|
description: 'List saved sessions',
|
|
242
|
+
usage: '[resume <id|name>]',
|
|
243
|
+
argsHint: '[resume <id|name>]',
|
|
241
244
|
async handler(_args, ctx) {
|
|
245
|
+
// Splash's "resume last session" hint and users' muscle memory both say
|
|
246
|
+
// `/sessions resume <id>` (plural, matching this command's own name) —
|
|
247
|
+
// but subcommands like `resume` are only implemented on the singular
|
|
248
|
+
// `/session` command. Forward instead of silently listing and dropping
|
|
249
|
+
// the subcommand+id on the floor.
|
|
250
|
+
if (_args.length > 0) {
|
|
251
|
+
await sessionCommand.handler(_args, ctx);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
242
254
|
const sessionManager = requireSessionManager(ctx);
|
|
243
255
|
const sessions = sessionManager.list();
|
|
244
256
|
if (ctx.openSelection) {
|
|
@@ -79,11 +79,23 @@ function buildTranscriptReviewLines(
|
|
|
79
79
|
];
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
// Exported (test-only concern) so the MIGRATE-TO-MODAL redirect-skip honesty
|
|
83
|
+
// (W6 review, finding 3: saved-layout restore with 'sessions' must not lie)
|
|
84
|
+
// can be unit-tested directly instead of through a full /resume harness.
|
|
85
|
+
export function reopenPanelsFromReturnContext(ctx: CommandContext, summary: SessionReturnContextSummary | undefined): string[] {
|
|
83
86
|
if (!summary?.openPanels || summary.openPanels.length === 0) return [];
|
|
84
87
|
const panelManager = requirePanelManager(ctx);
|
|
85
88
|
const reopened: string[] = [];
|
|
89
|
+
const movedToModal: string[] = [];
|
|
86
90
|
for (const panelId of summary.openPanels.slice(0, 4)) {
|
|
91
|
+
// W6.1 (the purge): a MIGRATE-TO-MODAL id has no panel to restore — a modal
|
|
92
|
+
// is not part of the saved panel layout. Skip it (don't pop a modal
|
|
93
|
+
// mid-resume) and note it once, rather than firing openModal + revealing an
|
|
94
|
+
// empty workspace during resume.
|
|
95
|
+
if (panelManager.getModalRedirect(panelId) !== undefined) {
|
|
96
|
+
movedToModal.push(panelId);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
87
99
|
try {
|
|
88
100
|
panelManager.open(panelId);
|
|
89
101
|
reopened.push(panelId);
|
|
@@ -92,6 +104,9 @@ function reopenPanelsFromReturnContext(ctx: CommandContext, summary: SessionRetu
|
|
|
92
104
|
}
|
|
93
105
|
}
|
|
94
106
|
if (reopened.length > 0) panelManager.show();
|
|
107
|
+
if (movedToModal.length > 0) {
|
|
108
|
+
ctx.print(`Note: ${movedToModal.join(', ')} moved to a modal — reopen via its command instead of as a panel.`);
|
|
109
|
+
}
|
|
95
110
|
return reopened;
|
|
96
111
|
}
|
|
97
112
|
|
|
@@ -260,6 +275,9 @@ export async function handleSessionWorkflowCommand(args: string[], ctx: CommandC
|
|
|
260
275
|
});
|
|
261
276
|
},
|
|
262
277
|
});
|
|
278
|
+
// Hydrate the footer's token counters from the resumed (+ journal-replayed)
|
|
279
|
+
// history now, before ctx.renderRequest() below (W0.9).
|
|
280
|
+
ctx.session.hydrateSessionUsage?.();
|
|
263
281
|
|
|
264
282
|
if (meta.model) {
|
|
265
283
|
try {
|
|
@@ -33,7 +33,7 @@ export function registerSettingsSyncRuntimeCommands(registry: CommandRegistry):
|
|
|
33
33
|
const controlPlaneConfigDir = ctx.platform.configManager.getControlPlaneConfigDir();
|
|
34
34
|
const sub = (args[0] ?? 'review').toLowerCase();
|
|
35
35
|
if (sub === 'panel' || sub === 'open') {
|
|
36
|
-
|
|
36
|
+
ctx.openModal?.('settings-sync-modal'); // W6.1: settings-sync panel -> config modal
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
39
|
if (sub === 'show') {
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
11
11
|
import { requireShellPaths } from './runtime-services.ts';
|
|
12
12
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
13
|
-
import { calcSessionCost } from '../../export/cost-utils.ts';
|
|
13
|
+
import { calcSessionCost, isModelPriced } from '../../export/cost-utils.ts';
|
|
14
14
|
import {
|
|
15
15
|
GistUploadTarget,
|
|
16
16
|
NO_TOKEN_GUIDANCE,
|
|
@@ -113,6 +113,13 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
|
113
113
|
const sessionCostUsd = calcSessionCost(
|
|
114
114
|
totalInput, totalOutput, totalCacheRead, totalCacheWrite, sessionModel,
|
|
115
115
|
);
|
|
116
|
+
// The exported document has no field for "this cost is a placeholder" —
|
|
117
|
+
// omit `cost` entirely rather than embed a misleading 0 when the model
|
|
118
|
+
// never resolved to a real price (WO-315).
|
|
119
|
+
const costIsUnpriced = !isModelPriced(sessionModel);
|
|
120
|
+
if (costIsUnpriced) {
|
|
121
|
+
ctx.print(`Note: cost omitted from export — no pricing data for model "${sessionModel}".`);
|
|
122
|
+
}
|
|
116
123
|
|
|
117
124
|
const metadata = {
|
|
118
125
|
model: sessionModel,
|
|
@@ -120,7 +127,7 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
|
120
127
|
sessionId: ctx.session.runtime.sessionId,
|
|
121
128
|
title: ctx.session.conversationManager.title || undefined,
|
|
122
129
|
};
|
|
123
|
-
const options = { redact, cost: sessionCostUsd };
|
|
130
|
+
const options = { redact, cost: costIsUnpriced ? undefined : sessionCostUsd };
|
|
124
131
|
|
|
125
132
|
let outputContent: string;
|
|
126
133
|
try {
|
|
@@ -153,6 +153,8 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
153
153
|
{ id: '/template save', label: '/template save <name>', detail: 'Save prompt as template', category: 'Templates' },
|
|
154
154
|
{ id: '/template use', label: '/template use <name>', detail: 'Execute template', category: 'Templates' },
|
|
155
155
|
{ id: '/tools', label: '/tools', detail: 'List available tools', category: 'Tools & System' },
|
|
156
|
+
{ id: '/search', label: '/search <query> [--limit <n>]', detail: 'Search the web and show ranked results', category: 'Tools & System' },
|
|
157
|
+
{ id: '/imagine', label: '/imagine <prompt>', detail: 'Generate an image from a prompt', category: 'Tools & System' },
|
|
156
158
|
{ id: '/paste', label: '/paste', detail: 'Insert clipboard text or image into the prompt', category: 'Tools & System' },
|
|
157
159
|
{ id: '/shortcuts', label: '/shortcuts', detail: 'View keyboard shortcuts reference', category: 'Tools & System' },
|
|
158
160
|
{ id: '/commands', label: '/commands', detail: 'Browse all commands in a scrollable list', category: 'Tools & System' },
|
|
@@ -207,18 +209,24 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
207
209
|
description: 'Summarize conversation to free context window',
|
|
208
210
|
async handler(_args, ctx) {
|
|
209
211
|
const messages = ctx.session.conversationManager.getMessagesForLLM();
|
|
210
|
-
// contextWindow
|
|
211
|
-
//
|
|
212
|
-
|
|
212
|
+
// W5.4 (wo803): contextWindow IS reachable from CommandContext — the same
|
|
213
|
+
// call compactConversation() (runtime-services.ts) already makes to scale
|
|
214
|
+
// its own behaviour by window size. Previously hardcoded to 0 here, which
|
|
215
|
+
// silently suppressed buildCompactionPreview()'s capacity-% clause even
|
|
216
|
+
// though the builder has always supported it.
|
|
217
|
+
const contextWindow = ctx.provider.providerRegistry.getContextWindowForModel(
|
|
218
|
+
ctx.provider.providerRegistry.getCurrentModel(),
|
|
219
|
+
);
|
|
213
220
|
const memStore = ctx.session.sessionMemoryStore;
|
|
214
221
|
const pinnedMemoryCount = memStore ? memStore.list().length : 0;
|
|
215
222
|
// Pre-compact preview: honest estimate, clearly labelled.
|
|
216
223
|
const preview = buildCompactionPreview({ messages, contextWindow, pinnedMemoryCount, trigger: 'manual' });
|
|
217
224
|
ctx.print(preview);
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
220
|
-
// Post-compact notice: uses real CompactionEvent figures
|
|
221
|
-
|
|
225
|
+
const result = await compactConversation(ctx);
|
|
226
|
+
if (result) {
|
|
227
|
+
// Post-compact notice: uses real CompactionEvent figures, plus an
|
|
228
|
+
// out-of-band quality-score grade line when one was computed (W5.4/B28).
|
|
229
|
+
ctx.print(buildCompactionAfterNotice({ event: result.event, pinnedMemoryCount, qualityScore: result.qualityScore }));
|
|
222
230
|
} else {
|
|
223
231
|
ctx.print('[Context] Compact complete.');
|
|
224
232
|
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
updateInstalledEcosystemEntry,
|
|
12
12
|
upsertEcosystemCatalogEntry,
|
|
13
13
|
} from '@/runtime/index.ts';
|
|
14
|
-
import {
|
|
14
|
+
import { openModalCommand, requireEcosystemCatalogPaths, requireShellPaths } from './runtime-services.ts';
|
|
15
15
|
|
|
16
16
|
export function registerSkillsRuntimeCommands(registry: CommandRegistry): void {
|
|
17
17
|
registry.register({
|
|
@@ -22,13 +22,7 @@ export function registerSkillsRuntimeCommands(registry: CommandRegistry): void {
|
|
|
22
22
|
async handler(args, ctx) {
|
|
23
23
|
const sub = args[0] ?? 'open';
|
|
24
24
|
if (sub === 'open' || sub === 'panel') {
|
|
25
|
-
|
|
26
|
-
else {
|
|
27
|
-
const panelManager = requirePanelManager(ctx);
|
|
28
|
-
panelManager.open('skills');
|
|
29
|
-
panelManager.show();
|
|
30
|
-
ctx.renderRequest();
|
|
31
|
-
}
|
|
25
|
+
openModalCommand(ctx, 'skills-modal');
|
|
32
26
|
return;
|
|
33
27
|
}
|
|
34
28
|
const skills = await discoverSkills(requireShellPaths(ctx));
|
|
@@ -79,8 +79,8 @@ export function registerSubscriptionRuntimeCommands(registry: CommandRegistry):
|
|
|
79
79
|
usage: '[review|list|providers|inspect <provider>|login <provider> start [--no-browser] [--manual]|finish <code-or-url>|logout <provider>|bundle export <path>|bundle inspect <path>]',
|
|
80
80
|
async handler(args, ctx) {
|
|
81
81
|
const shellPaths = requireShellPaths(ctx);
|
|
82
|
-
if (args.length === 0 && ctx.
|
|
83
|
-
ctx.
|
|
82
|
+
if (args.length === 0 && ctx.openModal) {
|
|
83
|
+
ctx.openModal('subscription-modal'); // W6.1: subscription panel -> config modal
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
const sub = (args[0] ?? 'review').toLowerCase();
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { loadPackageScripts, getSkippedGateReason } from '@pellux/goodvibes-sdk/platform/agents';
|
|
2
|
+
import type { ToolCall } from '@pellux/goodvibes-sdk/platform/types';
|
|
3
|
+
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
4
|
+
import { requireShellPaths } from './runtime-services.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Timeout for a /test run, in milliseconds. Deliberately much longer than
|
|
8
|
+
* WRFC's WRFC_GATE_TIMEOUT_MS (120s, tuned for CI gates) — this repo's own
|
|
9
|
+
* suite is 600+ test files (scripts/run-tests.ts), and /test is an
|
|
10
|
+
* interactive command with no other caller waiting on it.
|
|
11
|
+
*
|
|
12
|
+
* Exported (rather than a private const) so tests can pass a shorter override
|
|
13
|
+
* straight through to runTestCommand's optional third parameter instead of
|
|
14
|
+
* waiting out the real timeout.
|
|
15
|
+
*/
|
|
16
|
+
export const TEST_RUN_TIMEOUT_MS = 300_000;
|
|
17
|
+
|
|
18
|
+
/** How often buffered stdout/stderr lines are flushed to the transcript during
|
|
19
|
+
* a run. Batching avoids hammering the transcript.append_one perf budget
|
|
20
|
+
* (6ms, scripts/perf-baseline.json) with one ctx.print() call per output line. */
|
|
21
|
+
const STREAM_FLUSH_INTERVAL_MS = 250;
|
|
22
|
+
|
|
23
|
+
/** Cap on individually-listed failing test file names before truncating. */
|
|
24
|
+
const MAX_FAILING_NAMES_SHOWN = 20;
|
|
25
|
+
|
|
26
|
+
/** Lines of raw output shown when structured results could not be parsed. */
|
|
27
|
+
const RAW_TAIL_LINES = 30;
|
|
28
|
+
|
|
29
|
+
const FAIL_LINE_RE = /^==> (.+?)\s+\[FAIL\]$/gm;
|
|
30
|
+
const SUMMARY_LINE_RE = /^Test files: (\d+), passed: (\d+), failed: (\d+)$/m;
|
|
31
|
+
// Plain `bun test` output: " N pass" / " N fail" counters (tests, not files),
|
|
32
|
+
// "Ran N tests across M files.", and "(fail) suite > name" per failing test.
|
|
33
|
+
const BUN_PASS_RE = /^\s*(\d+) pass\s*$/m;
|
|
34
|
+
const BUN_FAIL_RE = /^\s*(\d+) fail\s*$/m;
|
|
35
|
+
const BUN_FILES_RE = /across (\d+) files?\./;
|
|
36
|
+
const BUN_FAIL_TEST_RE = /^\(fail\) (.+)$/gm;
|
|
37
|
+
|
|
38
|
+
interface ParsedTestResults {
|
|
39
|
+
totalFiles: number;
|
|
40
|
+
passed: number;
|
|
41
|
+
failed: number;
|
|
42
|
+
failingFiles: string[];
|
|
43
|
+
/** Whether passed/failed count files (goodvibes runner) or tests (bun test). */
|
|
44
|
+
unit: 'files' | 'tests';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse this repo's own scripts/run-tests.ts output shape:
|
|
49
|
+
* per-file: `==> path/to/file.test.ts [FAIL]` (only present on failure)
|
|
50
|
+
* summary: `Test files: N, passed: P, failed: F`
|
|
51
|
+
* Returns null when the summary line isn't present at all — the fallback
|
|
52
|
+
* path (raw tail, no fabricated counts) covers a different project's
|
|
53
|
+
* jest/vitest/pytest output, or any run that bypassed run-tests.ts.
|
|
54
|
+
*/
|
|
55
|
+
export function parseTestOutput(output: string): ParsedTestResults | null {
|
|
56
|
+
const summaryMatch = SUMMARY_LINE_RE.exec(output);
|
|
57
|
+
if (summaryMatch) {
|
|
58
|
+
const failingFiles: string[] = [];
|
|
59
|
+
FAIL_LINE_RE.lastIndex = 0;
|
|
60
|
+
let match: RegExpExecArray | null;
|
|
61
|
+
while ((match = FAIL_LINE_RE.exec(output)) !== null) {
|
|
62
|
+
failingFiles.push(match[1]!);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
totalFiles: Number(summaryMatch[1]),
|
|
66
|
+
passed: Number(summaryMatch[2]),
|
|
67
|
+
failed: Number(summaryMatch[3]),
|
|
68
|
+
failingFiles,
|
|
69
|
+
unit: 'files',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Plain `bun test`: counts are per-test, failing entries are test names.
|
|
73
|
+
const bunPass = BUN_PASS_RE.exec(output);
|
|
74
|
+
const bunFail = BUN_FAIL_RE.exec(output);
|
|
75
|
+
if (bunPass && bunFail) {
|
|
76
|
+
const failingTests: string[] = [];
|
|
77
|
+
BUN_FAIL_TEST_RE.lastIndex = 0;
|
|
78
|
+
let match: RegExpExecArray | null;
|
|
79
|
+
while ((match = BUN_FAIL_TEST_RE.exec(output)) !== null) {
|
|
80
|
+
failingTests.push(match[1]!);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
totalFiles: Number(BUN_FILES_RE.exec(output)?.[1] ?? 0),
|
|
84
|
+
passed: Number(bunPass[1]),
|
|
85
|
+
failed: Number(bunFail[1]),
|
|
86
|
+
failingFiles: failingTests,
|
|
87
|
+
unit: 'tests',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Single-quote shell-escape a value for safe interpolation into a
|
|
95
|
+
* `/bin/sh -c` string: wrap in single quotes, turning any embedded single
|
|
96
|
+
* quote into `'\''` (close quote, escaped literal quote, reopen quote).
|
|
97
|
+
* Mirrors the injection-risk class already present in the SDK's
|
|
98
|
+
* wrfc-gates.ts (executeGateCommand) and this repo's git-runtime.ts /
|
|
99
|
+
* diff-runtime.ts, which build shell command strings the same way — a
|
|
100
|
+
* pattern arg is never string-concatenated into the command raw.
|
|
101
|
+
*/
|
|
102
|
+
export function shQuote(value: string): string {
|
|
103
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Reads a ReadableStream<Uint8Array> incrementally, decoding and forwarding
|
|
107
|
+
* each chunk of text as it arrives (display-only accumulation happens in the
|
|
108
|
+
* caller; this just pumps the stream to completion). */
|
|
109
|
+
async function pumpStream(
|
|
110
|
+
stream: ReadableStream<Uint8Array> | undefined,
|
|
111
|
+
onChunk: (text: string) => void,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
if (!stream) return;
|
|
114
|
+
const decoder = new TextDecoder();
|
|
115
|
+
for await (const chunk of stream) {
|
|
116
|
+
onChunk(decoder.decode(chunk, { stream: true }));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function registerTestRuntimeCommands(registry: CommandRegistry): void {
|
|
121
|
+
registry.register({
|
|
122
|
+
name: 'test',
|
|
123
|
+
aliases: [],
|
|
124
|
+
description: 'Run the project test script and show pass/fail results',
|
|
125
|
+
usage: '[pattern]',
|
|
126
|
+
argsHint: '[pattern]',
|
|
127
|
+
async handler(args, ctx) {
|
|
128
|
+
await runTestCommand(args, ctx);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Core /test implementation, factored out of the registered handler so tests
|
|
135
|
+
* can call it directly with a shortened `timeoutMs` override (the timeout
|
|
136
|
+
* path would otherwise take TEST_RUN_TIMEOUT_MS to exercise).
|
|
137
|
+
*/
|
|
138
|
+
export async function runTestCommand(
|
|
139
|
+
args: string[],
|
|
140
|
+
ctx: CommandContext,
|
|
141
|
+
timeoutMs: number = TEST_RUN_TIMEOUT_MS,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
const cwd = requireShellPaths(ctx).workingDirectory;
|
|
144
|
+
const pkgScripts = await loadPackageScripts(cwd);
|
|
145
|
+
const skip = getSkippedGateReason('test', cwd, pkgScripts);
|
|
146
|
+
if (skip) {
|
|
147
|
+
ctx.print(skip);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const pattern = args.length > 0 ? args.join(' ') : undefined;
|
|
152
|
+
const command = args.length > 0
|
|
153
|
+
? `${pkgScripts.test} ${args.map(shQuote).join(' ')}`
|
|
154
|
+
: pkgScripts.test!;
|
|
155
|
+
const toolCall: ToolCall = { id: 'test-run', name: 'test', arguments: pattern ? { pattern } : {} };
|
|
156
|
+
|
|
157
|
+
ctx.print(`Running: ${command}`);
|
|
158
|
+
|
|
159
|
+
const startedAt = Date.now();
|
|
160
|
+
const proc = Bun.spawn(['/bin/sh', '-c', command], { cwd, stdout: 'pipe', stderr: 'pipe' });
|
|
161
|
+
|
|
162
|
+
let timedOut = false;
|
|
163
|
+
const timer = setTimeout(() => {
|
|
164
|
+
timedOut = true;
|
|
165
|
+
proc.kill();
|
|
166
|
+
}, timeoutMs);
|
|
167
|
+
timer.unref?.();
|
|
168
|
+
|
|
169
|
+
let combinedOutput = '';
|
|
170
|
+
let pendingDisplay = '';
|
|
171
|
+
const appendChunk = (text: string): void => {
|
|
172
|
+
combinedOutput += text;
|
|
173
|
+
pendingDisplay += text;
|
|
174
|
+
};
|
|
175
|
+
const flushDisplay = (force = false): void => {
|
|
176
|
+
if (!pendingDisplay) return;
|
|
177
|
+
if (force) {
|
|
178
|
+
ctx.print(pendingDisplay);
|
|
179
|
+
pendingDisplay = '';
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const lastNewline = pendingDisplay.lastIndexOf('\n');
|
|
183
|
+
if (lastNewline === -1) return; // no complete line buffered yet
|
|
184
|
+
const toPrint = pendingDisplay.slice(0, lastNewline);
|
|
185
|
+
pendingDisplay = pendingDisplay.slice(lastNewline + 1);
|
|
186
|
+
if (toPrint) ctx.print(toPrint);
|
|
187
|
+
};
|
|
188
|
+
const flushTimer = setInterval(() => flushDisplay(false), STREAM_FLUSH_INTERVAL_MS);
|
|
189
|
+
|
|
190
|
+
let exitCode: number;
|
|
191
|
+
try {
|
|
192
|
+
[exitCode] = await Promise.all([
|
|
193
|
+
proc.exited,
|
|
194
|
+
pumpStream(proc.stdout, appendChunk),
|
|
195
|
+
pumpStream(proc.stderr, appendChunk),
|
|
196
|
+
]);
|
|
197
|
+
} finally {
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
clearInterval(flushTimer);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const durationMs = Date.now() - startedAt;
|
|
203
|
+
|
|
204
|
+
if (timedOut) {
|
|
205
|
+
flushDisplay(true);
|
|
206
|
+
const timeoutSeconds = Math.round(timeoutMs / 1000);
|
|
207
|
+
ctx.print(`Test run timed out after ${timeoutSeconds}s and was killed.`);
|
|
208
|
+
ctx.session.conversationManager.logToolResultBlock(
|
|
209
|
+
toolCall,
|
|
210
|
+
'error',
|
|
211
|
+
'timed out',
|
|
212
|
+
durationMs,
|
|
213
|
+
`timed out after ${timeoutSeconds}s`,
|
|
214
|
+
);
|
|
215
|
+
// Durable end state: logToolResultBlock (above) renders straight into the
|
|
216
|
+
// display-only history buffer and never touches the real message list, so
|
|
217
|
+
// it does not survive the next full rebuildHistory() (which rebuilds
|
|
218
|
+
// strictly from conversationManager.getMessageSnapshot()) — the very next
|
|
219
|
+
// dirty render silently wipes it back to how the transcript looked before
|
|
220
|
+
// /test ran. addSystemMessage persists the same content as a real message
|
|
221
|
+
// (same durable pattern as /rewind's confirm notice in
|
|
222
|
+
// checkpoint-runtime.ts) so the timeout outcome survives like any other
|
|
223
|
+
// command's output. The streamed output during the run is allowed to stay
|
|
224
|
+
// transient — only this final state needs to persist.
|
|
225
|
+
ctx.session.conversationManager.addSystemMessage(
|
|
226
|
+
`[Test] Timed out after ${timeoutSeconds}s and was killed.`,
|
|
227
|
+
);
|
|
228
|
+
ctx.renderRequest();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
flushDisplay(true);
|
|
233
|
+
|
|
234
|
+
const ok = exitCode === 0;
|
|
235
|
+
const parsed = parseTestOutput(combinedOutput);
|
|
236
|
+
|
|
237
|
+
if (parsed) {
|
|
238
|
+
const summary = parsed.unit === 'files'
|
|
239
|
+
? `${parsed.passed}/${parsed.totalFiles} files passed`
|
|
240
|
+
: `${parsed.passed} passed, ${parsed.failed} failed${parsed.totalFiles ? ` across ${parsed.totalFiles} file${parsed.totalFiles === 1 ? '' : 's'}` : ''}`;
|
|
241
|
+
const failedNoun = parsed.unit === 'files' ? 'file' : 'test';
|
|
242
|
+
const errorMsg = ok ? undefined : `${parsed.failed} ${failedNoun}${parsed.failed === 1 ? '' : 's'} failed`;
|
|
243
|
+
ctx.session.conversationManager.logToolResultBlock(toolCall, ok ? 'done' : 'error', summary, durationMs, errorMsg);
|
|
244
|
+
const durableLines = [`[Test] ${summary}${errorMsg ? ` — ${errorMsg}` : ''}`];
|
|
245
|
+
if (parsed.failingFiles.length > 0) {
|
|
246
|
+
const shown = parsed.failingFiles.slice(0, MAX_FAILING_NAMES_SHOWN);
|
|
247
|
+
const failListHeader = parsed.unit === 'files' ? 'Failing test files:' : 'Failing tests:';
|
|
248
|
+
const lines = [failListHeader, ...shown.map((f) => ` - ${f}`)];
|
|
249
|
+
const remaining = parsed.failingFiles.length - shown.length;
|
|
250
|
+
if (remaining > 0) lines.push(` ...and ${remaining} more`);
|
|
251
|
+
ctx.print(lines.join('\n'));
|
|
252
|
+
durableLines.push(...lines);
|
|
253
|
+
}
|
|
254
|
+
// See the timeout branch's comment above: logToolResultBlock's tool-styled
|
|
255
|
+
// render is display-only and does not survive the next full history
|
|
256
|
+
// rebuild. addSystemMessage persists the pass/fail summary AND the
|
|
257
|
+
// failing-file detail as a real message so both remain in the transcript
|
|
258
|
+
// after the command completes, not just during the ~1s before the next
|
|
259
|
+
// render wipes the display-only copy.
|
|
260
|
+
ctx.session.conversationManager.addSystemMessage(durableLines.join('\n'));
|
|
261
|
+
} else {
|
|
262
|
+
ctx.session.conversationManager.logToolResultBlock(
|
|
263
|
+
toolCall,
|
|
264
|
+
ok ? 'done' : 'error',
|
|
265
|
+
`exit code ${exitCode}`,
|
|
266
|
+
durationMs,
|
|
267
|
+
ok ? undefined : `exit code ${exitCode}`,
|
|
268
|
+
);
|
|
269
|
+
const tail = combinedOutput.split('\n').slice(-RAW_TAIL_LINES).join('\n');
|
|
270
|
+
ctx.print(`(could not parse structured test results; showing raw tail)\n${tail}`);
|
|
271
|
+
ctx.session.conversationManager.addSystemMessage(
|
|
272
|
+
`[Test] exit code ${exitCode} (could not parse structured test results)`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
ctx.renderRequest();
|
|
277
|
+
}
|