@pellux/goodvibes-tui 0.28.0 → 1.0.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 +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import { createEmptyLine } from '../types/grid.ts';
|
|
2
3
|
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
5
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
4
6
|
import {
|
|
5
7
|
buildEmptyState,
|
|
6
8
|
buildGuidanceLine,
|
|
@@ -12,27 +14,41 @@ import {
|
|
|
12
14
|
} from './polish.ts';
|
|
13
15
|
import {
|
|
14
16
|
type EcosystemCatalogPathOptions,
|
|
17
|
+
installEcosystemCatalogEntry,
|
|
15
18
|
listInstalledEcosystemEntries,
|
|
16
19
|
loadEcosystemCatalog,
|
|
17
20
|
reviewEcosystemCatalogEntry,
|
|
21
|
+
uninstallEcosystemCatalogEntry,
|
|
18
22
|
type EcosystemCatalogEntry,
|
|
19
23
|
type EcosystemEntryKind,
|
|
20
24
|
} from '@/runtime/index.ts';
|
|
21
25
|
import type { UiMarketplaceSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
22
26
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
// Base chrome only — title band and text tokens come straight from
|
|
29
|
+
// DEFAULT_PANEL_PALETTE (WO-002).
|
|
30
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
31
|
+
|
|
32
|
+
type MarketplaceReview = ReturnType<typeof reviewEcosystemCatalogEntry>;
|
|
29
33
|
|
|
30
34
|
type MarketplaceRow = {
|
|
31
35
|
kind: EcosystemEntryKind;
|
|
32
36
|
entry: EcosystemCatalogEntry;
|
|
33
37
|
installed: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Computed once per refresh() (not per render()) so that render() never
|
|
40
|
+
* touches disk — see the class-level note on the render()/refresh() split.
|
|
41
|
+
*/
|
|
42
|
+
review: MarketplaceReview;
|
|
34
43
|
};
|
|
35
44
|
|
|
45
|
+
/** What a pending install/uninstall confirm will do once the user confirms. */
|
|
46
|
+
interface MarketplaceConfirmSubject {
|
|
47
|
+
readonly kind: EcosystemEntryKind;
|
|
48
|
+
readonly entryId: string;
|
|
49
|
+
readonly action: 'install' | 'uninstall';
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
function statusColor(installed: boolean): string {
|
|
37
53
|
return installed ? C.good : C.dim;
|
|
38
54
|
}
|
|
@@ -40,12 +56,16 @@ function statusColor(installed: boolean): string {
|
|
|
40
56
|
export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
|
|
41
57
|
private rows: MarketplaceRow[] = [];
|
|
42
58
|
private readonly unsub: (() => void) | null;
|
|
59
|
+
// I1: confirm state for install/uninstall (destructive-adjacent — mutates disk)
|
|
60
|
+
private confirm: ConfirmState<MarketplaceConfirmSubject> | null = null;
|
|
61
|
+
// Entry id whose full review detail is expanded (Enter toggles); cleared on refresh.
|
|
62
|
+
private expandedEntryId: string | null = null;
|
|
43
63
|
|
|
44
64
|
public constructor(
|
|
45
65
|
private readonly readModel?: UiReadModel<UiMarketplaceSnapshot>,
|
|
46
66
|
private readonly ecosystemPaths?: EcosystemCatalogPathOptions,
|
|
47
67
|
) {
|
|
48
|
-
super('marketplace', 'Marketplace', '
|
|
68
|
+
super('marketplace', 'Marketplace', '◩', 'automation-control');
|
|
49
69
|
this.filterEnabled = true;
|
|
50
70
|
this.filterLabel = 'Filter marketplace';
|
|
51
71
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
@@ -61,11 +81,19 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
|
|
|
61
81
|
this.unsub?.();
|
|
62
82
|
}
|
|
63
83
|
|
|
84
|
+
// Disk reload happens here (activate) and on explicit 'r' refresh — never
|
|
85
|
+
// inside render(), which used to reload all four catalogs every frame.
|
|
64
86
|
public override onActivate(): void {
|
|
65
87
|
super.onActivate();
|
|
66
88
|
this.refresh();
|
|
67
89
|
}
|
|
68
90
|
|
|
91
|
+
protected override onSelect(row: MarketplaceRow): void {
|
|
92
|
+
// Enter toggles the full review-detail block for the selected entry.
|
|
93
|
+
this.expandedEntryId = this.expandedEntryId === row.entry.id ? null : row.entry.id;
|
|
94
|
+
this.markDirty();
|
|
95
|
+
}
|
|
96
|
+
|
|
69
97
|
// ---------------------------------------------------------------------------
|
|
70
98
|
// ScrollableListPanel implementation
|
|
71
99
|
// ---------------------------------------------------------------------------
|
|
@@ -101,22 +129,118 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
|
|
|
101
129
|
];
|
|
102
130
|
}
|
|
103
131
|
|
|
132
|
+
public override handleInput(key: string): boolean {
|
|
133
|
+
if (this.lastError !== null) this.clearError();
|
|
134
|
+
|
|
135
|
+
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
136
|
+
if (confirmResult === 'confirmed') {
|
|
137
|
+
const subject = this.confirm!.subject;
|
|
138
|
+
this.confirm = null;
|
|
139
|
+
this._applyConfirmedAction(subject);
|
|
140
|
+
this.markDirty();
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (confirmResult === 'cancelled') {
|
|
144
|
+
this.confirm = null;
|
|
145
|
+
this.markDirty();
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
if (confirmResult === 'absorbed') return true;
|
|
149
|
+
|
|
150
|
+
if (!this.filterActive) {
|
|
151
|
+
if (key === 'r') {
|
|
152
|
+
this.refresh();
|
|
153
|
+
this.markDirty();
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// selectedIndex is relative to the filtered view, so the acted-on row
|
|
158
|
+
// must come from getVisibleItems() — this.rows would desync under '/'.
|
|
159
|
+
const selectedRow = this.getSelectedItem();
|
|
160
|
+
if (key === 'i' && selectedRow && !selectedRow.installed && this.ecosystemPaths) {
|
|
161
|
+
this.confirm = {
|
|
162
|
+
subject: { kind: selectedRow.kind, entryId: selectedRow.entry.id, action: 'install' },
|
|
163
|
+
label: `${selectedRow.kind} ${selectedRow.entry.name}`,
|
|
164
|
+
verb: 'Install',
|
|
165
|
+
};
|
|
166
|
+
this.markDirty();
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (key === 'u' && selectedRow && selectedRow.installed && this.ecosystemPaths) {
|
|
170
|
+
this.confirm = {
|
|
171
|
+
subject: { kind: selectedRow.kind, entryId: selectedRow.entry.id, action: 'uninstall' },
|
|
172
|
+
label: `${selectedRow.kind} ${selectedRow.entry.name}`,
|
|
173
|
+
verb: 'Uninstall',
|
|
174
|
+
};
|
|
175
|
+
this.markDirty();
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Recommendations become actionable jumps: pressing the digit shown next
|
|
180
|
+
// to a recommendation row jumps the selection to (and expands) that
|
|
181
|
+
// catalog entry, rather than only printing a static suggestion.
|
|
182
|
+
if (/^[1-9]$/.test(key)) {
|
|
183
|
+
const recommendations = this.readModel?.getSnapshot()?.recommendations ?? [];
|
|
184
|
+
const recommendation = recommendations[Number(key) - 1];
|
|
185
|
+
if (recommendation) {
|
|
186
|
+
// Search the visible (possibly filtered) list: selectedIndex is
|
|
187
|
+
// visible-relative, so a this.rows index would land on the wrong row.
|
|
188
|
+
const targetIndex = this.getVisibleItems().findIndex((row) => row.kind === recommendation.kind && row.entry.id === recommendation.entry.id);
|
|
189
|
+
if (targetIndex >= 0) {
|
|
190
|
+
this.selectedIndex = targetIndex;
|
|
191
|
+
this.expandedEntryId = recommendation.entry.id;
|
|
192
|
+
this.markDirty();
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return super.handleInput(key);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private _applyConfirmedAction(subject: MarketplaceConfirmSubject): void {
|
|
203
|
+
if (!this.ecosystemPaths) return;
|
|
204
|
+
const verb = subject.action === 'install' ? 'Install' : 'Uninstall';
|
|
205
|
+
try {
|
|
206
|
+
const result = subject.action === 'install'
|
|
207
|
+
? installEcosystemCatalogEntry(subject.kind, subject.entryId, this.ecosystemPaths)
|
|
208
|
+
: uninstallEcosystemCatalogEntry(subject.kind, subject.entryId, this.ecosystemPaths);
|
|
209
|
+
if (!result.ok) {
|
|
210
|
+
this.setError(`${verb} failed: ${result.error}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
this.clearError();
|
|
214
|
+
this.refresh();
|
|
215
|
+
} catch (e) {
|
|
216
|
+
this.setError(`${verb} failed: ${summarizeError(e)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
104
220
|
private refresh(): void {
|
|
105
221
|
if (!this.ecosystemPaths) {
|
|
106
222
|
this.rows = [];
|
|
107
223
|
this.clampSelection();
|
|
108
224
|
return;
|
|
109
225
|
}
|
|
226
|
+
const ecosystemPaths = this.ecosystemPaths;
|
|
110
227
|
try {
|
|
111
|
-
const installedPlugins = new Set(listInstalledEcosystemEntries('plugin',
|
|
112
|
-
const installedSkills = new Set(listInstalledEcosystemEntries('skill',
|
|
113
|
-
const installedHookPacks = new Set(listInstalledEcosystemEntries('hook-pack',
|
|
114
|
-
const installedPolicyPacks = new Set(listInstalledEcosystemEntries('policy-pack',
|
|
228
|
+
const installedPlugins = new Set(listInstalledEcosystemEntries('plugin', ecosystemPaths).map((receipt) => receipt.entry.id));
|
|
229
|
+
const installedSkills = new Set(listInstalledEcosystemEntries('skill', ecosystemPaths).map((receipt) => receipt.entry.id));
|
|
230
|
+
const installedHookPacks = new Set(listInstalledEcosystemEntries('hook-pack', ecosystemPaths).map((receipt) => receipt.entry.id));
|
|
231
|
+
const installedPolicyPacks = new Set(listInstalledEcosystemEntries('policy-pack', ecosystemPaths).map((receipt) => receipt.entry.id));
|
|
232
|
+
const build = (kind: EcosystemEntryKind, installed: Set<string>): MarketplaceRow[] =>
|
|
233
|
+
loadEcosystemCatalog(kind, ecosystemPaths).map((entry) => ({
|
|
234
|
+
kind,
|
|
235
|
+
entry,
|
|
236
|
+
installed: installed.has(entry.id),
|
|
237
|
+
review: reviewEcosystemCatalogEntry(entry, ecosystemPaths),
|
|
238
|
+
}));
|
|
115
239
|
const rows: MarketplaceRow[] = [
|
|
116
|
-
...
|
|
117
|
-
...
|
|
118
|
-
...
|
|
119
|
-
...
|
|
240
|
+
...build('plugin', installedPlugins),
|
|
241
|
+
...build('skill', installedSkills),
|
|
242
|
+
...build('hook-pack', installedHookPacks),
|
|
243
|
+
...build('policy-pack', installedPolicyPacks),
|
|
120
244
|
];
|
|
121
245
|
this.rows = rows.sort((a, b) => a.entry.name.localeCompare(b.entry.name));
|
|
122
246
|
this.clampSelection();
|
|
@@ -130,7 +254,16 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
|
|
|
130
254
|
|
|
131
255
|
public render(width: number, height: number): Line[] {
|
|
132
256
|
this.clampSelection();
|
|
133
|
-
|
|
257
|
+
|
|
258
|
+
if (this.confirm) {
|
|
259
|
+
const lines = buildPanelWorkspace(width, height, {
|
|
260
|
+
title: 'Marketplace Control Room',
|
|
261
|
+
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
|
|
262
|
+
palette: C,
|
|
263
|
+
});
|
|
264
|
+
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
265
|
+
return lines.slice(0, height);
|
|
266
|
+
}
|
|
134
267
|
|
|
135
268
|
const intro = 'Curated local-first ecosystem with provenance, compatibility, rollback history, and receipt-aware lifecycle review.';
|
|
136
269
|
const installedCount = this.rows.filter((row) => row.installed).length;
|
|
@@ -170,14 +303,16 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
|
|
|
170
303
|
{ label: 'hooks', value: String(this.rows.filter((row) => row.kind === 'hook-pack').length), valueColor: C.info },
|
|
171
304
|
{ label: 'policies', value: String(this.rows.filter((row) => row.kind === 'policy-pack').length), valueColor: C.info },
|
|
172
305
|
], C),
|
|
173
|
-
buildGuidanceLine(width, '/marketplace open', 'browse curated entries and inspect compatibility, provenance, and receipts', C),
|
|
174
306
|
];
|
|
175
307
|
|
|
308
|
+
// Recommendations become actionable jumps: each row is numbered and the
|
|
309
|
+
// matching digit key jumps the main selection straight to that entry
|
|
310
|
+
// (see handleInput) instead of only printing a static suggestion.
|
|
176
311
|
const recommendationLines = recommendations.length > 0
|
|
177
|
-
? recommendations.slice(0, 4).map((recommendation) => buildPanelLine(width, [
|
|
178
|
-
[
|
|
179
|
-
[fitDisplay(`${recommendation.kind} ${recommendation.entry.id}`,
|
|
180
|
-
[truncateDisplay(` ${recommendation.title}`, Math.max(0, width -
|
|
312
|
+
? recommendations.slice(0, 4).map((recommendation, index) => buildPanelLine(width, [
|
|
313
|
+
[` [${index + 1}] `, C.info],
|
|
314
|
+
[fitDisplay(`${recommendation.kind} ${recommendation.entry.id}`, 26), C.info],
|
|
315
|
+
[truncateDisplay(` ${recommendation.title}`, Math.max(0, width - 33)), C.dim],
|
|
181
316
|
]))
|
|
182
317
|
: [buildPanelLine(width, [[' No contextual marketplace recommendations right now.', C.dim]])];
|
|
183
318
|
|
|
@@ -185,10 +320,12 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
|
|
|
185
320
|
? startupIssues.slice(0, 4).map((issue) => buildPanelLine(width, [[' ', C.label], [truncateDisplay(issue, Math.max(0, width - 2)), C.warn]]))
|
|
186
321
|
: [buildPanelLine(width, [[' No startup or lifecycle issues are currently pushing marketplace repair recommendations.', C.dim]])];
|
|
187
322
|
|
|
188
|
-
const selectedRow = this.
|
|
323
|
+
const selectedRow = this.getSelectedItem();
|
|
189
324
|
const selectedLines: Line[] = [];
|
|
190
325
|
if (selectedRow) {
|
|
191
|
-
|
|
326
|
+
// review is computed once in refresh() (not here) so render() never
|
|
327
|
+
// touches disk.
|
|
328
|
+
const review = selectedRow.review;
|
|
192
329
|
selectedLines.push(buildPanelLine(width, [
|
|
193
330
|
[' Provenance: ', C.label],
|
|
194
331
|
[truncateDisplay(selectedRow.entry.provenance ?? '(none)', Math.max(0, width - 15)), selectedRow.entry.provenance ? C.info : C.dim],
|
|
@@ -202,16 +339,46 @@ export class MarketplacePanel extends ScrollableListPanel<MarketplaceRow> {
|
|
|
202
339
|
{ label: 'Risk', value: review.riskLevel, valueColor: review.riskLevel === 'low' ? C.good : C.warn },
|
|
203
340
|
{ label: 'State', value: selectedRow.installed ? 'installed' : 'curated', valueColor: statusColor(selectedRow.installed) },
|
|
204
341
|
], C));
|
|
205
|
-
|
|
342
|
+
|
|
343
|
+
if (this.expandedEntryId === selectedRow.entry.id) {
|
|
344
|
+
// Enter = full review detail: every field reviewEcosystemCatalogEntry
|
|
345
|
+
// returns, not just the compact compatibility/risk/state summary above.
|
|
346
|
+
selectedLines.push(buildPanelLine(width, [
|
|
347
|
+
[' Source path: ', C.label],
|
|
348
|
+
[truncateDisplay(review.sourcePath, Math.max(0, width - 16)), review.sourceExists ? C.good : C.warn],
|
|
349
|
+
]));
|
|
350
|
+
selectedLines.push(buildKeyValueLine(width, [
|
|
351
|
+
{ label: 'Source kind', value: review.sourceKind, valueColor: C.info },
|
|
352
|
+
{ label: 'Source exists', value: review.sourceExists ? 'yes' : 'no', valueColor: review.sourceExists ? C.good : C.warn },
|
|
353
|
+
{ label: 'Recommended scope', value: review.recommendedScope, valueColor: C.info },
|
|
354
|
+
], C));
|
|
355
|
+
selectedLines.push(buildPanelLine(width, [
|
|
356
|
+
[' Runtime fit: ', C.label],
|
|
357
|
+
[review.runtimeFit.status, review.runtimeFit.status === 'supported' ? C.good : C.warn],
|
|
358
|
+
[review.runtimeFit.reasons.length > 0 ? ` (${review.runtimeFit.reasons.join('; ')})` : '', C.dim],
|
|
359
|
+
]));
|
|
360
|
+
if (review.compatibility.reasons.length > 0) {
|
|
361
|
+
selectedLines.push(buildPanelLine(width, [
|
|
362
|
+
[' Compatibility notes: ', C.label],
|
|
363
|
+
[truncateDisplay(review.compatibility.reasons.join('; '), Math.max(0, width - 24)), C.warn],
|
|
364
|
+
]));
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
selectedLines.push(buildGuidanceLine(width, 'Enter', 'expand full compatibility, receipt, and runtime-fit detail for the selected entry', C));
|
|
368
|
+
}
|
|
206
369
|
}
|
|
207
370
|
|
|
208
|
-
// Context-aware hints: filter mode vs. browse mode (install only
|
|
209
|
-
//
|
|
371
|
+
// Context-aware hints: filter mode vs. browse mode (install/uninstall only
|
|
372
|
+
// make sense given the selected entry's current install state).
|
|
210
373
|
const hints = this.filterActive
|
|
211
374
|
? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
|
|
212
375
|
: [
|
|
213
376
|
{ keys: 'Up/Down', label: 'move' },
|
|
214
|
-
|
|
377
|
+
{ keys: 'Enter', label: 'detail' },
|
|
378
|
+
...(selectedRow && !selectedRow.installed ? [{ keys: 'i', label: 'install' }] : []),
|
|
379
|
+
...(selectedRow && selectedRow.installed ? [{ keys: 'u', label: 'uninstall' }] : []),
|
|
380
|
+
...(recommendations.length > 0 ? [{ keys: '1-9', label: 'jump to recommendation' }] : []),
|
|
381
|
+
{ keys: 'r', label: 'refresh' },
|
|
215
382
|
{ keys: '/', label: 'filter' },
|
|
216
383
|
];
|
|
217
384
|
|
|
@@ -13,31 +13,26 @@
|
|
|
13
13
|
import type { Line } from '../types/grid.ts';
|
|
14
14
|
import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
|
|
15
15
|
import type { MemoryClass, MemoryRecord, MemoryReviewState } from '@pellux/goodvibes-sdk/platform/state';
|
|
16
|
-
import { ScrollableListPanel
|
|
16
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
17
17
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
18
18
|
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
19
19
|
import {
|
|
20
20
|
buildBodyText,
|
|
21
|
-
buildGuidanceLine,
|
|
22
21
|
buildKeyValueLine,
|
|
23
22
|
buildPanelLine,
|
|
24
23
|
buildPanelWorkspace,
|
|
25
24
|
extendPalette,
|
|
26
25
|
DEFAULT_PANEL_PALETTE,
|
|
27
26
|
} from './polish.ts';
|
|
28
|
-
import {
|
|
29
|
-
getPanelSearchFocusTransition,
|
|
30
|
-
isPanelSearchCancel,
|
|
31
|
-
} from './search-focus.ts';
|
|
32
27
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
33
28
|
|
|
34
29
|
// ---------------------------------------------------------------------------
|
|
35
30
|
// Colour palette
|
|
36
31
|
// ---------------------------------------------------------------------------
|
|
37
32
|
|
|
33
|
+
// Domain accents only — one hue per memory class (base chrome: header/
|
|
34
|
+
// headerBg/selectBg come from DEFAULT_PANEL_PALETTE).
|
|
38
35
|
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
39
|
-
header: '#94a3b8',
|
|
40
|
-
headerBg: '#1e293b',
|
|
41
36
|
decision: '#38bdf8',
|
|
42
37
|
constraint: '#f97316',
|
|
43
38
|
incident: '#ef4444',
|
|
@@ -47,9 +42,6 @@ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
|
47
42
|
runbook: '#eab308',
|
|
48
43
|
architecture: '#60a5fa',
|
|
49
44
|
ownership: '#14b8a6',
|
|
50
|
-
selected: '#1e3a5f',
|
|
51
|
-
searchBg: '#0f172a',
|
|
52
|
-
searchFg: '#e2e8f0',
|
|
53
45
|
});
|
|
54
46
|
|
|
55
47
|
// ---------------------------------------------------------------------------
|
|
@@ -104,14 +96,13 @@ function formatConfidence(confidence: number): string {
|
|
|
104
96
|
// MemoryPanel
|
|
105
97
|
// ---------------------------------------------------------------------------
|
|
106
98
|
|
|
107
|
-
export class MemoryPanel extends
|
|
99
|
+
export class MemoryPanel extends ScrollableListPanel<MemoryRecord> {
|
|
108
100
|
private readonly registry: MemoryRegistry;
|
|
109
101
|
private filterMode: FilterMode = 'all';
|
|
110
|
-
private filterFocused = false;
|
|
111
102
|
private unsubscribe?: () => void;
|
|
112
103
|
|
|
113
|
-
//
|
|
114
|
-
private confirm: ConfirmState<{ id: string; action: 'stale' | 'contradicted' }> | null = null;
|
|
104
|
+
// Confirm state (for destructive stale/contradict/delete actions)
|
|
105
|
+
private confirm: ConfirmState<{ id: string; action: 'stale' | 'contradicted' | 'delete' }> | null = null;
|
|
115
106
|
|
|
116
107
|
// Cached records for review-mode (reviewQueue-first, same as former KnowledgePanel)
|
|
117
108
|
private reviewRecords: MemoryRecord[] = [];
|
|
@@ -119,6 +110,10 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
119
110
|
constructor(registry: MemoryRegistry) {
|
|
120
111
|
super('memory', 'Memory', 'M', 'agent');
|
|
121
112
|
this.registry = registry;
|
|
113
|
+
// WO-153: converged modal '/' filter — only active in 'all' mode; review
|
|
114
|
+
// mode uses r/s/c/f/d single-letter actions instead of text filtering.
|
|
115
|
+
this.filterEnabled = true;
|
|
116
|
+
this.filterLabel = 'Search';
|
|
122
117
|
}
|
|
123
118
|
|
|
124
119
|
// ---------------------------------------------------------------------------
|
|
@@ -127,13 +122,11 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
127
122
|
|
|
128
123
|
override onActivate(): void {
|
|
129
124
|
super.onActivate();
|
|
130
|
-
this.
|
|
131
|
-
this.
|
|
132
|
-
this.filterFocused = false;
|
|
125
|
+
this.filterQuery = '';
|
|
126
|
+
this.filterActive = false;
|
|
133
127
|
this.confirm = null;
|
|
134
128
|
this.refreshReviewRecords();
|
|
135
129
|
this.unsubscribe = this.registry.subscribe(() => {
|
|
136
|
-
this.invalidateFilter();
|
|
137
130
|
this.refreshReviewRecords();
|
|
138
131
|
this.markDirty();
|
|
139
132
|
});
|
|
@@ -149,16 +142,11 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
149
142
|
}
|
|
150
143
|
|
|
151
144
|
// ---------------------------------------------------------------------------
|
|
152
|
-
//
|
|
145
|
+
// ScrollableListPanel implementation (filter is only enabled in 'all' mode)
|
|
153
146
|
// ---------------------------------------------------------------------------
|
|
154
147
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
protected matchesSearch(record: MemoryRecord, query: string): boolean {
|
|
160
|
-
const q = query.trim().toLowerCase();
|
|
161
|
-
if (!q) return true;
|
|
148
|
+
/** `q` arrives already trimmed + lower-cased from ScrollableListPanel.getVisibleItems(). */
|
|
149
|
+
protected override filterMatches(record: MemoryRecord, q: string): boolean {
|
|
162
150
|
const haystack = [
|
|
163
151
|
record.summary,
|
|
164
152
|
record.detail ?? '',
|
|
@@ -181,7 +169,7 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
181
169
|
]);
|
|
182
170
|
}
|
|
183
171
|
// All-mode row: scope/class + id + time + summary (matches former MemoryPanel row)
|
|
184
|
-
const bg = selected ? C.
|
|
172
|
+
const bg = selected ? C.selectBg : undefined;
|
|
185
173
|
return buildPanelLine(width, [
|
|
186
174
|
[' ', C.label, bg],
|
|
187
175
|
[`[${record.scope.slice(0, 1).toUpperCase()}/${record.cls.slice(0, 3).toUpperCase()}] `, classColor(record.cls), bg],
|
|
@@ -197,9 +185,9 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
197
185
|
|
|
198
186
|
protected override getEmptyStateMessage() {
|
|
199
187
|
if (this.filterMode === 'review') return 'No records in the review queue.';
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
188
|
+
// The filter-narrowed-to-empty case is handled by ScrollableListPanel's
|
|
189
|
+
// own 'No matches for "..."' message (renderList) before this is reached.
|
|
190
|
+
return ' No memory records. Use /recall add <class> <summary> to create one.';
|
|
203
191
|
}
|
|
204
192
|
|
|
205
193
|
protected override getEmptyStateActions() {
|
|
@@ -215,14 +203,15 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
215
203
|
// ---------------------------------------------------------------------------
|
|
216
204
|
|
|
217
205
|
/**
|
|
218
|
-
*
|
|
219
|
-
* clampSelection, navigation bounds
|
|
220
|
-
*
|
|
221
|
-
*
|
|
206
|
+
* getItems() so that ScrollableListPanel infrastructure (renderList,
|
|
207
|
+
* clampSelection, navigation bounds, and — in 'all' mode — the modal '/'
|
|
208
|
+
* filter) all operate on the right data set per mode — preventing the
|
|
209
|
+
* render/action index desync where rendered list index N did not
|
|
210
|
+
* correspond to reviewRecords[N] when the two lists differ.
|
|
222
211
|
*/
|
|
223
|
-
protected
|
|
212
|
+
protected getItems(): readonly MemoryRecord[] {
|
|
224
213
|
if (this.filterMode === 'review') return this.reviewRecords;
|
|
225
|
-
return
|
|
214
|
+
return this.registry.search({ limit: 100 });
|
|
226
215
|
}
|
|
227
216
|
|
|
228
217
|
private refreshReviewRecords(): void {
|
|
@@ -230,11 +219,19 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
230
219
|
this.clampSelection();
|
|
231
220
|
}
|
|
232
221
|
|
|
222
|
+
/** Count of records beyond the 100-record fetch cap in getItems(). */
|
|
223
|
+
private getHiddenCount(): number {
|
|
224
|
+
const total = this.registry.search({}).length;
|
|
225
|
+
return Math.max(0, total - 100);
|
|
226
|
+
}
|
|
227
|
+
|
|
233
228
|
private cycleFilter(): void {
|
|
234
229
|
const modes: FilterMode[] = ['all', 'review'];
|
|
235
230
|
const next = modes[(modes.indexOf(this.filterMode) + 1) % modes.length];
|
|
236
231
|
this.filterMode = next;
|
|
237
|
-
|
|
232
|
+
// The modal '/' filter only applies in 'all' mode — review mode uses
|
|
233
|
+
// r/s/c/f/d single-letter actions instead of text filtering.
|
|
234
|
+
this.filterEnabled = next === 'all';
|
|
238
235
|
this.refreshReviewRecords();
|
|
239
236
|
this.markDirty();
|
|
240
237
|
}
|
|
@@ -250,6 +247,16 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
250
247
|
if (result === 'confirmed') {
|
|
251
248
|
const { id, action } = this.confirm.subject;
|
|
252
249
|
this.confirm = null;
|
|
250
|
+
if (action === 'delete') {
|
|
251
|
+
try {
|
|
252
|
+
this.registry.delete(id);
|
|
253
|
+
} catch (e) {
|
|
254
|
+
this.setError(`Delete failed: ${summarizeError(e)}`);
|
|
255
|
+
}
|
|
256
|
+
this.refreshReviewRecords();
|
|
257
|
+
this.markDirty();
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
253
260
|
const record = this.reviewRecords.find((r) => r.id === id);
|
|
254
261
|
if (record) {
|
|
255
262
|
try {
|
|
@@ -292,7 +299,9 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
292
299
|
|
|
293
300
|
// Review-mode specific actions (r/s/c/f)
|
|
294
301
|
if (this.filterMode === 'review') {
|
|
295
|
-
|
|
302
|
+
// In review mode getItems() returns reviewRecords and filterEnabled is
|
|
303
|
+
// false, so getSelectedItem() resolves the highlighted review record.
|
|
304
|
+
const selected = this.getSelectedItem();
|
|
296
305
|
|
|
297
306
|
if (key === 'enter' || key === 'return' || key === 'r') {
|
|
298
307
|
if (!selected) return false;
|
|
@@ -328,46 +337,35 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
328
337
|
this.markDirty();
|
|
329
338
|
return true;
|
|
330
339
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const items = this.getItems();
|
|
337
|
-
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
|
|
338
|
-
if (transition === 'focus-list') {
|
|
339
|
-
this.filterFocused = false;
|
|
340
|
-
this.markDirty();
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
|
-
if (isPanelSearchCancel(key)) {
|
|
344
|
-
this.filterFocused = false;
|
|
345
|
-
return super.handleInput(key);
|
|
346
|
-
}
|
|
347
|
-
return super.handleInput(key);
|
|
340
|
+
if (key === 'd') {
|
|
341
|
+
if (!selected) return false;
|
|
342
|
+
this.confirm = { subject: { id: selected.id, action: 'delete' }, label: truncateDisplay(selected.summary, 40) };
|
|
343
|
+
this.markDirty();
|
|
344
|
+
return true;
|
|
348
345
|
}
|
|
346
|
+
}
|
|
349
347
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
348
|
+
// All-mode: 'r' reload and 'd' delete are single-letter actions that
|
|
349
|
+
// coexist with the modal '/' filter — they only fire outside filter
|
|
350
|
+
// mode, so they remain typeable into the filter query while it's active.
|
|
351
|
+
if (this.filterMode === 'all' && !this.filterActive) {
|
|
352
|
+
if (key === 'r') {
|
|
354
353
|
this.markDirty();
|
|
355
354
|
return true;
|
|
356
355
|
}
|
|
357
356
|
|
|
358
|
-
if (key === '
|
|
359
|
-
this.
|
|
357
|
+
if (key === 'd') {
|
|
358
|
+
const selected = this.getSelectedItem();
|
|
359
|
+
if (!selected) return false;
|
|
360
|
+
this.confirm = { subject: { id: selected.id, action: 'delete' }, label: truncateDisplay(selected.summary, 40) };
|
|
360
361
|
this.markDirty();
|
|
361
362
|
return true;
|
|
362
363
|
}
|
|
363
364
|
}
|
|
364
365
|
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
if (this.filterMode === 'review') {
|
|
369
|
-
return ScrollableListPanel.prototype.handleInput.call(this, key);
|
|
370
|
-
}
|
|
366
|
+
// Navigation + (in 'all' mode) the modal '/' filter: delegate to
|
|
367
|
+
// ScrollableListPanel. filterEnabled is false in review mode, so '/' is
|
|
368
|
+
// inert there and single-char keys like 'j'/'k' navigate normally.
|
|
371
369
|
return super.handleInput(key);
|
|
372
370
|
}
|
|
373
371
|
|
|
@@ -403,13 +401,16 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
403
401
|
}
|
|
404
402
|
|
|
405
403
|
private renderAllMode(width: number, height: number, filterToggleLine: Line): Line[] {
|
|
406
|
-
|
|
404
|
+
// getVisibleItems() applies the modal '/' filter (getItems() is the
|
|
405
|
+
// unfiltered 100-cap fetch); the filter input line itself is
|
|
406
|
+
// auto-injected by renderList() below (filterEnabled=true in 'all' mode).
|
|
407
|
+
const records = this.getVisibleItems();
|
|
407
408
|
const byClass = new Map<MemoryClass, number>();
|
|
408
409
|
for (const record of records) {
|
|
409
410
|
byClass.set(record.cls, (byClass.get(record.cls) ?? 0) + 1);
|
|
410
411
|
}
|
|
411
412
|
|
|
412
|
-
const
|
|
413
|
+
const hiddenCount = this.getHiddenCount();
|
|
413
414
|
|
|
414
415
|
const summaryLines: Line[] = [
|
|
415
416
|
buildKeyValueLine(width, [
|
|
@@ -420,11 +421,12 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
420
421
|
{ label: 'runbooks', value: String(byClass.get('runbook') ?? 0), valueColor: C.runbook },
|
|
421
422
|
], C),
|
|
422
423
|
filterToggleLine,
|
|
423
|
-
|
|
424
|
-
|
|
424
|
+
...(hiddenCount > 0
|
|
425
|
+
? [buildPanelLine(width, [[` +${hiddenCount} hidden (100-record cap)`, C.dim]])]
|
|
426
|
+
: []),
|
|
425
427
|
];
|
|
426
428
|
|
|
427
|
-
const selected =
|
|
429
|
+
const selected = this.getSelectedItem();
|
|
428
430
|
const selectedLines: Line[] = [];
|
|
429
431
|
if (selected) {
|
|
430
432
|
selectedLines.push(buildKeyValueLine(width, [
|
|
@@ -450,7 +452,7 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
450
452
|
header: summaryLines,
|
|
451
453
|
footer: [
|
|
452
454
|
...selectedLines,
|
|
453
|
-
buildPanelLine(width, [[' / search j/k or Up/Down move r reload Tab: Review Queue Esc clear search', C.dim]]),
|
|
455
|
+
buildPanelLine(width, [[' / search j/k or Up/Down move r reload d delete Tab: Review Queue Esc clear search', C.dim]]),
|
|
454
456
|
],
|
|
455
457
|
});
|
|
456
458
|
}
|
|
@@ -500,10 +502,9 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
500
502
|
[' team ', C.label], [String(byScope.get('team') ?? 0), C.good],
|
|
501
503
|
]),
|
|
502
504
|
filterToggleLine,
|
|
503
|
-
buildGuidanceLine(width, '/recall review', 'work the stale and contradicted queue from the command surface', C),
|
|
504
505
|
];
|
|
505
506
|
|
|
506
|
-
const selectedRecord = this.
|
|
507
|
+
const selectedRecord = this.getSelectedItem();
|
|
507
508
|
const selectedLines: Line[] = [];
|
|
508
509
|
if (selectedRecord) {
|
|
509
510
|
selectedLines.push(buildPanelLine(width, [[' Selected', C.label]]));
|
|
@@ -550,7 +551,7 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
|
550
551
|
header: [...classLines, ...reviewLines],
|
|
551
552
|
footer: [
|
|
552
553
|
...(selectedLines.length > 0 ? selectedLines : []),
|
|
553
|
-
buildPanelLine(width, [[' Tab: All Records Up/Down move r/Enter reviewed s stale c contradicted f fresh', C.dim]]),
|
|
554
|
+
buildPanelLine(width, [[' Tab: All Records Up/Down move r/Enter reviewed s stale c contradicted f fresh d delete', C.dim]]),
|
|
554
555
|
],
|
|
555
556
|
});
|
|
556
557
|
}
|