@pellux/goodvibes-tui 0.28.0 → 0.29.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 (118) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/input/commands/cost-runtime.ts +49 -0
  5. package/src/input/commands/operator-runtime.ts +5 -1
  6. package/src/input/commands.ts +2 -0
  7. package/src/input/handler-feed-routes.ts +12 -35
  8. package/src/input/handler-feed.ts +4 -8
  9. package/src/input/handler-shortcuts.ts +51 -0
  10. package/src/input/handler.ts +43 -6
  11. package/src/input/keybindings.ts +48 -8
  12. package/src/input/panel-integration-actions.ts +107 -1
  13. package/src/main.ts +5 -1
  14. package/src/panels/agent-inspector-panel.ts +125 -36
  15. package/src/panels/agent-inspector-shared.ts +144 -0
  16. package/src/panels/approval-panel.ts +67 -16
  17. package/src/panels/automation-control-panel.ts +368 -124
  18. package/src/panels/builtin/agent.ts +28 -37
  19. package/src/panels/builtin/development.ts +40 -32
  20. package/src/panels/builtin/knowledge.ts +15 -6
  21. package/src/panels/builtin/operations.ts +178 -117
  22. package/src/panels/builtin/session.ts +35 -9
  23. package/src/panels/builtin/shared.ts +98 -6
  24. package/src/panels/cockpit-panel.ts +232 -73
  25. package/src/panels/communication-panel.ts +58 -20
  26. package/src/panels/confirm-state.ts +8 -1
  27. package/src/panels/control-plane-panel.ts +351 -118
  28. package/src/panels/cost-tracker-panel.ts +165 -7
  29. package/src/panels/debug-panel.ts +312 -159
  30. package/src/panels/diff-panel.ts +203 -57
  31. package/src/panels/docs-panel.ts +152 -66
  32. package/src/panels/eval-panel.ts +220 -42
  33. package/src/panels/file-explorer-panel.ts +202 -122
  34. package/src/panels/file-preview-panel.ts +132 -57
  35. package/src/panels/git-panel.ts +309 -128
  36. package/src/panels/hooks-panel.ts +150 -22
  37. package/src/panels/incident-review-panel.ts +223 -35
  38. package/src/panels/index.ts +0 -4
  39. package/src/panels/intelligence-panel.ts +212 -86
  40. package/src/panels/knowledge-graph-panel.ts +461 -101
  41. package/src/panels/local-auth-panel.ts +240 -28
  42. package/src/panels/marketplace-panel.ts +193 -26
  43. package/src/panels/memory-panel.ts +78 -77
  44. package/src/panels/ops-control-panel.ts +146 -29
  45. package/src/panels/ops-strategy-panel.ts +72 -4
  46. package/src/panels/orchestration-panel.ts +231 -69
  47. package/src/panels/panel-list-panel.ts +154 -131
  48. package/src/panels/panel-manager.ts +97 -9
  49. package/src/panels/plan-dashboard-panel.ts +333 -17
  50. package/src/panels/plugins-panel.ts +184 -29
  51. package/src/panels/policy-panel.ts +210 -38
  52. package/src/panels/polish-core.ts +7 -2
  53. package/src/panels/polish.ts +23 -4
  54. package/src/panels/project-planning-answer-actions.ts +134 -0
  55. package/src/panels/project-planning-panel.ts +62 -113
  56. package/src/panels/provider-health-panel.ts +434 -518
  57. package/src/panels/provider-health-routes.ts +203 -0
  58. package/src/panels/provider-health-tracker.ts +194 -6
  59. package/src/panels/provider-health-views.ts +560 -0
  60. package/src/panels/qr-panel.ts +116 -30
  61. package/src/panels/remote-panel.ts +114 -36
  62. package/src/panels/routes-panel.ts +63 -22
  63. package/src/panels/sandbox-panel.ts +174 -42
  64. package/src/panels/scrollable-list-panel.ts +19 -135
  65. package/src/panels/security-panel.ts +133 -33
  66. package/src/panels/services-panel.ts +116 -64
  67. package/src/panels/session-browser-panel.ts +73 -2
  68. package/src/panels/session-maintenance.ts +4 -122
  69. package/src/panels/settings-sync-panel.ts +335 -72
  70. package/src/panels/skills-panel.ts +157 -89
  71. package/src/panels/subscription-panel.ts +86 -33
  72. package/src/panels/symbol-outline-panel.ts +248 -108
  73. package/src/panels/system-messages-panel.ts +114 -13
  74. package/src/panels/tasks-panel.ts +326 -139
  75. package/src/panels/thinking-panel.ts +43 -10
  76. package/src/panels/token-budget-panel.ts +194 -18
  77. package/src/panels/tool-inspector-panel.ts +144 -34
  78. package/src/panels/types.ts +40 -4
  79. package/src/panels/work-plan-panel.ts +280 -17
  80. package/src/panels/worktree-panel.ts +175 -42
  81. package/src/panels/wrfc-panel.ts +116 -24
  82. package/src/renderer/conversation-overlays.ts +25 -11
  83. package/src/renderer/footer-tips.ts +41 -0
  84. package/src/renderer/fullscreen-primitives.ts +22 -16
  85. package/src/renderer/help-overlay.ts +91 -14
  86. package/src/renderer/hint-grammar.ts +52 -0
  87. package/src/renderer/layout.ts +7 -7
  88. package/src/renderer/modal-factory.ts +23 -15
  89. package/src/renderer/model-picker-overlay.ts +21 -7
  90. package/src/renderer/overlay-box.ts +16 -10
  91. package/src/renderer/process-indicator.ts +3 -1
  92. package/src/renderer/progress.ts +5 -4
  93. package/src/renderer/search-overlay.ts +27 -6
  94. package/src/renderer/session-picker-modal.ts +6 -4
  95. package/src/renderer/settings-modal.ts +70 -10
  96. package/src/renderer/shell-surface.ts +41 -15
  97. package/src/renderer/status-glyphs.ts +11 -9
  98. package/src/renderer/theme.ts +60 -3
  99. package/src/renderer/ui-factory.ts +41 -30
  100. package/src/renderer/ui-primitives.ts +23 -2
  101. package/src/runtime/bootstrap-shell.ts +35 -18
  102. package/src/runtime/diagnostics/panels/index.ts +0 -3
  103. package/src/shell/ui-openers.ts +14 -22
  104. package/src/utils/format-duration.ts +2 -2
  105. package/src/utils/splash-lines.ts +44 -3
  106. package/src/version.ts +1 -1
  107. package/src/work-plans/work-plan-store.ts +13 -0
  108. package/src/panels/agent-logs-panel.ts +0 -635
  109. package/src/panels/agent-logs-shared.ts +0 -129
  110. package/src/panels/context-visualizer-panel.ts +0 -238
  111. package/src/panels/forensics-panel.ts +0 -378
  112. package/src/panels/provider-account-snapshot.ts +0 -259
  113. package/src/panels/provider-accounts-panel.ts +0 -255
  114. package/src/panels/provider-stats-panel.ts +0 -391
  115. package/src/panels/schedule-panel.ts +0 -365
  116. package/src/panels/watchers-panel.ts +0 -213
  117. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  118. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -109,12 +109,41 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
