@pellux/goodvibes-agent 1.4.4 → 1.5.1

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.
Files changed (230) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +52 -7
  3. package/dist/package/main.js +7491 -12253
  4. package/docs/README.md +2 -2
  5. package/docs/channels-remote-and-api.md +1 -1
  6. package/docs/getting-started.md +2 -2
  7. package/docs/release-and-publishing.md +1 -1
  8. package/docs/tools-and-commands.md +5 -5
  9. package/package.json +4 -2
  10. package/release/live-verification/live-verification.json +13 -13
  11. package/release/live-verification/live-verification.md +15 -15
  12. package/release/performance-snapshot.json +2 -2
  13. package/release/release-notes.md +5 -7
  14. package/release/release-readiness.json +6 -6
  15. package/src/agent/behavior-discovery-summary.ts +4 -18
  16. package/src/agent/calendar-registry.ts +322 -0
  17. package/src/agent/competitive-feature-inventory.ts +268 -130
  18. package/src/agent/document-registry.ts +5 -1
  19. package/src/agent/email/email-service.ts +350 -0
  20. package/src/agent/email/imap-client.ts +596 -0
  21. package/src/agent/email/smtp-client.ts +453 -0
  22. package/src/agent/ics-calendar.ts +662 -0
  23. package/src/agent/ics-timezone.ts +172 -0
  24. package/src/agent/markdown-frontmatter.ts +31 -0
  25. package/src/agent/memory-safety.ts +1 -1
  26. package/src/agent/note-registry.ts +3 -9
  27. package/src/agent/persona-discovery.ts +3 -15
  28. package/src/agent/persona-registry.ts +1 -10
  29. package/src/agent/research-source-registry.ts +5 -1
  30. package/src/agent/routine-discovery.ts +3 -15
  31. package/src/agent/runtime-profile.ts +3 -0
  32. package/src/agent/skill-discovery.ts +3 -15
  33. package/src/agent/skill-draft-proposer.ts +203 -0
  34. package/src/agent/skill-draft-runner.ts +201 -0
  35. package/src/agent/skill-registry.ts +36 -1
  36. package/src/agent/skill-standard.ts +99 -0
  37. package/src/agent/vibe-file.ts +7 -22
  38. package/src/cli/completion.ts +1 -1
  39. package/src/cli/config-overrides.ts +10 -1
  40. package/src/cli/redaction.ts +17 -5
  41. package/src/config/provider-model.ts +2 -1
  42. package/src/config/secret-config.ts +13 -6
  43. package/src/core/activity-feed.ts +97 -0
  44. package/src/core/away-digest.ts +161 -0
  45. package/src/core/conversation-rendering.ts +7 -3
  46. package/src/core/conversation.ts +22 -15
  47. package/src/core/hardware-profile.ts +362 -0
  48. package/src/core/last-seen-store.ts +78 -0
  49. package/src/core/plain-language.ts +52 -0
  50. package/src/core/setup-incomplete-hint.ts +90 -0
  51. package/src/core/system-message-router.ts +38 -87
  52. package/src/input/agent-workspace-basic-command-editor-submission.ts +60 -220
  53. package/src/input/agent-workspace-basic-command-editors.ts +39 -141
  54. package/src/input/agent-workspace-categories.ts +40 -125
  55. package/src/input/agent-workspace-command-editor.ts +4 -0
  56. package/src/input/agent-workspace-host-category.ts +0 -5
  57. package/src/input/agent-workspace-local-editor-submission.ts +3 -1
  58. package/src/input/agent-workspace-navigation.ts +10 -3
  59. package/src/input/agent-workspace-onboarding-actions.ts +132 -0
  60. package/src/input/agent-workspace-onboarding-categories.ts +29 -26
  61. package/src/input/agent-workspace-onboarding-finish.ts +11 -4
  62. package/src/input/agent-workspace-onboarding-state.ts +111 -0
  63. package/src/input/agent-workspace-profile-editor-submission.ts +260 -0
  64. package/src/input/agent-workspace-profile-editors.ts +155 -0
  65. package/src/input/agent-workspace-subscription-editor.ts +7 -0
  66. package/src/input/agent-workspace-types.ts +5 -1
  67. package/src/input/agent-workspace.ts +65 -39
  68. package/src/input/command-registry.ts +18 -5
  69. package/src/input/commands/agent-runtime-profile-runtime.ts +2 -1
  70. package/src/input/commands/agent-skills-runtime.ts +60 -3
  71. package/src/input/commands/calendar-runtime.ts +209 -0
  72. package/src/input/commands/channels-runtime.ts +1 -0
  73. package/src/input/commands/command-error.ts +9 -0
  74. package/src/input/commands/compat-runtime.ts +1 -0
  75. package/src/input/commands/config.ts +1 -0
  76. package/src/input/commands/conversation-runtime.ts +1 -1
  77. package/src/input/commands/delegation-runtime.ts +5 -6
  78. package/src/input/commands/email-runtime.ts +387 -0
  79. package/src/input/commands/experience-runtime.ts +17 -4
  80. package/src/input/commands/guidance-runtime.ts +1 -1
  81. package/src/input/commands/health-runtime.ts +6 -1
  82. package/src/input/commands/knowledge-format.ts +73 -0
  83. package/src/input/commands/knowledge.ts +9 -74
  84. package/src/input/commands/local-provider-runtime.ts +2 -1
  85. package/src/input/commands/local-runtime.ts +10 -4
  86. package/src/input/commands/mcp-runtime.ts +7 -0
  87. package/src/input/commands/notify-runtime.ts +17 -1
  88. package/src/input/commands/onboarding-runtime.ts +1 -0
  89. package/src/input/commands/operator-actions-runtime.ts +1 -0
  90. package/src/input/commands/operator-runtime.ts +3 -0
  91. package/src/input/commands/personas-runtime.ts +1 -0
  92. package/src/input/commands/platform-access-runtime.ts +40 -17
  93. package/src/input/commands/product-runtime.ts +6 -1
  94. package/src/input/commands/provider-accounts-runtime.ts +3 -2
  95. package/src/input/commands/qrcode-runtime.ts +1 -0
  96. package/src/input/commands/routines-runtime.ts +1 -0
  97. package/src/input/commands/runtime-services.ts +0 -13
  98. package/src/input/commands/security-runtime.ts +1 -0
  99. package/src/input/commands/session-content.ts +1 -0
  100. package/src/input/commands/shell-core.ts +5 -2
  101. package/src/input/commands/subscription-runtime.ts +33 -9
  102. package/src/input/commands/support-bundle-runtime.ts +8 -1
  103. package/src/input/commands/tasks-runtime.ts +1 -0
  104. package/src/input/commands/tts-runtime.ts +1 -0
  105. package/src/input/commands/vibe-runtime.ts +1 -0
  106. package/src/input/commands.ts +4 -0
  107. package/src/input/feed-context-factory.ts +3 -12
  108. package/src/input/handler-command-route.ts +7 -60
  109. package/src/input/handler-feed-routes.ts +0 -194
  110. package/src/input/handler-feed.ts +2 -54
  111. package/src/input/handler-interactions.ts +0 -2
  112. package/src/input/handler-modal-stack.ts +0 -9
  113. package/src/input/handler-picker-routes.ts +24 -1
  114. package/src/input/handler-shortcuts.ts +11 -40
  115. package/src/input/handler-ui-state.ts +13 -0
  116. package/src/input/handler.ts +8 -35
  117. package/src/input/keybindings.ts +40 -17
  118. package/src/input/model-picker-local-fit.ts +130 -0
  119. package/src/input/shell-passthrough.ts +58 -0
  120. package/src/input/submission-router.ts +0 -5
  121. package/src/main.ts +129 -90
  122. package/src/renderer/activity-sidebar.ts +186 -0
  123. package/src/renderer/agent-workspace-context-lines.ts +5 -48
  124. package/src/renderer/agent-workspace-style.ts +1 -1
  125. package/src/renderer/agent-workspace.ts +14 -2
  126. package/src/renderer/compositor.ts +37 -125
  127. package/src/renderer/conversation-overlays.ts +3 -3
  128. package/src/renderer/help-overlay.ts +7 -5
  129. package/src/renderer/model-workspace.ts +101 -86
  130. package/src/{panels → renderer}/polish.ts +3 -3
  131. package/src/renderer/shell-surface.ts +4 -5
  132. package/src/renderer/status-token.ts +31 -11
  133. package/src/renderer/tab-strip.ts +1 -1
  134. package/src/renderer/tool-call.ts +7 -8
  135. package/src/renderer/tool-labels.ts +92 -0
  136. package/src/renderer/ui-factory.ts +48 -96
  137. package/src/runtime/bootstrap-command-context.ts +0 -5
  138. package/src/runtime/bootstrap-command-parts.ts +6 -15
  139. package/src/runtime/bootstrap-core.ts +34 -13
  140. package/src/runtime/bootstrap-hook-bridge.ts +3 -1
  141. package/src/runtime/bootstrap-shell.ts +3 -50
  142. package/src/runtime/bootstrap.ts +3 -4
  143. package/src/runtime/context.ts +1 -1
  144. package/src/runtime/diagnostics/panels/index.ts +0 -1
  145. package/src/runtime/diagnostics/panels/policy.ts +1 -1
  146. package/src/runtime/execution-ledger.ts +16 -1
  147. package/src/runtime/index.ts +6 -1
  148. package/src/runtime/onboarding/index.ts +1 -0
  149. package/src/runtime/onboarding/onboarding-state.ts +200 -0
  150. package/src/{panels → runtime}/provider-account-snapshot.ts +5 -2
  151. package/src/runtime/services.ts +16 -4
  152. package/src/runtime/terminal-output-guard.ts +7 -0
  153. package/src/runtime/tool-permission-safety.ts +1 -1
  154. package/src/runtime/ui/model-picker/data-provider.ts +1 -1
  155. package/src/runtime/ui/model-picker/health-enrichment.ts +2 -2
  156. package/src/runtime/ui/model-picker/types.ts +2 -2
  157. package/src/runtime/ui/provider-health/data-provider.ts +3 -3
  158. package/src/runtime/ui/provider-health/types.ts +2 -2
  159. package/src/runtime/ui-services.ts +0 -2
  160. package/src/shell/agent-workspace-fullscreen.ts +0 -1
  161. package/src/shell/autonomy-surfacing.ts +272 -0
  162. package/src/shell/session-continuity-hints.ts +0 -6
  163. package/src/shell/startup-wiring.ts +221 -0
  164. package/src/shell/ui-openers.ts +18 -29
  165. package/src/tools/agent-context-policy.ts +1 -1
  166. package/src/tools/agent-harness-background-processes.ts +4 -4
  167. package/src/tools/agent-harness-browser-cockpit-route.ts +3 -3
  168. package/src/tools/agent-harness-command-runner.ts +6 -1
  169. package/src/tools/agent-harness-document-ops.ts +26 -53
  170. package/src/tools/agent-harness-execution-history.ts +1 -13
  171. package/src/tools/agent-harness-execution-posture.ts +0 -15
  172. package/src/tools/agent-harness-keybinding-metadata.ts +28 -29
  173. package/src/tools/agent-harness-local-model-cookbook.ts +24 -1
  174. package/src/tools/agent-harness-metadata.ts +16 -0
  175. package/src/tools/agent-harness-mode-catalog.ts +2 -9
  176. package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
  177. package/src/tools/agent-harness-setup-posture-utils.ts +1 -1
  178. package/src/tools/agent-harness-tool-schema.ts +13 -14
  179. package/src/tools/agent-harness-tool.ts +27 -23
  180. package/src/tools/agent-harness-ui-surface-metadata.ts +10 -20
  181. package/src/tools/agent-harness-workspace-action-runner.ts +3 -4
  182. package/src/tools/agent-workspace-tool.ts +2 -33
  183. package/src/utils/terminal-width.ts +9 -3
  184. package/src/version.ts +1 -1
  185. package/src/input/agent-workspace-panel-route.ts +0 -44
  186. package/src/input/panel-integration-actions.ts +0 -26
  187. package/src/panels/approval-panel.ts +0 -149
  188. package/src/panels/automation-control-panel.ts +0 -212
  189. package/src/panels/base-panel.ts +0 -254
  190. package/src/panels/builtin/agent.ts +0 -58
  191. package/src/panels/builtin/knowledge.ts +0 -26
  192. package/src/panels/builtin/operations.ts +0 -121
  193. package/src/panels/builtin/session.ts +0 -138
  194. package/src/panels/builtin/shared.ts +0 -275
  195. package/src/panels/builtin/usage.ts +0 -21
  196. package/src/panels/builtin-panels.ts +0 -23
  197. package/src/panels/confirm-state.ts +0 -61
  198. package/src/panels/context-visualizer-panel.ts +0 -204
  199. package/src/panels/cost-tracker-panel.ts +0 -444
  200. package/src/panels/docs-panel.ts +0 -285
  201. package/src/panels/index.ts +0 -25
  202. package/src/panels/knowledge-panel.ts +0 -417
  203. package/src/panels/memory-panel.ts +0 -226
  204. package/src/panels/panel-list-panel.ts +0 -464
  205. package/src/panels/panel-manager.ts +0 -570
  206. package/src/panels/provider-accounts-panel.ts +0 -233
  207. package/src/panels/provider-health-domains.ts +0 -208
  208. package/src/panels/provider-health-panel.ts +0 -720
  209. package/src/panels/provider-health-tracker.ts +0 -115
  210. package/src/panels/provider-stats-panel.ts +0 -366
  211. package/src/panels/qr-panel.ts +0 -207
  212. package/src/panels/schedule-panel.ts +0 -321
  213. package/src/panels/scrollable-list-panel.ts +0 -491
  214. package/src/panels/search-focus.ts +0 -32
  215. package/src/panels/security-panel.ts +0 -295
  216. package/src/panels/session-browser-panel.ts +0 -395
  217. package/src/panels/session-maintenance.ts +0 -125
  218. package/src/panels/subscription-panel.ts +0 -263
  219. package/src/panels/system-messages-panel.ts +0 -230
  220. package/src/panels/tasks-panel.ts +0 -344
  221. package/src/panels/thinking-panel.ts +0 -304
  222. package/src/panels/token-budget-panel.ts +0 -475
  223. package/src/panels/tool-inspector-panel.ts +0 -436
  224. package/src/panels/types.ts +0 -54
  225. package/src/renderer/panel-composite.ts +0 -158
  226. package/src/renderer/panel-tab-bar.ts +0 -69
  227. package/src/renderer/panel-workspace-bar.ts +0 -42
  228. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  229. package/src/tools/agent-harness-panel-metadata.ts +0 -211
  230. /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
- }