@pellux/goodvibes-tui 0.18.12 → 0.18.17

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 (196) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +3 -2
  5. package/src/config/index.ts +1 -138
  6. package/src/core/conversation-rendering.ts +3 -3
  7. package/src/core/conversation.ts +176 -423
  8. package/src/core/history.ts +45 -0
  9. package/src/core/orchestrator.ts +3 -735
  10. package/src/core/system-message-router.ts +19 -58
  11. package/src/daemon/cli.ts +82 -6
  12. package/src/input/command-registry.ts +2 -0
  13. package/src/input/commands/control-room-runtime.ts +1 -1
  14. package/src/input/commands/health-runtime.ts +1 -1
  15. package/src/input/commands/local-setup-review.ts +1 -1
  16. package/src/input/commands/platform-access-runtime.ts +1 -1
  17. package/src/input/commands/qrcode-runtime.ts +20 -0
  18. package/src/input/commands/subscription-runtime.ts +1 -1
  19. package/src/input/commands.ts +2 -0
  20. package/src/input/handler-content-actions.ts +2 -2
  21. package/src/input/handler-feed.ts +7 -1
  22. package/src/input/handler-modal-routes.ts +19 -2
  23. package/src/input/handler-modal-token-routes.ts +4 -1
  24. package/src/input/handler-picker-routes.ts +4 -2
  25. package/src/input/handler-ui-state.ts +1 -1
  26. package/src/input/handler.ts +1 -1
  27. package/src/input/model-picker.ts +11 -0
  28. package/src/input/search.ts +1 -1
  29. package/src/input/selection.ts +2 -2
  30. package/src/input/settings-modal.ts +31 -3
  31. package/src/main.ts +1 -1
  32. package/src/panels/agent-inspector-panel.ts +3 -3
  33. package/src/panels/agent-logs-panel.ts +26 -27
  34. package/src/panels/approval-panel.ts +2 -2
  35. package/src/panels/automation-control-panel.ts +3 -3
  36. package/src/panels/base-panel.ts +14 -14
  37. package/src/panels/builtin/operations.ts +1 -1
  38. package/src/panels/builtin/session.ts +67 -1
  39. package/src/panels/builtin/shared.ts +4 -4
  40. package/src/panels/cockpit-panel.ts +2 -2
  41. package/src/panels/communication-panel.ts +3 -3
  42. package/src/panels/context-visualizer-panel.ts +2 -2
  43. package/src/panels/control-plane-panel.ts +3 -3
  44. package/src/panels/cost-tracker-panel.ts +3 -3
  45. package/src/panels/debug-panel.ts +2 -2
  46. package/src/panels/diff-panel.ts +2 -2
  47. package/src/panels/docs-panel.ts +1 -1
  48. package/src/panels/eval-panel.ts +2 -2
  49. package/src/panels/file-explorer-panel.ts +3 -3
  50. package/src/panels/file-preview-panel.ts +3 -3
  51. package/src/panels/forensics-panel.ts +2 -2
  52. package/src/panels/git-panel.ts +1 -1
  53. package/src/panels/hooks-panel.ts +3 -3
  54. package/src/panels/incident-review-panel.ts +1 -1
  55. package/src/panels/intelligence-panel.ts +2 -2
  56. package/src/panels/knowledge-panel.ts +1 -1
  57. package/src/panels/local-auth-panel.ts +2 -2
  58. package/src/panels/marketplace-panel.ts +1 -1
  59. package/src/panels/mcp-panel.ts +3 -3
  60. package/src/panels/memory-panel.ts +1 -1
  61. package/src/panels/ops-control-panel.ts +3 -3
  62. package/src/panels/ops-strategy-panel.ts +2 -2
  63. package/src/panels/orchestration-panel.ts +2 -2
  64. package/src/panels/panel-list-panel.ts +6 -6
  65. package/src/panels/plan-dashboard-panel.ts +1 -1
  66. package/src/panels/plugins-panel.ts +2 -2
  67. package/src/panels/policy-panel.ts +2 -2
  68. package/src/panels/polish.ts +3 -3
  69. package/src/panels/provider-account-snapshot.ts +1 -1
  70. package/src/panels/provider-accounts-panel.ts +25 -29
  71. package/src/panels/provider-health-panel.ts +2 -2
  72. package/src/panels/provider-stats-panel.ts +3 -3
  73. package/src/panels/qr-panel.ts +182 -0
  74. package/src/panels/remote-panel.ts +3 -3
  75. package/src/panels/routes-panel.ts +3 -3
  76. package/src/panels/sandbox-panel.ts +2 -2
  77. package/src/panels/schedule-panel.ts +1 -1
  78. package/src/panels/scrollable-list-panel.ts +407 -0
  79. package/src/panels/security-panel.ts +2 -2
  80. package/src/panels/services-panel.ts +3 -3
  81. package/src/panels/session-browser-panel.ts +2 -2
  82. package/src/panels/settings-sync-panel.ts +2 -2
  83. package/src/panels/skills-panel.ts +6 -6
  84. package/src/panels/subscription-panel.ts +3 -3
  85. package/src/panels/symbol-outline-panel.ts +3 -3
  86. package/src/panels/system-messages-panel.ts +4 -4
  87. package/src/panels/tasks-panel.ts +2 -2
  88. package/src/panels/thinking-panel.ts +3 -3
  89. package/src/panels/token-budget-panel.ts +1 -1
  90. package/src/panels/tool-inspector-panel.ts +3 -3
  91. package/src/panels/types.ts +5 -5
  92. package/src/panels/watchers-panel.ts +3 -3
  93. package/src/panels/welcome-panel.ts +1 -1
  94. package/src/panels/worktree-panel.ts +22 -21
  95. package/src/panels/wrfc-panel.ts +3 -3
  96. package/src/permissions/prompt.ts +3 -22
  97. package/src/plugins/loader.ts +15 -304
  98. package/src/renderer/agent-detail-modal.ts +1 -1
  99. package/src/renderer/autocomplete-overlay.ts +2 -2
  100. package/src/renderer/bookmark-modal.ts +1 -1
  101. package/src/renderer/bottom-bar.ts +2 -2
  102. package/src/renderer/buffer.ts +1 -1
  103. package/src/renderer/code-block.ts +2 -2
  104. package/src/renderer/compositor.ts +2 -2
  105. package/src/renderer/context-inspector.ts +1 -1
  106. package/src/renderer/conversation-layout.ts +2 -2
  107. package/src/renderer/conversation-overlays.ts +1 -1
  108. package/src/renderer/conversation-surface.ts +2 -2
  109. package/src/renderer/diff-view.ts +2 -2
  110. package/src/renderer/diff.ts +1 -1
  111. package/src/renderer/file-picker-overlay.ts +2 -2
  112. package/src/renderer/file-tree.ts +2 -2
  113. package/src/renderer/help-overlay.ts +1 -1
  114. package/src/renderer/history-search-overlay.ts +2 -2
  115. package/src/renderer/live-tail-modal.ts +1 -1
  116. package/src/renderer/markdown.ts +2 -2
  117. package/src/renderer/modal-factory.ts +3 -3
  118. package/src/renderer/model-picker-overlay.ts +2 -2
  119. package/src/renderer/overlay-box.ts +2 -2
  120. package/src/renderer/panel-composite.ts +1 -1
  121. package/src/renderer/panel-picker-overlay.ts +2 -2
  122. package/src/renderer/panel-tab-bar.ts +1 -1
  123. package/src/renderer/panel-workspace-bar.ts +1 -1
  124. package/src/renderer/process-indicator.ts +2 -2
  125. package/src/renderer/process-modal.ts +1 -1
  126. package/src/renderer/profile-picker-modal.ts +2 -2
  127. package/src/renderer/progress.ts +2 -2
  128. package/src/renderer/qr-renderer.ts +117 -0
  129. package/src/renderer/search-overlay.ts +2 -2
  130. package/src/renderer/selection-modal-overlay.ts +2 -2
  131. package/src/renderer/session-picker-modal.ts +2 -2
  132. package/src/renderer/settings-modal-helpers.ts +122 -0
  133. package/src/renderer/settings-modal.ts +149 -113
  134. package/src/renderer/shell-surface.ts +1 -1
  135. package/src/renderer/system-message.ts +1 -1
  136. package/src/renderer/tab-strip.ts +2 -2
  137. package/src/renderer/text-layout.ts +1 -1
  138. package/src/renderer/thinking.ts +1 -1
  139. package/src/renderer/tool-call.ts +2 -2
  140. package/src/renderer/ui-factory.ts +2 -2
  141. package/src/runtime/bootstrap-command-context.ts +5 -6
  142. package/src/runtime/bootstrap-command-parts.ts +32 -18
  143. package/src/runtime/bootstrap-core.ts +3 -2
  144. package/src/runtime/bootstrap-hook-bridge.ts +15 -174
  145. package/src/runtime/bootstrap-shell.ts +4 -4
  146. package/src/runtime/bootstrap.ts +7 -2
  147. package/src/runtime/context.ts +4 -20
  148. package/src/runtime/diagnostics/panels/index.ts +6 -6
  149. package/src/runtime/diagnostics/panels/ops.ts +1 -1
  150. package/src/runtime/diagnostics/panels/panel-resources.ts +118 -0
  151. package/src/runtime/perf/panel-contracts.ts +32 -0
  152. package/src/runtime/perf/panel-health-monitor.ts +18 -0
  153. package/src/runtime/services.ts +5 -5
  154. package/src/runtime/store/domains/domain-read-matrix.ts +0 -2
  155. package/src/runtime/store/selectors/index.ts +11 -6
  156. package/src/runtime/store/state.ts +12 -4
  157. package/src/runtime/ui-events.ts +1 -0
  158. package/src/runtime/ui-read-model-helpers.ts +1 -32
  159. package/src/runtime/ui-read-models-observability-maintenance.ts +1 -81
  160. package/src/runtime/ui-read-models-observability-options.ts +1 -5
  161. package/src/runtime/ui-read-models-observability-remote.ts +1 -73
  162. package/src/runtime/ui-read-models-observability-security.ts +1 -172
  163. package/src/runtime/ui-read-models-observability-system.ts +1 -217
  164. package/src/runtime/ui-read-models-observability.ts +1 -59
  165. package/src/runtime/ui-service-queries.ts +1 -114
  166. package/src/runtime/ui-services.ts +1 -1
  167. package/src/shell/ui-openers.ts +1 -1
  168. package/src/tools/index.ts +1 -186
  169. package/src/types/grid.ts +48 -0
  170. package/src/utils/clipboard.ts +21 -0
  171. package/src/utils/splash-lines.ts +1 -1
  172. package/src/utils/terminal-width.ts +185 -0
  173. package/src/version.ts +1 -1
  174. package/src/config/service-registry.ts +0 -1
  175. package/src/config/subscription-providers.ts +0 -127
  176. package/src/daemon/facade-composition.ts +0 -398
  177. package/src/daemon/facade.ts +0 -638
  178. package/src/daemon/surface-policy.ts +0 -60
  179. package/src/daemon/types.ts +0 -191
  180. package/src/runtime/diagnostics/actions.ts +0 -776
  181. package/src/runtime/diagnostics/index.ts +0 -99
  182. package/src/runtime/diagnostics/panels/agents.ts +0 -252
  183. package/src/runtime/diagnostics/panels/events.ts +0 -188
  184. package/src/runtime/diagnostics/panels/health.ts +0 -242
  185. package/src/runtime/diagnostics/panels/tasks.ts +0 -251
  186. package/src/runtime/diagnostics/panels/tool-calls.ts +0 -267
  187. package/src/runtime/diagnostics/provider.ts +0 -262
  188. package/src/runtime/store/domains/conversation.ts +0 -181
  189. package/src/runtime/store/domains/permissions.ts +0 -143
  190. package/src/runtime/store/helpers/reducers/conversation.ts +0 -228
  191. package/src/runtime/store/helpers/reducers/lifecycle.ts +0 -440
  192. package/src/runtime/store/helpers/reducers/shared.ts +0 -60
  193. package/src/runtime/store/helpers/reducers/sync.ts +0 -555
  194. package/src/runtime/store/helpers/reducers.ts +0 -30
  195. package/src/runtime/ui-read-models-core.ts +0 -95
  196. package/src/runtime/ui-read-models-operations.ts +0 -203
