lark-coding-assistant 0.1.1

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.
@@ -0,0 +1,2794 @@
1
+ // src/daemon/server.ts
2
+ import { appendFile, chmod as chmod3, open as open2, readFile as readFile2, rm } from "fs/promises";
3
+ import { createServer } from "net";
4
+
5
+ // src/platform/process.ts
6
+ import { execFile, spawn } from "child_process";
7
+ function runFile(file, args, options = {}) {
8
+ return new Promise((resolve, reject) => {
9
+ execFile(
10
+ file,
11
+ [...args],
12
+ {
13
+ cwd: options.cwd,
14
+ timeout: options.timeoutMs ?? 1e4,
15
+ encoding: "utf8",
16
+ maxBuffer: 4 * 1024 * 1024
17
+ },
18
+ (error, stdout, stderr) => {
19
+ if (error) {
20
+ reject(Object.assign(error, { stdout, stderr }));
21
+ return;
22
+ }
23
+ resolve({ stdout, stderr });
24
+ }
25
+ );
26
+ });
27
+ }
28
+ function runFileWithInput(file, args, input) {
29
+ return new Promise((resolve, reject) => {
30
+ const child = spawn(file, [...args], { stdio: ["pipe", "pipe", "pipe"] });
31
+ let stdout = "";
32
+ let stderr = "";
33
+ child.stdout.setEncoding("utf8");
34
+ child.stderr.setEncoding("utf8");
35
+ child.stdout.on("data", (chunk) => stdout += chunk);
36
+ child.stderr.on("data", (chunk) => stderr += chunk);
37
+ child.once("error", reject);
38
+ child.once("exit", (code, signal) => {
39
+ if (code === 0) resolve({ stdout, stderr });
40
+ else reject(new Error(`${file} exited with ${code ?? signal}: ${stderr.trim()}`));
41
+ });
42
+ child.stdin.end(input, "utf8");
43
+ });
44
+ }
45
+
46
+ // src/screen/normalize.ts
47
+ var ANSI = /\u001b(?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
48
+ function stripAnsi(raw) {
49
+ return raw.replace(ANSI, "");
50
+ }
51
+ function normalizeScreen(raw) {
52
+ return stripAnsi(raw).replace(/\r/g, "").split("\n").map((line) => line.replace(/\u00a0|\u3000/g, " ").replace(/\t/g, " ").replace(/ +$/g, "")).filter((line, index, all) => line !== "" || all[index - 1] !== "").join("\n").trim();
53
+ }
54
+ function tailScreen(raw, lines = 60) {
55
+ return normalizeScreen(raw).split("\n").slice(-lines).join("\n");
56
+ }
57
+
58
+ // src/core/store.ts
59
+ import { randomBytes, scryptSync, timingSafeEqual } from "crypto";
60
+ import { chmod as chmod2, mkdir as mkdir2 } from "fs/promises";
61
+
62
+ // src/core/model.ts
63
+ function emptyState(now = Date.now()) {
64
+ return { schemaVersion: 2, sessions: {}, updatedAt: now };
65
+ }
66
+
67
+ // src/core/atomic-json.ts
68
+ import { chmod, mkdir, open, readFile, rename } from "fs/promises";
69
+ import { dirname } from "path";
70
+ async function readJson(path) {
71
+ try {
72
+ return JSON.parse(await readFile(path, "utf8"));
73
+ } catch (error) {
74
+ if (error.code === "ENOENT") return void 0;
75
+ throw error;
76
+ }
77
+ }
78
+ async function writeJsonAtomic(path, value, mode = 384) {
79
+ const parent = dirname(path);
80
+ await mkdir(parent, { recursive: true, mode: 448 });
81
+ await chmod(parent, 448).catch(() => void 0);
82
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
83
+ const file = await open(temporary, "w", mode);
84
+ try {
85
+ await file.writeFile(`${JSON.stringify(value, null, 2)}
86
+ `, "utf8");
87
+ await file.sync();
88
+ } finally {
89
+ await file.close();
90
+ }
91
+ await rename(temporary, path);
92
+ await chmod(path, mode);
93
+ }
94
+
95
+ // src/core/store.ts
96
+ var AppStore = class {
97
+ constructor(paths2) {
98
+ this.paths = paths2;
99
+ }
100
+ paths;
101
+ async ensure() {
102
+ await mkdir2(this.paths.root, { recursive: true, mode: 448 });
103
+ await mkdir2(this.paths.runtimeDir, { recursive: true, mode: 448 });
104
+ await mkdir2(this.paths.logsDir, { recursive: true, mode: 448 });
105
+ await Promise.all([
106
+ chmod2(this.paths.root, 448),
107
+ chmod2(this.paths.runtimeDir, 448),
108
+ chmod2(this.paths.logsDir, 448)
109
+ ]);
110
+ }
111
+ loadConfig() {
112
+ return readJson(this.paths.config);
113
+ }
114
+ saveConfig(config) {
115
+ return writeJsonAtomic(this.paths.config, config);
116
+ }
117
+ loadSecrets() {
118
+ return readJson(this.paths.secrets);
119
+ }
120
+ saveSecrets(secrets) {
121
+ return writeJsonAtomic(this.paths.secrets, secrets);
122
+ }
123
+ async loadState() {
124
+ return await readJson(this.paths.state) ?? emptyState();
125
+ }
126
+ saveState(state) {
127
+ return writeJsonAtomic(this.paths.state, state);
128
+ }
129
+ };
130
+ function createBindCode() {
131
+ return randomBytes(16).toString("base64url");
132
+ }
133
+ function hashBindCode(code) {
134
+ const salt = randomBytes(16);
135
+ const digest = scryptSync(code, salt, 32);
136
+ return `${salt.toString("base64url")}.${digest.toString("base64url")}`;
137
+ }
138
+ function verifyBindCode(code, encoded) {
139
+ const [saltText, digestText] = encoded.split(".");
140
+ if (!saltText || !digestText) return false;
141
+ const expected = Buffer.from(digestText, "base64url");
142
+ const actual = scryptSync(code, Buffer.from(saltText, "base64url"), expected.length);
143
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
144
+ }
145
+
146
+ // src/tmux/controller.ts
147
+ import { randomBytes as randomBytes2 } from "crypto";
148
+
149
+ // src/tmux/input.ts
150
+ var ANSI2 = /\u001b(?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
151
+ var FORBIDDEN_CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
152
+ function sanitizeRemoteInput(input) {
153
+ return input.replace(ANSI2, "").replace(/\r\n?/g, "\n").replace(FORBIDDEN_CONTROL, "");
154
+ }
155
+ function assertSafeTmuxTarget(target) {
156
+ if (!/^%\d+$/.test(target)) throw new Error(`unsafe tmux pane target: ${target}`);
157
+ }
158
+ function shellQuote(value) {
159
+ return `'${value.replace(/'/g, `'\\''`)}'`;
160
+ }
161
+
162
+ // src/tmux/controller.ts
163
+ var PANE_FORMAT = "#{session_name} #{pane_id} #{pane_pid} #{pane_current_command} #{pane_current_path} #{pane_dead} #{cursor_x} #{cursor_y}";
164
+ var TmuxController = class {
165
+ constructor(binary = "tmux") {
166
+ this.binary = binary;
167
+ }
168
+ binary;
169
+ writes = Promise.resolve();
170
+ async version() {
171
+ return (await runFile(this.binary, ["-V"])).stdout.trim();
172
+ }
173
+ async create(options) {
174
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(options.sessionName)) {
175
+ throw new Error("tmux session name must contain only letters, digits, underscore, or dash");
176
+ }
177
+ if (await this.hasSession(options.sessionName)) {
178
+ throw new Error(`tmux session already exists: ${options.sessionName}`);
179
+ }
180
+ const environment = Object.entries(options.env ?? {}).map(([key, value]) => {
181
+ if (!/^[A-Z][A-Z0-9_]*$/.test(key)) throw new Error(`unsafe tmux environment key: ${key}`);
182
+ return `${key}=${value}`;
183
+ });
184
+ const command = [
185
+ ...environment.length > 0 ? ["/usr/bin/env", ...environment] : [],
186
+ options.binary,
187
+ ...options.args ?? []
188
+ ].map(shellQuote).join(" ");
189
+ await runFile(this.binary, [
190
+ "new-session",
191
+ "-d",
192
+ "-s",
193
+ options.sessionName,
194
+ "-c",
195
+ options.cwd,
196
+ "-x",
197
+ "120",
198
+ "-y",
199
+ "40",
200
+ command
201
+ ]);
202
+ const pane = await this.findBySession(options.sessionName);
203
+ if (!pane) throw new Error("tmux created a session without a discoverable pane");
204
+ return pane;
205
+ }
206
+ async hasSession(sessionName) {
207
+ try {
208
+ await runFile(this.binary, ["has-session", "-t", `=${sessionName}`]);
209
+ return true;
210
+ } catch {
211
+ return false;
212
+ }
213
+ }
214
+ async findBySession(sessionName) {
215
+ const { stdout } = await runFile(this.binary, [
216
+ "list-panes",
217
+ "-t",
218
+ `=${sessionName}`,
219
+ "-F",
220
+ PANE_FORMAT
221
+ ]);
222
+ return stdout.split("\n").map(parsePane).find(Boolean);
223
+ }
224
+ async inspect(paneId) {
225
+ assertSafeTmuxTarget(paneId);
226
+ try {
227
+ const { stdout } = await runFile(this.binary, [
228
+ "display-message",
229
+ "-p",
230
+ "-t",
231
+ paneId,
232
+ PANE_FORMAT
233
+ ]);
234
+ return parsePane(stdout.trim());
235
+ } catch {
236
+ return void 0;
237
+ }
238
+ }
239
+ async capture(paneId, lines = 200) {
240
+ assertSafeTmuxTarget(paneId);
241
+ const { stdout } = await runFile(this.binary, [
242
+ "capture-pane",
243
+ "-p",
244
+ "-J",
245
+ "-e",
246
+ "-t",
247
+ paneId,
248
+ "-S",
249
+ `-${Math.max(1, lines)}`
250
+ ]);
251
+ return stdout;
252
+ }
253
+ sendText(paneId, input, submit = true) {
254
+ assertSafeTmuxTarget(paneId);
255
+ const text = sanitizeRemoteInput(input);
256
+ if (!text.trim()) return Promise.reject(new Error("message is empty after sanitization"));
257
+ const operation = async () => {
258
+ const bufferName = `lca-${process.pid}-${randomBytes2(6).toString("hex")}`;
259
+ await runFileWithInput(this.binary, ["load-buffer", "-b", bufferName, "-"], text);
260
+ try {
261
+ await runFile(this.binary, ["paste-buffer", "-b", bufferName, "-d", "-t", paneId]);
262
+ } finally {
263
+ await runFile(this.binary, ["delete-buffer", "-b", bufferName]).catch(() => void 0);
264
+ }
265
+ if (submit) await runFile(this.binary, ["send-keys", "-t", paneId, "Enter"]);
266
+ };
267
+ this.writes = this.writes.then(operation, operation);
268
+ return this.writes;
269
+ }
270
+ async sendKey(paneId, key) {
271
+ assertSafeTmuxTarget(paneId);
272
+ if (!/^(Enter|Escape|Space|Tab|BSpace|Up|Down|Left|Right|C-c|C-u|C-k|C-Enter|[yandpcq1-9])$/.test(key)) {
273
+ throw new Error(`unsupported tmux key: ${key}`);
274
+ }
275
+ await runFile(this.binary, ["send-keys", "-t", paneId, key]);
276
+ }
277
+ async killSession(sessionName) {
278
+ await runFile(this.binary, ["kill-session", "-t", `=${sessionName}`]);
279
+ }
280
+ };
281
+ function parsePane(line) {
282
+ const [sessionName, paneId, pidText, currentCommand, cwd, deadText, cursorXText, cursorYText] = line.trim().split(" ");
283
+ const pid = Number(pidText);
284
+ const cursorX = Number(cursorXText);
285
+ const cursorY = Number(cursorYText);
286
+ if (!sessionName || !paneId || !Number.isInteger(pid) || !currentCommand || !cwd || !Number.isInteger(cursorX) || !Number.isInteger(cursorY)) return void 0;
287
+ return { sessionName, paneId, pid, currentCommand, cwd, dead: deadText === "1", cursorX, cursorY };
288
+ }
289
+
290
+ // src/screen/detector.ts
291
+ import { createHash } from "crypto";
292
+
293
+ // src/screen/dialects.ts
294
+ var commonHeaders = [
295
+ /^\s*(?:Would you like to|Do you want to|Approval required|Allow command|.*requires approval)/i
296
+ ];
297
+ var commonFooters = [
298
+ /(?:press\s+)?enter\s+to\s+(?:confirm|submit(?:\s+answer)?|select|choose|continue)/i,
299
+ /esc\s+to\s+cancel.*(?:tab\s+to\s+amend|ctrl\+e\s+to\s+explain)/i
300
+ ];
301
+ var CODEX_DIALECT = {
302
+ id: "codex",
303
+ headerPatterns: [...commonHeaders, /^\s*Question\s+\d+\/\d+/i],
304
+ footerPatterns: commonFooters,
305
+ submitControls: [/^(?:submit|confirm)$/i],
306
+ customInputControls: [/^(?:type something|none of the above|add notes)$/i],
307
+ chatControls: [/^chat about this$/i]
308
+ };
309
+ var TRAE_DIALECT = {
310
+ ...CODEX_DIALECT,
311
+ id: "trae-cli",
312
+ headerPatterns: [...commonHeaders, /^\s*Question\s+\d+\/\d+/i],
313
+ customInputControls: [/^(?:other|none of the above|add notes)$/i]
314
+ };
315
+ var CLAUDE_DIALECT = {
316
+ id: "claude-code",
317
+ headerPatterns: [
318
+ ...commonHeaders,
319
+ /^\s*(?:←\s*)?[☐☑☒]\s+.+?(?:\s+✔\s+Submit\s*→)?\s*$/i,
320
+ /^\s*[☐☑☒]\s+\S/
321
+ ],
322
+ footerPatterns: commonFooters,
323
+ footerlessChoiceHeaders: [/^ready to submit your answers\?$/i],
324
+ submitControls: [/^(?:submit|confirm)$/i],
325
+ customInputControls: [/^(?:type(?:\s+.+)?|add notes)\.?$/i, /^notes:\s*press\s+n\s+to\s+add\b/i],
326
+ directInputControls: [/^type(?:\s+.+)?\.?$/i],
327
+ customInputValuePattern: /^type\s+(.+?)\.?$/i,
328
+ chatControls: [/^chat about this$/i]
329
+ };
330
+
331
+ // src/screen/detector.ts
332
+ function detectCodexScreen(raw, paneAlive = true, cursor) {
333
+ return detectAgentScreen(raw, paneAlive, {
334
+ brandPattern: /OpenAI Codex|codex/i,
335
+ brandEvidence: "codex",
336
+ failurePattern: /fatal error|panicked at|segmentation fault|codex exited/i,
337
+ dialect: CODEX_DIALECT
338
+ }, cursor);
339
+ }
340
+ function detectTraeScreen(raw, paneAlive = true, cursor) {
341
+ return detectAgentScreen(raw, paneAlive, {
342
+ brandPattern: /TraeCode CLI|traecli/i,
343
+ brandEvidence: "trae-cli",
344
+ failurePattern: /fatal error|panicked at|segmentation fault|traecli exited/i,
345
+ dialect: TRAE_DIALECT
346
+ }, cursor);
347
+ }
348
+ function detectClaudeScreen(raw, paneAlive = true, cursor) {
349
+ return detectAgentScreen(raw, paneAlive, {
350
+ brandPattern: /Claude Code/i,
351
+ brandEvidence: "claude-code",
352
+ failurePattern: /fatal error|segmentation fault|Claude Code exited/i,
353
+ dialect: CLAUDE_DIALECT
354
+ }, cursor);
355
+ }
356
+ function detectAgentScreen(raw, paneAlive, options, cursor) {
357
+ const normalized = normalizeScreen(raw);
358
+ const lines = normalized.split("\n").slice(-80);
359
+ const tail = lines.join("\n");
360
+ const bottom = lines.slice(-16);
361
+ const bottomTail = bottom.join("\n");
362
+ const fingerprint = createHash("sha256").update(tail).digest("hex");
363
+ if (!paneAlive) return result("exited", 1, normalized, fingerprint, ["pane exited"]);
364
+ if (options.failurePattern.test(bottomTail)) {
365
+ return result("failed", 0.95, normalized, fingerprint, matching(bottom, /fatal|panic|exited/i));
366
+ }
367
+ const choice = parseChoiceInteraction(lines.slice(-60), options.dialect);
368
+ if (choice) {
369
+ const choiceFingerprint = choice.interaction?.interactionId ?? createHash("sha256").update(choice.evidence.join("\n")).digest("hex");
370
+ return { ...choice, normalized, fingerprint: choiceFingerprint, hasDraftInput: false };
371
+ }
372
+ const inputEvidence = matching(bottom, /(?:select an option|choose one|enter your answer|provide .*input|request_user_input|do you trust|trust the contents)/i);
373
+ if (inputEvidence.length > 0) return result("input", 0.72, normalized, fingerprint, inputEvidence);
374
+ const activeRunningPattern = /(?:model:\s+loading|esc to interrupt|running\s+.+?hooks?|^\s*[•◦◆◇◈✦✻▍]?\s*(?:working|thinking|executing)(?:…|\s*\(|$))/i;
375
+ const activeRunningEvidence = matching(bottom, activeRunningPattern);
376
+ if (activeRunningEvidence.length > 0) return result("running", 0.85, normalized, fingerprint, activeRunningEvidence);
377
+ const runningPattern = /(?:waiting for)/i;
378
+ const runningEvidence = matching(bottom, runningPattern);
379
+ const lastRunningIndex = lastMatchingIndex(bottom, runningPattern);
380
+ const lastPromptIndex = lastMatchingIndex(bottom, /^\s*[›>❯]\s?/);
381
+ if (lastRunningIndex > lastPromptIndex) return result("running", 0.75, normalized, fingerprint, runningEvidence);
382
+ const promptLines = bottom.slice(-4).filter((line) => /^\s*[›>❯]\s?/.test(line));
383
+ if (promptLines.length > 0) {
384
+ const last = promptLines.at(-1) ?? "";
385
+ const content = last.replace(/^\s*[›>❯]\s?/, "").trim();
386
+ const rawPrompt = raw.split("\n").filter((line) => /^\s*[›>❯]\s?/.test(stripAnsi(line))).at(-1) ?? "";
387
+ const draft = content.length > 0 && !promptContentIsDim(rawPrompt) && !cursorIsAtPromptStart(rawPrompt, cursor?.x);
388
+ return { ...result("idle", 0.8, normalized, fingerprint, [last]), hasDraftInput: draft };
389
+ }
390
+ if (runningEvidence.length > 0) return result("running", 0.75, normalized, fingerprint, runningEvidence);
391
+ if (options.brandPattern.test(tail)) {
392
+ return result("starting", 0.55, normalized, fingerprint, [options.brandEvidence]);
393
+ }
394
+ return result("unknown", 0.2, normalized, fingerprint, []);
395
+ }
396
+ function cursorIsAtPromptStart(rawLine, cursorX) {
397
+ if (cursorX === void 0) return false;
398
+ const visible = stripAnsi(rawLine);
399
+ const marker = visible.search(/[›>❯]/);
400
+ if (marker === -1) return false;
401
+ let contentStart = marker + 1;
402
+ while (/\s/.test(visible[contentStart] ?? "")) contentStart += 1;
403
+ return cursorX === contentStart;
404
+ }
405
+ function promptContentIsDim(rawLine) {
406
+ let dim = false;
407
+ let visible = "";
408
+ const dimAt = [];
409
+ const sgr = /\u001b\[([0-9;]*)m/g;
410
+ let cursor = 0;
411
+ for (const match of rawLine.matchAll(sgr)) {
412
+ const index = match.index ?? cursor;
413
+ const text = rawLine.slice(cursor, index);
414
+ visible += text;
415
+ dimAt.push(...Array.from(text, () => dim));
416
+ const params = (match[1] || "0").split(";").map(Number);
417
+ if (params.includes(0)) dim = false;
418
+ if (params.includes(2)) dim = true;
419
+ if (params.includes(22)) dim = false;
420
+ cursor = index + match[0].length;
421
+ }
422
+ const rest = rawLine.slice(cursor);
423
+ visible += rest;
424
+ dimAt.push(...Array.from(rest, () => dim));
425
+ const marker = visible.search(/[›>❯]/);
426
+ if (marker === -1) return false;
427
+ let contentStart = marker + 1;
428
+ while (/\s/.test(visible[contentStart] ?? "")) contentStart += 1;
429
+ const contentEnd = visible.trimEnd().length;
430
+ return contentEnd > contentStart && dimAt.slice(contentStart, contentEnd).every(Boolean);
431
+ }
432
+ function parseChoiceInteraction(lines, dialect) {
433
+ const explicitFooterIndex = lastMatchingIndex(lines, new RegExp(dialect.footerPatterns.map(({ source }) => `(?:${source})`).join("|"), "i"));
434
+ const footerlessHeaderIndex = dialect.footerlessChoiceHeaders ? lastMatchingIndex(lines, new RegExp(dialect.footerlessChoiceHeaders.map(({ source }) => `(?:${source})`).join("|"), "i")) : -1;
435
+ if (explicitFooterIndex === -1 && footerlessHeaderIndex === -1) return void 0;
436
+ const footerIndex = explicitFooterIndex >= 0 ? explicitFooterIndex : lines.length;
437
+ if (explicitFooterIndex >= 0 && lines.slice(footerIndex + 1).some((line) => /^\s*[›>❯]\s?/.test(line))) return void 0;
438
+ const allStarts = lines.slice(0, footerIndex).map((line, index) => choiceOptionStart(line) ? index : -1).filter((index) => index >= 0);
439
+ if (allStarts.length < 2) return void 0;
440
+ const optionStarts = latestOptionGroup(allStarts);
441
+ if (optionStarts.length < 2) return void 0;
442
+ if (explicitFooterIndex === -1 && (optionStarts[0] ?? -1) <= footerlessHeaderIndex) return void 0;
443
+ const firstOption = optionStarts[0] ?? 0;
444
+ const beforeOptions = lines.slice(0, firstOption);
445
+ const knownHeader = lastMatchingIndex(beforeOptions, new RegExp(dialect.headerPatterns.map(({ source }) => `(?:${source})`).join("|"), "i"));
446
+ const hardBoundary = lastMatchingIndex(beforeOptions, /^(?:\s*[-─━═]{8,}\s*|\s*[›>❯]\s+.+|\s*[✻◆◦•]\s+.+)$/);
447
+ const contextStart = knownHeader > hardBoundary ? knownHeader : hardBoundary >= 0 ? hardBoundary + 1 : Math.max(0, firstOption - 12);
448
+ const context = lines.slice(contextStart, firstOption).filter((line) => Boolean(line) && !/^\s*[-─━═]{8,}\s*$/.test(line));
449
+ const footer = lines[footerIndex] ?? "";
450
+ const indexedActions = optionStarts.flatMap((start, optionIndex) => {
451
+ const firstLine = lines[start] ?? "";
452
+ const parsed = parseChoiceOptionLine(firstLine);
453
+ if (!parsed) return [];
454
+ const end = optionStarts[optionIndex + 1] ?? footerIndex;
455
+ const rawLabel = parsed.label;
456
+ const marker = selectionMarker(parsed.marker);
457
+ const segments = [
458
+ rawLabel,
459
+ ...lines.slice(start + 1, end).map((line) => line.trim()).filter((line) => Boolean(line) && !/^[-─━═]{3,}$/.test(line) && !matchesAny(withoutFocusMarker(line), dialect.submitControls) && !isSidePanelLine(line) && !matchesAny(withoutFocusMarker(line), dialect.customInputControls) && !matchesAny(withoutFocusMarker(line), dialect.chatControls) && !/^notes$/i.test(line) && !/^\[[^\]]+\]$/.test(line))
460
+ ];
461
+ const shortcutMatch = segments.join(" ").match(/\(([^()\s]{1,16})\)\s*$/);
462
+ if (shortcutMatch) {
463
+ const last = segments.length - 1;
464
+ segments[last] = (segments[last] ?? "").replace(/\s*\([^()\s]{1,16}\)\s*$/, "").trim();
465
+ }
466
+ const parts = (segments.shift() ?? "").split(/\s{2,}/).map((part) => part.trim()).filter(Boolean);
467
+ const label = parts.shift();
468
+ if (!label) return [];
469
+ const continuation = segments.join(" ").replace(/\bnotes:\s*press\s+n\s+to\s+add\s+notes\b/gi, "").trim();
470
+ let description = parts.filter((part) => !isSidePanelLine(part)).join(" ").trim();
471
+ let fullLabel = label;
472
+ if (continuation && marker) {
473
+ description = [description, continuation].filter(Boolean).join(" ");
474
+ } else if (continuation) {
475
+ if (description) description = `${description} ${continuation}`;
476
+ else fullLabel = `${fullLabel} ${continuation}`;
477
+ }
478
+ const role = controlRole(fullLabel, dialect);
479
+ const customValue = role === "custom-input" ? dialect.customInputValuePattern?.exec(fullLabel)?.[1]?.trim() : void 0;
480
+ const inputValue = customValue && !/^something\.?$/i.test(customValue) ? customValue : void 0;
481
+ if (role === "custom-input" && /^type\b/i.test(fullLabel)) fullLabel = "Type something";
482
+ const risk = choiceRisk(`${fullLabel} ${description}`);
483
+ const editor = role === "custom-input" ? /(?:notes?\s*\(tab\)|tab\s+to\s+(?:add|edit)\s+notes?)/i.test(`${description} ${footer}`) ? { openKey: "Tab", submitKey: "Enter", commitsInteraction: true } : dialect.directInputControls && matchesAny(fullLabel, dialect.directInputControls) ? { submitKey: "Enter", commitsInteraction: true } : void 0 : void 0;
484
+ return [{ index: start, action: {
485
+ id: `option-${parsed.key}`,
486
+ label: fullLabel,
487
+ key: parsed.key,
488
+ description: description || void 0,
489
+ inputValue,
490
+ shortcut: shortcutMatch?.[1]?.toLowerCase(),
491
+ editor,
492
+ focused: parsed.focused,
493
+ marker,
494
+ role,
495
+ risk,
496
+ danger: risk === "persistent" || risk === "privileged"
497
+ } }];
498
+ });
499
+ const standaloneControls = lines.slice(firstOption, footerIndex).flatMap((line, relativeIndex) => {
500
+ const visible = line.replace(/^\s*[›>❯]\s*/, "").trim();
501
+ const role = matchesAny(visible, dialect.submitControls) ? "submit" : matchesAny(visible, dialect.customInputControls) ? "custom-input" : matchesAny(visible, dialect.chatControls) ? "chat" : void 0;
502
+ if (!role) return [];
503
+ const id = role === "submit" ? "submit" : role === "chat" ? "chat" : "custom-input";
504
+ const label = /^notes:/i.test(visible) ? "Add notes" : visible;
505
+ return [{ index: firstOption + relativeIndex, action: {
506
+ id,
507
+ key: id,
508
+ label,
509
+ role,
510
+ shortcut: /^notes:/i.test(visible) ? "n" : void 0,
511
+ focused: /^\s*[›>❯]/.test(line),
512
+ risk: "normal",
513
+ danger: false
514
+ } }];
515
+ });
516
+ const actions = [...indexedActions, ...standaloneControls].sort((left, right) => left.index - right.index).map(({ action }) => action);
517
+ const submitIndex = actions.findIndex(({ role }) => role === "submit");
518
+ const inlineCustomIndex = submitIndex - 1;
519
+ const inlineCustom = actions[inlineCustomIndex];
520
+ if (inlineCustom?.role === "answer" && (inlineCustom.marker === "checked" || inlineCustom.marker === "unchecked") && !inlineCustom.description && actions.slice(0, inlineCustomIndex).some(({ description, marker }) => Boolean(description && marker))) {
521
+ const value = inlineCustom.label.trim();
522
+ inlineCustom.role = "custom-input";
523
+ inlineCustom.inputValue = /^(?:type something|其他|其它|other)$/i.test(value) ? void 0 : value;
524
+ inlineCustom.label = "Type something";
525
+ }
526
+ if (actions.length < 2 || actions.filter(({ focused }) => focused).length !== 1) return void 0;
527
+ const kind = classifyChoice(context, actions, footer);
528
+ const semantics = selectionSemantics(actions, footer);
529
+ const canonical = actions.map(({ key, label, description, role, editor }) => ({ key, label, description, role, editor }));
530
+ const identityContext = context.map((line) => line.replace(/([←\s]*)[☐☑☒]/, "$1\u2610"));
531
+ const interactionId = createHash("sha256").update(JSON.stringify([kind, identityContext, canonical])).digest("hex");
532
+ const revision = createHash("sha256").update(JSON.stringify([
533
+ interactionId,
534
+ actions.map(({ key, marker, inputValue }) => ({ key, marker, inputValue }))
535
+ ])).digest("hex");
536
+ const questionContext = context.filter((line) => !matchesAny(line, dialect.headerPatterns));
537
+ const questionTitle = [...questionContext].reverse().find((line) => /[??]/.test(line)) ?? questionContext.at(-1);
538
+ return {
539
+ state: kind === "approval" ? "approval" : "input",
540
+ confidence: kind === "question" ? 0.92 : kind === "approval" ? 0.9 : 0.85,
541
+ evidence: lines.slice(contextStart, footerIndex + 1).filter(Boolean),
542
+ actions,
543
+ interaction: {
544
+ kind,
545
+ title: questionTitle?.trim() || context[0]?.trim() || "\u8BF7\u9009\u62E9\u4E00\u4E2A\u9009\u9879",
546
+ context,
547
+ interactionId,
548
+ revision,
549
+ semantics,
550
+ contentConfidence: knownHeader >= 0 || hardBoundary >= 0 ? 0.95 : 0.65,
551
+ actionConfidence: semantics ? semantics.confidence : 0.55
552
+ }
553
+ };
554
+ }
555
+ function selectionMarker(value) {
556
+ if (!value) return void 0;
557
+ if (value === "[ ]" || value === "\u2610") return "unchecked";
558
+ if (/^\[[xX✓✔]\]$/.test(value) || value === "\u2611") return "checked";
559
+ if (value === "\u25CB") return "unselected";
560
+ if (value === "\u25CF") return "selected";
561
+ return void 0;
562
+ }
563
+ function matchesAny(value, patterns) {
564
+ return patterns.some((pattern) => pattern.test(value));
565
+ }
566
+ function withoutFocusMarker(value) {
567
+ return value.replace(/^\s*[›>❯]\s*/, "").trim();
568
+ }
569
+ function isSidePanelLine(value) {
570
+ return /[│┌┐└┘├┤┬┴┼╔╗╚╝╠╣╦╩╬║═]/.test(value);
571
+ }
572
+ function controlRole(label, dialect) {
573
+ if (matchesAny(label, dialect.customInputControls)) return "custom-input";
574
+ if (matchesAny(label, dialect.chatControls)) return "chat";
575
+ return "answer";
576
+ }
577
+ function selectionSemantics(actions, footer) {
578
+ const answers = actions.filter(({ role }) => role === "answer");
579
+ const submit = actions.find(({ role }) => role === "submit");
580
+ const hasCheckbox = answers.length > 0 && answers.every(({ marker }) => marker === "checked" || marker === "unchecked");
581
+ const hasRadio = answers.length > 0 && answers.every(({ marker }) => marker === "selected" || marker === "unselected");
582
+ if (hasCheckbox || hasRadio) {
583
+ const commit = submit ? { mode: "explicit", controlId: submit.id } : /ctrl\+enter\s+to\s+(?:submit|confirm)/i.test(footer) ? { mode: "key", key: "C-Enter" } : /enter\s+to\s+(?:submit|confirm)(?:\s+answer)?/i.test(footer) ? { mode: "key", key: "Enter" } : void 0;
584
+ if (!commit) return void 0;
585
+ const toggleKey = /space\s+to\s+toggle/i.test(footer) ? "Space" : "Enter";
586
+ return {
587
+ cardinality: hasCheckbox ? "many" : "one",
588
+ activation: "toggle",
589
+ toggleKey,
590
+ commit,
591
+ confidence: 0.97,
592
+ evidence: [hasCheckbox ? "answer controls use checkbox markers" : "answer controls use radio markers", "an explicit commit mechanism exists"]
593
+ };
594
+ }
595
+ if (answers.some(({ marker }) => marker)) return void 0;
596
+ return {
597
+ cardinality: "one",
598
+ activation: "submit",
599
+ commit: { mode: "immediate" },
600
+ confidence: 0.9,
601
+ evidence: ["numbered controls use a single focus marker and no persistent selection markers"]
602
+ };
603
+ }
604
+ function latestOptionGroup(starts) {
605
+ const group = [starts.at(-1)];
606
+ for (let index = starts.length - 2; index >= 0; index -= 1) {
607
+ const start = starts[index];
608
+ if (group[0] - start > 8) break;
609
+ group.unshift(start);
610
+ }
611
+ return group;
612
+ }
613
+ function classifyChoice(context, actions, footer) {
614
+ const contextText = context.join(" ");
615
+ if (/(?:^|\s)[☐☑☒]\s+\S|Question\s+\d+\/\d+/i.test(contextText) || /submit answer|add notes/i.test(footer)) return "question";
616
+ const optionText = actions.map(({ label, description }) => `${label} ${description ?? ""}`).join(" ");
617
+ if (/(?:Would you like to|Do you want to).*(?:run|edit|grant|access)|approval required|allow command|requires approval/i.test(contextText) || /full access|don't ask again|do not ask again|always allow|auto mode|permissions?\b/i.test(optionText)) return "approval";
618
+ return "choice";
619
+ }
620
+ function choiceOptionStart(line) {
621
+ return Boolean(parseChoiceOptionLine(line));
622
+ }
623
+ function parseChoiceOptionLine(line) {
624
+ const match = line.match(/^\s*([›>❯])?\s*(?:(\[[ xX✓✔]\]|[☐☑○●])\s*)?(\d+)[.)]\s+(?:(\[[ xX✓✔]\]|[☐☑○●])\s*)?(.+?)\s*$/);
625
+ if (!match?.[3] || !match[5]) return void 0;
626
+ return { focused: Boolean(match[1]), marker: match[2] ?? match[4], key: match[3], label: match[5] };
627
+ }
628
+ function choiceRisk(text) {
629
+ if (/full access|bypass|without sandbox|dangerously/i.test(text)) return "privileged";
630
+ if (/always|don't ask again|do not ask again|auto mode|persist/i.test(text)) return "persistent";
631
+ if (/^\s*no\b|reject|deny|cancel|do differently/i.test(text)) return "reject";
632
+ return "normal";
633
+ }
634
+ function result(state, confidence, normalized, fingerprint, evidence) {
635
+ return { state, confidence, normalized, fingerprint, evidence, actions: [], hasDraftInput: false };
636
+ }
637
+ function matching(lines, pattern) {
638
+ return lines.filter((line) => pattern.test(line)).slice(-4);
639
+ }
640
+ function lastMatchingIndex(lines, pattern) {
641
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
642
+ if (pattern.test(lines[index] ?? "")) return index;
643
+ }
644
+ return -1;
645
+ }
646
+
647
+ // src/agents/resume.ts
648
+ function resumeArgs(resume) {
649
+ if (!resume) return [];
650
+ if (resume.mode === "last") return ["resume", "--last"];
651
+ if (resume.mode === "session") return ["resume", resume.sessionId];
652
+ return resume.all ? ["resume", "--all"] : ["resume"];
653
+ }
654
+ function claudeResumeArgs(resume) {
655
+ if (!resume) return [];
656
+ if (resume.mode === "last") return ["--continue"];
657
+ if (resume.mode === "session") return ["--resume", resume.sessionId];
658
+ return ["--resume"];
659
+ }
660
+
661
+ // src/agents/stop-hook.ts
662
+ function codexStyleStopHookArgs(command) {
663
+ const hook = `{hooks=[{type="command",command=${JSON.stringify(command)},timeout=5}]}`;
664
+ return ["--dangerously-bypass-hook-trust", "-c", `hooks.Stop=[${hook}]`];
665
+ }
666
+ function claudeStopHookArgs(command) {
667
+ return ["--settings", JSON.stringify({
668
+ hooks: {
669
+ Stop: [{ hooks: [{ type: "command", command, timeout: 5 }] }]
670
+ }
671
+ })];
672
+ }
673
+
674
+ // src/agents/codex.ts
675
+ var codexAdapter = {
676
+ id: "codex",
677
+ displayName: "Codex",
678
+ groupOrder: 10,
679
+ binary: (config) => config.agentBinaries.codex,
680
+ versionArgs: ["--version"],
681
+ buildLaunchArgs: ({ resume, stopHookCommand }) => [
682
+ ...codexStyleStopHookArgs(stopHookCommand),
683
+ ...resumeArgs(resume)
684
+ ],
685
+ detectScreen: detectCodexScreen
686
+ };
687
+
688
+ // src/agents/trae-cli.ts
689
+ var traeCliAdapter = {
690
+ id: "trae-cli",
691
+ displayName: "Trae CLI",
692
+ groupOrder: 20,
693
+ binary: (config) => config.agentBinaries["trae-cli"],
694
+ versionArgs: ["--version"],
695
+ buildLaunchArgs: ({ resume, stopHookCommand }) => [
696
+ ...codexStyleStopHookArgs(stopHookCommand),
697
+ ...resumeArgs(resume)
698
+ ],
699
+ detectScreen: detectTraeScreen
700
+ };
701
+
702
+ // src/agents/claude-code.ts
703
+ var claudeCodeAdapter = {
704
+ id: "claude-code",
705
+ displayName: "Claude Code",
706
+ groupOrder: 30,
707
+ binary: (config) => config.agentBinaries["claude-code"],
708
+ versionArgs: ["--version"],
709
+ buildLaunchArgs: ({ resume, stopHookCommand }) => [
710
+ ...claudeStopHookArgs(stopHookCommand),
711
+ ...claudeResumeArgs(resume)
712
+ ],
713
+ detectScreen: detectClaudeScreen
714
+ };
715
+
716
+ // src/agents/types.ts
717
+ var AGENT_IDS = ["codex", "trae-cli", "claude-code"];
718
+
719
+ // src/agents/registry.ts
720
+ var adapters = /* @__PURE__ */ new Map([
721
+ [codexAdapter.id, codexAdapter],
722
+ [traeCliAdapter.id, traeCliAdapter],
723
+ [claudeCodeAdapter.id, claudeCodeAdapter]
724
+ ]);
725
+ function getAgentAdapter(id) {
726
+ const adapter = adapters.get(id);
727
+ if (!adapter) throw new Error(`unsupported coding agent: ${id}`);
728
+ return adapter;
729
+ }
730
+ function listAgentAdapters() {
731
+ return [...adapters.values()].sort((left, right) => left.groupOrder - right.groupOrder);
732
+ }
733
+ function isAgentId(value) {
734
+ return AGENT_IDS.includes(value);
735
+ }
736
+
737
+ // src/agents/stop-event.ts
738
+ function validTurnCompleteCandidate(value) {
739
+ if (!value || typeof value !== "object") return false;
740
+ const candidate = value;
741
+ return ["sessionId", "eventId", "agentSessionId", "cwd", "lastAssistantMessage"].every((key) => typeof candidate[key] === "string" && candidate[key].length > 0);
742
+ }
743
+
744
+ // src/lark/gateway.ts
745
+ import {
746
+ createLarkChannel
747
+ } from "@larksuite/channel";
748
+
749
+ // src/lark/action-signing.ts
750
+ import { createHmac, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
751
+ var APPROVAL_NONCE_RETENTION_MS = 7 * 24 * 60 * 6e4;
752
+ function remainsValidWhilePending(kind) {
753
+ return kind === "choice";
754
+ }
755
+ var ActionSigner = class {
756
+ constructor(secret, now = Date.now) {
757
+ this.secret = secret;
758
+ this.now = now;
759
+ }
760
+ secret;
761
+ now;
762
+ usedNonces = /* @__PURE__ */ new Map();
763
+ sign(action, ttlMs = 5 * 6e4) {
764
+ const value = {
765
+ v: 1,
766
+ ...action,
767
+ nonce: randomBytes3(18).toString("base64url"),
768
+ expiresAt: this.now() + ttlMs
769
+ };
770
+ return { ...value, sig: this.mac(value) };
771
+ }
772
+ verify(value, expectedChatId) {
773
+ const now = this.now();
774
+ this.prune(now);
775
+ if (!isSignedAction(value) || expectedChatId && value.chatId !== expectedChatId || !remainsValidWhilePending(value.kind) && value.expiresAt < now || this.usedNonces.has(value.nonce)) return void 0;
776
+ const expected = Buffer.from(this.mac(withoutSignature(value)), "base64url");
777
+ const actual = Buffer.from(value.sig, "base64url");
778
+ if (actual.length !== expected.length || !timingSafeEqual2(actual, expected)) return void 0;
779
+ this.usedNonces.set(
780
+ value.nonce,
781
+ remainsValidWhilePending(value.kind) ? Math.max(value.expiresAt, now + APPROVAL_NONCE_RETENTION_MS) : value.expiresAt
782
+ );
783
+ return value;
784
+ }
785
+ mac(value) {
786
+ const canonical = JSON.stringify([
787
+ value.v,
788
+ value.kind,
789
+ value.interactionKind ?? null,
790
+ value.sessionId ?? null,
791
+ value.manualMode ?? null,
792
+ value.agent,
793
+ value.action,
794
+ value.paneId,
795
+ value.fingerprint,
796
+ value.chatId,
797
+ value.nonce,
798
+ value.expiresAt
799
+ ]);
800
+ return createHmac("sha256", this.secret).update(canonical).digest("base64url");
801
+ }
802
+ prune(now) {
803
+ for (const [nonce, expiresAt] of this.usedNonces) {
804
+ if (expiresAt < now) this.usedNonces.delete(nonce);
805
+ }
806
+ }
807
+ };
808
+ function withoutSignature(value) {
809
+ const { sig: _sig, ...unsigned } = value;
810
+ return unsigned;
811
+ }
812
+ function isSignedAction(value) {
813
+ if (!value || typeof value !== "object") return false;
814
+ const item = value;
815
+ return item.v === 1 && (item.kind === "choice" || item.kind === "stop" || item.kind === "session" || item.kind === "manual") && (item.kind !== "choice" || item.interactionKind === "approval" || item.interactionKind === "question" || item.interactionKind === "choice") && (item.kind !== "manual" || typeof item.sessionId === "string" && (item.manualMode === "explicit" || item.manualMode === "fallback")) && typeof item.agent === "string" && isAgentId(item.agent) && typeof item.action === "string" && typeof item.paneId === "string" && typeof item.fingerprint === "string" && typeof item.chatId === "string" && typeof item.nonce === "string" && typeof item.expiresAt === "number" && typeof item.sig === "string";
816
+ }
817
+
818
+ // src/lark/cards.ts
819
+ import { homedir } from "os";
820
+ function choiceCard(chatId, paneId, screen, signer, agent = "codex") {
821
+ if (!screen.interaction) throw new Error("choice card requires a structured interaction");
822
+ const interaction = screen.interaction;
823
+ const buttons = screen.actions.map((action) => ({
824
+ tag: "button",
825
+ text: { tag: "plain_text", content: choiceActionLabel(action.key, action.label, action.description) },
826
+ type: choiceButtonType(action.risk, action.focused),
827
+ width: "fill",
828
+ size: "medium",
829
+ behaviors: [{
830
+ type: "callback",
831
+ value: signer.sign({
832
+ kind: "choice",
833
+ interactionKind: interaction.kind,
834
+ agent,
835
+ action: action.key,
836
+ paneId,
837
+ fingerprint: interaction.revision ?? screen.fingerprint,
838
+ chatId
839
+ })
840
+ }]
841
+ }));
842
+ const context = interaction.context.join("\n").slice(0, 1800);
843
+ if (interaction.semantics?.activation === "toggle") {
844
+ const formElements = screen.actions.flatMap((action, index) => {
845
+ if (action.role === "custom-input") {
846
+ const selected2 = action.marker === "checked" || action.marker === "selected";
847
+ return [
848
+ {
849
+ tag: "checker",
850
+ name: choiceFormFieldName(index),
851
+ checked: selected2,
852
+ text: { tag: "plain_text", content: `${action.key}. ${action.label}` }
853
+ },
854
+ {
855
+ tag: "input",
856
+ name: inlineInputName(index),
857
+ placeholder: { tag: "plain_text", content: "\u8BF7\u8F93\u5165\u81EA\u5B9A\u4E49\u5185\u5BB9" },
858
+ input_type: "multiline_text",
859
+ rows: 2,
860
+ auto_resize: true,
861
+ max_rows: 4,
862
+ max_length: 1e3,
863
+ width: "fill",
864
+ default_value: action.inputValue ?? ""
865
+ }
866
+ ];
867
+ }
868
+ if (action.role === "submit") {
869
+ return [{ tag: "hr" }, {
870
+ tag: "button",
871
+ name: choiceFormSubmitName(),
872
+ text: { tag: "plain_text", content: "\u63D0\u4EA4\u7B54\u6848" },
873
+ type: "primary",
874
+ width: "fill",
875
+ size: "medium",
876
+ form_action_type: "submit"
877
+ }];
878
+ }
879
+ if (action.role !== "answer") return [];
880
+ const selected = action.marker === "checked" || action.marker === "selected";
881
+ const details = action.description ? `
882
+ ${action.description}` : "";
883
+ return [{
884
+ tag: "checker",
885
+ name: choiceFormFieldName(index),
886
+ checked: selected,
887
+ text: { tag: "plain_text", content: `${action.key}. ${action.label}${details}` }
888
+ }];
889
+ });
890
+ if (!screen.actions.some(({ role }) => role === "submit")) {
891
+ formElements.push({ tag: "hr" }, {
892
+ tag: "button",
893
+ name: choiceFormSubmitName(),
894
+ text: { tag: "plain_text", content: "\u63D0\u4EA4\u7B54\u6848" },
895
+ type: "primary",
896
+ width: "fill",
897
+ size: "medium",
898
+ form_action_type: "submit"
899
+ });
900
+ }
901
+ const immediateButtons = screen.actions.filter(({ role }) => role !== "answer" && role !== "custom-input" && role !== "submit").map((action) => ({
902
+ tag: "button",
903
+ text: { tag: "plain_text", content: choiceActionLabel(action.key, action.label, action.description) },
904
+ type: choiceButtonType(action.risk, action.focused),
905
+ width: "fill",
906
+ size: "medium",
907
+ behaviors: [{
908
+ type: "callback",
909
+ value: signer.sign({
910
+ kind: "choice",
911
+ interactionKind: interaction.kind,
912
+ agent,
913
+ action: action.key,
914
+ paneId,
915
+ fingerprint: interaction.revision ?? screen.fingerprint,
916
+ chatId
917
+ })
918
+ }]
919
+ }));
920
+ return cardElements(
921
+ `${getAgentAdapter(agent).displayName} ${interactionTitle(interaction.kind)}`,
922
+ [{ tag: "markdown", content: `\`\`\`text
923
+ ${escapeFence(context || interaction.title)}
924
+ \`\`\`` }, {
925
+ tag: "form",
926
+ name: "choice_form",
927
+ direction: "vertical",
928
+ vertical_spacing: "8px",
929
+ elements: formElements
930
+ }, ...immediateButtons]
931
+ );
932
+ }
933
+ return card(
934
+ `${getAgentAdapter(agent).displayName} ${interactionTitle(interaction.kind)}`,
935
+ `\`\`\`text
936
+ ${escapeFence(context || "\u8BF7\u9009\u62E9\u4E00\u4E2A\u9009\u9879")}
937
+ \`\`\``,
938
+ buttons
939
+ );
940
+ }
941
+ var CHOICE_FORM_SUBMIT_ACTION = "__choice_form_submit__";
942
+ function choiceFormSubmitName() {
943
+ return "choice_form_submit";
944
+ }
945
+ function choiceFormFieldName(index) {
946
+ return `choice_selected_${index}`;
947
+ }
948
+ function inlineInputName(index) {
949
+ return `custom_input_${index}`;
950
+ }
951
+ function stopCard(chatId, paneId, fingerprint, signer, agent = "codex") {
952
+ const agentName = getAgentAdapter(agent).displayName;
953
+ return card(`\u786E\u8BA4\u505C\u6B62 ${agentName}\uFF1F`, `\u8FD9\u4F1A\u7EC8\u6B62\u5F53\u524D ${agentName} \u8FDB\u7A0B\u548C\u53D7\u7BA1 tmux \u4F1A\u8BDD\u3002`, [{
954
+ tag: "button",
955
+ text: { tag: "plain_text", content: "\u505C\u6B62\u4F1A\u8BDD" },
956
+ type: "danger",
957
+ behaviors: [{
958
+ type: "callback",
959
+ value: signer.sign({ kind: "stop", agent, action: "confirm", paneId, fingerprint, chatId }, 2 * 6e4)
960
+ }]
961
+ }]);
962
+ }
963
+ function interactionInputCard(agent, label) {
964
+ return card(
965
+ `${getAgentAdapter(agent).displayName} \u7B49\u5F85\u8865\u5145\u5185\u5BB9`,
966
+ `\u5DF2\u8FDB\u5165 **${escapeMarkdown(label)}**\u3002\u8BF7\u76F4\u63A5\u53D1\u9001\u8865\u5145\u8BF4\u660E\u3002`,
967
+ [],
968
+ "orange"
969
+ );
970
+ }
971
+ var MANUAL_TEXT_FIELD = "manual_text";
972
+ var MANUAL_TYPE_ACTION = "type";
973
+ var MANUAL_SUBMIT_ACTION = "submit";
974
+ function manualControlCard(chatId, view, signer) {
975
+ const state = view.state ?? "active";
976
+ const mode = view.mode ?? "fallback";
977
+ const agentName = getAgentAdapter(view.session.agent).displayName;
978
+ const fingerprint = view.screen.fingerprint;
979
+ const sign = (action) => signer.sign({
980
+ kind: "manual",
981
+ sessionId: view.session.id,
982
+ manualMode: mode,
983
+ agent: view.session.agent,
984
+ action,
985
+ paneId: view.session.paneId,
986
+ fingerprint,
987
+ chatId
988
+ }, 10 * 6e4);
989
+ const status = state === "recovered" ? "\u2705 \u5DF2\u6062\u590D\u7ED3\u6784\u5316\u8BC6\u522B\uFF0C\u8BF7\u4F7F\u7528\u65B0\u53D1\u9001\u7684\u8BED\u4E49\u5316\u64CD\u4F5C\u5361\u3002" : state === "exited" ? "\u2705 \u5DF2\u9000\u51FA\u624B\u52A8\u9065\u63A7\u6A21\u5F0F\uFF0C\u672C\u5730 Agent \u548C tmux \u4ECD\u5728\u8FD0\u884C\u3002" : state === "stale" ? "\u26A0\uFE0F \u7EC8\u7AEF\u753B\u9762\u5DF2\u53D8\u5316\uFF0C\u65E7\u64CD\u4F5C\u672A\u6267\u884C\uFF1B\u8BF7\u786E\u8BA4\u6700\u65B0\u753B\u9762\u540E\u91CD\u8BD5\u3002" : state === "error" ? `\u26A0\uFE0F ${escapeMarkdown(view.notice ?? "\u624B\u52A8\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u786E\u8BA4\u6700\u65B0\u753B\u9762\u3002")}` : mode === "explicit" ? "\u26A0\uFE0F \u5DF2\u9501\u5B9A\u624B\u52A8\u9065\u63A7\uFF1B\u5373\u4F7F\u6062\u590D\u7ED3\u6784\u5316\u8BC6\u522B\uFF0C\u4E5F\u4F1A\u7EE7\u7EED\u7531\u4F60\u76F4\u63A5\u64CD\u4F5C\u7EC8\u7AEF\u3002" : "\u26A0\uFE0F \u5F53\u524D\u4E3A\u624B\u52A8\u9065\u63A7\u6A21\u5F0F\uFF0C\u6240\u6709\u64CD\u4F5C\u90FD\u4E0D\u4F1A\u8FDB\u884C\u8BED\u4E49\u5B89\u5168\u5224\u65AD\u3002";
990
+ const metadata = [
991
+ `**Session** \`${escapeInlineCode(view.session.id)}\` \xB7 **Agent** ${escapeMarkdown(agentName)}`,
992
+ `**\u72B6\u6001** \`${view.screen.state}\` \xB7 **\u91C7\u96C6\u65F6\u95F4** ${formatHandledAt(view.capturedAt)}`,
993
+ view.lastOperation ? `**\u6700\u8FD1\u64CD\u4F5C** ${escapeMarkdown(view.lastOperation)}` : void 0,
994
+ view.notice && state !== "error" ? escapeMarkdown(view.notice) : void 0
995
+ ].filter(Boolean).join("\n");
996
+ const elements = [
997
+ { tag: "markdown", content: status },
998
+ { tag: "markdown", content: metadata },
999
+ { tag: "markdown", content: `\`\`\`text
1000
+ ${escapeFence(view.output).slice(-5e3)}
1001
+ \`\`\`` }
1002
+ ];
1003
+ if (state === "recovered" || state === "exited") {
1004
+ return cardElements(`${view.session.id} \xB7 ${agentName} \u624B\u52A8\u9065\u63A7`, elements, state === "recovered" ? "green" : "grey");
1005
+ }
1006
+ elements.push(
1007
+ manualButtonRow([
1008
+ manualButton("\u2196 \u5237\u65B0", sign("refresh")),
1009
+ manualButton("\u2191", sign("up")),
1010
+ manualButton("Esc", sign("esc"))
1011
+ ]),
1012
+ manualButtonRow([
1013
+ manualButton("\u2190", sign("left")),
1014
+ manualButton("Enter", sign("enter"), "primary"),
1015
+ manualButton("\u2192", sign("right"))
1016
+ ]),
1017
+ manualButtonRow([
1018
+ manualButton("Tab", sign("tab")),
1019
+ manualButton("\u2193", sign("down")),
1020
+ manualButton("Space", sign("space"))
1021
+ ]),
1022
+ {
1023
+ tag: "form",
1024
+ name: "manual_text_form",
1025
+ direction: "vertical",
1026
+ vertical_spacing: "8px",
1027
+ elements: [
1028
+ {
1029
+ tag: "input",
1030
+ name: MANUAL_TEXT_FIELD,
1031
+ input_type: "multiline_text",
1032
+ rows: 2,
1033
+ auto_resize: true,
1034
+ max_rows: 4,
1035
+ max_length: 1e3,
1036
+ width: "fill",
1037
+ placeholder: { tag: "plain_text", content: "\u8F93\u5165\u8981\u53D1\u9001\u5230\u5F53\u524D\u7EC8\u7AEF\u7684\u6587\u672C" }
1038
+ },
1039
+ manualFormButton("\u4EC5\u8F93\u5165", MANUAL_TYPE_ACTION),
1040
+ manualFormButton("\u8F93\u5165\u5E76\u63D0\u4EA4", MANUAL_SUBMIT_ACTION, "primary")
1041
+ ]
1042
+ },
1043
+ manualButtonRow([
1044
+ manualButton("\u232B \u9000\u683C", sign("backspace")),
1045
+ manualButton("Ctrl+C", sign("ctrl-c"), "danger"),
1046
+ manualButton("\u7ED3\u675F\u9065\u63A7", sign("exit"))
1047
+ ])
1048
+ );
1049
+ return cardElements(`${view.session.id} \xB7 ${agentName} \u624B\u52A8\u9065\u63A7`, elements, "orange");
1050
+ }
1051
+ function manualButton(label, value, type = "default") {
1052
+ return {
1053
+ tag: "button",
1054
+ text: { tag: "plain_text", content: label },
1055
+ type,
1056
+ width: "fill",
1057
+ size: "medium",
1058
+ behaviors: [{ type: "callback", value }]
1059
+ };
1060
+ }
1061
+ function manualFormButton(label, name, type = "default") {
1062
+ return {
1063
+ tag: "button",
1064
+ name,
1065
+ text: { tag: "plain_text", content: label },
1066
+ type,
1067
+ width: "fill",
1068
+ size: "medium",
1069
+ form_action_type: "submit"
1070
+ };
1071
+ }
1072
+ function manualButtonRow(buttons) {
1073
+ return {
1074
+ tag: "column_set",
1075
+ flex_mode: "none",
1076
+ horizontal_spacing: "6px",
1077
+ columns: buttons.map((button) => ({
1078
+ tag: "column",
1079
+ width: "weighted",
1080
+ weight: 1,
1081
+ elements: [button]
1082
+ }))
1083
+ };
1084
+ }
1085
+ function statusCard(status) {
1086
+ const session = status.session;
1087
+ if (!session) {
1088
+ return {
1089
+ schema: "2.0",
1090
+ config: { update_multi: true },
1091
+ header: { title: { tag: "plain_text", content: "Coding Assistant \u72B6\u6001" }, template: "grey" },
1092
+ body: {
1093
+ padding: "12px",
1094
+ elements: [{
1095
+ tag: "markdown",
1096
+ content: `\u26AA **\u6682\u65E0 active session**
1097
+
1098
+ \u5F53\u524D\u5171\u6709 ${Object.keys(status.state.sessions ?? {}).length} \u4E2A\u53EF\u7528 session\uFF0C\u53EF\u53D1\u9001 /sessions \u8FDB\u884C\u9009\u62E9\u3002`
1099
+ }]
1100
+ }
1101
+ };
1102
+ }
1103
+ const adapter = getAgentAdapter(session.agent);
1104
+ const visual = statusVisual(status);
1105
+ const path = formatDisplayPath(session.cwd);
1106
+ return {
1107
+ schema: "2.0",
1108
+ config: { update_multi: true },
1109
+ header: { title: { tag: "plain_text", content: `${session.id} \xB7 ${adapter.displayName}` }, template: visual.template },
1110
+ body: {
1111
+ vertical_spacing: "10px",
1112
+ padding: "12px",
1113
+ elements: [
1114
+ { tag: "markdown", content: `<text_tag color='${visual.color}'>\u25CF ${visual.label}</text_tag> **\u5F53\u524D\u8FDE\u63A5**` },
1115
+ {
1116
+ tag: "column_set",
1117
+ flex_mode: "none",
1118
+ horizontal_spacing: "8px",
1119
+ columns: [
1120
+ statusColumn("Agent", `${agentMarker(session.agent)} ${adapter.displayName}`),
1121
+ statusColumn("tmux", status.paneAlive ? "\u2705 \u8FD0\u884C\u4E2D" : "\u26D4 \u5DF2\u505C\u6B62")
1122
+ ]
1123
+ },
1124
+ {
1125
+ tag: "column_set",
1126
+ flex_mode: "none",
1127
+ horizontal_spacing: "0px",
1128
+ columns: [{
1129
+ tag: "column",
1130
+ width: "weighted",
1131
+ weight: 1,
1132
+ background_style: "grey",
1133
+ padding: "9px 10px",
1134
+ elements: [{ tag: "markdown", content: `**\u5DE5\u4F5C\u76EE\u5F55**
1135
+ \`${escapeInlineCode(path)}\`` }]
1136
+ }]
1137
+ },
1138
+ { tag: "markdown", content: `**\u7248\u672C** \`${escapeInlineCode(session.agentVersion)}\`
1139
+ **\u98DE\u4E66\u8FDE\u63A5** ${status.state.boundChatId ? "\u5DF2\u7ED1\u5B9A" : "\u672A\u7ED1\u5B9A"}` }
1140
+ ]
1141
+ }
1142
+ };
1143
+ }
1144
+ function sessionPickerCard(chatId, sessions, activeSessionId, signer) {
1145
+ const groups = listAgentAdapters().map((adapter) => ({ adapter, sessions: sessions.filter((session) => session.agent === adapter.id) })).filter(({ sessions: group }) => group.length > 0);
1146
+ const elements = groups.flatMap(({ adapter, sessions: group }, groupIndex) => [
1147
+ ...groupIndex > 0 ? [{ tag: "hr", margin: "6px 0px" }] : [],
1148
+ {
1149
+ tag: "markdown",
1150
+ content: `${agentMarker(adapter.id)} **${adapter.displayName}** \xB7 ${group.length} \u4E2A session`,
1151
+ text_size: "heading",
1152
+ margin: "2px 0px 0px 0px"
1153
+ },
1154
+ ...group.map((session) => sessionCard(chatId, session, session.id === activeSessionId, signer))
1155
+ ]);
1156
+ return {
1157
+ schema: "2.0",
1158
+ config: { update_multi: true },
1159
+ header: { title: { tag: "plain_text", content: "\u9009\u62E9 Coding Session" }, template: "blue" },
1160
+ body: {
1161
+ vertical_spacing: "10px",
1162
+ padding: "12px",
1163
+ elements: elements.length > 0 ? elements : [{ tag: "markdown", content: "\u6682\u65E0\u53EF\u8FDE\u63A5\u7684 Coding Session\u3002" }]
1164
+ }
1165
+ };
1166
+ }
1167
+ function sessionCard(chatId, session, active, signer) {
1168
+ const content = [
1169
+ active ? `**${escapeMarkdown(session.id)}** <text_tag color='green'>\u25CF \u5F53\u524D\u8FDE\u63A5</text_tag>` : `**${escapeMarkdown(session.id)}**`,
1170
+ `\u{1F4C1} \`${escapeInlineCode(formatDisplayPath(session.cwd))}\``
1171
+ ].join("\n\n");
1172
+ const elements = [{ tag: "markdown", content }];
1173
+ if (!active) {
1174
+ elements.push({
1175
+ tag: "button",
1176
+ text: { tag: "plain_text", content: `\u8FDE\u63A5 ${session.id}` },
1177
+ type: "primary",
1178
+ width: "fill",
1179
+ size: "medium",
1180
+ margin: "2px 0px 0px 0px",
1181
+ behaviors: [{
1182
+ type: "callback",
1183
+ value: signer.sign({
1184
+ kind: "session",
1185
+ agent: session.agent,
1186
+ action: session.id,
1187
+ paneId: session.paneId,
1188
+ fingerprint: String(session.updatedAt),
1189
+ chatId
1190
+ })
1191
+ }]
1192
+ });
1193
+ }
1194
+ const contentColumn = {
1195
+ tag: "column",
1196
+ width: "weighted",
1197
+ weight: 1,
1198
+ vertical_spacing: "8px",
1199
+ background_style: "grey",
1200
+ padding: "10px 12px",
1201
+ elements
1202
+ };
1203
+ return {
1204
+ tag: "column_set",
1205
+ flex_mode: "none",
1206
+ horizontal_spacing: "0px",
1207
+ margin: "0px",
1208
+ columns: active ? [{
1209
+ tag: "column",
1210
+ width: "auto",
1211
+ background_style: "green",
1212
+ padding: "0px 3px",
1213
+ elements: []
1214
+ }, contentColumn] : [contentColumn]
1215
+ };
1216
+ }
1217
+ function formatDisplayPath(path, home = homedir(), maxLength = 44) {
1218
+ const display = path === home ? "~" : path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
1219
+ if (display.length <= maxLength) return display;
1220
+ const segments = display.split("/").filter(Boolean);
1221
+ return segments.length <= 2 ? display : `\u2026/${segments.slice(-2).join("/")}`;
1222
+ }
1223
+ function agentMarker(agent) {
1224
+ if (agent === "codex") return "\u{1F535}";
1225
+ if (agent === "trae-cli") return "\u{1F7E3}";
1226
+ return "\u{1F7E0}";
1227
+ }
1228
+ function statusColumn(title, value) {
1229
+ return {
1230
+ tag: "column",
1231
+ width: "weighted",
1232
+ weight: 1,
1233
+ background_style: "grey",
1234
+ padding: "9px 10px",
1235
+ elements: [{ tag: "markdown", content: `**${title}**
1236
+ ${value}` }]
1237
+ };
1238
+ }
1239
+ function statusVisual(status) {
1240
+ if (!status.paneAlive || status.screen?.state === "exited") return { label: "\u5DF2\u505C\u6B62", color: "red", template: "red" };
1241
+ if (status.screen?.state === "failed") return { label: "\u6267\u884C\u5931\u8D25", color: "red", template: "red" };
1242
+ if (status.screen?.interaction?.kind === "approval") return { label: "\u7B49\u5F85\u5BA1\u6279", color: "orange", template: "orange" };
1243
+ if (status.screen?.interaction?.kind === "question") return { label: "\u7B49\u5F85\u56DE\u7B54", color: "orange", template: "orange" };
1244
+ if (status.screen?.interaction?.kind === "choice") return { label: "\u7B49\u5F85\u9009\u62E9", color: "orange", template: "orange" };
1245
+ if (status.screen?.state === "input") return { label: "\u7B49\u5F85\u8F93\u5165", color: "orange", template: "orange" };
1246
+ if (status.screen?.state === "running") return { label: "\u6267\u884C\u4E2D", color: "blue", template: "blue" };
1247
+ if (status.screen?.state === "starting") return { label: "\u542F\u52A8\u4E2D", color: "blue", template: "blue" };
1248
+ if (status.screen?.state === "idle") return { label: "\u7B49\u5F85\u7528\u6237\u8F93\u5165", color: "green", template: "green" };
1249
+ return { label: "\u72B6\u6001\u672A\u77E5", color: "grey", template: "grey" };
1250
+ }
1251
+ function handledActionCard(action, result2, handledAt = /* @__PURE__ */ new Date()) {
1252
+ const agentName = getAgentAdapter(action.agent).displayName;
1253
+ const title = action.kind === "choice" ? `${agentName} ${handledInteractionTitle(action.interactionKind)}` : action.kind === "session" ? `${agentName} session \u5DF2\u5207\u6362` : `${agentName} \u4F1A\u8BDD\u5DF2\u505C\u6B62`;
1254
+ const actionLabel = action.kind === "choice" ? `\u9009\u62E9\u7B2C ${action.action} \u9879` : action.kind === "session" ? `\u8FDE\u63A5 ${action.action}` : "\u505C\u6B62\u4F1A\u8BDD";
1255
+ const content = [
1256
+ `\u2705 ${escapeMarkdown(result2)}`,
1257
+ "",
1258
+ `**\u64CD\u4F5C**\uFF1A${escapeMarkdown(actionLabel)}`,
1259
+ `**\u5904\u7406\u65F6\u95F4**\uFF1A${formatHandledAt(handledAt)}`
1260
+ ].join("\n");
1261
+ return card(title, content, [], "green");
1262
+ }
1263
+ function card(title, content, buttons, template = "blue") {
1264
+ return cardElements(title, [{ tag: "markdown", content }, ...buttons], template);
1265
+ }
1266
+ function cardElements(title, elements, template = "blue") {
1267
+ return {
1268
+ schema: "2.0",
1269
+ config: { update_multi: true },
1270
+ header: { title: { tag: "plain_text", content: title }, template },
1271
+ body: { vertical_spacing: "8px", padding: "12px", elements }
1272
+ };
1273
+ }
1274
+ function interactionTitle(kind) {
1275
+ if (kind === "approval") return "\u7B49\u5F85\u5BA1\u6279";
1276
+ if (kind === "question") return "\u7B49\u5F85\u56DE\u7B54";
1277
+ return "\u7B49\u5F85\u9009\u62E9";
1278
+ }
1279
+ function handledInteractionTitle(kind) {
1280
+ if (kind === "approval") return "\u5BA1\u6279\u5DF2\u5904\u7406";
1281
+ if (kind === "question") return "\u56DE\u7B54\u5DF2\u63D0\u4EA4";
1282
+ return "\u9009\u62E9\u5DF2\u63D0\u4EA4";
1283
+ }
1284
+ function choiceButtonType(risk, focused) {
1285
+ if (risk === "persistent" || risk === "privileged") return "danger";
1286
+ if (risk === "reject") return "default";
1287
+ return focused ? "primary" : "default";
1288
+ }
1289
+ function choiceActionLabel(key, label, description) {
1290
+ const suffix = description ? ` \xB7 ${description}` : "";
1291
+ return `${key}. ${label}${suffix}`.slice(0, 80);
1292
+ }
1293
+ function escapeFence(value) {
1294
+ return value.replace(/```/g, "``\\`");
1295
+ }
1296
+ function escapeInlineCode(value) {
1297
+ return value.replace(/`/g, "\\`");
1298
+ }
1299
+ function formatHandledAt(value) {
1300
+ const parts = new Intl.DateTimeFormat("zh-CN", {
1301
+ timeZone: "Asia/Shanghai",
1302
+ year: "numeric",
1303
+ month: "2-digit",
1304
+ day: "2-digit",
1305
+ hour: "2-digit",
1306
+ minute: "2-digit",
1307
+ second: "2-digit",
1308
+ hour12: false
1309
+ }).formatToParts(value);
1310
+ const part = (type) => parts.find((item) => item.type === type)?.value ?? "";
1311
+ return `${part("year")}-${part("month")}-${part("day")} ${part("hour")}:${part("minute")}:${part("second")}`;
1312
+ }
1313
+ function escapeMarkdown(value) {
1314
+ return value.replace(/([\\`*_{}[\]()#+.!|>-])/g, "\\$1");
1315
+ }
1316
+
1317
+ // src/lark/gateway.ts
1318
+ var LarkGateway = class {
1319
+ constructor(config, secrets, handler) {
1320
+ this.handler = handler;
1321
+ this.signer = new ActionSigner(secrets.callbackSecret);
1322
+ this.channel = createLarkChannel({
1323
+ appId: config.appId,
1324
+ appSecret: secrets.appSecret,
1325
+ domain: config.tenant === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn",
1326
+ source: "lark-coding-assistant",
1327
+ policy: { dmMode: "open", requireMention: false, respondToMentionAll: false },
1328
+ safety: { chatQueue: { enabled: true, mergeWhileBusy: false } },
1329
+ handshakeTimeoutMs: 8e3,
1330
+ httpTimeoutMs: 3e4,
1331
+ respectProxyEnv: true
1332
+ });
1333
+ this.channel.on("message", async (message) => this.handler.onMessage(message));
1334
+ this.channel.on("cardAction", async (event) => {
1335
+ const mappedFormAction = event.action.formValue && event.action.name ? this.formActions.get(event.messageId)?.get(event.action.name) : void 0;
1336
+ const action = this.signer.verify(event.action.value, event.chatId) ?? (mappedFormAction ? this.signer.verify(mappedFormAction, event.chatId) : void 0);
1337
+ if (!action) return { toast: { type: "error", content: "\u64CD\u4F5C\u5DF2\u5931\u6548\uFF0C\u8BF7\u7B49\u5F85\u65B0\u5361\u7247\u3002" } };
1338
+ if (event.action.formValue || action.kind === "manual") {
1339
+ if (this.formActionsInFlight.has(event.messageId)) {
1340
+ return { toast: { type: "warning", content: action.kind === "manual" ? "\u7EC8\u7AEF\u64CD\u4F5C\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7A0D\u5019\u3002" : "\u7B54\u6848\u6B63\u5728\u63D0\u4EA4\uFF0C\u8BF7\u7A0D\u5019\u3002" } };
1341
+ }
1342
+ this.formActionsInFlight.add(event.messageId);
1343
+ void this.handleDeferredCardAction(event, action).finally(() => {
1344
+ this.formActionsInFlight.delete(event.messageId);
1345
+ });
1346
+ return { toast: { type: "success", content: action.kind === "manual" ? "\u6B63\u5728\u64CD\u4F5C\u672C\u5730\u7EC8\u7AEF\u2026" : "\u6B63\u5728\u63D0\u4EA4\u5230\u672C\u5730\u7EC8\u7AEF\u2026" } };
1347
+ }
1348
+ const result2 = await this.handler.onAction(event, action);
1349
+ if (result2.type === "error") return { toast: result2 };
1350
+ if (result2.type === "manual") {
1351
+ this.rememberManualFormActions(event.messageId, event.chatId, result2.view);
1352
+ return {
1353
+ toast: { type: "success", content: result2.content },
1354
+ card: { type: "raw", data: manualControlCard(event.chatId, result2.view, this.signer) }
1355
+ };
1356
+ }
1357
+ if (result2.type === "refresh") {
1358
+ this.rememberFormActions(event.messageId, event.chatId, result2.paneId, result2.screen, result2.agent);
1359
+ return {
1360
+ toast: { type: "success", content: result2.content },
1361
+ card: { type: "raw", data: choiceCard(event.chatId, result2.paneId, result2.screen, this.signer, result2.agent) }
1362
+ };
1363
+ }
1364
+ if (result2.type === "awaiting-input") {
1365
+ return {
1366
+ toast: { type: "success", content: result2.content },
1367
+ card: { type: "raw", data: interactionInputCard(result2.agent, result2.label) }
1368
+ };
1369
+ }
1370
+ return {
1371
+ toast: result2,
1372
+ card: { type: "raw", data: handledActionCard(action, result2.content) }
1373
+ };
1374
+ });
1375
+ }
1376
+ handler;
1377
+ channel;
1378
+ signer;
1379
+ formActions = /* @__PURE__ */ new Map();
1380
+ formActionsInFlight = /* @__PURE__ */ new Set();
1381
+ async handleDeferredCardAction(event, action) {
1382
+ try {
1383
+ const result2 = await this.handler.onAction(event, action);
1384
+ if (result2.type === "error") {
1385
+ await this.channel.send(event.chatId, { text: `\u5361\u7247\u64CD\u4F5C\u672A\u5B8C\u6210\uFF1A${result2.content}` });
1386
+ return;
1387
+ }
1388
+ if (result2.type === "refresh") {
1389
+ await this.updateCardAfterAction(
1390
+ event.messageId,
1391
+ choiceCard(event.chatId, result2.paneId, result2.screen, this.signer, result2.agent)
1392
+ );
1393
+ this.rememberFormActions(event.messageId, event.chatId, result2.paneId, result2.screen, result2.agent);
1394
+ return;
1395
+ }
1396
+ if (result2.type === "manual") {
1397
+ await this.updateCardAfterAction(event.messageId, manualControlCard(event.chatId, result2.view, this.signer));
1398
+ this.rememberManualFormActions(event.messageId, event.chatId, result2.view);
1399
+ return;
1400
+ }
1401
+ if (result2.type === "awaiting-input") {
1402
+ await this.updateCardAfterAction(event.messageId, interactionInputCard(result2.agent, result2.label));
1403
+ return;
1404
+ }
1405
+ await this.updateCardAfterAction(event.messageId, handledActionCard(action, result2.content));
1406
+ } catch (error) {
1407
+ const detail = error instanceof Error ? error.message : String(error);
1408
+ await this.channel.send(event.chatId, { text: `\u5361\u7247\u64CD\u4F5C\u540C\u6B65\u5931\u8D25\uFF1A${detail}` }).catch(() => void 0);
1409
+ }
1410
+ }
1411
+ async updateCardAfterAction(messageId, card2) {
1412
+ const retryDelays = [0, 300, 800, 1600];
1413
+ let lastError;
1414
+ for (const delay of retryDelays) {
1415
+ if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
1416
+ try {
1417
+ await this.channel.updateCard(messageId, card2);
1418
+ return;
1419
+ } catch (error) {
1420
+ lastError = error;
1421
+ if (!cardActionLocked(error)) throw error;
1422
+ }
1423
+ }
1424
+ throw lastError;
1425
+ }
1426
+ connect() {
1427
+ return this.channel.connect();
1428
+ }
1429
+ disconnect() {
1430
+ return this.channel.disconnect();
1431
+ }
1432
+ sendText(chatId, text) {
1433
+ return this.channel.send(chatId, { text });
1434
+ }
1435
+ sendMarkdown(chatId, markdown) {
1436
+ return this.channel.send(chatId, { markdown });
1437
+ }
1438
+ async sendChoice(chatId, paneId, screen, agent) {
1439
+ const result2 = await this.channel.send(chatId, { card: choiceCard(chatId, paneId, screen, this.signer, agent) });
1440
+ this.rememberFormActions(result2.messageId, chatId, paneId, screen, agent);
1441
+ return result2;
1442
+ }
1443
+ async updateChoice(messageId, chatId, paneId, screen, agent) {
1444
+ await this.channel.updateCard(messageId, choiceCard(chatId, paneId, screen, this.signer, agent));
1445
+ this.rememberFormActions(messageId, chatId, paneId, screen, agent);
1446
+ }
1447
+ async completeChoiceInput(messageId, action, content) {
1448
+ await this.channel.updateCard(messageId, handledActionCard(action, content));
1449
+ this.formActions.delete(messageId);
1450
+ }
1451
+ async sendManual(chatId, view) {
1452
+ const result2 = await this.channel.send(chatId, { card: manualControlCard(chatId, view, this.signer) });
1453
+ this.rememberManualFormActions(result2.messageId, chatId, view);
1454
+ return result2;
1455
+ }
1456
+ rememberManualFormActions(messageId, chatId, view) {
1457
+ if (view.state === "recovered" || view.state === "exited") {
1458
+ this.formActions.delete(messageId);
1459
+ return;
1460
+ }
1461
+ const common = {
1462
+ kind: "manual",
1463
+ sessionId: view.session.id,
1464
+ manualMode: view.mode ?? "fallback",
1465
+ agent: view.session.agent,
1466
+ paneId: view.session.paneId,
1467
+ fingerprint: view.screen.fingerprint,
1468
+ chatId
1469
+ };
1470
+ this.formActions.set(messageId, /* @__PURE__ */ new Map([
1471
+ [MANUAL_TYPE_ACTION, this.signer.sign({ ...common, action: MANUAL_TYPE_ACTION }, 10 * 6e4)],
1472
+ [MANUAL_SUBMIT_ACTION, this.signer.sign({ ...common, action: MANUAL_SUBMIT_ACTION }, 10 * 6e4)]
1473
+ ]));
1474
+ }
1475
+ rememberFormActions(messageId, chatId, paneId, screen, agent) {
1476
+ if (screen.interaction?.semantics?.activation !== "toggle") {
1477
+ this.formActions.delete(messageId);
1478
+ return;
1479
+ }
1480
+ const actions = /* @__PURE__ */ new Map();
1481
+ actions.set(choiceFormSubmitName(), this.signer.sign({
1482
+ kind: "choice",
1483
+ interactionKind: screen.interaction?.kind,
1484
+ agent,
1485
+ action: CHOICE_FORM_SUBMIT_ACTION,
1486
+ paneId,
1487
+ fingerprint: screen.interaction?.revision ?? screen.fingerprint,
1488
+ chatId
1489
+ }));
1490
+ this.formActions.set(messageId, actions);
1491
+ while (this.formActions.size > 256) {
1492
+ const oldest = this.formActions.keys().next().value;
1493
+ if (!oldest) break;
1494
+ this.formActions.delete(oldest);
1495
+ }
1496
+ }
1497
+ sendStatus(chatId, status) {
1498
+ return this.channel.send(chatId, { card: statusCard(status) });
1499
+ }
1500
+ sendSessionPicker(chatId, sessions, activeSessionId) {
1501
+ return this.channel.send(chatId, { card: sessionPickerCard(chatId, sessions, activeSessionId, this.signer) });
1502
+ }
1503
+ sendStopConfirmation(chatId, paneId, fingerprint, agent) {
1504
+ return this.channel.send(chatId, { card: stopCard(chatId, paneId, fingerprint, this.signer, agent) });
1505
+ }
1506
+ };
1507
+ function cardActionLocked(error) {
1508
+ if (typeof error === "string") return /card action is lock/i.test(error);
1509
+ if (error instanceof Error && /card action is lock/i.test(error.message)) return true;
1510
+ if (!error || typeof error !== "object") return false;
1511
+ const record = error;
1512
+ return ["message", "msg", "cause"].some((key) => cardActionLocked(record[key]));
1513
+ }
1514
+
1515
+ // src/daemon/server.ts
1516
+ var AssistantDaemon = class {
1517
+ constructor(store, paths2, gatewayFactory = (config, secrets, handler) => new LarkGateway(config, secrets, handler), sessionName = "lark-coding-assistant", stopHookCommand = "lark-coding-assistant-hook", completionQuietMs = 2500, appVersion = "dev") {
1518
+ this.store = store;
1519
+ this.paths = paths2;
1520
+ this.gatewayFactory = gatewayFactory;
1521
+ this.sessionName = sessionName;
1522
+ this.stopHookCommand = stopHookCommand;
1523
+ this.completionQuietMs = completionQuietMs;
1524
+ this.appVersion = appVersion;
1525
+ }
1526
+ store;
1527
+ paths;
1528
+ gatewayFactory;
1529
+ sessionName;
1530
+ stopHookCommand;
1531
+ completionQuietMs;
1532
+ appVersion;
1533
+ config;
1534
+ state;
1535
+ tmux;
1536
+ screen;
1537
+ previousScreen;
1538
+ completedEvents = /* @__PURE__ */ new Set();
1539
+ pendingCompletion;
1540
+ pendingCompletionAt = 0;
1541
+ outputStableSince = Date.now();
1542
+ timer;
1543
+ pollInFlight;
1544
+ ownsRuntimeFiles = false;
1545
+ closing = false;
1546
+ gateway;
1547
+ pendingMessages = [];
1548
+ interactionNotificationsSuppressed = 0;
1549
+ unresolvedCandidate;
1550
+ unresolvedNotified = /* @__PURE__ */ new Set();
1551
+ closedManualCards = /* @__PURE__ */ new Set();
1552
+ pendingInteractionInput;
1553
+ attachAttempts = /* @__PURE__ */ new Map();
1554
+ server = createServer((socket) => this.handleSocket(socket));
1555
+ async start() {
1556
+ await this.store.ensure();
1557
+ const config = await this.store.loadConfig();
1558
+ if (!config) throw new Error("not initialized; run lark-coding-assistant init first");
1559
+ this.config = config;
1560
+ this.state = await this.store.loadState();
1561
+ this.tmux = new TmuxController(config.tmuxBinary);
1562
+ const secrets = await this.store.loadSecrets();
1563
+ if (!secrets) throw new Error("missing secrets; run lark-coding-assistant init again");
1564
+ await this.acquireRuntimeFiles();
1565
+ try {
1566
+ await this.reconcileSessions();
1567
+ await rm(this.paths.socket, { force: true });
1568
+ await new Promise((resolve, reject) => {
1569
+ this.server.once("error", reject);
1570
+ this.server.listen(this.paths.socket, () => resolve());
1571
+ });
1572
+ await chmod3(this.paths.socket, 384);
1573
+ this.gateway = this.gatewayFactory(config, secrets, {
1574
+ onMessage: (message) => this.onLarkMessage(message),
1575
+ onAction: (event, action) => this.onLarkAction(event, action)
1576
+ });
1577
+ await this.gateway.connect();
1578
+ this.schedulePoll();
1579
+ await this.log("daemon started");
1580
+ } catch (error) {
1581
+ await this.releaseRuntimeFiles();
1582
+ throw error;
1583
+ }
1584
+ }
1585
+ async close() {
1586
+ this.closing = true;
1587
+ await this.log("daemon stopping").catch(() => void 0);
1588
+ if (this.timer) clearTimeout(this.timer);
1589
+ await this.pollInFlight?.catch(() => void 0);
1590
+ await this.gateway?.disconnect().catch(() => void 0);
1591
+ await new Promise((resolve) => this.server.close(() => resolve()));
1592
+ await this.releaseRuntimeFiles();
1593
+ }
1594
+ async acquireRuntimeFiles() {
1595
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1596
+ try {
1597
+ const pidFile = await open2(this.paths.pid, "wx", 384);
1598
+ try {
1599
+ await pidFile.writeFile(`${process.pid}
1600
+ `);
1601
+ } finally {
1602
+ await pidFile.close();
1603
+ }
1604
+ this.ownsRuntimeFiles = true;
1605
+ return;
1606
+ } catch (error) {
1607
+ if (!isAlreadyExists(error)) throw error;
1608
+ const existingPid = Number.parseInt(await readFile2(this.paths.pid, "utf8").catch(() => ""), 10);
1609
+ if (Number.isInteger(existingPid) && processIsAlive(existingPid)) {
1610
+ throw new Error(`daemon is already running with PID ${existingPid}`);
1611
+ }
1612
+ await rm(this.paths.pid, { force: true });
1613
+ }
1614
+ }
1615
+ throw new Error("failed to acquire daemon PID file");
1616
+ }
1617
+ async releaseRuntimeFiles() {
1618
+ if (!this.ownsRuntimeFiles) return;
1619
+ const ownerPid = Number.parseInt(await readFile2(this.paths.pid, "utf8").catch(() => ""), 10);
1620
+ if (ownerPid === process.pid) {
1621
+ await Promise.all([rm(this.paths.socket, { force: true }), rm(this.paths.pid, { force: true })]);
1622
+ }
1623
+ this.ownsRuntimeFiles = false;
1624
+ }
1625
+ handleSocket(socket) {
1626
+ socket.setEncoding("utf8");
1627
+ let buffer = "";
1628
+ let dispatched = false;
1629
+ socket.on("data", (chunk) => {
1630
+ if (dispatched) return;
1631
+ buffer += chunk;
1632
+ const newline = buffer.indexOf("\n");
1633
+ if (newline === -1) return;
1634
+ dispatched = true;
1635
+ const line = buffer.slice(0, newline);
1636
+ void Promise.resolve().then(() => JSON.parse(line)).then((request) => this.dispatch(request)).then((response) => socket.end(`${JSON.stringify(response)}
1637
+ `)).catch((error) => socket.end(`${JSON.stringify(fail(error))}
1638
+ `));
1639
+ });
1640
+ socket.on("end", () => {
1641
+ if (!dispatched) socket.end();
1642
+ });
1643
+ }
1644
+ async dispatch(request) {
1645
+ switch (request.method) {
1646
+ case "ping":
1647
+ return { ok: true, value: { version: this.appVersion, pid: process.pid } };
1648
+ case "shutdown": {
1649
+ setTimeout(() => void this.close(), 25);
1650
+ return { ok: true };
1651
+ }
1652
+ case "start":
1653
+ return this.startSession(request.sessionId, request.cwd, request.agent, request.resume);
1654
+ case "status":
1655
+ return { ok: true, value: await this.runtimeStatus(request.sessionId) };
1656
+ case "tail":
1657
+ return { ok: true, value: await this.tail(request.lines ?? 80) };
1658
+ case "send":
1659
+ return this.send(request.text);
1660
+ case "key":
1661
+ return this.key(request.key, request.fingerprint);
1662
+ case "stop":
1663
+ return this.stopSession(request.sessionId);
1664
+ case "useSession":
1665
+ return this.useSession(request.sessionId);
1666
+ case "bindCode":
1667
+ return this.rotateBindCode();
1668
+ case "resetOwner":
1669
+ return this.resetOwner();
1670
+ case "turnComplete":
1671
+ return this.handleTurnComplete(request.candidate);
1672
+ }
1673
+ }
1674
+ async startSession(sessionId, cwd, agentId, resume) {
1675
+ if (!validSessionId(sessionId)) return { ok: false, error: "session name must use letters, digits, underscore, or dash" };
1676
+ const existing = this.state.sessions?.[sessionId];
1677
+ if (existing && await this.tmux.inspect(existing.paneId)) {
1678
+ return { ok: false, error: `managed coding-agent session is already running: ${sessionId}` };
1679
+ }
1680
+ const becomesActive = !this.state.activeSessionId;
1681
+ if (becomesActive) {
1682
+ this.pendingMessages.length = 0;
1683
+ this.previousScreen = void 0;
1684
+ this.completedEvents.clear();
1685
+ this.clearPendingCompletion();
1686
+ }
1687
+ const adapter = getAgentAdapter(agentId);
1688
+ const binary = adapter.binary(this.config);
1689
+ const agentVersion = (await runFile(binary, [...adapter.versionArgs])).stdout.trim();
1690
+ const tmuxSessionName = existing?.sessionName ?? `${this.sessionName}-${sessionId}`;
1691
+ const pane = await this.tmux.create({
1692
+ sessionName: tmuxSessionName,
1693
+ cwd,
1694
+ binary,
1695
+ args: adapter.buildLaunchArgs({
1696
+ resume,
1697
+ stopHookCommand: this.stopHookCommand
1698
+ }),
1699
+ env: {
1700
+ LARK_CODING_ASSISTANT_SOCKET: this.paths.socket,
1701
+ LARK_CODING_ASSISTANT_SESSION_ID: sessionId
1702
+ }
1703
+ });
1704
+ const binding = this.createSessionBinding();
1705
+ const session = {
1706
+ id: sessionId,
1707
+ agent: agentId,
1708
+ sessionName: pane.sessionName,
1709
+ paneId: pane.paneId,
1710
+ cwd,
1711
+ agentVersion,
1712
+ updatedAt: Date.now()
1713
+ };
1714
+ this.state = {
1715
+ ...this.state,
1716
+ sessions: { ...this.state.sessions, [sessionId]: session },
1717
+ activeSessionId: this.state.activeSessionId ?? sessionId,
1718
+ boundChatId: binding.mode === "reused" ? this.state.boundChatId : void 0,
1719
+ bindCodeHash: binding.mode === "code" ? hashBindCode(binding.bindCode) : void 0,
1720
+ bindCodeExpiresAt: binding.mode === "code" ? Date.now() + 10 * 6e4 : void 0,
1721
+ updatedAt: Date.now()
1722
+ };
1723
+ await this.store.saveState(this.state);
1724
+ await this.poll();
1725
+ return { ok: true, value: { pane, session, binding, active: this.state.activeSessionId === sessionId } };
1726
+ }
1727
+ createSessionBinding() {
1728
+ if (this.state.ownerOpenId && this.state.boundChatId && !this.state.autoBindDisabled) {
1729
+ return { mode: "reused" };
1730
+ }
1731
+ if (this.state.ownerOpenId && !this.state.autoBindDisabled) {
1732
+ return { mode: "awaiting-owner-message" };
1733
+ }
1734
+ return { mode: "code", bindCode: createBindCode(), expiresInSeconds: 600 };
1735
+ }
1736
+ async runtimeStatus(sessionId = this.state.activeSessionId) {
1737
+ const session = sessionId ? this.state.sessions?.[sessionId] : void 0;
1738
+ const pane = session ? await this.tmux.inspect(session.paneId) : void 0;
1739
+ return { state: this.state, session, screen: sessionId === this.state.activeSessionId ? this.screen : void 0, paneAlive: Boolean(pane && !pane.dead) };
1740
+ }
1741
+ async tail(lines) {
1742
+ const session = this.activeSession();
1743
+ if (!session) throw new Error("no active managed session");
1744
+ return tailScreen(await this.tmux.capture(session.paneId, lines), lines);
1745
+ }
1746
+ async send(text) {
1747
+ const session = this.activeSession();
1748
+ if (!session) return { ok: false, error: "no active managed session" };
1749
+ await this.poll();
1750
+ if (this.screen?.state === "approval") return { ok: false, error: "approval is pending" };
1751
+ if (this.screen?.hasDraftInput) return { ok: false, error: "local draft input detected" };
1752
+ if (!this.screen || this.screen.state === "unknown") return { ok: false, error: "screen state is unknown" };
1753
+ this.clearPendingCompletion();
1754
+ await this.tmux.sendText(session.paneId, text);
1755
+ return { ok: true };
1756
+ }
1757
+ async onLarkMessage(message) {
1758
+ if (message.chatType !== "p2p" || message.senderIsBot || message.senderType === "bot") return;
1759
+ const text = message.content.trim();
1760
+ const attachCode = text.match(/^\/attach\s+([^\s]+)\s*$/)?.[1];
1761
+ if (!this.state.boundChatId) {
1762
+ if (this.canAutoBind(message)) {
1763
+ await this.bindChat(message, true);
1764
+ } else {
1765
+ if (!attachCode) return;
1766
+ await this.handleAttach(message, attachCode);
1767
+ return;
1768
+ }
1769
+ }
1770
+ if (message.senderId !== this.state.ownerOpenId || message.chatId !== this.state.boundChatId) return;
1771
+ const tailMatch = text.match(/^\/tail(?:\s+(\d+))?$/);
1772
+ if (tailMatch) {
1773
+ const lines = tailMatch[1] ? Number(tailMatch[1]) : 80;
1774
+ if (!Number.isInteger(lines) || lines < 20 || lines > 300) {
1775
+ await this.gateway?.sendText(message.chatId, "\u7528\u6CD5\uFF1A/tail [20-300]");
1776
+ return;
1777
+ }
1778
+ const output = await this.tail(lines).catch((error) => `\u8BFB\u53D6\u5931\u8D25\uFF1A${errorMessage(error)}`);
1779
+ const session = this.activeSession();
1780
+ const metadata = session ? `**${session.id} \xB7 ${getAgentAdapter(session.agent).displayName}** \xB7 \u72B6\u6001 \`${this.screen?.state ?? "unknown"}\` \xB7 ${manualTimestamp()}` : "**\u5F53\u524D\u6CA1\u6709 active session**";
1781
+ await this.gateway?.sendMarkdown(message.chatId, `${metadata}
1782
+
1783
+ \`\`\`text
1784
+ ${escapeFence2(output).slice(-6800)}
1785
+ \`\`\``);
1786
+ return;
1787
+ }
1788
+ if (text.startsWith("/tail")) {
1789
+ await this.gateway?.sendText(message.chatId, "\u7528\u6CD5\uFF1A/tail [20-300]");
1790
+ return;
1791
+ }
1792
+ if (text === "/manual") {
1793
+ await this.poll();
1794
+ const view = this.currentManualView();
1795
+ if (!view) await this.gateway?.sendText(message.chatId, "\u5F53\u524D\u6CA1\u6709\u53EF\u9065\u63A7\u7684 active tmux session\u3002");
1796
+ else {
1797
+ try {
1798
+ await this.gateway?.sendManual(message.chatId, view);
1799
+ } catch (error) {
1800
+ await this.log(`manual card failed: ${errorMessage(error)}`);
1801
+ await this.gateway?.sendText(
1802
+ message.chatId,
1803
+ "\u624B\u52A8\u9065\u63A7\u5361\u53D1\u9001\u5931\u8D25\u3002\u53EF\u4F7F\u7528 /tail 120 \u67E5\u770B\u7EC8\u7AEF\uFF0C\u6216\u4F7F\u7528 /key\u3001/type\u3001/submit \u64CD\u4F5C\u3002"
1804
+ );
1805
+ }
1806
+ }
1807
+ return;
1808
+ }
1809
+ const manualKey = text.match(/^\/key\s+(up|down|left|right|enter|esc|tab|space|backspace|ctrl-c)$/i)?.[1];
1810
+ if (manualKey) {
1811
+ await this.executeManualCommand(message.chatId, `\u6309\u952E ${manualKey}`, async (session) => {
1812
+ await this.tmux.sendKey(session.paneId, manualTmuxKey(manualKey));
1813
+ });
1814
+ return;
1815
+ }
1816
+ if (text.startsWith("/key")) {
1817
+ await this.gateway?.sendText(message.chatId, "\u7528\u6CD5\uFF1A/key up|down|left|right|enter|esc|tab|space|backspace|ctrl-c");
1818
+ return;
1819
+ }
1820
+ const typeMatch = text.match(/^\/(type|submit)\s+([\s\S]+)$/);
1821
+ if (typeMatch?.[1] && typeMatch[2]?.trim()) {
1822
+ const submit = typeMatch[1] === "submit";
1823
+ await this.executeManualCommand(message.chatId, submit ? "\u8F93\u5165\u5E76\u63D0\u4EA4" : "\u4EC5\u8F93\u5165", async (session) => {
1824
+ await this.tmux.sendText(session.paneId, typeMatch[2], submit);
1825
+ });
1826
+ return;
1827
+ }
1828
+ if (text === "/type" || text === "/submit") {
1829
+ await this.gateway?.sendText(message.chatId, `\u7528\u6CD5\uFF1A${text} <\u6587\u672C>`);
1830
+ return;
1831
+ }
1832
+ if (text === "/status") {
1833
+ const status = await this.runtimeStatus();
1834
+ await this.gateway?.sendStatus(message.chatId, status);
1835
+ return;
1836
+ }
1837
+ if (text === "/sessions") {
1838
+ const sessions = await this.reconcileSessions();
1839
+ try {
1840
+ await this.gateway?.sendSessionPicker(
1841
+ message.chatId,
1842
+ sessions,
1843
+ this.state.activeSessionId
1844
+ );
1845
+ } catch (error) {
1846
+ await this.log(`session picker notification failed: ${errorMessage(error)}`);
1847
+ await this.gateway?.sendText(
1848
+ message.chatId,
1849
+ "Session \u9009\u62E9\u5361\u7247\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF1B\u4E5F\u53EF\u4EE5\u53D1\u9001 /use <session \u540D\u79F0> \u8FDB\u884C\u5207\u6362\u3002"
1850
+ );
1851
+ }
1852
+ return;
1853
+ }
1854
+ const useSessionId = text.match(/^\/use\s+([a-zA-Z0-9_-]+)\s*$/)?.[1];
1855
+ if (useSessionId) {
1856
+ const result3 = await this.useSession(useSessionId);
1857
+ const target = this.state.sessions?.[useSessionId];
1858
+ const reply = result3.ok ? `\u5DF2\u8FDE\u63A5\u5230 ${target ? getAgentAdapter(target.agent).displayName : "coding agent"} session\uFF1A${useSessionId}` : `\u5207\u6362\u5931\u8D25\uFF1A${result3.error}`;
1859
+ await this.gateway?.sendText(
1860
+ message.chatId,
1861
+ reply
1862
+ );
1863
+ return;
1864
+ }
1865
+ if (text === "/detach") {
1866
+ this.pendingMessages.length = 0;
1867
+ this.pendingInteractionInput = void 0;
1868
+ this.unresolvedCandidate = void 0;
1869
+ this.unresolvedNotified.clear();
1870
+ this.state = {
1871
+ ...this.state,
1872
+ boundChatId: void 0,
1873
+ autoBindDisabled: true,
1874
+ updatedAt: Date.now()
1875
+ };
1876
+ await this.store.saveState(this.state);
1877
+ await this.gateway?.sendText(message.chatId, "\u5DF2\u89E3\u9664\u672C\u6B21\u98DE\u4E66\u7ED1\u5B9A\uFF1Bcoding agent \u548C tmux \u4ECD\u5728\u8FD0\u884C\u3002");
1878
+ return;
1879
+ }
1880
+ if (text === "/stop") {
1881
+ await this.poll();
1882
+ const session = this.activeSession();
1883
+ if (!session || !this.screen) {
1884
+ await this.gateway?.sendText(message.chatId, "\u5F53\u524D\u6CA1\u6709\u53EF\u505C\u6B62\u7684 coding agent \u4F1A\u8BDD\u3002");
1885
+ } else {
1886
+ await this.gateway?.sendStopConfirmation(message.chatId, session.paneId, this.screen.fingerprint, session.agent);
1887
+ }
1888
+ return;
1889
+ }
1890
+ const pendingInput = this.pendingInteractionInput;
1891
+ if (pendingInput) {
1892
+ const session = this.activeSession();
1893
+ if (!session || session.id !== pendingInput.sessionId) {
1894
+ this.pendingInteractionInput = void 0;
1895
+ await this.gateway?.sendText(message.chatId, "\u4EA4\u4E92\u4F1A\u8BDD\u5DF2\u7ECF\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u64CD\u4F5C\u3002");
1896
+ return;
1897
+ }
1898
+ await this.tmux.sendText(session.paneId, message.content, pendingInput.submitOnInput);
1899
+ this.pendingInteractionInput = void 0;
1900
+ if (pendingInput.submitOnInput) {
1901
+ await this.waitForInteractionChange(pendingInput.interactionId);
1902
+ if (this.screen?.interaction?.interactionId === pendingInput.interactionId) {
1903
+ await this.gateway?.sendText(message.chatId, "\u8865\u5145\u5185\u5BB9\u5DF2\u8F93\u5165\uFF0C\u4F46\u7EC8\u7AEF\u4ECD\u505C\u7559\u5728\u539F\u95EE\u9898\uFF0C\u8BF7\u7528 /tail \u68C0\u67E5\u3002");
1904
+ return;
1905
+ }
1906
+ const content = `\u5DF2\u5411 ${getAgentAdapter(session.agent).displayName} \u63D0\u4EA4\u8865\u5145\u5185\u5BB9\u3002`;
1907
+ await this.gateway?.completeChoiceInput(pendingInput.cardMessageId, pendingInput.action, content);
1908
+ await this.flushPending();
1909
+ return;
1910
+ }
1911
+ if (pendingInput.role === "custom-input") {
1912
+ const refreshed = await this.withInteractionNotificationsSuppressed(async () => {
1913
+ await this.waitForCustomInputValue(pendingInput.interactionId, pendingInput.controlId, message.content);
1914
+ const current = this.screen;
1915
+ if (current?.interaction?.semantics && (current.interaction.actionConfidence ?? 0) >= 0.85) {
1916
+ await this.gateway?.updateChoice(
1917
+ pendingInput.cardMessageId,
1918
+ message.chatId,
1919
+ session.paneId,
1920
+ current,
1921
+ session.agent
1922
+ );
1923
+ return true;
1924
+ }
1925
+ return false;
1926
+ });
1927
+ if (refreshed) return;
1928
+ }
1929
+ await this.gateway?.sendText(message.chatId, `\u5DF2\u5411 ${getAgentAdapter(session.agent).displayName} \u63D0\u4EA4\u8865\u5145\u5185\u5BB9\u3002`);
1930
+ return;
1931
+ }
1932
+ await this.poll();
1933
+ if (this.shouldQueueMessage()) {
1934
+ if (this.pendingMessages.length >= 100) {
1935
+ await this.gateway?.sendText(message.chatId, "\u5F85\u53D1\u9001\u961F\u5217\u5DF2\u6EE1\uFF0C\u8BF7\u5148\u5904\u7406\u5F53\u524D\u7EC8\u7AEF\u72B6\u6001\u3002");
1936
+ return;
1937
+ }
1938
+ this.pendingMessages.push(message.content);
1939
+ await this.gateway?.sendText(message.chatId, `\u5F53\u524D\u7EC8\u7AEF\u6682\u4E0D\u53EF\u5B89\u5168\u5199\u5165\uFF0C\u6D88\u606F\u5DF2\u6392\u961F\uFF08${this.pendingMessages.length} \u6761\uFF09\u3002`);
1940
+ return;
1941
+ }
1942
+ const result2 = await this.send(message.content);
1943
+ if (!result2.ok) await this.gateway?.sendText(message.chatId, `\u672A\u53D1\u9001\uFF1A${result2.error}`);
1944
+ }
1945
+ async handleAttach(message, code) {
1946
+ if (this.state.ownerOpenId && message.senderId !== this.state.ownerOpenId) return;
1947
+ if (!this.allowAttachAttempt(message.senderId)) return;
1948
+ const valid = Boolean(
1949
+ this.state.bindCodeHash && this.state.bindCodeExpiresAt && this.state.bindCodeExpiresAt >= Date.now() && verifyBindCode(code, this.state.bindCodeHash)
1950
+ );
1951
+ if (!valid) {
1952
+ await this.gateway?.sendText(message.chatId, "\u7ED1\u5B9A\u5931\u8D25\uFF1A\u7ED1\u5B9A\u7801\u65E0\u6548\u6216\u5DF2\u8FC7\u671F\u3002");
1953
+ return;
1954
+ }
1955
+ await this.bindChat(message, false);
1956
+ }
1957
+ canAutoBind(message) {
1958
+ return !this.state.autoBindDisabled && Boolean(this.state.ownerOpenId) && message.senderId === this.state.ownerOpenId;
1959
+ }
1960
+ async bindChat(message, automatic) {
1961
+ this.state = {
1962
+ ...this.state,
1963
+ ownerOpenId: this.state.ownerOpenId ?? message.senderId,
1964
+ boundChatId: message.chatId,
1965
+ autoBindDisabled: false,
1966
+ bindCodeHash: void 0,
1967
+ bindCodeExpiresAt: void 0,
1968
+ updatedAt: Date.now()
1969
+ };
1970
+ await this.store.saveState(this.state);
1971
+ await this.gateway?.sendText(
1972
+ message.chatId,
1973
+ automatic ? "\u5DF2\u81EA\u52A8\u8FDE\u63A5\u5F53\u524D coding agent \u4F1A\u8BDD\u3002\u4E4B\u540E\u76F4\u63A5\u53D1\u9001\u666E\u901A\u6D88\u606F\u5373\u53EF\u3002" : "\u7ED1\u5B9A\u6210\u529F\u3002\u4E4B\u540E\u7684\u666E\u901A\u6D88\u606F\u4F1A\u53D1\u9001\u5230\u5F53\u524D tmux \u4E2D\u7684 coding agent\uFF1B\u53EF\u7528 /tail\u3001/status\u3001/sessions\u3001/detach\u3001/stop\u3002"
1974
+ );
1975
+ this.previousScreen = void 0;
1976
+ await this.poll();
1977
+ }
1978
+ allowAttachAttempt(senderId) {
1979
+ const cutoff = Date.now() - 6e4;
1980
+ const attempts = (this.attachAttempts.get(senderId) ?? []).filter((time) => time >= cutoff);
1981
+ if (attempts.length >= 5) return false;
1982
+ attempts.push(Date.now());
1983
+ this.attachAttempts.set(senderId, attempts);
1984
+ return true;
1985
+ }
1986
+ shouldQueueMessage() {
1987
+ return !this.screen || this.screen.state === "approval" || this.screen.state === "unknown" || this.screen.state === "failed" || this.screen.state === "exited" || this.screen.hasDraftInput;
1988
+ }
1989
+ async onLarkAction(event, action) {
1990
+ if (event.operator.openId !== this.state.ownerOpenId || event.chatId !== this.state.boundChatId) {
1991
+ return { type: "error", content: "\u65E0\u6743\u64CD\u4F5C\u5F53\u524D\u4F1A\u8BDD\u3002" };
1992
+ }
1993
+ if (action.kind === "session") {
1994
+ const target = this.state.sessions?.[action.action];
1995
+ if (!target || target.agent !== action.agent || target.paneId !== action.paneId || String(target.updatedAt) !== action.fingerprint) {
1996
+ return { type: "error", content: "\u76EE\u6807 session \u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 /sessions\u3002" };
1997
+ }
1998
+ const result3 = await this.useSession(target.id);
1999
+ return result3.ok ? { type: "success", content: `\u5DF2\u8FDE\u63A5\u5230 ${target.id}` } : { type: "error", content: result3.error };
2000
+ }
2001
+ if (action.kind === "manual") {
2002
+ return this.withInteractionNotificationsSuppressed(() => this.handleManualAction(action, event));
2003
+ }
2004
+ await this.poll();
2005
+ const session = this.activeSession();
2006
+ if (action.kind === "choice") {
2007
+ return this.withInteractionNotificationsSuppressed(() => this.handleChoiceAction(action, session, event));
2008
+ }
2009
+ if (action.paneId !== session?.paneId || action.agent !== session.agent || action.fingerprint !== this.screen?.fingerprint) {
2010
+ return { type: "error", content: "\u4F1A\u8BDD\u753B\u9762\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 /stop\u3002" };
2011
+ }
2012
+ const result2 = await this.stopSession(session.id);
2013
+ return result2.ok ? { type: "success", content: `${getAgentAdapter(session.agent).displayName} \u4F1A\u8BDD\u5DF2\u505C\u6B62\u3002` } : { type: "error", content: result2.error };
2014
+ }
2015
+ async handleManualAction(action, event) {
2016
+ await this.poll();
2017
+ const session = this.activeSession();
2018
+ const screen = this.screen;
2019
+ if (!session || !screen || action.sessionId !== session.id || action.agent !== session.agent || action.paneId !== session.paneId) {
2020
+ return { type: "error", content: "\u624B\u52A8\u9065\u63A7\u5BF9\u5E94\u7684 session \u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 /manual\u3002" };
2021
+ }
2022
+ if (this.closedManualCards.has(event.messageId)) {
2023
+ return {
2024
+ type: "manual",
2025
+ content: "\u8BE5\u624B\u52A8\u9065\u63A7\u5361\u5DF2\u7ECF\u7ED3\u675F\u3002",
2026
+ view: this.manualView(session, screen, "exited", void 0, "\u7ED3\u675F\u9065\u63A7", action.manualMode)
2027
+ };
2028
+ }
2029
+ if (action.action !== "refresh" && action.fingerprint !== screen.fingerprint) {
2030
+ return {
2031
+ type: "manual",
2032
+ content: "\u7EC8\u7AEF\u753B\u9762\u5DF2\u53D8\u5316\uFF0C\u65E7\u64CD\u4F5C\u672A\u6267\u884C\u3002",
2033
+ view: this.manualView(session, screen, "stale", "\u8BF7\u786E\u8BA4\u6700\u65B0\u7EC8\u7AEF\u753B\u9762\u540E\u91CD\u8BD5\u3002", void 0, action.manualMode)
2034
+ };
2035
+ }
2036
+ if (action.action === "exit") {
2037
+ remember(this.closedManualCards, event.messageId, 256);
2038
+ return {
2039
+ type: "manual",
2040
+ content: "\u5DF2\u7ED3\u675F\u624B\u52A8\u9065\u63A7\u3002",
2041
+ view: this.manualView(session, screen, "exited", void 0, "\u7ED3\u675F\u9065\u63A7", action.manualMode)
2042
+ };
2043
+ }
2044
+ let operation = "\u5237\u65B0\u7EC8\u7AEF\u8F93\u51FA";
2045
+ try {
2046
+ if (action.action === "type" || action.action === "submit") {
2047
+ const value = event.action.formValue?.[MANUAL_TEXT_FIELD];
2048
+ if (typeof value !== "string" || !value.trim()) return { type: "error", content: "\u8BF7\u8F93\u5165\u8981\u53D1\u9001\u5230\u7EC8\u7AEF\u7684\u6587\u672C\u3002" };
2049
+ const submit = action.action === "submit";
2050
+ operation = submit ? "\u8F93\u5165\u6587\u672C\u5E76\u63D0\u4EA4" : "\u4EC5\u8F93\u5165\u6587\u672C";
2051
+ await this.tmux.sendText(session.paneId, value, submit);
2052
+ } else if (action.action !== "refresh") {
2053
+ const key = manualTmuxKey(action.action);
2054
+ operation = `\u6309\u952E ${action.action}`;
2055
+ await this.tmux.sendKey(session.paneId, key);
2056
+ }
2057
+ if (action.action === "refresh") await this.poll();
2058
+ else await this.settleManualScreen();
2059
+ } catch (error) {
2060
+ await this.poll().catch(() => void 0);
2061
+ const current2 = this.screen ?? screen;
2062
+ return {
2063
+ type: "manual",
2064
+ content: "\u624B\u52A8\u64CD\u4F5C\u5931\u8D25\u3002",
2065
+ view: this.manualView(session, current2, "error", errorMessage(error), operation, action.manualMode)
2066
+ };
2067
+ }
2068
+ const current = this.screen ?? screen;
2069
+ if (action.manualMode !== "explicit" && safeStructuredInteraction(current)) {
2070
+ await this.gateway?.sendChoice(event.chatId, session.paneId, current, session.agent);
2071
+ return {
2072
+ type: "manual",
2073
+ content: "\u5DF2\u6062\u590D\u7ED3\u6784\u5316\u8BC6\u522B\u3002",
2074
+ view: this.manualView(session, current, "recovered", void 0, operation, action.manualMode)
2075
+ };
2076
+ }
2077
+ return {
2078
+ type: "manual",
2079
+ content: `${operation}\u5DF2\u6267\u884C\u3002`,
2080
+ view: this.manualView(session, current, "active", void 0, operation, action.manualMode)
2081
+ };
2082
+ }
2083
+ currentManualView() {
2084
+ const session = this.activeSession();
2085
+ if (!session || !this.screen || this.screen.state === "exited") return void 0;
2086
+ return this.manualView(session, this.screen, "active", void 0, void 0, "explicit");
2087
+ }
2088
+ async settleManualScreen() {
2089
+ let previousFingerprint;
2090
+ const deadline = Date.now() + 900;
2091
+ do {
2092
+ await new Promise((resolve) => setTimeout(resolve, 120));
2093
+ await this.poll();
2094
+ const fingerprint = this.screen?.fingerprint;
2095
+ if (fingerprint && fingerprint === previousFingerprint) return;
2096
+ previousFingerprint = fingerprint;
2097
+ } while (Date.now() < deadline);
2098
+ }
2099
+ manualView(session, screen, state = "active", notice, lastOperation, mode = "fallback") {
2100
+ return {
2101
+ session,
2102
+ screen,
2103
+ output: tailScreen(screen.normalized, 40),
2104
+ capturedAt: /* @__PURE__ */ new Date(),
2105
+ state,
2106
+ notice,
2107
+ lastOperation,
2108
+ mode
2109
+ };
2110
+ }
2111
+ async executeManualCommand(chatId, operation, execute) {
2112
+ await this.poll();
2113
+ const session = this.activeSession();
2114
+ if (!session || !this.screen || this.screen.state === "exited") {
2115
+ await this.gateway?.sendText(chatId, "\u5F53\u524D\u6CA1\u6709\u53EF\u9065\u63A7\u7684 active tmux session\u3002");
2116
+ return;
2117
+ }
2118
+ try {
2119
+ await execute(session);
2120
+ await new Promise((resolve) => setTimeout(resolve, 120));
2121
+ await this.poll();
2122
+ const output = this.screen ? tailScreen(this.screen.normalized, 60) : "\u65E0\u6CD5\u8BFB\u53D6\u6700\u65B0\u7EC8\u7AEF\u753B\u9762\u3002";
2123
+ await this.gateway?.sendMarkdown(chatId, `**\u624B\u52A8\u64CD\u4F5C\uFF1A${operation}**
2124
+
2125
+ \`\`\`text
2126
+ ${escapeFence2(output).slice(-6500)}
2127
+ \`\`\``);
2128
+ } catch (error) {
2129
+ await this.gateway?.sendText(chatId, `\u624B\u52A8\u64CD\u4F5C\u5931\u8D25\uFF1A${errorMessage(error)}`);
2130
+ }
2131
+ }
2132
+ async handleChoiceAction(action, session, event) {
2133
+ if (action.paneId !== session?.paneId || action.agent !== session.agent) return { type: "error", content: "\u4F1A\u8BDD\u5DF2\u53D8\u5316\u3002" };
2134
+ if (event.action.formValue) {
2135
+ if (action.action !== CHOICE_FORM_SUBMIT_ACTION) return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u8BE5\u8868\u5355\u64CD\u4F5C\uFF0C\u8BF7\u4F7F\u7528\u6700\u65B0\u5361\u7247\u3002" };
2136
+ return this.handleToggleFormSubmit(action, session, event.action.formValue);
2137
+ }
2138
+ const before = this.screen?.interaction;
2139
+ const target = this.screen?.actions.find(({ key }) => key === action.action);
2140
+ const fingerprint = before?.revision ?? this.screen?.fingerprint ?? action.fingerprint;
2141
+ const customAlreadySelected = target?.role === "custom-input" && (target.marker === "checked" || target.marker === "selected") && fingerprint === action.fingerprint && before?.kind === action.interactionKind;
2142
+ const opensEditor = target?.role === "custom-input" && target.editor;
2143
+ let result2;
2144
+ if (customAlreadySelected) {
2145
+ result2 = { ok: true, value: `\u7EE7\u7EED\u586B\u5199\uFF1A${target.label}` };
2146
+ } else if (opensEditor && before?.interactionId) {
2147
+ const navigation = await this.navigateToControl(session.paneId, target.id, before.interactionId);
2148
+ if (!navigation.ok) return { type: "error", content: navigation.error };
2149
+ if (opensEditor.openKey) await this.tmux.sendKey(session.paneId, opensEditor.openKey);
2150
+ result2 = { ok: true, value: `\u7EE7\u7EED\u586B\u5199\uFF1A${target.label}` };
2151
+ } else {
2152
+ result2 = await this.submitChoice(action.action, fingerprint, action.interactionKind);
2153
+ }
2154
+ if (!result2.ok) return { type: "error", content: result2.error };
2155
+ if (before?.semantics?.activation === "toggle" && target?.role === "answer") {
2156
+ await this.waitForControlMarker(before.interactionId, target.id, target.marker);
2157
+ const currentScreen = this.screen;
2158
+ const currentTarget = currentScreen?.actions.find(({ id }) => id === target.id);
2159
+ if (!currentScreen || !currentTarget || currentScreen.interaction?.interactionId !== before.interactionId || currentTarget.marker === target.marker) {
2160
+ return { type: "error", content: "\u7EC8\u7AEF\u9009\u62E9\u72B6\u6001\u6CA1\u6709\u6309\u9884\u671F\u66F4\u65B0\uFF0C\u8BF7\u7528 /tail \u68C0\u67E5\u3002" };
2161
+ }
2162
+ return {
2163
+ type: "refresh",
2164
+ content: `${currentTarget.marker === "checked" || currentTarget.marker === "selected" ? "\u5DF2\u9009\u62E9" : "\u5DF2\u53D6\u6D88"}\uFF1A${target.label}`,
2165
+ screen: currentScreen,
2166
+ paneId: session.paneId,
2167
+ agent: session.agent
2168
+ };
2169
+ }
2170
+ if (target?.role === "custom-input" || target?.role === "chat") {
2171
+ await new Promise((resolve) => setTimeout(resolve, 100));
2172
+ await this.poll();
2173
+ if (!before?.interactionId) return { type: "error", content: "\u65E0\u6CD5\u786E\u8BA4\u5F53\u524D\u4EA4\u4E92\uFF0C\u8BF7\u7528 /tail \u68C0\u67E5\u3002" };
2174
+ this.pendingInteractionInput = {
2175
+ sessionId: session.id,
2176
+ interactionId: this.screen?.interaction?.interactionId ?? before.interactionId,
2177
+ controlId: target.id,
2178
+ role: target.role,
2179
+ label: target.label,
2180
+ cardMessageId: event.messageId,
2181
+ submitOnInput: target.role === "chat" || before.semantics?.activation === "submit",
2182
+ action
2183
+ };
2184
+ return { type: "awaiting-input", content: "\u8BF7\u53D1\u9001\u4E0B\u4E00\u6761\u666E\u901A\u6D88\u606F\u3002", agent: session.agent, label: target.label };
2185
+ }
2186
+ await this.waitForInteractionChange(before?.interactionId);
2187
+ if (this.screen?.interaction && this.screen.interaction.interactionId !== before?.interactionId && this.screen.interaction.semantics && (this.screen.interaction.actionConfidence ?? 0) >= 0.85) {
2188
+ return {
2189
+ type: "refresh",
2190
+ content: "\u5DF2\u8FDB\u5165\u4E0B\u4E00\u6B65\u786E\u8BA4\u3002",
2191
+ screen: this.screen,
2192
+ paneId: session.paneId,
2193
+ agent: session.agent
2194
+ };
2195
+ }
2196
+ if ((target?.role === "submit" || before?.semantics?.commit.mode === "immediate") && this.screen?.interaction?.interactionId === before?.interactionId) {
2197
+ return { type: "error", content: "\u7EC8\u7AEF\u4ECD\u505C\u7559\u5728\u539F\u95EE\u9898\uFF0C\u672A\u786E\u8BA4\u63D0\u4EA4\u6210\u529F\u3002" };
2198
+ }
2199
+ await this.flushPending();
2200
+ return { type: "success", content: result2.value };
2201
+ }
2202
+ async handleToggleFormSubmit(action, session, formValue) {
2203
+ const initialScreen = this.screen;
2204
+ const interaction = initialScreen?.interaction;
2205
+ if (!initialScreen || !interaction?.interactionId || interaction.semantics?.activation !== "toggle" || (interaction.revision ?? initialScreen.fingerprint) !== action.fingerprint || interaction.kind !== action.interactionKind) {
2206
+ return { type: "error", content: "\u5361\u7247\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u4F7F\u7528\u6700\u65B0\u5361\u7247\u3002" };
2207
+ }
2208
+ const synced = await this.syncToggleFormState(session.paneId, initialScreen, formValue);
2209
+ if (!synced.ok) return { type: "error", content: synced.error };
2210
+ if (synced.committed) {
2211
+ await this.waitForInteractionChange(interaction.interactionId);
2212
+ if (this.screen?.interaction?.interactionId === interaction.interactionId) {
2213
+ return { type: "error", content: "\u81EA\u5B9A\u4E49\u5185\u5BB9\u5DF2\u540C\u6B65\uFF0C\u4F46\u7EC8\u7AEF\u4ECD\u505C\u7559\u5728\u539F\u95EE\u9898\u3002" };
2214
+ }
2215
+ if (this.screen?.interaction?.semantics && (this.screen.interaction.actionConfidence ?? 0) >= 0.85) {
2216
+ return {
2217
+ type: "refresh",
2218
+ content: "\u7B54\u6848\u5DF2\u63D0\u4EA4\uFF0C\u5DF2\u8FDB\u5165\u4E0B\u4E00\u6B65\u786E\u8BA4\u3002",
2219
+ screen: this.screen,
2220
+ paneId: session.paneId,
2221
+ agent: session.agent
2222
+ };
2223
+ }
2224
+ await this.flushPending();
2225
+ return { type: "success", content: `\u5DF2\u5411 ${getAgentAdapter(session.agent).displayName} \u63D0\u4EA4\u7B54\u6848\u3002` };
2226
+ }
2227
+ await this.poll();
2228
+ const current = this.screen;
2229
+ if (!current?.interaction || current.interaction.interactionId !== interaction.interactionId) {
2230
+ return { type: "error", content: "\u7EC8\u7AEF\u95EE\u9898\u5728\u63D0\u4EA4\u524D\u5DF2\u7ECF\u53D8\u5316\uFF0C\u8BF7\u4F7F\u7528\u6700\u65B0\u5361\u7247\u3002" };
2231
+ }
2232
+ const commit = current.interaction.semantics?.commit;
2233
+ if (!commit || commit.mode === "immediate") return { type: "error", content: "\u65E0\u6CD5\u786E\u5B9A\u7EC8\u7AEF\u63D0\u4EA4\u65B9\u5F0F\uFF0C\u8BF7\u7528 /tail \u68C0\u67E5\u3002" };
2234
+ if (commit.mode === "key") {
2235
+ await this.tmux.sendKey(session.paneId, commit.key);
2236
+ } else {
2237
+ const submit = current.actions.find(({ id }) => id === commit.controlId);
2238
+ if (!submit) return { type: "error", content: "\u7EC8\u7AEF\u63D0\u4EA4\u6309\u94AE\u5DF2\u7ECF\u53D8\u5316\uFF0C\u8BF7\u4F7F\u7528\u6700\u65B0\u5361\u7247\u3002" };
2239
+ const navigation = await this.navigateToControl(session.paneId, submit.id, interaction.interactionId);
2240
+ if (!navigation.ok) return { type: "error", content: navigation.error };
2241
+ await this.tmux.sendKey(session.paneId, "Enter");
2242
+ }
2243
+ await this.waitForInteractionChange(interaction.interactionId);
2244
+ if (this.screen?.interaction?.interactionId === interaction.interactionId) {
2245
+ return { type: "error", content: "\u7EC8\u7AEF\u4ECD\u505C\u7559\u5728\u539F\u95EE\u9898\uFF0C\u672A\u786E\u8BA4\u63D0\u4EA4\u6210\u529F\u3002" };
2246
+ }
2247
+ if (this.screen?.interaction?.semantics && (this.screen.interaction.actionConfidence ?? 0) >= 0.85) {
2248
+ return {
2249
+ type: "refresh",
2250
+ content: "\u7B54\u6848\u5DF2\u63D0\u4EA4\uFF0C\u5DF2\u8FDB\u5165\u4E0B\u4E00\u6B65\u786E\u8BA4\u3002",
2251
+ screen: this.screen,
2252
+ paneId: session.paneId,
2253
+ agent: session.agent
2254
+ };
2255
+ }
2256
+ await this.flushPending();
2257
+ return { type: "success", content: `\u5DF2\u5411 ${getAgentAdapter(session.agent).displayName} \u63D0\u4EA4\u7B54\u6848\u3002` };
2258
+ }
2259
+ async withInteractionNotificationsSuppressed(operation) {
2260
+ this.interactionNotificationsSuppressed += 1;
2261
+ try {
2262
+ return await operation();
2263
+ } finally {
2264
+ this.interactionNotificationsSuppressed -= 1;
2265
+ }
2266
+ }
2267
+ async key(key, fingerprint) {
2268
+ return this.submitChoice(key, fingerprint);
2269
+ }
2270
+ async submitChoice(choice, fingerprint, expectedKind) {
2271
+ const session = this.activeSession();
2272
+ if (!session) return { ok: false, error: "no active managed session" };
2273
+ await this.poll();
2274
+ const screen = this.screen;
2275
+ const interaction = screen?.interaction;
2276
+ const revision = interaction?.revision ?? screen?.fingerprint;
2277
+ if (!screen || !interaction?.semantics || !interaction.actionConfidence || interaction.actionConfidence < 0.85 || revision !== fingerprint || screen.actions.length === 0 || expectedKind && interaction.kind !== expectedKind) {
2278
+ return { ok: false, error: "choice screen changed; refusing stale action" };
2279
+ }
2280
+ const target = screen.actions.find((action) => action.key === choice);
2281
+ if (!target) return { ok: false, error: "option is not valid for current choice" };
2282
+ if (target.shortcut && (target.role === "custom-input" || target.role === "chat")) {
2283
+ await this.tmux.sendKey(session.paneId, target.shortcut);
2284
+ } else {
2285
+ const navigation = await this.navigateToControl(session.paneId, target.id, interaction.interactionId);
2286
+ if (!navigation.ok) return navigation;
2287
+ await this.tmux.sendKey(session.paneId, "Enter");
2288
+ }
2289
+ const verb = interaction.kind === "approval" ? "\u63D0\u4EA4\u5BA1\u6279\u9009\u62E9" : interaction.kind === "question" ? "\u63D0\u4EA4\u7B54\u6848" : "\u63D0\u4EA4\u9009\u62E9";
2290
+ return { ok: true, value: `\u5DF2\u5411 ${getAgentAdapter(session.agent).displayName} ${verb}\uFF1A${target.label}` };
2291
+ }
2292
+ async navigateToControl(paneId, targetId, interactionId) {
2293
+ for (let step = 0; step < 24; step += 1) {
2294
+ const screen = this.screen;
2295
+ if (!screen?.interaction || screen.interaction.interactionId !== interactionId) {
2296
+ return { ok: false, error: "interaction changed while navigating; refusing action" };
2297
+ }
2298
+ const focusedIndex = screen.actions.findIndex(({ focused }) => focused);
2299
+ const targetIndex = screen.actions.findIndex(({ id }) => id === targetId);
2300
+ if (focusedIndex === -1 || targetIndex === -1) return { ok: false, error: "cannot determine current or target focus" };
2301
+ if (focusedIndex === targetIndex) return { ok: true };
2302
+ const direction = targetIndex > focusedIndex ? "Down" : "Up";
2303
+ await this.tmux.sendKey(paneId, direction);
2304
+ await new Promise((resolve) => setTimeout(resolve, 40));
2305
+ await this.poll();
2306
+ const nextFocused = this.screen?.actions.findIndex(({ focused }) => focused) ?? -1;
2307
+ if (nextFocused === focusedIndex) return { ok: false, error: "terminal focus did not move as expected" };
2308
+ }
2309
+ return { ok: false, error: "terminal focus navigation exceeded its safe step limit" };
2310
+ }
2311
+ async waitForInteractionChange(interactionId) {
2312
+ const deadline = Date.now() + 1500;
2313
+ while (Date.now() < deadline) {
2314
+ await new Promise((resolve) => setTimeout(resolve, 75));
2315
+ await this.poll();
2316
+ const current = this.screen?.interaction;
2317
+ if (!current || current.interactionId !== interactionId) return;
2318
+ }
2319
+ }
2320
+ async waitForControlMarker(interactionId, controlId, marker) {
2321
+ const deadline = Date.now() + 1500;
2322
+ while (Date.now() < deadline) {
2323
+ await new Promise((resolve) => setTimeout(resolve, 75));
2324
+ await this.poll();
2325
+ const current = this.screen?.interaction;
2326
+ const control = this.screen?.actions.find(({ id }) => id === controlId);
2327
+ if (!current || current.interactionId !== interactionId || control?.marker !== marker) return;
2328
+ }
2329
+ }
2330
+ async waitForCustomInputValue(interactionId, controlId, input) {
2331
+ const expected = input.trim();
2332
+ const deadline = Date.now() + 1500;
2333
+ while (Date.now() < deadline) {
2334
+ await new Promise((resolve) => setTimeout(resolve, 75));
2335
+ await this.poll();
2336
+ const control = this.screen?.actions.find(({ id }) => id === controlId);
2337
+ if (this.screen?.interaction?.interactionId === interactionId && control?.inputValue && (control.inputValue === expected || expected.startsWith(control.inputValue))) return;
2338
+ }
2339
+ }
2340
+ async syncToggleFormState(paneId, initialScreen, formValue) {
2341
+ const interactionId = initialScreen.interaction?.interactionId;
2342
+ if (!interactionId) return { ok: false, error: "\u65E0\u6CD5\u786E\u5B9A\u5F53\u524D\u7EC8\u7AEF\u95EE\u9898\u3002" };
2343
+ const desiredControls = initialScreen.actions.flatMap((control, index) => {
2344
+ if (control.role !== "answer" && control.role !== "custom-input") return [];
2345
+ const selected = formChecked(formValue[choiceFormFieldName(index)]);
2346
+ const rawInput = formValue[inlineInputName(index)];
2347
+ const input = control.role === "custom-input" && typeof rawInput === "string" ? rawInput.trim() : void 0;
2348
+ return [{ id: control.id, role: control.role, selected, input, editor: control.editor }];
2349
+ }).sort((left, right) => Number(left.role === "custom-input") - Number(right.role === "custom-input"));
2350
+ const emptyCustom = desiredControls.find(({ role, selected, input }) => role === "custom-input" && selected && !input);
2351
+ if (emptyCustom) return { ok: false, error: "\u5DF2\u52FE\u9009\u81EA\u5B9A\u4E49\u8F93\u5165\uFF0C\u4F46\u5185\u5BB9\u4E3A\u7A7A\u3002\u8BF7\u586B\u5199\u5185\u5BB9\u540E\u518D\u63D0\u4EA4\u3002" };
2352
+ const toggleKey = initialScreen.interaction?.semantics?.toggleKey ?? "Enter";
2353
+ for (const desired of desiredControls) {
2354
+ await this.poll();
2355
+ let control = this.screen?.actions.find(({ id }) => id === desired.id);
2356
+ if (!control || this.screen?.interaction?.interactionId !== interactionId) {
2357
+ return { ok: false, error: "\u7EC8\u7AEF\u9009\u9879\u5DF2\u7ECF\u53D8\u5316\uFF0C\u8BF7\u4F7F\u7528\u6700\u65B0\u5361\u7247\u3002" };
2358
+ }
2359
+ const currentlySelected = control.marker === "checked" || control.marker === "selected";
2360
+ if (currentlySelected !== desired.selected) {
2361
+ const navigation2 = await this.navigateToControl(paneId, control.id, interactionId);
2362
+ if (!navigation2.ok) return navigation2;
2363
+ const previousMarker = control.marker;
2364
+ await this.tmux.sendKey(paneId, toggleKey);
2365
+ await this.waitForControlMarker(interactionId, control.id, previousMarker);
2366
+ control = this.screen?.actions.find(({ id }) => id === desired.id);
2367
+ const selectedAfterToggle = control?.marker === "checked" || control?.marker === "selected";
2368
+ if (!control || selectedAfterToggle !== desired.selected) {
2369
+ return { ok: false, error: `\u7EC8\u7AEF\u9009\u9879\u201C${control?.label ?? desired.id}\u201D\u6CA1\u6709\u6309\u9884\u671F\u66F4\u65B0\u3002` };
2370
+ }
2371
+ }
2372
+ if (desired.role !== "custom-input" || !desired.selected || desired.input === void 0) continue;
2373
+ control = this.screen?.actions.find(({ id }) => id === desired.id);
2374
+ if (control?.inputValue?.trim() === desired.input) continue;
2375
+ const navigation = await this.navigateToControl(paneId, desired.id, interactionId);
2376
+ if (!navigation.ok) return navigation;
2377
+ if (desired.editor) {
2378
+ if (desired.editor.openKey) await this.tmux.sendKey(paneId, desired.editor.openKey);
2379
+ await new Promise((resolve) => setTimeout(resolve, 100));
2380
+ await this.tmux.sendKey(paneId, "C-u");
2381
+ await this.tmux.sendKey(paneId, "C-k");
2382
+ await this.tmux.sendText(paneId, desired.input, false);
2383
+ await this.tmux.sendKey(paneId, desired.editor.submitKey);
2384
+ return { ok: true, committed: desired.editor.commitsInteraction };
2385
+ }
2386
+ await this.tmux.sendKey(paneId, "C-u");
2387
+ await this.tmux.sendKey(paneId, "C-k");
2388
+ await this.tmux.sendText(paneId, desired.input, false);
2389
+ await this.waitForCustomInputValue(interactionId, desired.id, desired.input);
2390
+ control = this.screen?.actions.find(({ id }) => id === desired.id);
2391
+ if (!control?.inputValue || !(desired.input === control.inputValue || desired.input.startsWith(control.inputValue))) {
2392
+ return { ok: false, error: "\u81EA\u5B9A\u4E49\u5185\u5BB9\u6CA1\u6709\u5B8C\u6574\u540C\u6B65\u5230\u7EC8\u7AEF\uFF0C\u8BF7\u91CD\u8BD5\u3002" };
2393
+ }
2394
+ }
2395
+ return { ok: true, committed: false };
2396
+ }
2397
+ async stopSession(sessionId = this.state.activeSessionId) {
2398
+ if (!sessionId) return { ok: false, error: "no active managed session" };
2399
+ const session = this.state.sessions?.[sessionId];
2400
+ if (!session) return { ok: false, error: `unknown session: ${sessionId}` };
2401
+ if (await this.tmux.hasSession(session.sessionName)) {
2402
+ await this.tmux.killSession(session.sessionName);
2403
+ }
2404
+ const sessions = { ...this.state.sessions };
2405
+ delete sessions[sessionId];
2406
+ const wasActive = this.state.activeSessionId === sessionId;
2407
+ const activeSessionId = wasActive ? Object.keys(sessions)[0] : this.state.activeSessionId;
2408
+ this.state = {
2409
+ ...this.state,
2410
+ sessions,
2411
+ activeSessionId,
2412
+ updatedAt: Date.now()
2413
+ };
2414
+ if (wasActive) {
2415
+ this.screen = void 0;
2416
+ this.previousScreen = void 0;
2417
+ this.completedEvents.clear();
2418
+ this.clearPendingCompletion();
2419
+ this.pendingMessages.length = 0;
2420
+ this.pendingInteractionInput = void 0;
2421
+ this.unresolvedCandidate = void 0;
2422
+ this.unresolvedNotified.clear();
2423
+ }
2424
+ await this.store.saveState(this.state);
2425
+ return { ok: true };
2426
+ }
2427
+ async resetOwner() {
2428
+ this.pendingMessages.length = 0;
2429
+ this.pendingInteractionInput = void 0;
2430
+ this.unresolvedCandidate = void 0;
2431
+ this.unresolvedNotified.clear();
2432
+ this.state = {
2433
+ ...this.state,
2434
+ ownerOpenId: void 0,
2435
+ boundChatId: void 0,
2436
+ autoBindDisabled: true,
2437
+ updatedAt: Date.now()
2438
+ };
2439
+ await this.store.saveState(this.state);
2440
+ return { ok: true };
2441
+ }
2442
+ async rotateBindCode() {
2443
+ if (Object.keys(this.state.sessions ?? {}).length === 0) return { ok: false, error: "no managed coding-agent session is running" };
2444
+ const bindCode = createBindCode();
2445
+ this.state = {
2446
+ ...this.state,
2447
+ boundChatId: void 0,
2448
+ autoBindDisabled: true,
2449
+ bindCodeHash: hashBindCode(bindCode),
2450
+ bindCodeExpiresAt: Date.now() + 10 * 6e4,
2451
+ updatedAt: Date.now()
2452
+ };
2453
+ await this.store.saveState(this.state);
2454
+ return { ok: true, value: { bindCode, expiresInSeconds: 600 } };
2455
+ }
2456
+ schedulePoll() {
2457
+ if (this.closing) return;
2458
+ this.timer = setTimeout(() => {
2459
+ void this.runScheduledPoll();
2460
+ }, this.config.pollIntervalMs);
2461
+ }
2462
+ async runScheduledPoll() {
2463
+ const operation = this.poll().catch(async (error) => {
2464
+ if (!this.closing) await this.log(`poll failed: ${errorMessage(error)}`);
2465
+ });
2466
+ this.pollInFlight = operation;
2467
+ try {
2468
+ await operation;
2469
+ } finally {
2470
+ if (this.pollInFlight === operation) this.pollInFlight = void 0;
2471
+ if (!this.closing) this.schedulePoll();
2472
+ }
2473
+ }
2474
+ async poll() {
2475
+ const session = this.activeSession();
2476
+ const paneId = session?.paneId;
2477
+ if (!paneId) {
2478
+ await this.reconcileSessions();
2479
+ return;
2480
+ }
2481
+ const pane = await this.tmux.inspect(paneId);
2482
+ const adapter = getAgentAdapter(session.agent);
2483
+ if (!pane || pane.dead) {
2484
+ this.updateScreen(adapter.detectScreen("", false));
2485
+ await this.notifyTransition();
2486
+ await this.maybeNotifyUnresolved();
2487
+ await this.maybeNotifyCompletion();
2488
+ await this.reconcileSessions();
2489
+ return;
2490
+ }
2491
+ const raw = await this.tmux.capture(paneId, 160);
2492
+ this.updateScreen(adapter.detectScreen(raw, true, { x: pane.cursorX, y: pane.cursorY }));
2493
+ await this.notifyTransition();
2494
+ await this.maybeNotifyUnresolved();
2495
+ await this.maybeNotifyCompletion();
2496
+ await this.flushPending();
2497
+ await this.reconcileSessions();
2498
+ }
2499
+ async notifyTransition() {
2500
+ const current = this.screen;
2501
+ const previous = this.previousScreen;
2502
+ this.previousScreen = current;
2503
+ const session = this.activeSession();
2504
+ if (!current || !session || !this.gateway) return;
2505
+ const agentName = getAgentAdapter(session.agent).displayName;
2506
+ if (previous?.state === current.state && previous.fingerprint === current.fingerprint && selectionSnapshot(previous) === selectionSnapshot(current)) return;
2507
+ if (this.interactionNotificationsSuppressed > 0 && current.interaction) return;
2508
+ try {
2509
+ if (!this.state.boundChatId) return;
2510
+ if (current.interaction?.semantics && current.interaction.actionConfidence && current.interaction.actionConfidence >= 0.85 && current.actions.length > 0 && current.confidence >= 0.85) {
2511
+ await this.gateway.sendChoice(this.state.boundChatId, session.paneId, current, session.agent);
2512
+ } else if (current.state === "failed" && previous?.state !== "failed") {
2513
+ await this.gateway.sendText(this.state.boundChatId, `${agentName} \u68C0\u6D4B\u5230\u5931\u8D25\u72B6\u6001\uFF0C\u8BF7\u7528 /tail \u67E5\u770B\u3002`);
2514
+ } else if (current.state === "exited" && previous?.state !== "exited") {
2515
+ await this.gateway.sendText(this.state.boundChatId, `${agentName}/tmux pane \u5DF2\u9000\u51FA\u3002`);
2516
+ }
2517
+ } catch (error) {
2518
+ await this.log(`notification failed: ${errorMessage(error)}`);
2519
+ }
2520
+ }
2521
+ async maybeNotifyUnresolved() {
2522
+ const screen = this.screen;
2523
+ const session = this.activeSession();
2524
+ if (this.interactionNotificationsSuppressed > 0 || !screen || !session || !this.gateway || !this.state.boundChatId || screen.state !== "input" && screen.state !== "unknown" || safeStructuredInteraction(screen)) {
2525
+ this.unresolvedCandidate = void 0;
2526
+ return;
2527
+ }
2528
+ const key = `${session.id}:${session.paneId}:${screen.fingerprint}`;
2529
+ if (this.unresolvedCandidate?.key !== key) {
2530
+ this.unresolvedCandidate = { key, since: Date.now() };
2531
+ return;
2532
+ }
2533
+ if (Date.now() - this.unresolvedCandidate.since < 3e3 || this.unresolvedNotified.has(key)) return;
2534
+ remember(this.unresolvedNotified, key, 256);
2535
+ try {
2536
+ await this.gateway.sendManual(
2537
+ this.state.boundChatId,
2538
+ this.manualView(session, screen, "active", "\u5F53\u524D\u7EC8\u7AEF\u4EA4\u4E92\u65E0\u6CD5\u5B89\u5168\u8BC6\u522B\uFF0C\u5DF2\u81EA\u52A8\u8FDB\u5165\u624B\u52A8\u9065\u63A7\u515C\u5E95\u3002")
2539
+ );
2540
+ } catch (error) {
2541
+ await this.log(`manual fallback card failed: ${errorMessage(error)}`);
2542
+ await this.gateway.sendText(
2543
+ this.state.boundChatId,
2544
+ "\u5F53\u524D\u7EC8\u7AEF\u4EA4\u4E92\u65E0\u6CD5\u5B89\u5168\u8BC6\u522B\uFF0C\u4E14\u624B\u52A8\u9065\u63A7\u5361\u53D1\u9001\u5931\u8D25\u3002\u53EF\u4F7F\u7528 /tail 120\u3001/key\u3001/type \u6216 /submit \u5904\u7406\u3002"
2545
+ ).catch((sendError) => this.log(`manual fallback text failed: ${errorMessage(sendError)}`));
2546
+ }
2547
+ }
2548
+ async handleTurnComplete(candidate) {
2549
+ if (!validTurnCompleteCandidate(candidate)) return { ok: false, error: "invalid turn-complete candidate" };
2550
+ const eventKey = `${candidate.agentSessionId}:${candidate.eventId}`;
2551
+ if (this.completedEvents.has(eventKey)) return { ok: true };
2552
+ remember(this.completedEvents, eventKey, 256);
2553
+ if (candidate.sessionId !== this.state.activeSessionId) return { ok: true };
2554
+ if (!this.activeSession() || !this.state.boundChatId || !this.gateway) return { ok: true };
2555
+ this.pendingCompletion = candidate;
2556
+ this.pendingCompletionAt = Date.now();
2557
+ return { ok: true };
2558
+ }
2559
+ updateScreen(next) {
2560
+ if (this.screen?.fingerprint !== next.fingerprint || this.screen.state !== next.state) {
2561
+ this.outputStableSince = Date.now();
2562
+ }
2563
+ this.screen = next;
2564
+ }
2565
+ async maybeNotifyCompletion() {
2566
+ const candidate = this.pendingCompletion;
2567
+ const session = this.activeSession();
2568
+ if (!candidate || !session || !this.state.boundChatId || !this.gateway) return;
2569
+ if (this.screen?.state === "approval" || this.screen?.state === "input" || this.screen?.state === "failed" || this.screen?.state === "exited") {
2570
+ this.clearPendingCompletion();
2571
+ return;
2572
+ }
2573
+ if (this.screen?.state !== "idle") return;
2574
+ const quietSince = Math.max(this.pendingCompletionAt, this.outputStableSince);
2575
+ if (Date.now() - quietSince < this.completionQuietMs) return;
2576
+ this.clearPendingCompletion();
2577
+ const adapter = getAgentAdapter(session.agent);
2578
+ const output = candidate.lastAssistantMessage.trim();
2579
+ if (!output) return;
2580
+ await this.gateway.sendMarkdown(
2581
+ this.state.boundChatId,
2582
+ `**${adapter.displayName} \u7B49\u5F85\u7528\u6237\u8F93\u5165**
2583
+
2584
+ ${output}`
2585
+ );
2586
+ }
2587
+ clearPendingCompletion() {
2588
+ this.pendingCompletion = void 0;
2589
+ this.pendingCompletionAt = 0;
2590
+ }
2591
+ async flushPending() {
2592
+ const session = this.activeSession();
2593
+ if (!session || this.shouldQueueMessage()) return;
2594
+ while (this.pendingMessages.length > 0 && !this.shouldQueueMessage()) {
2595
+ const message = this.pendingMessages[0];
2596
+ if (!message) {
2597
+ this.pendingMessages.shift();
2598
+ continue;
2599
+ }
2600
+ this.clearPendingCompletion();
2601
+ await this.tmux.sendText(session.paneId, message);
2602
+ this.pendingMessages.shift();
2603
+ }
2604
+ if (this.pendingMessages.length === 0 && this.state.boundChatId) {
2605
+ }
2606
+ }
2607
+ activeSession() {
2608
+ return this.state.activeSessionId ? this.state.sessions?.[this.state.activeSessionId] : void 0;
2609
+ }
2610
+ async reconcileSessions() {
2611
+ const sessions = Object.values(this.state.sessions ?? {});
2612
+ const inspections = await Promise.all(sessions.map(async (session) => ({
2613
+ session,
2614
+ pane: await this.tmux.inspect(session.paneId)
2615
+ })));
2616
+ const liveSessions = inspections.filter(({ pane }) => pane && !pane.dead).map(({ session }) => session);
2617
+ const liveById = Object.fromEntries(liveSessions.map((session) => [session.id, session]));
2618
+ const activeSessionId = this.state.activeSessionId && liveById[this.state.activeSessionId] ? this.state.activeSessionId : liveSessions[0]?.id;
2619
+ const sessionsChanged = liveSessions.length !== sessions.length;
2620
+ const activeChanged = activeSessionId !== this.state.activeSessionId;
2621
+ if (!sessionsChanged && !activeChanged) return liveSessions;
2622
+ const removedActive = this.state.activeSessionId ? this.state.sessions?.[this.state.activeSessionId] : void 0;
2623
+ if (activeChanged && removedActive && !liveById[removedActive.id] && this.previousScreen?.state !== "exited" && this.state.boundChatId) {
2624
+ await this.gateway?.sendText(
2625
+ this.state.boundChatId,
2626
+ `${getAgentAdapter(removedActive.agent).displayName}/tmux pane \u5DF2\u9000\u51FA\u3002`
2627
+ ).catch((error) => this.log(`exit notification failed: ${errorMessage(error)}`));
2628
+ }
2629
+ this.state = {
2630
+ ...this.state,
2631
+ sessions: liveById,
2632
+ activeSessionId,
2633
+ updatedAt: Date.now()
2634
+ };
2635
+ if (activeChanged) {
2636
+ this.screen = void 0;
2637
+ this.previousScreen = void 0;
2638
+ this.completedEvents.clear();
2639
+ this.clearPendingCompletion();
2640
+ this.pendingMessages.length = 0;
2641
+ this.pendingInteractionInput = void 0;
2642
+ this.unresolvedCandidate = void 0;
2643
+ this.unresolvedNotified.clear();
2644
+ }
2645
+ await this.store.saveState(this.state);
2646
+ return liveSessions;
2647
+ }
2648
+ async useSession(sessionId) {
2649
+ const session = this.state.sessions?.[sessionId];
2650
+ if (!session || !await this.tmux.inspect(session.paneId)) return { ok: false, error: `unknown or stopped session: ${sessionId}` };
2651
+ this.state = { ...this.state, activeSessionId: sessionId, updatedAt: Date.now() };
2652
+ this.screen = void 0;
2653
+ this.previousScreen = void 0;
2654
+ this.completedEvents.clear();
2655
+ this.clearPendingCompletion();
2656
+ this.pendingMessages.length = 0;
2657
+ this.pendingInteractionInput = void 0;
2658
+ this.unresolvedCandidate = void 0;
2659
+ this.unresolvedNotified.clear();
2660
+ await this.store.saveState(this.state);
2661
+ await this.poll();
2662
+ return { ok: true, value: session };
2663
+ }
2664
+ async log(message) {
2665
+ await appendFile(this.paths.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${message}
2666
+ `, { mode: 384 });
2667
+ }
2668
+ };
2669
+ function fail(error) {
2670
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
2671
+ }
2672
+ function errorMessage(error) {
2673
+ return error instanceof Error ? error.message : String(error);
2674
+ }
2675
+ function isAlreadyExists(error) {
2676
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
2677
+ }
2678
+ function processIsAlive(pid) {
2679
+ try {
2680
+ process.kill(pid, 0);
2681
+ return true;
2682
+ } catch (error) {
2683
+ return error instanceof Error && "code" in error && error.code === "EPERM";
2684
+ }
2685
+ }
2686
+ function validSessionId(value) {
2687
+ return /^[a-zA-Z0-9_-]{1,40}$/.test(value);
2688
+ }
2689
+ function remember(values, value, limit) {
2690
+ values.add(value);
2691
+ while (values.size > limit) {
2692
+ const oldest = values.values().next().value;
2693
+ if (oldest === void 0) break;
2694
+ values.delete(oldest);
2695
+ }
2696
+ }
2697
+ function escapeFence2(value) {
2698
+ return value.replace(/```/g, "``\\`");
2699
+ }
2700
+ function selectionSnapshot(screen) {
2701
+ return screen.actions.map(({ id, marker, inputValue }) => `${id}:${marker ?? ""}:${inputValue ?? ""}`).join("|");
2702
+ }
2703
+ function formChecked(value) {
2704
+ return value === true || value === 1 || value === "true" || value === "1" || value === "on" || value === "checked";
2705
+ }
2706
+ function safeStructuredInteraction(screen) {
2707
+ return Boolean(
2708
+ screen.interaction?.semantics && screen.interaction.actionConfidence && screen.interaction.actionConfidence >= 0.85 && screen.actions.length > 0 && screen.confidence >= 0.85
2709
+ );
2710
+ }
2711
+ function manualTmuxKey(value) {
2712
+ const keys = {
2713
+ up: "Up",
2714
+ down: "Down",
2715
+ left: "Left",
2716
+ right: "Right",
2717
+ enter: "Enter",
2718
+ esc: "Escape",
2719
+ tab: "Tab",
2720
+ space: "Space",
2721
+ backspace: "BSpace",
2722
+ "ctrl-c": "C-c"
2723
+ };
2724
+ const key = keys[value.toLowerCase()];
2725
+ if (!key) throw new Error(`\u4E0D\u652F\u6301\u7684\u624B\u52A8\u6309\u952E\uFF1A${value}`);
2726
+ return key;
2727
+ }
2728
+ function manualTimestamp() {
2729
+ return new Intl.DateTimeFormat("zh-CN", {
2730
+ timeZone: "Asia/Shanghai",
2731
+ year: "numeric",
2732
+ month: "2-digit",
2733
+ day: "2-digit",
2734
+ hour: "2-digit",
2735
+ minute: "2-digit",
2736
+ second: "2-digit",
2737
+ hour12: false
2738
+ }).format(/* @__PURE__ */ new Date());
2739
+ }
2740
+
2741
+ // src/core/paths.ts
2742
+ import { homedir as homedir2 } from "os";
2743
+ import { join } from "path";
2744
+ function resolveAppPaths(root = process.env.LARK_CODING_ASSISTANT_HOME) {
2745
+ const base = root || join(homedir2(), ".lark-coding-assistant");
2746
+ return {
2747
+ root: base,
2748
+ config: join(base, "config.json"),
2749
+ secrets: join(base, "secrets.json"),
2750
+ state: join(base, "state.json"),
2751
+ logsDir: join(base, "logs"),
2752
+ logFile: join(base, "logs", "assistant.log"),
2753
+ runtimeDir: join(base, "runtime"),
2754
+ socket: join(base, "runtime", "daemon.sock"),
2755
+ pid: join(base, "runtime", "daemon.pid")
2756
+ };
2757
+ }
2758
+
2759
+ // src/daemon-entry.ts
2760
+ import { readFile as readFile3 } from "fs/promises";
2761
+ var paths = resolveAppPaths();
2762
+ var packageInfo = JSON.parse(
2763
+ await readFile3(new URL("../package.json", import.meta.url), "utf8")
2764
+ );
2765
+ var daemon = new AssistantDaemon(
2766
+ new AppStore(paths),
2767
+ paths,
2768
+ void 0,
2769
+ void 0,
2770
+ void 0,
2771
+ void 0,
2772
+ packageInfo.version
2773
+ );
2774
+ try {
2775
+ await daemon.start();
2776
+ for (const signal of ["SIGINT", "SIGTERM"]) {
2777
+ process.on(signal, () => {
2778
+ console.error(`${(/* @__PURE__ */ new Date()).toISOString()} daemon received ${signal}; shutting down`);
2779
+ void daemon.close().finally(() => process.exit(0));
2780
+ });
2781
+ }
2782
+ } catch (error) {
2783
+ console.error(`${(/* @__PURE__ */ new Date()).toISOString()} daemon crashed during startup`, error);
2784
+ process.exitCode = 1;
2785
+ }
2786
+ process.on("uncaughtException", (error) => {
2787
+ console.error(`${(/* @__PURE__ */ new Date()).toISOString()} daemon uncaught exception`, error);
2788
+ process.exit(1);
2789
+ });
2790
+ process.on("unhandledRejection", (error) => {
2791
+ console.error(`${(/* @__PURE__ */ new Date()).toISOString()} daemon unhandled rejection`, error);
2792
+ process.exit(1);
2793
+ });
2794
+ //# sourceMappingURL=daemon-entry.js.map