@pellux/goodvibes-tui 0.19.89 → 0.19.90

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,11 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.19.90] — 2026-05-10
8
+
9
+ ### Changes
10
+ - 1dae8a6e fix: make planning panel usable
11
+
7
12
  ## [0.19.89] — 2026-05-10
8
13
 
9
14
  ### Changes
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.19.89-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.19.90-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.89",
3
+ "version": "0.19.90",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -101,6 +101,7 @@ export interface CommandShellUiOpeners {
101
101
  openPanelPicker?: () => void;
102
102
  showPanel?: (panelId: string, pane?: 'top' | 'bottom') => void;
103
103
  focusPanels?: () => void;
104
+ focusPrompt?: () => void;
104
105
  openOpsPanel?: () => void;
105
106
  openCockpitPanel?: () => void;
106
107
  openOrchestrationPanel?: () => void;
@@ -18,6 +18,7 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
18
18
  else {
19
19
  pm.open('panel-list');
20
20
  pm.show();
21
+ ctx.focusPanels?.();
21
22
  ctx.renderRequest();
22
23
  }
23
24
  } catch {
@@ -30,6 +31,7 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
30
31
  else {
31
32
  pm.open('panel-list');
32
33
  pm.show();
34
+ ctx.focusPanels?.();
33
35
  ctx.renderRequest();
34
36
  }
35
37
  } catch {
@@ -49,6 +51,7 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
49
51
  else {
50
52
  pm.open(id, pane as 'top' | 'bottom' | undefined);
51
53
  pm.show();
54
+ ctx.focusPanels?.();
52
55
  ctx.renderRequest();
53
56
  }
54
57
  ctx.print(`Panel opened: ${id}${pane ? ` (${pane} pane)` : ''}`);
@@ -60,6 +63,7 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
60
63
  if (!id) { ctx.print('Usage: /panel close <panel-id>'); return; }
61
64
  try {
62
65
  pm.close(id);
66
+ ctx.focusPrompt?.();
63
67
  ctx.renderRequest();
64
68
  ctx.print(`Panel closed: ${id}`);
65
69
  } catch (e) {
@@ -130,6 +134,7 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
130
134
  else {
131
135
  pm.open(id);
132
136
  pm.show();
137
+ ctx.focusPanels?.();
133
138
  ctx.renderRequest();
134
139
  }
135
140
  } catch {
@@ -3,6 +3,7 @@ import type { CommandContext, CommandRegistry } from './command-registry.ts';
3
3
  import type { AutocompleteEngine } from './autocomplete.ts';
4
4
  import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
5
5
  import type { ConversationManager } from '../core/conversation';
6
+ import type { PanelManager } from '../panels/panel-manager.ts';
6
7
 
7
8
  export type CommandModeRouteState = {
8
9
  commandMode: boolean;
@@ -12,6 +13,8 @@ export type CommandModeRouteState = {
12
13
  modalStack: string[];
13
14
  commandRegistry: CommandRegistry | null;
14
15
  commandContext?: CommandContext;
16
+ panelFocused: boolean;
17
+ panelManager: PanelManager;
15
18
  conversationManager: ConversationManager | null;
16
19
  requestRender: () => void;
17
20
  handleEscape: () => void;
@@ -76,8 +79,11 @@ export function handleCommandModeToken(state: CommandModeRouteState, token: Inpu
76
79
  const parts = raw.slice(1).trim().split(/\s+/);
77
80
  const name = parts[0];
78
81
  const args = parts.slice(1);
79
- const ctx = state.commandContext;
80
- (ctx.executeCommand?.(name, args) ?? state.commandRegistry.execute(name, args, ctx)).then((handled) => {
82
+ const ctx = withPanelFocusSync(state.commandContext, state);
83
+ const commandPromise = state.commandRegistry.get(name)
84
+ ? state.commandRegistry.execute(name, args, ctx)
85
+ : (ctx.executeCommand?.(name, args) ?? Promise.resolve(false));
86
+ commandPromise.then((handled) => {
81
87
  if (handled) {
82
88
  state.requestRender();
83
89
  } else {
@@ -104,3 +110,44 @@ export function handleCommandModeToken(state: CommandModeRouteState, token: Inpu
104
110
 
105
111
  return token.logicalName !== 'left' && token.logicalName !== 'right';
106
112
  }
113
+
114
+ function withPanelFocusSync(context: CommandContext, state: CommandModeRouteState): CommandContext {
115
+ const panelIsFocusable = (): boolean =>
116
+ state.panelManager.isVisible() && state.panelManager.getAllOpen().length > 0;
117
+
118
+ return {
119
+ ...context,
120
+ openPanelPicker: context.openPanelPicker
121
+ ? () => {
122
+ context.openPanelPicker?.();
123
+ state.panelFocused = panelIsFocusable();
124
+ }
125
+ : undefined,
126
+ showPanel: context.showPanel
127
+ ? (panelId, pane) => {
128
+ context.showPanel?.(panelId, pane);
129
+ state.panelFocused = true;
130
+ }
131
+ : undefined,
132
+ focusPanels: context.focusPanels
133
+ ? () => {
134
+ context.focusPanels?.();
135
+ state.panelFocused = panelIsFocusable();
136
+ }
137
+ : undefined,
138
+ focusPrompt: context.focusPrompt
139
+ ? () => {
140
+ context.focusPrompt?.();
141
+ state.panelFocused = false;
142
+ }
143
+ : undefined,
144
+ executeCommand: async (name, args) => {
145
+ const wrapped = withPanelFocusSync(context, state);
146
+ const handled = state.commandRegistry?.get(name)
147
+ ? await state.commandRegistry.execute(name, args, wrapped)
148
+ : false;
149
+ if (handled) return true;
150
+ return (await context.executeCommand?.(name, args)) ?? false;
151
+ },
152
+ };
153
+ }
@@ -90,10 +90,8 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
90
90
  const active = pm.getActivePanel();
91
91
  if (active) {
92
92
  pm.close(active.id);
93
- if (pm.getAllOpen().length === 0) {
94
- panelFocused = false;
95
- }
96
93
  }
94
+ panelFocused = false;
97
95
  state.requestRender();
98
96
  return { handled: true, panelFocused };
99
97
  }
@@ -336,10 +334,16 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
336
334
  return { handled: true, prompt, cursorPos, commandMode, indicatorFocused };
337
335
  }
338
336
  if (text) {
339
- state.inputHistory?.add(text);
337
+ const expanded = state.expandPrompt(text);
338
+ const historyRecallText = typeof expanded === 'string'
339
+ ? expanded
340
+ : expanded
341
+ .filter((p): p is { type: 'text'; text: string } => p.type === 'text')
342
+ .map(p => p.text)
343
+ .join('');
344
+ state.inputHistory?.add(text, { recallText: historyRecallText });
340
345
  prompt = '';
341
346
  cursorPos = 0;
342
- const expanded = state.expandPrompt(text);
343
347
  if (typeof expanded === 'string') {
344
348
  state.commandContext?.submitInput?.(expanded);
345
349
  } else {
@@ -237,6 +237,14 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
237
237
  }
238
238
 
239
239
  if (token.type === 'key') {
240
+ if (
241
+ context.panelFocused
242
+ && (!context.panelManager.isVisible()
243
+ || context.panelManager.getAllOpen().length === 0
244
+ || context.panelManager.getActivePanel() === null)
245
+ ) {
246
+ context.panelFocused = false;
247
+ }
240
248
  const shortcutState = {
241
249
  panelFocused: context.panelFocused,
242
250
  prompt: context.prompt,
@@ -272,6 +280,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
272
280
  context.prompt = shortcutState.prompt;
273
281
  context.cursorPos = shortcutState.cursorPos;
274
282
  context.commandMode = shortcutState.commandMode;
283
+ context.panelFocused = shortcutState.panelFocused;
275
284
  continue;
276
285
  }
277
286
  }
@@ -339,6 +348,8 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
339
348
  modalStack: context.modalStack,
340
349
  commandRegistry: context.commandRegistry,
341
350
  commandContext: context.commandContext,
351
+ panelFocused: context.panelFocused,
352
+ panelManager: context.panelManager,
342
353
  conversationManager: context.conversationManager,
343
354
  requestRender: context.requestRender,
344
355
  handleEscape: context.handleEscape,
@@ -347,6 +358,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
347
358
  context.commandMode = commandState.commandMode;
348
359
  context.prompt = commandState.prompt;
349
360
  context.cursorPos = commandState.cursorPos;
361
+ context.panelFocused = commandState.panelFocused;
350
362
  continue;
351
363
  }
352
364
 
@@ -90,6 +90,7 @@ export function handleGlobalShortcutToken(
90
90
  const pm = state.panelManager;
91
91
  for (const p of pm.getAllOpen()) pm.close(p.id);
92
92
  pm.hide();
93
+ state.panelFocused = false;
93
94
  state.requestRender();
94
95
  return true;
95
96
  }
@@ -101,11 +102,13 @@ export function handleGlobalShortcutToken(
101
102
  pm.close(active.id);
102
103
  state.requestRender();
103
104
  }
105
+ state.panelFocused = false;
104
106
  return true;
105
107
  }
106
108
 
107
109
  case 'panel-picker':
108
110
  state.commandContext?.openPanelPicker?.();
111
+ state.panelFocused = state.panelManager.isVisible() && state.panelManager.getAllOpen().length > 0;
109
112
  state.requestRender();
110
113
  return true;
111
114
 
@@ -107,6 +107,11 @@ export interface InputHistoryOptions {
107
107
  readonly persist?: boolean;
108
108
  }
109
109
 
110
+ type StoredInputHistoryEntry = string | {
111
+ readonly text: string;
112
+ readonly recallText?: string;
113
+ };
114
+
110
115
  function resolveHistoryPath(options?: InputHistoryOptions): string {
111
116
  if (options?.historyPath) {
112
117
  return options.historyPath;
@@ -119,7 +124,7 @@ function resolveHistoryPath(options?: InputHistoryOptions): string {
119
124
  }
120
125
 
121
126
  export class InputHistory {
122
- private entries: string[] = [];
127
+ private entries: StoredInputHistoryEntry[] = [];
123
128
  private position = -1; // -1 = not browsing
124
129
  private draft = ''; // Saves current input when entering history
125
130
  private maxEntries = 500;
@@ -140,17 +145,21 @@ export class InputHistory {
140
145
  * - Deduplicates consecutive identical entries.
141
146
  * - Resets browsing position.
142
147
  */
143
- add(text: string): void {
148
+ add(text: string, options: { readonly recallText?: string } = {}): void {
144
149
  const trimmed = text.trim();
145
150
  if (!trimmed) return;
151
+ const recallText = options.recallText?.trim();
152
+ const entry: StoredInputHistoryEntry = recallText && recallText !== trimmed
153
+ ? { text: trimmed, recallText }
154
+ : trimmed;
146
155
 
147
156
  // Dedup: skip if same as most recent entry
148
- if (this.entries.length > 0 && this.entries[0] === trimmed) {
157
+ if (this.entries.length > 0 && this.sameEntry(this.entries[0]!, entry)) {
149
158
  this.resetPosition();
150
159
  return;
151
160
  }
152
161
 
153
- this.entries.unshift(trimmed);
162
+ this.entries.unshift(entry);
154
163
  if (this.entries.length > this.maxEntries) {
155
164
  this.entries.length = this.maxEntries;
156
165
  }
@@ -179,9 +188,10 @@ export class InputHistory {
179
188
  // Try to advance to an older single-line entry
180
189
  let next = this.position + 1;
181
190
  while (next < this.entries.length) {
182
- if (!this.entries[next].includes('\n')) {
191
+ const entry = this.entries[next]!;
192
+ if (!this.getDisplayText(entry).includes('\n')) {
183
193
  this.position = next;
184
- return this.entries[this.position];
194
+ return this.getRecallText(this.entries[this.position]!);
185
195
  }
186
196
  next++;
187
197
  }
@@ -201,9 +211,10 @@ export class InputHistory {
201
211
  // Try to find a newer single-line entry
202
212
  let prev = this.position - 1;
203
213
  while (prev >= 0) {
204
- if (!this.entries[prev].includes('\n')) {
214
+ const entry = this.entries[prev]!;
215
+ if (!this.getDisplayText(entry).includes('\n')) {
205
216
  this.position = prev;
206
- return this.entries[this.position];
217
+ return this.getRecallText(this.entries[this.position]!);
207
218
  }
208
219
  prev--;
209
220
  }
@@ -232,7 +243,7 @@ export class InputHistory {
232
243
  * Return entries as readonly for use by HistorySearch.
233
244
  */
234
245
  getEntries(): readonly string[] {
235
- return this.entries;
246
+ return this.entries.map((entry) => this.getRecallText(entry));
236
247
  }
237
248
 
238
249
  /**
@@ -256,7 +267,10 @@ export class InputHistory {
256
267
  const raw = readFileSync(this.historyPath, 'utf-8');
257
268
  const parsed = JSON.parse(raw) as unknown;
258
269
  if (Array.isArray(parsed)) {
259
- this.entries = (parsed as unknown[]).filter((e): e is string => typeof e === 'string').slice(0, this.maxEntries);
270
+ this.entries = (parsed as unknown[])
271
+ .map((entry) => this.normalizeStoredEntry(entry))
272
+ .filter((entry): entry is StoredInputHistoryEntry => entry !== null)
273
+ .slice(0, this.maxEntries);
260
274
  }
261
275
  }
262
276
  } catch (err) {
@@ -264,4 +278,30 @@ export class InputHistory {
264
278
  this.entries = [];
265
279
  }
266
280
  }
281
+
282
+ private getDisplayText(entry: StoredInputHistoryEntry): string {
283
+ return typeof entry === 'string' ? entry : entry.text;
284
+ }
285
+
286
+ private getRecallText(entry: StoredInputHistoryEntry): string {
287
+ return typeof entry === 'string' ? entry : entry.recallText ?? entry.text;
288
+ }
289
+
290
+ private sameEntry(a: StoredInputHistoryEntry, b: StoredInputHistoryEntry): boolean {
291
+ return this.getDisplayText(a) === this.getDisplayText(b)
292
+ && this.getRecallText(a) === this.getRecallText(b);
293
+ }
294
+
295
+ private normalizeStoredEntry(entry: unknown): StoredInputHistoryEntry | null {
296
+ if (typeof entry === 'string') return entry;
297
+ if (!entry || typeof entry !== 'object') return null;
298
+ const record = entry as Record<string, unknown>;
299
+ if (typeof record.text !== 'string') return null;
300
+ const text = record.text.trim();
301
+ if (!text) return null;
302
+ if (typeof record.recallText === 'string' && record.recallText.trim() && record.recallText.trim() !== text) {
303
+ return { text, recallText: record.recallText.trim() };
304
+ }
305
+ return text;
306
+ }
267
307
  }
@@ -102,6 +102,7 @@ export function registerAgentPanels(manager: PanelManager, deps: ResolvedBuiltin
102
102
  projectId: deps.projectPlanningProjectId,
103
103
  requestRender: deps.requestRender,
104
104
  submitAnswer: deps.submitPlanningAnswer,
105
+ dismissPlanning: deps.dismissPlanning,
105
106
  }),
106
107
  });
107
108
 
@@ -57,6 +57,8 @@ export interface BuiltinPanelDeps {
57
57
  requestRender?: () => void;
58
58
  /** Submit a Planning panel answer through the normal TUI chat/planning coordinator path. */
59
59
  submitPlanningAnswer?: (answer: string) => void;
60
+ /** Pause the TUI-owned planning loop and return focus to normal prompt input. */
61
+ dismissPlanning?: () => void;
60
62
  /** ForensicsRegistry for the Forensics panel. */
61
63
  forensicsRegistry?: import('@/runtime/index.ts').ForensicsRegistry;
62
64
  /** EvalRegistry for the Eval panel. */
@@ -42,6 +42,7 @@ export interface ProjectPlanningPanelOptions {
42
42
  readonly projectId: string;
43
43
  readonly requestRender?: () => void;
44
44
  readonly submitAnswer?: (answer: string) => void;
45
+ readonly dismissPlanning?: () => void;
45
46
  }
46
47
 
47
48
  interface PlanningAnswerAction {
@@ -49,15 +50,20 @@ interface PlanningAnswerAction {
49
50
  readonly label: string;
50
51
  readonly detail: string;
51
52
  readonly answer: string;
52
- readonly kind?: 'answer' | 'approve';
53
+ readonly kind?: 'answer' | 'approve' | 'dismiss';
53
54
  readonly disabled?: boolean;
54
55
  }
55
56
 
57
+ interface RenderedPlanningSection extends PanelWorkspaceSection {
58
+ readonly selectedLineIndex?: number;
59
+ }
60
+
56
61
  export class ProjectPlanningPanel extends BasePanel {
57
62
  private readonly service: ProjectPlanningService;
58
63
  private readonly projectId: string;
59
64
  private readonly requestRender: () => void;
60
65
  private readonly submitAnswer: ((answer: string) => void) | undefined;
66
+ private readonly dismissPlanning: (() => void) | undefined;
61
67
  private snapshot: ProjectPlanningPanelSnapshot | null = null;
62
68
  private loading = false;
63
69
  private scrollOffset = 0;
@@ -70,6 +76,7 @@ export class ProjectPlanningPanel extends BasePanel {
70
76
  this.projectId = options.projectId;
71
77
  this.requestRender = options.requestRender ?? (() => {});
72
78
  this.submitAnswer = options.submitAnswer;
79
+ this.dismissPlanning = options.dismissPlanning;
73
80
  }
74
81
 
75
82
  public override onActivate(): void {
@@ -155,7 +162,7 @@ export class ProjectPlanningPanel extends BasePanel {
155
162
  return this.trackedRender(() => {
156
163
  if (!this.snapshot && !this.loading) this.refresh();
157
164
 
158
- const sections: PanelWorkspaceSection[] = [];
165
+ const sections: RenderedPlanningSection[] = [];
159
166
  const status = this.snapshot?.status;
160
167
  const state = this.snapshot?.state;
161
168
  const evaluation = this.snapshot?.evaluation ?? null;
@@ -202,10 +209,7 @@ export class ProjectPlanningPanel extends BasePanel {
202
209
  if (line) sections.push({ title: 'Error', lines: [line] });
203
210
  }
204
211
 
205
- const flattened = sections.flatMap((section) => [
206
- ...(section.title ? [buildPanelLine(width, [[` ${section.title}`, C.label]])] : []),
207
- ...section.lines,
208
- ]);
212
+ const { lines: flattened, selectedIndex } = this.flattenSections(width, sections);
209
213
  const scroll = resolveScrollablePanelSection(width, height, {
210
214
  intro: 'Project planning state, readiness gaps, decisions, language, task graph, verification gates, and agent handoff metadata.',
211
215
  footerLines: this.footerLines(width),
@@ -213,8 +217,9 @@ export class ProjectPlanningPanel extends BasePanel {
213
217
  section: {
214
218
  title: 'Project Planning',
215
219
  scrollableLines: flattened,
216
- selectedIndex: 0,
220
+ selectedIndex,
217
221
  scrollOffset: this.scrollOffset,
222
+ appendWindowSummary: {},
218
223
  minRows: 8,
219
224
  },
220
225
  });
@@ -242,7 +247,7 @@ export class ProjectPlanningPanel extends BasePanel {
242
247
  ['Backspace/Delete', C.info],
243
248
  [' edit ', C.dim],
244
249
  ['Enter', C.info],
245
- [' submit Esc close panel focus', C.dim],
250
+ [' submit Esc prompt focus Ctrl+X close panel', C.dim],
246
251
  ]),
247
252
  ];
248
253
  }
@@ -253,12 +258,30 @@ export class ProjectPlanningPanel extends BasePanel {
253
258
  ['r', C.info],
254
259
  [' refresh ', C.dim],
255
260
  ['a', C.info],
256
- [' approve execution-ready plan Esc close panel focus', C.dim],
261
+ [' approve execution-ready plan Esc prompt focus Ctrl+X close panel', C.dim],
257
262
  ]),
258
263
  ];
259
264
  }
260
265
 
261
- private buildQuestionSection(width: number, question: ProjectPlanningQuestion): PanelWorkspaceSection {
266
+ private flattenSections(
267
+ width: number,
268
+ sections: readonly RenderedPlanningSection[],
269
+ ): { readonly lines: Line[]; readonly selectedIndex: number } {
270
+ const lines: Line[] = [];
271
+ let selectedIndex = 0;
272
+ for (const section of sections) {
273
+ const sectionStart = lines.length;
274
+ const titleOffset = section.title ? 1 : 0;
275
+ if (section.title) lines.push(buildPanelLine(width, [[` ${section.title}`, C.label]]));
276
+ lines.push(...section.lines);
277
+ if (section.selectedLineIndex !== undefined) {
278
+ selectedIndex = sectionStart + titleOffset + section.selectedLineIndex;
279
+ }
280
+ }
281
+ return { lines, selectedIndex };
282
+ }
283
+
284
+ private buildQuestionSection(width: number, question: ProjectPlanningQuestion): RenderedPlanningSection {
262
285
  const actions = this.getAnswerActions(question);
263
286
  this.selectedActionIndex = this.clampActionIndex(actions.length);
264
287
  const lines: Line[] = [
@@ -280,6 +303,7 @@ export class ProjectPlanningPanel extends BasePanel {
280
303
  ' Select an answer below or type your own. Enter sends it through the normal planning chat path.',
281
304
  C.dim,
282
305
  ]]));
306
+ const selectedLineIndex = lines.length + this.selectedActionIndex;
283
307
  actions.forEach((action, index) => {
284
308
  const selected = index === this.selectedActionIndex;
285
309
  lines.push(buildPanelListRow(width, [
@@ -290,7 +314,7 @@ export class ProjectPlanningPanel extends BasePanel {
290
314
  marker: selected ? '▶' : ' ',
291
315
  }));
292
316
  });
293
- return { title: 'Answer Current Question', lines };
317
+ return { title: 'Answer Current Question', lines, selectedLineIndex };
294
318
  }
295
319
 
296
320
  private buildStateSection(
@@ -540,6 +564,13 @@ export class ProjectPlanningPanel extends BasePanel {
540
564
  answer: this.draftAnswer.trim(),
541
565
  disabled: !this.draftAnswer.trim(),
542
566
  });
567
+ actions.push({
568
+ id: 'dismiss-planning',
569
+ label: 'Close planning and continue without it',
570
+ detail: 'Pause project planning for this workspace. Normal chat continues; /plan can reopen it later.',
571
+ answer: 'Pause project planning for this workspace and continue without the planning panel.',
572
+ kind: 'dismiss',
573
+ });
543
574
  return actions;
544
575
  }
545
576
 
@@ -561,6 +592,10 @@ export class ProjectPlanningPanel extends BasePanel {
561
592
  this.approveExecution();
562
593
  return;
563
594
  }
595
+ if (action.kind === 'dismiss') {
596
+ this.pausePlanning(question);
597
+ return;
598
+ }
564
599
  if (!this.submitAnswer) {
565
600
  this.setError('Planning answer submission is not wired in this runtime.');
566
601
  this.requestRender();
@@ -596,6 +631,41 @@ export class ProjectPlanningPanel extends BasePanel {
596
631
  });
597
632
  }
598
633
 
634
+ private pausePlanning(question: ProjectPlanningQuestion): void {
635
+ const state = this.snapshot?.state;
636
+ if (!state) {
637
+ this.dismissPlanning?.();
638
+ this.requestRender();
639
+ return;
640
+ }
641
+ void (async () => {
642
+ try {
643
+ await this.service.upsertState({
644
+ projectId: this.projectId,
645
+ state: {
646
+ ...state,
647
+ openQuestions: state.openQuestions.map((entry) =>
648
+ entry.id === question.id
649
+ ? { ...entry, status: entry.status ?? 'open' }
650
+ : entry,
651
+ ),
652
+ metadata: {
653
+ ...(state.metadata ?? {}),
654
+ active: false,
655
+ pausedAt: Date.now(),
656
+ pausedFrom: 'project-planning-panel',
657
+ },
658
+ },
659
+ });
660
+ this.dismissPlanning?.();
661
+ this.refresh(true);
662
+ } catch (err) {
663
+ this.setError(err instanceof Error ? err.message : String(err));
664
+ this.requestRender();
665
+ }
666
+ })();
667
+ }
668
+
599
669
  private clampActionIndex(count: number): number {
600
670
  if (count <= 0) return 0;
601
671
  return Math.max(0, Math.min(count - 1, this.selectedActionIndex));
@@ -41,6 +41,8 @@ const PLANNING_INTENT_PATTERNS: readonly RegExp[] = [
41
41
  const CANCEL_PATTERNS: readonly RegExp[] = [
42
42
  /\b(stop|cancel|pause|exit) (the )?planning\b/i,
43
43
  /\bplanning (is )?(done|cancelled|canceled|paused)\b/i,
44
+ /\b(skip|dismiss) (the )?planning\b/i,
45
+ /\bcontinue without (the )?planning\b/i,
44
46
  ];
45
47
 
46
48
  const APPROVAL_PATTERNS: readonly RegExp[] = [
@@ -120,6 +120,11 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
120
120
  }
121
121
  commandContextRef.submitInput(answer);
122
122
  },
123
+ dismissPlanning: () => {
124
+ services.panelManager.close('project-planning');
125
+ commandContextRef?.focusPrompt?.();
126
+ requestRender();
127
+ },
123
128
  forensicsRegistry,
124
129
  policyRuntimeState,
125
130
  approvalBroker: services.approvalBroker,
@@ -325,6 +325,12 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
325
325
  render();
326
326
  };
327
327
 
328
+ commandContext.focusPrompt = () => {
329
+ input.panelFocused = false;
330
+ input.indicatorFocused = false;
331
+ render();
332
+ };
333
+
328
334
  commandContext.showPanel = (panelId, pane) => {
329
335
  panelManager.open(panelId, pane);
330
336
  panelManager.show();
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.19.89';
9
+ let _version = '0.19.90';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;