@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,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
- }