@pellux/goodvibes-tui 0.28.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-line-cache.ts +432 -0
  5. package/src/core/conversation-rendering.ts +11 -3
  6. package/src/core/conversation.ts +50 -4
  7. package/src/input/commands/cost-runtime.ts +49 -0
  8. package/src/input/commands/operator-runtime.ts +5 -1
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/handler-content-actions.ts +8 -3
  11. package/src/input/handler-feed-routes.ts +12 -35
  12. package/src/input/handler-feed.ts +4 -8
  13. package/src/input/handler-shortcuts.ts +51 -0
  14. package/src/input/handler.ts +43 -6
  15. package/src/input/input-history.ts +17 -4
  16. package/src/input/keybindings.ts +48 -8
  17. package/src/input/panel-integration-actions.ts +107 -1
  18. package/src/main.ts +19 -11
  19. package/src/panels/agent-inspector-panel.ts +125 -36
  20. package/src/panels/agent-inspector-shared.ts +144 -0
  21. package/src/panels/approval-panel.ts +67 -16
  22. package/src/panels/automation-control-panel.ts +368 -124
  23. package/src/panels/base-panel.ts +1 -1
  24. package/src/panels/builtin/agent.ts +28 -37
  25. package/src/panels/builtin/development.ts +40 -32
  26. package/src/panels/builtin/knowledge.ts +15 -6
  27. package/src/panels/builtin/operations.ts +178 -117
  28. package/src/panels/builtin/session.ts +35 -9
  29. package/src/panels/builtin/shared.ts +98 -6
  30. package/src/panels/cockpit-panel.ts +232 -73
  31. package/src/panels/communication-panel.ts +58 -20
  32. package/src/panels/confirm-state.ts +8 -1
  33. package/src/panels/control-plane-panel.ts +351 -118
  34. package/src/panels/cost-tracker-panel.ts +165 -7
  35. package/src/panels/debug-panel.ts +312 -159
  36. package/src/panels/diff-panel.ts +209 -57
  37. package/src/panels/docs-panel.ts +152 -66
  38. package/src/panels/eval-panel.ts +220 -42
  39. package/src/panels/file-explorer-panel.ts +202 -122
  40. package/src/panels/file-preview-panel.ts +132 -57
  41. package/src/panels/git-panel.ts +310 -129
  42. package/src/panels/hooks-panel.ts +150 -22
  43. package/src/panels/incident-review-panel.ts +223 -35
  44. package/src/panels/index.ts +0 -4
  45. package/src/panels/intelligence-panel.ts +212 -86
  46. package/src/panels/knowledge-graph-panel.ts +461 -101
  47. package/src/panels/local-auth-panel.ts +240 -28
  48. package/src/panels/marketplace-panel.ts +193 -26
  49. package/src/panels/memory-panel.ts +78 -77
  50. package/src/panels/ops-control-panel.ts +146 -29
  51. package/src/panels/ops-strategy-panel.ts +72 -4
  52. package/src/panels/orchestration-panel.ts +231 -69
  53. package/src/panels/panel-list-panel.ts +154 -131
  54. package/src/panels/panel-manager.ts +97 -9
  55. package/src/panels/plan-dashboard-panel.ts +333 -17
  56. package/src/panels/plugins-panel.ts +184 -29
  57. package/src/panels/policy-panel.ts +210 -38
  58. package/src/panels/polish-core.ts +7 -2
  59. package/src/panels/polish.ts +23 -4
  60. package/src/panels/project-planning-answer-actions.ts +134 -0
  61. package/src/panels/project-planning-panel.ts +62 -113
  62. package/src/panels/provider-health-panel.ts +434 -518
  63. package/src/panels/provider-health-routes.ts +203 -0
  64. package/src/panels/provider-health-tracker.ts +194 -6
  65. package/src/panels/provider-health-views.ts +560 -0
  66. package/src/panels/qr-panel.ts +116 -30
  67. package/src/panels/remote-panel.ts +114 -36
  68. package/src/panels/routes-panel.ts +63 -22
  69. package/src/panels/sandbox-panel.ts +174 -42
  70. package/src/panels/scrollable-list-panel.ts +19 -135
  71. package/src/panels/security-panel.ts +133 -33
  72. package/src/panels/services-panel.ts +116 -64
  73. package/src/panels/session-browser-panel.ts +73 -2
  74. package/src/panels/session-maintenance.ts +4 -122
  75. package/src/panels/settings-sync-panel.ts +335 -72
  76. package/src/panels/skills-panel.ts +157 -89
  77. package/src/panels/subscription-panel.ts +86 -33
  78. package/src/panels/symbol-outline-panel.ts +248 -108
  79. package/src/panels/system-messages-panel.ts +114 -13
  80. package/src/panels/tasks-panel.ts +326 -139
  81. package/src/panels/thinking-panel.ts +43 -10
  82. package/src/panels/token-budget-panel.ts +194 -18
  83. package/src/panels/tool-inspector-panel.ts +144 -34
  84. package/src/panels/types.ts +40 -4
  85. package/src/panels/work-plan-panel.ts +280 -17
  86. package/src/panels/worktree-panel.ts +175 -42
  87. package/src/panels/wrfc-panel.ts +116 -24
  88. package/src/renderer/agent-detail-modal.ts +37 -8
  89. package/src/renderer/code-block.ts +58 -28
  90. package/src/renderer/context-inspector.ts +3 -6
  91. package/src/renderer/conversation-overlays.ts +25 -11
  92. package/src/renderer/conversation-surface.ts +1 -21
  93. package/src/renderer/diff-view.ts +6 -4
  94. package/src/renderer/footer-tips.ts +41 -0
  95. package/src/renderer/fullscreen-primitives.ts +23 -17
  96. package/src/renderer/fullscreen-workspace.ts +2 -7
  97. package/src/renderer/help-overlay.ts +91 -14
  98. package/src/renderer/hint-grammar.ts +52 -0
  99. package/src/renderer/history-search-overlay.ts +10 -16
  100. package/src/renderer/layout.ts +7 -7
  101. package/src/renderer/markdown.ts +9 -1
  102. package/src/renderer/modal-factory.ts +23 -15
  103. package/src/renderer/model-picker-overlay.ts +8 -485
  104. package/src/renderer/model-workspace.ts +9 -24
  105. package/src/renderer/overlay-box.ts +23 -19
  106. package/src/renderer/process-indicator.ts +15 -25
  107. package/src/renderer/profile-picker-modal.ts +13 -14
  108. package/src/renderer/prompt-content-width.ts +16 -0
  109. package/src/renderer/search-overlay.ts +27 -6
  110. package/src/renderer/selection-modal-overlay.ts +3 -1
  111. package/src/renderer/semantic-diff.ts +1 -1
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal-helpers.ts +10 -34
  114. package/src/renderer/settings-modal.ts +70 -10
  115. package/src/renderer/shell-surface.ts +54 -15
  116. package/src/renderer/status-glyphs.ts +11 -9
  117. package/src/renderer/surface-layout.ts +0 -12
  118. package/src/renderer/term-caps.ts +1 -1
  119. package/src/renderer/theme.ts +60 -3
  120. package/src/renderer/tool-call.ts +6 -20
  121. package/src/renderer/ui-factory.ts +48 -37
  122. package/src/renderer/ui-primitives.ts +41 -3
  123. package/src/runtime/bootstrap-shell.ts +35 -18
  124. package/src/runtime/diagnostics/panels/index.ts +0 -3
  125. package/src/runtime/render-scheduler.ts +80 -0
  126. package/src/shell/ui-openers.ts +14 -22
  127. package/src/utils/format-duration.ts +2 -2
  128. package/src/utils/splash-lines.ts +54 -5
  129. package/src/version.ts +1 -1
  130. package/src/work-plans/work-plan-store.ts +13 -0
  131. package/src/panels/agent-logs-panel.ts +0 -635
  132. package/src/panels/agent-logs-shared.ts +0 -129
  133. package/src/panels/context-visualizer-panel.ts +0 -238
  134. package/src/panels/forensics-panel.ts +0 -378
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -255
  137. package/src/panels/provider-stats-panel.ts +0 -391
  138. package/src/panels/schedule-panel.ts +0 -365
  139. package/src/panels/watchers-panel.ts +0 -213
  140. package/src/renderer/file-tree.ts +0 -153
  141. package/src/renderer/progress.ts +0 -99
  142. package/src/renderer/status-token.ts +0 -67
  143. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  144. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,7 +1,12 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine, createStyledCell } from '../types/grid.ts';
