@imricci/zaker 0.1.1 → 0.1.3

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 (121) hide show
  1. package/dist/commands/app-server.d.ts +67 -0
  2. package/dist/commands/app-server.js +601 -0
  3. package/dist/commands/app-server.js.map +1 -0
  4. package/dist/commands/audit.js +2 -1
  5. package/dist/commands/audit.js.map +1 -1
  6. package/dist/commands/build.d.ts +3 -0
  7. package/dist/commands/build.js +22 -0
  8. package/dist/commands/build.js.map +1 -0
  9. package/dist/commands/confirm.d.ts +0 -2
  10. package/dist/commands/confirm.js +2 -16
  11. package/dist/commands/confirm.js.map +1 -1
  12. package/dist/commands/dialog-handlers/auth.d.ts +3 -0
  13. package/dist/commands/dialog-handlers/auth.js +174 -0
  14. package/dist/commands/dialog-handlers/auth.js.map +1 -0
  15. package/dist/commands/dialog-handlers/basic.d.ts +7 -0
  16. package/dist/commands/dialog-handlers/basic.js +82 -0
  17. package/dist/commands/dialog-handlers/basic.js.map +1 -0
  18. package/dist/commands/dialog-handlers/bootstrap.d.ts +10 -0
  19. package/dist/commands/dialog-handlers/bootstrap.js +38 -0
  20. package/dist/commands/dialog-handlers/bootstrap.js.map +1 -0
  21. package/dist/commands/dialog-handlers/index.d.ts +2 -0
  22. package/dist/commands/dialog-handlers/index.js +6 -0
  23. package/dist/commands/dialog-handlers/index.js.map +1 -0
  24. package/dist/commands/dialog-handlers/message.d.ts +2 -0
  25. package/dist/commands/dialog-handlers/message.js +103 -0
  26. package/dist/commands/dialog-handlers/message.js.map +1 -0
  27. package/dist/commands/dialog-handlers/model.d.ts +2 -0
  28. package/dist/commands/dialog-handlers/model.js +168 -0
  29. package/dist/commands/dialog-handlers/model.js.map +1 -0
  30. package/dist/commands/dialog-handlers/new.d.ts +2 -0
  31. package/dist/commands/dialog-handlers/new.js +53 -0
  32. package/dist/commands/dialog-handlers/new.js.map +1 -0
  33. package/dist/commands/dialog-handlers/resume.d.ts +2 -0
  34. package/dist/commands/dialog-handlers/resume.js +25 -0
  35. package/dist/commands/dialog-handlers/resume.js.map +1 -0
  36. package/dist/commands/dialog-handlers/router.d.ts +11 -0
  37. package/dist/commands/dialog-handlers/router.js +112 -0
  38. package/dist/commands/dialog-handlers/router.js.map +1 -0
  39. package/dist/commands/dialog-handlers/run.d.ts +2 -0
  40. package/dist/commands/dialog-handlers/run.js +161 -0
  41. package/dist/commands/dialog-handlers/run.js.map +1 -0
  42. package/dist/commands/dialog-handlers/status.d.ts +2 -0
  43. package/dist/commands/dialog-handlers/status.js +13 -0
  44. package/dist/commands/dialog-handlers/status.js.map +1 -0
  45. package/dist/commands/dialog-handlers/types.d.ts +107 -0
  46. package/dist/commands/dialog-handlers/types.js +3 -0
  47. package/dist/commands/dialog-handlers/types.js.map +1 -0
  48. package/dist/commands/dialog.d.ts +92 -0
  49. package/dist/commands/dialog.js +1784 -236
  50. package/dist/commands/dialog.js.map +1 -1
  51. package/dist/commands/init.d.ts +3 -1
  52. package/dist/commands/init.js +6 -4
  53. package/dist/commands/init.js.map +1 -1
  54. package/dist/commands/plan.js +10 -6
  55. package/dist/commands/plan.js.map +1 -1
  56. package/dist/commands/run.js +8 -10
  57. package/dist/commands/run.js.map +1 -1
  58. package/dist/commands/status.js +6 -12
  59. package/dist/commands/status.js.map +1 -1
  60. package/dist/commands/tui-launcher.d.ts +1 -0
  61. package/dist/commands/tui-launcher.js +8 -0
  62. package/dist/commands/tui-launcher.js.map +1 -0
  63. package/dist/core/alignment-reply.d.ts +1 -0
  64. package/dist/core/alignment-reply.js +44 -0
  65. package/dist/core/alignment-reply.js.map +1 -0
  66. package/dist/core/checkpoint.js +3 -1
  67. package/dist/core/checkpoint.js.map +1 -1
  68. package/dist/core/planner.d.ts +16 -16
  69. package/dist/core/planner.js +3 -1
  70. package/dist/core/planner.js.map +1 -1
  71. package/dist/core/planning-prep.d.ts +12 -0
  72. package/dist/core/planning-prep.js +26 -0
  73. package/dist/core/planning-prep.js.map +1 -0
  74. package/dist/core/preflight.js +1 -2
  75. package/dist/core/preflight.js.map +1 -1
  76. package/dist/core/provider-onboarding.js +6 -11
  77. package/dist/core/provider-onboarding.js.map +1 -1
  78. package/dist/core/readonly-checkpoint.d.ts +2 -0
  79. package/dist/core/readonly-checkpoint.js +35 -0
  80. package/dist/core/readonly-checkpoint.js.map +1 -0
  81. package/dist/core/run-loop.js +3 -1
  82. package/dist/core/run-loop.js.map +1 -1
  83. package/dist/core/types.d.ts +20 -1
  84. package/dist/index.js +20 -6
  85. package/dist/index.js.map +1 -1
  86. package/dist/infra/artifact-schema.d.ts +25 -0
  87. package/dist/infra/artifact-schema.js +353 -0
  88. package/dist/infra/artifact-schema.js.map +1 -0
  89. package/dist/infra/config.d.ts +14 -1
  90. package/dist/infra/config.js +542 -22
  91. package/dist/infra/config.js.map +1 -1
  92. package/dist/infra/dependency-report.d.ts +5 -0
  93. package/dist/infra/dependency-report.js +22 -0
  94. package/dist/infra/dependency-report.js.map +1 -0
  95. package/dist/infra/dialog-session.d.ts +29 -0
  96. package/dist/infra/dialog-session.js +244 -0
  97. package/dist/infra/dialog-session.js.map +1 -0
  98. package/dist/infra/intent.js +63 -13
  99. package/dist/infra/intent.js.map +1 -1
  100. package/dist/infra/model-accounts.d.ts +22 -0
  101. package/dist/infra/model-accounts.js +172 -0
  102. package/dist/infra/model-accounts.js.map +1 -0
  103. package/dist/infra/model-catalog.d.ts +4 -1
  104. package/dist/infra/model-catalog.js +102 -27
  105. package/dist/infra/model-catalog.js.map +1 -1
  106. package/dist/infra/openai-codex-oauth.d.ts +18 -0
  107. package/dist/infra/openai-codex-oauth.js +267 -0
  108. package/dist/infra/openai-codex-oauth.js.map +1 -0
  109. package/dist/infra/provider-registry.d.ts +36 -0
  110. package/dist/infra/provider-registry.js +403 -0
  111. package/dist/infra/provider-registry.js.map +1 -0
  112. package/dist/infra/session-status.d.ts +6 -0
  113. package/dist/infra/session-status.js +34 -0
  114. package/dist/infra/session-status.js.map +1 -0
  115. package/dist/infra/tui-utils.d.ts +6 -0
  116. package/dist/infra/tui-utils.js +163 -0
  117. package/dist/infra/tui-utils.js.map +1 -0
  118. package/dist/infra/tui-view.d.ts +44 -0
  119. package/dist/infra/tui-view.js +314 -0
  120. package/dist/infra/tui-view.js.map +1 -0
  121. package/package.json +4 -1
@@ -3,23 +3,217 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildCommandCompletionCandidates = buildCommandCompletionCandidates;
7
+ exports.resolveSlashMenuCandidates = resolveSlashMenuCandidates;
8
+ exports.createTaskBoardState = createTaskBoardState;
9
+ exports.buildTuiFramePreview = buildTuiFramePreview;
10
+ exports.normalizeInteractiveInputChunk = normalizeInteractiveInputChunk;
11
+ exports.parseMouseWheelDeltas = parseMouseWheelDeltas;
12
+ exports.shouldAppendInteractiveTextChunk = shouldAppendInteractiveTextChunk;
13
+ exports.shouldSubmitOnUnparsedLineBreak = shouldSubmitOnUnparsedLineBreak;
14
+ exports.isPlainSubmitSequence = isPlainSubmitSequence;
15
+ exports.resolveAutocompleteCommandCandidatesForTest = resolveAutocompleteCommandCandidatesForTest;
16
+ exports.renderCommandMenuWithSelectListForTest = renderCommandMenuWithSelectListForTest;
17
+ exports.moveCommandSelectionWithSelectListForTest = moveCommandSelectionWithSelectListForTest;
18
+ exports.applyInteractiveBufferEdit = applyInteractiveBufferEdit;
19
+ exports.runInteractiveEditorKeySequenceForTest = runInteractiveEditorKeySequenceForTest;
20
+ exports.shouldTreatCtrlDAsExit = shouldTreatCtrlDAsExit;
21
+ exports.resolveMenuEnterOutcome = resolveMenuEnterOutcome;
22
+ exports.resolveInteractiveLoginCommand = resolveInteractiveLoginCommand;
23
+ exports.handleDialogInput = handleDialogInput;
6
24
  exports.runDialogueSession = runDialogueSession;
7
25
  exports.registerDialogCommand = registerDialogCommand;
8
- const node_readline_1 = __importDefault(require("node:readline"));
9
26
  const chalk_1 = __importDefault(require("chalk"));
27
+ const pi_tui_1 = require("@mariozechner/pi-tui");
10
28
  const verdict_panel_1 = require("../core/verdict-panel");
11
29
  const config_1 = require("../infra/config");
30
+ const dialog_session_1 = require("../infra/dialog-session");
31
+ const openai_codex_oauth_1 = require("../infra/openai-codex-oauth");
32
+ const model_accounts_1 = require("../infra/model-accounts");
33
+ const tui_view_1 = require("../infra/tui-view");
34
+ const provider_registry_1 = require("../infra/provider-registry");
12
35
  const intent_1 = require("../infra/intent");
36
+ const session_status_1 = require("../infra/session-status");
37
+ const dialog_handlers_1 = require("./dialog-handlers");
13
38
  const init_1 = require("./init");
14
39
  const run_1 = require("./run");
15
40
  const MAX_DIALOG_HISTORY = 200;
16
41
  const MAX_DIALOG_ROUNDS = 50;
17
42
  const MAX_SCREEN_HISTORY = 14;
