@pellux/goodvibes-tui 1.1.0 → 1.7.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.
- package/CHANGELOG.md +85 -21
- package/README.md +20 -11
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +36 -0
- package/src/input/command-registry.ts +38 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +15 -7
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-feed-routes.ts +61 -23
- package/src/input/handler-feed.ts +47 -23
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +6 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +24 -27
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +13 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/turn-injection.ts +114 -0
- package/src/runtime/bootstrap-command-context.ts +21 -1
- package/src/runtime/bootstrap-command-parts.ts +34 -7
- package/src/runtime/bootstrap-core.ts +14 -4
- package/src/runtime/bootstrap-shell.ts +29 -16
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -1,731 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ProjectPlanningDecision,
|
|
3
|
-
ProjectPlanningEvaluation,
|
|
4
|
-
ProjectPlanningLanguageArtifact,
|
|
5
|
-
ProjectPlanningQuestion,
|
|
6
|
-
ProjectPlanningService,
|
|
7
|
-
ProjectPlanningState,
|
|
8
|
-
ProjectPlanningStatus,
|
|
9
|
-
} from '@pellux/goodvibes-sdk/platform/knowledge';
|
|
10
|
-
import type { Line } from '../types/grid.ts';
|
|
11
|
-
import { BasePanel } from './base-panel.ts';
|
|
12
|
-
import { handleConfirmInput, renderConfirmLines, type ConfirmState } from './confirm-state.ts';
|
|
13
|
-
import { isTextBackspace, isTextForwardDelete } from '../input/delete-key-policy.ts';
|
|
14
|
-
import {
|
|
15
|
-
buildBodyText,
|
|
16
|
-
buildEmptyState,
|
|
17
|
-
buildKeyValueLine,
|
|
18
|
-
buildPanelLine,
|
|
19
|
-
buildPanelListRow,
|
|
20
|
-
buildPanelWorkspace,
|
|
21
|
-
DEFAULT_PANEL_PALETTE,
|
|
22
|
-
extendPalette,
|
|
23
|
-
resolveScrollablePanelSection,
|
|
24
|
-
type PanelWorkspaceSection,
|
|
25
|
-
} from './polish.ts';
|
|
26
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
27
|
-
import { buildAnswerActions, isGenericRecommendation, type PlanningAnswerAction } from './project-planning-answer-actions.ts';
|
|
28
|
-
|
|
29
|
-
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
30
|
-
planning: '#38bdf8',
|
|
31
|
-
blocked: '#f97316',
|
|
32
|
-
approved: '#22c55e',
|
|
33
|
-
rejected: '#ef4444',
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
interface ProjectPlanningPanelSnapshot {
|
|
37
|
-
readonly status: ProjectPlanningStatus | null;
|
|
38
|
-
readonly state: ProjectPlanningState | null;
|
|
39
|
-
readonly evaluation: ProjectPlanningEvaluation | null;
|
|
40
|
-
readonly decisions: readonly ProjectPlanningDecision[];
|
|
41
|
-
readonly language: ProjectPlanningLanguageArtifact | null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface ProjectPlanningPanelOptions {
|
|
45
|
-
readonly service: ProjectPlanningService;
|
|
46
|
-
readonly projectId: string;
|
|
47
|
-
readonly requestRender?: () => void;
|
|
48
|
-
readonly submitAnswer?: (answer: string) => void;
|
|
49
|
-
readonly dismissPlanning?: () => void;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface RenderedPlanningSection extends PanelWorkspaceSection {
|
|
53
|
-
readonly selectedLineIndex?: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export class ProjectPlanningPanel extends BasePanel {
|
|
57
|
-
private readonly service: ProjectPlanningService;
|
|
58
|
-
private readonly projectId: string;
|
|
59
|
-
private readonly requestRender: () => void;
|
|
60
|
-
private readonly submitAnswer: ((answer: string) => void) | undefined;
|
|
61
|
-
private readonly dismissPlanning: (() => void) | undefined;
|
|
62
|
-
private snapshot: ProjectPlanningPanelSnapshot | null = null;
|
|
63
|
-
private loading = false;
|
|
64
|
-
private scrollOffset = 0;
|
|
65
|
-
private selectedActionIndex = 0;
|
|
66
|
-
private draftAnswer = '';
|
|
67
|
-
// Pending confirmation for Delete (clear draft). Null when inactive.
|
|
68
|
-
private clearDraftConfirm: ConfirmState<'clear-draft'> | null = null;
|
|
69
|
-
|
|
70
|
-
public constructor(options: ProjectPlanningPanelOptions) {
|
|
71
|
-
super('project-planning', 'Planning', 'P', 'agent');
|
|
72
|
-
this.service = options.service;
|
|
73
|
-
this.projectId = options.projectId;
|
|
74
|
-
this.requestRender = options.requestRender ?? (() => {});
|
|
75
|
-
this.submitAnswer = options.submitAnswer;
|
|
76
|
-
this.dismissPlanning = options.dismissPlanning;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
public override onActivate(): void {
|
|
80
|
-
super.onActivate();
|
|
81
|
-
this.refresh();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* The draft-answer text field wants every character of a burst (paste, or
|
|
86
|
-
* fast typing landing in one input.feed() call) delivered one at a time,
|
|
87
|
-
* same as it always has — see the interface doc on
|
|
88
|
-
* `Panel.isCapturingTextBurst`.
|
|
89
|
-
*/
|
|
90
|
-
public isCapturingTextBurst(): boolean {
|
|
91
|
-
return this.getCurrentQuestion() !== null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
public handleInput(key: string): boolean {
|
|
95
|
-
if (this.lastError !== null) this.clearError();
|
|
96
|
-
|
|
97
|
-
// ConfirmState gate: Delete (clear draft) requires y/n confirmation.
|
|
98
|
-
// handleConfirmInput absorbs all keys while a confirmation is pending.
|
|
99
|
-
const confirmResult = handleConfirmInput(this.clearDraftConfirm, key);
|
|
100
|
-
if (confirmResult === 'confirmed') {
|
|
101
|
-
this.draftAnswer = '';
|
|
102
|
-
this.clearDraftConfirm = null;
|
|
103
|
-
this.markDirty();
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
if (confirmResult === 'cancelled') {
|
|
107
|
-
this.clearDraftConfirm = null;
|
|
108
|
-
this.markDirty();
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
if (confirmResult === 'absorbed') {
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
// confirmResult === 'inactive': proceed with normal dispatch.
|
|
115
|
-
|
|
116
|
-
// Ctrl+R (refresh) / Ctrl+A (approve) are alternate bindings for 'r'/'a'
|
|
117
|
-
// that stay reachable while a question is active, where plain 'r'/'a'
|
|
118
|
-
// are swallowed into the draft-answer text below instead. They are
|
|
119
|
-
// checked before the question gate so they work in both modes.
|
|
120
|
-
//
|
|
121
|
-
// NOTE: the input-routing layer (src/input/handler-feed-routes.ts, owned
|
|
122
|
-
// by WO-150 in this wave) does not yet forward ctrl-modified keys to the
|
|
123
|
-
// active panel — it currently intercepts all `token.ctrl` input before
|
|
124
|
-
// panel.handleInput is ever called, and a bare Ctrl+R is already bound
|
|
125
|
-
// globally to 'history-search'. This handler recognizes the logical key
|
|
126
|
-
// strings 'ctrl+r'/'ctrl+a' so the panel-side behavior is correct and
|
|
127
|
-
// testable now; wiring a real Ctrl+R/Ctrl+A keypress through to these
|
|
128
|
-
// strings is an input-handler change outside this work order's file
|
|
129
|
-
// scope.
|
|
130
|
-
if (key === 'ctrl+r') {
|
|
131
|
-
this.refresh(true);
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
if (key === 'ctrl+a') {
|
|
135
|
-
this.approveExecution();
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const question = this.getCurrentQuestion();
|
|
140
|
-
if (question) {
|
|
141
|
-
const actions = buildAnswerActions(question, this.draftAnswer);
|
|
142
|
-
this.selectedActionIndex = this.clampActionIndex(actions.length);
|
|
143
|
-
if (key === 'up') {
|
|
144
|
-
this.selectedActionIndex = Math.max(0, this.selectedActionIndex - 1);
|
|
145
|
-
this.markDirty();
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
if (key === 'down') {
|
|
149
|
-
this.selectedActionIndex = Math.min(Math.max(0, actions.length - 1), this.selectedActionIndex + 1);
|
|
150
|
-
this.markDirty();
|
|
151
|
-
return true;
|
|
152
|
-
}
|
|
153
|
-
if (key === 'enter' || key === 'return') {
|
|
154
|
-
this.submitSelectedAction(question, actions);
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
if (isTextBackspace(key)) {
|
|
158
|
-
this.draftAnswer = this.draftAnswer.slice(0, -1);
|
|
159
|
-
this.markDirty();
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
-
// 'delete' opens the clear-draft confirmation gate (per delete-key policy).
|
|
163
|
-
// The draft is not wiped until the user confirms with y/Enter.
|
|
164
|
-
if (isTextForwardDelete(key)) {
|
|
165
|
-
this.clearDraftConfirm = { subject: 'clear-draft', label: 'draft answer' };
|
|
166
|
-
this.markDirty();
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
if (key === 'space') {
|
|
170
|
-
this.draftAnswer += ' ';
|
|
171
|
-
this.markDirty();
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
if (key === 'pageup') {
|
|
175
|
-
this.scrollOffset = Math.max(0, this.scrollOffset - 6);
|
|
176
|
-
this.markDirty();
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
if (key === 'pagedown') {
|
|
180
|
-
this.scrollOffset += 6;
|
|
181
|
-
this.markDirty();
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
if (this.isPrintableKey(key)) {
|
|
185
|
-
this.draftAnswer += key;
|
|
186
|
-
this.markDirty();
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (key === 'r' || key === 'R') {
|
|
193
|
-
this.refresh(true);
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
if (key === 'a' || key === 'A') {
|
|
197
|
-
this.approveExecution();
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
if (key === 'up' || key === 'k') {
|
|
201
|
-
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
202
|
-
this.markDirty();
|
|
203
|
-
return true;
|
|
204
|
-
}
|
|
205
|
-
if (key === 'down' || key === 'j') {
|
|
206
|
-
this.scrollOffset += 1;
|
|
207
|
-
this.markDirty();
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
public render(width: number, height: number): Line[] {
|
|
214
|
-
return this.trackedRender(() => {
|
|
215
|
-
if (!this.snapshot && !this.loading) this.refresh();
|
|
216
|
-
|
|
217
|
-
const sections: RenderedPlanningSection[] = [];
|
|
218
|
-
const status = this.snapshot?.status;
|
|
219
|
-
const state = this.snapshot?.state;
|
|
220
|
-
const evaluation = this.snapshot?.evaluation ?? null;
|
|
221
|
-
const language = this.snapshot?.language ?? null;
|
|
222
|
-
const decisions = this.snapshot?.decisions ?? [];
|
|
223
|
-
|
|
224
|
-
sections.push({
|
|
225
|
-
title: 'Workspace',
|
|
226
|
-
lines: [
|
|
227
|
-
buildKeyValueLine(width, [
|
|
228
|
-
{ label: 'project', value: this.projectId, valueColor: C.planning },
|
|
229
|
-
{ label: 'space', value: status?.knowledgeSpaceId ?? `project:${this.projectId}`, valueColor: C.value },
|
|
230
|
-
{ label: 'mode', value: 'TUI-owned passive backing store', valueColor: C.info },
|
|
231
|
-
], C),
|
|
232
|
-
// Live artifact counts from the SDK status route (same counts /plan
|
|
233
|
-
// prints at planning-runtime.ts:90) instead of static filler prose.
|
|
234
|
-
status
|
|
235
|
-
? buildKeyValueLine(width, [
|
|
236
|
-
{ label: 'states', value: String(status.counts.states), valueColor: status.counts.states > 0 ? C.value : C.dim },
|
|
237
|
-
{ label: 'decisions', value: String(status.counts.decisions), valueColor: status.counts.decisions > 0 ? C.value : C.dim },
|
|
238
|
-
{ label: 'language', value: String(status.counts.languageArtifacts), valueColor: status.counts.languageArtifacts > 0 ? C.value : C.dim },
|
|
239
|
-
], C)
|
|
240
|
-
: buildPanelLine(width, [[' Artifact counts unavailable — refreshing…', C.dim]]),
|
|
241
|
-
],
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
if (!state) {
|
|
245
|
-
sections.push({
|
|
246
|
-
title: 'No Active Planning State',
|
|
247
|
-
lines: buildEmptyState(
|
|
248
|
-
width,
|
|
249
|
-
'No project planning state has been saved for this workspace.',
|
|
250
|
-
'Describe the intended change in normal chat to let the TUI start the planning interview. The SDK only stores and evaluates artifacts.',
|
|
251
|
-
[],
|
|
252
|
-
C,
|
|
253
|
-
),
|
|
254
|
-
});
|
|
255
|
-
} else {
|
|
256
|
-
sections.push(this.buildStateSection(width, state, evaluation));
|
|
257
|
-
const question = this.getCurrentQuestion();
|
|
258
|
-
if (question) sections.push(this.buildQuestionSection(width, question));
|
|
259
|
-
sections.push(this.buildGapsSection(width, evaluation));
|
|
260
|
-
sections.push(this.buildTasksSection(width, state));
|
|
261
|
-
sections.push(this.buildAnsweredHistorySection(width, state));
|
|
262
|
-
sections.push(this.buildDecisionsSection(width, state, decisions));
|
|
263
|
-
sections.push(this.buildLanguageSection(width, language));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (this.lastError) {
|
|
267
|
-
const line = this.renderErrorLine(width);
|
|
268
|
-
if (line) sections.push({ title: 'Error', lines: [line] });
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const { lines: flattened, selectedIndex } = this.flattenSections(width, sections);
|
|
272
|
-
const scroll = resolveScrollablePanelSection(width, height, {
|
|
273
|
-
intro: 'Project planning state, readiness gaps, decisions, language, task graph, verification gates, and agent handoff metadata.',
|
|
274
|
-
footerLines: this.footerLines(width),
|
|
275
|
-
palette: C,
|
|
276
|
-
section: {
|
|
277
|
-
title: 'Project Planning',
|
|
278
|
-
scrollableLines: flattened,
|
|
279
|
-
selectedIndex,
|
|
280
|
-
scrollOffset: this.scrollOffset,
|
|
281
|
-
appendWindowSummary: {},
|
|
282
|
-
minRows: 8,
|
|
283
|
-
},
|
|
284
|
-
});
|
|
285
|
-
this.scrollOffset = scroll.scrollOffset;
|
|
286
|
-
|
|
287
|
-
return buildPanelWorkspace(width, height, {
|
|
288
|
-
title: this.loading ? 'Project Planning - loading' : 'Project Planning',
|
|
289
|
-
intro: 'Passive SDK-backed planning artifacts for the current workspace. Conversation control stays inside this TUI.',
|
|
290
|
-
sections: [scroll.section],
|
|
291
|
-
footerLines: this.footerLines(width),
|
|
292
|
-
palette: C,
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
private footerLines(width: number): Line[] {
|
|
298
|
-
const hasQuestion = this.getCurrentQuestion() !== null;
|
|
299
|
-
if (hasQuestion) {
|
|
300
|
-
return [
|
|
301
|
-
buildPanelLine(width, [
|
|
302
|
-
[' Up/Down', C.info],
|
|
303
|
-
[' choose answer ', C.dim],
|
|
304
|
-
['type', C.info],
|
|
305
|
-
[' draft ', C.dim],
|
|
306
|
-
['Backspace', C.info],
|
|
307
|
-
[' edit ', C.dim],
|
|
308
|
-
['Del', C.info],
|
|
309
|
-
[' clear draft ', C.dim],
|
|
310
|
-
['Enter', C.info],
|
|
311
|
-
[' submit Esc prompt focus Ctrl+X close panel', C.dim],
|
|
312
|
-
]),
|
|
313
|
-
buildPanelLine(width, [
|
|
314
|
-
[' Ctrl+R', C.info],
|
|
315
|
-
[' refresh ', C.dim],
|
|
316
|
-
['Ctrl+A', C.info],
|
|
317
|
-
[' approve (r/a type into the draft while a question is active)', C.dim],
|
|
318
|
-
]),
|
|
319
|
-
];
|
|
320
|
-
}
|
|
321
|
-
return [
|
|
322
|
-
buildPanelLine(width, [
|
|
323
|
-
[' Up/Down', C.info],
|
|
324
|
-
[' scroll ', C.dim],
|
|
325
|
-
['r', C.info],
|
|
326
|
-
[' refresh ', C.dim],
|
|
327
|
-
['a', C.info],
|
|
328
|
-
[' approve execution-ready plan Esc prompt focus Ctrl+X close panel', C.dim],
|
|
329
|
-
]),
|
|
330
|
-
];
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
private flattenSections(
|
|
334
|
-
width: number,
|
|
335
|
-
sections: readonly RenderedPlanningSection[],
|
|
336
|
-
): { readonly lines: Line[]; readonly selectedIndex: number } {
|
|
337
|
-
const lines: Line[] = [];
|
|
338
|
-
let selectedIndex = 0;
|
|
339
|
-
for (const section of sections) {
|
|
340
|
-
const sectionStart = lines.length;
|
|
341
|
-
const titleOffset = section.title ? 1 : 0;
|
|
342
|
-
if (section.title) lines.push(buildPanelLine(width, [[` ${section.title}`, C.label]]));
|
|
343
|
-
lines.push(...section.lines);
|
|
344
|
-
if (section.selectedLineIndex !== undefined) {
|
|
345
|
-
selectedIndex = sectionStart + titleOffset + section.selectedLineIndex;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return { lines, selectedIndex };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
private buildQuestionSection(width: number, question: ProjectPlanningQuestion): RenderedPlanningSection {
|
|
352
|
-
const actions = buildAnswerActions(question, this.draftAnswer);
|
|
353
|
-
this.selectedActionIndex = this.clampActionIndex(actions.length);
|
|
354
|
-
// When a clear-draft confirmation is pending, show the confirm prompt
|
|
355
|
-
// inline above the draft line instead of the normal content.
|
|
356
|
-
if (this.clearDraftConfirm) {
|
|
357
|
-
const confirmLines = renderConfirmLines(width, this.clearDraftConfirm);
|
|
358
|
-
const lines: Line[] = [
|
|
359
|
-
...buildBodyText(width, question.prompt, C, C.planning),
|
|
360
|
-
...confirmLines,
|
|
361
|
-
];
|
|
362
|
-
return { title: 'Answer Current Question', lines, selectedLineIndex: undefined };
|
|
363
|
-
}
|
|
364
|
-
const lines: Line[] = [
|
|
365
|
-
...buildBodyText(width, question.prompt, C, C.planning),
|
|
366
|
-
];
|
|
367
|
-
if (question.whyItMatters) {
|
|
368
|
-
lines.push(...buildBodyText(width, `Why this matters: ${question.whyItMatters}`, C, C.dim));
|
|
369
|
-
}
|
|
370
|
-
if (question.recommendedAnswer && !isGenericRecommendation(question.recommendedAnswer)) {
|
|
371
|
-
lines.push(...buildBodyText(width, `Recommendation: ${question.recommendedAnswer}`, C, C.good));
|
|
372
|
-
}
|
|
373
|
-
lines.push(...buildBodyText(
|
|
374
|
-
width,
|
|
375
|
-
`Typed answer: ${this.draftAnswer || '(type here while this panel is focused)'}`,
|
|
376
|
-
C,
|
|
377
|
-
this.draftAnswer ? C.value : C.dim,
|
|
378
|
-
));
|
|
379
|
-
lines.push(buildPanelLine(width, [[
|
|
380
|
-
' Select an answer below or type your own. Enter sends it through the normal planning chat path.',
|
|
381
|
-
C.dim,
|
|
382
|
-
]]));
|
|
383
|
-
const selectedLineIndex = lines.length + this.selectedActionIndex;
|
|
384
|
-
actions.forEach((action, index) => {
|
|
385
|
-
const selected = index === this.selectedActionIndex;
|
|
386
|
-
lines.push(buildPanelListRow(width, [
|
|
387
|
-
{ text: action.label, fg: action.disabled ? C.dim : C.value, bold: selected },
|
|
388
|
-
{ text: ` ${action.detail}`, fg: C.dim },
|
|
389
|
-
], C, {
|
|
390
|
-
selected,
|
|
391
|
-
marker: selected ? '▶' : ' ',
|
|
392
|
-
}));
|
|
393
|
-
});
|
|
394
|
-
return { title: 'Answer Current Question', lines, selectedLineIndex };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
private buildStateSection(
|
|
398
|
-
width: number,
|
|
399
|
-
state: ProjectPlanningState,
|
|
400
|
-
evaluation: ProjectPlanningEvaluation | null,
|
|
401
|
-
): PanelWorkspaceSection {
|
|
402
|
-
const readiness = evaluation?.readiness ?? state.readiness;
|
|
403
|
-
const readinessColor = readiness === 'executable'
|
|
404
|
-
? C.approved
|
|
405
|
-
: readiness === 'needs-user-input'
|
|
406
|
-
? C.blocked
|
|
407
|
-
: C.dim;
|
|
408
|
-
const blockingGaps = (evaluation?.gaps ?? []).filter((gap) => gap.severity === 'blocking').length;
|
|
409
|
-
const openQuestions = state.openQuestions.filter((q) => (q.status ?? 'open') === 'open').length;
|
|
410
|
-
// Surface the most important thing first: is this plan executable, and what
|
|
411
|
-
// is the single blocker to getting there.
|
|
412
|
-
const nextStep = state.executionApproved
|
|
413
|
-
? 'approved — execution may proceed'
|
|
414
|
-
: openQuestions > 0
|
|
415
|
-
? `answer ${openQuestions} open question${openQuestions === 1 ? '' : 's'}`
|
|
416
|
-
: blockingGaps > 0
|
|
417
|
-
? `resolve ${blockingGaps} blocking gap${blockingGaps === 1 ? '' : 's'}`
|
|
418
|
-
: readiness === 'executable'
|
|
419
|
-
? 'press a to approve execution'
|
|
420
|
-
: 'continue the planning interview';
|
|
421
|
-
const lines: Line[] = [
|
|
422
|
-
buildKeyValueLine(width, [
|
|
423
|
-
{ label: 'readiness', value: readiness, valueColor: readinessColor },
|
|
424
|
-
{ label: 'approved', value: state.executionApproved ? 'yes' : 'no', valueColor: state.executionApproved ? C.approved : C.blocked },
|
|
425
|
-
{ label: 'questions', value: `${state.openQuestions.length} open / ${state.answeredQuestions.length} answered`, valueColor: C.value },
|
|
426
|
-
], C),
|
|
427
|
-
buildKeyValueLine(width, [
|
|
428
|
-
{ label: 'blocking gaps', value: String(blockingGaps), valueColor: blockingGaps > 0 ? C.blocked : C.good },
|
|
429
|
-
{ label: 'tasks', value: String(state.tasks.length), valueColor: state.tasks.length > 0 ? C.value : C.dim },
|
|
430
|
-
{ label: 'gates', value: String(state.verificationGates.length), valueColor: state.verificationGates.length > 0 ? C.good : C.dim },
|
|
431
|
-
], C),
|
|
432
|
-
buildPanelLine(width, [
|
|
433
|
-
[' Next step ', C.label],
|
|
434
|
-
[nextStep, state.executionApproved ? C.approved : C.planning],
|
|
435
|
-
]),
|
|
436
|
-
...buildBodyText(width, `Goal: ${state.goal || '(not set)'}`, C, state.goal ? C.value : C.blocked),
|
|
437
|
-
];
|
|
438
|
-
if (state.scope) lines.push(...buildBodyText(width, `Scope: ${state.scope}`, C, C.value));
|
|
439
|
-
if (state.knownContext.length) {
|
|
440
|
-
lines.push(...buildBodyText(width, `Known context: ${state.knownContext.join(' | ')}`, C, C.dim));
|
|
441
|
-
}
|
|
442
|
-
if (evaluation?.nextQuestion) {
|
|
443
|
-
lines.push(...buildBodyText(width, `Next question: ${evaluation.nextQuestion.prompt}`, C, C.planning));
|
|
444
|
-
if (evaluation.nextQuestion.whyItMatters) {
|
|
445
|
-
lines.push(...buildBodyText(width, `Why it matters: ${evaluation.nextQuestion.whyItMatters}`, C, C.dim));
|
|
446
|
-
}
|
|
447
|
-
if (evaluation.nextQuestion.recommendedAnswer && !isGenericRecommendation(evaluation.nextQuestion.recommendedAnswer)) {
|
|
448
|
-
lines.push(...buildBodyText(width, `Recommended answer: ${evaluation.nextQuestion.recommendedAnswer}`, C, C.good));
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
return { title: 'State', lines };
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
private buildGapsSection(width: number, evaluation: ProjectPlanningEvaluation | null): PanelWorkspaceSection {
|
|
455
|
-
const gaps = evaluation?.gaps ?? [];
|
|
456
|
-
if (gaps.length === 0) {
|
|
457
|
-
return {
|
|
458
|
-
title: 'Readiness Gaps',
|
|
459
|
-
lines: [buildPanelLine(width, [[' No readiness gaps.', C.good]])],
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
return {
|
|
463
|
-
title: 'Readiness Gaps',
|
|
464
|
-
lines: gaps.slice(0, 12).flatMap((gap) => buildBodyText(
|
|
465
|
-
width,
|
|
466
|
-
`${gap.severity.toUpperCase()} ${gap.kind}: ${gap.message}`,
|
|
467
|
-
C,
|
|
468
|
-
gap.severity === 'blocking' ? C.blocked : C.warn,
|
|
469
|
-
)),
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
private buildTasksSection(width: number, state: ProjectPlanningState): PanelWorkspaceSection {
|
|
474
|
-
const lines: Line[] = [];
|
|
475
|
-
if (state.tasks.length === 0) {
|
|
476
|
-
lines.push(buildPanelLine(width, [[' No decomposed tasks recorded yet.', C.dim]]));
|
|
477
|
-
} else {
|
|
478
|
-
for (const task of state.tasks) {
|
|
479
|
-
lines.push(...buildBodyText(
|
|
480
|
-
width,
|
|
481
|
-
`${task.id}: ${task.title} [${task.status ?? 'pending'}]${task.canRunConcurrently ? ' - concurrent' : ''}`,
|
|
482
|
-
C,
|
|
483
|
-
task.blockedOnUserInput ? C.blocked : C.value,
|
|
484
|
-
));
|
|
485
|
-
if (task.dependencies?.length) lines.push(...buildBodyText(width, `Dependencies: ${task.dependencies.join(', ')}`, C, C.dim));
|
|
486
|
-
if (task.verification?.length) lines.push(...buildBodyText(width, `Verification: ${task.verification.join(' | ')}`, C, C.good));
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (state.verificationGates.length) {
|
|
491
|
-
lines.push(buildPanelLine(width, [[' Verification gates:', C.label]]));
|
|
492
|
-
for (const gate of state.verificationGates) {
|
|
493
|
-
lines.push(...buildBodyText(width, `${gate.id}: ${gate.description} [${gate.status ?? 'pending'}]`, C, gate.required === false ? C.dim : C.good));
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
if (state.agentAssignments.length) {
|
|
497
|
-
lines.push(buildPanelLine(width, [[' Agent handoff candidates:', C.label]]));
|
|
498
|
-
for (const assignment of state.agentAssignments) {
|
|
499
|
-
lines.push(...buildBodyText(
|
|
500
|
-
width,
|
|
501
|
-
`${assignment.taskId}: ${assignment.agentType ?? 'none'}${assignment.canRunConcurrently ? ' - can run concurrently' : ''}`,
|
|
502
|
-
C,
|
|
503
|
-
C.info,
|
|
504
|
-
));
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
return { title: 'Task Graph', lines };
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
private buildAnsweredHistorySection(width: number, state: ProjectPlanningState): PanelWorkspaceSection {
|
|
511
|
-
const answered = state.answeredQuestions;
|
|
512
|
-
if (answered.length === 0) {
|
|
513
|
-
return {
|
|
514
|
-
title: 'Answered Questions',
|
|
515
|
-
lines: [buildPanelLine(width, [[' No questions answered yet.', C.dim]])],
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
const ordered = [...answered].sort((a, b) => (b.answeredAt ?? 0) - (a.answeredAt ?? 0));
|
|
519
|
-
return {
|
|
520
|
-
title: 'Answered Questions',
|
|
521
|
-
lines: ordered.slice(0, 12).flatMap((entry) => [
|
|
522
|
-
...buildBodyText(width, `Q: ${entry.prompt}`, C, C.value),
|
|
523
|
-
...buildBodyText(width, `A: ${entry.answer?.trim() || '(no answer recorded)'}`, C, entry.answer?.trim() ? C.good : C.dim),
|
|
524
|
-
]),
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
private buildDecisionsSection(
|
|
529
|
-
width: number,
|
|
530
|
-
state: ProjectPlanningState,
|
|
531
|
-
storedDecisions: readonly ProjectPlanningDecision[],
|
|
532
|
-
): PanelWorkspaceSection {
|
|
533
|
-
const byId = new Map<string, ProjectPlanningDecision>();
|
|
534
|
-
for (const decision of [...storedDecisions, ...state.decisions]) byId.set(decision.id, decision);
|
|
535
|
-
const decisions = [...byId.values()];
|
|
536
|
-
if (decisions.length === 0) {
|
|
537
|
-
return {
|
|
538
|
-
title: 'Decisions',
|
|
539
|
-
lines: [buildPanelLine(width, [[' No durable planning decisions recorded yet.', C.dim]])],
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
return {
|
|
543
|
-
title: 'Decisions',
|
|
544
|
-
lines: decisions.slice(0, 12).flatMap((decision) => buildBodyText(
|
|
545
|
-
width,
|
|
546
|
-
`${decision.title}: ${decision.decision} [${decision.status ?? 'accepted'}]`,
|
|
547
|
-
C,
|
|
548
|
-
decision.status === 'rejected' ? C.rejected : C.value,
|
|
549
|
-
)),
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
private buildLanguageSection(width: number, language: ProjectPlanningLanguageArtifact | null): PanelWorkspaceSection {
|
|
554
|
-
if (!language || (language.terms.length === 0 && language.ambiguities.length === 0)) {
|
|
555
|
-
return {
|
|
556
|
-
title: 'Project Language',
|
|
557
|
-
lines: [buildPanelLine(width, [[' No project language terms or ambiguity resolutions recorded yet.', C.dim]])],
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
const lines: Line[] = [];
|
|
561
|
-
for (const term of language.terms.slice(0, 8)) {
|
|
562
|
-
lines.push(...buildBodyText(width, `${term.term}: ${term.definition}`, C, C.value));
|
|
563
|
-
if (term.avoid?.length) lines.push(...buildBodyText(width, `Avoid: ${term.avoid.join(', ')}`, C, C.blocked));
|
|
564
|
-
}
|
|
565
|
-
for (const ambiguity of language.ambiguities.slice(0, 8)) {
|
|
566
|
-
lines.push(...buildBodyText(width, `Resolved ambiguity - ${ambiguity.phrase}: ${ambiguity.resolution}`, C, C.info));
|
|
567
|
-
}
|
|
568
|
-
return { title: 'Project Language', lines };
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
private refresh(force = false): void {
|
|
572
|
-
if (this.loading && !force) return;
|
|
573
|
-
this.loading = true;
|
|
574
|
-
this.markDirty();
|
|
575
|
-
this.requestRender();
|
|
576
|
-
void (async () => {
|
|
577
|
-
try {
|
|
578
|
-
const [status, stateResult, decisionsResult, languageResult] = await Promise.all([
|
|
579
|
-
this.service.status({ projectId: this.projectId }),
|
|
580
|
-
this.service.getState({ projectId: this.projectId }),
|
|
581
|
-
this.service.listDecisions({ projectId: this.projectId }),
|
|
582
|
-
this.service.getLanguage({ projectId: this.projectId }),
|
|
583
|
-
]);
|
|
584
|
-
const evaluation = await this.service.evaluate({ projectId: this.projectId });
|
|
585
|
-
this.snapshot = {
|
|
586
|
-
status,
|
|
587
|
-
state: stateResult.state,
|
|
588
|
-
evaluation,
|
|
589
|
-
decisions: decisionsResult.decisions,
|
|
590
|
-
language: languageResult.language,
|
|
591
|
-
};
|
|
592
|
-
this.clearError();
|
|
593
|
-
} catch (err) {
|
|
594
|
-
this.setError(summarizeError(err));
|
|
595
|
-
} finally {
|
|
596
|
-
this.loading = false;
|
|
597
|
-
this.markDirty();
|
|
598
|
-
this.requestRender();
|
|
599
|
-
}
|
|
600
|
-
})();
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
private getCurrentQuestion(): ProjectPlanningQuestion | null {
|
|
604
|
-
const state = this.snapshot?.state;
|
|
605
|
-
const open = state?.openQuestions.find((question) => (question.status ?? 'open') === 'open');
|
|
606
|
-
return open ?? this.snapshot?.evaluation?.nextQuestion ?? null;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
private submitSelectedAction(question: ProjectPlanningQuestion, actions: readonly PlanningAnswerAction[]): void {
|
|
610
|
-
const action = actions[this.clampActionIndex(actions.length)];
|
|
611
|
-
if (!action || action.disabled || !action.answer.trim()) {
|
|
612
|
-
this.setError('Type an answer or choose a non-empty answer option.');
|
|
613
|
-
this.requestRender();
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
if (action.kind === 'approve') {
|
|
617
|
-
this.approveExecution();
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
if (action.kind === 'dismiss') {
|
|
621
|
-
this.pausePlanning(question);
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
if (!this.submitAnswer) {
|
|
625
|
-
this.setError('Planning answer submission is not wired in this runtime.');
|
|
626
|
-
this.requestRender();
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
void (async () => {
|
|
630
|
-
try {
|
|
631
|
-
await this.persistQuestionIfNeeded(question);
|
|
632
|
-
this.draftAnswer = '';
|
|
633
|
-
this.submitAnswer?.(action.answer.trim());
|
|
634
|
-
this.refresh(true);
|
|
635
|
-
this.registerTimer(setTimeout(() => this.refresh(true), 250));
|
|
636
|
-
} catch (err) {
|
|
637
|
-
this.setError(summarizeError(err));
|
|
638
|
-
this.requestRender();
|
|
639
|
-
}
|
|
640
|
-
})();
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
private async persistQuestionIfNeeded(question: ProjectPlanningQuestion): Promise<void> {
|
|
644
|
-
const state = this.snapshot?.state;
|
|
645
|
-
if (!state) return;
|
|
646
|
-
if (state.openQuestions.some((entry) => entry.id === question.id)) return;
|
|
647
|
-
await this.service.upsertState({
|
|
648
|
-
projectId: this.projectId,
|
|
649
|
-
state: {
|
|
650
|
-
...state,
|
|
651
|
-
openQuestions: [
|
|
652
|
-
{ ...question, status: question.status ?? 'open' },
|
|
653
|
-
...state.openQuestions,
|
|
654
|
-
],
|
|
655
|
-
},
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
private pausePlanning(question: ProjectPlanningQuestion): void {
|
|
660
|
-
const state = this.snapshot?.state;
|
|
661
|
-
if (!state) {
|
|
662
|
-
this.dismissPlanning?.();
|
|
663
|
-
this.requestRender();
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
void (async () => {
|
|
667
|
-
try {
|
|
668
|
-
await this.service.upsertState({
|
|
669
|
-
projectId: this.projectId,
|
|
670
|
-
state: {
|
|
671
|
-
...state,
|
|
672
|
-
openQuestions: state.openQuestions.map((entry) =>
|
|
673
|
-
entry.id === question.id
|
|
674
|
-
? { ...entry, status: entry.status ?? 'open' }
|
|
675
|
-
: entry,
|
|
676
|
-
),
|
|
677
|
-
metadata: {
|
|
678
|
-
...(state.metadata ?? {}),
|
|
679
|
-
active: false,
|
|
680
|
-
pausedAt: Date.now(),
|
|
681
|
-
pausedFrom: 'project-planning-panel',
|
|
682
|
-
},
|
|
683
|
-
},
|
|
684
|
-
});
|
|
685
|
-
this.dismissPlanning?.();
|
|
686
|
-
this.refresh(true);
|
|
687
|
-
} catch (err) {
|
|
688
|
-
this.setError(summarizeError(err));
|
|
689
|
-
this.requestRender();
|
|
690
|
-
}
|
|
691
|
-
})();
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
private clampActionIndex(count: number): number {
|
|
695
|
-
if (count <= 0) return 0;
|
|
696
|
-
return Math.max(0, Math.min(count - 1, this.selectedActionIndex));
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
private isPrintableKey(key: string): boolean {
|
|
700
|
-
return key.length === 1 && key >= ' ';
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
private approveExecution(): void {
|
|
704
|
-
const state = this.snapshot?.state;
|
|
705
|
-
if (!state) {
|
|
706
|
-
this.setError('No planning state exists to approve.');
|
|
707
|
-
this.requestRender();
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
void (async () => {
|
|
711
|
-
try {
|
|
712
|
-
await this.service.upsertState({
|
|
713
|
-
projectId: this.projectId,
|
|
714
|
-
state: {
|
|
715
|
-
...state,
|
|
716
|
-
executionApproved: true,
|
|
717
|
-
metadata: {
|
|
718
|
-
...(state.metadata ?? {}),
|
|
719
|
-
approvedFrom: 'project-planning-panel',
|
|
720
|
-
approvedAt: Date.now(),
|
|
721
|
-
},
|
|
722
|
-
},
|
|
723
|
-
});
|
|
724
|
-
this.refresh(true);
|
|
725
|
-
} catch (err) {
|
|
726
|
-
this.setError(summarizeError(err));
|
|
727
|
-
this.requestRender();
|
|
728
|
-
}
|
|
729
|
-
})();
|
|
730
|
-
}
|
|
731
|
-
}
|