@xenonbyte/da-vinci-workflow 0.2.2 → 0.2.4

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 (72) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +49 -14
  3. package/README.zh-CN.md +169 -14
  4. package/commands/claude/dv/breakdown.md +8 -0
  5. package/commands/claude/dv/build.md +16 -0
  6. package/commands/claude/dv/continue.md +4 -0
  7. package/commands/claude/dv/design.md +5 -2
  8. package/commands/claude/dv/tasks.md +14 -0
  9. package/commands/claude/dv/verify.md +11 -0
  10. package/commands/codex/prompts/dv-breakdown.md +8 -0
  11. package/commands/codex/prompts/dv-build.md +16 -0
  12. package/commands/codex/prompts/dv-continue.md +4 -0
  13. package/commands/codex/prompts/dv-design.md +5 -2
  14. package/commands/codex/prompts/dv-tasks.md +14 -0
  15. package/commands/codex/prompts/dv-verify.md +10 -0
  16. package/commands/gemini/dv/breakdown.toml +8 -0
  17. package/commands/gemini/dv/build.toml +16 -0
  18. package/commands/gemini/dv/continue.toml +4 -0
  19. package/commands/gemini/dv/design.toml +5 -2
  20. package/commands/gemini/dv/tasks.toml +14 -0
  21. package/commands/gemini/dv/verify.toml +10 -0
  22. package/commands/templates/dv-continue.shared.md +4 -0
  23. package/docs/discipline-and-orchestration-upgrade.md +83 -0
  24. package/docs/dv-command-reference.md +61 -2
  25. package/docs/execution-chain-migration.md +23 -0
  26. package/docs/execution-chain-plan.md +10 -3
  27. package/docs/mode-use-cases.md +2 -1
  28. package/docs/pencil-rendering-workflow.md +15 -12
  29. package/docs/prompt-entrypoints.md +5 -0
  30. package/docs/prompt-presets/README.md +1 -1
  31. package/docs/prompt-presets/desktop-app.md +3 -3
  32. package/docs/prompt-presets/mobile-app.md +3 -3
  33. package/docs/prompt-presets/tablet-app.md +3 -3
  34. package/docs/prompt-presets/web-app.md +3 -3
  35. package/docs/skill-usage.md +61 -38
  36. package/docs/workflow-examples.md +16 -13
  37. package/docs/workflow-overview.md +19 -0
  38. package/docs/zh-CN/dv-command-reference.md +59 -2
  39. package/docs/zh-CN/execution-chain-migration.md +23 -0
  40. package/docs/zh-CN/mode-use-cases.md +2 -1
  41. package/docs/zh-CN/pencil-rendering-workflow.md +15 -12
  42. package/docs/zh-CN/prompt-entrypoints.md +5 -0
  43. package/docs/zh-CN/prompt-presets/README.md +1 -1
  44. package/docs/zh-CN/prompt-presets/desktop-app.md +3 -3
  45. package/docs/zh-CN/prompt-presets/mobile-app.md +3 -3
  46. package/docs/zh-CN/prompt-presets/tablet-app.md +3 -3
  47. package/docs/zh-CN/prompt-presets/web-app.md +3 -3
  48. package/docs/zh-CN/skill-usage.md +61 -38
  49. package/docs/zh-CN/workflow-examples.md +15 -13
  50. package/docs/zh-CN/workflow-overview.md +19 -0
  51. package/examples/greenfield-spec-markupflow/.da-vinci/state/execution-signals/demo__lint-tasks.json +16 -0
  52. package/lib/audit-parsers.js +166 -10
  53. package/lib/audit.js +3 -26
  54. package/lib/cli.js +156 -2
  55. package/lib/design-source-registry.js +146 -0
  56. package/lib/execution-profile.js +143 -0
  57. package/lib/execution-signals.js +19 -1
  58. package/lib/lint-tasks.js +86 -2
  59. package/lib/planning-parsers.js +255 -18
  60. package/lib/save-current-design.js +790 -0
  61. package/lib/supervisor-review.js +3 -2
  62. package/lib/task-execution.js +160 -0
  63. package/lib/task-review.js +197 -0
  64. package/lib/verify.js +152 -1
  65. package/lib/workflow-bootstrap.js +2 -13
  66. package/lib/workflow-persisted-state.js +3 -1
  67. package/lib/workflow-state.js +503 -33
  68. package/lib/worktree-preflight.js +214 -0
  69. package/package.json +1 -1
  70. package/references/artifact-templates.md +56 -6
  71. package/tui/catalog.js +103 -0
  72. package/tui/index.js +2274 -418
package/tui/index.js CHANGED
@@ -1,15 +1,17 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
1
3
  const path = require("path");
2
4
  const readline = require("readline");
3
5
  const { spawnSync } = require("child_process");
