@pellux/goodvibes-tui 0.18.19 → 0.18.23
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 +170 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-rendering.ts +20 -6
- package/src/input/commands/session.ts +0 -1
- package/src/input/feed-context-factory.ts +236 -0
- package/src/input/handler-feed-routes.ts +10 -0
- package/src/input/handler-feed.ts +44 -6
- package/src/input/handler-shortcuts.ts +138 -125
- package/src/input/handler.ts +121 -119
- package/src/input/keybindings.ts +30 -0
- package/src/panels/approval-panel.ts +54 -74
- package/src/panels/automation-control-panel.ts +119 -161
- package/src/panels/base-panel.ts +71 -0
- package/src/panels/communication-panel.ts +68 -107
- package/src/panels/confirm-state.ts +61 -0
- package/src/panels/control-plane-panel.ts +116 -172
- package/src/panels/git-panel.ts +9 -0
- package/src/panels/hooks-panel.ts +101 -138
- package/src/panels/incident-review-panel.ts +55 -107
- package/src/panels/knowledge-panel.ts +63 -14
- package/src/panels/local-auth-panel.ts +76 -93
- package/src/panels/marketplace-panel.ts +19 -12
- package/src/panels/mcp-panel.ts +108 -155
- package/src/panels/ops-control-panel.ts +50 -85
- package/src/panels/panel-manager.ts +22 -2
- package/src/panels/plugins-panel.ts +36 -60
- package/src/panels/routes-panel.ts +89 -141
- package/src/panels/scrollable-list-panel.ts +71 -16
- package/src/panels/security-panel.ts +101 -137
- package/src/panels/services-panel.ts +58 -102
- package/src/panels/settings-sync-panel.ts +76 -122
- package/src/panels/skills-panel.ts +44 -0
- package/src/panels/subscription-panel.ts +69 -80
- package/src/panels/tasks-panel.ts +129 -179
- package/src/panels/watchers-panel.ts +88 -137
- package/src/renderer/buffer.ts +11 -0
- package/src/renderer/diff.ts +8 -0
- package/src/renderer/help-overlay.ts +37 -28
- package/src/renderer/markdown.ts +3 -145
- package/src/renderer/status-token.ts +71 -0
- package/src/version.ts +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { BasePanel } from './base-panel.ts';
|
|
3
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
3
4
|
import type { MemoryClass, MemoryRecord, MemoryRegistry, MemoryReviewState } from '@pellux/goodvibes-sdk/platform/state/memory-store';
|
|
4
5
|
import {
|
|
5
6
|
buildBodyText,
|
|
@@ -47,6 +48,8 @@ export class KnowledgePanel extends BasePanel {
|
|
|
47
48
|
private selectedIndex = 0;
|
|
48
49
|
private scrollOffset = 0;
|
|
49
50
|
private records: MemoryRecord[] = [];
|
|
51
|
+
// I1: confirm for destructive review-state mutations
|
|
52
|
+
private confirm: ConfirmState<{ id: string; action: 'stale' | 'contradicted' }> | null = null;
|
|
50
53
|
|
|
51
54
|
public constructor(registry: MemoryRegistry) {
|
|
52
55
|
super('knowledge', 'Knowledge', 'K', 'agent');
|
|
@@ -72,6 +75,50 @@ export class KnowledgePanel extends BasePanel {
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
public handleInput(key: string): boolean {
|
|
78
|
+
// I1: y/n confirm for stale/contradict
|
|
79
|
+
if (this.confirm) {
|
|
80
|
+
const result = handleConfirmInput(this.confirm, key);
|
|
81
|
+
if (result === 'confirmed') {
|
|
82
|
+
const { id, action } = this.confirm.subject;
|
|
83
|
+
this.confirm = null;
|
|
84
|
+
const selected = this.records.find((r) => r.id === id);
|
|
85
|
+
if (selected) {
|
|
86
|
+
try {
|
|
87
|
+
if (action === 'stale') {
|
|
88
|
+
this.registry.review(id, {
|
|
89
|
+
state: 'stale',
|
|
90
|
+
confidence: Math.min(selected.confidence, 40),
|
|
91
|
+
reviewedBy: 'operator',
|
|
92
|
+
staleReason: 'marked stale from the knowledge panel',
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
this.registry.review(id, {
|
|
96
|
+
state: 'contradicted',
|
|
97
|
+
confidence: 0,
|
|
98
|
+
reviewedBy: 'operator',
|
|
99
|
+
staleReason: 'marked contradicted from the knowledge panel',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
// I2: surface async failure
|
|
104
|
+
this.setError(`Review update failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
this.refresh();
|
|
108
|
+
this.markDirty();
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (result === 'cancelled') {
|
|
112
|
+
this.confirm = null;
|
|
113
|
+
this.markDirty();
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (result === 'absorbed') return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// I2: auto-clear error on next keypress
|
|
120
|
+
if (this.lastError) this.clearError();
|
|
121
|
+
|
|
75
122
|
if (this.records.length === 0) return false;
|
|
76
123
|
if (key === 'ArrowUp' || key === 'k') {
|
|
77
124
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
@@ -98,24 +145,14 @@ export class KnowledgePanel extends BasePanel {
|
|
|
98
145
|
return true;
|
|
99
146
|
}
|
|
100
147
|
if (key === 's') {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
confidence: Math.min(selected.confidence, 40),
|
|
104
|
-
reviewedBy: 'operator',
|
|
105
|
-
staleReason: 'marked stale from the knowledge panel',
|
|
106
|
-
});
|
|
107
|
-
this.refresh();
|
|
148
|
+
// I1: prompt confirm before marking stale
|
|
149
|
+
this.confirm = { subject: { id: selected.id, action: 'stale' }, label: selected.summary.slice(0, 40) };
|
|
108
150
|
this.markDirty();
|
|
109
151
|
return true;
|
|
110
152
|
}
|
|
111
153
|
if (key === 'c') {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
confidence: 0,
|
|
115
|
-
reviewedBy: 'operator',
|
|
116
|
-
staleReason: 'marked contradicted from the knowledge panel',
|
|
117
|
-
});
|
|
118
|
-
this.refresh();
|
|
154
|
+
// I1: prompt confirm before marking contradicted
|
|
155
|
+
this.confirm = { subject: { id: selected.id, action: 'contradicted' }, label: selected.summary.slice(0, 40) };
|
|
119
156
|
this.markDirty();
|
|
120
157
|
return true;
|
|
121
158
|
}
|
|
@@ -141,6 +178,18 @@ export class KnowledgePanel extends BasePanel {
|
|
|
141
178
|
|
|
142
179
|
public render(width: number, height: number): Line[] {
|
|
143
180
|
this.needsRender = false;
|
|
181
|
+
|
|
182
|
+
// I1: show confirm dialog in place of normal content
|
|
183
|
+
if (this.confirm) {
|
|
184
|
+
return buildPanelWorkspace(width, height, {
|
|
185
|
+
title: 'Knowledge Control Room',
|
|
186
|
+
intro: '',
|
|
187
|
+
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
|
|
188
|
+
footerLines: [buildPanelLine(width, [[' y confirm n / Esc cancel', C.dim]])],
|
|
189
|
+
palette: C,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
144
193
|
if (this.records.length === 0) this.refresh();
|
|
145
194
|
|
|
146
195
|
const intro = 'Typed project knowledge, reviewed evidence, and operator-governed memory across session, project, and team scopes.';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import {
|
|
3
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
4
|
import {
|
|
5
5
|
buildDetailBlock,
|
|
6
6
|
buildGuidanceLine,
|
|
@@ -9,8 +9,7 @@ import {
|
|
|
9
9
|
buildSummaryBlock,
|
|
10
10
|
buildPanelWorkspace,
|
|
11
11
|
DEFAULT_PANEL_PALETTE,
|
|
12
|
-
|
|
13
|
-
type PanelWorkspaceSection,
|
|
12
|
+
type PanelPalette,
|
|
14
13
|
} from './polish.ts';
|
|
15
14
|
import type { LocalAuthSnapshot } from '@pellux/goodvibes-sdk/platform/security/user-auth';
|
|
16
15
|
import type { LocalAuthInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
@@ -27,9 +26,9 @@ function formatRoles(roles: readonly string[]): string {
|
|
|
27
26
|
return roles.length > 0 ? roles.join(', ') : '(none)';
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
type LocalAuthUser = LocalAuthSnapshot['users'][number];
|
|
30
|
+
|
|
31
|
+
export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
33
32
|
private readonly authManager: LocalAuthInspectionQuery;
|
|
34
33
|
|
|
35
34
|
public constructor(authManager: LocalAuthInspectionQuery) {
|
|
@@ -37,110 +36,94 @@ export class LocalAuthPanel extends BasePanel {
|
|
|
37
36
|
this.authManager = authManager;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
39
|
+
protected override getPalette(): PanelPalette {
|
|
40
|
+
return C;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
protected getItems(): readonly LocalAuthUser[] {
|
|
44
|
+
return this.authManager.inspect().users;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected renderItem(user: LocalAuthUser, _index: number, selected: boolean, width: number): Line {
|
|
48
|
+
return buildPanelListRow(width, [
|
|
49
|
+
{ text: user.username.padEnd(20), fg: C.value },
|
|
50
|
+
{ text: ` roles=${formatRoles(user.roles)}`.slice(0, Math.max(0, width - 24)), fg: C.info },
|
|
51
|
+
], C, { selected });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected override getEmptyStateMessage(): string {
|
|
55
|
+
return ' No local auth users configured.';
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
public render(width: number, height: number): Line[] {
|
|
57
|
-
this.needsRender = false;
|
|
58
59
|
const intro = 'Manage local daemon and HTTP-listener auth users, bootstrap state, and active sessions.';
|
|
59
|
-
const footerLines = [buildPanelLine(width, [[' /auth local review /auth local add-user /auth local rotate-password /auth local revoke-session ', C.dim]])];
|
|
60
60
|
const snapshot = this.authManager.inspect();
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const users = this.getItems();
|
|
62
|
+
|
|
63
63
|
const issueMessages: string[] = [];
|
|
64
64
|
if (snapshot.bootstrapCredentialPresent) issueMessages.push('Bootstrap credential file still exists and should be cleared after password rotation.');
|
|
65
65
|
if (snapshot.userCount <= 1) issueMessages.push('Only one local auth user is configured.');
|
|
66
66
|
if (snapshot.sessionCount === 0) issueMessages.push('No active local auth sessions are currently tracked.');
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
},
|
|
67
|
+
|
|
68
|
+
const headerLines: Line[] = [
|
|
69
|
+
...buildSummaryBlock(width, 'Local auth posture', [
|
|
70
|
+
buildPanelLine(width, [
|
|
71
|
+
[' users ', C.label],
|
|
72
|
+
[String(snapshot.userCount), C.value],
|
|
73
|
+
[' sessions ', C.label],
|
|
74
|
+
[String(snapshot.sessionCount), snapshot.sessionCount > 0 ? C.info : C.dim],
|
|
75
|
+
[' bootstrap ', C.label],
|
|
76
|
+
[snapshot.bootstrapCredentialPresent ? 'present' : 'cleared', snapshot.bootstrapCredentialPresent ? C.warn : C.good],
|
|
77
|
+
]),
|
|
78
|
+
buildPanelLine(width, [[' user store ', C.label], [snapshot.userStorePath.slice(0, Math.max(0, width - 13)), C.dim]]),
|
|
79
|
+
buildPanelLine(width, [[' bootstrap file ', C.label], [snapshot.bootstrapCredentialPath.slice(0, Math.max(0, width - 18)), C.dim]]),
|
|
80
|
+
...(issueMessages.length > 0
|
|
81
|
+
? issueMessages.map((issue) => buildPanelLine(width, [[` issue: ${issue}`.slice(0, Math.max(0, width)), C.warn]]))
|
|
82
|
+
: [buildPanelLine(width, [[' local auth posture looks healthy.', C.good]])]),
|
|
83
|
+
buildGuidanceLine(width, '/auth local rotate-password <user> <password>', 'rotate bootstrap/default credentials and revoke older sessions as needed', C),
|
|
84
|
+
], C),
|
|
86
85
|
];
|
|
87
86
|
|
|
88
|
-
if (
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
lines: buildDetailBlock(width, 'Selected user', [
|
|
92
|
-
buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [formatRoles(selected.roles).slice(0, Math.max(0, width - 23)), C.info]]),
|
|
93
|
-
buildPanelLine(width, [[` next: /auth local rotate-password ${selected.username} <password>`.slice(0, Math.max(0, width)), C.dim]]),
|
|
94
|
-
buildPanelLine(width, [[` next: /auth local delete-user ${selected.username}`.slice(0, Math.max(0, width)), C.dim]]),
|
|
95
|
-
], C),
|
|
96
|
-
}
|
|
97
|
-
: null;
|
|
98
|
-
const activeSessionsSection: PanelWorkspaceSection | null = snapshot.sessions.length > 0
|
|
99
|
-
? {
|
|
100
|
-
title: 'Active Sessions',
|
|
101
|
-
lines: snapshot.sessions.slice(0, 8).map((session) => buildPanelLine(width, [
|
|
102
|
-
[' ', C.label],
|
|
103
|
-
[session.username.padEnd(18), C.value],
|
|
104
|
-
[` expires ${new Date(session.expiresAt).toLocaleString()}`.slice(0, Math.max(0, width - 20)), C.dim],
|
|
105
|
-
])),
|
|
106
|
-
}
|
|
107
|
-
: null;
|
|
108
|
-
const rawUserLines: Line[] = snapshot.users.map((user, absolute) => {
|
|
109
|
-
return buildPanelListRow(width, [
|
|
110
|
-
{ text: user.username.padEnd(20), fg: C.value },
|
|
111
|
-
{ text: ` roles=${formatRoles(user.roles)}`.slice(0, Math.max(0, width - 24)), fg: C.info },
|
|
112
|
-
], C, { selected: absolute === this.selectedIndex });
|
|
113
|
-
});
|
|
114
|
-
const resolvedUsersSection = resolvePrimaryScrollableSection(width, height, {
|
|
87
|
+
if (users.length === 0) {
|
|
88
|
+
const workspace = buildPanelWorkspace(width, height, {
|
|
89
|
+
title: 'Local Auth Control Room',
|
|
115
90
|
intro,
|
|
116
|
-
|
|
91
|
+
sections: [{ lines: headerLines }],
|
|
117
92
|
palette: C,
|
|
118
|
-
beforeSections: sections,
|
|
119
|
-
section: {
|
|
120
|
-
title: 'Users',
|
|
121
|
-
scrollableLines: rawUserLines,
|
|
122
|
-
selectedIndex: this.selectedIndex,
|
|
123
|
-
scrollOffset: this.scrollOffset,
|
|
124
|
-
guardRows: 1,
|
|
125
|
-
minRows: 4,
|
|
126
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
127
|
-
},
|
|
128
|
-
afterSections: [selectedUserSection, activeSessionsSection].filter(Boolean) as PanelWorkspaceSection[],
|
|
129
93
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
94
|
+
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
95
|
+
return workspace;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.clampSelection();
|
|
99
|
+
const selected = users[this.selectedIndex];
|
|
100
|
+
|
|
101
|
+
const footerLines: Line[] = [];
|
|
102
|
+
if (selected) {
|
|
103
|
+
footerLines.push(
|
|
104
|
+
...buildDetailBlock(width, 'Selected user', [
|
|
105
|
+
buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [formatRoles(selected.roles).slice(0, Math.max(0, width - 23)), C.info]]),
|
|
106
|
+
buildPanelLine(width, [[` next: /auth local rotate-password ${selected.username} <password>`.slice(0, Math.max(0, width)), C.dim]]),
|
|
107
|
+
buildPanelLine(width, [[` next: /auth local delete-user ${selected.username}`.slice(0, Math.max(0, width)), C.dim]]),
|
|
108
|
+
], C),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (snapshot.sessions.length > 0) {
|
|
113
|
+
footerLines.push(
|
|
114
|
+
...snapshot.sessions.slice(0, 8).map((session) => buildPanelLine(width, [
|
|
115
|
+
[' ', C.label],
|
|
116
|
+
[session.username.padEnd(18), C.value],
|
|
117
|
+
[` expires ${new Date(session.expiresAt).toLocaleString()}`.slice(0, Math.max(0, width - 20)), C.dim],
|
|
118
|
+
])),
|
|
119
|
+
);
|
|
134
120
|
}
|
|
121
|
+
footerLines.push(buildPanelLine(width, [[' /auth local review /auth local add-user /auth local rotate-password /auth local revoke-session ', C.dim]]));
|
|
135
122
|
|
|
136
|
-
|
|
123
|
+
return this.renderList(width, height, {
|
|
137
124
|
title: 'Local Auth Control Room',
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
footerLines,
|
|
141
|
-
palette: C,
|
|
125
|
+
header: headerLines,
|
|
126
|
+
footer: footerLines,
|
|
142
127
|
});
|
|
143
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
144
|
-
return lines.slice(0, height);
|
|
145
128
|
}
|
|
146
129
|
}
|
|
@@ -81,18 +81,25 @@ export class MarketplacePanel extends BasePanel {
|
|
|
81
81
|
this.scrollOffset = 0;
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
try {
|
|
85
|
+
const installedPlugins = new Set(listInstalledEcosystemEntries('plugin', this.ecosystemPaths).map((receipt) => receipt.entry.id));
|
|
86
|
+
const installedSkills = new Set(listInstalledEcosystemEntries('skill', this.ecosystemPaths).map((receipt) => receipt.entry.id));
|
|
87
|
+
const installedHookPacks = new Set(listInstalledEcosystemEntries('hook-pack', this.ecosystemPaths).map((receipt) => receipt.entry.id));
|
|
88
|
+
const installedPolicyPacks = new Set(listInstalledEcosystemEntries('policy-pack', this.ecosystemPaths).map((receipt) => receipt.entry.id));
|
|
89
|
+
const rows: MarketplaceRow[] = [
|
|
90
|
+
...loadEcosystemCatalog('plugin', this.ecosystemPaths).map((entry) => ({ kind: 'plugin' as const, entry, installed: installedPlugins.has(entry.id) })),
|
|
91
|
+
...loadEcosystemCatalog('skill', this.ecosystemPaths).map((entry) => ({ kind: 'skill' as const, entry, installed: installedSkills.has(entry.id) })),
|
|
92
|
+
...loadEcosystemCatalog('hook-pack', this.ecosystemPaths).map((entry) => ({ kind: 'hook-pack' as const, entry, installed: installedHookPacks.has(entry.id) })),
|
|
93
|
+
...loadEcosystemCatalog('policy-pack', this.ecosystemPaths).map((entry) => ({ kind: 'policy-pack' as const, entry, installed: installedPolicyPacks.has(entry.id) })),
|
|
94
|
+
];
|
|
95
|
+
this.rows = rows.sort((a, b) => a.entry.name.localeCompare(b.entry.name));
|
|
96
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.rows.length - 1));
|
|
97
|
+
// I2: clear any previous catalog load error on successful refresh
|
|
98
|
+
this.clearError();
|
|
99
|
+
} catch (e) {
|
|
100
|
+
// I2: surface catalog load failure
|
|
101
|
+
this.setError(`Catalog load failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
102
|
+
}
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
public render(width: number, height: number): Line[] {
|