@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 +5 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/operator-panel-runtime.ts +5 -0
- package/src/input/handler-command-route.ts +49 -2
- package/src/input/handler-feed-routes.ts +9 -5
- package/src/input/handler-feed.ts +12 -0
- package/src/input/handler-shortcuts.ts +3 -0
- package/src/input/input-history.ts +50 -10
- package/src/panels/builtin/agent.ts +1 -0
- package/src/panels/builtin/shared.ts +2 -0
- package/src/panels/project-planning-panel.ts +81 -11
- package/src/planning/project-planning-coordinator.ts +2 -0
- package/src/runtime/bootstrap-shell.ts +5 -0
- package/src/shell/ui-openers.ts +6 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](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.
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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]
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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[])
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
261
|
+
[' approve execution-ready plan Esc prompt focus Ctrl+X close panel', C.dim],
|
|
257
262
|
]),
|
|
258
263
|
];
|
|
259
264
|
}
|
|
260
265
|
|
|
261
|
-
private
|
|
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,
|
package/src/shell/ui-openers.ts
CHANGED
|
@@ -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.
|
|
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;
|