@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,570 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // PanelManager — central manager for panel lifecycle, navigation, and split
3
- // ---------------------------------------------------------------------------
4
-
5
- import type { Panel, PanelRegistration, PanelCategory } from './types.ts';
6
-
7
- // ---------------------------------------------------------------------------
8
- // Pane
9
- // ---------------------------------------------------------------------------
10
-
11
- export interface Pane {
12
- panels: Panel[];
13
- activeIndex: number;
14
- }
15
-
16
- export interface WorkspaceTab {
17
- readonly id: string;
18
- readonly name: string;
19
- readonly icon: string;
20
- readonly pane: 'top' | 'bottom';
21
- readonly active: boolean;
22
- readonly focused: boolean;
23
- }
24
-
25
- // ---------------------------------------------------------------------------
26
- // PanelManager
27
- // ---------------------------------------------------------------------------
28
-
29
- export class PanelManager {
30
- private registry: PanelRegistration[] = [];
31
- private retainedPanels = new Map<string, Panel>();
32
- private _visible: boolean = false;
33
- private _splitRatio: number = 0.6;
34
-
35
- // Two panes for the top/bottom split within the panel area
36
- private topPane: Pane = { panels: [], activeIndex: 0 };
37
- private bottomPane: Pane = { panels: [], activeIndex: 0 };
38
- private _focusedPane: 'top' | 'bottom' = 'top';
39
- private _verticalSplitRatio: number = 0.5; // top gets 50% of panel height
40
- private _bottomPaneVisible: boolean = false;
41
-
42
- // Cache for getWorkspaceTabs() — invalidated on every panel lifecycle event
43
- private _cachedWorkspaceTabs: readonly WorkspaceTab[] | null = null;
44
-
45
- // -------------------------------------------------------------------------
46
- // Registration
47
- // -------------------------------------------------------------------------
48
-
49
- registerType(registration: PanelRegistration): void {
50
- const existing = this.registry.findIndex(r => r.id === registration.id);
51
- if (existing >= 0) {
52
- this.registry[existing] = registration;
53
- } else {
54
- this.registry.push(registration);
55
- }
56
- }
57
-
58
- getRegisteredTypes(): PanelRegistration[] {
59
- return [...this.registry];
60
- }
61
-
62
- getTypesByCategory(): Map<PanelCategory, PanelRegistration[]> {
63
- const map = new Map<PanelCategory, PanelRegistration[]>();
64
- for (const reg of this.registry) {
65
- const list = map.get(reg.category) ?? [];
66
- list.push(reg);
67
- map.set(reg.category, list);
68
- }
69
- return map;
70
- }
71
-
72
- prewarmRegistered(): void {
73
- for (const registration of this.registry) {
74
- if (!registration.preload) continue;
75
- if (this.getPanel(registration.id) || this.retainedPanels.has(registration.id)) continue;
76
- const panel = registration.factory();
77
- this.retainedPanels.set(registration.id, panel);
78
- }
79
- }
80
-
81
- // -------------------------------------------------------------------------
82
- // Panel lifecycle — operates on a specific pane (defaults to focused)
83
- // -------------------------------------------------------------------------
84
-
85
- /** Invalidate the workspace tab cache. Call on every panel lifecycle mutation. */
86
- private _invalidateWorkspaceTabs(): void {
87
- this._cachedWorkspaceTabs = null;
88
- }
89
-
90
- open(panelId: string, pane?: 'top' | 'bottom'): Panel {
91
- const existingPane = this._findPaneOf(panelId);
92
- if (existingPane) {
93
- this._activateByIdInPane(panelId, existingPane);
94
- this._visible = true;
95
- this._focusedPane = existingPane;
96
- if (existingPane === 'bottom') this._bottomPaneVisible = true;
97
- return this._getPane(existingPane).panels[this._getPane(existingPane).activeIndex]!;
98
- }
99
-
100
- const targetPane = pane ?? this._focusedPane;
101
- const p = this._getPane(targetPane);
102
-
103
- const oldPanel = p.panels[p.activeIndex];
104
- if (oldPanel) oldPanel.onDeactivate();
105
-
106
- const panel = this._obtainPanel(panelId);
107
- p.panels.push(panel);
108
- p.activeIndex = p.panels.length - 1;
109
- this._visible = true;
110
- // If opening into bottom pane, also make it visible
111
- if (targetPane === 'bottom') {
112
- this._bottomPaneVisible = true;
113
- this._focusedPane = 'bottom';
114
- } else {
115
- this._focusedPane = 'top';
116
- }
117
- panel.onActivate();
118
- this._invalidateWorkspaceTabs();
119
- return panel;
120
- }
121
-
122
- close(panelId: string): void {
123
- // Search both panes
124
- for (const which of ['top', 'bottom'] as const) {
125
- const p = this._getPane(which);
126
- const index = p.panels.findIndex(panel => panel.id === panelId);
127
- if (index < 0) continue;
128
-
129
- const panel = p.panels[index];
130
- const wasActive = index === p.activeIndex;
131
- if (wasActive) panel.onDeactivate();
132
- if (this._shouldRetain(panelId)) {
133
- this.retainedPanels.set(panelId, panel);
134
- } else {
135
- panel.onDestroy();
136
- }
137
- p.panels.splice(index, 1);
138
-
139
- if (p.panels.length === 0) {
140
- p.activeIndex = 0;
141
- if (which === 'bottom') {
142
- this._bottomPaneVisible = false;
143
- // Move focus to top if we were focused on empty bottom
144
- if (this._focusedPane === 'bottom') this._focusedPane = 'top';
145
- }
146
- } else {
147
- p.activeIndex = Math.min(p.activeIndex, p.panels.length - 1);
148
- if (wasActive) {
149
- const newActive = p.panels[p.activeIndex];
150
- if (newActive) newActive.onActivate();
151
- }
152
- }
153
-
154
- // Hide sidebar if no panels remain in either pane
155
- if (this.topPane.panels.length === 0 && this.bottomPane.panels.length === 0) {
156
- this._visible = false;
157
- }
158
- this._invalidateWorkspaceTabs();
159
- return;
160
- }
161
- }
162
-
163
- /**
164
- * Move a panel to a specific pane. If panelId is omitted, moves the active
165
- * panel from the currently focused pane.
166
- */
167
- moveToPane(dest: 'top' | 'bottom', panelId?: string): void {
168
- const srcPaneName = panelId
169
- ? this._findPaneOf(panelId) ?? this._focusedPane
170
- : this._focusedPane;
171
- if (srcPaneName === dest) return; // already there
172
- const dstPaneName = dest;
173
- this._moveBetweenPanes(srcPaneName, dstPaneName, panelId);
174
- }
175
-
176
- /**
177
- * Move a panel to the other pane. If panelId is omitted, moves the active
178
- * panel from the currently focused pane.
179
- */
180
- moveToOtherPane(panelId?: string): void {
181
- const srcPaneName = panelId
182
- ? this._findPaneOf(panelId) ?? this._focusedPane
183
- : this._focusedPane;
184
- const dstPaneName: 'top' | 'bottom' = srcPaneName === 'top' ? 'bottom' : 'top';
185
- this._moveBetweenPanes(srcPaneName, dstPaneName, panelId);
186
- }
187
-
188
- // -------------------------------------------------------------------------
189
- // Navigation — operates on focused pane
190
- // -------------------------------------------------------------------------
191
-
192
- nextPanel(): void {
193
- const p = this._getFocusedPane();
194
- if (p.panels.length === 0) return;
195
- const oldPanel = p.panels[p.activeIndex];
196
- if (oldPanel) oldPanel.onDeactivate();
197
- p.activeIndex = (p.activeIndex + 1) % p.panels.length;
198
- const newPanel = p.panels[p.activeIndex];
199
- if (newPanel) newPanel.onActivate();
200
- this._invalidateWorkspaceTabs();
201
- }
202
-
203
- nextWorkspaceTab(): void {
204
- this._cycleWorkspaceTab(1);
205
- }
206
-
207
- prevWorkspaceTab(): void {
208
- this._cycleWorkspaceTab(-1);
209
- }
210
-
211
- prevPanel(): void {
212
- const p = this._getFocusedPane();
213
- if (p.panels.length === 0) return;
214
- const oldPanel = p.panels[p.activeIndex];
215
- if (oldPanel) oldPanel.onDeactivate();
216
- p.activeIndex = (p.activeIndex - 1 + p.panels.length) % p.panels.length;
217
- const newPanel = p.panels[p.activeIndex];
218
- if (newPanel) newPanel.onActivate();
219
- this._invalidateWorkspaceTabs();
220
- }
221
-
222
- activateByIndex(index: number): void {
223
- const p = this._getFocusedPane();
224
- if (index < 0 || index >= p.panels.length) return;
225
- if (index === p.activeIndex) return;
226
- const oldPanel = p.panels[p.activeIndex];
227
- if (oldPanel) oldPanel.onDeactivate();
228
- p.activeIndex = index;
229
- const newPanel = p.panels[p.activeIndex];
230
- if (newPanel) newPanel.onActivate();
231
- this._invalidateWorkspaceTabs();
232
- }
233
-
234
- activateById(panelId: string): void {
235
- const which = this._findPaneOf(panelId);
236
- if (!which) return;
237
- this._activateByIdInPane(panelId, which);
238
- }
239
-
240
- // -------------------------------------------------------------------------
241
- // Pane focus control
242
- // -------------------------------------------------------------------------
243
-
244
- focusPane(pane: 'top' | 'bottom'): void {
245
- if (pane === 'bottom' && !this._bottomPaneVisible) return;
246
- this._focusedPane = pane;
247
- this._invalidateWorkspaceTabs();
248
- }
249
-
250
- getFocusedPane(): 'top' | 'bottom' {
251
- return this._focusedPane;
252
- }
253
-
254
- /** Get the currently active (focused) panel, or null if none. */
255
- getActivePanel(): Panel | null {
256
- const p = this._getFocusedPane();
257
- return p.panels[p.activeIndex] ?? null;
258
- }
259
-
260
- togglePaneFocus(): void {
261
- if (!this._bottomPaneVisible || this.bottomPane.panels.length === 0) return;
262
- this._focusedPane = this._focusedPane === 'top' ? 'bottom' : 'top';
263
- this._invalidateWorkspaceTabs();
264
- }
265
-
266
- // -------------------------------------------------------------------------
267
- // Pane visibility
268
- // -------------------------------------------------------------------------
269
-
270
- toggleBottomPane(): void {
271
- this._invalidateWorkspaceTabs();
272
- if (this._bottomPaneVisible) {
273
- this._bottomPaneVisible = false;
274
- if (this._focusedPane === 'bottom') this._focusedPane = 'top';
275
- } else {
276
- this._bottomPaneVisible = true;
277
- // If bottom pane is empty, populate it
278
- if (this.bottomPane.panels.length === 0) {
279
- if (this.topPane.panels.length > 1) {
280
- // Move last panel from top to bottom
281
- const panel = this.topPane.panels.pop()!;
282
- if (this.topPane.activeIndex >= this.topPane.panels.length) {
283
- this.topPane.activeIndex = Math.max(0, this.topPane.panels.length - 1);
284
- }
285
- this.bottomPane.panels.push(panel);
286
- this.bottomPane.activeIndex = 0;
287
- } else {
288
- // Open a default panel in bottom pane
289
- const firstType = this.registry[0];
290
- if (firstType) {
291
- this.open(firstType.id, 'bottom');
292
- }
293
- }
294
- }
295
- this._focusedPane = 'bottom';
296
- }
297
- }
298
-
299
- isBottomPaneVisible(): boolean {
300
- return this._bottomPaneVisible && this.bottomPane.panels.length > 0;
301
- }
302
-
303
- // -------------------------------------------------------------------------
304
- // Pane state accessors
305
- // -------------------------------------------------------------------------
306
-
307
- getTopPane(): Readonly<Pane> {
308
- return this.topPane;
309
- }
310
-
311
- getBottomPane(): Readonly<Pane> {
312
- return this.bottomPane;
313
- }
314
-
315
- // -------------------------------------------------------------------------
316
- // Backward-compatible accessors (operate on focused pane)
317
- // -------------------------------------------------------------------------
318
-
319
- getOpen(): Panel[] {
320
- const p = this._getFocusedPane();
321
- return [...p.panels];
322
- }
323
-
324
- /**
325
- * Returns all panels across both panes (top then bottom).
326
- * Use this when you need to know if any panels exist at all.
327
- */
328
- getAllOpen(): Panel[] {
329
- return [...this.topPane.panels, ...this.bottomPane.panels];
330
- }
331
-
332
- getActive(): Panel | null {
333
- const p = this._getFocusedPane();
334
- if (p.panels.length === 0) return null;
335
- return p.panels[p.activeIndex] ?? null;
336
- }
337
-
338
- getPanel(panelId: string): Panel | null {
339
- return this.topPane.panels.find((panel) => panel.id === panelId)
340
- ?? this.bottomPane.panels.find((panel) => panel.id === panelId)
341
- ?? null;
342
- }
343
-
344
- getPaneOf(panelId: string): 'top' | 'bottom' | null {
345
- return this._findPaneOf(panelId);
346
- }
347
-
348
- getWorkspaceTabs(): readonly WorkspaceTab[] {
349
- if (this._cachedWorkspaceTabs !== null) return this._cachedWorkspaceTabs;
350
- // `active` = the currently selected tab in its own pane (independent of focus).
351
- // `focused` = true only for the one tab in the globally focused pane that is active.
352
- const focusedPanelId = this.getActivePanel()?.id;
353
- const topActivePanelId = this.topPane.panels[this.topPane.activeIndex]?.id;
354
- const bottomActivePanelId = this.bottomPane.panels[this.bottomPane.activeIndex]?.id;
355
- const topTabs = this.topPane.panels.map((panel) => ({
356
- id: panel.id,
357
- name: panel.name,
358
- icon: panel.icon,
359
- pane: 'top' as const,
360
- active: panel.id === topActivePanelId,
361
- focused: panel.id === focusedPanelId,
362
- }));
363
- const bottomTabs = this.bottomPane.panels.map((panel) => ({
364
- id: panel.id,
365
- name: panel.name,
366
- icon: panel.icon,
367
- pane: 'bottom' as const,
368
- active: panel.id === bottomActivePanelId,
369
- focused: panel.id === focusedPanelId,
370
- }));
371
- const tabs = [...topTabs, ...bottomTabs] as WorkspaceTab[];
372
- this._cachedWorkspaceTabs = tabs;
373
- return tabs;
374
- }
375
-
376
- activateWorkspaceIndex(index: number): void {
377
- const tabs = this.getWorkspaceTabs();
378
- if (index < 0 || index >= tabs.length) return;
379
- const tab = tabs[index]!;
380
- this._focusedPane = tab.pane;
381
- if (tab.pane === 'bottom') this._bottomPaneVisible = true;
382
- this._activateByIdInPane(tab.id, tab.pane);
383
- this._invalidateWorkspaceTabs();
384
- }
385
-
386
- // -------------------------------------------------------------------------
387
- // Visibility
388
- // -------------------------------------------------------------------------
389
-
390
- toggle(): void {
391
- this._visible = !this._visible;
392
- // Auto-open a default panel if toggling visible with nothing open
393
- if (this._visible && this.topPane.panels.length === 0 && this.bottomPane.panels.length === 0) {
394
- const defaultPanel = this._getRegistration('panel-list') ?? this.registry[0];
395
- if (defaultPanel) this.open(defaultPanel.id);
396
- }
397
- }
398
-
399
- show(): void {
400
- this._visible = true;
401
- }
402
-
403
- hide(): void {
404
- this._visible = false;
405
- }
406
-
407
- isVisible(): boolean {
408
- return this._visible;
409
- }
410
-
411
- // -------------------------------------------------------------------------
412
- // Horizontal split control (left/right)
413
- // -------------------------------------------------------------------------
414
-
415
- getSplitRatio(): number {
416
- return this._splitRatio;
417
- }
418
-
419
- setSplitRatio(ratio: number): void {
420
- this._splitRatio = Math.max(0.3, Math.min(0.85, ratio));
421
- }
422
-
423
- widenLeft(): void {
424
- this.setSplitRatio(this._splitRatio + 0.05);
425
- }
426
-
427
- widenRight(): void {
428
- this.setSplitRatio(this._splitRatio - 0.05);
429
- }
430
-
431
- getLeftWidth(totalWidth: number): number {
432
- return Math.floor(totalWidth * this._splitRatio);
433
- }
434
-
435
- getRightWidth(totalWidth: number): number {
436
- return totalWidth - this.getLeftWidth(totalWidth);
437
- }
438
-
439
- // -------------------------------------------------------------------------
440
- // Vertical split control (top/bottom within panel area)
441
- // -------------------------------------------------------------------------
442
-
443
- getVerticalSplitRatio(): number {
444
- return this._verticalSplitRatio;
445
- }
446
-
447
- setVerticalSplitRatio(ratio: number): void {
448
- this._verticalSplitRatio = Math.max(0.2, Math.min(0.8, ratio));
449
- }
450
-
451
- // -------------------------------------------------------------------------
452
- // Cleanup
453
- // -------------------------------------------------------------------------
454
-
455
- destroyAll(): void {
456
- for (const panel of [...this.topPane.panels, ...this.bottomPane.panels, ...this.retainedPanels.values()]) {
457
- panel.onDestroy();
458
- }
459
- this.topPane = { panels: [], activeIndex: 0 };
460
- this.bottomPane = { panels: [], activeIndex: 0 };
461
- this.retainedPanels.clear();
462
- this.registry = [];
463
- this._focusedPane = 'top';
464
- this._bottomPaneVisible = false;
465
- this._visible = false;
466
- this._invalidateWorkspaceTabs();
467
- }
468
-
469
- // -------------------------------------------------------------------------
470
- // Private helpers
471
- // -------------------------------------------------------------------------
472
-
473
- private _getPane(which: 'top' | 'bottom'): Pane {
474
- return which === 'top' ? this.topPane : this.bottomPane;
475
- }
476
-
477
- private _getFocusedPane(): Pane {
478
- return this._getPane(this._focusedPane);
479
- }
480
-
481
- private _findPaneOf(panelId: string): 'top' | 'bottom' | null {
482
- if (this.topPane.panels.some(p => p.id === panelId)) return 'top';
483
- if (this.bottomPane.panels.some(p => p.id === panelId)) return 'bottom';
484
- return null;
485
- }
486
-
487
- private _moveBetweenPanes(srcPaneName: 'top' | 'bottom', dstPaneName: 'top' | 'bottom', panelId?: string): void {
488
- const src = this._getPane(srcPaneName);
489
- const dst = this._getPane(dstPaneName);
490
-
491
- const id = panelId ?? src.panels[src.activeIndex]?.id;
492
- if (!id) return;
493
-
494
- const index = src.panels.findIndex(p => p.id === id);
495
- if (index < 0) return;
496
-
497
- const panel = src.panels[index];
498
- const wasActive = index === src.activeIndex;
499
- if (wasActive) panel.onDeactivate();
500
- src.panels.splice(index, 1);
501
- src.activeIndex = Math.min(src.activeIndex, Math.max(0, src.panels.length - 1));
502
-
503
- if (wasActive && src.panels.length > 0) {
504
- src.panels[src.activeIndex]?.onActivate();
505
- }
506
-
507
- // Deactivate current active in dest
508
- const oldDstActive = dst.panels[dst.activeIndex];
509
- if (oldDstActive) oldDstActive.onDeactivate();
510
-
511
- dst.panels.push(panel);
512
- dst.activeIndex = dst.panels.length - 1;
513
- panel.onActivate();
514
-
515
- if (dstPaneName === 'bottom') {
516
- this._bottomPaneVisible = true;
517
- }
518
- this._focusedPane = dstPaneName;
519
- this._invalidateWorkspaceTabs();
520
- }
521
-
522
- private _cycleWorkspaceTab(direction: 1 | -1): void {
523
- const tabs = this.getWorkspaceTabs();
524
- if (tabs.length === 0) return;
525
- const currentIndex = tabs.findIndex((tab) => tab.focused);
526
- const nextIndex = currentIndex < 0
527
- ? 0
528
- : (currentIndex + direction + tabs.length) % tabs.length;
529
- this.activateWorkspaceIndex(nextIndex);
530
- }
531
-
532
- private _obtainPanel(panelId: string): Panel {
533
- const retained = this.retainedPanels.get(panelId);
534
- if (retained) {
535
- this.retainedPanels.delete(panelId);
536
- return retained;
537
- }
538
- const registration = this._getRegistration(panelId);
539
- if (!registration) {
540
- throw new Error(`No panel type registered with id: ${panelId}`);
541
- }
542
- return registration.factory();
543
- }
544
-
545
- private _getRegistration(panelId: string): PanelRegistration | undefined {
546
- return this.registry.find((registration) => registration.id === panelId);
547
- }
548
-
549
- private _shouldRetain(panelId: string): boolean {
550
- return this._getRegistration(panelId)?.preload === true;
551
- }
552
-
553
- private _activateByIdInPane(panelId: string, which: 'top' | 'bottom'): void {
554
- const p = this._getPane(which);
555
- const index = p.panels.findIndex(panel => panel.id === panelId);
556
- if (index < 0) return;
557
- if (index === p.activeIndex) {
558
- p.panels[index]?.onActivate();
559
- return;
560
- }
561
- if (index !== p.activeIndex) {
562
- const oldPanel = p.panels[p.activeIndex];
563
- if (oldPanel) oldPanel.onDeactivate();
564
- p.activeIndex = index;
565
- const newPanel = p.panels[p.activeIndex];
566
- if (newPanel) newPanel.onActivate();
567
- this._invalidateWorkspaceTabs();
568
- }
569
- }
570
- }