3
3
  import { getDisplayWidth } from '../utils/terminal-width.ts';
4
- import { UI_TONES } from '../renderer/ui-primitives.ts';
4
+ import { resolveUiTones } from '../renderer/theme.ts';
5
+
6
+ // DEFAULT_PANEL_PALETTE is built from the mode-resolved chrome tones
7
+ // (resolveUiTones) rather than the static UI_TONES constant — WO-001 single
8
+ // read path. Mode is fixed to 'dark' until the terminal-bg-probe lands.
9
+ const UI_TONES = resolveUiTones('dark');
5
10
 
6
11
  // ---------------------------------------------------------------------------
7
12
  // Panel palette + core line primitives.
@@ -41,7 +46,7 @@ export const DEFAULT_PANEL_PALETTE: Readonly<Required<PanelPalette>> = {
41
46
  good: UI_TONES.state.good,
42
47
  warn: UI_TONES.state.warn,
43
48
  bad: UI_TONES.state.bad,
44
- empty: '#334155',
49
+ empty: UI_TONES.fg.empty,
45
50
  surfaceBg: UI_TONES.bg.surface,
46
51
  sectionBg: UI_TONES.bg.section,
47
52
  summaryBg: UI_TONES.bg.summary,
@@ -447,16 +447,26 @@ export function resolveScrollablePanelSection(
447
447
  afterSections: options.afterSections,
448
448
  minRows: options.section.minRows ?? 1,
449
449
  });