109
109
  homeDirectory: services.homeDirectory,
110
110
  });
111
111
 
112
+ const foundationClients = createRuntimeFoundationClients({
113
+ runtimeServices: services,
114
+ tasksReadModel: uiServices.readModels.tasks,
115
+ taskManager,
116
+ opsControlPlane,
117
+ });
118
+ const {
119
+ directTransport,
120
+ hookApi,
121
+ knowledgeApi,
122
+ mcpApi,
123
+ opsApi,
124
+ providerApi,
125
+ } = foundationClients;
126
+ const planRuntime = createShellPlanRuntime({
127
+ adaptivePlanner: services.adaptivePlanner,
128
+ runtimeBus,
129
+ });
130
+
112
131
  const openAgentDetailRef: { fn: (agentId: string) => void } = { fn: (_agentId: string) => {} };
113
132
 
133
+ // WO-139: initial cost-budget alert threshold (USD; 0/unset = disabled).
134
+ // Once the session starts, the real control surface is the CostTrackerPanel
135
+ // itself — the in-panel 'b' key and /cost budget <usd> both call
136
+ // CostTrackerPanel.setBudgetThreshold() directly on the live panel instance.
137
+ const parsedBudgetThreshold = Number(process.env.GOODVIBES_COST_BUDGET_USD);
138
+ const initialCostBudgetThreshold = Number.isFinite(parsedBudgetThreshold) && parsedBudgetThreshold > 0
139
+ ? parsedBudgetThreshold
140
+ : 0;
141
+
114
142
  let commandContextRef: CommandContext | null = null;