@@ -1,6 +1,6 @@
1
- import type { Line } from '@pellux/goodvibes-sdk/platform/types/grid';
2
- import { createEmptyLine } from '@pellux/goodvibes-sdk/platform/types/grid';
3
- import { BasePanel } from './base-panel.ts';
1
+ import type { Line } from '../types/grid.ts';
2
+ import { createEmptyLine } from '../types/grid.ts';
3
+ import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
4
  import { buildKeyValueLine, buildPanelLine, buildPanelWorkspace, DEFAULT_PANEL_PALETTE, resolvePrimaryScrollableSection, type PanelWorkspaceSection } from './polish.ts';
5
5
  import { summarizeWorktreeOwnership, type WorktreeRegistry, type WorktreeStatusRecord } from '@pellux/goodvibes-sdk/platform/runtime/worktree/registry';
6
6
 
@@ -22,10 +22,8 @@ function stateColor(state: WorktreeStatusRecord['state']): string {
22
22
  }
23
23
  }
24
24
 
25
- export class WorktreePanel extends BasePanel {
25
+ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
26
26
  private rows: WorktreeStatusRecord[] = [];
27
- private selectedIndex = 0;
28
- private scrollOffset = 0;
29
27
  private loading = false;
30
28
  private readonly worktreeRegistry: WorktreeRegistry;
31
29
 
@@ -45,18 +43,21 @@ export class WorktreePanel extends BasePanel {
45
43
  void this.refresh();
46
44
  return true;
47
45
  }
48
- if (this.rows.length === 0) return false;
49
- if (key === 'up' || key === 'k') {
50
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
51
- this.markDirty();
52
- return true;
53
- }
54
- if (key === 'down' || key === 'j') {
55
- this.selectedIndex = Math.min(this.rows.length - 1, this.selectedIndex + 1);
56
- this.markDirty();
57
- return true;
58
- }
59
- return false;
46
+ return super.handleInput(key);
47
+ }
48
+
49
+ protected getItems(): readonly WorktreeStatusRecord[] {
50
+ return this.rows;
51
+ }
52
+
53
+ protected renderItem(row: WorktreeStatusRecord, index: number, _selected: boolean, width: number): Line {
54
+ const bg = index === this.selectedIndex ? C.headerBg : undefined;
55
+ return buildPanelLine(width, [
56
+ [` ${row.kind}`.padEnd(14), C.info, bg],
57
+ [` ${row.state}`.padEnd(16), stateColor(row.state), bg],
58
+ [` ${row.branch}`.padEnd(24), C.value, bg],
59
+ [` ${row.path}`.slice(0, Math.max(0, width - 56)), C.dim, bg],
60
+ ]);
60
61
  }
