@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
|
@@ -2,10 +2,95 @@ import { type Line } from '../types/grid.ts';
|
|
|
2
2
|
import { UIFactory } from '../renderer/ui-factory.ts';
|
|
3
3
|
import type { PermissionCategory, PermissionRequestAnalysis } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
4
4
|
import { buildPermissionApprovalBrief, getDisplayArg } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
5
|
+
import { DIFF_TONES, UI_TONES } from '../renderer/ui-primitives.ts';
|
|
6
|
+
import type { HunkSelectionState } from './hunk-selection.ts';
|
|
7
|
+
export { buildPendingPermissionExtras } from './hunk-selection.ts';
|
|
5
8
|
|
|
6
9
|
import type { PermissionPromptRequest, PermissionPromptDecision, PermissionRequestHandler, PermissionRequest } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
7
10
|
export type { PermissionPromptRequest, PermissionPromptDecision, PermissionRequestHandler, PermissionRequest };
|
|
8
11
|
|
|
12
|
+
/** Visible hunk rows before a "+N more" trailer kicks in (Risk 3). */
|
|
13
|
+
const MAX_VISIBLE_HUNKS = 8;
|
|
14
|
+
|
|
15
|
+
/** Path rows shown before collapsing to a "N files: a, b, +K more" line (UX-B 2a). */
|
|
16
|
+
const MAX_PATH_ROWS = 3;
|
|
17
|
+
|
|
18
|
+
/** Trailing filename for compact display of a long/absolute path. */
|
|
19
|
+
function baseName(p: string): string {
|
|
20
|
+
const cleaned = p.replace(/[/\\]+$/, '');
|
|
21
|
+
const idx = Math.max(cleaned.lastIndexOf('/'), cleaned.lastIndexOf('\\'));
|
|
22
|
+
return idx >= 0 ? cleaned.slice(idx + 1) : cleaned;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Make an absolute path relative to the working directory when it sits under it. */
|
|
26
|
+
function relativize(p: string, cwd?: string): string {
|
|
27
|
+
if (!cwd || !p.startsWith('/')) return p;
|
|
28
|
+
if (p === cwd) return '.';
|
|
29
|
+
const prefix = cwd.endsWith('/') ? cwd : `${cwd}/`;
|
|
30
|
+
return p.startsWith(prefix) ? p.slice(prefix.length) : p;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Collect string elements or `el[field]` strings from an array arg. */
|
|
34
|
+
function collectField(arr: unknown, field: string): string[] {
|
|
35
|
+
if (!Array.isArray(arr)) return [];
|
|
36
|
+
const out: string[] = [];
|
|
37
|
+
for (const el of arr) {
|
|
38
|
+
if (typeof el === 'string') out.push(el);
|
|
39
|
+
else if (el && typeof el === 'object' && typeof (el as Record<string, unknown>)[field] === 'string') {
|
|
40
|
+
out.push((el as Record<string, unknown>)[field] as string);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The real display target(s) of a tool invocation — the paths/commands/urls a
|
|
48
|
+
* user needs to see, extracted from the actual arg shapes so a nested
|
|
49
|
+
* `{files:[{path}]}` no longer falls through to a raw JSON blob. (UX-B 2a.)
|
|
50
|
+
* Returns [] when nothing recognisable is present (caller falls back).
|
|
51
|
+
*/
|
|
52
|
+
function cardTargets(args: Record<string, unknown>): string[] {
|
|
53
|
+
const files = collectField(args.files, 'path');
|
|
54
|
+
if (files.length) return files;
|
|
55
|
+
if (typeof args.path === 'string') return [args.path];
|
|
56
|
+
if (typeof args.file === 'string') return [args.file];
|
|
57
|
+
const commands = collectField(args.commands, 'cmd');
|
|
58
|
+
if (commands.length) return commands;
|
|
59
|
+
if (typeof args.command === 'string') return [args.command];
|
|
60
|
+
if (typeof args.cmd === 'string') return [args.cmd];
|
|
61
|
+
if (typeof args.pattern === 'string') return [args.pattern];
|
|
62
|
+
const urls = collectField(args.urls, 'url');
|
|
63
|
+
if (urls.length) return urls;
|
|
64
|
+
const queries = collectField(args.queries, 'query');
|
|
65
|
+
if (queries.length) return queries;
|
|
66
|
+
if (typeof args.query === 'string') return [args.query];
|
|
67
|
+
if (typeof args.task === 'string') return [args.task];
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Short human verb for a permission category, for the condensed summary line. */
|
|
72
|
+
function categoryVerb(category: PermissionCategory): string {
|
|
73
|
+
switch (category) {
|
|
74
|
+
case 'write': return 'write';
|
|
75
|
+
case 'execute': return 'run';
|
|
76
|
+
case 'delegate': return 'delegate';
|
|
77
|
+
default: return 'read';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Single source of truth for how many Line rows the hunk-list section
|
|
83
|
+
* occupies: 1 header row + up to MAX_VISIBLE_HUNKS checkbox rows + 1 trailer
|
|
84
|
+
* row (rendered as "+N more" when truncated, blank otherwise — always
|
|
85
|
+
* present so the row count never depends on which branch fires). Both
|
|
86
|
+
* getPromptHeight and createPromptLines call this SAME function, so they
|
|
87
|
+
* cannot drift apart (Risk 2 — main.ts's render loop reserves viewport
|
|
88
|
+
* space from getPromptHeight *before* the real render happens).
|
|
89
|
+
*/
|
|
90
|
+
function hunkListRowCount(hunkState: HunkSelectionState): number {
|
|
91
|
+
return Math.min(hunkState.hunks.length, MAX_VISIBLE_HUNKS) + 2;
|
|
92
|
+
}
|
|
93
|
+
|
|
9
94
|
/**
|
|
10
95
|
* PermissionPromptUI - Renders a permission prompt as Line[] fragments.
|
|
11
96
|
*
|
|
@@ -30,11 +115,62 @@ export class PermissionPromptUI {
|
|
|
30
115
|
};
|
|
31
116
|
}
|
|
32
117
|
|
|
33
|
-
|
|
118
|
+
/** The relativised display target(s) for the Path field, with a raw fallback. */
|
|
119
|
+
private static resolvedTargets(request: PermissionPromptRequest): string[] {
|
|
120
|
+
const cwd = request.workingDirectory;
|
|
121
|
+
const targets = cardTargets(request.args).map((t) => relativize(t, cwd));
|
|
122
|
+
return targets.length > 0 ? targets : [getDisplayArg(request.tool, request.args)];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Path-field row texts — one per target when few, a "N files:" summary when many. */
|
|
126
|
+
private static pathRowTexts(targets: string[], maxLen: number): string[] {
|
|
127
|
+
const clamp = (s: string): string => (s.length > maxLen ? `...${s.slice(-(maxLen - 3))}` : s);
|
|
128
|
+
if (targets.length <= 1) return [clamp(targets[0] ?? '(unknown)')];
|
|
129
|
+
if (targets.length <= MAX_PATH_ROWS) return targets.map(clamp);
|
|
130
|
+
const shown = targets.slice(0, 2).map(baseName).join(', ');
|
|
131
|
+
return [clamp(`${targets.length} files: ${shown}, +${targets.length - 2} more`)];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A low-risk, project-local request is shown condensed (one summary line +
|
|
136
|
+
* choices) unless the user expanded it with `d`. High/critical risk, external
|
|
137
|
+
* scope, and hunk-selectable edits always show the full block. (UX-B 2b.)
|
|
138
|
+
*/
|
|
139
|
+
private static isCondensed(
|
|
140
|
+
request: PermissionPromptRequest,
|
|
141
|
+
hunkState: HunkSelectionState | undefined,
|
|
142
|
+
detailsExpanded: boolean,
|
|
143
|
+
): boolean {
|
|
144
|
+
if (hunkState || detailsExpanded) return false;
|
|
145
|
+
// Shell execution, delegation and network requests always show the full
|
|
146
|
+
// card — their action semantics (command, side effects, host, checklist)
|
|
147
|
+
// warrant scrutiny even when the risk model rates them low. Only mundane
|
|
148
|
+
// low-risk filesystem reads/writes condense. (UX-B 2b.)
|
|
149
|
+
if (request.category === 'execute' || request.category === 'delegate') return false;
|
|
150
|
+
const analysis = this.fallbackAnalysis(request);
|
|
151
|
+
if (analysis.riskLevel !== 'low') return false;
|
|
152
|
+
const scope = analysis.blastRadius;
|
|
153
|
+
return scope === undefined || scope === 'local' || scope === 'project';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static getPromptHeight(
|
|
157
|
+
request: PermissionPromptRequest,
|
|
158
|
+
hunkState?: HunkSelectionState,
|
|
159
|
+
detailsExpanded = false,
|
|
160
|
+
): number {
|
|
161
|
+
// Condensed low-risk card: top separator, title, summary, choices, bottom
|
|
162
|
+
// separator — see createPromptLines' condensed branch (must stay in sync).
|
|
163
|
+
if (this.isCondensed(request, hunkState, detailsExpanded)) return 5;
|
|
34
164
|
const analysis = this.fallbackAnalysis(request);
|
|
35
165
|
const reasonLines = Math.min(2, Math.max(1, analysis.reasons.length));
|
|
36
166
|
const extraLines = (analysis.host ? 1 : 0) + (analysis.surface ? 1 : 0) + (analysis.sideEffects && analysis.sideEffects.length > 0 ? 1 : 0);
|
|
37
|
-
|
|
167
|
+
const hunkLines = hunkState ? hunkListRowCount(hunkState) : 0;
|
|
168
|
+
// Base 12 counted a single arg line; the Path field now spans `pathRows`
|
|
169
|
+
// lines and the full card adds one raw-args row (2a reachability), so the
|
|
170
|
+
// arg allotment becomes pathRows + 1 → base 12 + pathRows (12 already
|
|
171
|
+
// included one of those). See createPromptLines' full branch.
|
|
172
|
+
const pathRows = this.pathRowTexts(this.resolvedTargets(request), 999).length;
|
|
173
|
+
return 12 + pathRows + reasonLines + extraLines + hunkLines;
|
|
38
174
|
}
|
|
39
175
|
|
|
40
176
|
/** Returns the key argument to display for a given tool invocation. */
|
|
@@ -64,12 +200,16 @@ export class PermissionPromptUI {
|
|
|
64
200
|
* createPromptLines - Renders the permission prompt as an array of Lines.
|
|
65
201
|
* Injected into the viewport by the render function when a request is pending.
|
|
66
202
|
*/
|
|
67
|
-
static createPromptLines(
|
|
203
|
+
static createPromptLines(
|
|
204
|
+
width: number,
|
|
205
|
+
request: PermissionRequest,
|
|
206
|
+
hunkState?: HunkSelectionState,
|
|
207
|
+
detailsExpanded = false,
|
|
208
|
+
): Line[] {
|
|
68
209
|
const lines: Line[] = [];
|
|
69
210
|
const { tool, args, category } = request;
|
|
70
211
|
const analysis = this.fallbackAnalysis(request);
|
|
71
212
|
const brief = buildPermissionApprovalBrief(request);
|
|
72
|
-
const displayArg = this.getDisplayArg(tool, args);
|
|
73
213
|
const { label, color } = this.getCategoryLabel(category);
|
|
74
214
|
|
|
75
215
|
const ACCENT = '135'; // purple
|
|
@@ -77,6 +217,24 @@ export class PermissionPromptUI {
|
|
|
77
217
|
const TEXT = '252';
|
|
78
218
|
const DIM = '244';
|
|
79
219
|
|
|
220
|
+
const maxArgLen = Math.max(10, width - 16);
|
|
221
|
+
const pathRows = this.pathRowTexts(this.resolvedTargets(request), maxArgLen);
|
|
222
|
+
|
|
223
|
+
// Condensed low-risk / project-local card: one summary line (verb → target
|
|
224
|
+
// (scope)) plus the choices. Full block is one `d` away. (UX-B 2b.)
|
|
225
|
+
if (this.isCondensed(request, hunkState, detailsExpanded)) {
|
|
226
|
+
lines.push(UIFactory.stringToLine('─'.repeat(width), width, { fg: ACCENT, dim: true }));
|
|
227
|
+
lines.push(UIFactory.stringToLine(` [${label}] ${brief.title} `.padEnd(width), width, { fg: WARN, bold: true }));
|
|
228
|
+
const scopeText = analysis.blastRadius ? ` (${analysis.blastRadius})` : '';
|
|
229
|
+
const summaryLine = ` ${categoryVerb(category)} → ${pathRows[0]}${scopeText}`;
|
|
230
|
+
lines.push(UIFactory.stringToLine(summaryLine.padEnd(width), width, { fg: TEXT }));
|
|
231
|
+
lines.push(UIFactory.stringToLine(
|
|
232
|
+
` [Y] Allow once [A] Allow always (session) [N] Deny [d] details`.padEnd(width),
|
|
233
|
+
width, { fg: ACCENT, bold: true }));
|
|
234
|
+
lines.push(UIFactory.stringToLine('─'.repeat(width), width, { fg: ACCENT, dim: true }));
|
|
235
|
+
return lines;
|
|
236
|
+
}
|
|
237
|
+
|
|
80
238
|
// Top separator
|
|
81
239
|
lines.push(UIFactory.stringToLine('─'.repeat(width), width, { fg: ACCENT, dim: true }));
|
|
82
240
|
|
|
@@ -89,13 +247,13 @@ export class PermissionPromptUI {
|
|
|
89
247
|
const toolLine = ` Tool : ${tool}`;
|
|
90
248
|
lines.push(UIFactory.stringToLine(toolLine.padEnd(width), width, { fg: TEXT }));
|
|
91
249
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
:
|
|
97
|
-
|
|
98
|
-
|
|
250
|
+
// Path/subject row(s): actual target path(s), one per line when few, a
|
|
251
|
+
// "N files: a, b, +K more" summary when many. Raw args move to the Args row
|
|
252
|
+
// below so this field never shows a truncated JSON blob. (UX-B 2a.)
|
|
253
|
+
pathRows.forEach((rowText, i) => {
|
|
254
|
+
const labelCol = i === 0 ? brief.subjectLabel.padEnd(9) : ' '.repeat(9);
|
|
255
|
+
lines.push(UIFactory.stringToLine(` ${labelCol}: ${rowText}`.padEnd(width), width, { fg: TEXT }));
|
|
256
|
+
});
|
|
99
257
|
|
|
100
258
|
// Working directory row
|
|
101
259
|
const cwd = request.workingDirectory ?? '(unknown)';
|
|
@@ -150,11 +308,56 @@ export class PermissionPromptUI {
|
|
|
150
308
|
const checklistLine = ` Checklist : ${truncatedChecklist}`;
|
|
151
309
|
lines.push(UIFactory.stringToLine(checklistLine.padEnd(width), width, { fg: DIM }));
|
|
152
310
|
|
|
311
|
+
// Raw args row — the full tool arguments live here in the details view so
|
|
312
|
+
// the Path field above can render clean path(s), never a JSON blob. (2a.)
|
|
313
|
+
const rawArgs = JSON.stringify(args);
|
|
314
|
+
const maxRawLen = Math.max(10, width - 16);
|
|
315
|
+
const truncatedRaw = rawArgs.length > maxRawLen ? `${rawArgs.slice(0, maxRawLen - 3)}...` : rawArgs;
|
|
316
|
+
lines.push(UIFactory.stringToLine(` Args : ${truncatedRaw}`.padEnd(width), width, { fg: DIM }));
|
|
317
|
+
|
|
153
318
|
// Blank spacer
|
|
154
319
|
lines.push(UIFactory.stringToLine(' '.repeat(width), width));
|
|
155
320
|
|
|
156
|
-
|
|
157
|
-
|
|
321
|
+
if (hunkState) {
|
|
322
|
+
// Hunk list — see hunkListRowCount() for the row-count contract this
|
|
323
|
+
// block must match exactly (Risk 2: getPromptHeight/createPromptLines
|
|
324
|
+
// parity is what keeps main.ts's render loop from clipping the
|
|
325
|
+
// viewport).
|
|
326
|
+
const { hunks, cursor, selected } = hunkState;
|
|
327
|
+
const headerLine = ` Hunks (${selected.size}/${hunks.length} selected):`;
|
|
328
|
+
lines.push(UIFactory.stringToLine(headerLine.padEnd(width), width, { fg: TEXT, bold: true }));
|
|
329
|
+
|
|
330
|
+
const visible = hunks.slice(0, MAX_VISIBLE_HUNKS);
|
|
331
|
+
const maxPreviewLen = Math.max(6, Math.floor((width - 20) / 2));
|
|
332
|
+
for (let i = 0; i < visible.length; i++) {
|
|
333
|
+
const hunk = visible[i]!;
|
|
334
|
+
const box = selected.has(i) ? '[x]' : '[ ]';
|
|
335
|
+
const findPreview = hunk.find.replace(/\n/g, '⏎').slice(0, maxPreviewLen);
|
|
336
|
+
const replacePreview = hunk.replace.replace(/\n/g, '⏎').slice(0, maxPreviewLen);
|
|
337
|
+
const rowText = ` ${box} ${i + 1}. ${hunk.path} — "${findPreview}" -> "${replacePreview}"`;
|
|
338
|
+
const isCursor = i === cursor;
|
|
339
|
+
lines.push(UIFactory.stringToLine(
|
|
340
|
+
rowText.padEnd(width),
|
|
341
|
+
width,
|
|
342
|
+
{
|
|
343
|
+
fg: selected.has(i) ? DIFF_TONES.add : DIFF_TONES.del,
|
|
344
|
+
bg: isCursor ? UI_TONES.bg.selected : undefined,
|
|
345
|
+
bold: isCursor,
|
|
346
|
+
},
|
|
347
|
+
));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const hiddenCount = hunks.length - visible.length;
|
|
351
|
+
const trailerLine = hiddenCount > 0 ? ` +${hiddenCount} more` : '';
|
|
352
|
+
lines.push(UIFactory.stringToLine(trailerLine.padEnd(width), width, { fg: DIM }));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Choices row. A condensable card shown in full is the expanded form, so it
|
|
356
|
+
// offers `[d]` to collapse back; other full cards omit the details toggle.
|
|
357
|
+
const collapseHint = this.isCondensed(request, hunkState, false) ? ' [d] hide details' : '';
|
|
358
|
+
const choicesLine = hunkState
|
|
359
|
+
? ` [j/k] Navigate [Space] Toggle [A] All [Enter] Apply selected [N] Deny`
|
|
360
|
+
: ` [Y] Allow once [A] Allow always (session) [N] Deny${collapseHint}`;
|
|
158
361
|
lines.push(UIFactory.stringToLine(choicesLine.padEnd(width), width, { fg: ACCENT, bold: true }));
|
|
159
362
|
|
|
160
363
|
// Bottom separator
|
|
@@ -81,12 +81,35 @@ export function renderAutocompleteOverlay(
|
|
|
81
81
|
}
|
|
82
82
|
const endIdx = Math.min(startIdx + maxVisible, total);
|
|
83
83
|
|
|
84
|
+
// UX-C palette curation (item 4): on a bare '/' (query === ''), the results
|
|
85
|
+
// list is "common tier" (score 2) followed by "alphabetical rest" (score
|
|
86
|
+
// 1) — see CommandRegistry.fuzzyMatch. commonCount marks that boundary;
|
|
87
|
+
// draw a one-row separator there when it falls inside the visible window,
|
|
88
|
+
// consuming one of the maxVisible slots so the box height never changes.
|
|
89
|
+
const hasCommonSeparator = state.query === '' && state.commonCount > 0 && state.commonCount < total;
|
|
90
|
+
type DisplayRow = { type: 'item'; index: number } | { type: 'separator' };
|
|
91
|
+
const displayRows: DisplayRow[] = [];
|
|
92
|
+
for (let i = startIdx; i < endIdx && displayRows.length < maxVisible; i++) {
|
|
93
|
+
if (hasCommonSeparator && i === state.commonCount) {
|
|
94
|
+
displayRows.push({ type: 'separator' });
|
|
95
|
+
if (displayRows.length >= maxVisible) break;
|
|
96
|
+
}
|
|
97
|
+
displayRows.push({ type: 'item', index: i });
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
const indicatorWidth = 2;
|
|
85
101
|
const maxCommandWidth = Math.min(18, Math.max(10, Math.floor(layout.innerWidth * 0.28)));
|
|
86
102
|
const gapWidth = 2;
|
|
87
103
|
const descWidth = Math.max(0, layout.innerWidth - indicatorWidth - maxCommandWidth - gapWidth);
|
|
88
104
|
|
|
89
|
-
for (
|
|
105
|
+
for (const row of displayRows) {
|
|
106
|
+
if (row.type === 'separator') {
|
|
107
|
+
const sepLine = createOverlayContentLine(width, layout);
|
|
108
|
+
putText(sepLine, layout.margin + 2, layout.innerWidth, '─'.repeat(layout.innerWidth), { fg: BORDER_FG, dim: true });
|
|
109
|
+
lines.push(sepLine);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const i = row.index;
|
|
90
113
|
const { command } = results[i];
|
|
91
114
|
const isSelected = i === state.selectedIndex;
|
|
92
115
|
const line = createOverlayContentLine(width, layout, BORDER_FG, isSelected ? SELECTED_BG : '');
|
|
@@ -139,7 +162,10 @@ export function renderAutocompleteOverlay(
|
|
|
139
162
|
}
|
|
140
163
|
|
|
141
164
|
const footerLine = createOverlayContentLine(width, layout);
|
|
142
|
-
|
|
165
|
+
// UX-C vocab unification: "Run"/"Close" match the /help selection modal's
|
|
166
|
+
// hint bar (selection-modal-overlay.ts) — both surfaces execute a command
|
|
167
|
+
// on Enter, so they now say so the same way.
|
|
168
|
+
const hints = '[Tab] Complete [Up/Down] Navigate [Enter] Run [Esc] Close';
|
|
143
169
|
putText(
|
|
144
170
|
footerLine,
|
|
145
171
|
layout.margin + 2,
|
|
@@ -10,11 +10,22 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { getCompactionEvents } from '@pellux/goodvibes-sdk/platform/core';
|
|
13
|
+
import { getCompactionQualityScore } from './compaction-quality.ts';
|
|
13
14
|
import type { CompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
|
|
15
|
+
import type { CompactionQualityScore } from './compaction-quality.ts';
|
|
14
16
|
|
|
15
17
|
// ─── formatCompactionEvent ────────────────────────────────────────────────────
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Exported for direct unit testing (W5.4/B28) — the grade suffix is a pure
|
|
21
|
+
* function of the event and an optional score, independent of whichever
|
|
22
|
+
* runtime lookup (getCompactionQualityScore) supplies that score.
|
|
23
|
+
*/
|
|
24
|
+
export function formatCompactionEvent(
|
|
25
|
+
ev: CompactionEvent,
|
|
26
|
+
n: number,
|
|
27
|
+
qualityScore?: CompactionQualityScore | null,
|
|
28
|
+
): string {
|
|
18
29
|
const date = new Date(ev.timestamp);
|
|
19
30
|
const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
20
31
|
const savings = Math.max(0, ev.tokensBeforeEstimate - ev.tokensAfterEstimate);
|
|
@@ -22,11 +33,15 @@ function formatCompactionEvent(ev: CompactionEvent, n: number): string {
|
|
|
22
33
|
? Math.round((savings / ev.tokensBeforeEstimate) * 100)
|
|
23
34
|
: 0;
|
|
24
35
|
const trigger = ev.trigger === 'auto' ? 'auto' : 'manual';
|
|
36
|
+
// Honest omission: no score means none was computed for this event (e.g. it
|
|
37
|
+
// pre-dates this feature, or ran through the small-window path, which has
|
|
38
|
+
// no CompactionEvent to key a score by) — never fabricate a grade.
|
|
39
|
+
const qualityStr = qualityScore ? ` quality=${qualityScore.grade} (${qualityScore.score.toFixed(2)})` : '';
|
|
25
40
|
return (
|
|
26
41
|
`#${n} ${timeStr} [${trigger}] ` +
|
|
27
42
|
`${ev.messagesBeforeCompaction}→${ev.messagesAfterCompaction} msgs ` +
|
|
28
43
|
`~${fmtN(ev.tokensBeforeEstimate)}→~${fmtN(ev.tokensAfterEstimate)} tok ` +
|
|
29
|
-
`saved ${savingsPct}
|
|
44
|
+
`saved ${savingsPct}%${qualityStr}`
|
|
30
45
|
);
|
|
31
46
|
}
|
|
32
47
|
|
|
@@ -48,7 +63,8 @@ export function buildCompactionHistoryText(): string {
|
|
|
48
63
|
];
|
|
49
64
|
const ordered = [...events].reverse();
|
|
50
65
|
for (let i = 0; i < ordered.length; i++) {
|
|
51
|
-
|
|
66
|
+
const ev = ordered[i]!;
|
|
67
|
+
lines.push(' ' + formatCompactionEvent(ev, ordered.length - i, getCompactionQualityScore(ev.timestamp)));
|
|
52
68
|
}
|
|
53
69
|
lines.push(' (Restore not available — the SDK does not yet expose a snapshot restore API.)');
|
|
54
70
|
return lines.join('\n');
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
|
|
19
19
|
import { estimateConversationTokens } from '@pellux/goodvibes-sdk/platform/core';
|
|
20
20
|
import { computeContextUsage } from '../core/context-usage.ts';
|
|
21
|
+
import { formatQualityScoreLine } from './compaction-quality.ts';
|
|
21
22
|
import type { CompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
|
|
22
23
|
import type { ProviderMessage } from '@pellux/goodvibes-sdk/platform/providers';
|
|
24
|
+
import type { CompactionQualityScore } from './compaction-quality.ts';
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Default compaction totalCeiling from context-compaction DEFAULT_COMPACTION_CONFIG.
|
|
@@ -44,6 +46,12 @@ export interface CompactionAfterOptions {
|
|
|
44
46
|
readonly event: CompactionEvent;
|
|
45
47
|
/** Number of session memories that survived compaction. */
|
|
46
48
|
readonly pinnedMemoryCount: number;
|
|
49
|
+
/**
|
|
50
|
+
* Out-of-band quality-score grade for this run (W5.4/B28). Omitted or null
|
|
51
|
+
* when no score was computed (e.g. the small-window compaction path, which
|
|
52
|
+
* has no CompactionEvent to key a score by) — no line is rendered then.
|
|
53
|
+
*/
|
|
54
|
+
readonly qualityScore?: CompactionQualityScore | null | undefined;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/**
|
|
@@ -82,7 +90,7 @@ export function buildCompactionPreview(opts: CompactionPreviewOptions): string {
|
|
|
82
90
|
* after token counts. The trigger field controls wording.
|
|
83
91
|
*/
|
|
84
92
|
export function buildCompactionAfterNotice(opts: CompactionAfterOptions): string {
|
|
85
|
-
const { event, pinnedMemoryCount } = opts;
|
|
93
|
+
const { event, pinnedMemoryCount, qualityScore } = opts;
|
|
86
94
|
const {
|
|
87
95
|
messagesBeforeCompaction,
|
|
88
96
|
messagesAfterCompaction,
|
|
@@ -102,11 +110,14 @@ export function buildCompactionAfterNotice(opts: CompactionAfterOptions): string
|
|
|
102
110
|
|
|
103
111
|
const triggerStr = trigger === 'auto' ? 'Auto-compact complete' : 'Compact complete';
|
|
104
112
|
|
|
113
|
+
const qualityStr = qualityScore ? `\n ${formatQualityScoreLine(qualityScore)}` : '';
|
|
114
|
+
|
|
105
115
|
return (
|
|
106
116
|
`[Context] ${triggerStr}: ${messagesBeforeCompaction} → ${messagesAfterCompaction} messages,` +
|
|
107
117
|
` ~${fmtN(tokensBeforeEstimate)} → ~${fmtN(tokensAfterEstimate)} tokens` +
|
|
108
118
|
` (saved ~${fmtN(savings)}, ${savingsPct}%).` +
|
|
109
|
-
(pinStr ? ` ${pinStr.trim()}` : '')
|
|
119
|
+
(pinStr ? ` ${pinStr.trim()}` : '') +
|
|
120
|
+
qualityStr
|
|
110
121
|
);
|
|
111
122
|
}
|
|
112
123
|
|
|
@@ -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
|
+
}
|
|
@@ -5,11 +5,9 @@ import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
|
5
5
|
import type { SearchManager } from '../input/search.ts';
|
|
6
6
|
import { allowTerminalWrite } from '../runtime/terminal-output-guard.ts';
|
|
7
7
|
import { probeTermCaps, type TermColorCaps } from './term-caps.ts';
|
|
8
|
-
import {
|
|
8
|
+
import { activeTheme } from './theme.ts';
|
|
9
9
|
import { UI_TONES } from './ui-primitives.ts';
|
|
10
10
|
|
|
11
|
-
const T = DARK_THEME;
|
|
12
|
-
|
|
13
11
|
// Accent / dim colors for the panel focus border. The focused pane's left
|
|
14
12
|
// border column is drawn in the accent tone; the unfocused pane stays dim.
|
|
15
13
|
const PANEL_FOCUS_ACCENT = UI_TONES.state.active; // bright blue
|
|
@@ -252,6 +250,7 @@ export class Compositor {
|
|
|
252
250
|
|
|
253
251
|
// Apply Search Match Highlighting Overlay (left side only)
|
|
254
252
|
if (search && search.manager.active && search.manager.query.length > 0 && i >= offset) {
|
|
253
|
+
const T = activeTheme();
|
|
255
254
|
const absoluteRow = search.scrollTop + (i - offset);
|
|
256
255
|
const lineMatches = search.manager.getMatchesOnLine(absoluteRow);
|
|
257
256
|
for (const match of lineMatches) {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import { ModalFactory, type ModalSection, type ModalTab } from './modal-factory.ts';
|
|
3
|
+
import { getOverlayMaxWidth } from './overlay-viewport.ts';
|
|
4
|
+
import { activeUiTones } from './theme.ts';
|
|
5
|
+
import type { ConfigModal, ConfigModalRenderModel } from '../input/config-modal.ts';
|
|
6
|
+
|
|
7
|
+
// Matches ModalFactory.createModal's own box-width computation (margin 4,
|
|
8
|
+
// requested max width 76 — see renderConfigModalModel below) so the
|
|
9
|
+
// wrap-clamp in ConfigModal.getRenderModel() measures against the SAME width
|
|
10
|
+
// the list section will actually wrap at, at the current terminal size.
|
|
11
|
+
const MODAL_MARGIN = 4;
|
|
12
|
+
const MODAL_MAX_WIDTH = 76;
|
|
13
|
+
/**
|
|
14
|
+
* renderConfigModal — the single render path for every ConfigModalSurface.
|
|
15
|
+
* Reads the host's frozen-structure-plus-live-values render model and maps it
|
|
16
|
+
* onto ModalFactory.createModal (the same renderer settings-modal uses), so the
|
|
17
|
+
* chrome, tab strip, list, and footer are one dialect across all migrated
|
|
18
|
+
* surfaces. Pure: takes the modal's render model, returns Line[].
|
|
19
|
+
*/
|
|
20
|
+
export function renderConfigModal(modal: ConfigModal, width: number, height: number): Line[] {
|
|
21
|
+
// Size the visible row window to the available height so the list scrolls
|
|
22
|
+
// rather than overflowing (posture header + separator + chrome ≈ 8 rows).
|
|
23
|
+
const visible = Math.max(3, Math.min(16, height - 8));
|
|
24
|
+
modal.setViewportRows(visible);
|
|
25
|
+
// DEBT-5 item 2: the list section wraps a row label at (contentW - 2),
|
|
26
|
+
// where contentW = boxW - 4 (see ModalFactory._renderListSection). Compute
|
|
27
|
+
// that exact column here so getRenderModel's wrap-clamp measures against
|
|
28
|
+
// reality instead of a fixed guess.
|
|
29
|
+
const boxW = Math.max(24, getOverlayMaxWidth(width, MODAL_MARGIN, MODAL_MAX_WIDTH));
|
|
30
|
+
const labelWrapWidth = Math.max(8, boxW - 4 - 2);
|
|
31
|
+
const model = modal.getRenderModel(labelWrapWidth);
|
|
32
|
+
return renderConfigModalModel(model, width);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Pure model→Line[] mapping — the shared inner step of renderConfigModal. */
|
|
36
|
+
function renderConfigModalModel(model: ConfigModalRenderModel, width: number): Line[] {
|
|
37
|
+
// Read the active-mode chrome tones per call (this render path is not on a
|
|
38
|
+
// hot loop; the modal renders once per keystroke).
|
|
39
|
+
const TONES = activeUiTones();
|
|
40
|
+
const sections: ModalSection[] = [];
|
|
41
|
+
|
|
42
|
+
if (model.degraded) {
|
|
43
|
+
sections.push({ type: 'text', content: `⚠ ${model.degraded}`, style: { fg: TONES.state.warn, bold: true } });
|
|
44
|
+
sections.push({ type: 'separator' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const line of model.header) {
|
|
48
|
+
sections.push({ type: 'text', content: line, style: { fg: TONES.fg.secondary } });
|
|
49
|
+
}
|
|
50
|
+
if (model.header.length > 0) sections.push({ type: 'separator' });
|
|
51
|
+
|
|
52
|
+
if (model.rows.length === 0) {
|
|
53
|
+
sections.push({ type: 'text', content: model.emptyText ?? 'Nothing to show.', style: { fg: TONES.fg.muted, dim: true } });
|
|
54
|
+
} else {
|
|
55
|
+
sections.push({
|
|
56
|
+
type: 'list',
|
|
57
|
+
items: model.rows.map((row) => ({
|
|
58
|
+
label: row.label,
|
|
59
|
+
selected: row.selected,
|
|
60
|
+
style: row.stale
|
|
61
|
+
? { fg: TONES.fg.muted, dim: true }
|
|
62
|
+
: row.style,
|
|
63
|
+
})),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const tabs: ModalTab[] | undefined = model.tabs.length > 1
|
|
68
|
+
? model.tabs.map((t) => ({ label: t.label, active: t.active }))
|
|
69
|
+
: undefined;
|
|
70
|
+
|
|
71
|
+
// Keep the content region a stable height so live-value ticks never change
|
|
72
|
+
// the box size: frozen header + separator + the fixed visible-row window.
|
|
73
|
+
const targetContentRows =
|
|
74
|
+
(model.degraded ? 2 : 0) +
|
|
75
|
+
model.header.length +
|
|
76
|
+
(model.header.length > 0 ? 1 : 0) +
|
|
77
|
+
model.scroll.visible;
|
|
78
|
+
|
|
79
|
+
const helpers = model.status
|
|
80
|
+
? [{ content: model.status, accent: true }]
|
|
81
|
+
: undefined;
|
|
82
|
+
|
|
83
|
+
return ModalFactory.createModal(
|
|
84
|
+
{
|
|
85
|
+
title: model.title,
|
|
86
|
+
width: MODAL_MAX_WIDTH,
|
|
87
|
+
tabs,
|
|
88
|
+
sections,
|
|
89
|
+
targetContentRows,
|
|
90
|
+
helpers,
|
|
91
|
+
hints: [...model.hints],
|
|
92
|
+
},
|
|
93
|
+
width,
|
|
94
|
+
);
|
|
95
|
+
}
|