115
143
  registerBuiltinPanels(services.panelManager, {
116
144
  configManager,
117
145
  getOrchestratorUsage: () => orchestrator.usage as { input: number; output: number; cacheRead: number; cacheWrite: number; model?: string },
146
+ budgetThreshold: initialCostBudgetThreshold,
118
147
  toolRegistry,
119
148
  providerRegistry: services.providerRegistry,
120
149
  contextWindow: services.providerRegistry.getContextWindowForModel(services.providerRegistry.getCurrentModel()),
@@ -153,6 +182,12 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
153
182
  mcpRegistry: services.mcpRegistry,
154
183
  openAgentDetail: (agentId: string) => openAgentDetailRef.fn(agentId),
155
184
  daemonHomeDir: join(services.homeDirectory, '.goodvibes', 'daemon'),
185
+ opsApi,
186
+ planRuntime,
187
+ watcherRegistry: services.watcherRegistry,
188
+ runtimeStore,
189
+ openPanel: (panelId: string) => { services.panelManager.open(panelId); },
190
+ knowledgeApi,
156
191
  });
157
192
  services.panelManager.prewarmRegistered();
158
193
 
@@ -170,29 +205,11 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
170
205
 
171
206
  const commandRegistry = new CommandRegistry();
172
207
  registerBuiltinCommands(commandRegistry);
173
- const foundationClients = createRuntimeFoundationClients({
174
- runtimeServices: services,
175
- tasksReadModel: uiServices.readModels.tasks,
176
- taskManager,
177
- opsControlPlane,
178
- });
179
- const {
180
- directTransport,
181
- hookApi,
182
- knowledgeApi,
183
- mcpApi,
184
- opsApi,
185
- providerApi,
186
- } = foundationClients;
187
208
  const remoteRuntime = createShellRemoteCommandService({
188
209
  readModels: uiServices.readModels,
189
210
  remoteRunnerRegistry: services.remoteRunnerRegistry,
190
211
  runtimeStore,
191
212
  });
192
- const planRuntime = createShellPlanRuntime({
193
- adaptivePlanner: services.adaptivePlanner,
194
- runtimeBus,
195
- });
196
213
 