450
+ // When a window-summary row will render (list longer than the budget can
451
+ // show), it occupies one of the section's rows. Reserve that row up front so
452
+ // the summary is accounted INSIDE the budget; otherwise the appended summary
453
+ // pushes total output one line past the panel's exact-height contract and the
454
+ // tail line (typically the footer's keyboard-hints row) is silently dropped.
455
+ const summaryReserved = options.section.appendWindowSummary
456
+ && options.section.scrollableLines.length > budget
457
+ ? 1
458
+ : 0;
459
+ const windowBudget = Math.max(0, budget - summaryReserved);
450
460
  const window = options.section.selectedIndex === undefined
451
461
  ? getVisibleWindow(
452
462
  options.section.scrollableLines.length,
453
463
  options.section.scrollOffset,
454
- budget,
464
+ windowBudget,
455
465
  )
456
466
  : getTrackedVisibleWindow(
457
467
  options.section.scrollableLines.length,
458
468
  options.section.selectedIndex,
459
- budget,
469
+ windowBudget,
460
470
  options.section.scrollOffset,
461
471
  options.section.guardRows ?? 1,
462
472
  );
@@ -529,12 +539,21 @@ export function resolveStackedScrollableSections(
529
539
 
530
540
  return options.sections.map((section, index) => {
531
541
  const fixedLines = [...(section.fixedLines ?? [])];
542
+ // Same reservation as resolveScrollablePanelSection: a window-summary row
543
+ // occupies one of this section's allocated rows, so shrink the visible
544
+ // window rather than overflowing the stack's exact-height budget.
545
+ const budget = budgets[index]!;
546
+ const summaryReserved = section.appendWindowSummary
547
+ && section.scrollableLines.length > budget
548
+ ? 1
549
+ : 0;
550
+ const windowBudget = Math.max(0, budget - summaryReserved);
532
551
  const window = section.selectedIndex === undefined
533
- ? getVisibleWindow(section.scrollableLines.length, section.scrollOffset, budgets[index]!)
552
+ ? getVisibleWindow(section.scrollableLines.length, section.scrollOffset, windowBudget)
534
553
  : getTrackedVisibleWindow(
535
554
  section.scrollableLines.length,
536
555
  section.selectedIndex,
537
- budgets[index]!,
556
+ windowBudget,
538
557
  section.scrollOffset,
539
558
  section.guardRows ?? 1,
540
559
  );
@@ -0,0 +1,134 @@
1
+ import type { ProjectPlanningQuestion } from '@pellux/goodvibes-sdk/platform/knowledge';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Answer-action helpers for the Project Planning panel.
5
+ //
6
+ // Extracted from project-planning-panel.ts to keep that module under the
7
+ // architecture line-count cap. Leaf module (only the SDK question type);
8
+ // the panel is their public import site.
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export interface PlanningAnswerAction {
12
+ readonly id: string;
13
+ readonly label: string;
14
+ readonly detail: string;
15
+ readonly answer: string;
16
+ readonly kind?: 'answer' | 'approve' | 'dismiss';
17
+ readonly disabled?: boolean;
18
+ }
19
+
20
+ /** SDK placeholder recommendations that carry no real signal — never surfaced as an answer action. */
21
+ export function isGenericRecommendation(value: string): boolean {
22
+ return /\bdefine the first-pass scope\b/i.test(value)
23
+ || /\bcreate task records\b/i.test(value)
24
+ || /\brecord concrete tests\b/i.test(value)
25
+ || /\bseparate out-of-scope work\b/i.test(value);
26
+ }
27
+
28
+ function compactAnswerDetail(text: string): string {
29
+ const normalized = text.replace(/\s+/g, ' ').trim();
30
+ return normalized.length > 86 ? `${normalized.slice(0, 83)}...` : normalized;
31
+ }
32
+
33
+ /**
34
+ * Builds the answer-action list for the current question: keyword-matched
35
+ * canned suggestions first (de-duplicated by normalized answer text — a
36
+ * question that matches more than one keyword category, e.g. scope +
37
+ * recommendedAnswer echoing one of the scope answers verbatim, must not show
38
+ * the same suggested answer twice), then the fixed ask-narrower/custom/
39
+ * dismiss actions.
40
+ */
41
+ export function buildAnswerActions(question: ProjectPlanningQuestion, draftAnswer: string): PlanningAnswerAction[] {
42
+ const canned: PlanningAnswerAction[] = [];
43
+ const prompt = question.prompt.toLowerCase();
44
+ const isScopeQuestion = prompt.includes('scope') || prompt.includes('in or out');
45
+ const isTaskQuestion = prompt.includes('task') || prompt.includes('dependency') || prompt.includes('work breakdown');
46
+ const isVerificationQuestion = prompt.includes('verification') || prompt.includes('test') || prompt.includes('prove');
47
+ const isApprovalQuestion = prompt.includes('approved') || prompt.includes('approve') || prompt.includes('execution');
48
+ if (isApprovalQuestion) {
49
+ canned.push({
50
+ id: 'approve-execution',
51
+ label: 'Approve execution',
52
+ detail: 'Mark this plan approved so execution may proceed.',
53
+ answer: 'Approve this planning state for execution.',
54
+ kind: 'approve',
55
+ });
56
+ }
57
+ if (isScopeQuestion) {
58
+ canned.push({
59
+ id: 'scope-focused-first-pass',
60
+ label: 'Use focused first-pass scope',
61
+ detail: 'Fill a concrete end-to-end scope for this goal and keep unrelated work out.',
62
+ answer: 'Use a focused first-pass scope for this goal.',
63
+ });
64
+ }
65
+ if (isTaskQuestion) {
66
+ canned.push({
67
+ id: 'tasks-default-breakdown',
68
+ label: 'Create default task breakdown',
69
+ detail: 'Create inspect, implement, wire, and verify tasks with dependencies.',
70
+ answer: 'Create the default task breakdown for this goal.',
71
+ });
72
+ }
73
+ if (isVerificationQuestion) {
74
+ canned.push({
75
+ id: 'verification-default-gates',
76
+ label: 'Use standard verification gates',
77
+ detail: 'Require focused regression coverage, typecheck/build validation, and a runtime smoke where feasible.',
78
+ answer: 'Use standard verification gates for this goal.',
79
+ });
80
+ }
81
+ if (question.recommendedAnswer?.trim() && !isGenericRecommendation(question.recommendedAnswer)) {
82
+ canned.push({
83
+ id: 'recommended',
84
+ label: 'Use recommended answer',
85
+ detail: compactAnswerDetail(question.recommendedAnswer),
86
+ answer: question.recommendedAnswer,
87
+ });
88
+ }
89
+ if (isScopeQuestion) {
90
+ canned.push({
91
+ id: 'scope-end-to-end',
92
+ label: 'End-to-end required scope',
93
+ detail: 'Let the plan include every component needed to make this work, but avoid unrelated cleanup.',
94
+ answer: 'Scope is everything required to make the requested outcome work end-to-end. Include TUI, daemon composition, configuration, docs, and tests if they are required. Do not include unrelated cleanup or broad refactors unless they are necessary for this task.',
95
+ });
96
+ canned.push({
97
+ id: 'scope-tui-first',
98
+ label: 'TUI-first scope',
99
+ detail: 'Fix TUI behavior here; report SDK blockers instead of patching around SDK-owned bugs.',
100
+ answer: 'Scope is TUI-owned behavior first. If a blocker is SDK-owned, report the exact SDK contract/runtime issue instead of patching around it in the TUI. Include daemon composition only where the TUI owns the wiring.',
101
+ });
102
+ }
103
+
104
+ const seenAnswers = new Set<string>();
105
+ const actions: PlanningAnswerAction[] = [];
106
+ for (const action of canned) {
107
+ const key = action.answer.trim().toLowerCase().replace(/\s+/g, ' ');
108
+ if (key.length > 0 && seenAnswers.has(key)) continue;
109
+ if (key.length > 0) seenAnswers.add(key);
110
+ actions.push(action);
111
+ }
112
+
113
+ actions.push({
114
+ id: 'ask-narrower',
115
+ label: 'I am not sure yet',
116
+ detail: 'Break this into smaller concrete choices with examples and a recommended default.',
117
+ answer: `I do not know enough to answer "${question.prompt}" as asked. Break it into smaller concrete questions with 2-4 specific choices, explain the tradeoffs, recommend a default, and ask me the first one.`,
118
+ });
119
+ actions.push({
120
+ id: 'custom',
121
+ label: 'Submit typed answer',
122
+ detail: draftAnswer ? compactAnswerDetail(draftAnswer) : 'Type an answer first; this row becomes the custom answer.',
123
+ answer: draftAnswer.trim(),
124
+ disabled: !draftAnswer.trim(),
125
+ });
126
+ actions.push({
127
+ id: 'dismiss-planning',
128
+ label: 'Close planning and continue without it',
129
+ detail: 'Pause project planning for this workspace. Normal chat continues; /plan can reopen it later.',
130
+ answer: 'Pause project planning for this workspace and continue without the planning panel.',
131
+ kind: 'dismiss',
132
+ });
133
+ return actions;
134
+ }
@@ -24,6 +24,7 @@ import {
24
24
  type PanelWorkspaceSection,
25
25
  } from './polish.ts';
26
26
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
27
+ import { buildAnswerActions, isGenericRecommendation, type PlanningAnswerAction } from './project-planning-answer-actions.ts';
27
28
 
28
29
  const C = extendPalette(DEFAULT_PANEL_PALETTE, {
29
30
  planning: '#38bdf8',
@@ -48,15 +49,6 @@ export interface ProjectPlanningPanelOptions {
48
49
  readonly dismissPlanning?: () => void;
49
50
  }
50
51
 
51
- interface PlanningAnswerAction {
52
- readonly id: string;
53
- readonly label: string;
54
- readonly detail: string;
55
- readonly answer: string;
56
- readonly kind?: 'answer' | 'approve' | 'dismiss';
57
- readonly disabled?: boolean;
58
- }
59
-
60
52
  interface RenderedPlanningSection extends PanelWorkspaceSection {
61
53
  readonly selectedLineIndex?: number;
62
54
  }
@@ -111,9 +103,32 @@ export class ProjectPlanningPanel extends BasePanel {
111
103
  }
112
104
  // confirmResult === 'inactive': proceed with normal dispatch.
113
105
 
106
+ // Ctrl+R (refresh) / Ctrl+A (approve) are alternate bindings for 'r'/'a'
107
+ // that stay reachable while a question is active, where plain 'r'/'a'
108
+ // are swallowed into the draft-answer text below instead. They are
109
+ // checked before the question gate so they work in both modes.
110
+ //
111
+ // NOTE: the input-routing layer (src/input/handler-feed-routes.ts, owned
112
+ // by WO-150 in this wave) does not yet forward ctrl-modified keys to the
113
+ // active panel — it currently intercepts all `token.ctrl` input before
114
+ // panel.handleInput is ever called, and a bare Ctrl+R is already bound
115
+ // globally to 'history-search'. This handler recognizes the logical key
116
+ // strings 'ctrl+r'/'ctrl+a' so the panel-side behavior is correct and
117
+ // testable now; wiring a real Ctrl+R/Ctrl+A keypress through to these
118
+ // strings is an input-handler change outside this work order's file
119
+ // scope.
120
+ if (key === 'ctrl+r') {
121
+ this.refresh(true);
122
+ return true;
123
+ }
124
+ if (key === 'ctrl+a') {
125
+ this.approveExecution();
126
+ return true;
127
+ }
128
+
114
129
  const question = this.getCurrentQuestion();
115
130
  if (question) {
116
- const actions = this.getAnswerActions(question);
131
+ const actions = buildAnswerActions(question, this.draftAnswer);
117
132
  this.selectedActionIndex = this.clampActionIndex(actions.length);
118
133
  if (key === 'up') {
119
134
  this.selectedActionIndex = Math.max(0, this.selectedActionIndex - 1);
@@ -204,9 +219,15 @@ export class ProjectPlanningPanel extends BasePanel {
204
219
  { label: 'space', value: status?.knowledgeSpaceId ?? `project:${this.projectId}`, valueColor: C.value },
205
220
  { label: 'mode', value: 'TUI-owned passive backing store', valueColor: C.info },
206
221
  ], C),
207
- buildPanelLine(width, [
208
- [' Planning never starts from daemon, webhooks, ntfy, Home Assistant, or companion surfaces.', C.dim],
209
- ]),
222
+ // Live artifact counts from the SDK status route (same counts /plan
223
+ // prints at planning-runtime.ts:90) instead of static filler prose.
224
+ status
225
+ ? buildKeyValueLine(width, [
226
+ { label: 'states', value: String(status.counts.states), valueColor: status.counts.states > 0 ? C.value : C.dim },
227
+ { label: 'decisions', value: String(status.counts.decisions), valueColor: status.counts.decisions > 0 ? C.value : C.dim },
228
+ { label: 'language', value: String(status.counts.languageArtifacts), valueColor: status.counts.languageArtifacts > 0 ? C.value : C.dim },
229
+ ], C)
230
+ : buildPanelLine(width, [[' Artifact counts unavailable — refreshing…', C.dim]]),
210
231
  ],
211
232
  });
212
233
 
@@ -227,6 +248,7 @@ export class ProjectPlanningPanel extends BasePanel {
227
248
  if (question) sections.push(this.buildQuestionSection(width, question));
228
249
  sections.push(this.buildGapsSection(width, evaluation));
229
250
  sections.push(this.buildTasksSection(width, state));
251
+ sections.push(this.buildAnsweredHistorySection(width, state));
230
252
  sections.push(this.buildDecisionsSection(width, state, decisions));
231
253
  sections.push(this.buildLanguageSection(width, language));
232
254
  }
@@ -278,6 +300,12 @@ export class ProjectPlanningPanel extends BasePanel {
278
300
  ['Enter', C.info],
279
301
  [' submit Esc prompt focus Ctrl+X close panel', C.dim],
280
302
  ]),
303
+ buildPanelLine(width, [
304
+ [' Ctrl+R', C.info],
305
+ [' refresh ', C.dim],
306
+ ['Ctrl+A', C.info],
307
+ [' approve (r/a type into the draft while a question is active)', C.dim],
308
+ ]),
281
309
  ];
282
310
  }
