@pellux/goodvibes-tui 0.26.0 → 0.28.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 +40 -0
- package/README.md +12 -7
- package/docs/foundation-artifacts/operator-contract.json +4 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +53 -8
- package/src/core/session-recovery.ts +26 -18
- package/src/core/system-message-router.ts +42 -26
- package/src/core/turn-event-wiring.ts +13 -16
- package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
- package/src/daemon/handlers/inbox/index.ts +2 -1
- package/src/daemon/handlers/inbox/poller.ts +2 -1
- package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
- package/src/daemon/handlers/inbox/providers/email.ts +2 -1
- package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
- package/src/daemon/handlers/triage/integration.ts +2 -1
- package/src/daemon/handlers/triage/pipeline.ts +2 -1
- package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
- package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
- package/src/export/gist-uploader.ts +3 -1
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +2 -1
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +2 -4
- package/src/input/commands/health-runtime.ts +13 -7
- package/src/input/commands/knowledge.ts +2 -1
- package/src/input/commands/runtime-services.ts +40 -10
- package/src/input/handler-command-route.ts +1 -1
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-interactions.ts +2 -1
- package/src/input/handler-modal-routes.ts +2 -1
- package/src/input/handler-onboarding-cloudflare.ts +2 -1
- package/src/input/handler-onboarding.ts +8 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +5 -1
- package/src/input/keybindings.ts +5 -0
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +10 -0
- package/src/input/tts-settings-actions.ts +2 -1
- package/src/main.ts +52 -60
- package/src/panels/agent-inspector-panel.ts +90 -15
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +149 -72
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +28 -3
- package/src/panels/builtin/agent.ts +3 -1
- package/src/panels/builtin/development.ts +2 -1
- package/src/panels/builtin/session.ts +1 -1
- package/src/panels/cockpit-panel.ts +24 -5
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +108 -43
- package/src/panels/context-visualizer-panel.ts +49 -15
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +87 -65
- package/src/panels/debug-panel.ts +61 -37
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +30 -24
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +42 -42
- package/src/panels/file-preview-panel.ts +11 -3
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +65 -84
- package/src/panels/hooks-panel.ts +36 -2
- package/src/panels/incident-review-panel.ts +31 -10
- package/src/panels/intelligence-panel.ts +152 -71
- package/src/panels/knowledge-graph-panel.ts +89 -27
- package/src/panels/local-auth-panel.ts +17 -11
- package/src/panels/marketplace-panel.ts +33 -13
- package/src/panels/memory-panel.ts +7 -5
- package/src/panels/ops-control-panel.ts +69 -7
- package/src/panels/ops-strategy-panel.ts +61 -43
- package/src/panels/orchestration-panel.ts +34 -4
- package/src/panels/panel-list-panel.ts +33 -8
- package/src/panels/panel-manager.ts +23 -4
- package/src/panels/plan-dashboard-panel.ts +183 -66
- package/src/panels/plugins-panel.ts +48 -10
- package/src/panels/policy-panel.ts +60 -23
- package/src/panels/polish-core.ts +157 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +44 -145
- package/src/panels/project-planning-panel.ts +27 -4
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +118 -87
- package/src/panels/provider-stats-panel.ts +47 -22
- package/src/panels/qr-panel.ts +23 -11
- package/src/panels/remote-panel.ts +12 -5
- package/src/panels/routes-panel.ts +27 -5
- package/src/panels/sandbox-panel.ts +62 -27
- package/src/panels/schedule-panel.ts +44 -24
- package/src/panels/scrollable-list-panel.ts +124 -10
- package/src/panels/security-panel.ts +72 -20
- package/src/panels/services-panel.ts +68 -22
- package/src/panels/session-browser-panel.ts +42 -26
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/skills-panel.ts +2 -1
- package/src/panels/subscription-panel.ts +21 -3
- package/src/panels/symbol-outline-panel.ts +40 -47
- package/src/panels/system-messages-panel.ts +60 -27
- package/src/panels/tasks-panel.ts +36 -14
- package/src/panels/thinking-panel.ts +32 -36
- package/src/panels/token-budget-panel.ts +80 -53
- package/src/panels/tool-inspector-panel.ts +37 -39
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +127 -35
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +53 -147
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/modal-utils.ts +0 -10
- package/src/renderer/model-picker-overlay.ts +9 -4
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-modal.ts +2 -2
- package/src/renderer/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +31 -31
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/onboarding/apply.ts +4 -3
- package/src/runtime/onboarding/snapshot.ts +4 -3
- package/src/runtime/process-lifecycle.ts +195 -0
- package/src/runtime/services.ts +4 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/runtime/wrfc-persistence.ts +20 -5
- package/src/shell/ui-openers.ts +3 -2
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/verification/live-verifier.ts +4 -3
- package/src/version.ts +1 -1
- package/src/core/context-auto-compact.ts +0 -110
- package/src/panels/panel-picker.ts +0 -106
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
|
@@ -4,9 +4,11 @@ import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
|
4
4
|
import type { UiReadModel, UiRoutesSnapshot } from '../runtime/ui-read-models.ts';
|
|
5
5
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
6
6
|
import {
|
|
7
|
+
buildDetailBlock,
|
|
7
8
|
buildEmptyState,
|
|
8
9
|
buildGuidanceLine,
|
|
9
10
|
buildKeyValueLine,
|
|
11
|
+
buildKeyboardHints,
|
|
10
12
|
buildPanelLine,
|
|
11
13
|
buildPanelWorkspace,
|
|
12
14
|
buildStatusPill,
|
|
@@ -39,10 +41,20 @@ export class RoutesPanel extends ScrollableListPanel<RouteBinding> {
|
|
|
39
41
|
public constructor(readModel?: UiReadModel<UiRoutesSnapshot>) {
|
|
40
42
|
super('routes', 'Routes', 'R', 'monitoring');
|
|
41
43
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
44
|
+
this.filterEnabled = true;
|
|
45
|
+
this.filterLabel = 'Filter routes';
|
|
42
46
|
this.readModel = readModel;
|
|
43
47
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
protected override filterMatches(binding: RouteBinding, q: string): boolean {
|
|
51
|
+
return binding.surfaceKind.toLowerCase().includes(q)
|
|
52
|
+
|| (binding.title ?? '').toLowerCase().includes(q)
|
|
53
|
+
|| binding.externalId.toLowerCase().includes(q)
|
|
54
|
+
|| (binding.sessionId ?? '').toLowerCase().includes(q)
|
|
55
|
+
|| (binding.runId ?? '').toLowerCase().includes(q);
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
public override onDestroy(): void {
|
|
47
59
|
this.unsub?.();
|
|
48
60
|
}
|
|
@@ -127,7 +139,7 @@ export class RoutesPanel extends ScrollableListPanel<RouteBinding> {
|
|
|
127
139
|
this.clampSelection();
|
|
128
140
|
const selected = bindings[this.selectedIndex]!;
|
|
129
141
|
|
|
130
|
-
const
|
|
142
|
+
const detailRows: Line[] = [
|
|
131
143
|
buildPanelLine(width, [
|
|
132
144
|
[' Binding: ', C.label],
|
|
133
145
|
[selected.id, C.value],
|
|
@@ -159,20 +171,30 @@ export class RoutesPanel extends ScrollableListPanel<RouteBinding> {
|
|
|
159
171
|
];
|
|
160
172
|
|
|
161
173
|
if (surfaceEntries.length > 0) {
|
|
162
|
-
|
|
163
|
-
...surfaceEntries.slice(0,
|
|
174
|
+
detailRows.push(
|
|
175
|
+
...surfaceEntries.slice(0, 4).map(([surface, ids]) => buildPanelLine(width, [
|
|
164
176
|
[' ', C.label],
|
|
165
177
|
[surface.padEnd(10), C.info],
|
|
166
178
|
[` ${String(ids.length)} binding(s)`, C.value],
|
|
167
179
|
])),
|
|
168
180
|
);
|
|
169
181
|
}
|
|
170
|
-
|
|
182
|
+
|
|
183
|
+
const hints = this.filterActive
|
|
184
|
+
? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
|
|
185
|
+
: [
|
|
186
|
+
{ keys: 'Up/Down', label: 'move' },
|
|
187
|
+
{ keys: '/communication', label: 'inspect flow' },
|
|
188
|
+
{ keys: '/', label: 'filter' },
|
|
189
|
+
];
|
|
171
190
|
|
|
172
191
|
return this.renderList(width, height, {
|
|
173
192
|
title: 'Route Bindings',
|
|
174
193
|
header: headerLines,
|
|
175
|
-
footer:
|
|
194
|
+
footer: [
|
|
195
|
+
...buildDetailBlock(width, `Binding · ${selected.surfaceKind}`, detailRows, C),
|
|
196
|
+
buildKeyboardHints(width, hints, C),
|
|
197
|
+
],
|
|
176
198
|
});
|
|
177
199
|
}
|
|
178
200
|
}
|
|
@@ -5,9 +5,11 @@ import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
|
5
5
|
import { buildSandboxReview, listSandboxPresets, listSandboxProfiles } from '@/runtime/index.ts';
|
|
6
6
|
import type { SandboxSessionRegistry } from '@/runtime/index.ts';
|
|
7
7
|
import {
|
|
8
|
+
buildAlignedRow,
|
|
8
9
|
buildBodyText,
|
|
9
10
|
buildEmptyState,
|
|
10
11
|
buildGuidanceLine,
|
|
12
|
+
buildKeyboardHints,
|
|
11
13
|
buildKeyValueLine,
|
|
12
14
|
buildPanelLine,
|
|
13
15
|
buildPanelWorkspace,
|
|
@@ -125,7 +127,10 @@ export class SandboxPanel extends BasePanel {
|
|
|
125
127
|
buildGuidanceLine(width, '/sandbox wrapper-test <profile>', 'validate the wrapper bridge contract before wiring a real guest transport', C),
|
|
126
128
|
buildGuidanceLine(width, '/sandbox session run <id> <command> [args...]', 'execute through a tracked sandbox session and capture runtime metadata on the session record', C),
|
|
127
129
|
buildGuidanceLine(width, 'GV_SANDBOX_WRAPPER_MODE=host-exec', 'validate the wrapper contract on the host before wiring a real guest transport', C),
|
|
128
|
-
|
|
130
|
+
buildKeyboardHints(width, [
|
|
131
|
+
{ keys: '↑/↓', label: 'select profile/session' },
|
|
132
|
+
{ keys: 'Home/End', label: 'jump to first profile / first session' },
|
|
133
|
+
], C),
|
|
129
134
|
];
|
|
130
135
|
|
|
131
136
|
const selectionLines: Line[] = [];
|
|
@@ -185,41 +190,71 @@ export class SandboxPanel extends BasePanel {
|
|
|
185
190
|
));
|
|
186
191
|
} else {
|
|
187
192
|
for (const session of sessions) {
|
|
188
|
-
const
|
|
189
|
-
sessionLines.push(
|
|
190
|
-
|
|
191
|
-
[
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
const selectedRow = selectedSession?.id === session.id;
|
|
194
|
+
sessionLines.push(buildAlignedRow(
|
|
195
|
+
width,
|
|
196
|
+
[
|
|
197
|
+
{ text: session.profileId, fg: C.info },
|
|
198
|
+
{ text: session.state, fg: session.state === 'running' ? C.good : session.state === 'failed' ? C.bad : C.warn },
|
|
199
|
+
{ text: session.shared ? 'shared' : 'dedicated', fg: C.value },
|
|
200
|
+
{ text: String(session.resolvedBackend ?? session.backend), fg: C.dim },
|
|
201
|
+
{ text: session.startupStatus ?? 'n/a', fg: session.startupStatus === 'verified' ? C.good : session.startupStatus === 'failed' ? C.bad : C.warn },
|
|
202
|
+
{ text: `${session.executionCount ?? 0}x`, fg: C.info },
|
|
203
|
+
{ text: session.id, fg: C.dim },
|
|
204
|
+
],
|
|
205
|
+
[
|
|
206
|
+
{ width: 15 },
|
|
207
|
+
{ width: 10 },
|
|
208
|
+
{ width: 12 },
|
|
209
|
+
{ width: 8 },
|
|
210
|
+
{ width: 9 },
|
|
211
|
+
{ width: 5, align: 'right' },
|
|
212
|
+
{ width: Math.max(8, width - 72) },
|
|
213
|
+
],
|
|
214
|
+
{ selected: selectedRow, selectedBg: C.headerBg },
|
|
215
|
+
));
|
|
199
216
|
}
|
|
200
217
|
}
|
|
201
218
|
|
|
202
219
|
const presetLines: Line[] = [];
|
|
203
220
|
for (const preset of presets.slice(0, 2)) {
|
|
204
|
-
presetLines.push(
|
|
205
|
-
|
|
206
|
-
[
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
221
|
+
presetLines.push(buildAlignedRow(
|
|
222
|
+
width,
|
|
223
|
+
[
|
|
224
|
+
{ text: preset.id, fg: C.info },
|
|
225
|
+
{ text: preset.config.replIsolation, fg: C.value },
|
|
226
|
+
{ text: preset.config.mcpIsolation, fg: C.dim },
|
|
227
|
+
{ text: preset.config.windowsMode, fg: C.warn },
|
|
228
|
+
],
|
|
229
|
+
[
|
|
230
|
+
{ width: 18 },
|
|
231
|
+
{ width: 16 },
|
|
232
|
+
{ width: 16 },
|
|
233
|
+
{ width: Math.max(8, width - 62) },
|
|
234
|
+
],
|
|
235
|
+
{},
|
|
236
|
+
));
|
|
211
237
|
}
|
|
212
238
|
|
|
213
239
|
const profileLines: Line[] = [];
|
|
214
240
|
for (const profile of profiles) {
|
|
215
|
-
const
|
|
216
|
-
profileLines.push(
|
|
217
|
-
|
|
218
|
-
[
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
241
|
+
const selectedRow = selectedProfile?.id === profile.id;
|
|
242
|
+
profileLines.push(buildAlignedRow(
|
|
243
|
+
width,
|
|
244
|
+
[
|
|
245
|
+
{ text: profile.id, fg: C.info },
|
|
246
|
+
{ text: profile.isolation, fg: C.value },
|
|
247
|
+
{ text: profile.kind, fg: C.dim },
|
|
248
|
+
{ text: `vm=${profile.requiresVm ? 'yes' : 'no'}`, fg: profile.requiresVm ? C.good : C.warn },
|
|
249
|
+
],
|
|
250
|
+
[
|
|
251
|
+
{ width: 15 },
|
|
252
|
+
{ width: 14 },
|
|
253
|
+
{ width: 12 },
|
|
254
|
+
{ width: Math.max(6, width - 53) },
|
|
255
|
+
],
|
|
256
|
+
{ selected: selectedRow, selectedBg: C.headerBg },
|
|
257
|
+
));
|
|
223
258
|
}
|
|
224
259
|
const postureSection: PanelWorkspaceSection = { title: 'Sandbox posture', lines: overviewLines };
|
|
225
260
|
const selectedSection: PanelWorkspaceSection = { title: selectedProfile ? 'Selected Profile' : 'Selected Session', lines: selectionLines };
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { BasePanel } from './base-panel.ts';
|
|
2
2
|
import { type Line } from '../types/grid.ts';
|
|
3
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import type { AutomationManager } from '@pellux/goodvibes-sdk/platform/automation';
|
|
4
5
|
import type { AutomationJob } from '@pellux/goodvibes-sdk/platform/automation';
|
|
5
6
|
import type { AutomationRun } from '@pellux/goodvibes-sdk/platform/automation';
|
|
6
7
|
import type { AutomationScheduleDefinition } from '@pellux/goodvibes-sdk/platform/automation';
|
|
7
8
|
import {
|
|
8
9
|
buildEmptyState,
|
|
10
|
+
buildKeyboardHints,
|
|
9
11
|
buildPanelLine,
|
|
10
12
|
buildPanelWorkspace,
|
|
13
|
+
extendPalette,
|
|
11
14
|
resolveScrollablePanelSection,
|
|
12
15
|
DEFAULT_PANEL_PALETTE,
|
|
13
16
|
type PanelWorkspaceSection,
|
|
@@ -17,12 +20,9 @@ import {
|
|
|
17
20
|
// Colors
|
|
18
21
|
// ---------------------------------------------------------------------------
|
|
19
22
|
|
|
20
|
-
const C = {
|
|
21
|
-
header: '#00d7ff',
|
|
22
|
-
sectionHeader: '244',
|
|
23
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
23
24
|
enabled: '#5fd700',
|
|
24
25
|
disabled: '#6c6c6c',
|
|
25
|
-
selected: '#1c1c1c',
|
|
26
26
|
selectedFg: '#ffffff',
|
|
27
27
|
id: '238',
|
|
28
28
|
cron: '#af87ff',
|
|
@@ -32,8 +32,7 @@ const C = {
|
|
|
32
32
|
runCount: '#ffaf00',
|
|
33
33
|
statusRunning: '#5fd700',
|
|
34
34
|
statusFailed: '#ff5f5f',
|
|
35
|
-
|
|
36
|
-
} as const;
|
|
35
|
+
});
|
|
37
36
|
|
|
38
37
|
// ---------------------------------------------------------------------------
|
|
39
38
|
// View items
|
|
@@ -90,11 +89,16 @@ export class SchedulePanel extends BasePanel {
|
|
|
90
89
|
private refreshTimerId: ReturnType<typeof setInterval> | null = null;
|
|
91
90
|
private readonly automationManager: ScheduleAutomationManager;
|
|
92
91
|
|
|
93
|
-
constructor(automationManager: ScheduleAutomationManager) {
|
|
92
|
+
constructor(automationManager: ScheduleAutomationManager, private readonly requestRender: () => void = () => {}) {
|
|
94
93
|
super('schedule', 'Schedule', 'Z', 'agent');
|
|
95
94
|
this.automationManager = automationManager;
|
|
96
95
|
}
|
|
97
96
|
|
|
97
|
+
private _markDirtyAndRender(): void {
|
|
98
|
+
this.markDirty();
|
|
99
|
+
this.requestRender();
|
|
100
|
+
}
|
|
101
|
+
|
|
98
102
|
// -------------------------------------------------------------------------
|
|
99
103
|
// Lifecycle
|
|
100
104
|
// -------------------------------------------------------------------------
|
|
@@ -103,12 +107,12 @@ export class SchedulePanel extends BasePanel {
|
|
|
103
107
|
super.onActivate();
|
|
104
108
|
void this.automationManager.start().then(() => {
|
|
105
109
|
this.rebuild();
|
|
106
|
-
this.
|
|
110
|
+
this._markDirtyAndRender();
|
|
107
111
|
});
|
|
108
112
|
this.rebuild();
|
|
109
113
|
this.refreshTimerId = this.registerTimer(setInterval(() => {
|
|
110
114
|
this.rebuild();
|
|
111
|
-
this.
|
|
115
|
+
this._markDirtyAndRender();
|
|
112
116
|
}, 5_000));
|
|
113
117
|
}
|
|
114
118
|
|
|
@@ -224,14 +228,19 @@ export class SchedulePanel extends BasePanel {
|
|
|
224
228
|
lines: buildEmptyState(
|
|
225
229
|
width,
|
|
226
230
|
' No scheduled tasks',
|
|
227
|
-
'
|
|
228
|
-
[
|
|
231
|
+
'Create a recurring task and its next-run timing, run history, and enablement state will appear here.',
|
|
232
|
+
[
|
|
233
|
+
{ command: '/schedule add cron 0 * * * * repo sweep', summary: 'create a recurring cron task' },
|
|
234
|
+
{ command: '/schedule list', summary: 'inspect scheduled tasks from the shell' },
|
|
235
|
+
],
|
|
229
236
|
DEFAULT_PANEL_PALETTE,
|
|
230
237
|
),
|
|
231
238
|
},
|
|
232
239
|
],
|
|
233
240
|
footerLines: [
|
|
234
|
-
|
|
241
|
+
buildKeyboardHints(width, [
|
|
242
|
+
{ keys: '/schedule add', label: 'create a task' },
|
|
243
|
+
], DEFAULT_PANEL_PALETTE),
|
|
235
244
|
],
|
|
236
245
|
palette: DEFAULT_PANEL_PALETTE,
|
|
237
246
|
});
|
|
@@ -239,6 +248,7 @@ export class SchedulePanel extends BasePanel {
|
|
|
239
248
|
|
|
240
249
|
const taskItems = this.items.filter((item): item is Extract<ViewItem, { kind: 'task' }> => item.kind === 'task');
|
|
241
250
|
this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, taskItems.length - 1));
|
|
251
|
+
const dueSoon = tasks.filter((task: AutomationJob) => typeof task.nextRunAt === 'number').length;
|
|
242
252
|
const summarySection: PanelWorkspaceSection = {
|
|
243
253
|
title: 'Summary',
|
|
244
254
|
lines: [
|
|
@@ -247,14 +257,28 @@ export class SchedulePanel extends BasePanel {
|
|
|
247
257
|
[String(tasks.length), DEFAULT_PANEL_PALETTE.value],
|
|
248
258
|
[' Enabled ', DEFAULT_PANEL_PALETTE.label],
|
|
249
259
|
[String(enabled), enabled > 0 ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim],
|
|
260
|
+
[' Paused ', DEFAULT_PANEL_PALETTE.label],
|
|
261
|
+
[String(tasks.length - enabled), tasks.length - enabled > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
|
|
262
|
+
[' Scheduled ', DEFAULT_PANEL_PALETTE.label],
|
|
263
|
+
[String(dueSoon), dueSoon > 0 ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
|
|
250
264
|
]),
|
|
251
265
|
],
|
|
252
266
|
};
|
|
267
|
+
const selectedTask = taskItems[this.selectedIndex]?.task;
|
|
268
|
+
const toggleLabel = selectedTask
|
|
269
|
+
? (selectedTask.enabled ? 'pause task' : 'enable task')
|
|
270
|
+
: 'toggle';
|
|
271
|
+
const footerLines = [
|
|
272
|
+
buildKeyboardHints(width, [
|
|
273
|
+
{ keys: 'Up/Down', label: 'navigate' },
|
|
274
|
+
{ keys: 'Space', label: toggleLabel },
|
|
275
|
+
{ keys: 'r', label: 'run now' },
|
|
276
|
+
{ keys: 'R', label: 'refresh' },
|
|
277
|
+
], DEFAULT_PANEL_PALETTE),
|
|
278
|
+
];
|
|
253
279
|
const scheduledTasksSection = resolveScrollablePanelSection(width, height, {
|
|
254
280
|
intro: 'Review recurring scheduled tasks, next run timing, recent history, and enablement state.',
|
|
255
|
-
footerLines
|
|
256
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Space', DEFAULT_PANEL_PALETTE.info], [' toggle', DEFAULT_PANEL_PALETTE.dim], [' r', DEFAULT_PANEL_PALETTE.info], [' run now', DEFAULT_PANEL_PALETTE.dim], [' R', DEFAULT_PANEL_PALETTE.info], [' refresh', DEFAULT_PANEL_PALETTE.dim]]),
|
|
257
|
-
],
|
|
281
|
+
footerLines,
|
|
258
282
|
palette: DEFAULT_PANEL_PALETTE,
|
|
259
283
|
beforeSections: [summarySection],
|
|
260
284
|
section: {
|
|
@@ -275,9 +299,7 @@ export class SchedulePanel extends BasePanel {
|
|
|
275
299
|
title: ' Schedule',
|
|
276
300
|
intro: 'Review recurring scheduled tasks, next run timing, recent history, and enablement state.',
|
|
277
301
|
sections,
|
|
278
|
-
footerLines
|
|
279
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Space', DEFAULT_PANEL_PALETTE.info], [' toggle', DEFAULT_PANEL_PALETTE.dim], [' r', DEFAULT_PANEL_PALETTE.info], [' run now', DEFAULT_PANEL_PALETTE.dim], [' R', DEFAULT_PANEL_PALETTE.info], [' refresh', DEFAULT_PANEL_PALETTE.dim]]),
|
|
280
|
-
],
|
|
302
|
+
footerLines,
|
|
281
303
|
palette: DEFAULT_PANEL_PALETTE,
|
|
282
304
|
});
|
|
283
305
|
}
|
|
@@ -289,16 +311,16 @@ export class SchedulePanel extends BasePanel {
|
|
|
289
311
|
* Row 3: prompt preview [history]
|
|
290
312
|
*/
|
|
291
313
|
private renderTask(task: AutomationJob, history: AutomationRun[], selected: boolean, width: number): Line[] {
|
|
292
|
-
const bg = selected ? C.
|
|
314
|
+
const bg = selected ? C.selectBg : undefined;
|
|
293
315
|
const fgBase = selected ? C.selectedFg : undefined;
|
|
294
316
|
|
|
295
317
|
const bullet = task.enabled ? '* ' : 'o ';
|
|
296
318
|
const bulletFg = task.enabled ? C.enabled : C.disabled;
|
|
297
|
-
const nameStr = task.name
|
|
319
|
+
const nameStr = fitDisplay(task.name, 28);
|
|
298
320
|
const scheduleText = formatSchedule(task.schedule);
|
|
299
321
|
const row1 = buildPanelLine(width, [
|
|
300
322
|
[bullet, bulletFg, bg],
|
|
301
|
-
[task.id
|
|
323
|
+
[fitDisplay(task.id, 12), fgBase ?? C.id, bg],
|
|
302
324
|
[' ', fgBase ?? C.prompt, bg],
|
|
303
325
|
[nameStr, fgBase ?? C.prompt, bg],
|
|
304
326
|
[' ', fgBase ?? C.prompt, bg],
|
|
@@ -321,9 +343,7 @@ export class SchedulePanel extends BasePanel {
|
|
|
321
343
|
|
|
322
344
|
const maxPromptLen = Math.max(20, width - indent.length - 30);
|
|
323
345
|
const prompt = task.execution.prompt ?? task.description ?? '';
|
|
324
|
-
const promptPreview = prompt
|
|
325
|
-
? prompt.slice(0, maxPromptLen - 1) + '\u2026'
|
|
326
|
-
: prompt;
|
|
346
|
+
const promptPreview = truncateDisplay(prompt, maxPromptLen);
|
|
327
347
|
|
|
328
348
|
// Show last 3 run statuses as colored dots
|
|
329
349
|
const recentRuns = history.slice(-3);
|
|
@@ -5,6 +5,7 @@ import type { PanelCategory } from './types.ts';
|
|
|
5
5
|
import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
|
|
6
6
|
import {
|
|
7
7
|
buildEmptyState,
|
|
8
|
+
buildKeyboardHints,
|
|
8
9
|
buildPanelWorkspace,
|
|
9
10
|
buildSearchInputLine,
|
|
10
11
|
DEFAULT_PANEL_PALETTE,
|
|
@@ -56,6 +57,96 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
|
|
|
56
57
|
*/
|
|
57
58
|
protected showSelectionGutter = false;
|
|
58
59
|
|
|
60
|
+
// -------------------------------------------------------------------------
|
|
61
|
+
// Opt-in inline filter ( / to filter, Esc to clear )
|
|
62
|
+
// -------------------------------------------------------------------------
|
|
63
|
+
// Set `filterEnabled = true` and override `filterMatches()` to give a list
|
|
64
|
+
// panel a `/`-to-filter affordance that coexists with single-letter action
|
|
65
|
+
// keys (filtering is modal: action keys work until you press `/`). A filter
|
|
66
|
+
// input line is auto-rendered at the top of the list.
|
|
67
|
+
protected filterEnabled = false;
|
|
68
|
+
protected filterQuery = '';
|
|
69
|
+
protected filterActive = false;
|
|
70
|
+
/** Short noun shown in the filter hint, e.g. "Filter sessions". */
|
|
71
|
+
protected filterLabel = 'Filter';
|
|
72
|
+
|
|
73
|
+
/** Override to define what the lower-cased filter query matches for an item. */
|
|
74
|
+
protected filterMatches(_item: T, _query: string): boolean {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The list after applying the active filter — used for both display and
|
|
80
|
+
* navigation. Identical to `getItems()` unless a filter is enabled and a
|
|
81
|
+
* non-empty query is set, so existing panels are unaffected.
|
|
82
|
+
*/
|
|
83
|
+
protected getVisibleItems(): readonly T[] {
|
|
84
|
+
const all = this.getItems();
|
|
85
|
+
const q = this.filterQuery.trim().toLowerCase();
|
|
86
|
+
if (!this.filterEnabled || !q) return all;
|
|
87
|
+
return all.filter((item) => this.filterMatches(item, q));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Filter-mode key handling. Returns `true`/`false` when consumed/ignored in
|
|
92
|
+
* filter context, or `null` to fall through to normal navigation.
|
|
93
|
+
*/
|
|
94
|
+
private _handleFilterKey(key: string): boolean | null {
|
|
95
|
+
if (!this.filterEnabled) return null;
|
|
96
|
+
if (this.filterActive) {
|
|
97
|
+
if (key === 'escape') {
|
|
98
|
+
this.filterActive = false;
|
|
99
|
+
this.filterQuery = '';
|
|
100
|
+
this.selectedIndex = 0;
|
|
101
|
+
this.needsRender = true;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (key === 'return' || key === 'enter') {
|
|
105
|
+
this.filterActive = false; // commit; keep the query applied
|
|
106
|
+
this.needsRender = true;
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (key === 'backspace' || key === 'delete') {
|
|
110
|
+
this.filterQuery = this.filterQuery.slice(0, -1);
|
|
111
|
+
this.selectedIndex = 0;
|
|
112
|
+
this.needsRender = true;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
// Arrow/paging keys navigate the filtered list — fall through.
|
|
116
|
+
if (key === 'up' || key === 'down' || key === 'pageup' || key === 'pagedown' || key === 'home' || key === 'end') {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
// Any printable character (including j/k/g/G) extends the query.
|
|
120
|
+
if (key.length === 1 && key >= ' ') {
|
|
121
|
+
this.filterQuery += key;
|
|
122
|
+
this.selectedIndex = 0;
|
|
123
|
+
this.needsRender = true;
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
if (key === '/') {
|
|
129
|
+
this.filterActive = true;
|
|
130
|
+
this.needsRender = true;
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The filter input line shown at the top of the list when filtering is enabled. */
|
|
137
|
+
protected buildFilterLine(width: number): Line {
|
|
138
|
+
const palette = this.getPalette();
|
|
139
|
+
const label = this.filterActive ? `[${this.filterLabel}] ` : `${this.filterLabel}: `;
|
|
140
|
+
const value = this.filterActive
|
|
141
|
+
? `${this.filterQuery}_`
|
|
142
|
+
: this.filterQuery || '(/ to filter)';
|
|
143
|
+
return buildSearchInputLine(width, label, value, palette, {
|
|
144
|
+
active: false,
|
|
145
|
+
bg: this.filterActive ? palette.inputBg : palette.sectionBg,
|
|
146
|
+
valueColor: this.filterActive ? palette.info : (this.filterQuery ? palette.value : palette.dim),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
59
150
|
constructor(
|
|
60
151
|
id: string,
|
|
61
152
|
name: string,
|
|
@@ -143,7 +234,11 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
|
|
|
143
234
|
// call this.clearError() at the start of their handler.
|
|
144
235
|
if (this.lastError !== null) this.clearError();
|
|
145
236
|
|
|
146
|
-
|
|
237
|
+
// Opt-in inline filter consumes input before navigation when active.
|
|
238
|
+
const filterResult = this._handleFilterKey(key);
|
|
239
|
+
if (filterResult !== null) return filterResult;
|
|
240
|
+
|
|
241
|
+
const items = this.getVisibleItems();
|
|
147
242
|
const total = items.length;
|
|
148
243
|
|
|
149
244
|
switch (key) {
|
|
@@ -202,7 +297,7 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
|
|
|
202
297
|
|
|
203
298
|
handleScroll(deltaRows: number): boolean {
|
|
204
299
|
if (this.lastError !== null) this.clearError();
|
|
205
|
-
const total = this.
|
|
300
|
+
const total = this.getVisibleItems().length;
|
|
206
301
|
const rows = Math.trunc(deltaRows);
|
|
207
302
|
if (total === 0 || rows === 0) return false;
|
|
208
303
|
const next = Math.max(0, Math.min(total - 1, this.selectedIndex + rows));
|
|
@@ -221,7 +316,7 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
|
|
|
221
316
|
* Must be called after any data refresh that may shrink the list.
|
|
222
317
|
*/
|
|
223
318
|
protected clampSelection(): void {
|
|
224
|
-
const total = this.
|
|
319
|
+
const total = this.getVisibleItems().length;
|
|
225
320
|
if (total === 0) {
|
|
226
321
|
this.selectedIndex = 0;
|
|
227
322
|
} else {
|
|
@@ -253,6 +348,7 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
|
|
|
253
348
|
options: {
|
|
254
349
|
readonly header?: readonly Line[];
|
|
255
350
|
readonly footer?: readonly Line[];
|
|
351
|
+
readonly hints?: ReadonlyArray<{ keys: string; label: string }>;
|
|
256
352
|
readonly emptyMessage?: string;
|
|
257
353
|
readonly title?: string;
|
|
258
354
|
readonly spinnerFrame?: number;
|
|
@@ -260,19 +356,32 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
|
|
|
260
356
|
): Line[] {
|
|
261
357
|
this.needsRender = false;
|
|
262
358
|
const palette = this.getPalette();
|
|
263
|
-
const items = this.
|
|
359
|
+
const items = this.getVisibleItems();
|
|
264
360
|
const title = options.title ?? this.name;
|
|
265
361
|
|
|
362
|
+
// Auto-inject the filter input line at the top of the header when filtering
|
|
363
|
+
// is enabled, so every filterable panel gets the same affordance for free.
|
|
364
|
+
const baseHeader = options.header ? [...options.header as Line[]] : [];
|
|
365
|
+
const header: Line[] | undefined = this.filterEnabled
|
|
366
|
+
? [this.buildFilterLine(width), ...baseHeader]
|
|
367
|
+
: (options.header ? baseHeader : undefined);
|
|
368
|
+
|
|
369
|
+
// Standardized keyboard-hints footer row (rendered below any explicit footer).
|
|
370
|
+
const hintsLine = options.hints && options.hints.length > 0
|
|
371
|
+
? buildKeyboardHints(width, options.hints, palette)
|
|
372
|
+
: null;
|
|
373
|
+
|
|
266
374
|
// I2: inject error line into footer when present
|
|
267
375
|
const errorLine = this.renderErrorLine(width);
|
|
268
376
|
const baseFooter = options.footer ? [...options.footer as Line[]] : [];
|
|
377
|
+
if (hintsLine) baseFooter.push(hintsLine);
|
|
269
378
|
const effectiveFooter: Line[] = errorLine ? [errorLine, ...baseFooter] : baseFooter;
|
|
270
379
|
|
|
271
380
|
// I3: if loading, show spinner in place of normal content
|
|
272
381
|
const spinnerLine = this.renderLoadingLine(width, options.spinnerFrame ?? 0);
|
|
273
382
|
if (spinnerLine) {
|
|
274
383
|
const loadingSection = { lines: [spinnerLine] };
|
|
275
|
-
const headerSection =
|
|
384
|
+
const headerSection = header ? [{ lines: header }] : [];
|
|
276
385
|
const lines = buildPanelWorkspace(width, height, {
|
|
277
386
|
title,
|
|
278
387
|
sections: [...headerSection, loadingSection],
|
|
@@ -306,17 +415,22 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
|
|
|
306
415
|
|
|
307
416
|
// Empty state
|
|
308
417
|
if (scrollableLines.length === 0) {
|
|
418
|
+
// Distinguish "filter hides everything" from "genuinely empty".
|
|
419
|
+
const filteredToEmpty = this.filterEnabled && this.filterQuery.trim() !== '' && this.getItems().length > 0;
|
|
420
|
+
const emptyMessage = filteredToEmpty
|
|
421
|
+
? ` No matches for "${this.filterQuery.trim()}"`
|
|
422
|
+
: (options.emptyMessage ?? this.getEmptyStateMessage());
|
|
309
423
|
const emptyLines = buildEmptyState(
|
|
310
424
|
width,
|
|
311
|
-
|
|
312
|
-
'',
|
|
313
|
-
this.getEmptyStateActions(),
|
|
425
|
+
emptyMessage,
|
|
426
|
+
filteredToEmpty ? 'Press Esc to clear the filter.' : '',
|
|
427
|
+
filteredToEmpty ? [] : this.getEmptyStateActions(),
|
|
314
428
|
palette,
|
|
315
429
|
);
|
|
316
430
|
const lines = buildPanelWorkspace(width, height, {
|
|
317
431
|
title,
|
|
318
432
|
sections: [
|
|
319
|
-
...(
|
|
433
|
+
...(header ? [{ lines: header }] : []),
|
|
320
434
|
{ lines: emptyLines },
|
|
321
435
|
...(effectiveFooter.length > 0 ? [{ lines: effectiveFooter }] : []),
|
|
322
436
|
],
|
|
@@ -327,7 +441,7 @@ export abstract class ScrollableListPanel<T> extends BasePanel {
|
|
|
327
441
|
}
|
|
328
442
|
|
|
329
443
|
// Resolve scrollable section (updates scrollStart)
|
|
330
|
-
const beforeSections =
|
|
444
|
+
const beforeSections = header ? [{ lines: header }] : [];
|
|
331
445
|
const afterSections = effectiveFooter.length > 0 ? [{ lines: effectiveFooter }] : [];
|
|
332
446
|
|
|
333
447
|
const resolved = resolveScrollablePanelSection(width, height, {
|