197
214
  const commandContext: CommandContext = createBootstrapCommandContext({
198
215
  configManager,
@@ -12,13 +12,10 @@ export type { InspectableDomain } from '@/runtime/index.ts';
12
12
  export { HealthPanel } from '@/runtime/index.ts';
13
13
  export { DivergencePanel } from '@/runtime/index.ts';
14
14
  export { ReplayPanel } from '@/runtime/index.ts';
15
- export { PolicyPanel } from './policy.ts';
16
- export type { PolicyPanelSnapshot } from './policy.ts';
17
15
  export { ToolContractsPanel } from '@/runtime/index.ts';
18
16
  export { TransportPanel } from '@/runtime/index.ts';
19
17
  export type { TransportPanelSnapshot } from '@/runtime/index.ts';
20
18
  export { OpsPanel } from './ops.ts';
21
19
  export type { OpsAuditEntry } from './ops.ts';
22
- export { PanelResourcesPanel } from './panel-resources.ts';
23
20
  export { SecurityPanel } from '@/runtime/index.ts';
24
21
  export type { SecurityPanelSnapshot } from '@/runtime/index.ts';
@@ -253,6 +253,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
253
253
  if (!input.helpOverlayActive) input.modalOpened('help');
254
254
  input.helpOverlayActive = !input.helpOverlayActive;
255
255
  input.helpScrollOffset = 0;
256
+ render();
256
257
  };
257
258
 
258
259
  commandContext.openShortcutsOverlay = () => {
@@ -292,19 +293,16 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
292
293
  };
293
294
 
294
295
  commandContext.openPanelPicker = () => {
295
- if (!panelManager.isVisible()) {
296
- if (panelManager.getAllOpen().length === 0) {
297
- try {
298
- panelManager.open('panel-list');
299
- } catch {
300
- // non-fatal
301
- }
302
- }
303
- panelManager.show();
304
- input.panelFocused = true;
305
- conversation.setSplashSuppressed(true);
296
+ // Focus ownership lives in PanelManager (focusTarget); read it there rather
297
+ // than tracking a parallel input.panelFocused flag. Toggle semantics: if the
298
+ // workspace is visible AND already focused, hide it; otherwise reveal and
299
+ // focus it (opening the panel list when nothing is open yet).
300
+ if (panelManager.isVisible() && panelManager.getFocusTarget() === 'panel') {
301
+ panelManager.hide();
302
+ panelManager.focusPrompt();
303
+ conversation.setSplashSuppressed(false);
306
304
  conversation.rebuildHistory();
307
- } else if (!input.panelFocused) {
305
+ } else {
308
306
  if (panelManager.getAllOpen().length === 0) {
309
307
  try {
310
308
  panelManager.open('panel-list');
@@ -313,26 +311,20 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
313
311
  }
314
312
  }
315
313
  panelManager.show();
316
- input.panelFocused = true;
314
+ panelManager.focusPanels();
317
315
  conversation.setSplashSuppressed(true);
318
316
  conversation.rebuildHistory();
319
- } else {
320
- panelManager.hide();
321
- input.panelFocused = false;
322
- conversation.setSplashSuppressed(false);
323
- conversation.rebuildHistory();
324
317
  }
325
318
  render();
326
319
  };
327
320
 
328
321
  commandContext.focusPanels = () => {
329
- if (!panelManager.isVisible() || panelManager.getAllOpen().length === 0) return;
330
- input.panelFocused = true;
322
+ panelManager.focusPanels();
331
323
  render();
332
324
  };
333
325
 
334
326
  commandContext.focusPrompt = () => {
335
- input.panelFocused = false;
327
+ panelManager.focusPrompt();
336
328
  input.indicatorFocused = false;
337
329
  render();
338
330
  };
@@ -340,7 +332,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
340
332
  commandContext.showPanel = (panelId, pane) => {
341
333
  panelManager.open(panelId, pane);
342
334
  panelManager.show();
343
- input.panelFocused = true;
335
+ panelManager.focusPanels();
344
336
  conversation.setSplashSuppressed(true);
345
337
  conversation.rebuildHistory();
346
338
  render();
@@ -10,7 +10,7 @@
10
10
  /**
11
11
  * Format a latency value in milliseconds with sub-second precision.
12
12
  *
13
- * Used by: debug-panel, provider-health-panel, provider-stats-panel
13
+ * Used by: debug-panel, provider-health-panel
14
14
  *
15
15
  * ms <= 0 → 'n/a'
16
16
  * ms >= 10000 → '12.3s' (one decimal)
@@ -42,7 +42,7 @@ export function formatDuration(ms: number): string {
42
42
  /**
43
43
  * Format a short eval/forensics duration; treats undefined as '?ms'.
44
44
  *
45
- * Used by: forensics-panel, eval-panel
45
+ * Used by: incident-review-panel, eval-panel
46
46
  *
47
47
  * undefined → '?ms'
48
48
  * ms < 1000 → '500ms'
@@ -1,4 +1,4 @@
1
- import { center, getDisplayWidth } from './terminal-width.ts';
1
+ import { center, getDisplayWidth, truncateDisplay } from './terminal-width.ts';
2
2
  import { VERSION } from '../version.ts';
3
3
 
4
4
  const ART_LINES = [
@@ -22,15 +22,33 @@ const TAGLINE = '[ good vibes ・ A I ・ いい雰囲気
22
22
 
23
23
  const VERSION_LINE = ` ✦ v${VERSION} █ terminal AI assistant █ 自動コード  ✦`;
24
24
 
25
+ /** Fixed hint line — the three primary shell entry points. */
26
+ const HINT_LINE = 'Ctrl+P panels / ? help / F2 processes';
27
+
25
28
  export interface SplashOptions {
26
29
  workingDir?: string;
27
30
  model?: string;
28
31
  provider?: string;
29
32
  toolCount?: number;
33
+ /**
34
+ * Session id of the most recent session, as resolved by readLastSessionPointer.
35
+ * When present, the splash advertises a resume affordance.
36
+ */
37
+ lastSessionId?: string;
38
+ }
39
+
40
+ /** Collapse a $HOME-prefixed working directory to a leading `~`. */
41
+ function collapseHome(dir: string): string {
42
+ const home = typeof process !== 'undefined' ? process.env.HOME ?? '' : '';
43
+ return home && dir.startsWith(home) ? '~' + dir.slice(home.length) : dir;
44
+ }
45
+
46
+ /** Center a meta line, truncating first so it never overflows the terminal width. */
47
+ function metaLine(text: string, columns: number): string {
48
+ return center(truncateDisplay(text, Math.max(0, columns)), columns);
30
49
  }
31
50
 
32
51
  export function getSplashLines(columns: number, opts: SplashOptions = {}): string[] {
33
- const splashHint = 'start chatting or type /help for commands';
34
52
  const lines: string[] = [
35
53
  center(TOP_BORDER, columns),
36
54
  ...ART_LINES.map((line) => center(line, columns)),
@@ -40,7 +58,30 @@ export function getSplashLines(columns: number, opts: SplashOptions = {}): strin
40
58
  '',
41
59
  ];
42
60
 
43
- lines.push(center(splashHint, columns));
61
+ // Live session context — real state pulled from SplashOptions rather than a
62
+ // static "/help" signpost: model (provider) and tool count on one line, cwd
63
+ // on the next.
64
+ const contextBits: string[] = [];
65
+ if (opts.model) {
66
+ contextBits.push(opts.provider ? `${opts.model} (${opts.provider})` : opts.model);
67
+ }
68
+ if (typeof opts.toolCount === 'number') {
69
+ contextBits.push(`${opts.toolCount} tool${opts.toolCount === 1 ? '' : 's'}`);
70
+ }
71
+ if (contextBits.length > 0) {
72
+ lines.push(metaLine(contextBits.join(' · '), columns));
73
+ }
74
+ if (opts.workingDir) {
75
+ lines.push(metaLine(collapseHome(opts.workingDir), columns));
76
+ }
77
+
78
+ // Last-session resume pointer (readLastSessionPointer result), when recorded.
79
+ if (opts.lastSessionId) {
80
+ lines.push(metaLine(`↩ resume last session — /sessions resume ${opts.lastSessionId}`, columns));
81
+ }
82
+
83
+ lines.push('');
84
+ lines.push(metaLine(HINT_LINE, columns));
44
85
 
45
86
  return lines;
46
87
  }
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.28.0';
9
+ let _version = '0.29.0';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;
@@ -267,6 +267,19 @@ export class WorkPlanStore {
267
267
  return removed;
268
268
  }
269
269
 
270
+ /**
271
+ * Writes the current plan's `toMarkdown()` output to a sibling `.md` file
272
+ * next to the JSON store file, so the checklist can be opened outside the
273
+ * TUI. Returns the written path alongside the markdown that was written.
274
+ */
275
+ exportMarkdown(): { readonly path: string; readonly markdown: string } {
276
+ const plan = this.readPlan();
277
+ const markdown = this.toMarkdown(plan);
278
+ const path = this.filePath.replace(/\.json$/, '.md');
279
+ atomicWriteFileSync(path, `${markdown}\n`, { mkdirp: true });
280
+ return { path, markdown };
281
+ }
282
+
270
283
  toMarkdown(plan: WorkPlan = this.readPlan()): string {
271
284
  const lines = [
272
285
  `# ${plan.title}`,