@pellux/goodvibes-agent 1.4.4 → 1.5.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 +14 -0
- package/README.md +7 -7
- package/dist/package/main.js +7424 -12241
- package/docs/README.md +2 -2
- package/docs/channels-remote-and-api.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/release-and-publishing.md +1 -1
- package/docs/tools-and-commands.md +5 -5
- package/package.json +4 -2
- package/release/performance-snapshot.json +2 -2
- package/release/release-notes.md +11 -7
- package/release/release-readiness.json +6 -6
- package/src/agent/behavior-discovery-summary.ts +4 -18
- package/src/agent/calendar-registry.ts +322 -0
- package/src/agent/competitive-feature-inventory.ts +268 -130
- package/src/agent/document-registry.ts +5 -1
- package/src/agent/email/email-service.ts +350 -0
- package/src/agent/email/imap-client.ts +596 -0
- package/src/agent/email/smtp-client.ts +453 -0
- package/src/agent/ics-calendar.ts +662 -0
- package/src/agent/ics-timezone.ts +172 -0
- package/src/agent/markdown-frontmatter.ts +31 -0
- package/src/agent/memory-safety.ts +1 -1
- package/src/agent/note-registry.ts +3 -9
- package/src/agent/persona-discovery.ts +3 -15
- package/src/agent/persona-registry.ts +1 -10
- package/src/agent/research-source-registry.ts +5 -1
- package/src/agent/routine-discovery.ts +3 -15
- package/src/agent/runtime-profile.ts +3 -0
- package/src/agent/skill-discovery.ts +3 -15
- package/src/agent/skill-draft-proposer.ts +203 -0
- package/src/agent/skill-draft-runner.ts +201 -0
- package/src/agent/skill-registry.ts +36 -1
- package/src/agent/skill-standard.ts +99 -0
- package/src/agent/vibe-file.ts +7 -22
- package/src/cli/completion.ts +1 -1
- package/src/cli/config-overrides.ts +10 -1
- package/src/cli/redaction.ts +17 -5
- package/src/config/provider-model.ts +2 -1
- package/src/config/secret-config.ts +13 -6
- package/src/core/activity-feed.ts +97 -0
- package/src/core/away-digest.ts +161 -0
- package/src/core/conversation-rendering.ts +7 -3
- package/src/core/conversation.ts +22 -15
- package/src/core/hardware-profile.ts +362 -0
- package/src/core/last-seen-store.ts +78 -0
- package/src/core/plain-language.ts +52 -0
- package/src/core/setup-incomplete-hint.ts +90 -0
- package/src/core/system-message-router.ts +38 -87
- package/src/input/agent-workspace-basic-command-editor-submission.ts +60 -220
- package/src/input/agent-workspace-basic-command-editors.ts +39 -141
- package/src/input/agent-workspace-categories.ts +40 -125
- package/src/input/agent-workspace-command-editor.ts +4 -0
- package/src/input/agent-workspace-host-category.ts +0 -5
- package/src/input/agent-workspace-local-editor-submission.ts +3 -1
- package/src/input/agent-workspace-navigation.ts +10 -3
- package/src/input/agent-workspace-onboarding-actions.ts +132 -0
- package/src/input/agent-workspace-onboarding-categories.ts +29 -26
- package/src/input/agent-workspace-onboarding-finish.ts +11 -4
- package/src/input/agent-workspace-onboarding-state.ts +111 -0
- package/src/input/agent-workspace-profile-editor-submission.ts +260 -0
- package/src/input/agent-workspace-profile-editors.ts +155 -0
- package/src/input/agent-workspace-subscription-editor.ts +7 -0
- package/src/input/agent-workspace-types.ts +5 -1
- package/src/input/agent-workspace.ts +65 -39
- package/src/input/command-registry.ts +18 -5
- package/src/input/commands/agent-runtime-profile-runtime.ts +2 -1
- package/src/input/commands/agent-skills-runtime.ts +60 -3
- package/src/input/commands/calendar-runtime.ts +209 -0
- package/src/input/commands/channels-runtime.ts +1 -0
- package/src/input/commands/command-error.ts +9 -0
- package/src/input/commands/compat-runtime.ts +1 -0
- package/src/input/commands/config.ts +1 -0
- package/src/input/commands/conversation-runtime.ts +1 -1
- package/src/input/commands/delegation-runtime.ts +5 -6
- package/src/input/commands/email-runtime.ts +387 -0
- package/src/input/commands/experience-runtime.ts +17 -4
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +6 -1
- package/src/input/commands/knowledge-format.ts +73 -0
- package/src/input/commands/knowledge.ts +9 -74
- package/src/input/commands/local-provider-runtime.ts +2 -1
- package/src/input/commands/local-runtime.ts +10 -4
- package/src/input/commands/mcp-runtime.ts +7 -0
- package/src/input/commands/notify-runtime.ts +17 -1
- package/src/input/commands/onboarding-runtime.ts +1 -0
- package/src/input/commands/operator-actions-runtime.ts +1 -0
- package/src/input/commands/operator-runtime.ts +3 -0
- package/src/input/commands/personas-runtime.ts +1 -0
- package/src/input/commands/platform-access-runtime.ts +40 -17
- package/src/input/commands/product-runtime.ts +6 -1
- package/src/input/commands/provider-accounts-runtime.ts +3 -2
- package/src/input/commands/qrcode-runtime.ts +1 -0
- package/src/input/commands/routines-runtime.ts +1 -0
- package/src/input/commands/runtime-services.ts +0 -13
- package/src/input/commands/security-runtime.ts +1 -0
- package/src/input/commands/session-content.ts +1 -0
- package/src/input/commands/shell-core.ts +5 -2
- package/src/input/commands/subscription-runtime.ts +33 -9
- package/src/input/commands/support-bundle-runtime.ts +8 -1
- package/src/input/commands/tasks-runtime.ts +1 -0
- package/src/input/commands/tts-runtime.ts +1 -0
- package/src/input/commands/vibe-runtime.ts +1 -0
- package/src/input/commands.ts +4 -0
- package/src/input/feed-context-factory.ts +3 -12
- package/src/input/handler-command-route.ts +7 -60
- package/src/input/handler-feed-routes.ts +0 -194
- package/src/input/handler-feed.ts +2 -54
- package/src/input/handler-interactions.ts +0 -2
- package/src/input/handler-modal-stack.ts +0 -9
- package/src/input/handler-picker-routes.ts +24 -1
- package/src/input/handler-shortcuts.ts +11 -40
- package/src/input/handler-ui-state.ts +13 -0
- package/src/input/handler.ts +8 -35
- package/src/input/keybindings.ts +40 -17
- package/src/input/model-picker-local-fit.ts +130 -0
- package/src/input/submission-router.ts +0 -5
- package/src/main.ts +111 -89
- package/src/renderer/activity-sidebar.ts +186 -0
- package/src/renderer/agent-workspace-context-lines.ts +5 -48
- package/src/renderer/agent-workspace-style.ts +1 -1
- package/src/renderer/agent-workspace.ts +14 -2
- package/src/renderer/compositor.ts +37 -125
- package/src/renderer/conversation-overlays.ts +3 -3
- package/src/renderer/help-overlay.ts +7 -5
- package/src/renderer/model-workspace.ts +101 -86
- package/src/{panels → renderer}/polish.ts +3 -3
- package/src/renderer/shell-surface.ts +4 -5
- package/src/renderer/status-token.ts +31 -11
- package/src/renderer/tab-strip.ts +1 -1
- package/src/renderer/tool-call.ts +7 -8
- package/src/renderer/tool-labels.ts +92 -0
- package/src/renderer/ui-factory.ts +48 -96
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +6 -15
- package/src/runtime/bootstrap-core.ts +34 -13
- package/src/runtime/bootstrap-hook-bridge.ts +3 -1
- package/src/runtime/bootstrap-shell.ts +3 -50
- package/src/runtime/bootstrap.ts +3 -4
- package/src/runtime/context.ts +1 -1
- package/src/runtime/diagnostics/panels/index.ts +0 -1
- package/src/runtime/diagnostics/panels/policy.ts +1 -1
- package/src/runtime/execution-ledger.ts +16 -1
- package/src/runtime/index.ts +6 -1
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/onboarding-state.ts +200 -0
- package/src/{panels → runtime}/provider-account-snapshot.ts +5 -2
- package/src/runtime/services.ts +16 -4
- package/src/runtime/terminal-output-guard.ts +7 -0
- package/src/runtime/tool-permission-safety.ts +1 -1
- package/src/runtime/ui/model-picker/data-provider.ts +1 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +2 -2
- package/src/runtime/ui/model-picker/types.ts +2 -2
- package/src/runtime/ui/provider-health/data-provider.ts +3 -3
- package/src/runtime/ui/provider-health/types.ts +2 -2
- package/src/runtime/ui-services.ts +0 -2
- package/src/shell/agent-workspace-fullscreen.ts +0 -1
- package/src/shell/autonomy-surfacing.ts +272 -0
- package/src/shell/session-continuity-hints.ts +0 -6
- package/src/shell/startup-wiring.ts +221 -0
- package/src/shell/ui-openers.ts +18 -29
- package/src/tools/agent-context-policy.ts +1 -1
- package/src/tools/agent-harness-background-processes.ts +4 -4
- package/src/tools/agent-harness-browser-cockpit-route.ts +3 -3
- package/src/tools/agent-harness-command-runner.ts +6 -1
- package/src/tools/agent-harness-document-ops.ts +26 -53
- package/src/tools/agent-harness-execution-history.ts +1 -13
- package/src/tools/agent-harness-execution-posture.ts +0 -15
- package/src/tools/agent-harness-keybinding-metadata.ts +28 -29
- package/src/tools/agent-harness-local-model-cookbook.ts +24 -1
- package/src/tools/agent-harness-metadata.ts +16 -0
- package/src/tools/agent-harness-mode-catalog.ts +2 -9
- package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
- package/src/tools/agent-harness-setup-posture-utils.ts +1 -1
- package/src/tools/agent-harness-tool-schema.ts +13 -14
- package/src/tools/agent-harness-tool.ts +27 -23
- package/src/tools/agent-harness-ui-surface-metadata.ts +10 -20
- package/src/tools/agent-harness-workspace-action-runner.ts +3 -4
- package/src/tools/agent-workspace-tool.ts +2 -33
- package/src/utils/terminal-width.ts +9 -3
- package/src/version.ts +1 -1
- package/src/input/agent-workspace-panel-route.ts +0 -44
- package/src/input/panel-integration-actions.ts +0 -26
- package/src/panels/approval-panel.ts +0 -149
- package/src/panels/automation-control-panel.ts +0 -212
- package/src/panels/base-panel.ts +0 -254
- package/src/panels/builtin/agent.ts +0 -58
- package/src/panels/builtin/knowledge.ts +0 -26
- package/src/panels/builtin/operations.ts +0 -121
- package/src/panels/builtin/session.ts +0 -138
- package/src/panels/builtin/shared.ts +0 -275
- package/src/panels/builtin/usage.ts +0 -21
- package/src/panels/builtin-panels.ts +0 -23
- package/src/panels/confirm-state.ts +0 -61
- package/src/panels/context-visualizer-panel.ts +0 -204
- package/src/panels/cost-tracker-panel.ts +0 -444
- package/src/panels/docs-panel.ts +0 -285
- package/src/panels/index.ts +0 -25
- package/src/panels/knowledge-panel.ts +0 -417
- package/src/panels/memory-panel.ts +0 -226
- package/src/panels/panel-list-panel.ts +0 -464
- package/src/panels/panel-manager.ts +0 -570
- package/src/panels/provider-accounts-panel.ts +0 -233
- package/src/panels/provider-health-domains.ts +0 -208
- package/src/panels/provider-health-panel.ts +0 -720
- package/src/panels/provider-health-tracker.ts +0 -115
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/qr-panel.ts +0 -207
- package/src/panels/schedule-panel.ts +0 -321
- package/src/panels/scrollable-list-panel.ts +0 -491
- package/src/panels/search-focus.ts +0 -32
- package/src/panels/security-panel.ts +0 -295
- package/src/panels/session-browser-panel.ts +0 -395
- package/src/panels/session-maintenance.ts +0 -125
- package/src/panels/subscription-panel.ts +0 -263
- package/src/panels/system-messages-panel.ts +0 -230
- package/src/panels/tasks-panel.ts +0 -344
- package/src/panels/thinking-panel.ts +0 -304
- package/src/panels/token-budget-panel.ts +0 -475
- package/src/panels/tool-inspector-panel.ts +0 -436
- package/src/panels/types.ts +0 -54
- package/src/renderer/panel-composite.ts +0 -158
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/renderer/panel-workspace-bar.ts +0 -42
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/tools/agent-harness-panel-metadata.ts +0 -211
- /package/src/runtime/perf/{panel-health-monitor.ts → component-health.ts} +0 -0
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
3
|
-
import type { TokenAuditResult } from '@pellux/goodvibes-sdk/platform/security';
|
|
4
|
-
import type { UiReadModel, UiSecuritySnapshot } from '../runtime/ui-read-models.ts';
|
|
5
|
-
import {
|
|
6
|
-
buildEmptyState,
|
|
7
|
-
buildGuidanceLine,
|
|
8
|
-
buildPanelLine,
|
|
9
|
-
buildPanelWorkspace,
|
|
10
|
-
buildStatusPill,
|
|
11
|
-
DEFAULT_PANEL_PALETTE,
|
|
12
|
-
} from './polish.ts';
|
|
13
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
14
|
-
|
|
15
|
-
const C = {
|
|
16
|
-
...DEFAULT_PANEL_PALETTE,
|
|
17
|
-
header: '#94a3b8',
|
|
18
|
-
headerBg: '#1e293b',
|
|
19
|
-
dim: '#475569',
|
|
20
|
-
ok: '#22c55e',
|
|
21
|
-
warn: '#eab308',
|
|
22
|
-
error: '#ef4444',
|
|
23
|
-
selectBg: '#0f172a',
|
|
24
|
-
} as const;
|
|
25
|
-
|
|
26
|
-
function managedColor(managed: boolean): string {
|
|
27
|
-
return managed ? C.warn : C.info;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function resultColor(result: TokenAuditResult): string {
|
|
31
|
-
if (result.blocked) return C.error;
|
|
32
|
-
if (result.scope.outcome === 'violation') return C.error;
|
|
33
|
-
if (result.rotation.outcome === 'overdue') return C.error;
|
|
34
|
-
if (result.rotation.outcome === 'warning') return C.warn;
|
|
35
|
-
return C.ok;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function resultSummary(result: TokenAuditResult): string {
|
|
39
|
-
const parts: string[] = [];
|
|
40
|
-
if (result.scope.outcome === 'violation') parts.push(`scope:${result.scope.excessScopes.join(',')}`);
|
|
41
|
-
if (result.rotation.outcome === 'warning') parts.push('rotation warning');
|
|
42
|
-
if (result.rotation.outcome === 'overdue') parts.push('rotation overdue');
|
|
43
|
-
if (result.blocked) parts.push('blocked');
|
|
44
|
-
return parts.length > 0 ? parts.join(' | ') : 'in policy';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function severityColor(severity: 'low' | 'medium' | 'high' | 'critical'): string {
|
|
48
|
-
switch (severity) {
|
|
49
|
-
case 'critical':
|
|
50
|
-
case 'high':
|
|
51
|
-
return C.error;
|
|
52
|
-
case 'medium':
|
|
53
|
-
return C.warn;
|
|
54
|
-
case 'low':
|
|
55
|
-
default:
|
|
56
|
-
return C.ok;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export class SecurityPanel extends ScrollableListPanel<TokenAuditResult> {
|
|
61
|
-
private readonly unsub: (() => void) | null;
|
|
62
|
-
|
|
63
|
-
public constructor(private readonly readModel: UiReadModel<UiSecuritySnapshot>) {
|
|
64
|
-
super('security', 'Security', 'U', 'monitoring');
|
|
65
|
-
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
66
|
-
this.unsub = this.readModel.subscribe(() => this.markDirty());
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
public override onDestroy(): void {
|
|
70
|
-
this.unsub?.();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
protected override getPalette() { return C; }
|
|
74
|
-
protected override getEmptyStateMessage() { return ' No API tokens are registered for security review yet.'; }
|
|
75
|
-
protected override getEmptyStateActions() {
|
|
76
|
-
return [
|
|
77
|
-
{ command: '/secrets list', summary: 'inspect stored secret references without printing values' },
|
|
78
|
-
{ command: '/security review', summary: 'review token, policy, MCP, plugin, and incident posture' },
|
|
79
|
-
{ command: '/mcp auth-review', summary: 'inspect active MCP trust and quarantine posture' },
|
|
80
|
-
];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
protected getItems(): readonly TokenAuditResult[] {
|
|
84
|
-
return this.readModel.getSnapshot().audit.results;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
protected renderItem(result: TokenAuditResult, index: number, selected: boolean, width: number): Line {
|
|
88
|
-
const bg = selected ? C.selectBg : undefined;
|
|
89
|
-
return buildPanelLine(width, [
|
|
90
|
-
[' ', C.label, bg],
|
|
91
|
-
[result.label.padEnd(22), C.value, bg],
|
|
92
|
-
[` ${result.tokenId.padEnd(12)}`, C.info, bg],
|
|
93
|
-
[` ${result.scope.policyId.padEnd(10)}`, C.label, bg],
|
|
94
|
-
[` ${resultSummary(result).slice(0, Math.max(0, width - 49))}`, resultColor(result), bg],
|
|
95
|
-
]);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
public handleInput(key: string): boolean {
|
|
99
|
-
if (key === 'r') {
|
|
100
|
-
this.markDirty();
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
return super.handleInput(key);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
public render(width: number, height: number): Line[] {
|
|
107
|
-
this.clampSelection();
|
|
108
|
-
const snapshot = this.readModel.getSnapshot();
|
|
109
|
-
const view = snapshot.audit;
|
|
110
|
-
const preflightStatus = snapshot.policy.preflightStatus;
|
|
111
|
-
const preflightIssueCount = snapshot.policy.preflightIssueCount;
|
|
112
|
-
const lintFindingCount = snapshot.policy.lintFindingCount;
|
|
113
|
-
const quarantinedMcp = snapshot.mcpServers.filter((server) => server.schemaFreshness === 'quarantined');
|
|
114
|
-
const elevatedMcp = snapshot.mcpServers.filter((server) => server.trustMode === 'allow-all');
|
|
115
|
-
const incidents = snapshot.incidents;
|
|
116
|
-
const latestIncident = snapshot.latestIncident;
|
|
117
|
-
const quarantinedPlugins = snapshot.quarantinedPlugins;
|
|
118
|
-
const untrustedPlugins = snapshot.untrustedPlugins;
|
|
119
|
-
const attackPathReview = snapshot.attackPathReview;
|
|
120
|
-
const intro = 'Token review, policy posture, MCP attack-path review, plugin trust, and incident pressure.';
|
|
121
|
-
const footerLine = buildGuidanceLine(width, '/security review', 'review proactive policy posture before risky work starts', C);
|
|
122
|
-
|
|
123
|
-
const governanceLines: Line[] = [
|
|
124
|
-
buildPanelLine(width, [
|
|
125
|
-
[' mode ', C.label],
|
|
126
|
-
[view.managed ? 'MANAGED' : 'ADVISORY', managedColor(view.managed)],
|
|
127
|
-
[' tokens ', C.label],
|
|
128
|
-
[String(view.totalTokens), C.value],
|
|
129
|
-
[' blocked ', C.label],
|
|
130
|
-
...buildStatusPill(view.blocked.length > 0 ? 'bad' : 'good', String(view.blocked.length)),
|
|
131
|
-
[' scope violations ', C.label],
|
|
132
|
-
...buildStatusPill(view.scopeViolations.length > 0 ? 'bad' : 'good', String(view.scopeViolations.length)),
|
|
133
|
-
[' overdue ', C.label],
|
|
134
|
-
...buildStatusPill(view.rotationOverdue.length > 0 ? 'bad' : 'good', String(view.rotationOverdue.length)),
|
|
135
|
-
[' warnings ', C.label],
|
|
136
|
-
...buildStatusPill(view.rotationWarnings.length > 0 ? 'warn' : 'good', String(view.rotationWarnings.length)),
|
|
137
|
-
]),
|
|
138
|
-
buildPanelLine(width, [
|
|
139
|
-
[' preflight ', C.label],
|
|
140
|
-
...buildStatusPill(preflightStatus === 'block' ? 'bad' : preflightStatus === 'warn' ? 'warn' : preflightStatus === 'pass' ? 'good' : 'info', preflightStatus.toUpperCase()),
|
|
141
|
-
[' issues ', C.label],
|
|
142
|
-
...buildStatusPill(preflightIssueCount > 0 ? 'warn' : 'good', String(preflightIssueCount)),
|
|
143
|
-
[' lint ', C.label],
|
|
144
|
-
...buildStatusPill(lintFindingCount > 0 ? 'warn' : 'good', String(lintFindingCount)),
|
|
145
|
-
[' denied permissions ', C.label],
|
|
146
|
-
...buildStatusPill(snapshot.deniedPermissions > 0 ? 'warn' : 'good', String(snapshot.deniedPermissions)),
|
|
147
|
-
]),
|
|
148
|
-
buildPanelLine(width, [
|
|
149
|
-
[' quarantined MCP ', C.label],
|
|
150
|
-
...buildStatusPill(quarantinedMcp.length > 0 ? 'bad' : 'good', String(quarantinedMcp.length)),
|
|
151
|
-
[' elevated MCP ', C.label],
|
|
152
|
-
...buildStatusPill(elevatedMcp.length > 0 ? 'warn' : 'good', String(elevatedMcp.length)),
|
|
153
|
-
[' quarantined plugins ', C.label],
|
|
154
|
-
...buildStatusPill(quarantinedPlugins.length > 0 ? 'bad' : 'good', String(quarantinedPlugins.length)),
|
|
155
|
-
[' untrusted plugins ', C.label],
|
|
156
|
-
...buildStatusPill(untrustedPlugins.length > 0 ? 'warn' : 'good', String(untrustedPlugins.length)),
|
|
157
|
-
]),
|
|
158
|
-
buildPanelLine(width, [
|
|
159
|
-
[' incidents ', C.label],
|
|
160
|
-
...buildStatusPill(incidents.length > 0 ? 'warn' : 'good', String(incidents.length)),
|
|
161
|
-
]),
|
|
162
|
-
];
|
|
163
|
-
|
|
164
|
-
const attackPathLines: Line[] = [
|
|
165
|
-
buildPanelLine(width, [
|
|
166
|
-
[' attack paths ', C.label],
|
|
167
|
-
...buildStatusPill(attackPathReview.criticalFindings > 0 ? 'bad' : 'good', String(attackPathReview.criticalFindings)),
|
|
168
|
-
[' critical ', C.label],
|
|
169
|
-
...buildStatusPill(attackPathReview.incoherentFindings > 0 ? 'warn' : 'good', String(attackPathReview.incoherentFindings)),
|
|
170
|
-
[' review ', C.label],
|
|
171
|
-
[attackPathReview.summary.slice(0, Math.max(0, width - 36)), C.dim],
|
|
172
|
-
]),
|
|
173
|
-
];
|
|
174
|
-
|
|
175
|
-
// Empty state: no token results yet — show governance + threat posture before base empty state
|
|
176
|
-
if (view.results.length === 0) {
|
|
177
|
-
const emptyStateLines = [
|
|
178
|
-
...governanceLines,
|
|
179
|
-
...buildEmptyState(
|
|
180
|
-
width,
|
|
181
|
-
this.getEmptyStateMessage(),
|
|
182
|
-
'The security control room can already review policy, MCP, plugin, and incident posture, but token-specific scope and rotation data has not been registered.',
|
|
183
|
-
this.getEmptyStateActions(),
|
|
184
|
-
C,
|
|
185
|
-
),
|
|
186
|
-
];
|
|
187
|
-
if (quarantinedMcp.length > 0) {
|
|
188
|
-
emptyStateLines.push(buildPanelLine(width, [[' MCP quarantine still active despite no registered tokens.', C.warn]]));
|
|
189
|
-
}
|
|
190
|
-
const workspace = buildPanelWorkspace(width, height, {
|
|
191
|
-
title: 'Security Control Room',
|
|
192
|
-
intro,
|
|
193
|
-
sections: [
|
|
194
|
-
{ title: 'Governance', lines: emptyStateLines },
|
|
195
|
-
{ title: 'Attack Paths', lines: attackPathLines },
|
|
196
|
-
],
|
|
197
|
-
footerLines: [footerLine],
|
|
198
|
-
palette: C,
|
|
199
|
-
});
|
|
200
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
201
|
-
return workspace.slice(0, height);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (attackPathReview.findings.length > 0) {
|
|
205
|
-
attackPathLines.push(buildPanelLine(width, [[' MCP attack-path review', C.label]]));
|
|
206
|
-
for (const finding of attackPathReview.findings.slice(0, 3)) {
|
|
207
|
-
attackPathLines.push(buildPanelLine(width, [[
|
|
208
|
-
` ${finding.severity.toUpperCase()} ${finding.serverName}: ${finding.route}`.slice(0, width),
|
|
209
|
-
severityColor(finding.severity),
|
|
210
|
-
]]));
|
|
211
|
-
attackPathLines.push(buildPanelLine(width, [[
|
|
212
|
-
` ${finding.reason}`.slice(0, width),
|
|
213
|
-
C.dim,
|
|
214
|
-
]]));
|
|
215
|
-
attackPathLines.push(buildPanelLine(width, [[
|
|
216
|
-
` evidence: ${finding.evidence.join(' | ')}`.slice(0, width),
|
|
217
|
-
C.dim,
|
|
218
|
-
]]));
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const selected = view.results[this.selectedIndex];
|
|
223
|
-
const detailLines: Line[] = [];
|
|
224
|
-
if (selected) {
|
|
225
|
-
detailLines.push(buildPanelLine(width, [
|
|
226
|
-
[' Token: ', C.label],
|
|
227
|
-
[selected.label, C.value],
|
|
228
|
-
[' Policy: ', C.label],
|
|
229
|
-
[selected.scope.policyId, C.info],
|
|
230
|
-
[' Blocked: ', C.label],
|
|
231
|
-
[selected.blocked ? 'yes' : 'no', selected.blocked ? C.error : C.ok],
|
|
232
|
-
]));
|
|
233
|
-
detailLines.push(buildPanelLine(width, [
|
|
234
|
-
[' Scope: ', C.label],
|
|
235
|
-
[selected.scope.outcome, selected.scope.outcome === 'violation' ? C.error : C.ok],
|
|
236
|
-
[' Excess: ', C.label],
|
|
237
|
-
[(selected.scope.excessScopes.length > 0 ? selected.scope.excessScopes.join(', ') : 'none').slice(0, Math.max(0, width - 27)), selected.scope.excessScopes.length > 0 ? C.error : C.dim],
|
|
238
|
-
]));
|
|
239
|
-
detailLines.push(buildPanelLine(width, [
|
|
240
|
-
[' Rotation: ', C.label],
|
|
241
|
-
[selected.rotation.outcome, selected.rotation.outcome === 'ok' ? C.ok : selected.rotation.outcome === 'warning' ? C.warn : C.error],
|
|
242
|
-
[' Due: ', C.label],
|
|
243
|
-
[new Date(selected.rotation.dueAt).toISOString(), C.value],
|
|
244
|
-
[' Age(d): ', C.label],
|
|
245
|
-
[String(Math.floor(selected.rotation.ageMs / (24 * 60 * 60 * 1000))), C.value],
|
|
246
|
-
]));
|
|
247
|
-
detailLines.push(buildPanelLine(width, [[
|
|
248
|
-
`Last review: ${view.lastAuditAt ? new Date(view.lastAuditAt).toISOString() : 'never'} Press r to refresh.`,
|
|
249
|
-
C.dim,
|
|
250
|
-
]]));
|
|
251
|
-
if (preflightStatus !== 'n/a') {
|
|
252
|
-
detailLines.push(buildPanelLine(width, [[
|
|
253
|
-
`Policy preflight: ${preflightStatus} (${preflightIssueCount} issue${preflightIssueCount === 1 ? '' : 's'})`.slice(0, width),
|
|
254
|
-
preflightStatus === 'block' ? C.error : preflightStatus === 'warn' ? C.warn : C.dim,
|
|
255
|
-
]]));
|
|
256
|
-
}
|
|
257
|
-
if (quarantinedMcp.length > 0) {
|
|
258
|
-
const server = quarantinedMcp[0]!;
|
|
259
|
-
detailLines.push(buildPanelLine(width, [[
|
|
260
|
-
`MCP quarantine: ${server.name} ${server.quarantineReason ?? 'unknown'}${server.quarantineDetail ? ` - ${server.quarantineDetail}` : ''}`.slice(0, width),
|
|
261
|
-
C.error,
|
|
262
|
-
]]));
|
|
263
|
-
}
|
|
264
|
-
if (quarantinedPlugins.length > 0) {
|
|
265
|
-
const plugin = quarantinedPlugins[0]!;
|
|
266
|
-
detailLines.push(buildPanelLine(width, [[
|
|
267
|
-
`Plugin quarantine: ${plugin.name} (${plugin.trustTier})`.slice(0, width),
|
|
268
|
-
C.error,
|
|
269
|
-
]]));
|
|
270
|
-
} else if (untrustedPlugins.length > 0) {
|
|
271
|
-
const plugin = untrustedPlugins[0]!;
|
|
272
|
-
detailLines.push(buildPanelLine(width, [[
|
|
273
|
-
`Plugin trust warning: ${plugin.name} remains untrusted.`.slice(0, width),
|
|
274
|
-
C.warn,
|
|
275
|
-
]]));
|
|
276
|
-
}
|
|
277
|
-
if (latestIncident) {
|
|
278
|
-
detailLines.push(buildPanelLine(width, [[
|
|
279
|
-
`Latest incident: ${latestIncident.classification} - ${latestIncident.summary}`.slice(0, width),
|
|
280
|
-
C.warn,
|
|
281
|
-
]]));
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return this.renderList(width, height, {
|
|
286
|
-
title: 'Security Control Room',
|
|
287
|
-
header: governanceLines,
|
|
288
|
-
footer: [
|
|
289
|
-
...detailLines,
|
|
290
|
-
...attackPathLines,
|
|
291
|
-
footerLine,
|
|
292
|
-
],
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// SessionBrowserPanel — browse, search, and resume old sessions.
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
import type { Line } from '../types/grid.ts';
|
|
6
|
-
import { BasePanel } from './base-panel.ts';
|
|
7
|
-
import type { SessionInfo } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
8
|
-
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
9
|
-
import type { SessionBrowserQuery } from '../runtime/ui-service-queries.ts';
|
|
10
|
-
import {
|
|
11
|
-
buildEmptyState,
|
|
12
|
-
buildPanelLine,
|
|
13
|
-
buildSearchInputLine,
|
|
14
|
-
buildStyledPanelLine,
|
|
15
|
-
buildPanelWorkspace,
|
|
16
|
-
resolveScrollablePanelSection,
|
|
17
|
-
DEFAULT_PANEL_PALETTE,
|
|
18
|
-
type PanelWorkspaceSection,
|
|
19
|
-
} from './polish.ts';
|
|
20
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
21
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
22
|
-
import {
|
|
23
|
-
getPanelSearchFocusTransition,
|
|
24
|
-
isPanelSearchBackspace,
|
|
25
|
-
isPanelSearchCancel,
|
|
26
|
-
isPanelSearchCommit,
|
|
27
|
-
isPanelSearchPrintable,
|
|
28
|
-
} from './search-focus.ts';
|
|
29
|
-
import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
|
|
30
|
-
|
|
31
|
-
const C = {
|
|
32
|
-
headerBg: '#1a1a2e',
|
|
33
|
-
headerFg: '#ffffff',
|
|
34
|
-
statusBar: '#222233',
|
|
35
|
-
statusFg: '#aaaaaa',
|
|
36
|
-
selected: '#00ffff',
|
|
37
|
-
selectedBg: '#1a2a3a',
|
|
38
|
-
normal: '#ccccdd',
|
|
39
|
-
dim: '#555566',
|
|
40
|
-
label: '#8888bb',
|
|
41
|
-
value: '#ccccdd',
|
|
42
|
-
dateFg: '#6699aa',
|
|
43
|
-
modelFg: '#99aacc',
|
|
44
|
-
countFg: '#88bbcc',
|
|
45
|
-
warnFg: '#ffcc44',
|
|
46
|
-
errorFg: '#ff6666',
|
|
47
|
-
separator: '#333355',
|
|
48
|
-
} as const;
|
|
49
|
-
|
|
50
|
-
function shortDate(ts: number): string {
|
|
51
|
-
const d = new Date(ts);
|
|
52
|
-
const Y = d.getFullYear();
|
|
53
|
-
const M = String(d.getMonth() + 1).padStart(2, '0');
|
|
54
|
-
const D = String(d.getDate()).padStart(2, '0');
|
|
55
|
-
const h = String(d.getHours()).padStart(2, '0');
|
|
56
|
-
const m = String(d.getMinutes()).padStart(2, '0');
|
|
57
|
-
return `${Y}-${M}-${D} ${h}:${m}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function formatReturnContextLines(returnContext: SessionInfo['returnContext']): string[] {
|
|
61
|
-
if (!returnContext) return [];
|
|
62
|
-
const lines: string[] = [];
|
|
63
|
-
if (returnContext.activityLabel) lines.push(`activity: ${returnContext.activityLabel}`);
|
|
64
|
-
if (returnContext.statusLabel) lines.push(`status: ${returnContext.statusLabel}`);
|
|
65
|
-
if (returnContext.activeTasks || returnContext.blockedTasks || returnContext.pendingApprovals) {
|
|
66
|
-
lines.push(`tasks: active ${returnContext.activeTasks ?? 0}; blocked ${returnContext.blockedTasks ?? 0}; approvals ${returnContext.pendingApprovals ?? 0}`);
|
|
67
|
-
}
|
|
68
|
-
if (returnContext.remoteRunners?.length) {
|
|
69
|
-
lines.push(`remote build hosts: ${returnContext.remoteRunners.join(', ')}`);
|
|
70
|
-
}
|
|
71
|
-
if (returnContext.openPanels?.length) {
|
|
72
|
-
lines.push(`saved panels ignored: ${returnContext.openPanels.join(', ')}`);
|
|
73
|
-
}
|
|
74
|
-
return lines;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
// Confirmation state for deletion
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// ConfirmState<string> — subject holds the session name to delete
|
|
81
|
-
|
|
82
|
-
export class SessionBrowserPanel extends BasePanel {
|
|
83
|
-
private sessions: SessionInfo[] = [];
|
|
84
|
-
private filtered: SessionInfo[] = [];
|
|
85
|
-
private searchQuery = '';
|
|
86
|
-
private searching = false; // true when user is actively typing a search
|
|
87
|
-
private cursorIndex = 0;
|
|
88
|
-
private scrollOffset = 0;
|
|
89
|
-
private confirm: ConfirmState<string> | null = null;
|
|
90
|
-
private deleteError = '';
|
|
91
|
-
private loadError = '';
|
|
92
|
-
private refreshTimerId: ReturnType<typeof setInterval> | null = null;
|
|
93
|
-
|
|
94
|
-
constructor(
|
|
95
|
-
private readonly sessionManager: SessionBrowserQuery,
|
|
96
|
-
private resumeSession?: (sessionId: string) => void,
|
|
97
|
-
) {
|
|
98
|
-
super('sessions', 'Sessions', 'H', 'session');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
override onActivate(): void {
|
|
102
|
-
super.onActivate();
|
|
103
|
-
this._load();
|
|
104
|
-
this.refreshTimerId = this.registerTimer(setInterval(() => { this._load(); }, 5000));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
override onDeactivate(): void {
|
|
108
|
-
if (this.refreshTimerId !== null) { this.clearTimer(this.refreshTimerId); this.refreshTimerId = null; }
|
|
109
|
-
this.searching = false;
|
|
110
|
-
this.confirm = null;
|
|
111
|
-
super.onDeactivate();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
handleInput(key: string): boolean {
|
|
115
|
-
// Confirmation dialog — use shared handleConfirmInput for y/n/Esc UX
|
|
116
|
-
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
117
|
-
if (confirmResult === 'confirmed') {
|
|
118
|
-
this._deleteConfirmed();
|
|
119
|
-
return true;
|
|
120
|
-
}
|
|
121
|
-
if (confirmResult === 'cancelled') {
|
|
122
|
-
this.confirm = null;
|
|
123
|
-
this.markDirty();
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
if (confirmResult === 'absorbed') return true;
|
|
127
|
-
|
|
128
|
-
// Search mode
|
|
129
|
-
if (this.searching) {
|
|
130
|
-
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.cursorIndex, itemCount: this.filtered.length });
|
|
131
|
-
if (transition === 'focus-list') {
|
|
132
|
-
this.searching = false;
|
|
133
|
-
this.cursorIndex = 0;
|
|
134
|
-
this.markDirty();
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
if (isPanelSearchCancel(key) || isPanelSearchCommit(key)) {
|
|
138
|
-
this.searching = false;
|
|
139
|
-
this.markDirty();
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
if (isPanelSearchBackspace(key)) {
|
|
143
|
-
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
144
|
-
this._filter();
|
|
145
|
-
return true;
|
|
146
|
-
}
|
|
147
|
-
if (isPanelSearchPrintable(key)) {
|
|
148
|
-
this.searchQuery += key;
|
|
149
|
-
this._filter();
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.cursorIndex, itemCount: this.filtered.length });
|
|
156
|
-
if (transition === 'focus-search') {
|
|
157
|
-
this._startSearch();
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
switch (key) {
|
|
162
|
-
case 'up': this._move(-1); return true;
|
|
163
|
-
case 'down': this._move(1); return true;
|
|
164
|
-
case 'pageup': this._move(-10); return true;
|
|
165
|
-
case 'pagedown': this._move(10); return true;
|
|
166
|
-
case 'return': this._resume(); return true;
|
|
167
|
-
case 'd': this._promptDelete(); return true;
|
|
168
|
-
case 'r': this._load(); return true;
|
|
169
|
-
default: return false;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
render(width: number, height: number): Line[] {
|
|
174
|
-
if (height <= 0 || width <= 0) return [];
|
|
175
|
-
const intro = 'Browse, search, resume, and prune saved conversations.';
|
|
176
|
-
|
|
177
|
-
const count = this.filtered.length;
|
|
178
|
-
const total = this.sessions.length;
|
|
179
|
-
const searchLine = this.searching
|
|
180
|
-
? ` Search ${this.searchQuery}_`
|
|
181
|
-
: this.loadError
|
|
182
|
-
? ` Error ${this.loadError}`
|
|
183
|
-
: this.deleteError
|
|
184
|
-
? ` Error ${this.deleteError}`
|
|
185
|
-
: this.searchQuery
|
|
186
|
-
? ` Filter ${this.searchQuery} (/ or up at top to edit)`
|
|
187
|
-
: ` / or up at top to search Enter resume d delete r refresh`;
|
|
188
|
-
const statusFg = this.loadError || this.deleteError ? DEFAULT_PANEL_PALETTE.bad : this.searching ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim;
|
|
189
|
-
const footerLines = [
|
|
190
|
-
buildSearchInputLine(width, '', searchLine.trimStart(), DEFAULT_PANEL_PALETTE, { active: this.searching, valueColor: statusFg }),
|
|
191
|
-
];
|
|
192
|
-
|
|
193
|
-
if (this.confirm) {
|
|
194
|
-
return buildPanelWorkspace(width, height, {
|
|
195
|
-
title: ` Sessions [${count}/${total}]`,
|
|
196
|
-
intro,
|
|
197
|
-
sections: [
|
|
198
|
-
{
|
|
199
|
-
title: 'Confirmation',
|
|
200
|
-
lines: [
|
|
201
|
-
buildPanelLine(width, [[` Delete "${this.confirm.subject}"?`, DEFAULT_PANEL_PALETTE.warn]]),
|
|
202
|
-
buildPanelLine(width, [[' y', DEFAULT_PANEL_PALETTE.info], [' confirm delete', DEFAULT_PANEL_PALETTE.dim], [' n / Esc', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim]]),
|
|
203
|
-
],
|
|
204
|
-
},
|
|
205
|
-
],
|
|
206
|
-
footerLines,
|
|
207
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (this.filtered.length === 0) {
|
|
212
|
-
const emptyTitle = this.searchQuery ? ` No sessions match "${this.searchQuery}"` : ' No sessions found';
|
|
213
|
-
const emptyBody = this.searchQuery
|
|
214
|
-
? 'Clear or change the current filter to surface saved conversations again.'
|
|
215
|
-
: 'Conversations are saved automatically. Once you have saved sessions, they appear here for review and resume.';
|
|
216
|
-
return buildPanelWorkspace(width, height, {
|
|
217
|
-
title: ` Sessions [${count}/${total}]`,
|
|
218
|
-
intro,
|
|
219
|
-
sections: [
|
|
220
|
-
{
|
|
221
|
-
lines: buildEmptyState(width, emptyTitle, emptyBody, [], DEFAULT_PANEL_PALETTE),
|
|
222
|
-
},
|
|
223
|
-
],
|
|
224
|
-
footerLines,
|
|
225
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, this.filtered.length - 1));
|
|
230
|
-
const summary: PanelWorkspaceSection = {
|
|
231
|
-
title: 'Summary',
|
|
232
|
-
lines: [
|
|
233
|
-
buildPanelLine(width, [
|
|
234
|
-
[' Sessions ', DEFAULT_PANEL_PALETTE.label],
|
|
235
|
-
[String(total), DEFAULT_PANEL_PALETTE.value],
|
|
236
|
-
[' Visible ', DEFAULT_PANEL_PALETTE.label],
|
|
237
|
-
[String(count), DEFAULT_PANEL_PALETTE.info],
|
|
238
|
-
[' Search ', DEFAULT_PANEL_PALETTE.label],
|
|
239
|
-
[this.searchQuery || 'none', this.searchQuery ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
|
|
240
|
-
]),
|
|
241
|
-
],
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const selected = this.filtered[this.cursorIndex];
|
|
245
|
-
const selectedSection: PanelWorkspaceSection = selected
|
|
246
|
-
? {
|
|
247
|
-
title: 'Selected',
|
|
248
|
-
lines: [
|
|
249
|
-
buildPanelLine(width, [[' Title ', DEFAULT_PANEL_PALETTE.label], [selected.title || selected.name || '(untitled)', DEFAULT_PANEL_PALETTE.value]]),
|
|
250
|
-
buildPanelLine(width, [[' Model ', DEFAULT_PANEL_PALETTE.label], [selected.model || 'unknown', DEFAULT_PANEL_PALETTE.info], [' Title ', DEFAULT_PANEL_PALETTE.label], [selected.titleSource === 'user' ? 'user-set' : 'system', selected.titleSource === 'user' ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim]]),
|
|
251
|
-
buildPanelLine(width, [[' Date ', DEFAULT_PANEL_PALETTE.label], [shortDate(selected.timestamp), DEFAULT_PANEL_PALETTE.value], [' Messages ', DEFAULT_PANEL_PALETTE.label], [String(selected.messageCount), DEFAULT_PANEL_PALETTE.value]]),
|
|
252
|
-
buildPanelLine(width, [
|
|
253
|
-
[' Tasks ', DEFAULT_PANEL_PALETTE.label],
|
|
254
|
-
[String(selected.returnContext?.activeTasks ?? 0), (selected.returnContext?.activeTasks ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
|
|
255
|
-
[' Blocked ', DEFAULT_PANEL_PALETTE.label],
|
|
256
|
-
[String(selected.returnContext?.blockedTasks ?? 0), (selected.returnContext?.blockedTasks ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
|
|
257
|
-
[' Approvals ', DEFAULT_PANEL_PALETTE.label],
|
|
258
|
-
[String(selected.returnContext?.pendingApprovals ?? 0), (selected.returnContext?.pendingApprovals ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
|
|
259
|
-
]),
|
|
260
|
-
buildPanelLine(width, [
|
|
261
|
-
[' Remote ', DEFAULT_PANEL_PALETTE.label],
|
|
262
|
-
[String(selected.returnContext?.remoteRunners?.length ?? 0), (selected.returnContext?.remoteRunners?.length ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
|
|
263
|
-
[' Saved Panels ', DEFAULT_PANEL_PALETTE.label],
|
|
264
|
-
[String(selected.returnContext?.openPanels?.length ?? 0), (selected.returnContext?.openPanels?.length ?? 0) > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
|
|
265
|
-
]),
|
|
266
|
-
...formatReturnContextLines(selected.returnContext).map((line) =>
|
|
267
|
-
buildPanelLine(width, [[' ', DEFAULT_PANEL_PALETTE.dim], [truncateDisplay(line, Math.max(0, width - 2)), DEFAULT_PANEL_PALETTE.dim]])
|
|
268
|
-
),
|
|
269
|
-
buildPanelLine(width, [[' Next ', DEFAULT_PANEL_PALETTE.label], ['/session resume', DEFAULT_PANEL_PALETTE.dim]]),
|
|
270
|
-
],
|
|
271
|
-
}
|
|
272
|
-
: { title: 'Selected', lines: [] };
|
|
273
|
-
|
|
274
|
-
const sessionsSection = resolveScrollablePanelSection(width, height, {
|
|
275
|
-
intro,
|
|
276
|
-
footerLines,
|
|
277
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
278
|
-
beforeSections: [summary],
|
|
279
|
-
section: {
|
|
280
|
-
title: 'Sessions',
|
|
281
|
-
scrollableLines: this.filtered.map((sess, index) =>
|
|
282
|
-
this._renderSession(width, sess, index === this.cursorIndex),
|
|
283
|
-
),
|
|
284
|
-
selectedIndex: this.cursorIndex,
|
|
285
|
-
scrollOffset: this.scrollOffset,
|
|
286
|
-
minRows: 6,
|
|
287
|
-
},
|
|
288
|
-
afterSections: [selectedSection],
|
|
289
|
-
});
|
|
290
|
-
this.scrollOffset = sessionsSection.scrollOffset;
|
|
291
|
-
|
|
292
|
-
return buildPanelWorkspace(width, height, {
|
|
293
|
-
title: ` Sessions [${count}/${total}]`,
|
|
294
|
-
intro,
|
|
295
|
-
sections: [
|
|
296
|
-
summary,
|
|
297
|
-
sessionsSection.section,
|
|
298
|
-
selectedSection,
|
|
299
|
-
],
|
|
300
|
-
footerLines,
|
|
301
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
private _renderSession(width: number, sess: SessionInfo, isCursor: boolean): Line {
|
|
306
|
-
const bg = isCursor ? C.selectedBg : '';
|
|
307
|
-
const date = shortDate(sess.timestamp);
|
|
308
|
-
const cnt = String(sess.messageCount).padStart(3) + 'm ';
|
|
309
|
-
const model = (sess.model || 'unknown').slice(0, 18).padEnd(18) + ' ';
|
|
310
|
-
const prefixLength = 1 + 16 + 1 + 4 + 19;
|
|
311
|
-
const title = truncateDisplay(sess.title || sess.name || '(untitled)', Math.max(0, width - prefixLength));
|
|
312
|
-
return buildStyledPanelLine(width, [
|
|
313
|
-
{ text: isCursor ? '▸' : ' ', fg: C.selected, bg, bold: isCursor },
|
|
314
|
-
{ text: date, fg: C.dateFg, bg },
|
|
315
|
-
{ text: ' ', fg: C.normal, bg },
|
|
316
|
-
{ text: cnt, fg: C.countFg, bg },
|
|
317
|
-
{ text: model, fg: C.modelFg, bg },
|
|
318
|
-
{ text: title, fg: isCursor ? C.selected : C.normal, bg, bold: isCursor },
|
|
319
|
-
]);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
private _load(): void {
|
|
323
|
-
try {
|
|
324
|
-
this.sessions = this.sessionManager.list();
|
|
325
|
-
this._filter();
|
|
326
|
-
this.loadError = '';
|
|
327
|
-
this.markDirty();
|
|
328
|
-
} catch (e) {
|
|
329
|
-
logger.debug('SessionBrowserPanel._load failed', { error: summarizeError(e) });
|
|
330
|
-
this.loadError = 'Failed to load sessions';
|
|
331
|
-
this.markDirty();
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
private _filter(): void {
|
|
336
|
-
if (!this.searchQuery.trim()) {
|
|
337
|
-
this.filtered = [...this.sessions];
|
|
338
|
-
} else {
|
|
339
|
-
const q = this.searchQuery.toLowerCase();
|
|
340
|
-
try {
|
|
341
|
-
const results = this.sessionManager.search(q);
|
|
342
|
-
const names = new Set(results.map(r => r.session.name));
|
|
343
|
-
this.filtered = this.sessions.filter(s => names.has(s.name));
|
|
344
|
-
} catch (e) {
|
|
345
|
-
logger.debug('SessionBrowserPanel._filter search failed, falling back', { error: summarizeError(e) });
|
|
346
|
-
this.filtered = this.sessions.filter(s =>
|
|
347
|
-
(s.title || '').toLowerCase().includes(q) ||
|
|
348
|
-
(s.model || '').toLowerCase().includes(q) ||
|
|
349
|
-
s.name.toLowerCase().includes(q)
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.filtered.length - 1));
|
|
354
|
-
this.markDirty();
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
private _startSearch(): void {
|
|
358
|
-
this.searching = true;
|
|
359
|
-
this.markDirty();
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
private _move(delta: number): void {
|
|
363
|
-
if (this.filtered.length === 0) return;
|
|
364
|
-
this.cursorIndex = Math.max(0, Math.min(this.filtered.length - 1, this.cursorIndex + delta));
|
|
365
|
-
this.markDirty();
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
private _resume(): void {
|
|
369
|
-
const sess = this.filtered[this.cursorIndex];
|
|
370
|
-
if (!sess) return;
|
|
371
|
-
this.resumeSession?.(sess.name);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
private _promptDelete(): void {
|
|
375
|
-
const sess = this.filtered[this.cursorIndex];
|
|
376
|
-
if (!sess) return;
|
|
377
|
-
this.confirm = { subject: sess.name, label: sess.name };
|
|
378
|
-
this.markDirty();
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
private _deleteConfirmed(): void {
|
|
382
|
-
if (!this.confirm) return;
|
|
383
|
-
const name = this.confirm.subject;
|
|
384
|
-
this.confirm = null;
|
|
385
|
-
try {
|
|
386
|
-
this.sessionManager.delete(name);
|
|
387
|
-
this.deleteError = '';
|
|
388
|
-
this._load();
|
|
389
|
-
} catch (e) {
|
|
390
|
-
logger.debug('SessionBrowserPanel._deleteConfirmed failed', { error: summarizeError(e) });
|
|
391
|
-
this.deleteError = `Delete failed: ${name}`;
|
|
392
|
-
}
|
|
393
|
-
this.markDirty();
|
|
394
|
-
}
|
|
395
|
-
}
|