@pellux/goodvibes-tui 1.1.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 +85 -21
- package/README.md +20 -11
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +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/conversation-rendering.ts +19 -0
- 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/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 +36 -0
- package/src/input/command-registry.ts +38 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- 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-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- 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-feed-routes.ts +61 -23
- package/src/input/handler-feed.ts +47 -23
- package/src/input/handler-interactions.ts +1 -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 +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +6 -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 +24 -27
- 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 +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 +36 -10
- package/src/panels/diff-panel.ts +12 -43
- 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/index.ts +7 -31
- 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/skills-panel.ts +7 -51
- package/src/panels/types.ts +13 -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/turn-injection.ts +114 -0
- package/src/runtime/bootstrap-command-context.ts +21 -1
- package/src/runtime/bootstrap-command-parts.ts +34 -7
- package/src/runtime/bootstrap-core.ts +14 -4
- package/src/runtime/bootstrap-shell.ts +29 -16
- 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 -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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction quality-score bridging (Wave-5 W5.4 / B28).
|
|
3
|
+
*
|
|
4
|
+
* Reality check (wo803): the SDK's `computeQualityScore()`
|
|
5
|
+
* (platform/runtime/compaction/quality-score.ts) is only ever called from a
|
|
6
|
+
* SEPARATE pipeline, `CompactionManager` (platform/runtime/compaction/manager.ts).
|
|
7
|
+
* The TUI's actual `/compact` path never instantiates `CompactionManager` — it
|
|
8
|
+
* goes through `platform/core/context-compaction.ts` via
|
|
9
|
+
* `ConversationManager.compact()` (see `compactConversation()` in
|
|
10
|
+
* runtime-services.ts), which has no scoring of its own. Adopting the whole
|
|
11
|
+
* `CompactionManager` pipeline just to get the score would be a far bigger
|
|
12
|
+
* change than this item calls for.
|
|
13
|
+
*
|
|
14
|
+
* Instead, this module calls `computeQualityScore()` directly and out-of-band,
|
|
15
|
+
* fed by the before/after message sets and token estimates
|
|
16
|
+
* `compactConversation()` already holds. That's enough to close the gap:
|
|
17
|
+
* quality scoring reaches `/compact` and `/compact-history` without adopting
|
|
18
|
+
* `CompactionManager`.
|
|
19
|
+
*
|
|
20
|
+
* Honesty note: the `strategy` value passed to `computeQualityScore()` is a
|
|
21
|
+
* BORROWED rubric label describing what `compactConversation()`'s structured
|
|
22
|
+
* multi-section extraction actually does (collapse the whole conversation
|
|
23
|
+
* into one handoff message) — it is NOT a real `CompactionStrategy`
|
|
24
|
+
* escalation result, and no strategy switch is triggered from it. Every
|
|
25
|
+
* rendering of the score says so explicitly (see `formatQualityScoreLine`).
|
|
26
|
+
*
|
|
27
|
+
* Storage: scores are kept in a TUI-local, timestamp-keyed store, never added
|
|
28
|
+
* to the SDK's public `CompactionEvent` type (that would force an
|
|
29
|
+
* api-extractor regen for a value the SDK's own compaction log never
|
|
30
|
+
* populates). Keyed by `CompactionEvent.timestamp` rather than array index,
|
|
31
|
+
* because `compactSmallWindow`'s null-event path (small-context-window
|
|
32
|
+
* models — see runtime-services.ts) can create index gaps.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { computeQualityScore, describeScore } from '@/runtime/index.ts';
|
|
36
|
+
import type { CompactionQualityScore, StrategyInput, StrategyOutput } from '@/runtime/index.ts';
|
|
37
|
+
import type { ProviderMessage } from '@pellux/goodvibes-sdk/platform/providers';
|
|
38
|
+
|
|
39
|
+
export type { CompactionQualityScore } from '@/runtime/index.ts';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Bound the TUI-local score store the same way the SDK bounds its own
|
|
43
|
+
* compaction event log (context-compaction.ts caps at 50 and shifts the
|
|
44
|
+
* oldest entry) — a long-running session should not accumulate this forever.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_TRACKED_SCORES = 50;
|
|
47
|
+
|
|
48
|
+
const scoresByTimestamp = new Map<number, CompactionQualityScore>();
|
|
49
|
+
|
|
50
|
+
/** Record a computed quality score, keyed by the CompactionEvent's timestamp. */
|
|
51
|
+
export function recordCompactionQualityScore(timestamp: number, score: CompactionQualityScore): void {
|
|
52
|
+
scoresByTimestamp.set(timestamp, score);
|
|
53
|
+
if (scoresByTimestamp.size > MAX_TRACKED_SCORES) {
|
|
54
|
+
const oldestKey = scoresByTimestamp.keys().next().value;
|
|
55
|
+
if (oldestKey !== undefined) scoresByTimestamp.delete(oldestKey);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Look up a previously-recorded quality score for a CompactionEvent, by timestamp. */
|
|
60
|
+
export function getCompactionQualityScore(timestamp: number): CompactionQualityScore | undefined {
|
|
61
|
+
return scoresByTimestamp.get(timestamp);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ScoreCompactionRunInput {
|
|
65
|
+
readonly sessionId: string;
|
|
66
|
+
readonly contextWindow: number;
|
|
67
|
+
readonly messagesBefore: readonly ProviderMessage[];
|
|
68
|
+
readonly messagesAfter: readonly ProviderMessage[];
|
|
69
|
+
readonly tokensBefore: number;
|
|
70
|
+
readonly tokensAfter: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Compute a CompactionQualityScore for one compaction run, out-of-band from
|
|
75
|
+
* the SDK's own (unused, in this TUI) CompactionManager pipeline.
|
|
76
|
+
*/
|
|
77
|
+
export function scoreCompactionRun(input: ScoreCompactionRunInput): CompactionQualityScore {
|
|
78
|
+
const strategyInput: StrategyInput = {
|
|
79
|
+
sessionId: input.sessionId,
|
|
80
|
+
messages: input.messagesBefore,
|
|
81
|
+
tokensBefore: input.tokensBefore,
|
|
82
|
+
contextWindow: input.contextWindow,
|
|
83
|
+
// Borrowed rubric label, not a real strategy escalation — see module doc.
|
|
84
|
+
strategy: 'collapse',
|
|
85
|
+
};
|
|
86
|
+
const strategyOutput: StrategyOutput = {
|
|
87
|
+
messages: [...input.messagesAfter],
|
|
88
|
+
tokensAfter: input.tokensAfter,
|
|
89
|
+
summary: '',
|
|
90
|
+
strategy: 'collapse',
|
|
91
|
+
durationMs: 0,
|
|
92
|
+
warnings: [],
|
|
93
|
+
};
|
|
94
|
+
return computeQualityScore(strategyInput, strategyOutput);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** One-line grade summary for the /compact after-notice and /compact-history entries. */
|
|
98
|
+
export function formatQualityScoreLine(score: CompactionQualityScore): string {
|
|
99
|
+
return `Quality: ${describeScore(score)} (rubric applied out-of-band; no strategy escalation ran)`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import { ModalFactory, type ModalSection, type ModalTab } from './modal-factory.ts';
|
|
3
|
+
import { resolveUiTones } from './theme.ts';
|
|
4
|
+
import type { ConfigModal, ConfigModalRenderModel } from '../input/config-modal.ts';
|
|
5
|
+
|
|
6
|
+
const TONES = resolveUiTones('dark');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* renderConfigModal — the single render path for every ConfigModalSurface.
|
|
10
|
+
* Reads the host's frozen-structure-plus-live-values render model and maps it
|
|
11
|
+
* onto ModalFactory.createModal (the same renderer settings-modal uses), so the
|
|
12
|
+
* chrome, tab strip, list, and footer are one dialect across all migrated
|
|
13
|
+
* surfaces. Pure: takes the modal's render model, returns Line[].
|
|
14
|
+
*/
|
|
15
|
+
export function renderConfigModal(modal: ConfigModal, width: number, height: number): Line[] {
|
|
16
|
+
// Size the visible row window to the available height so the list scrolls
|
|
17
|
+
// rather than overflowing (posture header + separator + chrome ≈ 8 rows).
|
|
18
|
+
const visible = Math.max(3, Math.min(16, height - 8));
|
|
19
|
+
modal.setViewportRows(visible);
|
|
20
|
+
const model = modal.getRenderModel();
|
|
21
|
+
return renderConfigModalModel(model, width);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Pure model→Line[] mapping — the shared inner step of renderConfigModal. */
|
|
25
|
+
function renderConfigModalModel(model: ConfigModalRenderModel, width: number): Line[] {
|
|
26
|
+
const sections: ModalSection[] = [];
|
|
27
|
+
|
|
28
|
+
if (model.degraded) {
|
|
29
|
+
sections.push({ type: 'text', content: `⚠ ${model.degraded}`, style: { fg: TONES.state.warn, bold: true } });
|
|
30
|
+
sections.push({ type: 'separator' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const line of model.header) {
|
|
34
|
+
sections.push({ type: 'text', content: line, style: { fg: TONES.fg.secondary } });
|
|
35
|
+
}
|
|
36
|
+
if (model.header.length > 0) sections.push({ type: 'separator' });
|
|
37
|
+
|
|
38
|
+
if (model.rows.length === 0) {
|
|
39
|
+
sections.push({ type: 'text', content: model.emptyText ?? 'Nothing to show.', style: { fg: TONES.fg.muted, dim: true } });
|
|
40
|
+
} else {
|
|
41
|
+
sections.push({
|
|
42
|
+
type: 'list',
|
|
43
|
+
items: model.rows.map((row) => ({
|
|
44
|
+
label: row.label,
|
|
45
|
+
selected: row.selected,
|
|
46
|
+
style: row.stale
|
|
47
|
+
? { fg: TONES.fg.muted, dim: true }
|
|
48
|
+
: row.style,
|
|
49
|
+
})),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tabs: ModalTab[] | undefined = model.tabs.length > 1
|
|
54
|
+
? model.tabs.map((t) => ({ label: t.label, active: t.active }))
|
|
55
|
+
: undefined;
|
|
56
|
+
|
|
57
|
+
// Keep the content region a stable height so live-value ticks never change
|
|
58
|
+
// the box size: frozen header + separator + the fixed visible-row window.
|
|
59
|
+
const targetContentRows =
|
|
60
|
+
(model.degraded ? 2 : 0) +
|
|
61
|
+
model.header.length +
|
|
62
|
+
(model.header.length > 0 ? 1 : 0) +
|
|
63
|
+
model.scroll.visible;
|
|
64
|
+
|
|
65
|
+
const helpers = model.status
|
|
66
|
+
? [{ content: model.status, accent: true }]
|
|
67
|
+
: undefined;
|
|
68
|
+
|
|
69
|
+
return ModalFactory.createModal(
|
|
70
|
+
{
|
|
71
|
+
title: model.title,
|
|
72
|
+
width: 76,
|
|
73
|
+
tabs,
|
|
74
|
+
sections,
|
|
75
|
+
targetContentRows,
|
|
76
|
+
helpers,
|
|
77
|
+
hints: [...model.hints],
|
|
78
|
+
},
|
|
79
|
+
width,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -8,11 +8,9 @@ import { renderModelWorkspace } from './model-workspace.ts';
|
|
|
8
8
|
import { renderSelectionModalOverlay } from './selection-modal-overlay.ts';
|
|
9
9
|
import { renderSearchOverlay } from './search-overlay.ts';
|
|
10
10
|
import { renderHistorySearchOverlay } from './history-search-overlay.ts';
|
|
11
|
-
import { renderProcessModal } from './process-modal.ts';
|
|
12
|
-
import { renderAgentDetailModal } from './agent-detail-modal.ts';
|
|
13
|
-
import { renderLiveTailModal } from './live-tail-modal.ts';
|
|
14
11
|
import { renderContextInspector } from './context-inspector.ts';
|
|
15
12
|
import { renderSettingsModal } from './settings-modal.ts';
|
|
13
|
+
import { renderConfigModal } from './config-modal.ts';
|
|
16
14
|
import { renderMcpWorkspace } from './mcp-workspace.ts';
|
|
17
15
|
import { renderSessionPickerModal } from './session-picker-modal.ts';
|
|
18
16
|
import { renderProfilePickerModal } from './profile-picker-modal.ts';
|
|
@@ -78,20 +76,9 @@ export function applyConversationOverlays(
|
|
|
78
76
|
next.push(...renderHistorySearchOverlay(input.historySearch, conversationWidth));
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!fullscreenClaimed && input.agentDetailModal.active) {
|
|
87
|
-
const lines = renderAgentDetailModal(input.agentDetailModal, conversationWidth, viewportHeight);
|
|
88
|
-
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!fullscreenClaimed && input.liveTailModal.active) {
|
|
92
|
-
const lines = renderLiveTailModal(input.liveTailModal, conversationWidth, viewportHeight);
|
|
93
|
-
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
94
|
-
}
|
|
79
|
+
// W6.1 retirement: the process, agent-detail, and live-tail modal overlays
|
|
80
|
+
// were removed with those modals — F2 now opens the Fleet panel, which
|
|
81
|
+
// subsumes the live process tree.
|
|
95
82
|
|
|
96
83
|
if (!fullscreenClaimed && input.contextInspectorModal.active) {
|
|
97
84
|
const lines = renderContextInspector(conversation, conversationWidth, viewportHeight, contextWindow);
|
|
@@ -104,6 +91,12 @@ export function applyConversationOverlays(
|
|
|
104
91
|
fullscreenClaimed = true;
|
|
105
92
|
}
|
|
106
93
|
|
|
94
|
+
if (!fullscreenClaimed && input.configModal.active) {
|
|
95
|
+
const lines = renderConfigModal(input.configModal, conversationWidth, viewportHeight);
|
|
96
|
+
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
97
|
+
fullscreenClaimed = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
107
100
|
if (input.mcpWorkspace.active) {
|
|
108
101
|
const lines = renderMcpWorkspace(input.mcpWorkspace, conversationWidth, viewportHeight);
|
|
109
102
|
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// fleet-tab-strip.ts
|
|
3
|
+
//
|
|
4
|
+
// Wave-3 (W3.1 Part C3) — renders FleetPanel's session-tab strip by reusing
|
|
5
|
+
// renderTabStrip exactly as panel-workspace-bar.ts does for the workspace
|
|
6
|
+
// tab bar. Deliberately a SEPARATE, visually distinct strip from the
|
|
7
|
+
// workspace bar: workspace tabs switch PANELS, fleet session tabs switch
|
|
8
|
+
// ATTACHED PROCESSES within one panel — conflating their styling would blur
|
|
9
|
+
// that distinction for the operator.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
import type { Line } from '../types/grid.ts';
|
|
13
|
+
import type { FleetTabsState } from '../panels/fleet-tabs.ts';
|
|
14
|
+
import { renderTabStrip, type TabHitRegion } from './tab-strip.ts';
|
|
15
|
+
import { UI_TONES } from './ui-primitives.ts';
|
|
16
|
+
|
|
17
|
+
const ACTIVE_FG = UI_TONES.fg.primary;
|
|
18
|
+
const ACTIVE_BG = UI_TONES.bg.selected;
|
|
19
|
+
const INACTIVE_FG = UI_TONES.fg.muted;
|
|
20
|
+
const SEPARATOR_FG = UI_TONES.fg.dim;
|
|
21
|
+
const LABEL_FG = UI_TONES.fg.secondary;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Render the fleet session-tab strip, or `null` when there are no attached
|
|
25
|
+
* tabs — the panel omits the strip entirely in that case (root-tab-only),
|
|
26
|
+
* which is what keeps the pre-Wave-3 fleet-panel goldens byte-identical.
|
|
27
|
+
*/
|
|
28
|
+
export function renderFleetTabStrip(
|
|
29
|
+
state: FleetTabsState,
|
|
30
|
+
width: number,
|
|
31
|
+
onLayout?: (regions: readonly TabHitRegion[]) => void,
|
|
32
|
+
): Line | null {
|
|
33
|
+
if (state.tabs.length === 0) return null;
|
|
34
|
+
return renderTabStrip({
|
|
35
|
+
width,
|
|
36
|
+
onLayout,
|
|
37
|
+
tabs: [
|
|
38
|
+
{ label: 'Tree', active: state.activeTabIndex === 0 },
|
|
39
|
+
...state.tabs.map((tab, index) => ({
|
|
40
|
+
label: tab.label,
|
|
41
|
+
active: state.activeTabIndex === index + 1,
|
|
42
|
+
})),
|
|
43
|
+
],
|
|
44
|
+
prefixLabel: ' SESSIONS ',
|
|
45
|
+
style: {
|
|
46
|
+
activeFg: ACTIVE_FG,
|
|
47
|
+
activeBg: ACTIVE_BG,
|
|
48
|
+
activeBold: true,
|
|
49
|
+
inactiveFg: INACTIVE_FG,
|
|
50
|
+
separatorFg: SEPARATOR_FG,
|
|
51
|
+
labelFg: LABEL_FG,
|
|
52
|
+
overflowFg: SEPARATOR_FG,
|
|
53
|
+
trailingFg: INACTIVE_FG,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -25,9 +25,17 @@ export function isAgentActive(composerStatus: string | undefined): boolean {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const TIP_PANELS = 'Ctrl+P panels';
|
|
28
|
-
|
|
28
|
+
// W6.2 e: F2 now opens the Fleet panel (the process modal was retired), so the
|
|
29
|
+
// tip names 'fleet', not 'processes'.
|
|
30
|
+
const TIP_PROCESSES = 'F2 fleet';
|
|
29
31
|
const TIP_HELP = '? help';
|
|
30
|
-
|
|
32
|
+
// W6.2 f: an empty-composer Ctrl+C does NOT quit on the first press — it arms a
|
|
33
|
+
// ~1s "press again to exit" confirm, and only a SECOND Ctrl+C within that window
|
|
34
|
+
// exits (an intentional accidental-exit guard). The old 'Ctrl+C quit' tip
|
|
35
|
+
// implied a single press quits, so two presses seconds apart appeared to do
|
|
36
|
+
// nothing. 'Ctrl+C x2 quit' matches the real double-press contract (and the
|
|
37
|
+
// help overlay's "Ctrl+C x2 Exit" row).
|
|
38
|
+
const TIP_QUIT = 'Ctrl+C x2 quit';
|
|
31
39
|
|
|
32
40
|
/**
|
|
33
41
|
* Build the footer discoverability tip. Segments join with the shared middle
|
|
@@ -24,6 +24,7 @@ export class GitStatusProvider {
|
|
|
24
24
|
private lastFetch = 0;
|
|
25
25
|
private readonly ttlMs = 2000;
|
|
26
26
|
private fetching = false;
|
|
27
|
+
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
27
28
|
|
|
28
29
|
constructor(private readonly workingDirectory: string) {}
|
|
29
30
|
|
|
@@ -53,6 +54,45 @@ export class GitStatusProvider {
|
|
|
53
54
|
return this.cache;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Start a lightweight live-repo-state poll. Mirrors DiffPanel's principle of
|
|
59
|
+
* never trusting a cached "is this a repo" flag (see diff-panel.ts
|
|
60
|
+
* showGitDiff/showFileDiffs/showStagedDiff, all gated on a fresh
|
|
61
|
+
* GitService.isGitRepo() check) but amortizes the cost for the header: the
|
|
62
|
+
* cheap synchronous isGitRepo() spawn runs every tick; the heavier async
|
|
63
|
+
* status()+branch() fetch (this.refresh()) only runs when that boolean
|
|
64
|
+
* actually flips — e.g. an external `git init`, or `.git` removed.
|
|
65
|
+
*/
|
|
66
|
+
startPolling(intervalMs: number, onChange: (info: GitHeaderInfo) => void): void {
|
|
67
|
+
if (this.pollTimer !== null) return;
|
|
68
|
+
const checkIsRepo = (): boolean | null => {
|
|
69
|
+
try {
|
|
70
|
+
return GitService.isGitRepo(this.workingDirectory);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
logger.debug('GitStatusProvider: isGitRepo poll check failed', { error: summarizeError(err) });
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
let lastKnownIsRepo = checkIsRepo();
|
|
77
|
+
this.pollTimer = setInterval(() => {
|
|
78
|
+
const isRepoNow = checkIsRepo();
|
|
79
|
+
if (isRepoNow !== null && isRepoNow !== lastKnownIsRepo) {
|
|
80
|
+
lastKnownIsRepo = isRepoNow;
|
|
81
|
+
this.refresh().then(onChange).catch((err) => {
|
|
82
|
+
logger.debug('GitStatusProvider: poll-triggered refresh failed', { error: summarizeError(err) });
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}, intervalMs);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Stop the live-repo-state poll started by startPolling(). No-op if not polling. */
|
|
89
|
+
stopPolling(): void {
|
|
90
|
+
if (this.pollTimer !== null) {
|
|
91
|
+
clearInterval(this.pollTimer);
|
|
92
|
+
this.pollTimer = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
56
96
|
private async _fetch(): Promise<void> {
|
|
57
97
|
if (this.fetching) return;
|
|
58
98
|
this.fetching = true;
|
|
@@ -289,7 +289,7 @@ export function renderShortcutsOverlay(
|
|
|
289
289
|
row(kb('block-copy'), 'Copy block to clipboard'),
|
|
290
290
|
row(kb('block-save'), 'Save block to file'),
|
|
291
291
|
row(kb('copy-selection'), 'Copy selection'),
|
|
292
|
-
row('F2', '
|
|
292
|
+
row('F2', 'Open the Fleet panel'),
|
|
293
293
|
row('?', 'Help overlay'),
|
|
294
294
|
row(`${kb('clear-cancel')} x2`, 'Exit'),
|
|
295
295
|
'',
|
|
@@ -303,7 +303,7 @@ export function renderShortcutsOverlay(
|
|
|
303
303
|
row(kb('panel-focus-toggle'), 'Swap focus between top / bottom pane'),
|
|
304
304
|
row(kb('panel-close'), 'Close active panel'),
|
|
305
305
|
row(kb('panel-close-all'), 'Close all panels'),
|
|
306
|
-
row(kb('panel-ops'), 'Open the
|
|
306
|
+
row(kb('panel-ops'), 'Open and focus the Fleet panel'),
|
|
307
307
|
'',
|
|
308
308
|
' In-Panel Controls',
|
|
309
309
|
' ' + '\u2500'.repeat(40),
|
|
@@ -124,6 +124,10 @@ function modelKey(model: ModelDefinition): string {
|
|
|
124
124
|
|
|
125
125
|
function targetSummary(info: ModelPickerTargetInfo): string {
|
|
126
126
|
if (!info.enabled) return 'disabled';
|
|
127
|
+
// 'embeddings' has no model concept — configuredNote carries the honest
|
|
128
|
+
// provider id + dimensions + configured-state summary instead of a
|
|
129
|
+
// computed provider:model route (which would print a phantom "model:").
|
|
130
|
+
if (info.configuredNote) return info.configuredNote;
|
|
127
131
|
const route = formatRoute(info.provider, info.model);
|
|
128
132
|
if (info.inherited) return `inherits ${route}`;
|
|
129
133
|
return route;
|
|
@@ -145,6 +149,7 @@ function targetLabelFor(target: string): string {
|
|
|
145
149
|
if (target === 'helper') return 'Helper Model';
|
|
146
150
|
if (target === 'tool') return 'Tool LLM';
|
|
147
151
|
if (target === 'tts') return 'TTS LLM';
|
|
152
|
+
if (target === 'embeddings') return 'Embeddings';
|
|
148
153
|
return 'Main Chat';
|
|
149
154
|
}
|
|
150
155
|
|
|
@@ -174,6 +179,13 @@ function detailLines(picker: ModelPickerModal, width: number): string[] {
|
|
|
174
179
|
}
|
|
175
180
|
} else if (picker.mode === 'effort') {
|
|
176
181
|
lines.push(`Reasoning effort applies to the main chat model. Select the default effort for this model.`);
|
|
182
|
+
} else if (picker.mode === 'embeddingProvider') {
|
|
183
|
+
lines.push('Embedding provider selection: choose the provider memory search and the code index use to embed content.');
|
|
184
|
+
const selectedEmbedding = picker.embeddingProviders[picker.selectedIndex];
|
|
185
|
+
if (selectedEmbedding) {
|
|
186
|
+
const status = selectedEmbedding.configured ? 'configured' : 'unconfigured';
|
|
187
|
+
lines.push(`Selected: ${selectedEmbedding.id} | ${selectedEmbedding.dimensions} dimensions | ${status}${selectedEmbedding.detail ? ` | ${selectedEmbedding.detail}` : ''}`);
|
|
188
|
+
}
|
|
177
189
|
} else {
|
|
178
190
|
lines.push(`Context cap overrides the detected local-model context window for this selection.`);
|
|
179
191
|
}
|
|
@@ -288,6 +300,28 @@ function renderContextCapRows(picker: ModelPickerModal, lines: Line[], rows: num
|
|
|
288
300
|
}
|
|
289
301
|
}
|
|
290
302
|
|
|
303
|
+
function renderEmbeddingProviderRows(picker: ModelPickerModal, lines: Line[], rows: number, startX: number, width: number): void {
|
|
304
|
+
const providers = picker.embeddingProviders;
|
|
305
|
+
const { start, end } = stableWindow(providers.length, picker.selectedIndex, rows);
|
|
306
|
+
for (let visibleRow = 0; visibleRow < rows; visibleRow += 1) {
|
|
307
|
+
const absolute = start + visibleRow;
|
|
308
|
+
const provider = providers[absolute];
|
|
309
|
+
const line = lines[visibleRow]!;
|
|
310
|
+
if (!provider) continue;
|
|
311
|
+
const selected = absolute === picker.selectedIndex;
|
|
312
|
+
const bg = selected ? PALETTE.selectedBg : PALETTE.bodyBg;
|
|
313
|
+
fillRange(line, startX, startX + width - 1, bg);
|
|
314
|
+
const marker = selected ? (picker.focusPane === 'items' ? GLYPHS.navigation.selected : '•') : ' ';
|
|
315
|
+
const status = provider.configured ? 'configured' : 'unconfigured';
|
|
316
|
+
writeText(line, startX + 1, width, padDisplay(marker, 2), { fg: PALETTE.text, bg, bold: selected });
|
|
317
|
+
writeText(line, startX + 3, Math.max(0, width - 30), padDisplay(provider.label, Math.max(0, width - 30)), { fg: selected ? PALETTE.text : PALETTE.muted, bg, bold: selected });
|
|
318
|
+
writeText(line, startX + Math.max(4, width - 25), 12, padDisplay(`${provider.dimensions}d`, 12), { fg: PALETTE.dim, bg });
|
|
319
|
+
writeText(line, startX + Math.max(4, width - 13), 12, padDisplay(status, 12), { fg: provider.configured ? PALETTE.good : PALETTE.warn, bg });
|
|
320
|
+
}
|
|
321
|
+
if (start > 0 && lines[0]) writeText(lines[0], startX + 1, width - 2, `${GLYPHS.navigation.moreAbove} ${start} more provider(s) above`, { fg: PALETTE.dim, bg: PALETTE.bodyBg, dim: true });
|
|
322
|
+
if (end < providers.length && lines[rows - 1]) writeText(lines[rows - 1]!, startX + 1, width - 2, `${GLYPHS.navigation.moreBelow} ${providers.length - end} more provider(s) below`, { fg: PALETTE.dim, bg: PALETTE.bodyBg, dim: true });
|
|
323
|
+
}
|
|
324
|
+
|
|
291
325
|
function writeTableHeader(line: Line, picker: ModelPickerModal, startX: number, width: number): void {
|
|
292
326
|
fillRange(line, startX, startX + width - 1, PALETTE.footerBg);
|
|
293
327
|
const style = { fg: PALETTE.muted, bg: PALETTE.footerBg, bold: true };
|
|
@@ -303,6 +337,10 @@ function writeTableHeader(line: Line, picker: ModelPickerModal, startX: number,
|
|
|
303
337
|
writeText(line, startX + 1, width - 2, 'Context cap input', style);
|
|
304
338
|
return;
|
|
305
339
|
}
|
|
340
|
+
if (picker.mode === 'embeddingProvider') {
|
|
341
|
+
writeText(line, startX + 1, width - 2, 'Embedding provider Dimensions Status', style);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
306
344
|
const providerW = clamp(Math.floor(width * 0.14), 10, 18);
|
|
307
345
|
const ctxW = 8;
|
|
308
346
|
const tierW = 8;
|
|
@@ -337,7 +375,7 @@ export function renderModelWorkspace(picker: ModelPickerModal, width: number, vi
|
|
|
337
375
|
const header = contentLine(safeWidth, PALETTE.footerBg);
|
|
338
376
|
writeText(header, 2, targetW - 2, 'Targets', { fg: PALETTE.subtitle, bold: true, bg: PALETTE.footerBg });
|
|
339
377
|
drawVertical(header, targetW, PALETTE.footerBg);
|
|
340
|
-
const modeLabel = picker.mode === 'provider' ? 'Provider list' : picker.mode === 'model' ? 'Model list' : picker.mode === 'effort' ? 'Reasoning effort' : 'Context cap';
|
|
378
|
+
const modeLabel = picker.mode === 'provider' ? 'Provider list' : picker.mode === 'model' ? 'Model list' : picker.mode === 'effort' ? 'Reasoning effort' : picker.mode === 'embeddingProvider' ? 'Embedding providers' : 'Context cap';
|
|
341
379
|
writeText(header, contentX + 1, contentW - 2, `${modeLabel} • ${picker.getItemCount()} item(s)`, { fg: PALETTE.subtitle, bold: true, bg: PALETTE.footerBg });
|
|
342
380
|
lines.push(header);
|
|
343
381
|
|
|
@@ -380,6 +418,8 @@ export function renderModelWorkspace(picker: ModelPickerModal, width: number, vi
|
|
|
380
418
|
renderModelRows(picker, listLines, listLines.length, contentX, contentW - 1);
|
|
381
419
|
} else if (picker.mode === 'contextCap') {
|
|
382
420
|
renderContextCapRows(picker, listLines, listLines.length, contentX, contentW - 1);
|
|
421
|
+
} else if (picker.mode === 'embeddingProvider') {
|
|
422
|
+
renderEmbeddingProviderRows(picker, listLines, listLines.length, contentX, contentW - 1);
|
|
383
423
|
} else {
|
|
384
424
|
renderEffortRows(picker, listLines, listLines.length, contentX, contentW - 1);
|
|
385
425
|
}
|
|
@@ -432,6 +472,8 @@ function getRenderCacheKey(picker: ModelPickerModal, width: number, viewportHeig
|
|
|
432
472
|
base.push(objectId(picker.effortLevels), picker.effortLevels.join('\u001f'), picker.pendingModel?.registryKey ?? picker.pendingModel?.id ?? '');
|
|
433
473
|
} else if (picker.mode === 'contextCap') {
|
|
434
474
|
base.push(picker.contextCapQuery, picker.contextCapPendingModel?.registryKey ?? picker.contextCapPendingModel?.id ?? '');
|
|
475
|
+
} else if (picker.mode === 'embeddingProvider') {
|
|
476
|
+
base.push(objectId(picker.embeddingProviders), picker.embeddingProviders.length);
|
|
435
477
|
}
|
|
436
478
|
|
|
437
479
|
return base.join('\u001e');
|
|
@@ -467,6 +509,7 @@ function keyForTargets(values: readonly ModelPickerTargetInfo[]): string {
|
|
|
467
509
|
entry.model,
|
|
468
510
|
entry.enabled ? 1 : 0,
|
|
469
511
|
entry.inherited ? 1 : 0,
|
|
512
|
+
entry.configuredNote ?? '',
|
|
470
513
|
].join('\u001d'))
|
|
471
514
|
.join('\u001f');
|
|
472
515
|
}
|
|
@@ -21,12 +21,18 @@ export function renderPanelWorkspaceBar(
|
|
|
21
21
|
return renderTabStrip({
|
|
22
22
|
width,
|
|
23
23
|
onLayout,
|
|
24
|
-
tabs: tabs.map((tab) => ({
|
|
24
|
+
tabs: tabs.map((tab, i) => ({
|
|
25
25
|
// tab.active = selected in its own pane (drives highlighted background).
|
|
26
26
|
// tab.focused = has keyboard focus (drives brighter text / focus indicator).
|
|
27
27
|
// A tab can be active-but-not-focused (selected in the unfocused pane) or
|
|
28
28
|
// active-and-focused (selected in the focused pane).
|
|
29
|
-
|
|
29
|
+
//
|
|
30
|
+
// A14: the pane marker uses ▲/▼ (top/bottom pane). The old '^'/'v'
|
|
31
|
+
// prefixes read as a Ctrl caret (^) implying a non-existent Ctrl+<tab>
|
|
32
|
+
// hotkey. The REAL per-tab jump key is Alt+N (panel-tab-1..9), rendered
|
|
33
|
+
// here as ⌥N for the first nine tabs so the shown affordance matches the
|
|
34
|
+
// binding (tabs past nine have no Alt+N chord, so no index is shown).
|
|
35
|
+
label: `${tab.pane === 'bottom' ? '▼' : '▲'}${i < 9 ? ` ⌥${i + 1}` : ''} ${tab.icon} ${tab.name}${tab.focused ? ' ▸' : ''}`,
|
|
30
36
|
active: tab.active,
|
|
31
37
|
})),
|
|
32
38
|
prefixLabel: ' PANELS ',
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-turn knowledge injection record rendering (Wave-5 W5.2, wo803).
|
|
3
|
+
*
|
|
4
|
+
* The SDK's passive per-turn retrieval engine (wo801, W5.1 —
|
|
5
|
+
* packages/sdk/src/platform/agents/turn-knowledge-injection.ts) stores one
|
|
6
|
+
* `TurnInjectionRecord` per turn on `AgentRecord.turnInjections` (a bounded
|
|
7
|
+
* ring, default 20 entries) and appends the same record to the agent's
|
|
8
|
+
* session transcript as `{type:'knowledge_injection', turn, ...record}`.
|
|
9
|
+
*
|
|
10
|
+
* `TurnInjectionRecord` itself is not re-exported from the SDK's
|
|
11
|
+
* `platform/agents` barrel (only `AgentRecord.turnInjections` carries its
|
|
12
|
+
* structural shape through the already-public `platform/tools` barrel), so
|
|
13
|
+
* the entry type here is DERIVED from `AgentRecord` rather than imported by
|
|
14
|
+
* name — this needs no SDK export change.
|
|
15
|
+
*
|
|
16
|
+
* Reality check (updated wo805): as of Wave-5 wo805 the TUI's main interactive
|
|
17
|
+
* session DOES route through this engine. The SDK `Orchestrator` runs per-turn
|
|
18
|
+
* passive injection on the evolving primary conversation and records each turn
|
|
19
|
+
* on its own bounded ring, exposed via `Orchestrator.getTurnInjections()` — the
|
|
20
|
+
* main-session counterpart to `AgentRecord.turnInjections` (there is no
|
|
21
|
+
* AgentRecord for the primary conversation). `buildMainSessionTurnInjectionsText`
|
|
22
|
+
* renders that ring for `/recall injections` with no agent id. Spawned agents
|
|
23
|
+
* (Task-tool runs, automation-triggered runs, session continuations) still
|
|
24
|
+
* record onto their own `AgentRecord.turnInjections`, rendered per-agent by
|
|
25
|
+
* `buildTurnInjectionsText` when an explicit agent id is given.
|
|
26
|
+
*/
|
|
27
|
+
import type { AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
|
|
28
|
+
|
|
29
|
+
/** One TurnInjectionRecord entry, derived from AgentRecord (see module doc). */
|
|
30
|
+
export type TurnInjectionEntry = NonNullable<AgentRecord['turnInjections']>[number];
|
|
31
|
+
|
|
32
|
+
function fmtN(n: number): string {
|
|
33
|
+
return n.toLocaleString();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Render a single TurnInjectionRecord as one readable line. */
|
|
37
|
+
export function formatTurnInjectionEntry(entry: TurnInjectionEntry): string {
|
|
38
|
+
const backendTag = entry.embeddingBackend === 'fallback-lexical' ? ' [lexical fallback]' : '';
|
|
39
|
+
if (entry.injectedIds.length === 0) {
|
|
40
|
+
// Honest empty state: the engine ran this turn but injected nothing —
|
|
41
|
+
// distinct from "the engine never ran" (see buildTurnInjectionsText).
|
|
42
|
+
const reasonText = entry.reason === 'no records cleared relevance floor'
|
|
43
|
+
? 'nothing injected this turn — nothing cleared the relevance floor'
|
|
44
|
+
: `nothing injected this turn — ${entry.reason ?? 'unknown reason'}`;
|
|
45
|
+
return ` turn ${entry.turn}: ${reasonText}${backendTag} (considered ${entry.candidatesConsidered}, floor ${entry.relevanceFloor})`;
|
|
46
|
+
}
|
|
47
|
+
const droppedStr = entry.droppedForBudget.length > 0
|
|
48
|
+
? `, dropped for budget: ${entry.droppedForBudget.join(', ')}`
|
|
49
|
+
: '';
|
|
50
|
+
// The retrieval query is part of the record's honesty contract — without it
|
|
51
|
+
// an injected line can't be traced back to WHY those ids were retrieved
|
|
52
|
+
// (Wave-5 replay flagged the omission). Truncated to keep the line scannable.
|
|
53
|
+
const queryStr = entry.query ? ` for ${JSON.stringify(truncateQuery(entry.query))}` : '';
|
|
54
|
+
return (
|
|
55
|
+
` turn ${entry.turn}: injected ${entry.injectedIds.join(', ')}${queryStr} ` +
|
|
56
|
+
`(~${fmtN(entry.tokenCost)}/${fmtN(entry.budgetTokens)} tok, floor ${entry.relevanceFloor})${droppedStr}${backendTag}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function truncateQuery(query: string): string {
|
|
61
|
+
const flat = query.replace(/\s+/g, ' ').trim();
|
|
62
|
+
return flat.length > 48 ? `${flat.slice(0, 47)}…` : flat;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Render the full per-turn injection history for one agent, most-recent-turn
|
|
67
|
+
* first, with an honest empty state when nothing has been recorded yet.
|
|
68
|
+
*
|
|
69
|
+
* An empty `entries` array is deliberately ambiguous about WHY it's empty
|
|
70
|
+
* (flag disabled, no turn with new input has run yet, or every turn's token
|
|
71
|
+
* budget had no headroom) — the SDK does not distinguish these cases in the
|
|
72
|
+
* ring itself, so this renders all three possibilities rather than guessing.
|
|
73
|
+
*/
|
|
74
|
+
export function buildTurnInjectionsText(agentId: string, entries: readonly TurnInjectionEntry[]): string {
|
|
75
|
+
if (entries.length === 0) {
|
|
76
|
+
return (
|
|
77
|
+
`[recall] No per-turn injection records for agent ${agentId} yet. This means one of: ` +
|
|
78
|
+
'passive knowledge injection is disabled, no turn with new input has run yet, or the ' +
|
|
79
|
+
'token budget had no headroom on every turn so far — there is no record either way.'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const lines = [`[recall] Per-turn knowledge injections for agent ${agentId} (${entries.length}, most recent first):`];
|
|
83
|
+
for (const entry of [...entries].reverse()) {
|
|
84
|
+
lines.push(formatTurnInjectionEntry(entry));
|
|
85
|
+
}
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Render the MAIN interactive session's per-turn injection ring
|
|
91
|
+
* (`Orchestrator.getTurnInjections()`, wo805), most-recent-turn first, with the
|
|
92
|
+
* same honest empty state as the per-agent path. The main-session counterpart
|
|
93
|
+
* to {@link buildTurnInjectionsText}: no agent id, main-session-appropriate
|
|
94
|
+
* wording, but the identical per-entry rendering (`formatTurnInjectionEntry`).
|
|
95
|
+
*
|
|
96
|
+
* As with the agent path, an empty `entries` array is deliberately ambiguous
|
|
97
|
+
* about WHY it's empty (flag disabled, no turn with new input has run yet, or
|
|
98
|
+
* every turn's token budget had no headroom) — the ring does not distinguish
|
|
99
|
+
* these, so this renders all three possibilities rather than guessing.
|
|
100
|
+
*/
|
|
101
|
+
export function buildMainSessionTurnInjectionsText(entries: readonly TurnInjectionEntry[]): string {
|
|
102
|
+
if (entries.length === 0) {
|
|
103
|
+
return (
|
|
104
|
+
'[recall] No per-turn injection records for the main session yet. This means one of: ' +
|
|
105
|
+
'passive knowledge injection is disabled, no turn with new input has run yet, or the ' +
|
|
106
|
+
'token budget had no headroom on every turn so far — there is no record either way.'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const lines = [`[recall] Per-turn knowledge injections for the main session (${entries.length}, most recent first):`];
|
|
110
|
+
for (const entry of [...entries].reverse()) {
|
|
111
|
+
lines.push(formatTurnInjectionEntry(entry));
|
|
112
|
+
}
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|