61
62
 
62
63
  private async refresh(): Promise<void> {
@@ -64,7 +65,7 @@ export class WorktreePanel extends BasePanel {
64
65
  this.markDirty();
65
66
  try {
66
67
  this.rows = await this.worktreeRegistry.list();
67
- this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.rows.length - 1));
68
+ this.clampSelection();
68
69
  } finally {
69
70
  this.loading = false;
70
71
  this.markDirty();
@@ -155,14 +156,14 @@ export class WorktreePanel extends BasePanel {
155
156
  ]);
156
157
  }),
157
158
  selectedIndex: this.selectedIndex,
158
- scrollOffset: this.scrollOffset,
159
+ scrollOffset: this.scrollStart,
159
160
  guardRows: 1,
160
161
  minRows: 4,
161
162
  appendWindowSummary: { dimColor: C.dim },
162
163
  },
163
164
  afterSections: [detailSection],
164
165
  });
165
- this.scrollOffset = resolvedWorktreesSection.scrollOffset;
166
+ this.scrollStart = resolvedWorktreesSection.scrollOffset;
166
167
  sections.push(resolvedWorktreesSection.section);
167
168
  sections.push(detailSection);
168
169
  }
@@ -1,9 +1,9 @@
1
- import type { Line } from '@pellux/goodvibes-sdk/platform/types/grid';
1
+ import type { Line } from '../types/grid.ts';
2
2
  import type { WrfcChain, WrfcState, QualityGateResult } from '@pellux/goodvibes-sdk/platform/agents/wrfc-types';