6
+ const { version: PACKAGE_VERSION } = require("../package.json");
4
7
  const {
8
+ SCENES,
5
9
  COMMANDS,
6
- STAGES,
7
10
  buildCommandArgs,
8
11
  buildDisplayCommand,
9
12
  getCommandParameterItems,
10
13
  getCommandById,
11
14
  getDefaultLanguage,
12
- getStageById,
13
15
  hasPlaceholders,
14
16
  normalizeLanguage,
15
17
  resolveLocalizedText,
@@ -17,112 +19,282 @@ const {
17
19
  } = require("./catalog");
18
20
 
19
21
  const DA_VINCI_BIN = path.resolve(__dirname, "..", "bin", "da-vinci.js");
22
+ const PREFERENCES_PATH = path.join(os.homedir(), ".da-vinci", "tui-preferences.json");
23
+ const LOGS_DIR_RELATIVE = path.join(".da-vinci", "logs");
24
+ const BRAND_LOGO_MIN_INNER_WIDTH = 51;
25
+ const MIN_TUI_WIDTH = 36;
26
+ const DEFAULT_TUI_WIDTH = 100;
27
+ const MIN_TUI_HEIGHT = 16;
28
+ const DEFAULT_TUI_HEIGHT = 40;
29
+
30
+ const PROMPT_PRESET_FILES = {
31
+ mobile: "mobile-app.md",
32
+ desktop: "desktop-app.md",
33
+ tablet: "tablet-app.md",
34
+ web: "web-app.md"
35
+ };
36
+
37
+ const PROMPT_MODE_TO_HEADING = {
38
+ mode1: "Simple Redesign",
39
+ mode2: "Complex Redesign",
40
+ mode3: "Redesign With Reference Directory",
41
+ mode4: "Design-Only",
42
+ mode5: "Continue"
43
+ };
44
+
45
+ const QUALITY_TO_VARIANT = {
46
+ normal: 1,
47
+ "high quality": 2,
48
+ masterpiece: 3
49
+ };
50
+
51
+ const DESIGN_PLATFORM_OPTIONS = [
52
+ {
53
+ value: "mobile",
54
+ label: { en: "Mobile", zh: "移动端" }
55
+ },
56
+ {
57
+ value: "desktop",
58
+ label: { en: "Desktop", zh: "桌面端" }
59
+ },
60
+ {
61
+ value: "tablet",
62
+ label: { en: "Tablet", zh: "平板端" }
63
+ },
64
+ {
65
+ value: "web",
66
+ label: { en: "Web", zh: "Web 端" }
67
+ }
68
+ ];
69
+
70
+ const DESIGN_QUALITY_OPTIONS = [
71
+ {
72
+ value: "normal",
73
+ variant: 1,
74
+ label: { en: "Normal", zh: "普通" }
75
+ },
76
+ {
77
+ value: "high quality",
78
+ variant: 2,
79
+ label: { en: "High Quality", zh: "高质量" }
80
+ },
81
+ {
82
+ value: "masterpiece",
83
+ variant: 3,
84
+ label: { en: "Masterpiece", zh: "杰作级" }
85
+ }
86
+ ];
87
+
88
+ const PLATFORM_INSTALL_OPTIONS = [
89
+ {
90
+ value: "codex",
91
+ label: { en: "Codex", zh: "Codex" }
92
+ },
93
+ {
94
+ value: "claude",
95
+ label: { en: "Claude Code", zh: "Claude Code" }
96
+ },
97
+ {
98
+ value: "gemini",
99
+ label: { en: "Gemini", zh: "Gemini" }
100
+ }
101
+ ];
20
102
 
21
103
  const HELP_TEXT = {
22
104
  en: [
23
105
  "Da Vinci TUI",
24
106
  "",
25
107
  "Usage:",
26
- " da-vinci tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error]",
27
- " da-vinci-tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error]",
108
+ " da-vinci tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error] [--tui-width <cols>] [--alt-screen|--no-alt-screen]",
109
+ " da-vinci-tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error] [--tui-width <cols>] [--alt-screen|--no-alt-screen]",
110
+ "",
111
+ "Model:",
112
+ " - root menu is scene-first, not command-first",
113
+ " - scene pages focus on operator workflows, not raw command browsing",
114
+ " - width defaults to current TTY columns; pin via --tui-width",
28
115
  "",
29
116
  "Keyboard:",
30
117
  " Up/Down or j/k move selection",
31
- " Enter or r run selected command",
32
- " m edit the preview command before running",
33
- " h show parameters for the selected command",
34
- " p set project path context",
35
- " c set change id context",
36
- " l toggle UI language",
37
- " s toggle --strict for supported commands",
38
- " J toggle --json for supported commands",
39
- " e toggle --continue-on-error for supported commands",
40
- " ? toggle help overlay",
41
- " q quit",
42
- "",
43
- "Notes:",
44
- " - specialized commands keep placeholders such as <pen-path>; press m to edit them before execution",
45
- " - when the TUI starts inside a project root, --project is omitted because CLI commands already fall back to cwd"
118
+ " Enter or r confirm selected menu item",
119
+ " Back/exit use menu items (Back / Exit)",
120
+ " t cycle theme mode",
121
+ " l toggle language",
122
+ " p set project path",
123
+ " c set change id",
124
+ " s toggle --strict",
125
+ " J toggle --json",
126
+ " e toggle --continue-on-error",
127
+ " ? toggle help",
128
+ " Ctrl-C emergency exit"
46
129
  ].join("\n"),
47
130
  zh: [
48
131
  "Da Vinci TUI",
49
132
  "",
50
133
  "用法:",
51
- " da-vinci tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error]",
52
- " da-vinci-tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error]",
134
+ " da-vinci tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error] [--tui-width <cols>] [--alt-screen|--no-alt-screen]",
135
+ " da-vinci-tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error] [--tui-width <cols>] [--alt-screen|--no-alt-screen]",
136
+ "",
137
+ "模型:",
138
+ " - 首屏按场景组织,不再先暴露命令目录",
139
+ " - 场景页聚焦操作流程,不暴露原子命令浏览",
140
+ " - 宽度默认跟随当前 TTY 列数;可用 --tui-width 固定",
53
141
  "",
54
142
  "键位:",
55
143
  " 上/下 或 j/k 移动选中项",
56
- " Enter 或 r 执行当前命令",
57
- " m 先编辑预览命令,再执行",
58
- " h 查看当前命令参数",
59
- " p 设置项目路径上下文",
60
- " c 设置 change id 上下文",
61
- " l 切换中英文界面",
62
- " s 为支持的命令切换 --strict",
63
- " J 为支持的命令切换 --json",
64
- " e 为支持的命令切换 --continue-on-error",
65
- " ? 打开或关闭帮助层",
66
- " q 退出",
67
- "",
68
- "说明:",
69
- " - 特殊命令会保留 <pen-path> 这类占位符,执行前按 m 编辑即可",
70
- " - 当 TUI 在项目根目录启动时,CLI 已默认使用 cwd,所以不会额外拼接 --project"
144
+ " Enter 或 r 确认当前菜单项",
145
+ " 返回/退出 通过菜单项(上一步/退出)",
146
+ " t 切换主题",
147
+ " l 切换语言",
148
+ " p 设置项目路径",
149
+ " c 设置 change id",
150
+ " s 切换 --strict",
151
+ " J 切换 --json",
152
+ " e 切换 --continue-on-error",
153
+ " ? 打开或关闭帮助",
154
+ " Ctrl-C 紧急退出"
71
155
  ].join("\n")
72
156
  };
73
157
 
74
- function formatTuiHelp(lang) {
75
- const normalized = normalizeLanguage(lang || getDefaultLanguage());
76
- return HELP_TEXT[normalized];
158
+ const ANSI_PATTERN = /\x1b\[[0-?]*[ -/]*[@-~]/g;
159
+
160
+ function detectTerminalThemeMode() {
161
+ const forced = String(process.env.DA_VINCI_TUI_THEME || "").trim().toLowerCase();
162
+ if (forced === "light" || forced === "dark" || forced === "mono") {
163
+ return forced;
164
+ }
165
+ const colorfgbg = String(process.env.COLORFGBG || "").trim();
166
+ if (colorfgbg) {
167
+ const tokens = colorfgbg
168
+ .split(";")
169
+ .map((token) => Number.parseInt(token, 10))
170
+ .filter((token) => Number.isFinite(token));
171
+ if (tokens.length > 0) {
172
+ const background = tokens[tokens.length - 1];
173
+ if (background >= 7) {
174
+ return "light";
175
+ }
176
+ return "dark";
177
+ }
178
+ }
179
+ return "dark";
77
180
  }
78
181
 
79
- function formatCommandParameterHelp(command, lang, width) {
80
- const items = getCommandParameterItems(command);
81
- const lines = [];
82
- const normalized = normalizeLanguage(lang || getDefaultLanguage());
83
- lines.push(normalized === "zh" ? "命令参数 (h):" : "Command Parameters (h):");
182
+ function createTheme(mode) {
183
+ if (mode === "light") {
184
+ return {
185
+ reset: "\x1b[0m",
186
+ bold: "\x1b[1m",
187
+ dim: "\x1b[2m",
188
+ textStrong: "\x1b[38;5;234m",
189
+ soft: "\x1b[38;5;238m",
190
+ muted: "\x1b[38;5;240m",
191
+ border: "\x1b[38;5;245m",
192
+ accent: "\x1b[38;5;18m",
193
+ green: "\x1b[38;5;22m",
194
+ amber: "\x1b[38;5;94m",
195
+ red: "\x1b[38;5;88m",
196
+ info: "\x1b[38;5;24m"
197
+ };
198
+ }
199
+ if (mode === "mono") {
200
+ return {
201
+ reset: "\x1b[0m",
202
+ bold: "",
203
+ dim: "",
204
+ textStrong: "",
205
+ soft: "",
206
+ muted: "",
207
+ border: "",
208
+ accent: "",
209
+ green: "",
210
+ amber: "",
211
+ red: "",
212
+ info: ""
213
+ };
214
+ }
215
+ return {
216
+ reset: "\x1b[0m",
217
+ bold: "\x1b[1m",
218
+ dim: "\x1b[2m",
219
+ textStrong: "\x1b[38;5;255m",
220
+ soft: "\x1b[38;5;252m",
221
+ muted: "\x1b[38;5;244m",
222
+ border: "\x1b[38;5;240m",
223
+ accent: "\x1b[38;5;45m",
224
+ green: "\x1b[38;5;78m",
225
+ amber: "\x1b[38;5;221m",
226
+ red: "\x1b[38;5;203m",
227
+ info: "\x1b[38;5;117m"
228
+ };
229
+ }
84
230
 
85
- if (items.length === 0) {
86
- lines.push(
87
- normalized === "zh"
88
- ? "这条命令在 TUI 里没有额外可调参数,直接按预览命令执行即可。"
89
- : "This command has no extra adjustable parameters in the TUI. Run it exactly as previewed."
90
- );
91
- return lines;
231
+ let THEME_MODE = detectTerminalThemeMode();
232
+ let THEME = createTheme(THEME_MODE);
233
+
234
+ function applyThemeMode(mode) {
235
+ THEME_MODE = mode;
236
+ THEME = createTheme(mode);
237
+ }
238
+
239
+ function cycleThemeMode(mode) {
240
+ const ordered = ["dark", "light", "mono"];
241
+ const currentIndex = ordered.indexOf(mode);
242
+ return ordered[(currentIndex + 1 + ordered.length) % ordered.length];
243
+ }
244
+
245
+ function supportsAnsiStyling(stream = process.stdout) {
246
+ return Boolean(stream && stream.isTTY && process.env.NO_COLOR == null);
247
+ }
248
+
249
+ function paint(text, ...styles) {
250
+ if (!supportsAnsiStyling()) {
251
+ return String(text || "");
92
252
  }
253
+ return `${styles.join("")}${text}${THEME.reset}`;
254
+ }
93
255
 
94
- for (const item of items) {
95
- const signature = [item.flag, item.value].filter(Boolean).join(" ");
96
- const requirementLabel = item.required
97
- ? normalized === "zh"
98
- ? "必填"
99
- : "required"
100
- : normalized === "zh"
101
- ? "可选"
102
- : "optional";
103
- const body = `${signature} [${requirementLabel}]: ${resolveLocalizedText(item.description, normalized)}`;
104
- const wrapped = wrapText(body, Math.max(16, width - 4));
105
- if (wrapped.length === 0) {
106
- continue;
107
- }
108
- lines.push(`- ${wrapped[0]}`);
109
- for (const line of wrapped.slice(1)) {
110
- lines.push(` ${line}`);
111
- }
256
+ function stripAnsi(text) {
257
+ return String(text || "").replace(ANSI_PATTERN, "");
258
+ }
259
+
260
+ function visibleLength(text) {
261
+ return stripAnsi(text).length;
262
+ }
263
+
264
+ function truncatePlain(text, width) {
265
+ const source = String(text || "");
266
+ if (source.length <= width) {
267
+ return source;
268
+ }
269
+ if (width <= 1) {
270
+ return source.slice(0, width);
112
271
  }
272
+ return `${source.slice(0, Math.max(0, width - 1))}…`;
273
+ }
113
274
 
114
- if (items.some((item) => item.required)) {
115
- lines.push(
116
- normalized === "zh"
117
- ? "提示: 标记为必填的参数通常需要先按 m 编辑命令并补全占位符。"
118
- : "Hint: parameters marked required usually need you to press m and replace placeholders before running."
119
- );
275
+ function truncateMiddle(text, width) {
276
+ const source = String(text || "");
277
+ if (source.length <= width) {
278
+ return source;
120
279
  }
121
- return lines;
280
+ if (width <= 5) {
281
+ return truncatePlain(source, width);
282
+ }
283
+ const left = Math.ceil((width - 1) / 2);
284
+ const right = Math.max(1, width - left - 1);
285
+ return `${source.slice(0, left)}…${source.slice(source.length - right)}`;
122
286
  }
123
287
 
124
- function clearScreen() {
125
- process.stdout.write("\x1b[2J\x1b[H");
288
+ function escapeRegExp(value) {
289
+ return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
290
+ }
291
+
292
+ function padVisible(text, width) {
293
+ const source = String(text || "");
294
+ const plain = stripAnsi(source);
295
+ const fitted = plain.length > width ? truncatePlain(plain, width) : source;
296
+ const missing = Math.max(0, width - visibleLength(fitted));
297
+ return `${fitted}${" ".repeat(missing)}`;
126
298
  }
127
299
 
128
300
  function wrapText(text, width) {
@@ -156,82 +328,173 @@ function wrapText(text, width) {
156
328
  return lines;
157
329
  }
158
330
 
159
- function truncate(text, width) {
160
- const source = String(text || "");
161
- if (source.length <= width) {
162
- return source;
331
+ function centerPlain(text, width) {
332
+ const source = truncatePlain(String(text || ""), width);
333
+ const missing = Math.max(0, width - source.length);
334
+ const left = Math.floor(missing / 2);
335
+ const right = Math.max(0, missing - left);
336
+ return `${" ".repeat(left)}${source}${" ".repeat(right)}`;
337
+ }
338
+
339
+ function parsePositiveInteger(value) {
340
+ const parsed = Number.parseInt(String(value || "").trim(), 10);
341
+ if (!Number.isFinite(parsed) || parsed <= 0) {
342
+ return null;
163
343
  }
164
- if (width <= 1) {
165
- return source.slice(0, width);
344
+ return Math.floor(parsed);
345
+ }
346
+
347
+ function normalizeLayoutWidth(value) {
348
+ const parsed = parsePositiveInteger(value);
349
+ if (!parsed) {
350
+ return null;
166
351
  }
167
- return `${source.slice(0, Math.max(0, width - 1))}…`;
352
+ return Math.max(MIN_TUI_WIDTH, parsed);
168
353
  }
169
354
 
170
- function buildCatalogRows(lang) {
171
- const rows = [];
172
- for (const stage of STAGES) {
173
- rows.push({
174
- type: "stage",
175
- id: stage.id,
176
- label: resolveLocalizedText(stage.title, lang)
177
- });
178
- for (const command of COMMANDS.filter((item) => item.stageId === stage.id)) {
179
- rows.push({
180
- type: "command",
181
- id: command.id,
182
- label: resolveLocalizedText(command.title, lang),
183
- command
184
- });
185
- }
355
+ function resolveRenderWidth(state, viewport = {}) {
356
+ const ttyWidth = parsePositiveInteger(viewport.columns) || parsePositiveInteger(process.stdout.columns) || DEFAULT_TUI_WIDTH;
357
+ const explicit =
358
+ normalizeLayoutWidth(viewport.layoutWidth) ||
359
+ normalizeLayoutWidth(state && state.layoutWidth) ||
360
+ normalizeLayoutWidth(process.env.DA_VINCI_TUI_WIDTH) ||
361
+ normalizeLayoutWidth(process.env.DA_VINCI_TUI_LAYOUT_WIDTH);
362
+ const bounded = explicit ? Math.min(ttyWidth, explicit) : ttyWidth;
363
+ return Math.max(MIN_TUI_WIDTH, bounded);
364
+ }
365
+
366
+ function resolveRenderHeight(viewport = {}) {
367
+ const ttyHeight = parsePositiveInteger(viewport.rows) || parsePositiveInteger(process.stdout.rows) || DEFAULT_TUI_HEIGHT;
368
+ return Math.max(MIN_TUI_HEIGHT, ttyHeight);
369
+ }
370
+
371
+ function parseBooleanPreference(value) {
372
+ if (value === true || value === false) {
373
+ return value;
186
374
  }
187
- return rows;
375
+ const normalized = String(value || "").trim().toLowerCase();
376
+ if (["1", "true", "yes", "on"].includes(normalized)) {
377
+ return true;
378
+ }
379
+ if (["0", "false", "no", "off"].includes(normalized)) {
380
+ return false;
381
+ }
382
+ return null;
188
383
  }
189
384
 
190
- function countCommandIndex(rows, rowIndex) {
191
- let count = -1;
192
- for (let index = 0; index <= rowIndex; index += 1) {
193
- if (rows[index] && rows[index].type === "command") {
194
- count += 1;
195
- }
385
+ function resolveAltScreenMode(options = {}, preferences = {}) {
386
+ const explicit = parseBooleanPreference(options.altScreen);
387
+ if (explicit !== null) {
388
+ return explicit;
196
389
  }
197
- return count;
390
+ const persisted = parseBooleanPreference(preferences.altScreen);
391
+ if (persisted !== null) {
392
+ return persisted;
393
+ }
394
+ const fromEnv = parseBooleanPreference(process.env.DA_VINCI_TUI_ALT_SCREEN);
395
+ if (fromEnv !== null) {
396
+ return fromEnv;
397
+ }
398
+ return false;
198
399
  }
199
400
 
200
- function getRowIndexForCommand(rows, commandIndex) {
201
- let seen = -1;
202
- for (let index = 0; index < rows.length; index += 1) {
203
- if (rows[index].type === "command") {
204
- seen += 1;
205
- if (seen === commandIndex) {
206
- return index;
207
- }
208
- }
401
+ function createPanel(title, lines, width) {
402
+ const panelWidth = Math.max(24, width);
403
+ const innerWidth = panelWidth - 2;
404
+ const top = `${paint("", THEME.border)}${paint("═".repeat(innerWidth), THEME.border)}${paint("╗", THEME.border)}`;
405
+ const titleLine = title
406
+ ? `${paint("║", THEME.border)}${paint(centerPlain(title, innerWidth), THEME.accent, THEME.bold)}${paint("║", THEME.border)}`
407
+ : null;
408
+ const divider = title
409
+ ? `${paint("╠", THEME.border)}${paint("═".repeat(innerWidth), THEME.border)}${paint("╣", THEME.border)}`
410
+ : null;
411
+ const body = (lines || []).map((line) => `${paint("║", THEME.border)}${padVisible(line, innerWidth)}${paint("║", THEME.border)}`);
412
+ const bottom = `${paint("╚", THEME.border)}${paint("═".repeat(innerWidth), THEME.border)}${paint("╝", THEME.border)}`;
413
+ return [top, ...(titleLine ? [titleLine, divider] : []), ...body, bottom];
414
+ }
415
+
416
+ function enterAlternateScreen() {
417
+ process.stdout.write("\x1b[?1049h");
418
+ }
419
+
420
+ function exitAlternateScreen() {
421
+ process.stdout.write("\x1b[?1049l");
422
+ }
423
+
424
+ function hideCursor() {
425
+ process.stdout.write("\x1b[?25l");
426
+ }
427
+
428
+ function showCursor() {
429
+ process.stdout.write("\x1b[?25h");
430
+ }
431
+
432
+ function insetBlock(lines, screenWidth, blockWidth, options = {}) {
433
+ const align = options.align || "left";
434
+ const gutter = Number.isFinite(options.gutter) ? Math.max(0, options.gutter) : 0;
435
+ const left = align === "center" ? Math.max(0, Math.floor((screenWidth - blockWidth) / 2)) : Math.max(0, gutter);
436
+ const prefix = " ".repeat(left);
437
+ return (lines || []).map((line) => `${prefix}${line}`);
438
+ }
439
+
440
+ function createMenuLine(label, selected) {
441
+ if (selected) {
442
+ return `${paint("❯", THEME.green, THEME.bold)} ${paint(label, THEME.accent, THEME.bold)}`;
209
443
  }
210
- return 0;
444
+ return `${paint("›", THEME.soft)} ${paint(label, THEME.soft)}`;
211
445
  }
212
446
 
213
- function getVisibleRows(rows, selectedRowIndex, limit) {
214
- if (rows.length <= limit) {
215
- return {
216
- start: 0,
217
- items: rows
218
- };
447
+ function formatThemeLabel(mode, lang) {
448
+ if (lang === "zh") {
449
+ if (mode === "light") {
450
+ return "浅色";
451
+ }
452
+ if (mode === "mono") {
453
+ return "单色";
454
+ }
455
+ return "深色";
219
456
  }
220
- const commandWindowStart = Math.max(0, selectedRowIndex - Math.floor(limit / 2));
221
- const start = Math.min(commandWindowStart, Math.max(0, rows.length - limit));
222
- return {
223
- start,
224
- items: rows.slice(start, start + limit)
225
- };
457
+ return mode;
226
458
  }
227
459
 
228
- function buildPreviewCommand(state) {
229
- const command = getCommandById(state.selectedCommandId);
230
- return {
231
- command,
232
- args: buildCommandArgs(command, state),
233
- display: buildDisplayCommand(command, state)
234
- };
460
+ function formatTuiHelp(lang) {
461
+ const normalized = normalizeLanguage(lang || getDefaultLanguage());
462
+ return HELP_TEXT[normalized];
463
+ }
464
+
465
+ function formatCommandParameterHelp(command, lang, width) {
466
+ const normalized = normalizeLanguage(lang || getDefaultLanguage());
467
+ const items = getCommandParameterItems(command);
468
+ const lines = [];
469
+ lines.push(normalized === "zh" ? "命令参数 (h):" : "Command Parameters (h):");
470
+ if (items.length === 0) {
471
+ lines.push(
472
+ normalized === "zh"
473
+ ? "这条命令在 TUI 里没有额外可调参数,直接按预览执行即可。"
474
+ : "This command has no extra adjustable parameters in the TUI. Run it exactly as previewed."
475
+ );
476
+ return lines;
477
+ }
478
+ for (const item of items) {
479
+ const signature = [item.flag, item.value].filter(Boolean).join(" ");
480
+ const requirementLabel = item.required
481
+ ? normalized === "zh"
482
+ ? "必填"
483
+ : "required"
484
+ : normalized === "zh"
485
+ ? "可选"
486
+ : "optional";
487
+ const content = `${signature} [${requirementLabel}]: ${resolveLocalizedText(item.description, normalized)}`;
488
+ const wrapped = wrapText(content, Math.max(16, width - 4));
489
+ if (wrapped.length === 0) {
490
+ continue;
491
+ }
492
+ lines.push(`- ${wrapped[0]}`);
493
+ for (const line of wrapped.slice(1)) {
494
+ lines.push(` ${line}`);
495
+ }
496
+ }
497
+ return lines;
235
498
  }
236
499
 
237
500
  function normalizeEditedCommand(input) {
@@ -247,15 +510,9 @@ function normalizeEditedCommand(input) {
247
510
 
248
511
  function parseEditedCommandInput(input) {
249
512
  try {
250
- return {
251
- args: normalizeEditedCommand(input),
252
- error: null
253
- };
513
+ return { args: normalizeEditedCommand(input), error: null };
254
514
  } catch (error) {
255
- return {
256
- args: null,
257
- error
258
- };
515
+ return { args: null, error };
259
516
  }
260
517
  }
261
518
 
@@ -263,290 +520,1735 @@ function getExecutionCwd(projectPath) {
263
520
  return path.resolve(projectPath || process.cwd());
264
521
  }
265
522
 
266
- function teardownInput(input, onKeypress) {
267
- if (!input) {
523
+ function ensureDir(dirPath) {
524
+ fs.mkdirSync(dirPath, { recursive: true });
525
+ }
526
+
527
+ function loadJsonFile(filePath, fallback = null) {
528
+ try {
529
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
530
+ } catch (_error) {
531
+ return fallback;
532
+ }
533
+ }
534
+
535
+ function saveJsonFile(filePath, value) {
536
+ ensureDir(path.dirname(filePath));
537
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
538
+ }
539
+
540
+ function loadPreferences() {
541
+ return loadJsonFile(PREFERENCES_PATH, {});
542
+ }
543
+
544
+ function savePreferences(updates) {
545
+ const current = loadPreferences() || {};
546
+ saveJsonFile(PREFERENCES_PATH, { ...current, ...updates });
547
+ }
548
+
549
+ function formatDateToken(date = new Date()) {
550
+ const y = date.getFullYear();
551
+ const m = `${date.getMonth() + 1}`.padStart(2, "0");
552
+ const d = `${date.getDate()}`.padStart(2, "0");
553
+ return `${y}-${m}-${d}`;
554
+ }
555
+
556
+ function resolveDailyLogPath(projectPath, date = new Date()) {
557
+ return path.join(getExecutionCwd(projectPath), LOGS_DIR_RELATIVE, `${formatDateToken(date)}.ndjson`);
558
+ }
559
+
560
+ function appendLogEvent(state, scene, event, status = "ok", details = "") {
561
+ try {
562
+ const logPath = resolveDailyLogPath(state.projectPath);
563
+ ensureDir(path.dirname(logPath));
564
+ const payload = {
565
+ time: new Date().toISOString(),
566
+ scene,
567
+ event,
568
+ status,
569
+ project: getExecutionCwd(state.projectPath),
570
+ change: String(state.changeId || "auto"),
571
+ details: String(details || "")
572
+ };
573
+ fs.appendFileSync(logPath, `${JSON.stringify(payload)}\n`, "utf8");
574
+ } catch (_error) {
575
+ // Logging is diagnostic-only. Never block interactive flows.
576
+ }
577
+ }
578
+
579
+ function parseJsonEnvelope(output) {
580
+ const text = String(output || "").trim();
581
+ if (!text) {
582
+ return null;
583
+ }
584
+ const start = text.indexOf("{");
585
+ const end = text.lastIndexOf("}");
586
+ if (start < 0 || end < 0 || end <= start) {
587
+ return null;
588
+ }
589
+ try {
590
+ return JSON.parse(text.slice(start, end + 1));
591
+ } catch (_error) {
592
+ return null;
593
+ }
594
+ }
595
+
596
+ function resolveActiveChangeId(projectPath, requestedChangeId) {
597
+ const requested = String(requestedChangeId || "").trim();
598
+ if (requested) {
599
+ return requested;
600
+ }
601
+ const changesRoot = path.join(getExecutionCwd(projectPath), ".da-vinci", "changes");
602
+ if (!fs.existsSync(changesRoot)) {
603
+ return "";
604
+ }
605
+ let dirs = [];
606
+ try {
607
+ dirs = fs
608
+ .readdirSync(changesRoot, { withFileTypes: true })
609
+ .filter((entry) => entry.isDirectory())
610
+ .map((entry) => entry.name)
611
+ .sort();
612
+ } catch (_error) {
613
+ return "";
614
+ }
615
+ const nonEmpty = dirs.filter((name) => {
616
+ const full = path.join(changesRoot, name);
617
+ try {
618
+ return fs.readdirSync(full).length > 0;
619
+ } catch (_error) {
620
+ return false;
621
+ }
622
+ });
623
+ if (nonEmpty.length === 1) {
624
+ return nonEmpty[0];
625
+ }
626
+ return "";
627
+ }
628
+
629
+ function getSceneById(sceneId) {
630
+ return SCENES.find((scene) => scene.id === sceneId) || null;
631
+ }
632
+
633
+ function createHeaderLines(state, lang, width) {
634
+ const innerWidth = Math.max(12, width - 4);
635
+ const lines = [];
636
+
637
+ if (innerWidth >= BRAND_LOGO_MIN_INNER_WIDTH) {
638
+ lines.push(paint(centerPlain("██████╗ █████╗ ██╗ ██╗██╗███╗ ██╗ ██████╗██╗", innerWidth), THEME.textStrong, THEME.bold));
639
+ lines.push(paint(centerPlain("██╔══██╗██╔══██╗ ██║ ██║██║████╗ ██║██╔════╝██║", innerWidth), THEME.textStrong, THEME.bold));
640
+ lines.push(paint(centerPlain("██║ ██║███████║ ██║ ██║██║██╔██╗ ██║██║ ██║", innerWidth), THEME.textStrong, THEME.bold));
641
+ lines.push(paint(centerPlain("██████╔╝██║ ██║ ╚██████╔╝██║██║ ╚████║╚██████╗██║", innerWidth), THEME.textStrong, THEME.bold));
642
+ } else {
643
+ lines.push(paint(centerPlain("DA VINCI", innerWidth), THEME.textStrong, THEME.bold));
644
+ }
645
+ lines.push("");
646
+ lines.push(paint(centerPlain(`Da Vinci Workflow v${PACKAGE_VERSION}`, innerWidth), THEME.accent, THEME.bold));
647
+
648
+ const projectText = truncateMiddle(state.projectPath || process.cwd(), Math.max(16, innerWidth - 12));
649
+ const changeText = resolveHeaderChangeText(state);
650
+ lines.push(paint(`[Project ${projectText}]`, THEME.soft, THEME.bold));
651
+ lines.push(paint(`[Change ${changeText}]`, THEME.soft, THEME.bold));
652
+ return lines;
653
+ }
654
+
655
+ function resolveHeaderChangeText(state) {
656
+ const explicit = String(state && state.changeId ? state.changeId : "").trim();
657
+ if (explicit) {
658
+ return explicit;
659
+ }
660
+ const hasChanges = listExistingChangeIds(state && state.projectPath).length > 0;
661
+ if (!hasChanges) {
662
+ return "none";
663
+ }
664
+ return "unselected";
665
+ }
666
+
667
+ function getSceneActions(state, sceneId) {
668
+ const lang = normalizeLanguage(state.lang);
669
+ const zh = lang === "zh";
670
+
671
+ const sceneActions = {
672
+ "prompt-templates": [
673
+ {
674
+ id: "prompt-mobile",
675
+ label: zh ? "移动端模板" : "Mobile Templates",
676
+ summary: zh ? "选择 mode1-5 并预览/复制英文模板" : "Select mode1-5 and preview/copy canonical English template",
677
+ run: async () => {
678
+ await runPromptTemplateFlow(state, "mobile");
679
+ }
680
+ },
681
+ {
682
+ id: "prompt-desktop",
683
+ label: zh ? "桌面端模板" : "Desktop Templates",
684
+ summary: zh ? "选择 mode1-5 并预览/复制英文模板" : "Select mode1-5 and preview/copy canonical English template",
685
+ run: async () => {
686
+ await runPromptTemplateFlow(state, "desktop");
687
+ }
688
+ },
689
+ {
690
+ id: "prompt-tablet",
691
+ label: zh ? "平板模板" : "Tablet Templates",
692
+ summary: zh ? "选择 mode1-5 并预览/复制英文模板" : "Select mode1-5 and preview/copy canonical English template",
693
+ run: async () => {
694
+ await runPromptTemplateFlow(state, "tablet");
695
+ }
696
+ },
697
+ {
698
+ id: "prompt-web",
699
+ label: zh ? "Web 模板" : "Web Templates",
700
+ summary: zh ? "选择 mode1-5 并预览/复制英文模板" : "Select mode1-5 and preview/copy canonical English template",
701
+ run: async () => {
702
+ await runPromptTemplateFlow(state, "web");
703
+ }
704
+ }
705
+ ],
706
+ "design-work": [
707
+ {
708
+ id: "design-icon-sync",
709
+ label: zh ? "同步图标库" : "Sync Icon Library",
710
+ summary: zh ? "运行 icon-sync" : "Run icon-sync",
711
+ run: async () => {
712
+ await runCommandById(state, "icon-sync", {
713
+ sceneId: "design-work",
714
+ eventName: "icon_sync",
715
+ loadingView: {
716
+ titleZh: "正在同步图标库",
717
+ titleEn: "Syncing Icon Library",
718
+ lines: [
719
+ zh
720
+ ? "该操作需要联网拉取多个图标源,通常比普通命令更慢。"
721
+ : "This operation fetches multiple icon sources over the network and can take longer than usual commands.",
722
+ zh
723
+ ? "完成后会自动进入结果页。"
724
+ : "The result page will appear automatically when sync finishes."
725
+ ]
726
+ }
727
+ });
728
+ }
729
+ },
730
+ {
731
+ id: "design-save-current",
732
+ label: zh ? "保存当前设计稿" : "Save Current Design",
733
+ summary: zh ? "把当前 live 设计回写到项目 .pen" : "Persist the current live design back to project .pen",
734
+ run: async () => {
735
+ await runCommandById(state, "save-current-design", {
736
+ sceneId: "design-work",
737
+ eventName: "save_current_design",
738
+ loadingView: {
739
+ titleZh: "正在保存当前设计稿",
740
+ titleEn: "Saving Current Design",
741
+ lines: [
742
+ zh
743
+ ? "正在从 live 设计会话抓取最新快照并写回项目 .pen 文件。"
744
+ : "Capturing the latest snapshot from the live design session and writing it back to the project .pen file.",
745
+ zh
746
+ ? "完成后会自动显示保存结果(成功或失败)。"
747
+ : "A final result page (success or failure) will be shown automatically."
748
+ ]
749
+ }
750
+ });
751
+ }
752
+ },
753
+ {
754
+ id: "design-quality",
755
+ label: zh ? "Visual Assist 预设" : "Visual Assist Preset",
756
+ summary: zh ? "先选平台再选3档质量,自动写入英文 Visual Assist" : "Pick platform then one of 3 quality levels and auto-write canonical English Visual Assist",
757
+ run: async () => {
758
+ await runDesignQualityFlow(state);
759
+ }
760
+ }
761
+ ],
762
+ "install-uninstall": [
763
+ {
764
+ id: "platform-status",
765
+ label: zh ? "状态" : "Status",
766
+ summary: zh ? "查看 da-vinci 当前安装状态" : "Show current da-vinci installation status",
767
+ run: async () => {
768
+ await runCommandById(state, "status", {
769
+ sceneId: "install-uninstall",
770
+ eventName: "install_uninstall_status"
771
+ });
772
+ }
773
+ },
774
+ {
775
+ id: "platform-install",
776
+ label: zh ? "安装" : "Install",
777
+ summary: zh ? "一键安装所有平台(Codex/Claude Code/Gemini)" : "Install all platforms in one step (Codex/Claude Code/Gemini)",
778
+ run: async () => {
779
+ await runPlatformLifecycleFlow(state, "install");
780
+ }
781
+ },
782
+ {
783
+ id: "platform-uninstall",
784
+ label: zh ? "卸载" : "Uninstall",
785
+ summary: zh ? "一键卸载所有平台(Codex/Claude Code/Gemini)" : "Uninstall all platforms in one step (Codex/Claude Code/Gemini)",
786
+ run: async () => {
787
+ await runPlatformLifecycleFlow(state, "uninstall");
788
+ }
789
+ }
790
+ ],
791
+ more: [
792
+ {
793
+ id: "more-language",
794
+ label: zh ? "语言" : "Language",
795
+ summary: zh ? "设置并持久化 TUI 语言" : "Set and persist TUI language",
796
+ run: async () => {
797
+ await chooseLanguage(state);
798
+ }
799
+ },
800
+ {
801
+ id: "more-logs",
802
+ label: zh ? "日志" : "Logs",
803
+ summary: zh ? "按天查看并复制项目日志" : "View and copy daily project logs",
804
+ run: async () => {
805
+ await showLogsFlow(state);
806
+ }
807
+ }
808
+ ]
809
+ };
810
+
811
+ return sceneActions[sceneId] || [];
812
+ }
813
+
814
+ function getSceneActionIndex(state) {
815
+ const key = state.activeSceneId;
816
+ const value = state.sceneSelectionById[key];
817
+ const actions = getSceneActions(state, state.activeSceneId);
818
+ const menuSize = actions.length + 1;
819
+ if (typeof value === "number" && Number.isFinite(value)) {
820
+ if (menuSize <= 0) {
821
+ return 0;
822
+ }
823
+ return ((value % menuSize) + menuSize) % menuSize;
824
+ }
825
+ return 0;
826
+ }
827
+
828
+ function setSceneActionIndex(state, value) {
829
+ const actions = getSceneActions(state, state.activeSceneId);
830
+ const menuSize = actions.length + 1;
831
+ if (menuSize <= 0) {
832
+ state.sceneSelectionById[state.activeSceneId] = 0;
833
+ return;
834
+ }
835
+ const next = (value + menuSize) % menuSize;
836
+ state.sceneSelectionById[state.activeSceneId] = next;
837
+ }
838
+
839
+ function buildSceneMenuLines(state, lang, width) {
840
+ const lines = [];
841
+ const menuSize = SCENES.length + 1;
842
+ const selectedIndex =
843
+ Number.isFinite(state.rootSelectionIndex) && menuSize > 0
844
+ ? ((state.rootSelectionIndex % menuSize) + menuSize) % menuSize
845
+ : 0;
846
+ for (let index = 0; index < SCENES.length; index += 1) {
847
+ const scene = SCENES[index];
848
+ const selected = selectedIndex === index;
849
+ lines.push(createMenuLine(truncatePlain(resolveLocalizedText(scene.title, lang), Math.max(12, width - 4)), selected));
850
+ }
851
+ lines.push("");
852
+ lines.push(createMenuLine(lang === "zh" ? "退出" : "Exit", selectedIndex === SCENES.length));
853
+ return lines;
854
+ }
855
+
856
+ function buildSceneSummaryLines(state, lang, width) {
857
+ if (state.rootSelectionIndex === SCENES.length) {
858
+ const exitTitle = lang === "zh" ? "退出" : "Exit";
859
+ const exitSummary =
860
+ lang === "zh"
861
+ ? "结束当前 TUI 会话并返回终端。"
862
+ : "Close the current TUI session and return to the terminal.";
863
+ return [paint(exitTitle, THEME.accent, THEME.bold), paint(exitSummary, THEME.soft)];
864
+ }
865
+ const scene = getSceneById(state.activeSceneId) || SCENES[0];
866
+ const lines = [paint(resolveLocalizedText(scene.title, lang), THEME.accent, THEME.bold)];
867
+ for (const line of wrapText(resolveLocalizedText(scene.summary, lang), width)) {
868
+ lines.push(paint(line, THEME.soft));
869
+ }
870
+ return lines;
871
+ }
872
+
873
+ function buildSceneActionsLines(state, lang, width) {
874
+ const actions = getSceneActions(state, state.activeSceneId);
875
+ const selectedIndex = getSceneActionIndex(state);
876
+ const lines = [];
877
+ for (let index = 0; index < actions.length; index += 1) {
878
+ const action = actions[index];
879
+ lines.push(createMenuLine(truncatePlain(action.label, Math.max(12, width - 4)), index === selectedIndex));
880
+ }
881
+ lines.push("");
882
+ lines.push(createMenuLine(lang === "zh" ? "上一步" : "Back", selectedIndex === actions.length));
883
+ return lines;
884
+ }
885
+
886
+ function buildSceneActionSummaryLines(state, lang, width) {
887
+ const actions = getSceneActions(state, state.activeSceneId);
888
+ const selectedIndex = getSceneActionIndex(state);
889
+ if (selectedIndex >= actions.length) {
890
+ return [
891
+ paint(lang === "zh" ? "上一步" : "Back", THEME.accent, THEME.bold),
892
+ paint(
893
+ lang === "zh"
894
+ ? "返回场景选择列表。"
895
+ : "Return to the workflow scene list.",
896
+ THEME.soft
897
+ )
898
+ ];
899
+ }
900
+ const selected = actions[selectedIndex] || null;
901
+ const lines = [];
902
+ if (!selected) {
903
+ lines.push(paint(lang === "zh" ? "当前场景没有可执行动作。" : "No actions configured for this scene.", THEME.soft));
904
+ return lines;
905
+ }
906
+ lines.push(paint(selected.label, THEME.textStrong, THEME.bold));
907
+ for (const line of wrapText(selected.summary, width)) {
908
+ lines.push(paint(line, THEME.soft));
909
+ }
910
+ return lines;
911
+ }
912
+
913
+ function getWorkItemSelectionIndex(state) {
914
+ const choices = Array.isArray(state.workItemChoices) ? state.workItemChoices : [];
915
+ const menuSize = choices.length + 1;
916
+ const value = Number.isFinite(state.workItemSelectionIndex) ? state.workItemSelectionIndex : 0;
917
+ if (menuSize <= 0) {
918
+ return 0;
919
+ }
920
+ return ((value % menuSize) + menuSize) % menuSize;
921
+ }
922
+
923
+ function setWorkItemSelectionIndex(state, value) {
924
+ const choices = Array.isArray(state.workItemChoices) ? state.workItemChoices : [];
925
+ const menuSize = choices.length + 1;
926
+ if (menuSize <= 0) {
927
+ state.workItemSelectionIndex = 0;
928
+ return;
929
+ }
930
+ state.workItemSelectionIndex = ((value % menuSize) + menuSize) % menuSize;
931
+ }
932
+
933
+ function buildWorkItemSwitchLines(state, lang, width) {
934
+ const choices = Array.isArray(state.workItemChoices) ? state.workItemChoices : [];
935
+ const selectedIndex = getWorkItemSelectionIndex(state);
936
+ const lines = [];
937
+ for (let index = 0; index < choices.length; index += 1) {
938
+ const label = choices[index] === state.changeId
939
+ ? `${choices[index]} ${lang === "zh" ? "(当前)" : "(current)"}`
940
+ : choices[index];
941
+ lines.push(createMenuLine(truncatePlain(label, Math.max(12, width - 4)), index === selectedIndex));
942
+ }
943
+ lines.push("");
944
+ lines.push(createMenuLine(lang === "zh" ? "上一步" : "Back", selectedIndex === choices.length));
945
+ return lines;
946
+ }
947
+
948
+ function buildWorkItemSwitchSummaryLines(state, lang, width) {
949
+ const choices = Array.isArray(state.workItemChoices) ? state.workItemChoices : [];
950
+ const selectedIndex = getWorkItemSelectionIndex(state);
951
+ if (selectedIndex >= choices.length) {
952
+ return [
953
+ paint(lang === "zh" ? "上一步" : "Back", THEME.accent, THEME.bold),
954
+ paint(
955
+ lang === "zh"
956
+ ? "返回场景列表。"
957
+ : "Return to the scene list.",
958
+ THEME.soft
959
+ )
960
+ ];
961
+ }
962
+ const selected = choices[selectedIndex] || "";
963
+ const lines = [
964
+ paint(lang === "zh" ? "切换到工作项" : "Switch To Work Item", THEME.accent, THEME.bold),
965
+ paint(selected, THEME.textStrong, THEME.bold)
966
+ ];
967
+ for (const line of wrapText(
968
+ lang === "zh"
969
+ ? "按 Enter 将当前上下文切换到这个 change。"
970
+ : "Press Enter to switch the active context to this change.",
971
+ width
972
+ )) {
973
+ lines.push(paint(line, THEME.soft));
974
+ }
975
+ return lines;
976
+ }
977
+
978
+ function buildScreenLines(state, viewport = {}) {
979
+ const lang = normalizeLanguage(state.lang);
980
+ const width = resolveRenderWidth(state, viewport);
981
+ const height = resolveRenderHeight(viewport);
982
+ const horizontalGutter = width >= 120 ? 3 : width >= 84 ? 2 : 1;
983
+ const contentWidth = Math.max(24, width - horizontalGutter * 2);
984
+ const headerWidth = contentWidth;
985
+ const compactHeight = height < 32;
986
+ const lines = [];
987
+
988
+ lines.push(
989
+ ...insetBlock(
990
+ createPanel("", createHeaderLines(state, lang, headerWidth), headerWidth),
991
+ width,
992
+ headerWidth,
993
+ { align: "left", gutter: horizontalGutter }
994
+ )
995
+ );
996
+ lines.push("");
997
+
998
+ if (state.viewMode === "scene_list") {
999
+ lines.push(
1000
+ ...insetBlock(
1001
+ buildSceneMenuLines(state, lang, contentWidth - 4),
1002
+ width,
1003
+ contentWidth,
1004
+ { align: "left", gutter: horizontalGutter }
1005
+ )
1006
+ );
1007
+ if (!compactHeight) {
1008
+ lines.push("");
1009
+ lines.push(
1010
+ ...insetBlock(
1011
+ createPanel(lang === "zh" ? "场景说明" : "Scene Summary", buildSceneSummaryLines(state, lang, contentWidth - 4), contentWidth),
1012
+ width,
1013
+ contentWidth,
1014
+ { align: "left", gutter: horizontalGutter }
1015
+ )
1016
+ );
1017
+ }
1018
+ } else if (state.viewMode === "scene_actions") {
1019
+ lines.push(
1020
+ ...insetBlock(
1021
+ buildSceneActionsLines(state, lang, contentWidth - 4),
1022
+ width,
1023
+ contentWidth,
1024
+ { align: "left", gutter: horizontalGutter }
1025
+ )
1026
+ );
1027
+ if (!compactHeight) {
1028
+ lines.push("");
1029
+ lines.push(
1030
+ ...insetBlock(
1031
+ createPanel(lang === "zh" ? "动作说明" : "Action Summary", buildSceneActionSummaryLines(state, lang, contentWidth - 4), contentWidth),
1032
+ width,
1033
+ contentWidth,
1034
+ { align: "left", gutter: horizontalGutter }
1035
+ )
1036
+ );
1037
+ }
1038
+ } else if (state.viewMode === "work_item_switch") {
1039
+ lines.push(
1040
+ ...insetBlock(
1041
+ buildWorkItemSwitchLines(state, lang, contentWidth - 4),
1042
+ width,
1043
+ contentWidth,
1044
+ { align: "left", gutter: horizontalGutter }
1045
+ )
1046
+ );
1047
+ if (!compactHeight) {
1048
+ lines.push("");
1049
+ lines.push(
1050
+ ...insetBlock(
1051
+ createPanel(
1052
+ lang === "zh" ? "工作项说明" : "Work Item Summary",
1053
+ buildWorkItemSwitchSummaryLines(state, lang, contentWidth - 4),
1054
+ contentWidth
1055
+ ),
1056
+ width,
1057
+ contentWidth,
1058
+ { align: "left", gutter: horizontalGutter }
1059
+ )
1060
+ );
1061
+ }
1062
+ } else {
1063
+ lines.push(
1064
+ ...insetBlock(
1065
+ buildSceneMenuLines(state, lang, contentWidth - 4),
1066
+ width,
1067
+ contentWidth,
1068
+ { align: "left", gutter: horizontalGutter }
1069
+ )
1070
+ );
1071
+ if (!compactHeight) {
1072
+ lines.push("");
1073
+ lines.push(
1074
+ ...insetBlock(
1075
+ createPanel(lang === "zh" ? "场景说明" : "Scene Summary", buildSceneSummaryLines(state, lang, contentWidth - 4), contentWidth),
1076
+ width,
1077
+ contentWidth,
1078
+ { align: "left", gutter: horizontalGutter }
1079
+ )
1080
+ );
1081
+ }
1082
+ }
1083
+
1084
+ if (state.helpVisible) {
1085
+ lines.push("");
1086
+ lines.push(
1087
+ ...insetBlock(
1088
+ createPanel(lang === "zh" ? "帮助" : "Help", formatTuiHelp(lang).split("\n").slice(1), contentWidth),
1089
+ width,
1090
+ contentWidth,
1091
+ { align: "left", gutter: horizontalGutter }
1092
+ )
1093
+ );
1094
+ }
1095
+
1096
+ if (lines.length <= height) {
1097
+ return lines;
1098
+ }
1099
+ const clipped = lines.slice(0, Math.max(1, height - 1));
1100
+ clipped.push(paint("[output clipped to viewport height]", THEME.muted));
1101
+ return clipped;
1102
+ }
1103
+
1104
+ function clearScreen() {
1105
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
1106
+ }
1107
+
1108
+ function renderState(state) {
1109
+ clearScreen();
1110
+ process.stdout.write(buildScreenLines(state).join("\n"));
1111
+ }
1112
+
1113
+ function teardownInput(input, onKeypress) {
1114
+ if (!input) {
1115
+ return;
1116
+ }
1117
+ if (typeof input.off === "function" && onKeypress) {
1118
+ input.off("keypress", onKeypress);
1119
+ }
1120
+ if (input.isTTY && typeof input.setRawMode === "function") {
1121
+ input.setRawMode(false);
1122
+ }
1123
+ if (typeof input.pause === "function") {
1124
+ input.pause();
1125
+ }
1126
+ }
1127
+
1128
+ function getKeyAction(str, key = {}) {
1129
+ if (key.ctrl && key.name === "c") {
1130
+ return "quit";
1131
+ }
1132
+ if (key.name === "up" || (key.name === "k" && key.shift !== true)) {
1133
+ return "move_up";
1134
+ }
1135
+ if (key.name === "down" || (key.name === "j" && key.shift !== true)) {
1136
+ return "move_down";
1137
+ }
1138
+ if (key.name === "l") {
1139
+ return "toggle_lang";
1140
+ }
1141
+ if (key.name === "t") {
1142
+ return "toggle_theme";
1143
+ }
1144
+ if (key.name === "s") {
1145
+ return "toggle_strict";
1146
+ }
1147
+ if ((key.name === "j" && key.shift === true) || str === "J") {
1148
+ return "toggle_json";
1149
+ }
1150
+ if (key.name === "e") {
1151
+ return "toggle_continue_on_error";
1152
+ }
1153
+ if (key.name === "p") {
1154
+ return "set_project";
1155
+ }
1156
+ if (key.name === "c") {
1157
+ return "set_change";
1158
+ }
1159
+ if (key.name === "return" || key.name === "r") {
1160
+ return "run";
1161
+ }
1162
+ if (str === "?") {
1163
+ return "toggle_help";
1164
+ }
1165
+ return null;
1166
+ }
1167
+
1168
+ function waitForAnyKey() {
1169
+ return new Promise((resolve) => {
1170
+ const handler = () => {
1171
+ process.stdin.off("keypress", handler);
1172
+ resolve();
1173
+ };
1174
+ process.stdin.on("keypress", handler);
1175
+ });
1176
+ }
1177
+
1178
+ async function promptInput(state, label, currentValue, options = {}) {
1179
+ state.rawModeEnabled = false;
1180
+ if (process.stdin.isTTY) {
1181
+ process.stdin.setRawMode(false);
1182
+ }
1183
+ process.stdin.pause();
1184
+ const rl = readline.createInterface({
1185
+ input: process.stdin,
1186
+ output: process.stdout
1187
+ });
1188
+ const promptLabel = currentValue ? `${label} [${currentValue}]: ` : `${label}: `;
1189
+ const answer = await new Promise((resolve) => {
1190
+ rl.question(promptLabel, (value) => resolve(value));
1191
+ });
1192
+ rl.close();
1193
+ process.stdin.resume();
1194
+ if (process.stdin.isTTY) {
1195
+ process.stdin.setRawMode(true);
1196
+ }
1197
+ state.rawModeEnabled = true;
1198
+ const normalized = String(answer || "").trim();
1199
+ if (!normalized && options.allowBlank) {
1200
+ return "";
1201
+ }
1202
+ return normalized || String(currentValue || "").trim();
1203
+ }
1204
+
1205
+ function resolveOptionLabel(option, lang) {
1206
+ const label = option && option.label != null ? option.label : option;
1207
+ if (typeof label === "string") {
1208
+ return label;
1209
+ }
1210
+ return resolveLocalizedText(label, lang);
1211
+ }
1212
+
1213
+ function resolveOptionSummary(option, lang) {
1214
+ if (!option || option.summary == null) {
1215
+ return "";
1216
+ }
1217
+ if (typeof option.summary === "string") {
1218
+ return option.summary;
1219
+ }
1220
+ return resolveLocalizedText(option.summary, lang);
1221
+ }
1222
+
1223
+ function clampOptionIndex(index, size) {
1224
+ if (!Number.isFinite(index)) {
1225
+ return 0;
1226
+ }
1227
+ if (size <= 0) {
1228
+ return 0;
1229
+ }
1230
+ const normalized = index % size;
1231
+ return normalized < 0 ? normalized + size : normalized;
1232
+ }
1233
+
1234
+ function renderPromptHeader(state, lang) {
1235
+ const width = resolveRenderWidth(state, {});
1236
+ const horizontalGutter = width >= 120 ? 3 : width >= 84 ? 2 : 1;
1237
+ const contentWidth = Math.max(24, width - horizontalGutter * 2);
1238
+ const headerLines = insetBlock(
1239
+ createPanel("", createHeaderLines(state, lang, contentWidth), contentWidth),
1240
+ width,
1241
+ contentWidth,
1242
+ { align: "left", gutter: horizontalGutter }
1243
+ );
1244
+ return `${headerLines.join("\n")}\n\n`;
1245
+ }
1246
+
1247
+ async function promptSelection(state, options = {}) {
1248
+ const lang = normalizeLanguage(state.lang);
1249
+ const zh = lang === "zh";
1250
+ const entries = Array.isArray(options.items) ? options.items.filter(Boolean) : [];
1251
+ if (entries.length === 0) {
1252
+ return null;
1253
+ }
1254
+
1255
+ const includeBack = options.includeBack === true;
1256
+ const backLabel = options.backLabel || (zh ? "上一步" : "Back");
1257
+ const showSummary = options.showSummary !== false;
1258
+ const showFooter = options.showFooter !== false;
1259
+ const menuSize = entries.length + (includeBack ? 1 : 0);
1260
+ let selectedIndex = clampOptionIndex(options.initialIndex, menuSize);
1261
+ const title = options.title || (zh ? "请选择" : "Select");
1262
+ const subtitle = options.subtitle || "";
1263
+ const footer = options.footer || (zh ? "Enter 确认,Esc 返回" : "Enter to confirm, Esc to go back");
1264
+
1265
+ const render = () => {
1266
+ clearScreen();
1267
+ process.stdout.write(renderPromptHeader(state, lang));
1268
+ process.stdout.write(`${title}\n\n`);
1269
+ if (subtitle) {
1270
+ process.stdout.write(`${subtitle}\n\n`);
1271
+ }
1272
+ for (let index = 0; index < entries.length; index += 1) {
1273
+ process.stdout.write(`${createMenuLine(resolveOptionLabel(entries[index], lang), index === selectedIndex)}\n`);
1274
+ }
1275
+ if (includeBack) {
1276
+ process.stdout.write("\n");
1277
+ process.stdout.write(`${createMenuLine(backLabel, selectedIndex === entries.length)}\n`);
1278
+ }
1279
+ if (showSummary && selectedIndex < entries.length) {
1280
+ const selected = entries[selectedIndex];
1281
+ const summary = resolveOptionSummary(selected, lang);
1282
+ if (summary) {
1283
+ process.stdout.write(`\n${summary}\n`);
1284
+ }
1285
+ }
1286
+ if (showFooter) {
1287
+ process.stdout.write(`\n${footer}\n`);
1288
+ }
1289
+ };
1290
+
1291
+ return await new Promise((resolve, reject) => {
1292
+ const cleanup = () => {
1293
+ process.stdin.off("keypress", onKeypress);
1294
+ };
1295
+
1296
+ const onKeypress = (str, key = {}) => {
1297
+ if (key.ctrl && key.name === "c") {
1298
+ cleanup();
1299
+ reject(new Error("Interrupted"));
1300
+ return;
1301
+ }
1302
+ if (key.name === "up" || (key.name === "k" && key.shift !== true)) {
1303
+ selectedIndex = (selectedIndex - 1 + menuSize) % menuSize;
1304
+ render();
1305
+ return;
1306
+ }
1307
+ if (key.name === "down" || (key.name === "j" && key.shift !== true)) {
1308
+ selectedIndex = (selectedIndex + 1) % menuSize;
1309
+ render();
1310
+ return;
1311
+ }
1312
+ if (key.name === "return" || key.name === "r") {
1313
+ cleanup();
1314
+ if (includeBack && selectedIndex === entries.length) {
1315
+ resolve(null);
1316
+ return;
1317
+ }
1318
+ resolve(entries[selectedIndex]);
1319
+ return;
1320
+ }
1321
+ if (key.name === "escape" || key.name === "b" || key.name === "q") {
1322
+ cleanup();
1323
+ resolve(null);
1324
+ }
1325
+ };
1326
+
1327
+ process.stdin.on("keypress", onKeypress);
1328
+ render();
1329
+ });
1330
+ }
1331
+
1332
+ function commandArgsWithOverrides(command, state, options = {}) {
1333
+ let args = buildCommandArgs(command, state);
1334
+ if (Array.isArray(options.appendArgs) && options.appendArgs.length > 0) {
1335
+ args = args.concat(options.appendArgs);
1336
+ }
1337
+ return args;
1338
+ }
1339
+
1340
+ function executeDaVinci(state, args) {
1341
+ return spawnSync(process.execPath, [DA_VINCI_BIN, ...args], {
1342
+ cwd: getExecutionCwd(state.projectPath),
1343
+ encoding: "utf8",
1344
+ maxBuffer: 16 * 1024 * 1024
1345
+ });
1346
+ }
1347
+
1348
+ function renderExecutionReport(lang, title, lines) {
1349
+ clearScreen();
1350
+ process.stdout.write(`${title}\n\n`);
1351
+ for (const line of lines) {
1352
+ process.stdout.write(`${line}\n`);
1353
+ }
1354
+ process.stdout.write(`\n${lang === "zh" ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`);
1355
+ }
1356
+
1357
+ function renderExecutionLoading(lang, title, lines = []) {
1358
+ clearScreen();
1359
+ process.stdout.write(`⏳ ${title}\n\n`);
1360
+ for (const line of lines) {
1361
+ process.stdout.write(`${line}\n`);
1362
+ }
1363
+ process.stdout.write(
1364
+ `\n${lang === "zh" ? "命令执行中,请稍候..." : "Command is running, please wait..."}\n`
1365
+ );
1366
+ }
1367
+
1368
+ async function runCommandById(state, commandId, options = {}) {
1369
+ const lang = normalizeLanguage(state.lang);
1370
+ const command = getCommandById(commandId);
1371
+ if (!command) {
1372
+ state.lastStatus = `${commandId} -> missing command`;
1373
+ return;
1374
+ }
1375
+
1376
+ let args = Array.isArray(options.overrideArgs) && options.overrideArgs.length > 0
1377
+ ? options.overrideArgs.slice()
1378
+ : commandArgsWithOverrides(command, state, options);
1379
+ let display = ["da-vinci", ...args].join(" ");
1380
+
1381
+ if (options.forceEdit || hasPlaceholders(args)) {
1382
+ renderState(state);
1383
+ const edited = await promptInput(
1384
+ state,
1385
+ lang === "zh" ? "编辑命令" : "Edit command",
1386
+ display
1387
+ );
1388
+ if (!edited) {
1389
+ state.lastStatus = `${commandId} -> skipped`;
1390
+ return;
1391
+ }
1392
+ const parsed = parseEditedCommandInput(edited);
1393
+ if (parsed.error) {
1394
+ clearScreen();
1395
+ process.stdout.write(
1396
+ `${lang === "zh" ? "命令编辑失败" : "Command edit failed"}\n\n${parsed.error.message || String(parsed.error)}\n\n`
1397
+ );
1398
+ process.stdout.write(`${lang === "zh" ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`);
1399
+ state.lastStatus = `${commandId} -> edit error`;
1400
+ await waitForAnyKey();
1401
+ return;
1402
+ }
1403
+ if (!parsed.args || parsed.args.length === 0) {
1404
+ state.lastStatus = `${commandId} -> skipped`;
1405
+ return;
1406
+ }
1407
+ args = parsed.args;
1408
+ display = ["da-vinci", ...args].join(" ");
1409
+ }
1410
+
1411
+ if (options.loadingView) {
1412
+ const loadingTitle =
1413
+ (lang === "zh" ? options.loadingView.titleZh : options.loadingView.titleEn) ||
1414
+ (lang === "zh" ? "正在执行命令" : "Running Command");
1415
+ const loadingLines = Array.isArray(options.loadingView.lines)
1416
+ ? options.loadingView.lines.filter((line) => String(line || "").trim().length > 0)
1417
+ : [];
1418
+ renderExecutionLoading(lang, loadingTitle, loadingLines);
1419
+ }
1420
+
1421
+ appendLogEvent(state, options.sceneId || "advanced", options.eventName || commandId, "start", display);
1422
+ const result = executeDaVinci(state, args);
1423
+ const stdout = String(result.stdout || "").trim();
1424
+ const stderr = String(result.stderr || "").trim();
1425
+ const merged = [stdout, stderr].filter(Boolean);
1426
+ const statusCode = result.status == null ? "null" : String(result.status);
1427
+ const success = result.status === 0;
1428
+ const reportLines = [
1429
+ lang === "zh" ? `结果: ${success ? "成功" : "失败"}` : `Result: ${success ? "PASS" : "FAIL"}`,
1430
+ `${lang === "zh" ? "退出码" : "Exit code"}: ${statusCode}`,
1431
+ "",
1432
+ ...(merged.length > 0 ? merged : [lang === "zh" ? "没有输出。" : "No output."])
1433
+ ];
1434
+
1435
+ renderExecutionReport(
1436
+ lang,
1437
+ `${lang === "zh" ? "执行结果" : "Execution Result"}: ${display}`,
1438
+ reportLines
1439
+ );
1440
+ await waitForAnyKey();
1441
+
1442
+ state.lastStatus = `${display} -> ${statusCode}`;
1443
+ appendLogEvent(
1444
+ state,
1445
+ options.sceneId || "advanced",
1446
+ options.eventName || commandId,
1447
+ result.status === 0 ? "ok" : "error",
1448
+ `exit=${statusCode}`
1449
+ );
1450
+
1451
+ return {
1452
+ status: result.status,
1453
+ stdout,
1454
+ stderr,
1455
+ args,
1456
+ display,
1457
+ json: parseJsonEnvelope(stdout)
1458
+ };
1459
+ }
1460
+
1461
+ async function runCommandChain(state, sceneId, chain) {
1462
+ const lang = normalizeLanguage(state.lang);
1463
+ const lines = [];
1464
+ let finalStatus = 0;
1465
+
1466
+ for (const step of chain) {
1467
+ const label = step.label;
1468
+ const command = getCommandById(step.commandId);
1469
+ if (!command) {
1470
+ lines.push(`- ${label}: missing command ${step.commandId}`);
1471
+ finalStatus = 1;
1472
+ if (step.stopOnFailure) {
1473
+ break;
1474
+ }
1475
+ continue;
1476
+ }
1477
+
1478
+ const args = commandArgsWithOverrides(command, state, step.options || {});
1479
+ const display = ["da-vinci", ...args].join(" ");
1480
+ appendLogEvent(state, sceneId, step.eventName || step.commandId, "start", display);
1481
+ const result = executeDaVinci(state, args);
1482
+ const statusCode = result.status == null ? "null" : String(result.status);
1483
+ const statusText = result.status === 0 ? "PASS" : "FAIL";
1484
+ lines.push(`- ${label}: ${statusText} (${display})`);
1485
+
1486
+ const stdout = String(result.stdout || "").trim();
1487
+ const stderr = String(result.stderr || "").trim();
1488
+ if (stdout) {
1489
+ for (const line of wrapText(stdout, 100).slice(0, 6)) {
1490
+ lines.push(` ${line}`);
1491
+ }
1492
+ }
1493
+ if (stderr) {
1494
+ for (const line of wrapText(stderr, 100).slice(0, 4)) {
1495
+ lines.push(` ${line}`);
1496
+ }
1497
+ }
1498
+
1499
+ appendLogEvent(state, sceneId, step.eventName || step.commandId, result.status === 0 ? "ok" : "error", `exit=${statusCode}`);
1500
+ if (result.status !== 0) {
1501
+ finalStatus = result.status || 1;
1502
+ if (step.stopOnFailure) {
1503
+ break;
1504
+ }
1505
+ }
1506
+ }
1507
+
1508
+ renderExecutionReport(
1509
+ lang,
1510
+ lang === "zh" ? "组合动作执行结果" : "Compound action execution report",
1511
+ lines
1512
+ );
1513
+ await waitForAnyKey();
1514
+ state.lastStatus = `${sceneId} chain -> ${finalStatus === 0 ? "PASS" : "FAIL"}`;
1515
+ }
1516
+
1517
+ function normalizeHeadingToken(text) {
1518
+ return String(text || "")
1519
+ .toLowerCase()
1520
+ .replace(/[^a-z0-9]+/g, " ")
1521
+ .trim();
1522
+ }
1523
+
1524
+ function findLevelTwoSectionRange(markdown, heading) {
1525
+ const text = String(markdown || "").replace(/\r\n?/g, "\n");
1526
+ const headingPattern = new RegExp(`^##\\s+${escapeRegExp(heading)}\\b.*$`, "m");
1527
+ const headingMatch = headingPattern.exec(text);
1528
+ if (!headingMatch || headingMatch.index == null) {
1529
+ return null;
1530
+ }
1531
+
1532
+ const start = headingMatch.index;
1533
+ const bodyStart = start + headingMatch[0].length;
1534
+ const remainder = text.slice(bodyStart);
1535
+ const nextHeadingMatch = /\n(?=##\s+)/.exec(remainder);
1536
+ const end = nextHeadingMatch ? bodyStart + nextHeadingMatch.index + 1 : text.length;
1537
+
1538
+ return {
1539
+ text,
1540
+ start,
1541
+ end
1542
+ };
1543
+ }
1544
+
1545
+ function readPromptPresetSection(platform, modeKey) {
1546
+ const fileName = PROMPT_PRESET_FILES[platform];
1547
+ const heading = PROMPT_MODE_TO_HEADING[modeKey];
1548
+ if (!fileName || !heading) {
1549
+ throw new Error(`Unknown platform/mode pair: ${platform}/${modeKey}`);
1550
+ }
1551
+
1552
+ const absolutePath = path.join(__dirname, "..", "docs", "prompt-presets", fileName);
1553
+ const markdown = fs.readFileSync(absolutePath, "utf8");
1554
+ const lines = markdown.split(/\r?\n/);
1555
+ const target = normalizeHeadingToken(heading);
1556
+ let start = -1;
1557
+ for (let index = 0; index < lines.length; index += 1) {
1558
+ const match = lines[index].match(/^##\s+(.+)$/);
1559
+ if (!match) {
1560
+ continue;
1561
+ }
1562
+ if (normalizeHeadingToken(match[1]) === target) {
1563
+ start = index + 1;
1564
+ break;
1565
+ }
1566
+ }
1567
+ if (start < 0) {
1568
+ throw new Error(`Preset heading not found: ${heading} in ${fileName}`);
1569
+ }
1570
+
1571
+ let end = lines.length;
1572
+ for (let index = start; index < lines.length; index += 1) {
1573
+ if (/^##\s+/.test(lines[index])) {
1574
+ end = index;
1575
+ break;
1576
+ }
1577
+ }
1578
+ const section = lines.slice(start, end).join("\n");
1579
+ const blockMatch = section.match(/```text\s*\n([\s\S]*?)\n```/i);
1580
+ if (!blockMatch) {
1581
+ throw new Error(`No text code block found in ${fileName} / ${heading}`);
1582
+ }
1583
+ return blockMatch[1].trim();
1584
+ }
1585
+
1586
+ function readVisualAssistVariant(platform, quality) {
1587
+ const fileName = PROMPT_PRESET_FILES[platform];
1588
+ const variant = QUALITY_TO_VARIANT[String(quality || "").toLowerCase()];
1589
+ if (!fileName || !variant) {
1590
+ throw new Error(`Unknown visual-assist selection: ${platform}/${quality}`);
1591
+ }
1592
+
1593
+ const absolutePath = path.join(__dirname, "..", "docs", "visual-assist-presets", fileName);
1594
+ const markdown = fs.readFileSync(absolutePath, "utf8");
1595
+ const lines = markdown.split(/\r?\n/);
1596
+ const headingPattern = new RegExp(`^###\\s+Variant\\s+${variant}\\b`, "i");
1597
+ let start = -1;
1598
+ for (let index = 0; index < lines.length; index += 1) {
1599
+ if (headingPattern.test(lines[index])) {
1600
+ start = index + 1;
1601
+ break;
1602
+ }
1603
+ }
1604
+ if (start < 0) {
1605
+ throw new Error(`Variant heading not found: ${variant} in ${fileName}`);
1606
+ }
1607
+
1608
+ let end = lines.length;
1609
+ for (let index = start; index < lines.length; index += 1) {
1610
+ if (/^###\s+Variant\s+\d+\b/i.test(lines[index])) {
1611
+ end = index;
1612
+ break;
1613
+ }
1614
+ }
1615
+ const section = lines.slice(start, end).join("\n");
1616
+ const blockMatch = section.match(/```md\s*\n([\s\S]*?)\n```/i);
1617
+ if (!blockMatch) {
1618
+ throw new Error(`No markdown code block found in ${fileName} / variant ${variant}`);
1619
+ }
1620
+ return blockMatch[1].trim();
1621
+ }
1622
+
1623
+ function parseVisualAssistOverrides(existingSection) {
1624
+ const overrides = {};
1625
+ const preserveKeys = [
1626
+ "Preferred adapters",
1627
+ "Design-supervisor reviewers",
1628
+ "Require Adapter",
1629
+ "Require Supervisor Review"
1630
+ ];
1631
+ const lines = String(existingSection || "").split(/\r?\n/);
1632
+ for (const key of preserveKeys) {
1633
+ const matcher = new RegExp(`^-\\s*${escapeRegExp(key)}\\s*:\\s*(.+)$`, "i");
1634
+ const matchLine = lines.find((line) => matcher.test(line.trim()));
1635
+ if (!matchLine) {
1636
+ continue;
1637
+ }
1638
+ const valueMatch = matchLine.trim().match(matcher);
1639
+ if (!valueMatch) {
1640
+ continue;
1641
+ }
1642
+ overrides[key] = String(valueMatch[1] || "").trim();
1643
+ }
1644
+ return overrides;
1645
+ }
1646
+
1647
+ function applyVisualAssistOverrides(blockText, overrides) {
1648
+ const lines = String(blockText || "").split(/\r?\n/);
1649
+ const keys = Object.keys(overrides || {});
1650
+ if (keys.length === 0) {
1651
+ return String(blockText || "").trim();
1652
+ }
1653
+ const updated = lines.map((line) => {
1654
+ for (const key of keys) {
1655
+ const matcher = new RegExp(`^(-\\s*${escapeRegExp(key)}\\s*:\\s*)(.+)$`, "i");
1656
+ const match = line.match(matcher);
1657
+ if (!match) {
1658
+ continue;
1659
+ }
1660
+ return `${match[1]}${overrides[key]}`;
1661
+ }
1662
+ return line;
1663
+ });
1664
+ return updated.join("\n").trim();
1665
+ }
1666
+
1667
+ function extractVisualAssistSection(markdown) {
1668
+ const range = findLevelTwoSectionRange(markdown, "Visual Assist");
1669
+ return range ? range.text.slice(range.start, range.end).trim() : "";
1670
+ }
1671
+
1672
+ function applyManagedVisualAssistBlock(originalContent, visualAssistBlock) {
1673
+ const original = String(originalContent || "").replace(/\r\n?/g, "\n");
1674
+ const normalizedBlock = String(visualAssistBlock || "").trim();
1675
+ if (!original.trim()) {
1676
+ return `# DA-VINCI\n\n${normalizedBlock}\n`;
1677
+ }
1678
+
1679
+ const range = findLevelTwoSectionRange(original, "Visual Assist");
1680
+ if (range) {
1681
+ const before = range.text.slice(0, range.start).trimEnd();
1682
+ const after = range.text.slice(range.end).trimStart();
1683
+ if (!before && !after) {
1684
+ return `${normalizedBlock}\n`;
1685
+ }
1686
+ if (!before) {
1687
+ return `${normalizedBlock}\n\n${after}`.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
1688
+ }
1689
+ if (!after) {
1690
+ return `${before}\n\n${normalizedBlock}\n`;
1691
+ }
1692
+ return `${before}\n\n${normalizedBlock}\n\n${after}`.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
1693
+ }
1694
+
1695
+ const base = original.trimEnd();
1696
+ return `${base}\n\n${normalizedBlock}\n`;
1697
+ }
1698
+
1699
+ function copyToClipboard(text) {
1700
+ const payload = String(text || "");
1701
+ const candidates = [
1702
+ { bin: "pbcopy", args: [] },
1703
+ { bin: "clip", args: [] },
1704
+ { bin: "xclip", args: ["-selection", "clipboard"] },
1705
+ { bin: "xsel", args: ["--clipboard", "--input"] }
1706
+ ];
1707
+ for (const candidate of candidates) {
1708
+ const result = spawnSync(candidate.bin, candidate.args, {
1709
+ input: payload,
1710
+ encoding: "utf8"
1711
+ });
1712
+ if (!result.error && result.status === 0) {
1713
+ return { ok: true, method: candidate.bin };
1714
+ }
1715
+ }
1716
+ return { ok: false, method: "" };
1717
+ }
1718
+
1719
+ async function runPlatformLifecycleFlow(state, commandId) {
1720
+ const lang = normalizeLanguage(state.lang);
1721
+ const zh = lang === "zh";
1722
+ const isInstall = commandId === "install";
1723
+ const selectedPlatforms = PLATFORM_INSTALL_OPTIONS.map((item) => item.value);
1724
+ const platformValue = selectedPlatforms.join(",");
1725
+ await runCommandById(state, commandId, {
1726
+ overrideArgs: [commandId, "--platform", platformValue],
1727
+ sceneId: "install-uninstall",
1728
+ eventName: `${commandId}_all_platforms`,
1729
+ loadingView: {
1730
+ titleZh: isInstall ? "正在安装资产" : "正在卸载资产",
1731
+ titleEn: isInstall ? "Installing Assets" : "Uninstalling Assets",
1732
+ lines: [
1733
+ `${zh ? "目标平台" : "Platforms"}: ${selectedPlatforms.join(", ")}`,
1734
+ zh
1735
+ ? "完成后会自动显示执行结果。"
1736
+ : "The execution result will be shown automatically when done."
1737
+ ]
1738
+ }
1739
+ });
1740
+ }
1741
+
1742
+ async function runPromptTemplateFlow(state, platform) {
1743
+ const lang = normalizeLanguage(state.lang);
1744
+ const zh = lang === "zh";
1745
+ appendLogEvent(state, "prompt-templates", "prompt_template_start", "ok", `platform=${platform}`);
1746
+
1747
+ const modeInput = await promptInput(
1748
+ state,
1749
+ zh ? "选择模式 (mode1-mode5)" : "Select mode (mode1-mode5)",
1750
+ "mode1"
1751
+ );
1752
+ const mode = String(modeInput || "").trim().toLowerCase();
1753
+ if (!PROMPT_MODE_TO_HEADING[mode]) {
1754
+ state.lastStatus = `${platform} prompt -> invalid mode`;
1755
+ appendLogEvent(state, "prompt-templates", "prompt_template_invalid_mode", "error", mode);
1756
+ return;
1757
+ }
1758
+
1759
+ let template = readPromptPresetSection(platform, mode);
1760
+ if (mode === "mode3") {
1761
+ const referenceDir = await promptInput(
1762
+ state,
1763
+ zh
1764
+ ? "<reference-directory>(可留空保留占位符)"
1765
+ : "<reference-directory> (leave blank to keep placeholder)",
1766
+ "",
1767
+ { allowBlank: true }
1768
+ );
1769
+ if (referenceDir) {
1770
+ template = template.replace(/<reference-directory>/g, referenceDir);
1771
+ }
1772
+ }
1773
+
1774
+ clearScreen();
1775
+ process.stdout.write(`${zh ? "模板预览" : "Template Preview"} (${platform} / ${mode})\n\n`);
1776
+ process.stdout.write(`${template}\n\n`);
1777
+ const copyAnswer = await promptInput(
1778
+ state,
1779
+ zh ? "复制到剪贴板? (Y/n)" : "Copy to clipboard? (Y/n)",
1780
+ "Y",
1781
+ { allowBlank: true }
1782
+ );
1783
+
1784
+ const wantsCopy = !String(copyAnswer || "").trim() || !/^n/i.test(String(copyAnswer || "").trim());
1785
+ if (wantsCopy) {
1786
+ const copied = copyToClipboard(template);
1787
+ if (copied.ok) {
1788
+ state.lastStatus = `${platform}/${mode} template copied via ${copied.method}`;
1789
+ appendLogEvent(state, "prompt-templates", "prompt_template_copy", "ok", `${platform}/${mode}`);
1790
+ } else {
1791
+ state.lastStatus = `${platform}/${mode} template ready (clipboard unavailable)`;
1792
+ appendLogEvent(state, "prompt-templates", "prompt_template_copy", "warn", "clipboard unavailable");
1793
+ }
1794
+ } else {
1795
+ state.lastStatus = `${platform}/${mode} template previewed`;
1796
+ }
1797
+
1798
+ process.stdout.write(`${zh ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`);
1799
+ await waitForAnyKey();
1800
+ }
1801
+
1802
+ async function runDesignQualityFlow(state) {
1803
+ const lang = normalizeLanguage(state.lang);
1804
+ const zh = lang === "zh";
1805
+
1806
+ while (true) {
1807
+ const platformChoice = await promptSelection(state, {
1808
+ title: zh ? "Visual Assist 预设:选择平台" : "Visual Assist Preset: Choose Platform",
1809
+ items: DESIGN_PLATFORM_OPTIONS,
1810
+ includeBack: true,
1811
+ showSummary: false,
1812
+ showFooter: false,
1813
+ initialIndex: Number.isFinite(state.designQualityPlatformSelectionIndex) ? state.designQualityPlatformSelectionIndex : 0
1814
+ });
1815
+ if (!platformChoice || !PROMPT_PRESET_FILES[platformChoice.value]) {
1816
+ state.lastStatus = zh ? "Visual Assist 预设已返回上一级" : "Visual Assist preset returned to previous level";
1817
+ appendLogEvent(state, "design-work", "visual_assist_platform_back", "ok", "back");
1818
+ return;
1819
+ }
1820
+ state.designQualityPlatformSelectionIndex = DESIGN_PLATFORM_OPTIONS.findIndex((item) => item.value === platformChoice.value);
1821
+
1822
+ const platform = platformChoice.value;
1823
+
1824
+ const qualityChoice = await promptSelection(state, {
1825
+ title: zh ? "Visual Assist 预设:选择质量" : "Visual Assist Preset: Choose Quality",
1826
+ items: DESIGN_QUALITY_OPTIONS,
1827
+ includeBack: true,
1828
+ showSummary: false,
1829
+ showFooter: false,
1830
+ initialIndex: Number.isFinite(state.designQualityQualitySelectionIndex) ? state.designQualityQualitySelectionIndex : 1
1831
+ });
1832
+ if (!qualityChoice || !QUALITY_TO_VARIANT[qualityChoice.value]) {
1833
+ state.lastStatus = zh ? "Visual Assist 预设:返回平台选择" : "Visual Assist preset: back to platform selection";
1834
+ appendLogEvent(state, "design-work", "visual_assist_quality_back", "ok", "back_to_platform");
1835
+ continue;
1836
+ }
1837
+ state.designQualityQualitySelectionIndex = DESIGN_QUALITY_OPTIONS.findIndex((item) => item.value === qualityChoice.value);
1838
+
1839
+ const quality = qualityChoice.value;
1840
+ const presetBlock = readVisualAssistVariant(platform, quality);
1841
+ const projectRoot = getExecutionCwd(state.projectPath);
1842
+ const daVinciPath = path.join(projectRoot, "DA-VINCI.md");
1843
+ const existingContent = fs.existsSync(daVinciPath) ? fs.readFileSync(daVinciPath, "utf8") : "";
1844
+ const existingSection = extractVisualAssistSection(existingContent);
1845
+ const overrides = parseVisualAssistOverrides(existingSection);
1846
+ const finalBlock = applyVisualAssistOverrides(presetBlock, overrides);
1847
+
1848
+ const nextContent = applyManagedVisualAssistBlock(existingContent, finalBlock);
1849
+ ensureDir(path.dirname(daVinciPath));
1850
+ fs.writeFileSync(daVinciPath, nextContent, "utf8");
1851
+ const variant = qualityChoice.variant || QUALITY_TO_VARIANT[quality];
1852
+ state.lastStatus = `Visual Assist updated (${platform}, variant ${variant}) -> ${daVinciPath}`;
1853
+ appendLogEvent(
1854
+ state,
1855
+ "design-work",
1856
+ "visual_assist_write",
1857
+ "ok",
1858
+ `platform=${platform};quality=${quality};variant=${variant};path=${daVinciPath}`
1859
+ );
268
1860
  return;
269
1861
  }
270
- if (typeof input.off === "function" && onKeypress) {
271
- input.off("keypress", onKeypress);
272
- }
273
- if (input.isTTY && typeof input.setRawMode === "function") {
274
- input.setRawMode(false);
275
- }
276
- if (typeof input.pause === "function") {
277
- input.pause();
278
- }
279
1862
  }
280
1863
 
281
- function getKeyAction(str, key = {}) {
282
- if (key.ctrl && key.name === "c") {
283
- return "quit";
284
- }
285
- if (key.name === "q") {
286
- return "quit";
287
- }
288
- if (key.name === "up" || (key.name === "k" && key.shift !== true)) {
289
- return "move_up";
290
- }
291
- if (key.name === "down" || (key.name === "j" && key.shift !== true)) {
292
- return "move_down";
293
- }
294
- if (key.name === "l") {
295
- return "toggle_lang";
296
- }
297
- if (key.name === "h") {
298
- return "toggle_command_help";
299
- }
300
- if (key.name === "s") {
301
- return "toggle_strict";
1864
+ function mapRouteToScene(route) {
1865
+ const value = String(route || "").trim();
1866
+ if (!value) {
1867
+ return "current-status";
302
1868
  }
303
- if ((key.name === "j" && key.shift === true) || str === "J") {
304
- return "toggle_json";
1869
+ if (value === "/dv:design") {
1870
+ return "design-work";
305
1871
  }
306
- if (key.name === "e") {
307
- return "toggle_continue_on_error";
1872
+ if (value === "/dv:verify") {
1873
+ return "pre-completion-validation";
308
1874
  }
309
- if (key.name === "p") {
310
- return "set_project";
1875
+ if (value === "select-change") {
1876
+ return "switch-work-item";
311
1877
  }
312
- if (key.name === "c") {
313
- return "set_change";
1878
+ if (
1879
+ value === "/dv:tasks" ||
1880
+ value === "/dv:build" ||
1881
+ value === "/dv:bootstrap" ||
1882
+ value === "bootstrap-project"
1883
+ ) {
1884
+ return null;
314
1885
  }
315
- if (key.name === "m") {
316
- return "edit_and_run";
1886
+ return "current-status";
1887
+ }
1888
+
1889
+ async function runCurrentStatusFlow(state) {
1890
+ const lang = normalizeLanguage(state.lang);
1891
+ const zh = lang === "zh";
1892
+
1893
+ const command = getCommandById("workflow-status");
1894
+ const statusArgs = commandArgsWithOverrides(command, state, { appendArgs: ["--json"] });
1895
+ const workflowStatusResult = executeDaVinci(state, statusArgs);
1896
+ const workflowPayload = parseJsonEnvelope(workflowStatusResult.stdout);
1897
+
1898
+ const nextCommand = getCommandById("next-step");
1899
+ const nextArgs = commandArgsWithOverrides(nextCommand, state, {});
1900
+ const nextStepResult = executeDaVinci(state, nextArgs);
1901
+
1902
+ const stage = workflowPayload && workflowPayload.stage ? String(workflowPayload.stage) : "unknown";
1903
+ const blockers = workflowPayload && Array.isArray(workflowPayload.failures) ? workflowPayload.failures : [];
1904
+ const nextRoute = workflowPayload && workflowPayload.nextStep && workflowPayload.nextStep.route
1905
+ ? String(workflowPayload.nextStep.route)
1906
+ : "";
1907
+ const recommendedScene = mapRouteToScene(nextRoute);
1908
+ const nextStepText = String(nextStepResult.stdout || "")
1909
+ .split(/\r?\n/)
1910
+ .map((line) => line.trimEnd())
1911
+ .filter((line) => line.trim().length > 0);
1912
+
1913
+ const lines = [];
1914
+ lines.push(`${zh ? "当前阶段" : "Stage"}: ${stage}`);
1915
+ lines.push(`${zh ? "阻塞项" : "Blockers"}: ${blockers.length}`);
1916
+ if (blockers.length > 0) {
1917
+ for (const blocker of blockers.slice(0, 5)) {
1918
+ lines.push(`- ${blocker}`);
1919
+ }
317
1920
  }
318
- if (key.name === "return" || key.name === "r") {
319
- return "run";
1921
+ if (nextRoute) {
1922
+ lines.push(`${zh ? "建议路由" : "Suggested route"}: ${nextRoute}`);
320
1923
  }
321
- if (str === "?") {
322
- return "toggle_help";
1924
+ const sceneRecord = recommendedScene ? getSceneById(recommendedScene) : null;
1925
+ const sceneTitle = sceneRecord
1926
+ ? resolveLocalizedText(sceneRecord.title, lang)
1927
+ : zh
1928
+ ? "直接执行命令"
1929
+ : "Run Recommended CLI Command";
1930
+ lines.push(`${zh ? "建议模块" : "Suggested module"}: ${sceneTitle}`);
1931
+ if (nextStepText.length > 0) {
1932
+ lines.push("");
1933
+ lines.push(zh ? "下一步总结:" : "Next-step summary:");
1934
+ for (const line of nextStepText.slice(0, 8)) {
1935
+ lines.push(`- ${line}`);
1936
+ }
323
1937
  }
324
- return null;
1938
+
1939
+ renderExecutionReport(
1940
+ lang,
1941
+ zh ? "当前状态" : "Current Status",
1942
+ lines
1943
+ );
1944
+ await waitForAnyKey();
1945
+
1946
+ state.lastStatus = `current-status -> stage=${stage}; route=${nextRoute || "n/a"}`;
1947
+ appendLogEvent(
1948
+ state,
1949
+ "current-status",
1950
+ "current_status",
1951
+ workflowStatusResult.status === 0 && nextStepResult.status === 0 ? "ok" : "error",
1952
+ `stage=${stage};route=${nextRoute}`
1953
+ );
1954
+ state.activeSceneId = "current-status";
1955
+ state.rootSelectionIndex = Math.max(0, SCENES.findIndex((scene) => scene.id === "current-status"));
1956
+ state.viewMode = "scene_list";
325
1957
  }
326
1958
 
327
- function waitForAnyKey() {
328
- return new Promise((resolve) => {
329
- const handler = () => {
330
- process.stdin.off("keypress", handler);
331
- resolve();
1959
+ function getArtifactReadiness(state) {
1960
+ const projectRoot = getExecutionCwd(state.projectPath);
1961
+ const changeId = resolveActiveChangeId(projectRoot, state.changeId);
1962
+ if (!changeId) {
1963
+ return {
1964
+ changeId: "",
1965
+ changeRoot: "",
1966
+ missingTasks: true,
1967
+ missingBindings: true,
1968
+ hasDesign: false,
1969
+ hasBindings: false,
1970
+ hasTasks: false
332
1971
  };
333
- process.stdin.on("keypress", handler);
334
- });
1972
+ }
1973
+
1974
+ const changeRoot = path.join(projectRoot, ".da-vinci", "changes", changeId);
1975
+ const hasTasks = fs.existsSync(path.join(changeRoot, "tasks.md"));
1976
+ const hasDesign = fs.existsSync(path.join(changeRoot, "pencil-design.md"));
1977
+ const hasBindings = fs.existsSync(path.join(changeRoot, "pencil-bindings.md"));
1978
+
1979
+ const missingTasks = !hasTasks;
1980
+ const missingBindings = hasDesign && !hasBindings;
1981
+
1982
+ return {
1983
+ changeId,
1984
+ changeRoot,
1985
+ missingTasks,
1986
+ missingBindings,
1987
+ hasDesign,
1988
+ hasBindings,
1989
+ hasTasks
1990
+ };
335
1991
  }
336
1992
 
337
- async function promptInput(state, label, currentValue, options = {}) {
338
- state.rawModeEnabled = false;
339
- if (process.stdin.isTTY) {
340
- process.stdin.setRawMode(false);
341
- }
342
- process.stdin.pause();
343
- const rl = readline.createInterface({
344
- input: process.stdin,
345
- output: process.stdout
346
- });
347
- const promptLabel = currentValue
348
- ? `${label} [${currentValue}]: `
349
- : `${label}: `;
350
- const answer = await new Promise((resolve) => {
351
- rl.question(promptLabel, (value) => resolve(value));
352
- });
353
- rl.close();
354
- process.stdin.resume();
355
- if (process.stdin.isTTY) {
356
- process.stdin.setRawMode(true);
357
- }
358
- state.rawModeEnabled = true;
359
- const normalized = String(answer || "").trim();
360
- if (!normalized && options.allowBlank) {
361
- return "";
362
- }
363
- return normalized || String(currentValue || "").trim();
1993
+ function resolvePreImplementationResult(readiness, stepResults) {
1994
+ const hasBlocker = Boolean(readiness && (readiness.missingTasks || readiness.missingBindings));
1995
+ const hasCommandFailure = (stepResults || []).some((step) => !step || step.status !== 0);
1996
+ const exitCode = hasBlocker || hasCommandFailure ? 1 : 0;
1997
+ const summaryStatus = hasBlocker ? "BLOCK" : hasCommandFailure ? "FAIL" : "PASS";
1998
+ return {
1999
+ hasBlocker,
2000
+ hasCommandFailure,
2001
+ exitCode,
2002
+ summaryStatus,
2003
+ logStatus: exitCode === 0 ? "ok" : "error"
2004
+ };
364
2005
  }
365
2006
 
366
- function renderState(state) {
367
- clearScreen();
2007
+ async function runPreImplementationChecks(state) {
368
2008
  const lang = normalizeLanguage(state.lang);
369
- const width = process.stdout.columns || 100;
370
- const rows = buildCatalogRows(lang);
371
- const selectedRowIndex = getRowIndexForCommand(
372
- rows,
373
- COMMANDS.findIndex((command) => command.id === state.selectedCommandId)
374
- );
375
- const listHeight = Math.max(10, Math.min(16, (process.stdout.rows || 40) - 20));
376
- const visible = getVisibleRows(rows, selectedRowIndex, listHeight);
377
- const preview = buildPreviewCommand(state);
378
- const stage = getStageById(preview.command.stageId);
379
- const selectedNumber = COMMANDS.findIndex((command) => command.id === preview.command.id) + 1;
2009
+ const zh = lang === "zh";
2010
+ const readiness = getArtifactReadiness(state);
380
2011
  const lines = [];
2012
+ const chain = [];
2013
+ const stepResults = [];
381
2014
 
382
- lines.push(lang === "zh" ? "Da Vinci TUI 终端导航" : "Da Vinci TUI");
383
- lines.push(
384
- lang === "zh"
385
- ? `语言: ${lang === "zh" ? "中文" : "English"} (l) | 项目: ${state.projectPath || process.cwd()} (p) | Change: ${
386
- state.changeId || "auto"
387
- } (c)`
388
- : `Language: ${lang === "zh" ? "Chinese" : "English"} (l) | Project: ${
389
- state.projectPath || process.cwd()
390
- } (p) | Change: ${state.changeId || "auto"} (c)`
391
- );
392
- lines.push(
393
- lang === "zh"
394
- ? `开关: strict=${state.strict ? "on" : "off"} (s) | json=${state.jsonOutput ? "on" : "off"} (J) | continue-on-error=${
395
- state.continueOnError ? "on" : "off"
396
- } (e)`
397
- : `Flags: strict=${state.strict ? "on" : "off"} (s) | json=${state.jsonOutput ? "on" : "off"} (J) | continue-on-error=${
398
- state.continueOnError ? "on" : "off"
399
- } (e)`
400
- );
401
- lines.push(
402
- lang === "zh"
403
- ? "操作: ↑↓/j/k 移动 | Enter/r 执行 | h 参数 | m 编辑命令 | ? 帮助 | q 退出"
404
- : "Keys: Up/Down/j/k move | Enter/r run | h params | m edit command | ? help | q quit"
405
- );
406
- lines.push("");
407
- lines.push(lang === "zh" ? "命令目录" : "Command Catalog");
408
- for (let index = 0; index < visible.items.length; index += 1) {
409
- const row = visible.items[index];
410
- const absoluteIndex = visible.start + index;
411
- if (row.type === "stage") {
412
- lines.push(` [${row.label}]`);
413
- continue;
414
- }
415
- const selected = absoluteIndex === selectedRowIndex;
416
- const prefix = selected ? ">" : " ";
417
- const title = truncate(row.label, Math.max(16, width - 8));
418
- lines.push(` ${prefix} ${title}`);
419
- }
420
- lines.push("");
421
- lines.push(
422
- lang === "zh"
423
- ? `当前命令 (${selectedNumber}/${COMMANDS.length}): ${resolveLocalizedText(preview.command.title, lang)}`
424
- : `Selected (${selectedNumber}/${COMMANDS.length}): ${resolveLocalizedText(preview.command.title, lang)}`
425
- );
426
- lines.push(
427
- lang === "zh"
428
- ? `阶段: ${resolveLocalizedText(stage.title, lang)}`
429
- : `Phase: ${resolveLocalizedText(stage.title, lang)}`
430
- );
431
- for (const line of wrapText(resolveLocalizedText(preview.command.summary, lang), width - 2)) {
432
- lines.push(line);
433
- }
434
- lines.push("");
435
- for (const line of wrapText(resolveLocalizedText(preview.command.details, lang), width - 2)) {
436
- lines.push(line);
437
- }
438
- lines.push("");
439
- lines.push(lang === "zh" ? "预览命令:" : "Preview:");
440
- for (const line of wrapText(preview.display, width - 2)) {
441
- lines.push(line);
2015
+ chain.push({
2016
+ commandId: "lint-spec",
2017
+ label: "lint-spec",
2018
+ eventName: "preimpl_lint_spec",
2019
+ options: {},
2020
+ stopOnFailure: false
2021
+ });
2022
+ chain.push({
2023
+ commandId: "scope-check",
2024
+ label: "scope-check",
2025
+ eventName: "preimpl_scope_check",
2026
+ options: {},
2027
+ stopOnFailure: false
2028
+ });
2029
+
2030
+ if (readiness.hasTasks) {
2031
+ chain.push({
2032
+ commandId: "lint-tasks",
2033
+ label: "lint-tasks",
2034
+ eventName: "preimpl_lint_tasks",
2035
+ options: {},
2036
+ stopOnFailure: false
2037
+ });
2038
+ } else {
2039
+ lines.push(zh ? "- BLOCK: 缺少 tasks.md,实施前检查不满足实现就绪。" : "- BLOCK: tasks.md is missing; implementation readiness is not met.");
442
2040
  }
443
- if (hasPlaceholders(preview.args)) {
2041
+
2042
+ if (readiness.hasBindings) {
2043
+ chain.push({
2044
+ commandId: "lint-bindings",
2045
+ label: "lint-bindings",
2046
+ eventName: "preimpl_lint_bindings",
2047
+ options: {},
2048
+ stopOnFailure: false
2049
+ });
2050
+ } else if (readiness.missingBindings) {
444
2051
  lines.push(
445
- lang === "zh"
446
- ? "提示: 当前命令包含占位符,按 m 编辑后再执行。"
447
- : "Hint: this command still contains placeholders; press m to edit before running."
2052
+ zh
2053
+ ? "- BLOCK: 已存在 pencil-design.md 但缺少 pencil-bindings.md。"
2054
+ : "- BLOCK: pencil-design.md exists but pencil-bindings.md is missing."
448
2055
  );
449
- }
450
- if (!state.commandHelpVisible) {
2056
+ } else {
451
2057
  lines.push(
452
- lang === "zh"
453
- ? "提示: h 查看当前命令支持的参数。"
454
- : "Hint: press h to inspect parameters for the selected command."
2058
+ zh
2059
+ ? "- SKIP: 当前没有 design/bindings 对,跳过 lint-bindings。"
2060
+ : "- SKIP: no design/bindings pair found, lint-bindings skipped safely."
455
2061
  );
456
2062
  }
457
- if (state.lastStatus) {
458
- lines.push("");
459
- lines.push(
460
- lang === "zh"
461
- ? `最近结果: ${state.lastStatus}`
462
- : `Last result: ${state.lastStatus}`
463
- );
2063
+
2064
+ const reportLines = [];
2065
+ for (const item of chain) {
2066
+ const command = getCommandById(item.commandId);
2067
+ const args = commandArgsWithOverrides(command, state, item.options || {});
2068
+ const display = ["da-vinci", ...args].join(" ");
2069
+ appendLogEvent(state, "pre-implementation-checks", item.eventName, "start", display);
2070
+ const result = executeDaVinci(state, args);
2071
+ const code = result.status == null ? "null" : String(result.status);
2072
+ stepResults.push({
2073
+ commandId: item.commandId,
2074
+ status: result.status
2075
+ });
2076
+ reportLines.push(`- ${item.label}: ${result.status === 0 ? "PASS" : "FAIL"} (${code})`);
2077
+ appendLogEvent(state, "pre-implementation-checks", item.eventName, result.status === 0 ? "ok" : "error", `exit=${code}`);
464
2078
  }
465
- if (state.helpVisible) {
466
- lines.push("");
467
- lines.push("-".repeat(Math.max(20, Math.min(width - 2, 48))));
468
- for (const line of formatTuiHelp(lang).split("\n")) {
469
- lines.push(line);
2079
+
2080
+ const finalLines = [...lines, ...reportLines];
2081
+ const outcome = resolvePreImplementationResult(readiness, stepResults);
2082
+ renderExecutionReport(
2083
+ lang,
2084
+ zh ? "实施前检查结果" : "Pre-Implementation Check Result",
2085
+ finalLines
2086
+ );
2087
+ await waitForAnyKey();
2088
+
2089
+ state.lastStatus = `pre-implementation -> ${outcome.summaryStatus}`;
2090
+ appendLogEvent(
2091
+ state,
2092
+ "pre-implementation-checks",
2093
+ "preimpl_summary",
2094
+ outcome.logStatus,
2095
+ `missingTasks=${readiness.missingTasks};missingBindings=${readiness.missingBindings};commandFailures=${outcome.hasCommandFailure}`
2096
+ );
2097
+ }
2098
+
2099
+ async function runPreCompletionValidation(state) {
2100
+ await runCommandChain(state, "pre-completion-validation", [
2101
+ {
2102
+ commandId: "verify-bindings",
2103
+ label: "verify-bindings",
2104
+ eventName: "verify_bindings",
2105
+ options: {},
2106
+ stopOnFailure: false
2107
+ },
2108
+ {
2109
+ commandId: "verify-implementation",
2110
+ label: "verify-implementation",
2111
+ eventName: "verify_implementation",
2112
+ options: {},
2113
+ stopOnFailure: false
2114
+ },
2115
+ {
2116
+ commandId: "verify-structure",
2117
+ label: "verify-structure",
2118
+ eventName: "verify_structure",
2119
+ options: {},
2120
+ stopOnFailure: false
2121
+ },
2122
+ {
2123
+ commandId: "verify-coverage",
2124
+ label: "verify-coverage",
2125
+ eventName: "verify_coverage",
2126
+ options: {},
2127
+ stopOnFailure: false
470
2128
  }
2129
+ ]);
2130
+ }
2131
+
2132
+ function listExistingChangeIds(projectPath) {
2133
+ const changesRoot = path.join(getExecutionCwd(projectPath), ".da-vinci", "changes");
2134
+ if (!fs.existsSync(changesRoot)) {
2135
+ return [];
471
2136
  }
472
- if (state.commandHelpVisible) {
473
- lines.push("");
474
- lines.push("-".repeat(Math.max(20, Math.min(width - 2, 48))));
475
- for (const line of formatCommandParameterHelp(preview.command, lang, width).flat()) {
476
- lines.push(line);
477
- }
2137
+ try {
2138
+ return fs
2139
+ .readdirSync(changesRoot, { withFileTypes: true })
2140
+ .filter((entry) => entry && entry.isDirectory())
2141
+ .map((entry) => entry.name)
2142
+ .sort();
2143
+ } catch (_error) {
2144
+ return [];
478
2145
  }
2146
+ }
479
2147
 
480
- process.stdout.write(`${lines.join("\n")}\n`);
2148
+ async function chooseExistingChange(state) {
2149
+ const lang = normalizeLanguage(state.lang);
2150
+ const zh = lang === "zh";
2151
+ const changeIds = listExistingChangeIds(state.projectPath);
2152
+ if (changeIds.length === 0) {
2153
+ clearScreen();
2154
+ process.stdout.write(`${zh ? "没有可切换的工作项。" : "No work items found."}\n\n`);
2155
+ process.stdout.write(`${zh ? "当前项目下还没有可供切换的 change。" : "This project does not have any switchable changes yet."}\n\n`);
2156
+ process.stdout.write(`${zh ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`);
2157
+ state.lastStatus = zh ? "暂无可切换工作项" : "no work items available";
2158
+ appendLogEvent(state, "switch-work-item", "change_switch", "warn", "no work items");
2159
+ await waitForAnyKey();
2160
+ return;
2161
+ }
2162
+ state.workItemChoices = changeIds;
2163
+ state.workItemSelectionIndex = Math.max(0, changeIds.findIndex((item) => item === state.changeId));
2164
+ state.viewMode = "work_item_switch";
2165
+ appendLogEvent(state, "switch-work-item", "change_switch_open", "ok", `${changeIds.length} items`);
481
2166
  }
482
2167
 
483
- async function runPreview(state, options = {}) {
2168
+ async function chooseLanguage(state) {
484
2169
  const lang = normalizeLanguage(state.lang);
485
- const preview = buildPreviewCommand(state);
486
- let args = preview.args;
2170
+ const zh = lang === "zh";
2171
+ const selected = await promptInput(
2172
+ state,
2173
+ zh ? "语言 (en/zh)" : "Language (en/zh)",
2174
+ state.lang
2175
+ );
2176
+ const next = normalizeLanguage(selected);
2177
+ state.lang = next;
2178
+ savePreferences({ lang: next });
2179
+ state.lastStatus = `language -> ${next}`;
2180
+ appendLogEvent(state, "more", "language_set", "ok", next);
2181
+ }
487
2182
 
488
- if (options.forceEdit || hasPlaceholders(args)) {
489
- renderState(state);
490
- const edited = await promptInput(
491
- state,
492
- lang === "zh" ? "编辑命令" : "Edit command",
493
- preview.display
494
- );
495
- if (!edited) {
496
- return;
497
- }
498
- const parsed = parseEditedCommandInput(edited);
499
- if (parsed.error) {
500
- clearScreen();
501
- process.stdout.write(
502
- `${lang === "zh" ? "命令编辑失败" : "Command edit failed"}\n\n${parsed.error.message || String(parsed.error)}\n\n`
503
- );
504
- process.stdout.write(
505
- `${lang === "zh" ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`
506
- );
507
- state.lastStatus = `${lang === "zh" ? "命令编辑失败" : "Command edit failed"}: ${
508
- parsed.error.message || String(parsed.error)
509
- }`;
510
- await waitForAnyKey();
511
- return;
512
- }
513
- args = parsed.args;
514
- if (args.length === 0) {
515
- return;
516
- }
2183
+ function listDailyLogFiles(projectPath) {
2184
+ const logsRoot = path.join(getExecutionCwd(projectPath), LOGS_DIR_RELATIVE);
2185
+ if (!fs.existsSync(logsRoot)) {
2186
+ return [];
2187
+ }
2188
+ let files = [];
2189
+ try {
2190
+ files = fs
2191
+ .readdirSync(logsRoot)
2192
+ .filter((name) => /^\d{4}-\d{2}-\d{2}\.ndjson$/.test(name))
2193
+ .sort()
2194
+ .reverse();
2195
+ } catch (_error) {
2196
+ return [];
517
2197
  }
2198
+ return files.map((name) => path.join(logsRoot, name));
2199
+ }
518
2200
 
519
- clearScreen();
520
- process.stdout.write(
521
- `${lang === "zh" ? "正在执行" : "Running"}: ${["da-vinci", ...args].join(" ")}\n\n`
522
- );
523
- const result = spawnSync(process.execPath, [DA_VINCI_BIN, ...args], {
524
- cwd: getExecutionCwd(state.projectPath),
525
- encoding: "utf8",
526
- maxBuffer: 16 * 1024 * 1024
527
- });
528
- if (result.error) {
529
- process.stdout.write(
530
- `${lang === "zh" ? "执行失败" : "Execution failed"}: ${result.error.message || String(result.error)}\n\n`
531
- );
532
- process.stdout.write(
533
- `${lang === "zh" ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`
534
- );
535
- state.lastStatus = `${["da-vinci", ...args].join(" ")} -> error`;
536
- await waitForAnyKey();
2201
+ async function showLogsFlow(state) {
2202
+ const lang = normalizeLanguage(state.lang);
2203
+ const zh = lang === "zh";
2204
+
2205
+ const files = listDailyLogFiles(state.projectPath);
2206
+ if (files.length === 0) {
2207
+ state.lastStatus = zh ? "日志为空" : "No logs found";
537
2208
  return;
538
2209
  }
539
- const stdout = String(result.stdout || "");
540
- const stderr = String(result.stderr || "");
541
- const combined = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n\n");
542
- process.stdout.write(`${combined || (lang === "zh" ? "没有输出。" : "No output.")}\n\n`);
543
- process.stdout.write(
544
- `${lang === "zh" ? "退出码" : "Exit code"}: ${result.status == null ? "null" : result.status}\n`
2210
+
2211
+ const promptLines = files
2212
+ .slice(0, 10)
2213
+ .map((absolutePath, index) => `${index + 1}. ${path.basename(absolutePath)}`)
2214
+ .join("\n");
2215
+ clearScreen();
2216
+ process.stdout.write(`${zh ? "可用日志" : "Available logs"}\n\n${promptLines}\n\n`);
2217
+
2218
+ const indexRaw = await promptInput(
2219
+ state,
2220
+ zh ? "选择日志编号" : "Pick log number",
2221
+ "1"
545
2222
  );
546
- process.stdout.write(
547
- `${lang === "zh" ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`
2223
+ const selectedIndex = Math.max(1, Number.parseInt(indexRaw, 10) || 1);
2224
+ const selectedPath = files[Math.min(files.length - 1, selectedIndex - 1)];
2225
+ const content = fs.readFileSync(selectedPath, "utf8");
2226
+
2227
+ clearScreen();
2228
+ process.stdout.write(`${path.basename(selectedPath)}\n\n${content}\n`);
2229
+
2230
+ const copyAnswer = await promptInput(
2231
+ state,
2232
+ zh ? "复制当天日志? (Y/n)" : "Copy this day log? (Y/n)",
2233
+ "Y",
2234
+ { allowBlank: true }
548
2235
  );
549
- state.lastStatus = `${["da-vinci", ...args].join(" ")} -> ${result.status == null ? "null" : result.status}`;
2236
+
2237
+ const wantsCopy = !String(copyAnswer || "").trim() || !/^n/i.test(String(copyAnswer || "").trim());
2238
+ if (wantsCopy) {
2239
+ const copied = copyToClipboard(content);
2240
+ if (copied.ok) {
2241
+ state.lastStatus = `${path.basename(selectedPath)} copied via ${copied.method}`;
2242
+ appendLogEvent(state, "more", "logs_copy", "ok", path.basename(selectedPath));
2243
+ } else {
2244
+ state.lastStatus = `${path.basename(selectedPath)} ready (clipboard unavailable)`;
2245
+ appendLogEvent(state, "more", "logs_copy", "warn", "clipboard unavailable");
2246
+ }
2247
+ } else {
2248
+ state.lastStatus = `${path.basename(selectedPath)} viewed`;
2249
+ }
2250
+
2251
+ process.stdout.write(`\n${zh ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`);
550
2252
  await waitForAnyKey();
551
2253
  }
552
2254
 
@@ -555,26 +2257,45 @@ async function launchTui(options = {}) {
555
2257
  throw new Error("`da-vinci tui` requires an interactive terminal. Use `--help` for usage.");
556
2258
  }
557
2259
 
2260
+ const preferences = loadPreferences();
2261
+ const initialLang = options.lang ? normalizeLanguage(options.lang) : normalizeLanguage(preferences.lang || getDefaultLanguage());
558
2262
  const initialProject = String(options.projectPath || process.cwd()).trim() || process.cwd();
559
2263
  const initialChange = String(options.changeId || "").trim();
2264
+ const initialLayoutWidth = normalizeLayoutWidth(
2265
+ options.tuiWidth != null ? options.tuiWidth : options.layoutWidth != null ? options.layoutWidth : preferences.layoutWidth
2266
+ );
2267
+ const useAltScreen = resolveAltScreenMode(options, preferences);
2268
+
560
2269
  const state = {
561
- lang: normalizeLanguage(options.lang || getDefaultLanguage()),
2270
+ lang: initialLang,
562
2271
  projectPath: initialProject,
563
2272
  changeId: initialChange,
2273
+ layoutWidth: initialLayoutWidth,
564
2274
  strict: options.strict === true,
565
2275
  jsonOutput: options.jsonOutput === true,
566
2276
  continueOnError: options.continueOnError === true,
567
- selectedCommandId: COMMANDS[0].id,
2277
+ themeMode: THEME_MODE,
2278
+ viewMode: "scene_list",
2279
+ activeSceneId: SCENES[0].id,
2280
+ rootSelectionIndex: 0,
2281
+ sceneSelectionById: {},
2282
+ designQualityPlatformSelectionIndex: 0,
2283
+ designQualityQualitySelectionIndex: 1,
568
2284
  helpVisible: false,
569
- commandHelpVisible: false,
570
2285
  lastStatus: "",
571
2286
  rawModeEnabled: true,
572
2287
  busy: false
573
2288
  };
574
2289
 
2290
+ appendLogEvent(state, "system", "tui_start", "ok", `lang=${state.lang};theme=${state.themeMode}`);
2291
+
575
2292
  readline.emitKeypressEvents(process.stdin);
576
2293
  process.stdin.setRawMode(true);
577
2294
  process.stdin.resume();
2295
+ if (useAltScreen) {
2296
+ enterAlternateScreen();
2297
+ }
2298
+ hideCursor();
578
2299
 
579
2300
  let closed = false;
580
2301
  return await new Promise((resolve) => {
@@ -583,28 +2304,48 @@ async function launchTui(options = {}) {
583
2304
  return;
584
2305
  }
585
2306
  closed = true;
2307
+ appendLogEvent(state, "system", "tui_exit", "ok", "quit");
586
2308
  teardownInput(process.stdin, onKeypress);
587
- clearScreen();
2309
+ if (useAltScreen) {
2310
+ exitAlternateScreen();
2311
+ }
2312
+ showCursor();
588
2313
  resolve();
589
2314
  };
590
2315
 
591
2316
  const moveSelection = (delta) => {
592
- const currentIndex = COMMANDS.findIndex((command) => command.id === state.selectedCommandId);
593
- const nextIndex = (currentIndex + delta + COMMANDS.length) % COMMANDS.length;
594
- state.selectedCommandId = COMMANDS[nextIndex].id;
595
- };
2317
+ if (state.viewMode === "scene_list") {
2318
+ const menuSize = SCENES.length + 1;
2319
+ const currentIndex = Number.isFinite(state.rootSelectionIndex) ? state.rootSelectionIndex : 0;
2320
+ const nextIndex = (currentIndex + delta + menuSize) % menuSize;
2321
+ state.rootSelectionIndex = nextIndex;
2322
+ if (nextIndex < SCENES.length) {
2323
+ state.activeSceneId = SCENES[nextIndex].id;
2324
+ }
2325
+ return;
2326
+ }
596
2327
 
597
- const onKeypress = async (str, key = {}) => {
598
- if (closed) {
2328
+ if (state.viewMode === "scene_actions") {
2329
+ const next = getSceneActionIndex(state) + delta;
2330
+ setSceneActionIndex(state, next);
599
2331
  return;
600
2332
  }
601
- if (state.busy) {
2333
+
2334
+ if (state.viewMode === "work_item_switch") {
2335
+ const next = getWorkItemSelectionIndex(state) + delta;
2336
+ setWorkItemSelectionIndex(state, next);
2337
+ }
2338
+ };
2339
+
2340
+ const onKeypress = async (str, key = {}) => {
2341
+ if (closed || state.busy) {
602
2342
  return;
603
2343
  }
604
2344
  const action = getKeyAction(str, key);
605
2345
  if (!action) {
606
2346
  return;
607
2347
  }
2348
+
608
2349
  if (action === "quit") {
609
2350
  close();
610
2351
  return;
@@ -621,26 +2362,35 @@ async function launchTui(options = {}) {
621
2362
  }
622
2363
  if (action === "toggle_lang") {
623
2364
  state.lang = state.lang === "zh" ? "en" : "zh";
2365
+ savePreferences({ lang: state.lang });
2366
+ state.lastStatus = `language -> ${state.lang}`;
2367
+ appendLogEvent(state, "system", "language_toggle", "ok", state.lang);
624
2368
  renderState(state);
625
2369
  return;
626
2370
  }
627
- if (action === "toggle_command_help") {
628
- state.commandHelpVisible = !state.commandHelpVisible;
2371
+ if (action === "toggle_theme") {
2372
+ state.themeMode = cycleThemeMode(state.themeMode);
2373
+ applyThemeMode(state.themeMode);
2374
+ state.lastStatus = `theme -> ${state.themeMode}`;
2375
+ appendLogEvent(state, "system", "theme_toggle", "ok", state.themeMode);
629
2376
  renderState(state);
630
2377
  return;
631
2378
  }
632
2379
  if (action === "toggle_strict") {
633
2380
  state.strict = !state.strict;
2381
+ appendLogEvent(state, "system", "strict_toggle", "ok", String(state.strict));
634
2382
  renderState(state);
635
2383
  return;
636
2384
  }
637
2385
  if (action === "toggle_json") {
638
2386
  state.jsonOutput = !state.jsonOutput;
2387
+ appendLogEvent(state, "system", "json_toggle", "ok", String(state.jsonOutput));
639
2388
  renderState(state);
640
2389
  return;
641
2390
  }
642
2391
  if (action === "toggle_continue_on_error") {
643
2392
  state.continueOnError = !state.continueOnError;
2393
+ appendLogEvent(state, "system", "continue_toggle", "ok", String(state.continueOnError));
644
2394
  renderState(state);
645
2395
  return;
646
2396
  }
@@ -653,6 +2403,8 @@ async function launchTui(options = {}) {
653
2403
  state.projectPath
654
2404
  );
655
2405
  state.projectPath = value || process.cwd();
2406
+ state.lastStatus = `project -> ${state.projectPath}`;
2407
+ appendLogEvent(state, "system", "project_set", "ok", state.projectPath);
656
2408
  } finally {
657
2409
  state.busy = false;
658
2410
  }
@@ -669,16 +2421,8 @@ async function launchTui(options = {}) {
669
2421
  { allowBlank: true }
670
2422
  );
671
2423
  state.changeId = value;
672
- } finally {
673
- state.busy = false;
674
- }
675
- renderState(state);
676
- return;
677
- }
678
- if (action === "edit_and_run") {
679
- state.busy = true;
680
- try {
681
- await runPreview(state, { forceEdit: true });
2424
+ state.lastStatus = `change -> ${state.changeId || "auto"}`;
2425
+ appendLogEvent(state, "system", "change_set", "ok", state.changeId || "auto");
682
2426
  } finally {
683
2427
  state.busy = false;
684
2428
  }
@@ -686,18 +2430,112 @@ async function launchTui(options = {}) {
686
2430
  return;
687
2431
  }
688
2432
  if (action === "run") {
689
- state.busy = true;
690
- try {
691
- await runPreview(state, { forceEdit: false });
692
- } finally {
693
- state.busy = false;
2433
+ if (state.viewMode === "scene_list") {
2434
+ const menuSize = SCENES.length + 1;
2435
+ const rootIndex =
2436
+ Number.isFinite(state.rootSelectionIndex) && menuSize > 0
2437
+ ? ((state.rootSelectionIndex % menuSize) + menuSize) % menuSize
2438
+ : 0;
2439
+ state.rootSelectionIndex = rootIndex;
2440
+ if (rootIndex === SCENES.length) {
2441
+ close();
2442
+ return;
2443
+ }
2444
+ const selectedScene = SCENES[rootIndex] || SCENES[0];
2445
+ state.activeSceneId = selectedScene.id;
2446
+ if (
2447
+ selectedScene.id === "current-status" ||
2448
+ selectedScene.id === "switch-work-item" ||
2449
+ selectedScene.id === "pre-implementation-checks" ||
2450
+ selectedScene.id === "pre-completion-validation"
2451
+ ) {
2452
+ state.busy = true;
2453
+ try {
2454
+ if (selectedScene.id === "current-status") {
2455
+ appendLogEvent(state, state.activeSceneId, "scene_run", "ok", "run current status summary");
2456
+ await runCurrentStatusFlow(state);
2457
+ appendLogEvent(state, state.activeSceneId, "scene_complete", "ok", "run current status summary");
2458
+ } else if (selectedScene.id === "pre-implementation-checks") {
2459
+ appendLogEvent(state, state.activeSceneId, "scene_run", "ok", "run implementation readiness checks");
2460
+ await runPreImplementationChecks(state);
2461
+ appendLogEvent(state, state.activeSceneId, "scene_complete", "ok", "run implementation readiness checks");
2462
+ } else if (selectedScene.id === "pre-completion-validation") {
2463
+ appendLogEvent(state, state.activeSceneId, "scene_run", "ok", "run standard completion validation");
2464
+ await runPreCompletionValidation(state);
2465
+ appendLogEvent(state, state.activeSceneId, "scene_complete", "ok", "run standard completion validation");
2466
+ } else {
2467
+ appendLogEvent(state, state.activeSceneId, "scene_run", "ok", "open work item switcher");
2468
+ await chooseExistingChange(state);
2469
+ appendLogEvent(state, state.activeSceneId, "scene_complete", "ok", "open work item switcher");
2470
+ }
2471
+ } catch (error) {
2472
+ state.lastStatus = `${selectedScene.id} -> error: ${error.message || String(error)}`;
2473
+ appendLogEvent(state, state.activeSceneId, "scene_complete", "error", error.message || String(error));
2474
+ } finally {
2475
+ state.busy = false;
2476
+ }
2477
+ renderState(state);
2478
+ return;
2479
+ }
2480
+ state.viewMode = "scene_actions";
2481
+ appendLogEvent(state, state.activeSceneId, "scene_enter", "ok", "open scene actions");
2482
+ renderState(state);
2483
+ return;
2484
+ }
2485
+
2486
+ if (state.viewMode === "scene_actions") {
2487
+ const actions = getSceneActions(state, state.activeSceneId);
2488
+ const selectedIndex = getSceneActionIndex(state);
2489
+ if (selectedIndex >= actions.length) {
2490
+ state.viewMode = "scene_list";
2491
+ state.rootSelectionIndex = Math.max(0, SCENES.findIndex((scene) => scene.id === state.activeSceneId));
2492
+ renderState(state);
2493
+ return;
2494
+ }
2495
+ const actionItem = actions[selectedIndex] || null;
2496
+ if (!actionItem) {
2497
+ renderState(state);
2498
+ return;
2499
+ }
2500
+ state.busy = true;
2501
+ try {
2502
+ appendLogEvent(state, state.activeSceneId, "action_start", "ok", actionItem.id);
2503
+ await actionItem.run(state);
2504
+ appendLogEvent(state, state.activeSceneId, "action_complete", "ok", actionItem.id);
2505
+ } catch (error) {
2506
+ state.lastStatus = `${actionItem.id} -> error: ${error.message || String(error)}`;
2507
+ appendLogEvent(state, state.activeSceneId, "action_complete", "error", `${actionItem.id}: ${error.message || String(error)}`);
2508
+ } finally {
2509
+ state.busy = false;
2510
+ }
2511
+ renderState(state);
2512
+ return;
2513
+ }
2514
+
2515
+ if (state.viewMode === "work_item_switch") {
2516
+ const choices = Array.isArray(state.workItemChoices) ? state.workItemChoices : [];
2517
+ const selectedIndex = getWorkItemSelectionIndex(state);
2518
+ if (selectedIndex >= choices.length) {
2519
+ state.viewMode = "scene_list";
2520
+ state.activeSceneId = "switch-work-item";
2521
+ state.rootSelectionIndex = Math.max(0, SCENES.findIndex((scene) => scene.id === "switch-work-item"));
2522
+ renderState(state);
2523
+ return;
2524
+ }
2525
+ state.changeId = choices[selectedIndex];
2526
+ state.lastStatus = `change -> ${state.changeId || "auto"}`;
2527
+ appendLogEvent(state, "switch-work-item", "change_switch", "ok", state.changeId || "auto");
2528
+ state.viewMode = "scene_list";
2529
+ state.activeSceneId = "switch-work-item";
2530
+ state.rootSelectionIndex = Math.max(0, SCENES.findIndex((scene) => scene.id === "switch-work-item"));
2531
+ renderState(state);
2532
+ return;
694
2533
  }
695
- renderState(state);
696
- return;
697
2534
  }
698
2535
  if (action === "toggle_help") {
699
2536
  state.helpVisible = !state.helpVisible;
700
2537
  renderState(state);
2538
+ return;
701
2539
  }
702
2540
  };
703
2541
 
@@ -707,10 +2545,11 @@ async function launchTui(options = {}) {
707
2545
  }
708
2546
 
709
2547
  module.exports = {
2548
+ SCENES,
710
2549
  COMMANDS,
711
- STAGES,
712
2550
  buildCommandArgs,
713
2551
  buildDisplayCommand,
2552
+ buildScreenLines,
714
2553
  formatCommandParameterHelp,
715
2554
  formatTuiHelp,
716
2555
  getExecutionCwd,
@@ -719,9 +2558,26 @@ module.exports = {
719
2558
  getDefaultLanguage,
720
2559
  getKeyAction,
721
2560
  launchTui,
2561
+ cycleThemeMode,
2562
+ detectTerminalThemeMode,
2563
+ enterAlternateScreen,
2564
+ exitAlternateScreen,
2565
+ hideCursor,
2566
+ showCursor,
722
2567
  normalizeEditedCommand,
723
2568
  parseEditedCommandInput,
2569
+ resolveActiveChangeId,
2570
+ resolveDailyLogPath,
2571
+ resolvePreImplementationResult,
2572
+ readPromptPresetSection,
2573
+ readVisualAssistVariant,
2574
+ applyManagedVisualAssistBlock,
2575
+ applyVisualAssistOverrides,
2576
+ extractVisualAssistSection,
2577
+ parseVisualAssistOverrides,
724
2578
  resolveLocalizedText,
2579
+ mapRouteToScene,
2580
+ stripAnsi,
725
2581
  teardownInput,
726
2582
  tokenizeCommandLine
727
2583
  };