@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.
- package/dist/commands/app-server.d.ts +67 -0
- package/dist/commands/app-server.js +601 -0
- package/dist/commands/app-server.js.map +1 -0
- package/dist/commands/audit.js +2 -1
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/build.d.ts +3 -0
- package/dist/commands/build.js +22 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/confirm.d.ts +0 -2
- package/dist/commands/confirm.js +2 -16
- package/dist/commands/confirm.js.map +1 -1
- package/dist/commands/dialog-handlers/auth.d.ts +3 -0
- package/dist/commands/dialog-handlers/auth.js +174 -0
- package/dist/commands/dialog-handlers/auth.js.map +1 -0
- package/dist/commands/dialog-handlers/basic.d.ts +7 -0
- package/dist/commands/dialog-handlers/basic.js +82 -0
- package/dist/commands/dialog-handlers/basic.js.map +1 -0
- package/dist/commands/dialog-handlers/bootstrap.d.ts +10 -0
- package/dist/commands/dialog-handlers/bootstrap.js +38 -0
- package/dist/commands/dialog-handlers/bootstrap.js.map +1 -0
- package/dist/commands/dialog-handlers/index.d.ts +2 -0
- package/dist/commands/dialog-handlers/index.js +6 -0
- package/dist/commands/dialog-handlers/index.js.map +1 -0
- package/dist/commands/dialog-handlers/message.d.ts +2 -0
- package/dist/commands/dialog-handlers/message.js +103 -0
- package/dist/commands/dialog-handlers/message.js.map +1 -0
- package/dist/commands/dialog-handlers/model.d.ts +2 -0
- package/dist/commands/dialog-handlers/model.js +168 -0
- package/dist/commands/dialog-handlers/model.js.map +1 -0
- package/dist/commands/dialog-handlers/new.d.ts +2 -0
- package/dist/commands/dialog-handlers/new.js +53 -0
- package/dist/commands/dialog-handlers/new.js.map +1 -0
- package/dist/commands/dialog-handlers/resume.d.ts +2 -0
- package/dist/commands/dialog-handlers/resume.js +25 -0
- package/dist/commands/dialog-handlers/resume.js.map +1 -0
- package/dist/commands/dialog-handlers/router.d.ts +11 -0
- package/dist/commands/dialog-handlers/router.js +112 -0
- package/dist/commands/dialog-handlers/router.js.map +1 -0
- package/dist/commands/dialog-handlers/run.d.ts +2 -0
- package/dist/commands/dialog-handlers/run.js +161 -0
- package/dist/commands/dialog-handlers/run.js.map +1 -0
- package/dist/commands/dialog-handlers/status.d.ts +2 -0
- package/dist/commands/dialog-handlers/status.js +13 -0
- package/dist/commands/dialog-handlers/status.js.map +1 -0
- package/dist/commands/dialog-handlers/types.d.ts +107 -0
- package/dist/commands/dialog-handlers/types.js +3 -0
- package/dist/commands/dialog-handlers/types.js.map +1 -0
- package/dist/commands/dialog.d.ts +92 -0
- package/dist/commands/dialog.js +1784 -236
- package/dist/commands/dialog.js.map +1 -1
- package/dist/commands/init.d.ts +3 -1
- package/dist/commands/init.js +6 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/plan.js +10 -6
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/run.js +8 -10
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/status.js +6 -12
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/tui-launcher.d.ts +1 -0
- package/dist/commands/tui-launcher.js +8 -0
- package/dist/commands/tui-launcher.js.map +1 -0
- package/dist/core/alignment-reply.d.ts +1 -0
- package/dist/core/alignment-reply.js +44 -0
- package/dist/core/alignment-reply.js.map +1 -0
- package/dist/core/checkpoint.js +3 -1
- package/dist/core/checkpoint.js.map +1 -1
- package/dist/core/planner.d.ts +16 -16
- package/dist/core/planner.js +3 -1
- package/dist/core/planner.js.map +1 -1
- package/dist/core/planning-prep.d.ts +12 -0
- package/dist/core/planning-prep.js +26 -0
- package/dist/core/planning-prep.js.map +1 -0
- package/dist/core/preflight.js +1 -2
- package/dist/core/preflight.js.map +1 -1
- package/dist/core/provider-onboarding.js +6 -11
- package/dist/core/provider-onboarding.js.map +1 -1
- package/dist/core/readonly-checkpoint.d.ts +2 -0
- package/dist/core/readonly-checkpoint.js +35 -0
- package/dist/core/readonly-checkpoint.js.map +1 -0
- package/dist/core/run-loop.js +3 -1
- package/dist/core/run-loop.js.map +1 -1
- package/dist/core/types.d.ts +20 -1
- package/dist/index.js +20 -6
- package/dist/index.js.map +1 -1
- package/dist/infra/artifact-schema.d.ts +25 -0
- package/dist/infra/artifact-schema.js +353 -0
- package/dist/infra/artifact-schema.js.map +1 -0
- package/dist/infra/config.d.ts +14 -1
- package/dist/infra/config.js +542 -22
- package/dist/infra/config.js.map +1 -1
- package/dist/infra/dependency-report.d.ts +5 -0
- package/dist/infra/dependency-report.js +22 -0
- package/dist/infra/dependency-report.js.map +1 -0
- package/dist/infra/dialog-session.d.ts +29 -0
- package/dist/infra/dialog-session.js +244 -0
- package/dist/infra/dialog-session.js.map +1 -0
- package/dist/infra/intent.js +63 -13
- package/dist/infra/intent.js.map +1 -1
- package/dist/infra/model-accounts.d.ts +22 -0
- package/dist/infra/model-accounts.js +172 -0
- package/dist/infra/model-accounts.js.map +1 -0
- package/dist/infra/model-catalog.d.ts +4 -1
- package/dist/infra/model-catalog.js +102 -27
- package/dist/infra/model-catalog.js.map +1 -1
- package/dist/infra/openai-codex-oauth.d.ts +18 -0
- package/dist/infra/openai-codex-oauth.js +267 -0
- package/dist/infra/openai-codex-oauth.js.map +1 -0
- package/dist/infra/provider-registry.d.ts +36 -0
- package/dist/infra/provider-registry.js +403 -0
- package/dist/infra/provider-registry.js.map +1 -0
- package/dist/infra/session-status.d.ts +6 -0
- package/dist/infra/session-status.js +34 -0
- package/dist/infra/session-status.js.map +1 -0
- package/dist/infra/tui-utils.d.ts +6 -0
- package/dist/infra/tui-utils.js +163 -0
- package/dist/infra/tui-utils.js.map +1 -0
- package/dist/infra/tui-view.d.ts +44 -0
- package/dist/infra/tui-view.js +314 -0
- package/dist/infra/tui-view.js.map +1 -0
- package/package.json +4 -1
package/dist/commands/dialog.js
CHANGED
|
@@ -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 | /
|
|
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
|
|
32
|
-
|
|
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: [...
|
|
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
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
1535
|
+
leftContent.push("(empty)");
|
|
194
1536
|
}
|
|
195
1537
|
else {
|
|
196
|
-
for (const entry of
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
let notice =
|
|
413
|
-
? "输入需求文本更新 intent.card
|
|
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
|
-
|
|
421
|
-
promptState = rendered.state.toLowerCase();
|
|
1826
|
+
await applyLaunchDebugMode();
|
|
422
1827
|
}
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
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
|
}
|