3
3
  import type { WrfcController } from '@pellux/goodvibes-sdk/platform/agents/wrfc-controller';
4
4
  import { BasePanel } from './base-panel.ts';
5
5
  import type { WorkflowEvent } from '@pellux/goodvibes-sdk/platform/runtime/events/index';
6
- import type { UiEventFeed } from '@pellux/goodvibes-sdk/platform/runtime/ui-events';
6
+ import type { UiEventFeed } from '../runtime/ui-events.ts';
7
7
  import {
8
8
  buildPanelLine,
9
9
  buildPanelWorkspace,
@@ -14,7 +14,7 @@ import {
14
14
  buildStyledPanelLine,
15
15
  buildEmptyState,
16
16
  } from './polish.ts';
17
- import { getDisplayWidth } from '@pellux/goodvibes-sdk/platform/utils/terminal-width';
17
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
18
18
 
19
19
  // ---------------------------------------------------------------------------
20
20
  // Colour palette
@@ -1,29 +1,10 @@
1
- import { type Line } from '@pellux/goodvibes-sdk/platform/types/grid';
1
+ import { type Line } from '../types/grid.ts';
2
2
  import { UIFactory } from '../renderer/ui-factory.ts';
3
3
  import type { PermissionCategory, PermissionRequestAnalysis } from '@pellux/goodvibes-sdk/platform/permissions/types';
4
4
  import { buildPermissionApprovalBrief, getDisplayArg } from '@pellux/goodvibes-sdk/platform/permissions/briefs/build';
5
5
 
6
- export interface PermissionPromptRequest {
7
- callId: string;
8
- tool: string;
9
- args: Record<string, unknown>;
10
- category: PermissionCategory;
11
- analysis: PermissionRequestAnalysis;
12
- workingDirectory?: string;
13
- }
14
-
15
- export interface PermissionPromptDecision {
16
- approved: boolean;
17
- remember?: boolean;
18
- }
19
-
20
- export type PermissionRequestHandler = (
21
- request: PermissionPromptRequest,
22
- ) => Promise<PermissionPromptDecision>;
23
-
24
- export interface PermissionRequest extends PermissionPromptRequest {
25
- resolve: (approved: boolean, remember?: boolean) => void;
26
- }
6
+ import type { PermissionPromptRequest, PermissionPromptDecision, PermissionRequestHandler, PermissionRequest } from '@pellux/goodvibes-sdk/platform/permissions/prompt';
7
+ export type { PermissionPromptRequest, PermissionPromptDecision, PermissionRequestHandler, PermissionRequest };
27
8
 
28
9
  /**
29
10
  * PermissionPromptUI - Renders a permission prompt as Line[] fragments.
@@ -1,304 +1,15 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
2
- import { join, resolve, isAbsolute } from 'path';
3
- import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
4
- import { createPluginAPI, type PluginAPIContext } from '@pellux/goodvibes-sdk/platform/plugins/api';
5
- import type { CommandRegistry } from '../input/command-registry.ts';
6
- import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers/registry';
7
- import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools/registry';
8
- import type { RuntimeEventBus } from '@pellux/goodvibes-sdk/platform/runtime/events/index';
9
- import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane/index';
10
- import type { ChannelDeliveryRouter, ChannelPluginRegistry } from '@pellux/goodvibes-sdk/platform/channels/index';
11
- import type { MemoryEmbeddingProviderRegistry } from '@pellux/goodvibes-sdk/platform/state/index';
12
- import type { VoiceProviderRegistry } from '@pellux/goodvibes-sdk/platform/voice/index';
13
- import type { MediaProviderRegistry } from '@pellux/goodvibes-sdk/platform/media/index';
14
- import type { WebSearchProviderRegistry } from '@pellux/goodvibes-sdk/platform/web-search/index';
15
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
16
-
17
- export interface PluginPathOptions {
18
- readonly cwd: string;
19
- readonly homeDir: string;
20
- }
21
-
22
- /**
23
- * Plugin search directories in precedence order.
24
- * Project-local plugins override global plugins with the same manifest name.
25
- */
26
- export function getUserPluginDirectory(options: PluginPathOptions): string {
27
- return join(options.homeDir, '.goodvibes', 'tui', 'plugins');
28
- }
29
-
30
- export function getPluginDirectories(options: PluginPathOptions): string[] {
31
- return [
32
- join(options.cwd, '.goodvibes', 'plugins'),
33
- join(options.cwd, '.goodvibes', 'tui', 'plugins'),
34
- getUserPluginDirectory(options),
35
- ];
36
- }
37
-
38
- /**
39
- * PluginManifest — The structure of a plugin's manifest.json.
40
- */
41
- export interface PluginManifest {
42
- /** Unique plugin identifier (no spaces, lowercase-kebab). */
43
- name: string;
44
- version: string;
45
- description: string;
46
- author?: string;
47
- /** Entry point relative to plugin directory. Defaults to "index.ts". */
48
- main?: string;
49
- /** Optional list of runtime event names the plugin subscribes to. */
50
- hooks?: string[];
51
- }
52
-
53
- /**
54
- * PluginEntryPoint — The exports expected from a plugin's entry file.
55
- */
56
- export interface PluginEntryPoint {
57
- /** Called once after the plugin is loaded. Receives the sandboxed PluginAPI. */
58
- init(api: ReturnType<typeof createPluginAPI>): void | Promise<void>;
59
- /** Optional: called when the plugin is activated (after init). */
60
- activate?(): void | Promise<void>;
61
- /** Optional: called when the plugin is deactivated (before cleanup). */
62
- deactivate?(): void | Promise<void>;
63
- }
64
-
65
- /**
66
- * LoadedPlugin — Runtime state of a single loaded plugin.
67
- */
68
- export interface LoadedPlugin {
69
- manifest: PluginManifest;
70
- /** Absolute path to the plugin directory. */
71
- pluginDir: string;
72
- /** Whether the plugin is currently active (init + activate completed). */
73
- active: boolean;
74
- /** Cleanup callbacks accumulated during plugin API use. */
75
- cleanup: Array<() => void>;
76
- /** The resolved entry point module (available after load). */
77
- entry?: PluginEntryPoint;
78
- }
79
-
80
- /**
81
- * DiscoveredPlugin — Result of scanning the plugins directory.
82
- */
83
- export interface DiscoveredPlugin {
84
- pluginDir: string;
85
- manifest: PluginManifest;
86
- }
87
-
88
- /**
89
- * discoverPlugins — Scan the configured plugin directories for valid plugin folders.
90
- * Each subdirectory with a readable manifest.json is a candidate.
91
- */
92
- function scanPluginDirectory(rootDir: string): DiscoveredPlugin[] {
93
- if (!existsSync(rootDir)) return [];
94
- const results: DiscoveredPlugin[] = [];
95
- let entries: string[];
96
- try {
97
- entries = readdirSync(rootDir);
98
- } catch (err) {
99
- logger.warn(`[plugins] Could not read plugins directory '${rootDir}': ${summarizeError(err)}`);
100
- return [];
101
- }
102
-
103
- for (const entry of entries) {
104
- const pluginDir = join(rootDir, entry);
105
- try {
106
- if (!statSync(pluginDir).isDirectory()) continue;
107
-
108
- const manifestPath = join(pluginDir, 'manifest.json');
109
- if (!existsSync(manifestPath)) continue;
110
-
111
- const raw = readFileSync(manifestPath, 'utf-8');
112
- const manifest = JSON.parse(raw) as PluginManifest;
113
-
114
- if (!manifest.name || !manifest.version) {
115
- logger.warn(`[plugins] ${entry}: manifest.json missing required fields (name, version)`);
116
- continue;
117
- }
118
-
119
- // Validate manifest field types
120
- if (typeof manifest.name !== 'string' || typeof manifest.version !== 'string') {
121
- logger.warn(`[plugins] ${entry}: manifest.json 'name' and 'version' must be strings`);
122
- continue;
123
- }
124
- if (manifest.main !== undefined) {
125
- if (typeof manifest.main !== 'string') {
126
- logger.warn(`[plugins] ${entry}: manifest.json 'main' must be a string`);
127
- continue;
128
- }
129
- if (isAbsolute(manifest.main)) {
130
- logger.warn(`[plugins] ${entry}: manifest.json 'main' must be a relative path, not absolute`);
131
- continue;
132
- }
133
- }
134
-
135
- results.push({ pluginDir, manifest });
136
- } catch (err) {
137
- logger.warn(`[plugins] ${entry}: failed to parse manifest — ${summarizeError(err)}`);
138
- }
139
- }
140
-
141
- return results;
142
- }
143
-
144
- export function discoverPlugins(options: PluginPathOptions): DiscoveredPlugin[] {
145
- const discovered = new Map<string, DiscoveredPlugin>();
146
- for (const dir of getPluginDirectories(options)) {
147
- for (const plugin of scanPluginDirectory(dir)) {
148
- if (!discovered.has(plugin.manifest.name)) {
149
- discovered.set(plugin.manifest.name, plugin);
150
- }
151
- }
152
- }
153
- return [...discovered.values()];
154
- }
155
-
156
- /**
157
- * PluginLoaderDeps — External dependencies injected into the loader.
158
- */
159
- export interface PluginLoaderDeps {
160
- runtimeBus: RuntimeEventBus;
161
- commandRegistry: CommandRegistry;
162
- providerRegistry: ProviderRegistry;
163
- toolRegistry: ToolRegistry;
164
- gatewayMethods: GatewayMethodCatalog;
165
- channelRegistry: ChannelPluginRegistry;
166
- channelDeliveryRouter: ChannelDeliveryRouter;
167
- memoryEmbeddingRegistry: MemoryEmbeddingProviderRegistry;
168
- voiceProviderRegistry: VoiceProviderRegistry;
169
- mediaProviderRegistry: MediaProviderRegistry;
170
- webSearchProviderRegistry: WebSearchProviderRegistry;
171
- /** Returns plugin-specific config given a plugin name. */
172
- getPluginConfig(name: string): Record<string, unknown>;
173
- /** Returns whether a plugin is enabled in persistent state. */
174
- isEnabled(name: string): boolean;
175
- }
176
-
177
- /**
178
- * loadPlugin — Load, init, and activate a single plugin.
179
- * Returns a LoadedPlugin on success, or null on failure.
180
- *
181
- * @param cacheBust - Optional timestamp suffix appended to the import URL to bypass
182
- * Bun's module cache. Pass `Date.now()` on reload to force fresh execution.
183
- */
184
- export async function loadPlugin(
185
- discovered: DiscoveredPlugin,
186
- deps: PluginLoaderDeps,
187
- cacheBust?: number,
188
- ): Promise<LoadedPlugin | null> {
189
- const { manifest, pluginDir } = discovered;
190
- const entryFile = manifest.main ?? 'index.ts';
191
- const entryPath = join(pluginDir, entryFile);
192
-
193
- // Path traversal guard: resolved entry must remain within pluginDir
194
- const resolvedEntry = resolve(entryPath);
195
- const resolvedPluginDir = resolve(pluginDir);
196
- if (!resolvedEntry.startsWith(resolvedPluginDir + '/') && resolvedEntry !== resolvedPluginDir) {
197
- logger.error(`[plugins] ${manifest.name}: path traversal detected — entry '${entryFile}' resolves outside plugin directory`);
198
- return null;
199
- }
200
-
201
- if (!existsSync(entryPath)) {
202
- logger.warn(`[plugins] ${manifest.name}: entry file not found: ${entryPath}`);
203
- return null;
204
- }
205
-
206
- // Trust notice — plugins run as trusted code (like VS Code extensions)
207
- logger.warn(`[plugins] Loading '${manifest.name}' — plugins are trusted code and run with full application access`);
208
-
209
- const loaded: LoadedPlugin = {
210
- manifest,
211
- pluginDir,
212
- active: false,
213
- cleanup: [],
214
- };
215
-
216
- try {
217
- // Dynamic import — Bun supports TS imports directly.
218
- // Append cache-bust query param on reload so Bun re-executes the module.
219
- const importPath = cacheBust !== undefined ? `${entryPath}?t=${cacheBust}` : entryPath;
220
- const mod = await import(importPath) as unknown;
221
-
222
- // Validate module shape before casting
223
- if (!mod || typeof mod !== 'object') {
224
- logger.warn(`[plugins] ${manifest.name}: entry file did not export a module object`);
225
- return null;
226
- }
227
- const modObj = mod as Record<string, unknown>;
228
- if (typeof modObj['init'] !== 'function') {
229
- logger.warn(`[plugins] ${manifest.name}: entry file must export an init() function`);
230
- return null;
231
- }
232
- if (modObj['activate'] !== undefined && typeof modObj['activate'] !== 'function') {
233
- logger.warn(`[plugins] ${manifest.name}: entry file 'activate' export must be a function`);
234
- return null;
235
- }
236
- if (modObj['deactivate'] !== undefined && typeof modObj['deactivate'] !== 'function') {
237
- logger.warn(`[plugins] ${manifest.name}: entry file 'deactivate' export must be a function`);
238
- return null;
239
- }
240
- const entry = mod as PluginEntryPoint;
241
-
242
- loaded.entry = entry;
243
-
244
- const ctx: PluginAPIContext = {
245
- pluginName: manifest.name,
246
- runtimeBus: deps.runtimeBus,
247
- commandRegistry: deps.commandRegistry as PluginAPIContext['commandRegistry'],
248
- providerRegistry: deps.providerRegistry,
249
- toolRegistry: deps.toolRegistry,
250
- gatewayMethods: deps.gatewayMethods,
251
- channelRegistry: deps.channelRegistry,
252
- channelDeliveryRouter: deps.channelDeliveryRouter,
253
- memoryEmbeddingRegistry: deps.memoryEmbeddingRegistry,
254
- voiceProviderRegistry: deps.voiceProviderRegistry,
255
- mediaProviderRegistry: deps.mediaProviderRegistry,
256
- webSearchProviderRegistry: deps.webSearchProviderRegistry,
257
- pluginConfig: deps.getPluginConfig(manifest.name),
258
- cleanup: loaded.cleanup,
259
- };
260
-
261
- const api = createPluginAPI(ctx);
262
-
263
- // Lifecycle: init
264
- await entry.init(api);
265
-
266
- // Lifecycle: activate
267
- if (typeof entry.activate === 'function') {
268
- await entry.activate();
269
- }
270
-
271
- loaded.active = true;
272
- logger.info(`[plugins] ${manifest.name} v${manifest.version} activated`);
273
- return loaded;
274
- } catch (err) {
275
- logger.error(`[plugins] ${manifest.name}: load failed — ${summarizeError(err)}`);
276
- // Run cleanup for anything that was registered before the error
277
- for (const fn of loaded.cleanup) {
278
- try { fn(); } catch { /* best-effort */ }
279
- }
280
- return null;
281
- }
282
- }
283
-
284
- /**
285
- * unloadPlugin — Deactivate a plugin and run all cleanup callbacks.
286
- */
287
- export async function unloadPlugin(plugin: LoadedPlugin): Promise<void> {
288
- if (!plugin.active) return;
289
-
290
- try {
291
- if (typeof plugin.entry?.deactivate === 'function') {
292
- await plugin.entry.deactivate();
293
- }
294
- } catch (err) {
295
- logger.warn(`[plugins] ${plugin.manifest.name}: deactivate threw — ${summarizeError(err)}`);
296
- }
297
-
298
- for (const fn of plugin.cleanup) {
299
- try { fn(); } catch { /* best-effort */ }
300
- }
301
- plugin.cleanup.length = 0;
302
- plugin.active = false;
303
- logger.info(`[plugins] ${plugin.manifest.name} deactivated`);
304
- }
1
+ export {
2
+ getUserPluginDirectory,
3
+ getPluginDirectories,
4
+ discoverPlugins,
5
+ loadPlugin,
6
+ unloadPlugin,
7
+ } from '@pellux/goodvibes-sdk/platform/plugins/loader';
8
+ export type {
9
+ PluginPathOptions,
10
+ PluginManifest,
11
+ PluginEntryPoint,
12
+ LoadedPlugin,
13
+ DiscoveredPlugin,
14
+ PluginLoaderDeps,
15
+ } from '@pellux/goodvibes-sdk/platform/plugins/loader';
@@ -1,6 +1,6 @@
1
1
  import { join } from 'path';
2
2
  import { readFile } from 'fs/promises';
3
- import { type Line } from '@pellux/goodvibes-sdk/platform/types/grid';
3
+ import { type Line } from '../types/grid.ts';
4
4
  import { ModalFactory } from './modal-factory.ts';
5
5
  import type { AgentManager } from '@pellux/goodvibes-sdk/platform/tools/agent/index';
6
6
  import type { AgentMessageBus } from '@pellux/goodvibes-sdk/platform/agents/message-bus';
@@ -1,5 +1,5 @@
1
- import { type Line } from '@pellux/goodvibes-sdk/platform/types/grid';
2
- import { fitDisplay, getDisplayWidth, truncateDisplay } from '@pellux/goodvibes-sdk/platform/utils/terminal-width';
1
+ import { type Line } from '../types/grid.ts';
2
+ import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
3
3
  import type { AutocompleteEngine } from '../input/autocomplete.ts';
4
4
  import {
5
5
  createOverlayBorderLine,
@@ -7,7 +7,7 @@
7
7
  * Footer hints: [Up/Down] Navigate [Enter] Jump [o] Open File [d] Remove [Esc] Close
8
8
  */
9
9
 
10
- import { type Line } from '@pellux/goodvibes-sdk/platform/types/grid';
10
+ import { type Line } from '../types/grid.ts';
11
11
  import { ModalFactory } from './modal-factory.ts';
12
12
  import { BookmarkModal } from '../input/bookmark-modal.ts';
13
13
  import type { BookmarkEntry } from '@pellux/goodvibes-sdk/platform/bookmarks/manager';
@@ -1,5 +1,5 @@
1
- import { createEmptyLine, createStyledCell, type Line } from '@pellux/goodvibes-sdk/platform/types/grid';
2
- import { getDisplayWidth } from '@pellux/goodvibes-sdk/platform/utils/terminal-width';
1
+ import { createEmptyLine, createStyledCell, type Line } from '../types/grid.ts';
2
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
3
3
 
4
4
  export interface BottomBarStyle {
5
5
  readonly fg: string;
@@ -1,4 +1,4 @@
1
- import { type Line, type Cell, createEmptyLine, createEmptyCell } from '@pellux/goodvibes-sdk/platform/types/grid';
1
+ import { type Line, type Cell, createEmptyLine, createEmptyCell } from '../types/grid.ts';
2
2
 
3
3
  /**
4
4
  * TerminalBuffer - Represents a 2D grid of styled cells.
@@ -1,6 +1,6 @@
1
- import { type Line, type Cell, createStyledCell, createEmptyLine } from '@pellux/goodvibes-sdk/platform/types/grid';
1
+ import { type Line, type Cell, createStyledCell, createEmptyLine } from '../types/grid.ts';
2
2
  import { UIFactory } from './ui-factory.ts';
3
- import { getDisplayWidth } from '@pellux/goodvibes-sdk/platform/utils/terminal-width';
3
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
4
4
  import { LAYOUT } from './layout.ts';
5
5
  import { SyntaxHighlighter, type SyntaxToken as HLToken } from './syntax-highlighter.ts';
6
6
 
@@ -1,7 +1,7 @@
1
1
  import { TerminalBuffer } from './buffer.ts';
2
2
  import { DiffEngine } from './diff.ts';
3
- import { type Line, createStyledCell } from '@pellux/goodvibes-sdk/platform/types/grid';
4
- import { getDisplayWidth } from '@pellux/goodvibes-sdk/platform/utils/terminal-width';
3
+ import { type Line, createStyledCell } from '../types/grid.ts';
4
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
5
5
  import type { SearchManager } from '../input/search.ts';
6
6
 
7
7
  export interface SelectionInfo {
@@ -1,4 +1,4 @@
1
- import { type Line } from '@pellux/goodvibes-sdk/platform/types/grid';
1
+ import { type Line } from '../types/grid.ts';
2
2
  import { ModalFactory } from './modal-factory.ts';
3
3
  import type { ConversationManager } from '../core/conversation';
4
4
  import { getOverlayContentBudget, getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
@@ -1,5 +1,5 @@
1
- import type { Line } from '@pellux/goodvibes-sdk/platform/types/grid';
2
- import { createEmptyLine } from '@pellux/goodvibes-sdk/platform/types/grid';
1
+ import type { Line } from '../types/grid.ts';
2
+ import { createEmptyLine } from '../types/grid.ts';
3
3
  import type { ConversationManager } from '../core/conversation';
4
4
 
5
5
  export interface ConversationViewportRequest {
@@ -1,4 +1,4 @@
1
- import type { Line } from '@pellux/goodvibes-sdk/platform/types/grid';
1
+ import type { Line } from '../types/grid.ts';
2
2
  import type { ConversationManager } from '../core/conversation';
3
3
  import type { CommandRegistry } from '../input/command-registry.ts';
4
4
  import type { InputHandler } from '../input/handler.ts';
@@ -1,5 +1,5 @@
1
- import { type Line, createEmptyLine, createStyledCell } from '@pellux/goodvibes-sdk/platform/types/grid';
2
- import { getDisplayWidth, truncateDisplay, wrapText } from '@pellux/goodvibes-sdk/platform/utils/terminal-width';
1
+ import { type Line, createEmptyLine, createStyledCell } from '../types/grid.ts';
2
+ import { getDisplayWidth, truncateDisplay, wrapText } from '../utils/terminal-width.ts';
3
3
  import { LAYOUT } from './layout.ts';
4
4
  import { GLYPHS } from './ui-primitives.ts';
5
5
 
@@ -1,6 +1,6 @@
1
- import { type Line, type Cell, createStyledCell } from '@pellux/goodvibes-sdk/platform/types/grid';
1
+ import { type Line, type Cell, createStyledCell } from '../types/grid.ts';
2
2
  import { UIFactory } from './ui-factory.ts';
3
- import { getDisplayWidth } from '@pellux/goodvibes-sdk/platform/utils/terminal-width';
3
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
4
4
 
5
5
  /**
6
6
  * renderDiffView - Render a unified diff string as styled Line[].
@@ -1,5 +1,5 @@
1
1
  import { TerminalBuffer } from './buffer.ts';
2
- import { type Cell } from '@pellux/goodvibes-sdk/platform/types/grid';
2
+ import { type Cell } from '../types/grid.ts';
3
3
 
4
4
  /**
5
5
  * DiffEngine - Generates minimal ANSI updates between two buffers.
@@ -1,5 +1,5 @@
1
- import { type Line } from '@pellux/goodvibes-sdk/platform/types/grid';
2
- import { fitDisplay, getDisplayWidth, truncateDisplay } from '@pellux/goodvibes-sdk/platform/utils/terminal-width';
1
+ import { type Line } from '../types/grid.ts';
2
+ import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
3
3
  import type { FilePickerModal } from '../input/file-picker.ts';
4
4
  import {
5
5
  createOverlayBoxLayout,
@@ -1,6 +1,6 @@
1
- import { type Line, createStyledCell } from '@pellux/goodvibes-sdk/platform/types/grid';
1
+ import { type Line, createStyledCell } from '../types/grid.ts';
2
2
  import { UIFactory } from './ui-factory.ts';
3
- import { getDisplayWidth } from '@pellux/goodvibes-sdk/platform/utils/terminal-width';
3
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
4
4
 
5
5
  /** Color by file extension category. */
6
6
  function getFileColor(name: string): string {
@@ -4,7 +4,7 @@
4
4
  * Toggle with `?` key or `/help` command.
5
5
  */
6
6
 
7
- import { type Line } from '@pellux/goodvibes-sdk/platform/types/grid';
7
+ import { type Line } from '../types/grid.ts';
8
8
  import { ModalFactory } from './modal-factory.ts';
9
9
  import type { SlashCommand } from '../input/command-registry.ts';
10
10
  import type { KeybindingsManager } from '../input/keybindings.ts';
@@ -1,5 +1,5 @@
1
- import type { Line } from '@pellux/goodvibes-sdk/platform/types/grid';
2
- import { getDisplayWidth } from '@pellux/goodvibes-sdk/platform/utils/terminal-width';
1
+ import type { Line } from '../types/grid.ts';
2
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
3
3
  import type { HistorySearch } from '../input/input-history.ts';
4
4
  import { createBottomBarLine, writeBottomBarText } from './bottom-bar.ts';
5
5
 
@@ -1,4 +1,4 @@
1
- import { type Line } from '@pellux/goodvibes-sdk/platform/types/grid';
1
+ import { type Line } from '../types/grid.ts';
2
2
  import { ModalFactory } from './modal-factory.ts';
3
3
  import type { ProcessManager } from '@pellux/goodvibes-sdk/platform/tools/shared/process-manager';
4
4
  import type { AgentManager } from '@pellux/goodvibes-sdk/platform/tools/agent/index';
@@ -1,7 +1,7 @@
1
- import { type Line, type Cell, createStyledCell } from '@pellux/goodvibes-sdk/platform/types/grid';
1
+ import { type Line, type Cell, createStyledCell } from '../types/grid.ts';
2
2
  import { UIFactory } from './ui-factory.ts';
3
3
  import { renderCodeBlock } from './code-block.ts';
4
- import { getDisplayWidth } from '@pellux/goodvibes-sdk/platform/utils/terminal-width';
4
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
5
5
  import { LAYOUT } from './layout.ts';
6
6
 
7
7
  export interface MarkdownRenderOptions {
@@ -1,6 +1,6 @@
1
- import type { Line, Cell } from '@pellux/goodvibes-sdk/platform/types/grid';
2
- import { createStyledCell } from '@pellux/goodvibes-sdk/platform/types/grid';
3
- import { fitDisplay, getDisplayWidth, truncateDisplay, wrapText } from '@pellux/goodvibes-sdk/platform/utils/terminal-width';
1
+ import type { Line, Cell } from '../types/grid.ts';
2
+ import { createStyledCell } from '../types/grid.ts';
3
+ import { fitDisplay, getDisplayWidth, truncateDisplay, wrapText } from '../utils/terminal-width.ts';
4
4
  import {
5
5
  createOverlayBorderLine,
6
6
  createOverlayBoxLayout,
@@ -1,5 +1,5 @@
1
- import { type Line } from '@pellux/goodvibes-sdk/platform/types/grid';
2
- import { fitDisplay, getDisplayWidth, truncateDisplay } from '@pellux/goodvibes-sdk/platform/utils/terminal-width';
1
+ import { type Line } from '../types/grid.ts';
2
+ import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
3
3
  import type { ModelPickerModal } from '../input/model-picker.ts';
4
4
  import { EFFORT_DESCRIPTIONS } from '@pellux/goodvibes-sdk/platform/providers/effort-levels';
5
5
  import { getQualityTier, getQualityTierFromScore } from '@pellux/goodvibes-sdk/platform/providers/model-benchmarks';