43
+ const ESC_DOUBLE_PRESS_WINDOW_MS = 700;
44
+ const MOUSE_REPORT_ENABLE = "\u001b[?1002h\u001b[?1006h";
45
+ const MOUSE_REPORT_DISABLE = "\u001b[?1002l\u001b[?1006l";
46
+ const SGR_MOUSE_EVENT_PATTERN = /\u001b\[<(\d+);(\d+);(\d+)([mM])/g;
47
+ const BASE_COMMAND_COMPLETION_CANDIDATES = [
48
+ "/init",
49
+ "/new",
50
+ "/resume",
51
+ "/model",
52
+ "/login",
53
+ "/logout",
54
+ "/build",
55
+ "/run",
56
+ "/edit ",
57
+ "/status",
58
+ "/debug ",
59
+ "/help",
60
+ "/exit"
61
+ ];
62
+ const LOGOUT_SECOND_LEVEL_CANDIDATES = (0, provider_registry_1.buildLogoutCommandTemplates)();
63
+ const TOP_LEVEL_SUBMENU_COMMANDS = new Set(["/resume", "/model", "/login", "/logout"]);
64
+ const EMPTY_MODEL_MENU_SNAPSHOT = {
65
+ providers: [],
66
+ providerCommands: [],
67
+ directModelCommands: [],
68
+ providerModelCommands: {}
69
+ };
70
+ const INTERACTIVE_COMMAND_MENU_MAX_VISIBLE = 5;
71
+ const INTERACTIVE_COMMAND_MENU_MIN_WIDTH = 24;
72
+ const INTERACTIVE_COMMAND_MENU_HORIZONTAL_PADDING = 2;
73
+ function isTopLevelSubmenuCommand(command) {
74
+ return TOP_LEVEL_SUBMENU_COMMANDS.has(command.trim());
75
+ }
76
+ function isLoginProviderSubmenuCommand(command) {
77
+ const tokens = command.trim().split(/\s+/).filter(Boolean);
78
+ return tokens.length === 2 && tokens[0] === "/login" && (0, provider_registry_1.isLoginProviderToken)(tokens[1] || "");
79
+ }
80
+ function isModelProviderSubmenuCommand(command, modelMenu) {
81
+ const tokens = command.trim().split(/\s+/).filter(Boolean);
82
+ return (tokens.length === 2 &&
83
+ tokens[0] === "/model" &&
84
+ (0, model_accounts_1.isModelProviderToken)(tokens[1] || "", modelMenu));
85
+ }
86
+ function uniqueCommandCandidates(values) {
87
+ const seen = new Set();
88
+ const ordered = [];
89
+ for (const value of values) {
90
+ if (!value.trim() || seen.has(value)) {
91
+ continue;
92
+ }
93
+ seen.add(value);
94
+ ordered.push(value);
95
+ }
96
+ return ordered;
97
+ }
98
+ function buildCommandCompletionCandidates() {
99
+ return uniqueCommandCandidates([...BASE_COMMAND_COMPLETION_CANDIDATES]);
100
+ }
101
+ function buildResumeSecondLevelCandidates(sessionIds = []) {
102
+ const resumeCandidates = sessionIds
103
+ .map((id) => id.trim())
104
+ .filter((id) => /^[a-zA-Z0-9_-]{1,40}$/.test(id))
105
+ .map((id) => `/resume ${id}`);
106
+ return uniqueCommandCandidates(["/resume", ...resumeCandidates]);
107
+ }
108
+ function resolveModelSlashCandidates(buffer, modelMenu) {
109
+ const normalized = buffer.trimEnd();
110
+ if (normalized === "/model" && !buffer.endsWith(" ")) {
111
+ return ["/model"];
112
+ }
113
+ if (!buffer.startsWith("/model ")) {
114
+ return [];
115
+ }
116
+ if (modelMenu.providers.length === 0) {
117
+ return ["/model"];
118
+ }
119
+ if (modelMenu.providers.length === 1) {
120
+ const commands = modelMenu.directModelCommands;
121
+ return commands.length > 0 ? commands : ["/model"];
122
+ }
123
+ const tokens = normalized.split(/\s+/).filter(Boolean);
124
+ if (tokens.length <= 1) {
125
+ return modelMenu.providerCommands;
126
+ }
127
+ const providerToken = tokens[1] || "";
128
+ if (tokens.length === 2) {
129
+ if (buffer.endsWith(" ") && (0, model_accounts_1.isModelProviderToken)(providerToken, modelMenu)) {
130
+ return modelMenu.providerModelCommands[providerToken] || [];
131
+ }
132
+ return modelMenu.providerCommands;
133
+ }
134
+ return modelMenu.providerModelCommands[providerToken] || [];
135
+ }
136
+ function resolveSlashMenuCandidates(buffer, sessionIds = [], modelMenu = EMPTY_MODEL_MENU_SNAPSHOT) {
137
+ if (!buffer.startsWith("/")) {
138
+ return [];
139
+ }
140
+ const normalized = buffer.trimEnd();
141
+ if (buffer.startsWith("/resume ")) {
142
+ return buildResumeSecondLevelCandidates(sessionIds);
143
+ }
144
+ if (buffer.startsWith("/model")) {
145
+ return resolveModelSlashCandidates(buffer, modelMenu);
146
+ }
147
+ if (buffer.startsWith("/login ")) {
148
+ const tokens = normalized.split(/\s+/).filter(Boolean);
149
+ if (tokens.length <= 1) {
150
+ return (0, provider_registry_1.buildLoginProviderCommandTemplates)();
151
+ }
152
+ const providerToken = tokens[1] ?? "";
153
+ if (tokens.length === 2) {
154
+ if (buffer.endsWith(" ") && (0, provider_registry_1.isLoginProviderToken)(providerToken)) {
155
+ return (0, provider_registry_1.buildLoginModeCommandTemplates)(providerToken);
156
+ }
157
+ return (0, provider_registry_1.buildLoginProviderCommandTemplates)();
158
+ }
159
+ return (0, provider_registry_1.buildLoginModeCommandTemplates)(providerToken);
160
+ }
161
+ if (buffer.startsWith("/logout ")) {
162
+ return LOGOUT_SECOND_LEVEL_CANDIDATES;
163
+ }
164
+ if (isTopLevelSubmenuCommand(normalized)) {
165
+ return buildCommandCompletionCandidates();
166
+ }
167
+ return buildCommandCompletionCandidates();
168
+ }
169
+ function createTaskBoardState() {
170
+ return {
171
+ run_stage: "IDLE",
172
+ run_status: "IDLE",
173
+ verification_status: "PENDING",
174
+ verification_passed: 0,
175
+ verification_total: 0,
176
+ risk_hit: "-",
177
+ audit_verdict: "PENDING",
178
+ audit_reason: "-",
179
+ audit_conclusion: "-",
180
+ retry_allowed: "-",
181
+ budget_plan_calls: 0,
182
+ budget_audit_calls: 0,
183
+ budget_challenge_used: 0
184
+ };
185
+ }
186
+ function resetBoard(board) {
187
+ const fresh = createTaskBoardState();
188
+ board.run_stage = fresh.run_stage;
189
+ board.run_status = fresh.run_status;
190
+ board.verification_status = fresh.verification_status;
191
+ board.verification_passed = fresh.verification_passed;
192
+ board.verification_total = fresh.verification_total;
193
+ board.risk_hit = fresh.risk_hit;
194
+ board.audit_verdict = fresh.audit_verdict;
195
+ board.audit_reason = fresh.audit_reason;
196
+ board.audit_conclusion = fresh.audit_conclusion;
197
+ board.retry_allowed = fresh.retry_allowed;
198
+ board.budget_plan_calls = fresh.budget_plan_calls;
199
+ board.budget_audit_calls = fresh.budget_audit_calls;
200
+ board.budget_challenge_used = fresh.budget_challenge_used;
201
+ }
18
202
  function now() {
19
203
  return new Date().toISOString();
20
204
  }
205
+ function isExitLifecycleNoise(entry) {
206
+ const content = entry.content.trim().toLowerCase();
207
+ if (entry.role === "user" && entry.kind === "command" && content === "/exit") {
208
+ return true;
209
+ }
210
+ if (entry.role === "system" && entry.kind === "status" && content === "dialog ended") {
211
+ return true;
212
+ }
213
+ return false;
214
+ }
21
215
  function helpText() {
22
- return "commands: /init | /confirm | /run | /edit <text> | /status | /debug on|off | /help | /exit";
216
+ return "commands: /init | /new [id] | /resume [id] | /model ... | /login ... | /logout ... | /build | /run | /edit <text> | /status | /debug on|off | /help | /exit";
23
217
  }
24
218
  function compactLine(input, maxLength = 120) {
25
219
  const singleLine = input.replace(/\s+/g, " ").trim();
@@ -28,8 +222,1029 @@ function compactLine(input, maxLength = 120) {
28
222
  }
29
223
  return `${singleLine.slice(0, maxLength - 3)}...`;
30
224
  }
31
- function divider(width = 80) {
32
- return "─".repeat(width);
225
+ function formatTuiModelInfo(config) {
226
+ const provider = config.model.provider?.trim() || "-";
227
+ const model = config.model.planner_model?.trim() || "auto";
228
+ return `${provider}/${model}`;
229
+ }
230
+ function buildTuiFramePreview(params, viewport = {}) {
231
+ return (0, tui_view_1.buildTuiFramePreview)({
232
+ ...params,
233
+ helpText: params.helpText ?? helpText()
234
+ }, viewport);
235
+ }
236
+ class PiFrameComponent {
237
+ readSnapshot;
238
+ readRows;
239
+ focused = false;
240
+ constructor(readSnapshot, readRows) {
241
+ this.readSnapshot = readSnapshot;
242
+ this.readRows = readRows;
243
+ }
244
+ render(width) {
245
+ const snapshot = this.readSnapshot();
246
+ if (!snapshot) {
247
+ return [];
248
+ }
249
+ return (0, tui_view_1.buildTwoPaneFrameLines)(snapshot, Math.max(width, tui_view_1.MIN_TUI_COLUMNS), Math.max(this.readRows(), tui_view_1.MIN_TUI_ROWS));
250
+ }
251
+ invalidate() {
252
+ // no-op
253
+ }
254
+ }
255
+ async function loadPiTuiModule() {
256
+ const dynamicImport = new Function("specifier", "return import(specifier);");
257
+ const loaded = await dynamicImport("@mariozechner/pi-tui");
258
+ return loaded;
259
+ }
260
+ async function createPiFrameRenderer(inputSource) {
261
+ let tui = null;
262
+ let detachInputListener = null;
263
+ try {
264
+ const module = await loadPiTuiModule();
265
+ const terminal = new pi_tui_1.ProcessTerminal();
266
+ tui = new module.TUI(terminal, true);
267
+ let snapshot = null;
268
+ const component = new PiFrameComponent(() => snapshot, () => process.stdout.rows || tui_view_1.MIN_TUI_ROWS);
269
+ tui.addChild(component);
270
+ tui.setFocus?.(component);
271
+ detachInputListener = tui.addInputListener((data) => {
272
+ inputSource.emitSequence(data);
273
+ return undefined;
274
+ });
275
+ tui.start();
276
+ const activeTui = tui;
277
+ activeTui.terminal?.write?.(MOUSE_REPORT_ENABLE);
278
+ return {
279
+ render(params) {
280
+ snapshot = {
281
+ ...params,
282
+ cursorMarker: pi_tui_1.CURSOR_MARKER
283
+ };
284
+ activeTui.requestRender();
285
+ },
286
+ async dispose() {
287
+ activeTui.terminal?.write?.(MOUSE_REPORT_DISABLE);
288
+ activeTui.setFocus?.(null);
289
+ detachInputListener?.();
290
+ inputSource.clear();
291
+ await activeTui.terminal?.drainInput?.(1000);
292
+ activeTui.stop();
293
+ }
294
+ };
295
+ }
296
+ catch (error) {
297
+ try {
298
+ detachInputListener?.();
299
+ }
300
+ catch {
301
+ // ignore input listener teardown failures during fallback
302
+ }
303
+ try {
304
+ tui?.terminal?.write?.(MOUSE_REPORT_DISABLE);
305
+ }
306
+ catch {
307
+ // ignore mouse-report teardown failures during fallback
308
+ }
309
+ try {
310
+ await tui?.terminal?.drainInput?.(1000);
311
+ }
312
+ catch {
313
+ // ignore terminal drain failures during fallback
314
+ }
315
+ try {
316
+ tui?.stop();
317
+ }
318
+ catch {
319
+ // ignore terminal teardown failures during fallback
320
+ }
321
+ const message = error instanceof Error ? error.message : String(error);
322
+ throw new Error(`failed to initialize pi-tui renderer: ${message}`);
323
+ }
324
+ }
325
+ const NON_TEXT_KEY_NAMES = new Set([
326
+ "up",
327
+ "down",
328
+ "left",
329
+ "right",
330
+ "pageup",
331
+ "pagedown",
332
+ "home",
333
+ "end",
334
+ "insert",
335
+ "delete",
336
+ "escape",
337
+ "tab",
338
+ "return",
339
+ "enter",
340
+ "backspace"
341
+ ]);
342
+ function normalizeInteractiveInputChunk(chunk) {
343
+ if (!chunk) {
344
+ return { text: "", hasLineBreak: false };
345
+ }
346
+ const unwrapped = chunk.replace(/\u001b\[200~/g, "").replace(/\u001b\[201~/g, "");
347
+ const withoutAnsi = unwrapped
348
+ .replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, "")
349
+ .replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
350
+ .replace(/\u001bO[ -~]/g, "");
351
+ const hasLineBreak = /[\r\n]/.test(withoutAnsi);
352
+ let text = withoutAnsi
353
+ .replace(/[\r\n]+/g, " ")
354
+ .replace(/[\u0000-\u0008\u000B-\u001F\u007F]/g, "")
355
+ .replace(/\uFF0F/g, "/")
356
+ .replace(/\u3000/g, " ");
357
+ if (hasLineBreak) {
358
+ text = text.replace(/\s+$/g, "");
359
+ }
360
+ return { text, hasLineBreak };
361
+ }
362
+ function parseMouseWheelDeltas(chunk) {
363
+ if (!chunk.includes("\u001b[<")) {
364
+ return [];
365
+ }
366
+ SGR_MOUSE_EVENT_PATTERN.lastIndex = 0;
367
+ const deltas = [];
368
+ let match = SGR_MOUSE_EVENT_PATTERN.exec(chunk);
369
+ while (match) {
370
+ const code = Number(match[1] || "");
371
+ if (Number.isFinite(code) && (code & 64) === 64) {
372
+ const isDown = (code & 1) === 1;
373
+ deltas.push(isDown ? -1 : 1);
374
+ }
375
+ match = SGR_MOUSE_EVENT_PATTERN.exec(chunk);
376
+ }
377
+ return deltas;
378
+ }
379
+ function shouldAppendInteractiveTextChunk(chunk, key, normalizedChunk) {
380
+ if (!normalizedChunk.text) {
381
+ return false;
382
+ }
383
+ if (key.ctrl || key.meta) {
384
+ return false;
385
+ }
386
+ const keyName = (key.name || "").toLowerCase();
387
+ if (NON_TEXT_KEY_NAMES.has(keyName)) {
388
+ return false;
389
+ }
390
+ if (chunk.includes("\u001b") && !isBracketedPasteSequence(chunk) && keyName.length > 0) {
391
+ return false;
392
+ }
393
+ if (keyName && keyName.length > 1 && keyName !== "space") {
394
+ return false;
395
+ }
396
+ return true;
397
+ }
398
+ function shouldSubmitOnUnparsedLineBreak(key, normalizedChunk) {
399
+ return !key.name && normalizedChunk.hasLineBreak && normalizedChunk.text.trim().length === 0;
400
+ }
401
+ function isPlainSubmitSequence(sequence) {
402
+ return sequence === "\r" || sequence === "\n" || sequence === "\r\n";
403
+ }
404
+ function isBracketedPasteSequence(sequence) {
405
+ return sequence.startsWith("\u001b[200~") && sequence.includes("\u001b[201~");
406
+ }
407
+ function isUnparsedControlSequence(sequence, parsedKey) {
408
+ if (parsedKey || isBracketedPasteSequence(sequence)) {
409
+ return false;
410
+ }
411
+ if (!sequence.includes("\u001b")) {
412
+ return false;
413
+ }
414
+ const normalized = normalizeInteractiveInputChunk(sequence);
415
+ return normalized.text.length === 0;
416
+ }
417
+ function normalizeParsedKeyName(name) {
418
+ const normalized = (name || "").toLowerCase();
419
+ if (normalized === "pageup") {
420
+ return "pageup";
421
+ }
422
+ if (normalized === "pagedown") {
423
+ return "pagedown";
424
+ }
425
+ if (normalized === "esc") {
426
+ return "escape";
427
+ }
428
+ if (normalized === "enter") {
429
+ return "enter";
430
+ }
431
+ return normalized;
432
+ }
433
+ function toReadlineKey(parsedKey) {
434
+ const key = {
435
+ name: undefined,
436
+ ctrl: false,
437
+ meta: false,
438
+ shift: false
439
+ };
440
+ if (!parsedKey) {
441
+ return key;
442
+ }
443
+ const tokens = parsedKey.split("+").filter((token) => token.length > 0);
444
+ const baseToken = tokens.pop();
445
+ key.name = normalizeParsedKeyName(baseToken);
446
+ key.ctrl = tokens.includes("ctrl");
447
+ key.meta = tokens.includes("alt");
448
+ key.shift = tokens.includes("shift");
449
+ return key;
450
+ }
451
+ class DialogInputBuffer {
452
+ listeners = new Set();
453
+ subscribe(listener) {
454
+ this.listeners.add(listener);
455
+ return () => {
456
+ this.listeners.delete(listener);
457
+ };
458
+ }
459
+ clear() {
460
+ this.listeners.clear();
461
+ }
462
+ emitSequence(sequence) {
463
+ if ((0, pi_tui_1.isKeyRelease)(sequence)) {
464
+ return;
465
+ }
466
+ const parsedKey = (0, pi_tui_1.parseKey)(sequence);
467
+ const event = {
468
+ sequence,
469
+ parsedKey,
470
+ key: toReadlineKey(parsedKey)
471
+ };
472
+ for (const listener of this.listeners) {
473
+ listener(event);
474
+ }
475
+ }
476
+ }
477
+ function buildSlashAutocompleteContext(resolveCandidates) {
478
+ const templateByName = new Map();
479
+ const topLevelCandidates = buildCommandCompletionCandidates();
480
+ for (const candidate of topLevelCandidates) {
481
+ const trimmed = candidate.trimEnd();
482
+ const token = trimmed.split(/\s+/).filter(Boolean)[0] || "";
483
+ if (!token.startsWith("/")) {
484
+ continue;
485
+ }
486
+ const name = token.slice(1);
487
+ if (!name) {
488
+ continue;
489
+ }
490
+ const previous = templateByName.get(name);
491
+ if (!previous || (candidate.endsWith(" ") && !previous.endsWith(" "))) {
492
+ templateByName.set(name, candidate);
493
+ }
494
+ }
495
+ const commands = [];
496
+ for (const [name, template] of templateByName.entries()) {
497
+ commands.push({
498
+ name,
499
+ description: (0, tui_view_1.describeCommand)(template),
500
+ getArgumentCompletions: (argumentPrefix) => {
501
+ const query = argumentPrefix.length > 0 ? `/${name} ${argumentPrefix}` : `/${name} `;
502
+ const items = resolveCandidates(query)
503
+ .filter((command) => command.startsWith(`/${name}`))
504
+ .map((command) => ({
505
+ value: command,
506
+ label: command,
507
+ description: (0, tui_view_1.describeCommand)(command)
508
+ }));
509
+ return items.length > 0 ? items : null;
510
+ }
511
+ });
512
+ }
513
+ return {
514
+ provider: new pi_tui_1.CombinedAutocompleteProvider(commands, process.cwd(), null),
515
+ templateByName
516
+ };
517
+ }
518
+ function normalizeAutocompleteCommandItem(item, prefix, templateByName) {
519
+ if (item.value.startsWith("/")) {
520
+ return item.value;
521
+ }
522
+ if (prefix.startsWith("/")) {
523
+ return templateByName.get(item.value) ?? `/${item.value}`;
524
+ }
525
+ return item.value;
526
+ }
527
+ function resolveAutocompleteCommandCandidates(buffer, cursorIndex, resolveCandidates) {
528
+ if (!buffer.startsWith("/")) {
529
+ return [];
530
+ }
531
+ const safeCursorIndex = Math.max(0, Math.min(cursorIndex, buffer.length));
532
+ const beforeCursor = buffer.slice(0, safeCursorIndex);
533
+ if (!beforeCursor.startsWith("/")) {
534
+ return [];
535
+ }
536
+ const { provider, templateByName } = buildSlashAutocompleteContext(resolveCandidates);
537
+ const suggestions = provider.getSuggestions([buffer], 0, safeCursorIndex);
538
+ if (!suggestions || suggestions.items.length === 0) {
539
+ return [];
540
+ }
541
+ const normalized = suggestions.items
542
+ .map((item) => normalizeAutocompleteCommandItem(item, suggestions.prefix, templateByName))
543
+ .filter((item) => Boolean(item))
544
+ .filter((item) => item.startsWith(beforeCursor));
545
+ return uniqueCommandCandidates(normalized);
546
+ }
547
+ function resolveAutocompleteCommandCandidatesForTest(buffer, cursorIndex, resolveCandidates) {
548
+ return resolveAutocompleteCommandCandidates(buffer, cursorIndex, resolveCandidates);
549
+ }
550
+ function resolveCompletion(buffer, cursorIndex, resolveCandidates) {
551
+ const candidates = resolveAutocompleteCommandCandidates(buffer, cursorIndex, resolveCandidates);
552
+ if (candidates.length === 1) {
553
+ return candidates[0];
554
+ }
555
+ if (candidates.length > 1) {
556
+ return undefined;
557
+ }
558
+ const fallbackCandidates = resolveCandidates(buffer).filter((command) => command.startsWith(buffer));
559
+ if (fallbackCandidates.length === 1) {
560
+ return fallbackCandidates[0];
561
+ }
562
+ return undefined;
563
+ }
564
+ function resolveCommandMenu(buffer, cursorIndex, selectedIndex, resolveCandidates) {
565
+ if (!buffer.startsWith("/")) {
566
+ return { active: false, items: [], selectedIndex: 0 };
567
+ }
568
+ const providerCandidates = resolveAutocompleteCommandCandidates(buffer, cursorIndex, resolveCandidates);
569
+ const items = providerCandidates.length > 0
570
+ ? providerCandidates
571
+ : resolveCandidates(buffer).filter((command) => command.startsWith(buffer));
572
+ if (items.length === 0) {
573
+ return { active: false, items: [], selectedIndex: 0 };
574
+ }
575
+ const normalizedSelected = Math.max(0, Math.min(selectedIndex, items.length - 1));
576
+ return {
577
+ active: true,
578
+ items,
579
+ selectedIndex: normalizedSelected
580
+ };
581
+ }
582
+ function createCommandSelectList(commandMenu) {
583
+ const selectItems = commandMenu.items.map((command) => ({
584
+ value: command,
585
+ label: command,
586
+ description: (0, tui_view_1.describeCommand)(command)
587
+ }));
588
+ const picker = new pi_tui_1.SelectList(selectItems, Math.min(selectItems.length, INTERACTIVE_COMMAND_MENU_MAX_VISIBLE), {
589
+ selectedPrefix: IDENTITY_STYLE,
590
+ selectedText: IDENTITY_STYLE,
591
+ description: IDENTITY_STYLE,
592
+ scrollInfo: IDENTITY_STYLE,
593
+ noMatch: IDENTITY_STYLE
594
+ });
595
+ picker.setSelectedIndex(Math.max(0, Math.min(commandMenu.selectedIndex, selectItems.length - 1)));
596
+ return picker;
597
+ }
598
+ function renderCommandMenuWithSelectList(commandMenu) {
599
+ if (!commandMenu.active || commandMenu.items.length === 0) {
600
+ return commandMenu;
601
+ }
602
+ const picker = createCommandSelectList(commandMenu);
603
+ const menuWidth = Math.max((process.stdout.columns || tui_view_1.MIN_TUI_COLUMNS) - INTERACTIVE_COMMAND_MENU_HORIZONTAL_PADDING, INTERACTIVE_COMMAND_MENU_MIN_WIDTH);
604
+ return {
605
+ active: true,
606
+ items: picker.render(menuWidth),
607
+ selectedIndex: 0,
608
+ renderMode: "raw"
609
+ };
610
+ }
611
+ function moveCommandSelectionWithSelectList(commandMenu, sequence) {
612
+ if (!commandMenu.active || commandMenu.items.length === 0) {
613
+ return commandMenu.selectedIndex;
614
+ }
615
+ const picker = createCommandSelectList(commandMenu);
616
+ let selectedValue = commandMenu.items[commandMenu.selectedIndex] ?? commandMenu.items[0];
617
+ picker.onSelectionChange = (item) => {
618
+ selectedValue = item.value;
619
+ };
620
+ picker.handleInput(sequence);
621
+ const nextIndex = commandMenu.items.findIndex((command) => command === selectedValue);
622
+ if (nextIndex < 0) {
623
+ return commandMenu.selectedIndex;
624
+ }
625
+ return nextIndex;
626
+ }
627
+ function renderCommandMenuWithSelectListForTest(items, selectedIndex = 0, width = tui_view_1.MIN_TUI_COLUMNS) {
628
+ const commandMenu = {
629
+ active: true,
630
+ items: [...items],
631
+ selectedIndex: Math.max(0, Math.min(selectedIndex, Math.max(0, items.length - 1)))
632
+ };
633
+ if (commandMenu.items.length === 0) {
634
+ return [];
635
+ }
636
+ const picker = createCommandSelectList(commandMenu);
637
+ const menuWidth = Math.max(width, INTERACTIVE_COMMAND_MENU_MIN_WIDTH);
638
+ return picker.render(menuWidth);
639
+ }
640
+ function moveCommandSelectionWithSelectListForTest(items, selectedIndex, sequence) {
641
+ if (items.length === 0) {
642
+ return 0;
643
+ }
644
+ return moveCommandSelectionWithSelectList({
645
+ active: true,
646
+ items: [...items],
647
+ selectedIndex: Math.max(0, Math.min(selectedIndex, items.length - 1))
648
+ }, sequence);
649
+ }
650
+ function applyInteractiveBufferEdit(state, action) {
651
+ const buffer = state.buffer;
652
+ const cursorIndex = Math.max(0, Math.min(state.cursorIndex, buffer.length));
653
+ if (action.type === "move_left") {
654
+ return { buffer, cursorIndex: Math.max(0, cursorIndex - 1) };
655
+ }
656
+ if (action.type === "move_right") {
657
+ return { buffer, cursorIndex: Math.min(buffer.length, cursorIndex + 1) };
658
+ }
659
+ if (action.type === "backspace") {
660
+ if (cursorIndex <= 0) {
661
+ return { buffer, cursorIndex };
662
+ }
663
+ return {
664
+ buffer: `${buffer.slice(0, cursorIndex - 1)}${buffer.slice(cursorIndex)}`,
665
+ cursorIndex: cursorIndex - 1
666
+ };
667
+ }
668
+ if (action.type === "delete") {
669
+ if (cursorIndex >= buffer.length) {
670
+ return { buffer, cursorIndex };
671
+ }
672
+ return {
673
+ buffer: `${buffer.slice(0, cursorIndex)}${buffer.slice(cursorIndex + 1)}`,
674
+ cursorIndex
675
+ };
676
+ }
677
+ if (!action.text) {
678
+ return { buffer, cursorIndex };
679
+ }
680
+ return {
681
+ buffer: `${buffer.slice(0, cursorIndex)}${action.text}${buffer.slice(cursorIndex)}`,
682
+ cursorIndex: cursorIndex + action.text.length
683
+ };
684
+ }
685
+ const IDENTITY_STYLE = (value) => value;
686
+ const INPUT_EDITOR_THEME = {
687
+ borderColor: IDENTITY_STYLE,
688
+ selectList: {
689
+ selectedPrefix: IDENTITY_STYLE,
690
+ selectedText: IDENTITY_STYLE,
691
+ description: IDENTITY_STYLE,
692
+ scrollInfo: IDENTITY_STYLE,
693
+ noMatch: IDENTITY_STYLE
694
+ }
695
+ };
696
+ function toAbsoluteCursorIndex(lines, cursorLine, cursorCol) {
697
+ const safeLines = lines.length > 0 ? lines : [""];
698
+ const clampedLine = Math.max(0, Math.min(cursorLine, safeLines.length - 1));
699
+ let index = 0;
700
+ for (let lineIndex = 0; lineIndex < clampedLine; lineIndex += 1) {
701
+ index += (safeLines[lineIndex] || "").length + 1;
702
+ }
703
+ const line = safeLines[clampedLine] || "";
704
+ index += Math.max(0, Math.min(cursorCol, line.length));
705
+ return index;
706
+ }
707
+ function createInteractiveEditorBridge(onSubmit) {
708
+ const tuiStub = {
709
+ terminal: {
710
+ get rows() {
711
+ return process.stdout.rows || tui_view_1.MIN_TUI_ROWS;
712
+ },
713
+ get columns() {
714
+ return process.stdout.columns || tui_view_1.MIN_TUI_COLUMNS;
715
+ }
716
+ },
717
+ requestRender() {
718
+ return undefined;
719
+ }
720
+ };
721
+ const editor = new pi_tui_1.Editor(tuiStub, INPUT_EDITOR_THEME);
722
+ editor.onSubmit = onSubmit;
723
+ const getSnapshot = () => {
724
+ const buffer = editor.getText();
725
+ const cursor = editor.getCursor();
726
+ const lines = editor.getLines();
727
+ return {
728
+ buffer,
729
+ cursorIndex: toAbsoluteCursorIndex(lines, cursor.line, cursor.col)
730
+ };
731
+ };
732
+ return {
733
+ getSnapshot,
734
+ setText(value) {
735
+ editor.setText(value);
736
+ },
737
+ insertText(value) {
738
+ if (!value) {
739
+ return;
740
+ }
741
+ editor.insertTextAtCursor(value);
742
+ },
743
+ handleInput(sequence) {
744
+ editor.handleInput(sequence);
745
+ return getSnapshot();
746
+ }
747
+ };
748
+ }
749
+ function runInteractiveEditorKeySequenceForTest(initialBuffer, sequences) {
750
+ const submissions = [];
751
+ const bridge = createInteractiveEditorBridge((value) => {
752
+ submissions.push(value);
753
+ });
754
+ bridge.setText(initialBuffer);
755
+ let snapshot = bridge.getSnapshot();
756
+ for (const sequence of sequences) {
757
+ snapshot = bridge.handleInput(sequence);
758
+ }
759
+ return {
760
+ buffer: snapshot.buffer,
761
+ cursorIndex: snapshot.cursorIndex,
762
+ submissions
763
+ };
764
+ }
765
+ function shouldTreatCtrlDAsExit(buffer, key) {
766
+ return key.ctrl === true && key.name === "d" && buffer.length === 0;
767
+ }
768
+ function resolveMenuEnterOutcome(buffer, selected, modelMenu = EMPTY_MODEL_MENU_SNAPSHOT) {
769
+ if (!selected) {
770
+ return { buffer, submit: true };
771
+ }
772
+ const selectedTrim = selected.trimEnd();
773
+ const currentTrim = buffer.trim();
774
+ const requiresArgument = selected.endsWith(" ");
775
+ if (isTopLevelSubmenuCommand(selectedTrim)) {
776
+ if (buffer !== `${selectedTrim} `) {
777
+ return { buffer: `${selectedTrim} `, submit: false };
778
+ }
779
+ return { buffer, submit: false };
780
+ }
781
+ if (isLoginProviderSubmenuCommand(selectedTrim)) {
782
+ if (buffer !== `${selectedTrim} `) {
783
+ return { buffer: `${selectedTrim} `, submit: false };
784
+ }
785
+ return { buffer, submit: false };
786
+ }
787
+ if (isModelProviderSubmenuCommand(selectedTrim, modelMenu)) {
788
+ if (buffer !== `${selectedTrim} `) {
789
+ return { buffer: `${selectedTrim} `, submit: false };
790
+ }
791
+ return { buffer, submit: false };
792
+ }
793
+ if (currentTrim !== selectedTrim) {
794
+ if (requiresArgument) {
795
+ return { buffer: selected, submit: false };
796
+ }
797
+ return { buffer: selected, submit: true };
798
+ }
799
+ return { buffer, submit: true };
800
+ }
801
+ function parseLoginCommandParts(line) {
802
+ const tokens = line.trim().split(/\s+/).filter(Boolean);
803
+ if (tokens[0] !== "/login") {
804
+ return null;
805
+ }
806
+ const providerToken = tokens[1]?.toLowerCase();
807
+ if (!providerToken) {
808
+ return null;
809
+ }
810
+ const modeToken = tokens[2]?.toLowerCase();
811
+ const hasModeToken = modeToken === "oauth" || modeToken === "api_key";
812
+ if (providerToken === "http") {
813
+ return {
814
+ provider: "http",
815
+ authMode: "api_key",
816
+ apiKey: hasModeToken ? tokens[3] : tokens[2],
817
+ endpointOrHost: hasModeToken ? tokens[4] : tokens[3]
818
+ };
819
+ }
820
+ if (providerToken === "ollama") {
821
+ return {
822
+ provider: "ollama",
823
+ authMode: "api_key",
824
+ apiKey: tokens[2],
825
+ endpointOrHost: tokens[2]
826
+ };
827
+ }
828
+ let provider;
829
+ try {
830
+ provider = (0, provider_registry_1.toLoginProviderId)(providerToken);
831
+ }
832
+ catch {
833
+ return null;
834
+ }
835
+ const authMode = modeToken === "oauth" ? "oauth" : "api_key";
836
+ if (providerToken === "openai" && authMode === "oauth") {
837
+ provider = "openai-codex";
838
+ }
839
+ return {
840
+ provider,
841
+ authMode,
842
+ apiKey: hasModeToken ? tokens[3] : tokens[2],
843
+ endpointOrHost: hasModeToken ? tokens[4] : tokens[3]
844
+ };
845
+ }
846
+ async function resolveInteractiveLoginCommand(params) {
847
+ const parts = parseLoginCommandParts(params.line);
848
+ if (!parts) {
849
+ return { action: "continue", line: params.line };
850
+ }
851
+ const config = await (0, config_1.readConfig)(params.cwd);
852
+ if (parts.provider !== "http" && parts.provider !== "ollama") {
853
+ const useOAuth = parts.authMode === "oauth";
854
+ if (useOAuth && !(0, provider_registry_1.providerSupportsOAuth)(parts.provider)) {
855
+ return { action: "cancel", notice: `Provider "${parts.provider}" does not support oauth login.` };
856
+ }
857
+ if (parts.provider === "openai-codex" &&
858
+ useOAuth &&
859
+ !parts.apiKey?.trim() &&
860
+ params.oauth_browser?.enabled) {
861
+ const notify = async (notice) => {
862
+ if (params.oauth_browser?.notify) {
863
+ await params.oauth_browser.notify(notice);
864
+ }
865
+ };
866
+ const oauthState = {
867
+ url: "",
868
+ instruction: "",
869
+ status: "ready"
870
+ };
871
+ const publishOAuthState = async () => {
872
+ const lines = [
873
+ "OpenAI OAuth",
874
+ oauthState.url ? `URL: ${oauthState.url}` : "URL: (pending)",
875
+ oauthState.instruction || "Use browser login. If auto-open fails, copy URL manually.",
876
+ `Status: ${oauthState.status}`,
877
+ "Esc: cancel OAuth flow"
878
+ ];
879
+ await notify(lines.join("\n"));
880
+ };
881
+ const loader = new pi_tui_1.CancellableLoader({ requestRender: () => undefined }, IDENTITY_STYLE, IDENTITY_STYLE, "OpenAI OAuth");
882
+ let cancelledByEsc = false;
883
+ loader.onAbort = () => {
884
+ cancelledByEsc = true;
885
+ };
886
+ const detachEscapeListener = params.input_source?.subscribe((event) => {
887
+ loader.handleInput(event.sequence);
888
+ });
889
+ const runOAuth = params.oauth_browser?.run_openai_oauth ?? (async (input) => await (0, openai_codex_oauth_1.loginOpenAICodexOAuth)({
890
+ onAuth: input.onAuth,
891
+ onProgress: input.onProgress,
892
+ onPrompt: input.onPrompt,
893
+ open_browser: true,
894
+ abort_signal: input.abort_signal
895
+ }));
896
+ try {
897
+ await publishOAuthState();
898
+ const oauthResult = await runOAuth({
899
+ onAuth: async ({ url, instructions }) => {
900
+ oauthState.url = url;
901
+ oauthState.instruction = instructions;
902
+ oauthState.status = "authorization URL ready";
903
+ await publishOAuthState();
904
+ },
905
+ onProgress: async (message) => {
906
+ oauthState.status = message;
907
+ await publishOAuthState();
908
+ },
909
+ onPrompt: async (notice) => await params.prompt(notice),
910
+ abort_signal: loader.signal
911
+ });
912
+ const token = oauthResult.access_token?.trim();
913
+ if (!token) {
914
+ return { action: "cancel", notice: "OpenAI OAuth cancelled: missing access token" };
915
+ }
916
+ return {
917
+ action: "continue",
918
+ line: `/login ${parts.provider} oauth ${token}`
919
+ };
920
+ }
921
+ catch (error) {
922
+ if (cancelledByEsc) {
923
+ return { action: "cancel", notice: "OpenAI OAuth cancelled (Esc)." };
924
+ }
925
+ const message = error instanceof Error ? error.message : String(error);
926
+ if (/cancel/i.test(message)) {
927
+ return { action: "cancel", notice: "OpenAI OAuth cancelled." };
928
+ }
929
+ return { action: "cancel", notice: `OpenAI OAuth failed: ${message}` };
930
+ }
931
+ finally {
932
+ detachEscapeListener?.();
933
+ loader.dispose();
934
+ }
935
+ }
936
+ if (parts.apiKey?.trim()) {
937
+ const endpoint = parts.endpointOrHost?.trim();
938
+ return {
939
+ action: "continue",
940
+ line: useOAuth
941
+ ? `/login ${parts.provider} oauth ${parts.apiKey}${endpoint ? ` ${endpoint}` : ""}`
942
+ : `/login ${parts.provider} ${parts.apiKey}${endpoint ? ` ${endpoint}` : ""}`
943
+ };
944
+ }
945
+ const credentialLabel = useOAuth ? "access_token" : "api_key";
946
+ const credential = (await params.prompt(`login ${parts.provider}: input ${credentialLabel} (Enter cancel)`)).trim();
947
+ if (credential === "/exit") {
948
+ return { action: "exit" };
949
+ }
950
+ if (credential === "") {
951
+ return { action: "cancel", notice: "login cancelled" };
952
+ }
953
+ const fallbackEndpoint = config.model.endpoint?.trim() || "";
954
+ const endpointInput = (await params.prompt(fallbackEndpoint
955
+ ? `login ${parts.provider}: input endpoint (Enter keep ${fallbackEndpoint})`
956
+ : `login ${parts.provider}: input endpoint (optional; Enter skip)`)).trim();
957
+ if (endpointInput === "/exit") {
958
+ return { action: "exit" };
959
+ }
960
+ const endpoint = endpointInput || fallbackEndpoint;
961
+ return {
962
+ action: "continue",
963
+ line: useOAuth
964
+ ? `/login ${parts.provider} oauth ${credential}${endpoint ? ` ${endpoint}` : ""}`
965
+ : `/login ${parts.provider} ${credential}${endpoint ? ` ${endpoint}` : ""}`
966
+ };
967
+ }
968
+ if (parts.provider === "http") {
969
+ let apiKey = parts.apiKey?.trim() || "";
970
+ if (!apiKey) {
971
+ apiKey = (await params.prompt("login http: input api_key (Enter cancel)")).trim();
972
+ if (apiKey === "/exit") {
973
+ return { action: "exit" };
974
+ }
975
+ if (!apiKey) {
976
+ return { action: "cancel", notice: "login cancelled" };
977
+ }
978
+ }
979
+ let endpoint = parts.endpointOrHost?.trim() || "";
980
+ if (!endpoint) {
981
+ const fallbackEndpoint = config.model.endpoint?.trim() || "";
982
+ const endpointInput = (await params.prompt(fallbackEndpoint
983
+ ? `login http: input endpoint (Enter keep ${fallbackEndpoint})`
984
+ : "login http: input endpoint (required)")).trim();
985
+ if (endpointInput === "/exit") {
986
+ return { action: "exit" };
987
+ }
988
+ endpoint = endpointInput || fallbackEndpoint;
989
+ if (!endpoint) {
990
+ return { action: "cancel", notice: "login cancelled: endpoint required" };
991
+ }
992
+ }
993
+ return {
994
+ action: "continue",
995
+ line: `/login http ${apiKey} ${endpoint}`
996
+ };
997
+ }
998
+ if (parts.endpointOrHost?.trim()) {
999
+ return { action: "continue", line: params.line };
1000
+ }
1001
+ const fallbackHost = config.execution.ollama_host?.trim() || "http://127.0.0.1:11434";
1002
+ const hostInput = (await params.prompt(`login ollama: input host (Enter keep ${fallbackHost})`)).trim();
1003
+ if (hostInput === "/exit") {
1004
+ return { action: "exit" };
1005
+ }
1006
+ const host = hostInput || fallbackHost;
1007
+ return {
1008
+ action: "continue",
1009
+ line: `/login ollama ${host}`
1010
+ };
1011
+ }
1012
+ async function readInteractiveInput(render, resolveCandidates, inputSource, modelMenu = EMPTY_MODEL_MENU_SNAPSHOT, onNavigationKey) {
1013
+ return await new Promise((resolve) => {
1014
+ let commandSelectedIndex = 0;
1015
+ const cursorVisible = true;
1016
+ let submittedValue = null;
1017
+ const editor = createInteractiveEditorBridge((value) => {
1018
+ submittedValue = value;
1019
+ });
1020
+ let renderInFlight = false;
1021
+ let pendingRender = null;
1022
+ const readSnapshot = () => editor.getSnapshot();
1023
+ const requestRender = (nextBuffer, nextCompletion, nextCommandMenu, nextCursorVisible, nextCommandMenuLabel, nextInputCursorIndex) => {
1024
+ pendingRender = {
1025
+ buffer: nextBuffer,
1026
+ completion: nextCompletion,
1027
+ commandMenu: nextCommandMenu,
1028
+ cursorVisible: nextCursorVisible,
1029
+ commandMenuLabel: nextCommandMenuLabel,
1030
+ inputCursorIndex: nextInputCursorIndex
1031
+ };
1032
+ if (renderInFlight) {
1033
+ return;
1034
+ }
1035
+ renderInFlight = true;
1036
+ void (async () => {
1037
+ while (pendingRender) {
1038
+ const next = pendingRender;
1039
+ pendingRender = null;
1040
+ await render(next.buffer, next.completion, next.commandMenu, next.cursorVisible, next.commandMenuLabel, next.inputCursorIndex);
1041
+ }
1042
+ renderInFlight = false;
1043
+ })();
1044
+ };
1045
+ const refresh = () => {
1046
+ const snapshot = readSnapshot();
1047
+ const buffer = snapshot.buffer;
1048
+ const completion = resolveCompletion(buffer, snapshot.cursorIndex, resolveCandidates);
1049
+ const commandMenu = resolveCommandMenu(buffer, snapshot.cursorIndex, commandSelectedIndex, resolveCandidates);
1050
+ if (commandMenu.active) {
1051
+ commandSelectedIndex = commandMenu.selectedIndex;
1052
+ }
1053
+ else {
1054
+ commandSelectedIndex = 0;
1055
+ }
1056
+ const menuForRender = renderCommandMenuWithSelectList(commandMenu);
1057
+ requestRender(buffer, completion, menuForRender, cursorVisible, undefined, snapshot.cursorIndex);
1058
+ };
1059
+ const applyCurrentCommandSelection = () => {
1060
+ const snapshot = readSnapshot();
1061
+ const buffer = snapshot.buffer;
1062
+ const commandMenu = resolveCommandMenu(buffer, snapshot.cursorIndex, commandSelectedIndex, resolveCandidates);
1063
+ if (!commandMenu.active || commandMenu.items.length === 0) {
1064
+ return false;
1065
+ }
1066
+ const selected = commandMenu.items[commandMenu.selectedIndex] ?? commandMenu.items[0];
1067
+ if (!selected) {
1068
+ return false;
1069
+ }
1070
+ editor.setText(selected);
1071
+ commandSelectedIndex = commandMenu.selectedIndex;
1072
+ refresh();
1073
+ return true;
1074
+ };
1075
+ const cleanup = () => {
1076
+ unsubscribeInput();
1077
+ };
1078
+ const finish = (value) => {
1079
+ cleanup();
1080
+ resolve(value);
1081
+ };
1082
+ const onInput = (event) => {
1083
+ const { sequence, key, parsedKey } = event;
1084
+ const submitCurrentBuffer = (buffer, cursorIndex) => {
1085
+ const commandMenu = resolveCommandMenu(buffer, cursorIndex, commandSelectedIndex, resolveCandidates);
1086
+ if (commandMenu.active && commandMenu.items.length > 0) {
1087
+ const selected = commandMenu.items[commandMenu.selectedIndex] ?? commandMenu.items[0];
1088
+ const outcome = resolveMenuEnterOutcome(buffer, selected, modelMenu);
1089
+ editor.setText(outcome.buffer);
1090
+ refresh();
1091
+ if (!outcome.submit) {
1092
+ return;
1093
+ }
1094
+ finish(outcome.buffer);
1095
+ return;
1096
+ }
1097
+ finish(buffer);
1098
+ };
1099
+ const current = readSnapshot();
1100
+ if ((0, pi_tui_1.matchesKey)(sequence, pi_tui_1.Key.ctrl("c")) || (key.ctrl && key.name === "c")) {
1101
+ finish("/exit");
1102
+ return;
1103
+ }
1104
+ if (shouldTreatCtrlDAsExit(current.buffer, key)) {
1105
+ finish("/exit");
1106
+ return;
1107
+ }
1108
+ if ((0, pi_tui_1.matchesKey)(sequence, pi_tui_1.Key.ctrl("l")) || (key.ctrl && key.name === "l")) {
1109
+ refresh();
1110
+ return;
1111
+ }
1112
+ if ((0, pi_tui_1.matchesKey)(sequence, pi_tui_1.Key.ctrl("pageUp")) ||
1113
+ (0, pi_tui_1.matchesKey)(sequence, pi_tui_1.Key.ctrl("pageDown")) ||
1114
+ (0, pi_tui_1.matchesKey)(sequence, pi_tui_1.Key.ctrl("home")) ||
1115
+ (0, pi_tui_1.matchesKey)(sequence, pi_tui_1.Key.ctrl("end")) ||
1116
+ (key.ctrl &&
1117
+ (key.name === "pageup" ||
1118
+ key.name === "pagedown" ||
1119
+ key.name === "home" ||
1120
+ key.name === "end"))) {
1121
+ onNavigationKey?.(key);
1122
+ refresh();
1123
+ return;
1124
+ }
1125
+ if ((0, pi_tui_1.matchesKey)(sequence, pi_tui_1.Key.up) || key.name === "up") {
1126
+ const commandMenu = resolveCommandMenu(current.buffer, current.cursorIndex, commandSelectedIndex, resolveCandidates);
1127
+ if (commandMenu.active && commandMenu.items.length > 0) {
1128
+ commandSelectedIndex = moveCommandSelectionWithSelectList(commandMenu, sequence);
1129
+ refresh();
1130
+ return;
1131
+ }
1132
+ }
1133
+ if ((0, pi_tui_1.matchesKey)(sequence, pi_tui_1.Key.down) || key.name === "down") {
1134
+ const commandMenu = resolveCommandMenu(current.buffer, current.cursorIndex, commandSelectedIndex, resolveCandidates);
1135
+ if (commandMenu.active && commandMenu.items.length > 0) {
1136
+ commandSelectedIndex = moveCommandSelectionWithSelectList(commandMenu, sequence);
1137
+ refresh();
1138
+ return;
1139
+ }
1140
+ }
1141
+ if ((0, pi_tui_1.matchesKey)(sequence, pi_tui_1.Key.tab) || key.name === "tab") {
1142
+ if (!applyCurrentCommandSelection()) {
1143
+ const completion = resolveCompletion(current.buffer, current.cursorIndex, resolveCandidates);
1144
+ if (completion) {
1145
+ editor.setText(completion);
1146
+ }
1147
+ }
1148
+ refresh();
1149
+ return;
1150
+ }
1151
+ if ((isPlainSubmitSequence(sequence) ||
1152
+ (0, pi_tui_1.matchesKey)(sequence, pi_tui_1.Key.enter) ||
1153
+ key.name === "return" ||
1154
+ key.name === "enter") &&
1155
+ !key.shift) {
1156
+ submitCurrentBuffer(current.buffer, current.cursorIndex);
1157
+ return;
1158
+ }
1159
+ const normalizedChunk = normalizeInteractiveInputChunk(sequence);
1160
+ if (isUnparsedControlSequence(sequence, parsedKey)) {
1161
+ return;
1162
+ }
1163
+ if (shouldSubmitOnUnparsedLineBreak(key, normalizedChunk) && !isBracketedPasteSequence(sequence)) {
1164
+ submitCurrentBuffer(current.buffer, current.cursorIndex);
1165
+ return;
1166
+ }
1167
+ const before = readSnapshot();
1168
+ if (!parsedKey &&
1169
+ sequence.includes("\u001b") &&
1170
+ !isBracketedPasteSequence(sequence) &&
1171
+ shouldAppendInteractiveTextChunk(sequence, key, normalizedChunk)) {
1172
+ editor.insertText(normalizedChunk.text);
1173
+ refresh();
1174
+ return;
1175
+ }
1176
+ submittedValue = null;
1177
+ const after = editor.handleInput(sequence);
1178
+ if (submittedValue !== null) {
1179
+ finish(submittedValue);
1180
+ return;
1181
+ }
1182
+ if (after.buffer !== before.buffer || after.cursorIndex !== before.cursorIndex) {
1183
+ refresh();
1184
+ }
1185
+ };
1186
+ const unsubscribeInput = inputSource.subscribe(onInput);
1187
+ refresh();
1188
+ });
1189
+ }
1190
+ async function readInteractiveResumePicker(render, inputSource, items, initialSelectedIndex = 0) {
1191
+ if (items.length === 0) {
1192
+ return { action: "cancel" };
1193
+ }
1194
+ return await new Promise((resolve) => {
1195
+ const pickerItems = items.map((item) => ({
1196
+ value: item.id,
1197
+ label: item.label
1198
+ }));
1199
+ const picker = new pi_tui_1.SelectList(pickerItems, Math.min(items.length, 4), {
1200
+ selectedPrefix: IDENTITY_STYLE,
1201
+ selectedText: IDENTITY_STYLE,
1202
+ description: IDENTITY_STYLE,
1203
+ scrollInfo: IDENTITY_STYLE,
1204
+ noMatch: IDENTITY_STYLE
1205
+ });
1206
+ picker.setSelectedIndex(Math.max(0, Math.min(initialSelectedIndex, items.length - 1)));
1207
+ const refresh = () => {
1208
+ const menuWidth = Math.max((process.stdout.columns || tui_view_1.MIN_TUI_COLUMNS) - 2, 24);
1209
+ const menuLines = picker.render(menuWidth);
1210
+ void render("/resume", "resume: ↑/↓ select · Enter confirm · Esc back", {
1211
+ active: true,
1212
+ items: menuLines,
1213
+ selectedIndex: 0,
1214
+ renderMode: "raw"
1215
+ }, true, "sessions:");
1216
+ };
1217
+ picker.onSelectionChange = () => {
1218
+ refresh();
1219
+ };
1220
+ picker.onSelect = (item) => {
1221
+ finish({ action: "select", id: item.value });
1222
+ };
1223
+ picker.onCancel = () => {
1224
+ finish({ action: "cancel" });
1225
+ };
1226
+ const cleanup = () => {
1227
+ unsubscribeInput();
1228
+ };
1229
+ const finish = (result) => {
1230
+ cleanup();
1231
+ resolve(result);
1232
+ };
1233
+ const onInput = (event) => {
1234
+ const { key, sequence } = event;
1235
+ if ((0, pi_tui_1.matchesKey)(sequence, pi_tui_1.Key.ctrl("c")) || (0, pi_tui_1.matchesKey)(sequence, pi_tui_1.Key.ctrl("d")) || (key.ctrl && (key.name === "c" || key.name === "d"))) {
1236
+ finish({ action: "exit" });
1237
+ return;
1238
+ }
1239
+ if ((0, pi_tui_1.matchesKey)(sequence, pi_tui_1.Key.ctrl("l")) || (key.ctrl && key.name === "l")) {
1240
+ refresh();
1241
+ return;
1242
+ }
1243
+ picker.handleInput(sequence);
1244
+ };
1245
+ const unsubscribeInput = inputSource.subscribe(onInput);
1246
+ refresh();
1247
+ });
33
1248
  }
34
1249
  function toSessionWithDialog(session) {
35
1250
  const dialog = session.dialog ?? {
@@ -38,19 +1253,24 @@ function toSessionWithDialog(session) {
38
1253
  rounds: [],
39
1254
  history: []
40
1255
  };
1256
+ const history = dialog.history.filter((entry) => !isExitLifecycleNoise(entry));
1257
+ const pendingBuildChoice = dialog.pending_build_choice
1258
+ ? { ...dialog.pending_build_choice }
1259
+ : undefined;
41
1260
  return {
42
1261
  ...session,
43
1262
  dialog: {
44
1263
  ...dialog,
45
1264
  rounds: [...dialog.rounds],
46
- history: [...dialog.history]
1265
+ history: [...history],
1266
+ pending_build_choice: pendingBuildChoice
47
1267
  }
48
1268
  };
49
1269
  }
50
1270
  async function requireSessionWithDialog(cwd) {
51
1271
  const session = await (0, intent_1.readSessionStore)(cwd);
52
1272
  if (!session) {
53
- throw new Error("Missing .zaker/session.json. Run `zaker init` first.");
1273
+ throw new Error("Missing .zaker/session.json. Run `/init` first.");
54
1274
  }
55
1275
  const normalized = toSessionWithDialog(session);
56
1276
  await (0, intent_1.writeSessionStore)(normalized, cwd);
@@ -86,24 +1306,39 @@ async function appendDialogEntry(cwd, role, kind, content) {
86
1306
  return normalized;
87
1307
  });
88
1308
  }
1309
+ async function setPendingBuildChoice(cwd, intentId) {
1310
+ await mutateDialogSession(cwd, (session) => {
1311
+ const normalized = toSessionWithDialog(session);
1312
+ const dialog = normalized.dialog ?? {
1313
+ debug: false,
1314
+ turn: 0,
1315
+ rounds: [],
1316
+ history: []
1317
+ };
1318
+ normalized.dialog = {
1319
+ debug: dialog.debug,
1320
+ turn: dialog.turn,
1321
+ rounds: [...dialog.rounds],
1322
+ history: [...dialog.history],
1323
+ pending_build_choice: intentId
1324
+ ? {
1325
+ intent_id: intentId,
1326
+ created_at: now()
1327
+ }
1328
+ : undefined
1329
+ };
1330
+ normalized.updated_at = now();
1331
+ return normalized;
1332
+ });
1333
+ }
89
1334
  async function buildSessionStatusLines(cwd) {
90
1335
  const session = await requireSessionWithDialog(cwd);
91
1336
  const draft = await (0, intent_1.readIntentCard)(cwd);
92
- const rounds = session.dialog?.rounds ?? [];
93
- const lines = [
94
- `state: ${session.state}`,
95
- `current_intent_id: ${session.current_intent_id ?? "-"}`,
96
- `debug: ${session.dialog?.debug ? "on" : "off"}`,
97
- `history_turns: ${session.dialog?.history.length ?? 0}`,
98
- `intent.card: ${draft ? `${draft.intent_id} (${draft.status})` : "-"}`
99
- ];
100
- if (rounds.length > 0) {
101
- lines.push("recent_rounds:");
102
- for (const item of rounds.slice(-5)) {
103
- lines.push(`- #${item.round} verdict=${item.verdict} reason=${item.reason_code} checkpoint=${item.checkpoint_id ?? "-"}`);
104
- }
105
- }
106
- return lines;
1337
+ const confirmed = await (0, intent_1.readConfirmedIntent)(cwd);
1338
+ return (0, session_status_1.formatSessionStatusLines)(session, draft, confirmed, {
1339
+ includeDialog: true,
1340
+ recentRoundLimit: 5
1341
+ });
107
1342
  }
108
1343
  async function upsertDraftIntent(cwd, content, replace = false) {
109
1344
  const text = content.trim();
@@ -118,6 +1353,97 @@ async function upsertDraftIntent(cwd, content, replace = false) {
118
1353
  const intent = await (0, intent_1.alignIntent)(nextDescription, config.scope, cwd);
119
1354
  return `intent.card updated: ${intent.intent_id} (state=ALIGN)`;
120
1355
  }
1356
+ async function generateAssistantReply(cwd, userMessage, intentUpdate, options = {}) {
1357
+ if (options.abort_signal?.aborted) {
1358
+ const error = new Error("ALIGN_ABORTED");
1359
+ error.name = "AbortError";
1360
+ throw error;
1361
+ }
1362
+ const config = await (0, config_1.readConfig)(cwd);
1363
+ if (config.model.provider === "mock") {
1364
+ throw new Error("ALIGN_MODEL_UNAVAILABLE: provider=mock. Please login a real model provider first.");
1365
+ }
1366
+ const provider = (0, config_1.createLLMProvider)(config);
1367
+ if (typeof provider.align !== "function") {
1368
+ throw new Error(`ALIGN_MODEL_UNAVAILABLE: provider "${config.model.provider}" does not support alignment replies.`);
1369
+ }
1370
+ try {
1371
+ const reply = await provider.align(userMessage, {
1372
+ intent_update: intentUpdate,
1373
+ abort_signal: options.abort_signal
1374
+ });
1375
+ const normalized = reply.trim();
1376
+ if (!normalized) {
1377
+ throw new Error("ALIGN_MODEL_EMPTY_REPLY: provider returned empty reply.");
1378
+ }
1379
+ return normalized;
1380
+ }
1381
+ catch (error) {
1382
+ if (error instanceof Error && (error.name === "AbortError" || /abort|cancel/i.test(error.message))) {
1383
+ throw error;
1384
+ }
1385
+ const message = error instanceof Error ? error.message : String(error);
1386
+ throw new Error(`ALIGN_MODEL_ERROR: ${message}`);
1387
+ }
1388
+ }
1389
+ async function detectBuildIntent(cwd, userMessage, intentUpdate, options = {}) {
1390
+ if (options.abort_signal?.aborted) {
1391
+ const error = new Error("ALIGN_ABORTED");
1392
+ error.name = "AbortError";
1393
+ throw error;
1394
+ }
1395
+ const config = await (0, config_1.readConfig)(cwd);
1396
+ if (config.model.provider === "mock") {
1397
+ return {
1398
+ ready: false,
1399
+ confidence: 0,
1400
+ signal: "mock_provider"
1401
+ };
1402
+ }
1403
+ const provider = (0, config_1.createLLMProvider)(config);
1404
+ if (typeof provider.detectBuildIntent !== "function") {
1405
+ return {
1406
+ ready: false,
1407
+ confidence: 0,
1408
+ signal: "unsupported"
1409
+ };
1410
+ }
1411
+ return await provider.detectBuildIntent(userMessage, {
1412
+ intent_update: intentUpdate,
1413
+ abort_signal: options.abort_signal
1414
+ });
1415
+ }
1416
+ async function readDraftIntent(cwd) {
1417
+ return await (0, intent_1.readIntentCard)(cwd);
1418
+ }
1419
+ async function buildIntent(cwd) {
1420
+ return await (0, intent_1.confirmIntent)(cwd);
1421
+ }
1422
+ async function setPendingBuildChoiceForIntent(cwd, intentId) {
1423
+ await setPendingBuildChoice(cwd, intentId);
1424
+ }
1425
+ async function detectBuildIntentSignal(cwd, userMessage, intentUpdate, options = {}) {
1426
+ try {
1427
+ const signal = await detectBuildIntent(cwd, userMessage, intentUpdate, options);
1428
+ if (signal.ready && signal.confidence >= 0.65) {
1429
+ return signal;
1430
+ }
1431
+ return {
1432
+ ...signal,
1433
+ ready: false
1434
+ };
1435
+ }
1436
+ catch (error) {
1437
+ if (error instanceof Error && (error.name === "AbortError" || /abort|cancel/i.test(error.message))) {
1438
+ throw error;
1439
+ }
1440
+ return {
1441
+ ready: false,
1442
+ confidence: 0,
1443
+ signal: "error"
1444
+ };
1445
+ }
1446
+ }
121
1447
  function printVerdictLines(lines, verdict) {
122
1448
  const first = lines[0] ?? verdict;
123
1449
  if (verdict === "PASS") {
@@ -175,264 +1501,461 @@ function summarizeVerdict(output, debug) {
175
1501
  `risk=${panel.risk_hit}`,
176
1502
  `retry=${panel.retry_allowed ? "YES" : "NO"}`
177
1503
  ].join(" | ");
178
- return { lines, historyEntry };
1504
+ return { lines, historyEntry, panel };
179
1505
  }
180
- async function renderDialogueScreen(cwd, notice) {
181
- const session = await requireSessionWithDialog(cwd);
182
- const draft = await (0, intent_1.readIntentCard)(cwd);
1506
+ function updateBoardFromSession(board, session, lastRoundReasonCode) {
1507
+ if (session.state === "RUNNING") {
1508
+ board.run_status = "RUNNING";
1509
+ }
1510
+ if (session.state === "ALIGN" && board.run_status === "RUNNING") {
1511
+ board.run_status = "IDLE";
1512
+ board.run_stage = "IDLE";
1513
+ }
1514
+ if (session.last_run_verdict) {
1515
+ board.audit_verdict = session.last_run_verdict;
1516
+ board.audit_reason = lastRoundReasonCode ?? board.audit_reason;
1517
+ }
1518
+ }
1519
+ function renderDialogueScreen(conversationId, session, _draft, board, notice, modelInfo, conversationScrollOffset, inputBuffer = "", completion, commandMenu, cursorVisible, commandMenuLabel, inputCursorIndex, frameRenderer = tui_view_1.renderTwoPaneFrame) {
183
1520
  const history = session.dialog?.history ?? [];
184
1521
  const rounds = session.dialog?.rounds ?? [];
185
1522
  const lastRound = rounds.at(-1);
186
- const lines = [];
187
- lines.push(chalk_1.default.bold("ZAKER Agent TUI"));
188
- lines.push(chalk_1.default.gray(`state=${session.state} | debug=${session.dialog?.debug ? "on" : "off"} | turns=${history.length} | rounds=${rounds.length}`));
189
- lines.push(chalk_1.default.gray(helpText()));
190
- lines.push(divider());
191
- lines.push(chalk_1.default.bold("Conversation"));
1523
+ const visibleHistory = Math.max(8, (process.stdout.rows || tui_view_1.MIN_TUI_ROWS) - 14);
1524
+ const maxScrollOffset = Math.max(0, history.length - visibleHistory);
1525
+ const normalizedScrollOffset = Math.max(0, Math.min(conversationScrollOffset, maxScrollOffset));
1526
+ const windowEnd = Math.max(0, history.length - normalizedScrollOffset);
1527
+ const windowStart = Math.max(0, windowEnd - visibleHistory);
1528
+ const visibleEntries = history.slice(windowStart, windowEnd);
1529
+ const leftTitle = normalizedScrollOffset > 0 && history.length > 0
1530
+ ? `Conversation (${windowStart + 1}-${windowEnd}/${history.length})`
1531
+ : "Conversation";
1532
+ const leftContent = [];
1533
+ const leftContentItems = [];
192
1534
  if (history.length === 0) {
193
- lines.push(chalk_1.default.gray("(empty) 输入需求文本以更新 intent.card,然后 /confirm -> /run"));
1535
+ leftContent.push("(empty)");
194
1536
  }
195
1537
  else {
196
- for (const entry of history.slice(-MAX_SCREEN_HISTORY)) {
197
- const role = entry.role === "user" ? "YOU " : "ZAKER";
198
- const kind = entry.kind.toUpperCase().padEnd(7, " ");
199
- lines.push(`${chalk_1.default.cyan(role)} [${kind}] ${compactLine(entry.content, 180)}`);
200
- }
201
- }
202
- lines.push(divider());
203
- lines.push(chalk_1.default.bold("Intent"));
204
- lines.push(`intent_id: ${draft?.intent_id ?? session.current_intent_id ?? "-"}`);
205
- lines.push(`goal: ${compactLine(draft?.goal ?? "-", 180)}`);
206
- if (lastRound) {
207
- lines.push(`last_round: #${lastRound.round} ${lastRound.verdict} (${lastRound.reason_code}) checkpoint=${lastRound.checkpoint_id ?? "-"}`);
208
- }
209
- if (notice) {
210
- lines.push(chalk_1.default.yellow(`notice: ${notice}`));
211
- }
212
- lines.push("");
213
- process.stdout.write("\x1Bc");
214
- process.stdout.write(`${lines.join("\n")}\n`);
215
- return session;
216
- }
217
- function renderBootstrapScreen(notice) {
218
- const lines = [];
219
- lines.push(chalk_1.default.bold("ZAKER Agent TUI"));
220
- lines.push(chalk_1.default.gray("state=UNINITIALIZED"));
221
- lines.push(chalk_1.default.gray(helpText()));
222
- lines.push(divider());
223
- lines.push(chalk_1.default.bold("Bootstrap"));
224
- lines.push("workspace is not initialized.");
225
- lines.push("run /init to create .zaker/config.json, .zaker/risk_paths.yaml, .zaker/memory.json, and session files.");
226
- if (notice) {
227
- lines.push(chalk_1.default.yellow(`notice: ${notice}`));
228
- }
229
- lines.push("");
230
- process.stdout.write("\x1Bc");
231
- process.stdout.write(`${lines.join("\n")}\n`);
1538
+ for (const entry of visibleEntries.slice(-Math.max(MAX_SCREEN_HISTORY, visibleHistory))) {
1539
+ const isBuildChoiceCard = entry.role === "system" &&
1540
+ entry.kind === "status" &&
1541
+ entry.content.includes("intent.card ready:") &&
1542
+ entry.content.includes("1) Build");
1543
+ const tone = entry.role === "user"
1544
+ ? "user"
1545
+ : isBuildChoiceCard
1546
+ ? "action_card"
1547
+ : entry.kind === "status"
1548
+ ? "status"
1549
+ : "default";
1550
+ leftContentItems.push({
1551
+ text: tone === "action_card" ? entry.content : compactLine(entry.content, 300),
1552
+ tone
1553
+ });
1554
+ }
1555
+ }
1556
+ updateBoardFromSession(board, session, lastRound?.reason_code);
1557
+ frameRenderer({
1558
+ conversationId,
1559
+ stateLabel: session.state,
1560
+ stageLabel: board.run_stage,
1561
+ verdictLabel: board.audit_verdict,
1562
+ debug: session.dialog?.debug ?? false,
1563
+ turns: history.length,
1564
+ rounds: rounds.length,
1565
+ leftTitle,
1566
+ leftContent,
1567
+ leftContentItems: leftContentItems.length > 0 ? leftContentItems : undefined,
1568
+ notice,
1569
+ modelInfo,
1570
+ inputBuffer,
1571
+ inputCursorIndex,
1572
+ completion,
1573
+ commandMenu,
1574
+ cursorVisible,
1575
+ commandMenuLabel
1576
+ });
1577
+ return {
1578
+ scrollOffset: normalizedScrollOffset,
1579
+ totalHistory: history.length,
1580
+ visibleHistory
1581
+ };
232
1582
  }
1583
+ function renderBootstrapScreen(conversationId, notice, modelInfo, inputBuffer = "", completion, commandMenu, cursorVisible, commandMenuLabel, inputCursorIndex, frameRenderer = tui_view_1.renderTwoPaneFrame) {
1584
+ const leftContent = [
1585
+ "workspace is not initialized.",
1586
+ "",
1587
+ "run /init to create required artifacts:",
1588
+ "- .zaker/config.json",
1589
+ "- .zaker/risk_paths.yaml",
1590
+ "- .zaker/memory.json",
1591
+ "- .zaker/intent.card.json",
1592
+ "- .zaker/session.json",
1593
+ "",
1594
+ "AVAILABLE COMMANDS",
1595
+ "/init",
1596
+ "/help",
1597
+ "/exit",
1598
+ "",
1599
+ "NEXT",
1600
+ "1) run /init",
1601
+ "2) write requirement text",
1602
+ "3) 触发 build 卡片后输入 1",
1603
+ "4) /run"
1604
+ ];
1605
+ frameRenderer({
1606
+ conversationId,
1607
+ stateLabel: "UNINITIALIZED",
1608
+ stageLabel: "BOOTSTRAP",
1609
+ verdictLabel: "PENDING",
1610
+ debug: false,
1611
+ turns: 0,
1612
+ rounds: 0,
1613
+ leftTitle: "Bootstrap",
1614
+ leftContent,
1615
+ notice,
1616
+ modelInfo,
1617
+ inputBuffer,
1618
+ inputCursorIndex,
1619
+ completion,
1620
+ commandMenu,
1621
+ cursorVisible,
1622
+ commandMenuLabel
1623
+ });
1624
+ }
1625
+ function createDialogLogger(options) {
1626
+ const enabled = !options.interactive && !options.quiet;
1627
+ return {
1628
+ info(message) {
1629
+ if (enabled) {
1630
+ console.log(chalk_1.default.gray(message));
1631
+ }
1632
+ },
1633
+ warn(message) {
1634
+ if (enabled) {
1635
+ console.log(chalk_1.default.yellow(message));
1636
+ }
1637
+ },
1638
+ success(message) {
1639
+ if (enabled) {
1640
+ console.log(chalk_1.default.green(message));
1641
+ }
1642
+ },
1643
+ error(message) {
1644
+ if (enabled) {
1645
+ console.log(chalk_1.default.red(message));
1646
+ }
1647
+ }
1648
+ };
1649
+ }
1650
+ function createDialogHandlerServices() {
1651
+ return {
1652
+ now,
1653
+ maxDialogRounds: MAX_DIALOG_ROUNDS,
1654
+ defaultConversationId: dialog_session_1.DEFAULT_CONVERSATION_ID,
1655
+ helpText,
1656
+ createDefaultConversationIndex: dialog_session_1.createDefaultConversationIndex,
1657
+ resetBoard,
1658
+ readSessionStore: async (cwd) => await (0, intent_1.readSessionStore)(cwd),
1659
+ requireSessionWithDialog,
1660
+ mutateDialogSession,
1661
+ appendDialogEntry,
1662
+ readIntentCard: readDraftIntent,
1663
+ upsertDraftIntent,
1664
+ generateAssistantReply,
1665
+ detectBuildIntent: detectBuildIntentSignal,
1666
+ setPendingBuildChoice: setPendingBuildChoiceForIntent,
1667
+ buildSessionStatusLines,
1668
+ runInit: async (cwd) => {
1669
+ await (0, init_1.runInit)(false, cwd, () => undefined);
1670
+ },
1671
+ persistActiveConversation: dialog_session_1.persistActiveConversation,
1672
+ listConversationIds: dialog_session_1.listConversationIds,
1673
+ switchConversation: dialog_session_1.switchConversation,
1674
+ normalizeConversationId: dialog_session_1.normalizeConversationId,
1675
+ buildIntent,
1676
+ runConfirmedPipeline: async (sopOut, checkpointOut, cwd, onStage) => await (0, run_1.runConfirmedPipeline)(sopOut, checkpointOut, cwd, {
1677
+ quiet: true,
1678
+ hooks: { onStage }
1679
+ }),
1680
+ summarizeVerdict: (output, debug) => summarizeVerdict(output, debug),
1681
+ printVerdictLines
1682
+ };
1683
+ }
1684
+ const dialogHandlerServices = createDialogHandlerServices();
233
1685
  async function handleDialogInput(line, cwd, options) {
234
- const input = line.trim();
235
- if (!input) {
236
- return { action: "continue" };
237
- }
238
1686
  const initialized = (await (0, intent_1.readSessionStore)(cwd)) !== null;
239
- if (!initialized) {
240
- if (!input.startsWith("/")) {
241
- const notice = "workspace not initialized. run /init first.";
242
- if (!options.interactive && !options.quiet) {
243
- console.log(chalk_1.default.yellow(notice));
244
- }
245
- return { action: "continue", notice };
246
- }
247
- const [bootstrapCommand] = input.split(" ");
248
- if (bootstrapCommand === "/exit" || bootstrapCommand === "/quit") {
249
- return { action: "exit", notice: "dialog ended" };
250
- }
251
- if (bootstrapCommand === "/help") {
252
- const help = helpText();
253
- if (!options.interactive && !options.quiet) {
254
- console.log(chalk_1.default.gray(help));
255
- }
256
- return { action: "continue", notice: "help shown" };
257
- }
258
- if (bootstrapCommand === "/init") {
259
- await (0, init_1.runInit)(false, cwd);
260
- await requireSessionWithDialog(cwd);
261
- const notice = "workspace initialized";
262
- if (!options.interactive && !options.quiet) {
263
- console.log(chalk_1.default.green(notice));
264
- }
265
- return { action: "continue", notice };
266
- }
267
- const notice = "workspace not initialized. only /init, /help, /exit are available.";
268
- if (!options.interactive && !options.quiet) {
269
- console.log(chalk_1.default.yellow(notice));
270
- }
271
- return { action: "continue", notice };
272
- }
273
- if (!input.startsWith("/")) {
274
- await appendDialogEntry(cwd, "user", "message", input);
275
- const updated = await upsertDraftIntent(cwd, input, false);
276
- await appendDialogEntry(cwd, "system", "status", updated);
277
- if (!options.interactive && !options.quiet) {
278
- console.log(chalk_1.default.gray(updated));
279
- }
280
- return { action: "continue", notice: updated };
281
- }
282
- const [command, ...args] = input.split(" ");
283
- const rest = args.join(" ").trim();
284
- await appendDialogEntry(cwd, "user", "command", input);
285
- if (command === "/exit" || command === "/quit") {
286
- await appendDialogEntry(cwd, "system", "status", "dialog ended");
287
- return { action: "exit", notice: "dialog ended" };
288
- }
289
- if (command === "/help") {
290
- const help = helpText();
291
- if (!options.interactive && !options.quiet) {
292
- console.log(chalk_1.default.gray(help));
293
- }
294
- await appendDialogEntry(cwd, "system", "status", help);
295
- return { action: "continue", notice: "help shown" };
296
- }
297
- if (command === "/init") {
298
- await (0, init_1.runInit)(false, cwd);
299
- const message = "workspace init checked";
300
- await appendDialogEntry(cwd, "system", "status", message);
301
- if (!options.interactive && !options.quiet) {
302
- console.log(chalk_1.default.gray(message));
303
- }
304
- return { action: "continue", notice: message };
305
- }
306
- if (command === "/status") {
307
- const lines = await buildSessionStatusLines(cwd);
308
- if (!options.interactive && !options.quiet) {
309
- for (const lineItem of lines) {
310
- console.log(chalk_1.default.gray(lineItem));
311
- }
312
- }
313
- await appendDialogEntry(cwd, "system", "status", `status snapshot: ${lines.join(" | ")}`);
314
- return { action: "continue", notice: "status refreshed" };
315
- }
316
- if (command === "/debug") {
317
- if (rest !== "on" && rest !== "off") {
318
- throw new Error("Usage: /debug on|off");
319
- }
320
- await mutateDialogSession(cwd, (session) => {
321
- const normalized = toSessionWithDialog(session);
322
- normalized.dialog.debug = rest === "on";
323
- normalized.updated_at = now();
324
- return normalized;
325
- });
326
- await appendDialogEntry(cwd, "system", "status", `debug mode ${rest}`);
327
- if (!options.interactive && !options.quiet) {
328
- console.log(chalk_1.default.gray(`debug mode: ${rest}`));
329
- }
330
- return { action: "continue", notice: `debug mode ${rest}` };
331
- }
332
- if (command === "/edit") {
333
- if (!rest) {
334
- throw new Error("Usage: /edit <text>");
335
- }
336
- const updated = await upsertDraftIntent(cwd, rest, true);
337
- await appendDialogEntry(cwd, "system", "status", "intent.card replaced");
338
- if (!options.interactive && !options.quiet) {
339
- console.log(chalk_1.default.gray(updated));
340
- }
341
- return { action: "continue", notice: "intent.card replaced" };
342
- }
343
- if (command === "/confirm") {
344
- const confirmed = await (0, intent_1.confirmIntent)(cwd);
345
- const message = `intent confirmed: ${confirmed.intent.intent_id} (state=READY, intent_sha256=${confirmed.binding.intent_sha256})`;
346
- await appendDialogEntry(cwd, "system", "status", message);
347
- if (!options.interactive && !options.quiet) {
348
- console.log(chalk_1.default.green(message));
349
- }
350
- return { action: "continue", notice: `confirmed ${confirmed.intent.intent_id}` };
351
- }
352
- if (command === "/run") {
353
- const roundStart = now();
354
- const session = await requireSessionWithDialog(cwd);
355
- const nextRound = (session.dialog?.rounds.at(-1)?.round ?? 0) + 1;
356
- const output = await (0, run_1.runConfirmedPipeline)(options.sopOut, options.checkpointOut, cwd, {
357
- quiet: true
358
- });
359
- const debug = (await requireSessionWithDialog(cwd)).dialog?.debug ?? false;
360
- const summary = summarizeVerdict(output, debug);
361
- if (!options.interactive && !options.quiet) {
362
- printVerdictLines(summary.lines, output.interpretation.verdict);
363
- }
364
- const reasonCode = output.finalAuditResult.reason_code || "UNKNOWN";
365
- await mutateDialogSession(cwd, (current) => {
366
- const normalized = toSessionWithDialog(current);
367
- normalized.dialog.rounds = [
368
- ...normalized.dialog.rounds,
369
- {
370
- round: nextRound,
371
- started_at: roundStart,
372
- completed_at: now(),
373
- verdict: output.interpretation.verdict,
374
- reason_code: reasonCode,
375
- checkpoint_id: output.checkpoint?.checkpoint_id
376
- }
377
- ].slice(-MAX_DIALOG_ROUNDS);
378
- normalized.updated_at = now();
379
- return normalized;
380
- });
381
- await appendDialogEntry(cwd, "system", "result", `round #${nextRound} ${summary.historyEntry}`);
382
- return { action: "continue", notice: `round #${nextRound} ${summary.historyEntry}` };
383
- }
384
- throw new Error(`Unknown command: ${command}`);
1687
+ return await (0, dialog_handlers_1.dispatchDialogInput)({
1688
+ input: line,
1689
+ initialized,
1690
+ cwd,
1691
+ options,
1692
+ services: dialogHandlerServices,
1693
+ logger: createDialogLogger(options)
1694
+ });
385
1695
  }
386
1696
  async function runDialogueSession(cwd = process.cwd(), options = {}) {
387
1697
  const sopOut = options.sopOut ?? "sop.json";
388
1698
  const checkpointOut = options.checkpointOut ?? "checkpoint.json";
389
1699
  const quiet = options.quiet ?? false;
390
1700
  const scripted = options.script;
1701
+ const launchDebugEnabled = options.debug === true;
1702
+ let launchDebugPending = launchDebugEnabled;
1703
+ const board = createTaskBoardState();
1704
+ const conversations = {
1705
+ index: await (0, dialog_session_1.readConversationIndex)(cwd)
1706
+ };
1707
+ const applyLaunchDebugMode = async () => {
1708
+ if (!launchDebugPending) {
1709
+ return;
1710
+ }
1711
+ const session = await (0, intent_1.readSessionStore)(cwd);
1712
+ if (!session) {
1713
+ return;
1714
+ }
1715
+ const currentDebug = session.dialog?.debug === true;
1716
+ if (!currentDebug) {
1717
+ await mutateDialogSession(cwd, (currentSession) => {
1718
+ const dialog = currentSession.dialog ?? {
1719
+ debug: false,
1720
+ turn: 0,
1721
+ rounds: [],
1722
+ history: []
1723
+ };
1724
+ return {
1725
+ ...currentSession,
1726
+ dialog: {
1727
+ ...dialog,
1728
+ debug: true,
1729
+ rounds: [...dialog.rounds],
1730
+ history: [...dialog.history]
1731
+ },
1732
+ updated_at: now()
1733
+ };
1734
+ });
1735
+ }
1736
+ launchDebugPending = false;
1737
+ };
391
1738
  if (Array.isArray(scripted)) {
1739
+ await applyLaunchDebugMode();
392
1740
  for (const line of scripted) {
393
1741
  const result = await handleDialogInput(line, cwd, {
394
1742
  sopOut,
395
1743
  checkpointOut,
396
1744
  interactive: false,
397
- quiet
1745
+ quiet,
1746
+ board,
1747
+ conversations
398
1748
  });
399
1749
  if (result.action === "exit") {
400
1750
  break;
401
1751
  }
402
1752
  }
1753
+ if (await (0, intent_1.readSessionStore)(cwd)) {
1754
+ await (0, dialog_session_1.persistActiveConversation)(conversations, cwd);
1755
+ }
403
1756
  return;
404
1757
  }
405
1758
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
406
1759
  throw new Error("Dialogue TUI requires a TTY or scripted inputs.");
407
1760
  }
408
- const rl = node_readline_1.default.createInterface({
409
- input: process.stdin,
410
- output: process.stdout
411
- });
412
- let notice = (await (0, intent_1.readSessionStore)(cwd))
413
- ? "输入需求文本更新 intent.card;/confirm 冻结;/run 执行。"
1761
+ const inputSource = new DialogInputBuffer();
1762
+ const frameRenderer = await createPiFrameRenderer(inputSource);
1763
+ await applyLaunchDebugMode();
1764
+ const initialSession = await (0, intent_1.readSessionStore)(cwd);
1765
+ let notice = initialSession
1766
+ ? "输入需求文本更新 intent.card;当系统给出 Build 卡片时输入 1 执行,输入 2 继续调整。"
414
1767
  : "workspace not initialized. run /init first.";
1768
+ let conversationScrollOffset = 0;
1769
+ let latestHistoryCount = 0;
1770
+ let latestVisibleHistory = 8;
1771
+ let activeTurnAbortController = null;
1772
+ let lastEscapeAtMs = 0;
1773
+ let renderFrameRef = null;
1774
+ const applyConversationScrollDelta = (delta, granularity = "page") => {
1775
+ if (!Number.isFinite(delta) || delta === 0) {
1776
+ return false;
1777
+ }
1778
+ const maxScrollOffset = Math.max(0, latestHistoryCount - latestVisibleHistory);
1779
+ const step = granularity === "line"
1780
+ ? 1
1781
+ : Math.max(3, Math.floor(latestVisibleHistory / 2));
1782
+ const next = Math.max(0, Math.min(maxScrollOffset, conversationScrollOffset + (delta * step)));
1783
+ if (next === conversationScrollOffset) {
1784
+ return false;
1785
+ }
1786
+ conversationScrollOffset = next;
1787
+ return true;
1788
+ };
1789
+ const detachGlobalInputListener = inputSource.subscribe((event) => {
1790
+ const wheelDeltas = parseMouseWheelDeltas(event.sequence);
1791
+ if (wheelDeltas.length > 0) {
1792
+ let changed = false;
1793
+ for (const delta of wheelDeltas) {
1794
+ changed = applyConversationScrollDelta(delta) || changed;
1795
+ }
1796
+ if (changed && renderFrameRef) {
1797
+ void renderFrameRef("", undefined);
1798
+ }
1799
+ return;
1800
+ }
1801
+ if (event.key.name === "escape" && activeTurnAbortController) {
1802
+ const nowMs = Date.now();
1803
+ if (nowMs - lastEscapeAtMs <= ESC_DOUBLE_PRESS_WINDOW_MS) {
1804
+ lastEscapeAtMs = 0;
1805
+ if (!activeTurnAbortController.signal.aborted) {
1806
+ activeTurnAbortController.abort();
1807
+ }
1808
+ notice = "assistant reply cancelling...";
1809
+ if (renderFrameRef) {
1810
+ void renderFrameRef("", undefined);
1811
+ }
1812
+ return;
1813
+ }
1814
+ lastEscapeAtMs = nowMs;
1815
+ notice = "press Esc again to cancel current reply";
1816
+ if (renderFrameRef) {
1817
+ void renderFrameRef("", undefined);
1818
+ }
1819
+ return;
1820
+ }
1821
+ });
415
1822
  try {
416
1823
  while (true) {
417
1824
  const session = await (0, intent_1.readSessionStore)(cwd);
418
- let promptState = "uninitialized";
419
1825
  if (session) {
420
- const rendered = await renderDialogueScreen(cwd, notice);
421
- promptState = rendered.state.toLowerCase();
1826
+ await applyLaunchDebugMode();
422
1827
  }
423
- else {
424
- renderBootstrapScreen(notice);
1828
+ if (session) {
1829
+ await requireSessionWithDialog(cwd);
1830
+ }
1831
+ let activeModelInfo = "-";
1832
+ let renderFrameGeneration = 0;
1833
+ const renderFrame = async (buffer, completion, commandMenu, cursorVisible, commandMenuLabel, inputCursorIndex) => {
1834
+ const generation = ++renderFrameGeneration;
1835
+ const latestSession = await (0, intent_1.readSessionStore)(cwd);
1836
+ if (latestSession) {
1837
+ const sessionWithDialog = await requireSessionWithDialog(cwd);
1838
+ const draftIntent = await (0, intent_1.readIntentCard)(cwd);
1839
+ if (generation !== renderFrameGeneration) {
1840
+ return;
1841
+ }
1842
+ const renderMeta = renderDialogueScreen(conversations.index.active_id || dialog_session_1.DEFAULT_CONVERSATION_ID, sessionWithDialog, draftIntent, board, notice, activeModelInfo, conversationScrollOffset, buffer, completion, commandMenu, cursorVisible, commandMenuLabel, inputCursorIndex, frameRenderer.render);
1843
+ conversationScrollOffset = renderMeta.scrollOffset;
1844
+ latestHistoryCount = renderMeta.totalHistory;
1845
+ latestVisibleHistory = renderMeta.visibleHistory;
1846
+ return;
1847
+ }
1848
+ if (generation !== renderFrameGeneration) {
1849
+ return;
1850
+ }
1851
+ latestHistoryCount = 0;
1852
+ latestVisibleHistory = 8;
1853
+ conversationScrollOffset = 0;
1854
+ renderBootstrapScreen(conversations.index.active_id || dialog_session_1.DEFAULT_CONVERSATION_ID, notice, activeModelInfo, buffer, completion, commandMenu, cursorVisible, commandMenuLabel, inputCursorIndex, frameRenderer.render);
1855
+ };
1856
+ renderFrameRef = renderFrame;
1857
+ const conversationIds = await (0, dialog_session_1.listConversationIds)(conversations.index, cwd);
1858
+ let modelMenu = EMPTY_MODEL_MENU_SNAPSHOT;
1859
+ try {
1860
+ const config = await (0, config_1.readConfig)(cwd);
1861
+ modelMenu = (0, model_accounts_1.buildModelMenuSnapshot)(config);
1862
+ activeModelInfo = formatTuiModelInfo(config);
1863
+ }
1864
+ catch {
1865
+ modelMenu = EMPTY_MODEL_MENU_SNAPSHOT;
1866
+ activeModelInfo = "-";
425
1867
  }
426
- const prompt = `zaker:${promptState} > `;
427
- const line = await new Promise((resolve) => {
428
- rl.question(prompt, resolve);
1868
+ let line = await readInteractiveInput(async (buffer, completion, commandMenu, cursorVisible, commandMenuLabel, inputCursorIndex) => {
1869
+ await renderFrame(buffer, completion, commandMenu, cursorVisible, commandMenuLabel, inputCursorIndex);
1870
+ }, (buffer) => resolveSlashMenuCandidates(buffer, conversationIds, modelMenu), inputSource, modelMenu, (key) => {
1871
+ if (key.name === "pageup") {
1872
+ applyConversationScrollDelta(1);
1873
+ return;
1874
+ }
1875
+ if (key.name === "pagedown") {
1876
+ applyConversationScrollDelta(-1);
1877
+ return;
1878
+ }
1879
+ const maxScrollOffset = Math.max(0, latestHistoryCount - latestVisibleHistory);
1880
+ if (key.name === "home") {
1881
+ conversationScrollOffset = maxScrollOffset;
1882
+ return;
1883
+ }
1884
+ if (key.name === "end") {
1885
+ conversationScrollOffset = 0;
1886
+ }
429
1887
  });
1888
+ if (line.trim() === "/resume" && (await (0, intent_1.readSessionStore)(cwd))) {
1889
+ const activeConversationId = conversations.index.active_id || dialog_session_1.DEFAULT_CONVERSATION_ID;
1890
+ const ids = await (0, dialog_session_1.listConversationIds)(conversations.index, cwd);
1891
+ const pickerItems = ids.map((id) => ({
1892
+ id,
1893
+ label: id === activeConversationId ? `${id} (active)` : id
1894
+ }));
1895
+ const initialSelectedIndex = Math.max(0, pickerItems.findIndex((item) => item.id === activeConversationId));
1896
+ const pickerResult = await readInteractiveResumePicker(async (buffer, completion, commandMenu, cursorVisible, commandMenuLabel) => {
1897
+ await renderFrame(buffer, completion, commandMenu, cursorVisible, commandMenuLabel);
1898
+ }, inputSource, pickerItems, initialSelectedIndex);
1899
+ if (pickerResult.action === "exit") {
1900
+ break;
1901
+ }
1902
+ if (pickerResult.action === "cancel") {
1903
+ notice = "resume cancelled";
1904
+ await renderFrame("", undefined);
1905
+ continue;
1906
+ }
1907
+ line = `/resume ${pickerResult.id}`;
1908
+ }
1909
+ const loginResolution = await resolveInteractiveLoginCommand({
1910
+ line,
1911
+ cwd,
1912
+ input_source: inputSource,
1913
+ prompt: async (promptNotice) => {
1914
+ const previousNotice = notice;
1915
+ notice = promptNotice;
1916
+ const prompted = await readInteractiveInput(async (buffer, completion, commandMenu, cursorVisible, commandMenuLabel, inputCursorIndex) => {
1917
+ await renderFrame(buffer, completion, commandMenu, cursorVisible, commandMenuLabel, inputCursorIndex);
1918
+ }, () => [], inputSource, EMPTY_MODEL_MENU_SNAPSHOT);
1919
+ notice = previousNotice;
1920
+ await renderFrame("", undefined);
1921
+ return prompted;
1922
+ },
1923
+ oauth_browser: {
1924
+ enabled: true,
1925
+ notify: async (oauthNotice) => {
1926
+ notice = oauthNotice;
1927
+ await renderFrame("", undefined);
1928
+ }
1929
+ }
1930
+ });
1931
+ if (loginResolution.action === "exit") {
1932
+ break;
1933
+ }
1934
+ if (loginResolution.action === "cancel") {
1935
+ notice = loginResolution.notice ?? "login cancelled";
1936
+ await renderFrame("", undefined);
1937
+ continue;
1938
+ }
1939
+ line = loginResolution.line ?? line;
1940
+ const turnAbortController = new AbortController();
1941
+ activeTurnAbortController = turnAbortController;
1942
+ lastEscapeAtMs = 0;
1943
+ conversationScrollOffset = 0;
430
1944
  try {
431
1945
  const result = await handleDialogInput(line, cwd, {
432
1946
  sopOut,
433
1947
  checkpointOut,
434
1948
  interactive: true,
435
- quiet
1949
+ quiet,
1950
+ turnAbortSignal: turnAbortController.signal,
1951
+ board,
1952
+ conversations,
1953
+ redraw: async (nextNotice) => {
1954
+ if (typeof nextNotice === "string") {
1955
+ notice = nextNotice;
1956
+ }
1957
+ await renderFrame("", undefined);
1958
+ }
436
1959
  });
437
1960
  notice = result.notice ?? "";
438
1961
  if (result.action === "exit") {
@@ -446,10 +1969,33 @@ async function runDialogueSession(cwd = process.cwd(), options = {}) {
446
1969
  await appendDialogEntry(cwd, "system", "status", `error: ${message}`);
447
1970
  }
448
1971
  }
1972
+ finally {
1973
+ activeTurnAbortController = null;
1974
+ lastEscapeAtMs = 0;
1975
+ }
449
1976
  }
450
1977
  }
451
1978
  finally {
452
- rl.close();
1979
+ let persistWarning;
1980
+ try {
1981
+ await frameRenderer.dispose();
1982
+ }
1983
+ catch {
1984
+ // ignore renderer teardown failures to prioritize terminal restore
1985
+ }
1986
+ try {
1987
+ if (await (0, intent_1.readSessionStore)(cwd)) {
1988
+ await (0, dialog_session_1.persistActiveConversation)(conversations, cwd);
1989
+ }
1990
+ }
1991
+ catch (error) {
1992
+ persistWarning = error instanceof Error ? error.message : String(error);
1993
+ }
1994
+ detachGlobalInputListener();
1995
+ inputSource.clear();
1996
+ if (persistWarning) {
1997
+ process.stderr.write(`zaker: warning: failed to persist active conversation on exit: ${persistWarning}\n`);
1998
+ }
453
1999
  }
454
2000
  }
455
2001
  function registerDialogCommand(program) {
@@ -458,10 +2004,12 @@ function registerDialogCommand(program) {
458
2004
  .description("interactive contract-gated dialogue TUI")
459
2005
  .option("-o, --sop-out <path>", "sop output path", "sop.json")
460
2006
  .option("-c, --checkpoint-out <path>", "checkpoint output path", "checkpoint.json")
2007
+ .option("--debug", "start dialogue with debug mode on", false)
461
2008
  .action(async (options) => {
462
2009
  await runDialogueSession(process.cwd(), {
463
2010
  sopOut: options.sopOut,
464
- checkpointOut: options.checkpointOut
2011
+ checkpointOut: options.checkpointOut,
2012
+ debug: options.debug === true
465
2013
  });
466
2014
  });
467
2015
  }