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