283
311
  return [
@@ -311,7 +339,7 @@ export class ProjectPlanningPanel extends BasePanel {
311
339
  }
312
340
 
313
341
  private buildQuestionSection(width: number, question: ProjectPlanningQuestion): RenderedPlanningSection {
314
- const actions = this.getAnswerActions(question);
342
+ const actions = buildAnswerActions(question, this.draftAnswer);
315
343
  this.selectedActionIndex = this.clampActionIndex(actions.length);
316
344
  // When a clear-draft confirmation is pending, show the confirm prompt
317
345
  // inline above the draft line instead of the normal content.
@@ -329,7 +357,7 @@ export class ProjectPlanningPanel extends BasePanel {
329
357
  if (question.whyItMatters) {
330
358
  lines.push(...buildBodyText(width, `Why this matters: ${question.whyItMatters}`, C, C.dim));
331
359
  }
332
- if (question.recommendedAnswer && !this.isGenericRecommendation(question.recommendedAnswer)) {
360
+ if (question.recommendedAnswer && !isGenericRecommendation(question.recommendedAnswer)) {
333
361
  lines.push(...buildBodyText(width, `Recommendation: ${question.recommendedAnswer}`, C, C.good));
334
362
  }
335
363
  lines.push(...buildBodyText(
@@ -406,7 +434,7 @@ export class ProjectPlanningPanel extends BasePanel {
406
434
  if (evaluation.nextQuestion.whyItMatters) {
407
435
  lines.push(...buildBodyText(width, `Why it matters: ${evaluation.nextQuestion.whyItMatters}`, C, C.dim));
408
436
  }
409
- if (evaluation.nextQuestion.recommendedAnswer && !this.isGenericRecommendation(evaluation.nextQuestion.recommendedAnswer)) {
437
+ if (evaluation.nextQuestion.recommendedAnswer && !isGenericRecommendation(evaluation.nextQuestion.recommendedAnswer)) {
410
438
  lines.push(...buildBodyText(width, `Recommended answer: ${evaluation.nextQuestion.recommendedAnswer}`, C, C.good));
411
439
  }
412
440
  }
@@ -469,6 +497,24 @@ export class ProjectPlanningPanel extends BasePanel {
469
497
  return { title: 'Task Graph', lines };
470
498
  }
471
499
 
500
+ private buildAnsweredHistorySection(width: number, state: ProjectPlanningState): PanelWorkspaceSection {
501
+ const answered = state.answeredQuestions;
502
+ if (answered.length === 0) {
503
+ return {
504
+ title: 'Answered Questions',
505
+ lines: [buildPanelLine(width, [[' No questions answered yet.', C.dim]])],
506
+ };
507
+ }
508
+ const ordered = [...answered].sort((a, b) => (b.answeredAt ?? 0) - (a.answeredAt ?? 0));
509
+ return {
510
+ title: 'Answered Questions',
511
+ lines: ordered.slice(0, 12).flatMap((entry) => [
512
+ ...buildBodyText(width, `Q: ${entry.prompt}`, C, C.value),
513
+ ...buildBodyText(width, `A: ${entry.answer?.trim() || '(no answer recorded)'}`, C, entry.answer?.trim() ? C.good : C.dim),
514
+ ]),
515
+ };
516
+ }
517
+
472
518
  private buildDecisionsSection(
473
519
  width: number,
474
520
  state: ProjectPlanningState,
@@ -550,98 +596,6 @@ export class ProjectPlanningPanel extends BasePanel {
550
596
  return open ?? this.snapshot?.evaluation?.nextQuestion ?? null;
551
597
  }
552
598
 
553
- private getAnswerActions(question: ProjectPlanningQuestion): PlanningAnswerAction[] {
554
- const actions: PlanningAnswerAction[] = [];
555
- const prompt = question.prompt.toLowerCase();
556
- const isScopeQuestion = prompt.includes('scope') || prompt.includes('in or out');
557
- const isTaskQuestion = prompt.includes('task') || prompt.includes('dependency') || prompt.includes('work breakdown');
558
- const isVerificationQuestion = prompt.includes('verification') || prompt.includes('test') || prompt.includes('prove');
559
- const isApprovalQuestion = prompt.includes('approved') || prompt.includes('approve') || prompt.includes('execution');
560
- if (isApprovalQuestion) {
561
- actions.push({
562
- id: 'approve-execution',
563
- label: 'Approve execution',
564
- detail: 'Mark this plan approved so execution may proceed.',
565
- answer: 'Approve this planning state for execution.',
566
- kind: 'approve',
567
- });
568
- }
569
- if (isScopeQuestion) {
570
- actions.push({
571
- id: 'scope-focused-first-pass',
572
- label: 'Use focused first-pass scope',
573
- detail: 'Fill a concrete end-to-end scope for this goal and keep unrelated work out.',
574
- answer: 'Use a focused first-pass scope for this goal.',
575
- });
576
- }
577
- if (isTaskQuestion) {
578
- actions.push({
579
- id: 'tasks-default-breakdown',
580
- label: 'Create default task breakdown',
581
- detail: 'Create inspect, implement, wire, and verify tasks with dependencies.',
582
- answer: 'Create the default task breakdown for this goal.',
583
- });
584
- }
585
- if (isVerificationQuestion) {
586
- actions.push({
587
- id: 'verification-default-gates',
588
- label: 'Use standard verification gates',
589
- detail: 'Require focused regression coverage, typecheck/build validation, and a runtime smoke where feasible.',
590
- answer: 'Use standard verification gates for this goal.',
591
- });
592
- }
593
- if (question.recommendedAnswer?.trim() && !this.isGenericRecommendation(question.recommendedAnswer)) {
594
- actions.push({
595
- id: 'recommended',
596
- label: 'Use recommended answer',
597
- detail: this.compact(question.recommendedAnswer),
598
- answer: question.recommendedAnswer,
599
- });
600
- }
601
- if (isScopeQuestion) {
602
- actions.push({
603
- id: 'scope-end-to-end',
604
- label: 'End-to-end required scope',
605
- detail: 'Let the plan include every component needed to make this work, but avoid unrelated cleanup.',
606
- answer: 'Scope is everything required to make the requested outcome work end-to-end. Include TUI, daemon composition, configuration, docs, and tests if they are required. Do not include unrelated cleanup or broad refactors unless they are necessary for this task.',
607
- });
608
- actions.push({
609
- id: 'scope-tui-first',
610
- label: 'TUI-first scope',
611
- detail: 'Fix TUI behavior here; report SDK blockers instead of patching around SDK-owned bugs.',
612
- answer: 'Scope is TUI-owned behavior first. If a blocker is SDK-owned, report the exact SDK contract/runtime issue instead of patching around it in the TUI. Include daemon composition only where the TUI owns the wiring.',
613
- });
614
- }
615
- actions.push({
616
- id: 'ask-narrower',
617
- label: 'I am not sure yet',
618
- detail: 'Break this into smaller concrete choices with examples and a recommended default.',
619
- answer: `I do not know enough to answer "${question.prompt}" as asked. Break it into smaller concrete questions with 2-4 specific choices, explain the tradeoffs, recommend a default, and ask me the first one.`,
620
- });
621
- actions.push({
622
- id: 'custom',
623
- label: 'Submit typed answer',
624
- detail: this.draftAnswer ? this.compact(this.draftAnswer) : 'Type an answer first; this row becomes the custom answer.',
625
- answer: this.draftAnswer.trim(),
626
- disabled: !this.draftAnswer.trim(),
627
- });
628
- actions.push({
629
- id: 'dismiss-planning',
630
- label: 'Close planning and continue without it',
631
- detail: 'Pause project planning for this workspace. Normal chat continues; /plan can reopen it later.',
632
- answer: 'Pause project planning for this workspace and continue without the planning panel.',
633
- kind: 'dismiss',
634
- });
635
- return actions;
636
- }
637
-
638
- private isGenericRecommendation(value: string): boolean {
639
- return /\bdefine the first-pass scope\b/i.test(value)
640
- || /\bcreate task records\b/i.test(value)
641
- || /\brecord concrete tests\b/i.test(value)
642
- || /\bseparate out-of-scope work\b/i.test(value);
643
- }
644
-
645
599
  private submitSelectedAction(question: ProjectPlanningQuestion, actions: readonly PlanningAnswerAction[]): void {
646
600
  const action = actions[this.clampActionIndex(actions.length)];
647
601
  if (!action || action.disabled || !action.answer.trim()) {
@@ -736,11 +690,6 @@ export class ProjectPlanningPanel extends BasePanel {
736
690
  return key.length === 1 && key >= ' ';
737
691
  }
738
692
 
739
- private compact(text: string): string {
740
- const normalized = text.replace(/\s+/g, ' ').trim();
741
- return normalized.length > 86 ? `${normalized.slice(0, 83)}...` : normalized;
742
- }
743
-
744
693
  private approveExecution(): void {
745
694
  const state = this.snapshot?.state;
746
695
  if (!state) {