lark-coding-assistant 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -4
- package/dist/cli.js +178 -61
- package/dist/cli.js.map +1 -1
- package/dist/daemon-entry.js +581 -149
- package/dist/daemon-entry.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon-entry.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createServer } from "net";
|
|
|
5
5
|
// src/platform/process.ts
|
|
6
6
|
import { execFile, spawn } from "child_process";
|
|
7
7
|
function runFile(file, args, options = {}) {
|
|
8
|
-
return new Promise((
|
|
8
|
+
return new Promise((resolve2, reject) => {
|
|
9
9
|
execFile(
|
|
10
10
|
file,
|
|
11
11
|
[...args],
|
|
@@ -20,13 +20,13 @@ function runFile(file, args, options = {}) {
|
|
|
20
20
|
reject(Object.assign(error, { stdout, stderr }));
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
resolve2({ stdout, stderr });
|
|
24
24
|
}
|
|
25
25
|
);
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
function runFileWithInput(file, args, input) {
|
|
29
|
-
return new Promise((
|
|
29
|
+
return new Promise((resolve2, reject) => {
|
|
30
30
|
const child = spawn(file, [...args], { stdio: ["pipe", "pipe", "pipe"] });
|
|
31
31
|
let stdout = "";
|
|
32
32
|
let stderr = "";
|
|
@@ -36,7 +36,7 @@ function runFileWithInput(file, args, input) {
|
|
|
36
36
|
child.stderr.on("data", (chunk) => stderr += chunk);
|
|
37
37
|
child.once("error", reject);
|
|
38
38
|
child.once("exit", (code, signal) => {
|
|
39
|
-
if (code === 0)
|
|
39
|
+
if (code === 0) resolve2({ stdout, stderr });
|
|
40
40
|
else reject(new Error(`${file} exited with ${code ?? signal}: ${stderr.trim()}`));
|
|
41
41
|
});
|
|
42
42
|
child.stdin.end(input, "utf8");
|
|
@@ -100,6 +100,81 @@ function normalizeAgentId(value) {
|
|
|
100
100
|
return AGENT_IDS.includes(value) ? value : void 0;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// src/workspace/path.ts
|
|
104
|
+
import { constants } from "fs";
|
|
105
|
+
import { access, stat } from "fs/promises";
|
|
106
|
+
import { homedir } from "os";
|
|
107
|
+
import { isAbsolute, normalize, resolve } from "path";
|
|
108
|
+
|
|
109
|
+
// src/core/errors.ts
|
|
110
|
+
var AppError = class extends Error {
|
|
111
|
+
code;
|
|
112
|
+
context;
|
|
113
|
+
constructor(code, message, context = {}, options = {}) {
|
|
114
|
+
super(message, options);
|
|
115
|
+
this.name = "AppError";
|
|
116
|
+
this.code = code;
|
|
117
|
+
this.context = context;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
function isAppError(error) {
|
|
121
|
+
return error instanceof AppError;
|
|
122
|
+
}
|
|
123
|
+
function errorMessage(error) {
|
|
124
|
+
return error instanceof Error ? error.message : String(error);
|
|
125
|
+
}
|
|
126
|
+
function serializeAppError(error) {
|
|
127
|
+
if (!isAppError(error)) return { error: errorMessage(error) };
|
|
128
|
+
const errorContext = Object.fromEntries(
|
|
129
|
+
Object.entries(error.context).filter(([, value]) => value !== void 0)
|
|
130
|
+
);
|
|
131
|
+
return {
|
|
132
|
+
error: error.message,
|
|
133
|
+
errorCode: error.code,
|
|
134
|
+
...Object.keys(errorContext).length > 0 ? { errorContext } : {}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function systemErrorCode(error) {
|
|
138
|
+
return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/workspace/path.ts
|
|
142
|
+
function normalizeWorkspacePath(input, home = homedir()) {
|
|
143
|
+
const value = input.trim();
|
|
144
|
+
if (!value) throw invalidCwd(input, "\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
|
|
145
|
+
let expanded = value;
|
|
146
|
+
if (value === "~") expanded = home;
|
|
147
|
+
else if (value.startsWith("~/")) expanded = resolve(home, value.slice(2));
|
|
148
|
+
else if (value.startsWith("~")) throw invalidCwd(input, "\u4E0D\u652F\u6301 ~other-user\uFF0C\u8BF7\u4F7F\u7528 ~ \u6216 ~/\u76EE\u5F55");
|
|
149
|
+
if (!isAbsolute(expanded)) throw invalidCwd(input, "\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u6216\u4F7F\u7528 ~/\u76EE\u5F55");
|
|
150
|
+
return normalize(expanded);
|
|
151
|
+
}
|
|
152
|
+
async function validateWorkspaceDirectory(input, home = homedir()) {
|
|
153
|
+
const cwd = normalizeWorkspacePath(input, home);
|
|
154
|
+
let info;
|
|
155
|
+
try {
|
|
156
|
+
info = await stat(cwd);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
const reason = systemErrorCode(error) === "EACCES" ? `\u65E0\u6743\u8BBF\u95EE\u5DE5\u4F5C\u76EE\u5F55\uFF1A${cwd}` : `\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u7528\uFF1A${cwd}`;
|
|
159
|
+
throw invalidCwd(cwd, reason, error);
|
|
160
|
+
}
|
|
161
|
+
if (!info.isDirectory()) throw invalidCwd(cwd, `\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u6587\u4EF6\u5939\uFF1A${cwd}`);
|
|
162
|
+
try {
|
|
163
|
+
await access(cwd, constants.R_OK | constants.X_OK);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw invalidCwd(cwd, `\u65E0\u6743\u8FDB\u5165\u5DE5\u4F5C\u76EE\u5F55\uFF1A${cwd}`, error);
|
|
166
|
+
}
|
|
167
|
+
return cwd;
|
|
168
|
+
}
|
|
169
|
+
function normalizeWorkspaceRoots(values, home = homedir()) {
|
|
170
|
+
const unique = /* @__PURE__ */ new Set();
|
|
171
|
+
for (const value of values) unique.add(normalizeWorkspacePath(value, home));
|
|
172
|
+
return [...unique];
|
|
173
|
+
}
|
|
174
|
+
function invalidCwd(cwd, reason, cause) {
|
|
175
|
+
return new AppError("INVALID_CWD", reason, { cwd, reason }, cause === void 0 ? {} : { cause });
|
|
176
|
+
}
|
|
177
|
+
|
|
103
178
|
// src/core/store.ts
|
|
104
179
|
var AppStore = class {
|
|
105
180
|
constructor(paths2) {
|
|
@@ -129,11 +204,15 @@ var AppStore = class {
|
|
|
129
204
|
traex: binaries.traex ?? binaries["trae-cli"] ?? "trae-cli",
|
|
130
205
|
claude: binaries.claude ?? binaries["claude-code"] ?? "claude"
|
|
131
206
|
},
|
|
132
|
-
pollIntervalMs: config.pollIntervalMs
|
|
207
|
+
pollIntervalMs: config.pollIntervalMs,
|
|
208
|
+
workspaceRoots: normalizeWorkspaceRoots(config.workspaceRoots ?? [])
|
|
133
209
|
};
|
|
134
210
|
}
|
|
135
211
|
saveConfig(config) {
|
|
136
|
-
return writeJsonAtomic(this.paths.config,
|
|
212
|
+
return writeJsonAtomic(this.paths.config, {
|
|
213
|
+
...config,
|
|
214
|
+
workspaceRoots: normalizeWorkspaceRoots(config.workspaceRoots)
|
|
215
|
+
});
|
|
137
216
|
}
|
|
138
217
|
loadSecrets() {
|
|
139
218
|
return readJson(this.paths.secrets);
|
|
@@ -148,12 +227,33 @@ var AppStore = class {
|
|
|
148
227
|
const agent = normalizeAgentId(session.agent);
|
|
149
228
|
return agent ? [[id, { ...session, agent }]] : [];
|
|
150
229
|
}));
|
|
151
|
-
|
|
230
|
+
const recentWorkspaces = await normalizeRecentWorkspaces(state.recentWorkspaces ?? []);
|
|
231
|
+
return { ...state, sessions, recentWorkspaces };
|
|
152
232
|
}
|
|
153
233
|
saveState(state) {
|
|
154
234
|
return writeJsonAtomic(this.paths.state, state);
|
|
155
235
|
}
|
|
156
236
|
};
|
|
237
|
+
async function normalizeRecentWorkspaces(values) {
|
|
238
|
+
const unique = /* @__PURE__ */ new Set();
|
|
239
|
+
const result2 = [];
|
|
240
|
+
const ordered = [...values].sort((left, right) => right.lastUsedAt - left.lastUsedAt);
|
|
241
|
+
for (const value of ordered) {
|
|
242
|
+
if (result2.length >= 30) break;
|
|
243
|
+
if (!Number.isFinite(value.lastUsedAt)) continue;
|
|
244
|
+
let cwd;
|
|
245
|
+
try {
|
|
246
|
+
cwd = normalizeWorkspacePath(value.cwd);
|
|
247
|
+
await validateWorkspaceDirectory(cwd);
|
|
248
|
+
} catch {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (unique.has(cwd)) continue;
|
|
252
|
+
unique.add(cwd);
|
|
253
|
+
result2.push({ cwd, lastUsedAt: value.lastUsedAt });
|
|
254
|
+
}
|
|
255
|
+
return result2;
|
|
256
|
+
}
|
|
157
257
|
function createBindCode() {
|
|
158
258
|
return randomBytes(16).toString("base64url");
|
|
159
259
|
}
|
|
@@ -170,41 +270,7 @@ function verifyBindCode(code, encoded) {
|
|
|
170
270
|
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
171
271
|
}
|
|
172
272
|
|
|
173
|
-
// src/core/errors.ts
|
|
174
|
-
var AppError = class extends Error {
|
|
175
|
-
code;
|
|
176
|
-
context;
|
|
177
|
-
constructor(code, message, context = {}, options = {}) {
|
|
178
|
-
super(message, options);
|
|
179
|
-
this.name = "AppError";
|
|
180
|
-
this.code = code;
|
|
181
|
-
this.context = context;
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
function isAppError(error) {
|
|
185
|
-
return error instanceof AppError;
|
|
186
|
-
}
|
|
187
|
-
function errorMessage(error) {
|
|
188
|
-
return error instanceof Error ? error.message : String(error);
|
|
189
|
-
}
|
|
190
|
-
function serializeAppError(error) {
|
|
191
|
-
if (!isAppError(error)) return { error: errorMessage(error) };
|
|
192
|
-
const errorContext = Object.fromEntries(
|
|
193
|
-
Object.entries(error.context).filter(([, value]) => value !== void 0)
|
|
194
|
-
);
|
|
195
|
-
return {
|
|
196
|
-
error: error.message,
|
|
197
|
-
errorCode: error.code,
|
|
198
|
-
...Object.keys(errorContext).length > 0 ? { errorContext } : {}
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
function systemErrorCode(error) {
|
|
202
|
-
return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
273
|
// src/session/start-request.ts
|
|
206
|
-
import { stat } from "fs/promises";
|
|
207
|
-
import { isAbsolute } from "path";
|
|
208
274
|
async function validateStartSessionRequest(request) {
|
|
209
275
|
if (!validSessionId(request.sessionId)) {
|
|
210
276
|
throw new AppError(
|
|
@@ -213,21 +279,13 @@ async function validateStartSessionRequest(request) {
|
|
|
213
279
|
{ sessionId: request.sessionId }
|
|
214
280
|
);
|
|
215
281
|
}
|
|
216
|
-
|
|
217
|
-
throw new AppError("INVALID_CWD", "working directory must be absolute", { cwd: request.cwd });
|
|
218
|
-
}
|
|
219
|
-
const info = await stat(request.cwd).catch((error) => {
|
|
220
|
-
throw new AppError("INVALID_CWD", `working directory is unavailable: ${request.cwd}`, { cwd: request.cwd }, { cause: error });
|
|
221
|
-
});
|
|
222
|
-
if (!info.isDirectory()) {
|
|
223
|
-
throw new AppError("INVALID_CWD", `working directory is not a directory: ${request.cwd}`, { cwd: request.cwd });
|
|
224
|
-
}
|
|
282
|
+
const cwd = await validateWorkspaceDirectory(request.cwd);
|
|
225
283
|
if (request.resume?.mode === "session" && !request.resume.sessionId.trim()) {
|
|
226
284
|
throw new AppError("INVALID_RESUME", "resume session id must not be empty", {
|
|
227
285
|
reason: "\u6062\u590D\u5386\u53F2\u4F1A\u8BDD\u65F6\u5FC5\u987B\u63D0\u4F9B session ID"
|
|
228
286
|
});
|
|
229
287
|
}
|
|
230
|
-
return request;
|
|
288
|
+
return { ...request, cwd };
|
|
231
289
|
}
|
|
232
290
|
function validSessionId(value) {
|
|
233
291
|
return /^[a-zA-Z0-9_-]{1,40}$/.test(value);
|
|
@@ -277,6 +335,15 @@ var SessionReconciler = class {
|
|
|
277
335
|
}
|
|
278
336
|
continue;
|
|
279
337
|
}
|
|
338
|
+
if (result2.status === "dead") {
|
|
339
|
+
await this.tmux.killSession(result2.pane.sessionName).catch((error) => this.log(
|
|
340
|
+
`failed to clean dead tmux session ${result2.pane.sessionName}: ${errorMessage2(error)}`
|
|
341
|
+
));
|
|
342
|
+
delete sessions[id];
|
|
343
|
+
this.misses.delete(id);
|
|
344
|
+
changed = true;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
280
347
|
const misses = (this.misses.get(id) ?? 0) + 1;
|
|
281
348
|
this.misses.set(id, misses);
|
|
282
349
|
if (misses < this.missingThreshold) continue;
|
|
@@ -310,9 +377,18 @@ var SessionReconciler = class {
|
|
|
310
377
|
}
|
|
311
378
|
let changed = false;
|
|
312
379
|
const seen = /* @__PURE__ */ new Set();
|
|
380
|
+
const liveSessionNames = new Set(panes.filter((pane) => !pane.dead).map((pane) => pane.sessionName));
|
|
313
381
|
for (const pane of panes) {
|
|
314
|
-
if (seen.has(pane.sessionName)
|
|
382
|
+
if (seen.has(pane.sessionName)) continue;
|
|
315
383
|
seen.add(pane.sessionName);
|
|
384
|
+
if (pane.dead) {
|
|
385
|
+
if (!liveSessionNames.has(pane.sessionName)) {
|
|
386
|
+
await this.tmux.killSession(pane.sessionName).catch((error) => this.log(
|
|
387
|
+
`failed to clean orphaned dead tmux session ${pane.sessionName}: ${errorMessage2(error)}`
|
|
388
|
+
));
|
|
389
|
+
}
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
316
392
|
const registered = Object.values(sessions).find((session) => session.sessionName === pane.sessionName);
|
|
317
393
|
if (registered) {
|
|
318
394
|
if (registered.paneId !== pane.paneId) {
|
|
@@ -397,10 +473,10 @@ function errorMessage2(error) {
|
|
|
397
473
|
|
|
398
474
|
// src/session/native-session.ts
|
|
399
475
|
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
400
|
-
import { homedir } from "os";
|
|
476
|
+
import { homedir as homedir2 } from "os";
|
|
401
477
|
import { join } from "path";
|
|
402
478
|
var UUID = "[0-9a-fA-F-]{32,36}";
|
|
403
|
-
async function resolveNativeAgentSessionId(agent, pid, home =
|
|
479
|
+
async function resolveNativeAgentSessionId(agent, pid, home = homedir2()) {
|
|
404
480
|
if (!Number.isInteger(pid) || pid <= 0) return void 0;
|
|
405
481
|
if (agent === "traex") {
|
|
406
482
|
const peer = await resolveTraexPeer(pid, home);
|
|
@@ -486,7 +562,7 @@ var TmuxController = class {
|
|
|
486
562
|
throw new AppError(
|
|
487
563
|
"SESSION_EXISTS",
|
|
488
564
|
`tmux session already exists: ${options.sessionName}`,
|
|
489
|
-
{ sessionId: displaySessionId(options.sessionName) }
|
|
565
|
+
{ sessionId: displaySessionId(options.sessionName), source: "tmux" }
|
|
490
566
|
);
|
|
491
567
|
}
|
|
492
568
|
const environment = Object.entries(options.env ?? {}).map(([key, value]) => {
|
|
@@ -692,7 +768,7 @@ function parsePane(line) {
|
|
|
692
768
|
function tmuxTargetMissing(error) {
|
|
693
769
|
const message = error instanceof Error ? error.message : String(error);
|
|
694
770
|
const stderr = error && typeof error === "object" && "stderr" in error ? String(error.stderr) : "";
|
|
695
|
-
return /can't find (?:pane|session|window)|no such (?:pane|session|window)|(?:pane|session|window) not found/i.test(`${message}
|
|
771
|
+
return /can't find (?:pane|session|window)|no such (?:pane|session|window)|(?:pane|session|window) not found|no server running/i.test(`${message}
|
|
696
772
|
${stderr}`);
|
|
697
773
|
}
|
|
698
774
|
|
|
@@ -1207,6 +1283,8 @@ var ActionSigner = class {
|
|
|
1207
1283
|
value.interactionKind ?? null,
|
|
1208
1284
|
value.sessionId ?? null,
|
|
1209
1285
|
value.manualMode ?? null,
|
|
1286
|
+
value.snapshotId ?? null,
|
|
1287
|
+
value.page ?? null,
|
|
1210
1288
|
value.agent,
|
|
1211
1289
|
value.action,
|
|
1212
1290
|
value.paneId,
|
|
@@ -1230,7 +1308,12 @@ function withoutSignature(value) {
|
|
|
1230
1308
|
function isSignedAction(value) {
|
|
1231
1309
|
if (!value || typeof value !== "object") return false;
|
|
1232
1310
|
const item = value;
|
|
1233
|
-
return item.v === 1 && (item.kind === "choice" || item.kind === "stop" || item.kind === "session" || item.kind === "session-stop" || item.kind === "session-create" || item.kind === "session-start-error" || item.kind === "startup-conflict" || item.kind === "resume-picker" || 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")) && (item.kind !== "session-stop" && item.kind !== "session-start-error" && item.kind !== "resume-picker" && item.kind !== "startup-conflict" || typeof item.sessionId === "string") && 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";
|
|
1311
|
+
return item.v === 1 && (item.kind === "choice" || item.kind === "stop" || item.kind === "session" || item.kind === "session-stop" || item.kind === "session-create" || item.kind === "session-start-error" || item.kind === "startup-conflict" || item.kind === "resume-picker" || 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")) && (item.snapshotId === void 0 || typeof item.snapshotId === "string") && (item.page === void 0 || typeof item.page === "number" && Number.isInteger(item.page) && item.page >= 0) && (item.kind !== "session-stop" && item.kind !== "session-start-error" && item.kind !== "resume-picker" && item.kind !== "startup-conflict" || typeof item.sessionId === "string") && 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";
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/workspace/session-create.ts
|
|
1315
|
+
function emptySessionCreateDraft() {
|
|
1316
|
+
return { agent: "codex", resumeMode: "new" };
|
|
1234
1317
|
}
|
|
1235
1318
|
|
|
1236
1319
|
// src/lark/cards.ts
|
|
@@ -1614,10 +1697,33 @@ var SESSION_CREATE_SUBMIT_ACTION = "session_create_submit";
|
|
|
1614
1697
|
var SESSION_CREATE_NAME_FIELD = "session_name";
|
|
1615
1698
|
var SESSION_CREATE_AGENT_FIELD = "session_agent";
|
|
1616
1699
|
var SESSION_CREATE_CWD_FIELD = "session_cwd";
|
|
1700
|
+
var SESSION_CREATE_PROJECT_FIELD = "session_project";
|
|
1617
1701
|
var SESSION_CREATE_RESUME_FIELD = "session_resume";
|
|
1618
|
-
|
|
1702
|
+
var SESSION_CREATE_MANUAL_VALUE = "__manual__";
|
|
1703
|
+
function sessionCreateCard(view2 = {
|
|
1704
|
+
mode: "manual",
|
|
1705
|
+
page: 0,
|
|
1706
|
+
pageCount: 1,
|
|
1707
|
+
candidates: [],
|
|
1708
|
+
partial: false,
|
|
1709
|
+
warnings: [],
|
|
1710
|
+
draft: emptySessionCreateDraft()
|
|
1711
|
+
}) {
|
|
1712
|
+
const directoryElements = view2.mode === "projects" ? projectDirectoryFields(view2) : [{
|
|
1713
|
+
tag: "input",
|
|
1714
|
+
name: SESSION_CREATE_CWD_FIELD,
|
|
1715
|
+
default_value: view2.draft.manualCwd,
|
|
1716
|
+
placeholder: { tag: "plain_text", content: "~/workspace/project \u6216 /absolute/path" },
|
|
1717
|
+
label: { tag: "plain_text", content: "\u9879\u76EE\u76EE\u5F55" }
|
|
1718
|
+
}];
|
|
1719
|
+
const noticeLines = [
|
|
1720
|
+
...view2.warnings.map((warning) => `\u26A0\uFE0F ${escapeMarkdown(warning)}`),
|
|
1721
|
+
...view2.partial ? ["\u26A0\uFE0F \u90E8\u5206\u76EE\u5F55\u672A\u52A0\u8F7D\uFF1B\u53EF\u9009\u62E9\u5DF2\u663E\u793A\u9879\u76EE\u6216\u624B\u52A8\u586B\u5199\u8DEF\u5F84\u3002"] : []
|
|
1722
|
+
];
|
|
1723
|
+
const notices = noticeLines.length > 0 ? [{ tag: "markdown", content: noticeLines.join("\n") }] : [];
|
|
1619
1724
|
return cardElements("\u65B0\u5EFA Coding Session", [
|
|
1620
1725
|
{ tag: "markdown", content: "\u5728\u672C\u673A\u53D7\u7BA1 tmux \u4E2D\u542F\u52A8\u4E00\u4E2A\u65B0\u4F1A\u8BDD\uFF1B\u521B\u5EFA\u6210\u529F\u540E\u4F1A\u81EA\u52A8\u8FDE\u63A5\u3002" },
|
|
1726
|
+
...notices,
|
|
1621
1727
|
{
|
|
1622
1728
|
tag: "form",
|
|
1623
1729
|
name: "session_create_form",
|
|
@@ -1628,6 +1734,7 @@ function sessionCreateCard() {
|
|
|
1628
1734
|
tag: "input",
|
|
1629
1735
|
name: SESSION_CREATE_NAME_FIELD,
|
|
1630
1736
|
required: true,
|
|
1737
|
+
default_value: view2.draft.sessionId,
|
|
1631
1738
|
placeholder: { tag: "plain_text", content: "Session \u540D\u79F0\uFF0C\u4F8B\u5982 helix" },
|
|
1632
1739
|
label: { tag: "plain_text", content: "Session \u540D\u79F0" }
|
|
1633
1740
|
},
|
|
@@ -1636,24 +1743,18 @@ function sessionCreateCard() {
|
|
|
1636
1743
|
name: SESSION_CREATE_AGENT_FIELD,
|
|
1637
1744
|
required: true,
|
|
1638
1745
|
placeholder: { tag: "plain_text", content: "\u9009\u62E9 Agent" },
|
|
1639
|
-
initial_option:
|
|
1746
|
+
initial_option: view2.draft.agent,
|
|
1640
1747
|
options: listAgentAdapters().map((adapter) => ({
|
|
1641
1748
|
text: { tag: "plain_text", content: adapter.displayName },
|
|
1642
1749
|
value: adapter.id
|
|
1643
1750
|
}))
|
|
1644
1751
|
},
|
|
1645
|
-
|
|
1646
|
-
tag: "input",
|
|
1647
|
-
name: SESSION_CREATE_CWD_FIELD,
|
|
1648
|
-
required: true,
|
|
1649
|
-
placeholder: { tag: "plain_text", content: "/absolute/path/to/project" },
|
|
1650
|
-
label: { tag: "plain_text", content: "\u5DE5\u4F5C\u76EE\u5F55\uFF08\u5FC5\u987B\u4E3A\u7EDD\u5BF9\u8DEF\u5F84\uFF09" }
|
|
1651
|
-
},
|
|
1752
|
+
...directoryElements,
|
|
1652
1753
|
{
|
|
1653
1754
|
tag: "select_static",
|
|
1654
1755
|
name: SESSION_CREATE_RESUME_FIELD,
|
|
1655
1756
|
placeholder: { tag: "plain_text", content: "\u9009\u62E9\u542F\u52A8\u65B9\u5F0F" },
|
|
1656
|
-
initial_option:
|
|
1757
|
+
initial_option: view2.draft.resumeMode,
|
|
1657
1758
|
options: [
|
|
1658
1759
|
{ text: { tag: "plain_text", content: "\u65B0\u4F1A\u8BDD" }, value: "new" },
|
|
1659
1760
|
{ text: { tag: "plain_text", content: "\u6253\u5F00\u539F\u751F Resume Picker" }, value: "picker" }
|
|
@@ -1672,6 +1773,26 @@ function sessionCreateCard() {
|
|
|
1672
1773
|
}
|
|
1673
1774
|
]);
|
|
1674
1775
|
}
|
|
1776
|
+
function projectDirectoryFields(view2) {
|
|
1777
|
+
const options = [...view2.candidates.map((candidate) => ({
|
|
1778
|
+
text: { tag: "plain_text", content: candidate.label },
|
|
1779
|
+
value: candidate.cwd
|
|
1780
|
+
})), { text: { tag: "plain_text", content: "\u624B\u52A8\u586B\u5199\u5176\u4ED6\u8DEF\u5F84\u2026" }, value: SESSION_CREATE_MANUAL_VALUE }];
|
|
1781
|
+
return [{
|
|
1782
|
+
tag: "select_static",
|
|
1783
|
+
name: SESSION_CREATE_PROJECT_FIELD,
|
|
1784
|
+
placeholder: { tag: "plain_text", content: "\u9009\u62E9\u9879\u76EE\u76EE\u5F55" },
|
|
1785
|
+
initial_option: view2.draft.projectCwd,
|
|
1786
|
+
required: true,
|
|
1787
|
+
options
|
|
1788
|
+
}, {
|
|
1789
|
+
tag: "input",
|
|
1790
|
+
name: SESSION_CREATE_CWD_FIELD,
|
|
1791
|
+
default_value: view2.draft.manualCwd,
|
|
1792
|
+
placeholder: { tag: "plain_text", content: "\u9009\u62E9\u201C\u624B\u52A8\u586B\u5199\u5176\u4ED6\u8DEF\u5F84\u2026\u201D\u65F6\u586B\u5199" },
|
|
1793
|
+
label: { tag: "plain_text", content: "\u5176\u4ED6\u8DEF\u5F84\uFF08\u53EF\u9009\uFF09" }
|
|
1794
|
+
}];
|
|
1795
|
+
}
|
|
1675
1796
|
function sessionCreateResultCard(success, content, session) {
|
|
1676
1797
|
const details = session ? `
|
|
1677
1798
|
|
|
@@ -2038,10 +2159,10 @@ var LarkGateway = class {
|
|
|
2038
2159
|
const result2 = await this.handler.onAction(event, action);
|
|
2039
2160
|
if (result2.type === "error") return { toast: result2 };
|
|
2040
2161
|
if (result2.type === "session-create-form") {
|
|
2041
|
-
this.
|
|
2162
|
+
this.rememberSessionCreateFormActions(event.messageId, event.chatId, result2.view);
|
|
2042
2163
|
return {
|
|
2043
2164
|
toast: { type: "success", content: result2.content },
|
|
2044
|
-
card: { type: "raw", data: sessionCreateCard() }
|
|
2165
|
+
card: { type: "raw", data: sessionCreateCard(result2.view) }
|
|
2045
2166
|
};
|
|
2046
2167
|
}
|
|
2047
2168
|
if (result2.type === "manual") {
|
|
@@ -2112,6 +2233,8 @@ var LarkGateway = class {
|
|
|
2112
2233
|
signer;
|
|
2113
2234
|
formActions = /* @__PURE__ */ new Map();
|
|
2114
2235
|
formActionsInFlight = /* @__PURE__ */ new Set();
|
|
2236
|
+
processingReactions = /* @__PURE__ */ new Map();
|
|
2237
|
+
processingGenerations = /* @__PURE__ */ new Map();
|
|
2115
2238
|
async handleDeferredCardAction(event, action) {
|
|
2116
2239
|
let initialResumePicker;
|
|
2117
2240
|
try {
|
|
@@ -2128,7 +2251,7 @@ var LarkGateway = class {
|
|
|
2128
2251
|
this.formActions.delete(event.messageId);
|
|
2129
2252
|
return;
|
|
2130
2253
|
}
|
|
2131
|
-
await this.
|
|
2254
|
+
await this.sendMessage(event.chatId, { text: `\u5361\u7247\u64CD\u4F5C\u672A\u5B8C\u6210\uFF1A${result2.content}` });
|
|
2132
2255
|
return;
|
|
2133
2256
|
}
|
|
2134
2257
|
if (result2.type === "session-created") {
|
|
@@ -2158,7 +2281,7 @@ var LarkGateway = class {
|
|
|
2158
2281
|
}
|
|
2159
2282
|
if (result2.type === "session-picker") {
|
|
2160
2283
|
if (action.kind === "session-start-error") {
|
|
2161
|
-
await this.
|
|
2284
|
+
await this.sendMessage(event.chatId, { card: sessionPickerCard(
|
|
2162
2285
|
event.chatId,
|
|
2163
2286
|
result2.sessions,
|
|
2164
2287
|
result2.activeSessionId,
|
|
@@ -2177,13 +2300,18 @@ var LarkGateway = class {
|
|
|
2177
2300
|
return;
|
|
2178
2301
|
}
|
|
2179
2302
|
if (result2.type === "session-create-form") {
|
|
2180
|
-
|
|
2181
|
-
|
|
2303
|
+
if (action.kind === "session-create" && action.action !== "open") {
|
|
2304
|
+
await this.updateCardAfterAction(event.messageId, sessionCreateCard(result2.view));
|
|
2305
|
+
this.rememberSessionCreateFormActions(event.messageId, event.chatId, result2.view);
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const sent = await this.sendMessage(event.chatId, { card: sessionCreateCard(result2.view) });
|
|
2309
|
+
this.rememberSessionCreateFormActions(sent.messageId, event.chatId, result2.view);
|
|
2182
2310
|
const sourceCard = action.kind === "session-create" && result2.sessions ? sessionPickerCard(event.chatId, result2.sessions, result2.activeSessionId, this.signer, void 0, false) : sessionCreateOpenedCard();
|
|
2183
2311
|
await this.updateCardAfterAction(event.messageId, sourceCard).catch(async (error) => {
|
|
2184
2312
|
const detail = cardErrorDetail(error);
|
|
2185
2313
|
console.error(`[lca] failed to retire session-create source card: message=${event.messageId} detail=${detail}`);
|
|
2186
|
-
await this.
|
|
2314
|
+
await this.sendMessage(event.chatId, {
|
|
2187
2315
|
text: "\u65B0\u5EFA\u8868\u5355\u5DF2\u53D1\u9001\uFF0C\u4F46\u539F\u5361\u7247\u72B6\u6001\u66F4\u65B0\u5931\u8D25\uFF1B\u8BF7\u4F7F\u7528\u6700\u65B0\u7684\u201C\u65B0\u5EFA Coding Session\u201D\u5361\u7247\u7EE7\u7EED\u64CD\u4F5C\u3002"
|
|
2188
2316
|
}).catch(() => void 0);
|
|
2189
2317
|
});
|
|
@@ -2216,7 +2344,7 @@ var LarkGateway = class {
|
|
|
2216
2344
|
}
|
|
2217
2345
|
if (action.kind === "session-create") this.formActions.delete(event.messageId);
|
|
2218
2346
|
console.error(`[lca] deferred card action failed: kind=${action.kind} action=${action.action} message=${event.messageId} detail=${detail}`);
|
|
2219
|
-
await this.
|
|
2347
|
+
await this.sendMessage(event.chatId, {
|
|
2220
2348
|
text: action.kind === "session-create" ? `\u65B0\u5EFA Session \u8868\u5355\u6253\u5F00\u5931\u8D25\uFF1A${detail}\u3002\u8BF7\u91CD\u65B0\u53D1\u9001 /sessions\uFF0C\u6216\u76F4\u63A5\u4F7F\u7528 /start \u547D\u4EE4\u3002` : `\u5361\u7247\u64CD\u4F5C\u540C\u6B65\u5931\u8D25\uFF1A${detail}`
|
|
2221
2349
|
}).catch(() => void 0);
|
|
2222
2350
|
}
|
|
@@ -2225,7 +2353,7 @@ var LarkGateway = class {
|
|
|
2225
2353
|
const retryDelays = [0, 300, 800, 1600];
|
|
2226
2354
|
let lastError;
|
|
2227
2355
|
for (const delay of retryDelays) {
|
|
2228
|
-
if (delay > 0) await new Promise((
|
|
2356
|
+
if (delay > 0) await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
2229
2357
|
try {
|
|
2230
2358
|
await this.channel.updateCard(messageId, card2);
|
|
2231
2359
|
return;
|
|
@@ -2242,7 +2370,7 @@ var LarkGateway = class {
|
|
|
2242
2370
|
} catch (error) {
|
|
2243
2371
|
const detail = cardErrorDetail(error);
|
|
2244
2372
|
console.error(`[lca] failed to mark expired card: message=${event.messageId} detail=${detail}`);
|
|
2245
|
-
await this.
|
|
2373
|
+
await this.sendMessage(event.chatId, {
|
|
2246
2374
|
text: "\u5361\u7247\u6216\u6309\u94AE\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u5BF9\u5E94\u547D\u4EE4\u83B7\u53D6\u6700\u65B0\u5361\u7247\uFF1BSession \u76F8\u5173\u64CD\u4F5C\u53EF\u53D1\u9001 /sessions\u3002"
|
|
2247
2375
|
}).catch(() => void 0);
|
|
2248
2376
|
}
|
|
@@ -2250,17 +2378,37 @@ var LarkGateway = class {
|
|
|
2250
2378
|
connect() {
|
|
2251
2379
|
return this.channel.connect();
|
|
2252
2380
|
}
|
|
2253
|
-
disconnect() {
|
|
2254
|
-
|
|
2381
|
+
async disconnect() {
|
|
2382
|
+
const chats = /* @__PURE__ */ new Set([...this.processingGenerations.keys(), ...this.processingReactions.keys()]);
|
|
2383
|
+
await Promise.all([...chats].map((chatId) => this.clearProcessing(chatId)));
|
|
2384
|
+
await this.channel.disconnect();
|
|
2385
|
+
}
|
|
2386
|
+
async startProcessing(message) {
|
|
2387
|
+
await this.clearProcessing(message.chatId);
|
|
2388
|
+
const generation = this.processingGenerations.get(message.chatId) ?? 0;
|
|
2389
|
+
try {
|
|
2390
|
+
const reactionId = await this.channel.addReaction(message.messageId, "Typing");
|
|
2391
|
+
if (this.processingGenerations.get(message.chatId) !== generation) {
|
|
2392
|
+
await this.removeProcessingReaction(message.messageId, reactionId);
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
const timer = setTimeout(() => {
|
|
2396
|
+
void this.clearProcessing(message.chatId, generation);
|
|
2397
|
+
}, 10 * 6e4);
|
|
2398
|
+
timer.unref?.();
|
|
2399
|
+
this.processingReactions.set(message.chatId, { messageId: message.messageId, reactionId, timer, generation });
|
|
2400
|
+
} catch (error) {
|
|
2401
|
+
console.error(`[lca] failed to add processing reaction: chat=${message.chatId} message=${message.messageId} detail=${cardErrorDetail(error)}`);
|
|
2402
|
+
}
|
|
2255
2403
|
}
|
|
2256
2404
|
sendText(chatId, text) {
|
|
2257
|
-
return this.
|
|
2405
|
+
return this.sendMessage(chatId, { text });
|
|
2258
2406
|
}
|
|
2259
2407
|
sendMarkdown(chatId, markdown) {
|
|
2260
|
-
return this.
|
|
2408
|
+
return this.sendMessage(chatId, { markdown });
|
|
2261
2409
|
}
|
|
2262
2410
|
async sendChoice(chatId, paneId, screen, agent) {
|
|
2263
|
-
const result2 = await this.
|
|
2411
|
+
const result2 = await this.sendMessage(chatId, { card: choiceCard(chatId, paneId, screen, this.signer, agent) });
|
|
2264
2412
|
this.rememberFormActions(result2.messageId, chatId, paneId, screen, agent);
|
|
2265
2413
|
return result2;
|
|
2266
2414
|
}
|
|
@@ -2273,7 +2421,7 @@ var LarkGateway = class {
|
|
|
2273
2421
|
this.formActions.delete(messageId);
|
|
2274
2422
|
}
|
|
2275
2423
|
async sendManual(chatId, view2) {
|
|
2276
|
-
const result2 = await this.
|
|
2424
|
+
const result2 = await this.sendMessage(chatId, { card: manualControlCard(chatId, view2, this.signer) });
|
|
2277
2425
|
this.rememberManualFormActions(result2.messageId, chatId, view2);
|
|
2278
2426
|
return result2;
|
|
2279
2427
|
}
|
|
@@ -2296,18 +2444,19 @@ var LarkGateway = class {
|
|
|
2296
2444
|
[MANUAL_SUBMIT_ACTION, this.signer.sign({ ...common, action: MANUAL_SUBMIT_ACTION }, 10 * 6e4)]
|
|
2297
2445
|
]));
|
|
2298
2446
|
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2447
|
+
rememberSessionCreateFormActions(messageId, chatId, view2) {
|
|
2448
|
+
const actions = /* @__PURE__ */ new Map();
|
|
2449
|
+
actions.set(SESSION_CREATE_SUBMIT_ACTION, this.signer.sign({
|
|
2450
|
+
kind: "session-create",
|
|
2451
|
+
agent: view2.draft.agent,
|
|
2452
|
+
action: "submit",
|
|
2453
|
+
paneId: "",
|
|
2454
|
+
fingerprint: view2.mode,
|
|
2455
|
+
chatId,
|
|
2456
|
+
snapshotId: view2.snapshotId,
|
|
2457
|
+
page: view2.page
|
|
2458
|
+
}, 10 * 6e4));
|
|
2459
|
+
this.formActions.set(messageId, actions);
|
|
2311
2460
|
}
|
|
2312
2461
|
rememberFormActions(messageId, chatId, paneId, screen, agent) {
|
|
2313
2462
|
if (screen.interaction?.semantics?.activation !== "toggle") {
|
|
@@ -2332,27 +2481,51 @@ var LarkGateway = class {
|
|
|
2332
2481
|
}
|
|
2333
2482
|
}
|
|
2334
2483
|
sendStatus(chatId, status) {
|
|
2335
|
-
return this.
|
|
2484
|
+
return this.sendMessage(chatId, { card: statusCard(status) });
|
|
2336
2485
|
}
|
|
2337
2486
|
sendSessionPicker(chatId, sessions, activeSessionId) {
|
|
2338
|
-
return this.
|
|
2487
|
+
return this.sendMessage(chatId, { card: sessionPickerCard(chatId, sessions, activeSessionId, this.signer) });
|
|
2339
2488
|
}
|
|
2340
2489
|
sendResumePicker(chatId, session, picker) {
|
|
2341
|
-
return this.
|
|
2490
|
+
return this.sendMessage(chatId, { card: resumePickerCard(chatId, session, picker, this.signer) });
|
|
2342
2491
|
}
|
|
2343
2492
|
sendStartupConflict(chatId, request, owner) {
|
|
2344
|
-
return this.
|
|
2493
|
+
return this.sendMessage(chatId, { card: startupConflictCard(chatId, requestSession(request), owner, this.signer) });
|
|
2345
2494
|
}
|
|
2346
|
-
async sendSessionCreate(chatId) {
|
|
2347
|
-
const result2 = await this.
|
|
2348
|
-
this.
|
|
2495
|
+
async sendSessionCreate(chatId, view2) {
|
|
2496
|
+
const result2 = await this.sendMessage(chatId, { card: sessionCreateCard(view2) });
|
|
2497
|
+
this.rememberSessionCreateFormActions(result2.messageId, chatId, view2);
|
|
2349
2498
|
return result2;
|
|
2350
2499
|
}
|
|
2351
2500
|
sendSessionStartupFailure(chatId, failure) {
|
|
2352
|
-
return this.
|
|
2501
|
+
return this.sendMessage(chatId, { card: sessionStartupFailureCard(chatId, failure, this.signer) });
|
|
2353
2502
|
}
|
|
2354
2503
|
sendStopConfirmation(chatId, paneId, fingerprint, agent) {
|
|
2355
|
-
return this.
|
|
2504
|
+
return this.sendMessage(chatId, { card: stopCard(chatId, paneId, fingerprint, this.signer, agent) });
|
|
2505
|
+
}
|
|
2506
|
+
async sendMessage(chatId, input) {
|
|
2507
|
+
await this.clearProcessing(chatId);
|
|
2508
|
+
return this.channel.send(chatId, input);
|
|
2509
|
+
}
|
|
2510
|
+
async clearProcessing(chatId, expectedGeneration) {
|
|
2511
|
+
const currentGeneration = this.processingGenerations.get(chatId) ?? 0;
|
|
2512
|
+
if (expectedGeneration !== void 0 && currentGeneration !== expectedGeneration) return;
|
|
2513
|
+
this.processingGenerations.set(chatId, currentGeneration + 1);
|
|
2514
|
+
const pending = this.processingReactions.get(chatId);
|
|
2515
|
+
if (!pending) return;
|
|
2516
|
+
this.processingReactions.delete(chatId);
|
|
2517
|
+
clearTimeout(pending.timer);
|
|
2518
|
+
await this.removeProcessingReaction(pending.messageId, pending.reactionId);
|
|
2519
|
+
}
|
|
2520
|
+
async removeProcessingReaction(messageId, reactionId) {
|
|
2521
|
+
try {
|
|
2522
|
+
await this.channel.removeReaction(messageId, reactionId);
|
|
2523
|
+
} catch (error) {
|
|
2524
|
+
console.error(`[lca] failed to remove processing reaction by id: message=${messageId} detail=${cardErrorDetail(error)}`);
|
|
2525
|
+
await this.channel.removeReactionByEmoji(messageId, "Typing").catch((fallbackError) => {
|
|
2526
|
+
console.error(`[lca] failed to remove processing reaction by emoji: message=${messageId} detail=${cardErrorDetail(fallbackError)}`);
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2356
2529
|
}
|
|
2357
2530
|
};
|
|
2358
2531
|
function requestSession(request) {
|
|
@@ -2594,6 +2767,152 @@ function redactSecrets(value) {
|
|
|
2594
2767
|
).replace(/([?&](?:api[_-]?key|access[_-]?token|token|secret|signature)=)[^&#\s]+/gi, "$1[REDACTED]");
|
|
2595
2768
|
}
|
|
2596
2769
|
|
|
2770
|
+
// src/workspace/discovery.ts
|
|
2771
|
+
import { lstat, readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
2772
|
+
import { homedir as homedir3 } from "os";
|
|
2773
|
+
import { basename, dirname as dirname2, join as join2, relative } from "path";
|
|
2774
|
+
async function discoverWorkspaces(options) {
|
|
2775
|
+
const maxDepth = options.maxDepth ?? 1;
|
|
2776
|
+
const maxDirectories = options.maxDirectories ?? 500;
|
|
2777
|
+
const timeBudgetMs = options.timeBudgetMs ?? 2e3;
|
|
2778
|
+
const now = options.now ?? Date.now;
|
|
2779
|
+
const home = options.home ?? homedir3();
|
|
2780
|
+
const startedAt = now();
|
|
2781
|
+
const warnings = [];
|
|
2782
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
2783
|
+
let visitedDirectories = 0;
|
|
2784
|
+
let partial = false;
|
|
2785
|
+
const budgetAvailable = () => {
|
|
2786
|
+
const available = visitedDirectories < maxDirectories && now() - startedAt <= timeBudgetMs;
|
|
2787
|
+
if (!available) partial = true;
|
|
2788
|
+
return available;
|
|
2789
|
+
};
|
|
2790
|
+
const add = async (input, source) => {
|
|
2791
|
+
if (!budgetAvailable()) return;
|
|
2792
|
+
let cwd;
|
|
2793
|
+
try {
|
|
2794
|
+
cwd = normalizeWorkspacePath(input, home);
|
|
2795
|
+
} catch {
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
if (byPath.has(cwd)) return;
|
|
2799
|
+
visitedDirectories += 1;
|
|
2800
|
+
try {
|
|
2801
|
+
const info = await lstat(cwd);
|
|
2802
|
+
if (!info.isDirectory() || info.isSymbolicLink()) return;
|
|
2803
|
+
byPath.set(cwd, { cwd, label: workspaceLabel(cwd, home), source, git: await hasGitEntry(cwd) });
|
|
2804
|
+
} catch {
|
|
2805
|
+
if (source === "configured") warnings.push(`\u65E0\u6CD5\u8BFB\u53D6 workspace\uFF1A${cwd}`);
|
|
2806
|
+
}
|
|
2807
|
+
};
|
|
2808
|
+
for (const session of options.activeSessions) await add(session.cwd, "active");
|
|
2809
|
+
for (const recent of [...options.recentWorkspaces].sort((a, b) => b.lastUsedAt - a.lastUsedAt)) {
|
|
2810
|
+
await add(recent.cwd, "recent");
|
|
2811
|
+
}
|
|
2812
|
+
for (const rootInput of options.workspaceRoots) {
|
|
2813
|
+
if (!budgetAvailable()) break;
|
|
2814
|
+
let root;
|
|
2815
|
+
try {
|
|
2816
|
+
root = normalizeWorkspacePath(rootInput, home);
|
|
2817
|
+
const info = await lstat(root);
|
|
2818
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("not a directory");
|
|
2819
|
+
} catch {
|
|
2820
|
+
warnings.push(`\u65E0\u6CD5\u8BFB\u53D6 workspace\uFF1A${rootInput}`);
|
|
2821
|
+
continue;
|
|
2822
|
+
}
|
|
2823
|
+
await add(root, "configured");
|
|
2824
|
+
let frontier = [root];
|
|
2825
|
+
for (let depth = 1; depth <= maxDepth && frontier.length > 0 && budgetAvailable(); depth += 1) {
|
|
2826
|
+
const next = [];
|
|
2827
|
+
for (const parent of frontier) {
|
|
2828
|
+
if (!budgetAvailable()) break;
|
|
2829
|
+
let entries;
|
|
2830
|
+
try {
|
|
2831
|
+
entries = await readdir2(parent, { withFileTypes: true });
|
|
2832
|
+
} catch {
|
|
2833
|
+
warnings.push(`\u65E0\u6CD5\u8BFB\u53D6\u76EE\u5F55\uFF1A${parent}`);
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
for (const entry of entries) {
|
|
2837
|
+
if (!budgetAvailable()) break;
|
|
2838
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || entry.name.startsWith(".")) continue;
|
|
2839
|
+
const child = join2(parent, entry.name);
|
|
2840
|
+
await add(child, "configured");
|
|
2841
|
+
if (depth < maxDepth) next.push(child);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
frontier = next;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
const priority = { active: 0, recent: 1, configured: 2 };
|
|
2848
|
+
const candidates = [...byPath.values()].sort(
|
|
2849
|
+
(left, right) => priority[left.source] - priority[right.source] || Number(right.git) - Number(left.git) || left.label.localeCompare(right.label, "zh-CN")
|
|
2850
|
+
);
|
|
2851
|
+
return { candidates, partial, warnings: [...new Set(warnings)], visitedDirectories };
|
|
2852
|
+
}
|
|
2853
|
+
function workspaceLabel(cwd, home = homedir3()) {
|
|
2854
|
+
const parent = dirname2(cwd);
|
|
2855
|
+
const rel = relative(home, parent);
|
|
2856
|
+
const displayParent = rel === "" ? "~" : !rel.startsWith("..") ? `~/${rel}` : parent;
|
|
2857
|
+
return `${basename(cwd) || cwd} \xB7 ${displayParent}`;
|
|
2858
|
+
}
|
|
2859
|
+
async function hasGitEntry(cwd) {
|
|
2860
|
+
return stat2(join2(cwd, ".git")).then(() => true).catch(() => false);
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
// src/workspace/recent.ts
|
|
2864
|
+
function rememberRecentWorkspace(existing, cwd, now = Date.now(), limit = 30) {
|
|
2865
|
+
const normalized = normalizeWorkspacePath(cwd);
|
|
2866
|
+
return [
|
|
2867
|
+
{ cwd: normalized, lastUsedAt: now },
|
|
2868
|
+
...(existing ?? []).filter((item) => normalizeWorkspacePath(item.cwd) !== normalized)
|
|
2869
|
+
].slice(0, limit);
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
// src/workspace/snapshot.ts
|
|
2873
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2874
|
+
var WorkspaceSnapshotStore = class {
|
|
2875
|
+
constructor(ttlMs = 10 * 6e4, capacity = 64, now = Date.now, createId = () => randomBytes4(12).toString("base64url")) {
|
|
2876
|
+
this.ttlMs = ttlMs;
|
|
2877
|
+
this.capacity = capacity;
|
|
2878
|
+
this.now = now;
|
|
2879
|
+
this.createId = createId;
|
|
2880
|
+
}
|
|
2881
|
+
ttlMs;
|
|
2882
|
+
capacity;
|
|
2883
|
+
now;
|
|
2884
|
+
createId;
|
|
2885
|
+
values = /* @__PURE__ */ new Map();
|
|
2886
|
+
create(input) {
|
|
2887
|
+
const now = this.now();
|
|
2888
|
+
this.prune(now);
|
|
2889
|
+
const snapshot = {
|
|
2890
|
+
...input,
|
|
2891
|
+
id: this.createId(),
|
|
2892
|
+
createdAt: now,
|
|
2893
|
+
expiresAt: now + this.ttlMs
|
|
2894
|
+
};
|
|
2895
|
+
this.values.set(snapshot.id, snapshot);
|
|
2896
|
+
while (this.values.size > this.capacity) {
|
|
2897
|
+
const oldest = this.values.keys().next().value;
|
|
2898
|
+
if (!oldest) break;
|
|
2899
|
+
this.values.delete(oldest);
|
|
2900
|
+
}
|
|
2901
|
+
return snapshot;
|
|
2902
|
+
}
|
|
2903
|
+
get(id, chatId, ownerOpenId) {
|
|
2904
|
+
const now = this.now();
|
|
2905
|
+
this.prune(now);
|
|
2906
|
+
const value = this.values.get(id);
|
|
2907
|
+
return value?.chatId === chatId && value.ownerOpenId === ownerOpenId ? value : void 0;
|
|
2908
|
+
}
|
|
2909
|
+
prune(now) {
|
|
2910
|
+
for (const [id, value] of this.values) {
|
|
2911
|
+
if (value.expiresAt <= now) this.values.delete(id);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
};
|
|
2915
|
+
|
|
2597
2916
|
// src/daemon/server.ts
|
|
2598
2917
|
var AssistantDaemon = class {
|
|
2599
2918
|
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") {
|
|
@@ -2635,6 +2954,7 @@ var AssistantDaemon = class {
|
|
|
2635
2954
|
pendingAgentSessionClaims = /* @__PURE__ */ new Map();
|
|
2636
2955
|
pendingResumePickers = /* @__PURE__ */ new Map();
|
|
2637
2956
|
pendingStartupConflicts = /* @__PURE__ */ new Map();
|
|
2957
|
+
workspaceSnapshots = new WorkspaceSnapshotStore();
|
|
2638
2958
|
pendingInteractionInput;
|
|
2639
2959
|
attachAttempts = /* @__PURE__ */ new Map();
|
|
2640
2960
|
server = createServer((socket) => this.handleSocket(socket));
|
|
@@ -2661,9 +2981,9 @@ var AssistantDaemon = class {
|
|
|
2661
2981
|
try {
|
|
2662
2982
|
await this.reconcileSessions(true);
|
|
2663
2983
|
await rm(this.paths.socket, { force: true });
|
|
2664
|
-
await new Promise((
|
|
2984
|
+
await new Promise((resolve2, reject) => {
|
|
2665
2985
|
this.server.once("error", reject);
|
|
2666
|
-
this.server.listen(this.paths.socket, () =>
|
|
2986
|
+
this.server.listen(this.paths.socket, () => resolve2());
|
|
2667
2987
|
});
|
|
2668
2988
|
await chmod3(this.paths.socket, 384);
|
|
2669
2989
|
this.gateway = this.gatewayFactory(config, secrets, {
|
|
@@ -2686,7 +3006,7 @@ var AssistantDaemon = class {
|
|
|
2686
3006
|
if (this.timer) clearTimeout(this.timer);
|
|
2687
3007
|
await this.pollInFlight?.catch(() => void 0);
|
|
2688
3008
|
await this.gateway?.disconnect().catch(() => void 0);
|
|
2689
|
-
await new Promise((
|
|
3009
|
+
await new Promise((resolve2) => this.server.close(() => resolve2()));
|
|
2690
3010
|
await this.releaseRuntimeFiles();
|
|
2691
3011
|
}
|
|
2692
3012
|
async acquireRuntimeFiles() {
|
|
@@ -2773,7 +3093,8 @@ var AssistantDaemon = class {
|
|
|
2773
3093
|
}
|
|
2774
3094
|
async startSession(sessionId, cwd, agentId, resume) {
|
|
2775
3095
|
try {
|
|
2776
|
-
await validateStartSessionRequest({ sessionId, cwd, agent: agentId, resume });
|
|
3096
|
+
const validated = await validateStartSessionRequest({ sessionId, cwd, agent: agentId, resume });
|
|
3097
|
+
cwd = validated.cwd;
|
|
2777
3098
|
} catch (error) {
|
|
2778
3099
|
return fail(error);
|
|
2779
3100
|
}
|
|
@@ -2901,6 +3222,7 @@ var AssistantDaemon = class {
|
|
|
2901
3222
|
await this.tmux.preserveOnExit(session.sessionName, false).catch((error) => this.log(
|
|
2902
3223
|
`failed to disable startup preservation for ${sessionId}: ${errorMessage3(error)}`
|
|
2903
3224
|
));
|
|
3225
|
+
await this.rememberSessionWorkspace(session.cwd);
|
|
2904
3226
|
}
|
|
2905
3227
|
await this.log(
|
|
2906
3228
|
`session created: session=${session.id} agent=${session.agent} pane=${session.paneId} active=${this.state.activeSessionId === session.id}`
|
|
@@ -2985,7 +3307,7 @@ var AssistantDaemon = class {
|
|
|
2985
3307
|
if (message.senderId !== this.state.ownerOpenId || message.chatId !== this.state.boundChatId) return;
|
|
2986
3308
|
if (text === "/start") {
|
|
2987
3309
|
try {
|
|
2988
|
-
await this.gateway?.sendSessionCreate(message.chatId);
|
|
3310
|
+
await this.gateway?.sendSessionCreate(message.chatId, await this.createSessionWorkspaceView(message.chatId));
|
|
2989
3311
|
} catch (error) {
|
|
2990
3312
|
await this.log(`session create card failed: ${errorMessage3(error)}`);
|
|
2991
3313
|
await this.gateway?.sendText(message.chatId, "\u65B0\u5EFA Session \u8868\u5355\u53D1\u9001\u5931\u8D25\u3002\u8BF7\u4F7F\u7528 /start <name> --agent <agent> --cwd <\u7EDD\u5BF9\u8DEF\u5F84>\u3002");
|
|
@@ -3178,6 +3500,7 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
3178
3500
|
await this.gateway?.sendText(message.chatId, `\u5DF2\u5411 ${getAgentAdapter(session.agent).displayName} \u63D0\u4EA4\u8865\u5145\u5185\u5BB9\u3002`);
|
|
3179
3501
|
return;
|
|
3180
3502
|
}
|
|
3503
|
+
await this.gateway?.startProcessing(message);
|
|
3181
3504
|
await this.poll();
|
|
3182
3505
|
if (this.shouldQueueMessage()) {
|
|
3183
3506
|
if (this.pendingMessages.length >= 100) {
|
|
@@ -3249,7 +3572,11 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
3249
3572
|
}
|
|
3250
3573
|
if (action.kind === "session-stop") return this.handleSessionStopAction(action);
|
|
3251
3574
|
if (action.kind === "session-start-error") {
|
|
3252
|
-
if (action.action === "create") return {
|
|
3575
|
+
if (action.action === "create") return {
|
|
3576
|
+
type: "session-create-form",
|
|
3577
|
+
content: "\u8BF7\u586B\u5199\u542F\u52A8\u4FE1\u606F\u3002",
|
|
3578
|
+
view: await this.createSessionWorkspaceView(event.chatId)
|
|
3579
|
+
};
|
|
3253
3580
|
if (action.action === "sessions") {
|
|
3254
3581
|
await this.reconcileSessions(true);
|
|
3255
3582
|
return this.sessionPickerActionResult("\u5DF2\u53D1\u9001\u6700\u65B0 Sessions\u3002");
|
|
@@ -3263,15 +3590,53 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
3263
3590
|
return {
|
|
3264
3591
|
type: "session-create-form",
|
|
3265
3592
|
content: "\u8BF7\u586B\u5199\u542F\u52A8\u4FE1\u606F\u3002",
|
|
3593
|
+
view: await this.createSessionWorkspaceView(event.chatId),
|
|
3266
3594
|
sessions: Object.values(this.state.sessions ?? {}),
|
|
3267
3595
|
activeSessionId: this.state.activeSessionId
|
|
3268
3596
|
};
|
|
3269
3597
|
}
|
|
3270
|
-
|
|
3598
|
+
const snapshot = action.snapshotId ? this.workspaceSnapshots.get(action.snapshotId, event.chatId, event.operator.openId) : void 0;
|
|
3599
|
+
if (!snapshot && action.snapshotId) {
|
|
3600
|
+
return {
|
|
3601
|
+
type: "session-create-form",
|
|
3602
|
+
content: "\u9879\u76EE\u76EE\u5F55\u5217\u8868\u5DF2\u8FC7\u671F\uFF0C\u5DF2\u91CD\u65B0\u52A0\u8F7D\u3002",
|
|
3603
|
+
view: await this.createSessionWorkspaceView(event.chatId)
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
if (action.action !== "submit") return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u65B0\u5EFA Session \u64CD\u4F5C\u3002" };
|
|
3607
|
+
if (!event.action.formValue) {
|
|
3271
3608
|
return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u65B0\u5EFA Session \u8868\u5355\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 /sessions\u3002" };
|
|
3272
3609
|
}
|
|
3273
|
-
const
|
|
3274
|
-
if (
|
|
3610
|
+
const submittedResumeMode = formString(event.action.formValue[SESSION_CREATE_RESUME_FIELD]) || "new";
|
|
3611
|
+
if (submittedResumeMode === "last") {
|
|
3612
|
+
return { type: "error", content: "\u98DE\u4E66\u5DF2\u4E0D\u518D\u652F\u6301\u201C\u6062\u590D\u4E0A\u6B21\u4F1A\u8BDD\u201D\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u8868\u5355\u5E76\u4F7F\u7528 Resume Picker\u3002" };
|
|
3613
|
+
}
|
|
3614
|
+
if (submittedResumeMode !== "new" && submittedResumeMode !== "picker") {
|
|
3615
|
+
return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u542F\u52A8\u65B9\u5F0F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u65B0\u5EFA Session \u8868\u5355\u3002" };
|
|
3616
|
+
}
|
|
3617
|
+
const submittedAgent = formString(event.action.formValue[SESSION_CREATE_AGENT_FIELD]);
|
|
3618
|
+
if (!normalizeAgentId(submittedAgent)) {
|
|
3619
|
+
return { type: "error", content: "\u8BF7\u9009\u62E9\u6709\u6548\u7684 Agent\uFF1Acodex\u3001traex \u6216 claude\u3002" };
|
|
3620
|
+
}
|
|
3621
|
+
const draft = sessionCreateDraftFromForm(event.action.formValue);
|
|
3622
|
+
const submittedProject = formString(event.action.formValue[SESSION_CREATE_PROJECT_FIELD]);
|
|
3623
|
+
const manualForm = action.fingerprint === "manual";
|
|
3624
|
+
const selectedProject = submittedProject !== SESSION_CREATE_MANUAL_VALUE ? submittedProject : "";
|
|
3625
|
+
if (selectedProject && !manualForm && (!snapshot || !snapshot.candidates.some((candidate) => candidate.cwd === selectedProject))) {
|
|
3626
|
+
return {
|
|
3627
|
+
type: "session-create-form",
|
|
3628
|
+
content: "\u9879\u76EE\u76EE\u5F55\u5217\u8868\u5DF2\u53D8\u5316\uFF0C\u5DF2\u91CD\u65B0\u52A0\u8F7D\u3002",
|
|
3629
|
+
view: await this.createSessionWorkspaceView(event.chatId)
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
const request = startRequestFromDraft(draft);
|
|
3633
|
+
if (!request.ok) {
|
|
3634
|
+
return {
|
|
3635
|
+
type: "session-create-form",
|
|
3636
|
+
content: request.error,
|
|
3637
|
+
view: this.sessionCreateRetryView(snapshot, draft)
|
|
3638
|
+
};
|
|
3639
|
+
}
|
|
3275
3640
|
const result3 = await this.startRemoteSession(request.value);
|
|
3276
3641
|
if (!result3.ok) return larkStartupError(result3.error, request.value);
|
|
3277
3642
|
if (result3.state === "picker") {
|
|
@@ -3476,6 +3841,7 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
3476
3841
|
await this.tmux.preserveOnExit(session.sessionName, false).catch(() => void 0);
|
|
3477
3842
|
const selected = await this.useSession(sessionId);
|
|
3478
3843
|
if (!selected.ok) return { type: "error", content: remoteError(selected) };
|
|
3844
|
+
await this.rememberSessionWorkspace(session.cwd);
|
|
3479
3845
|
return { type: "session-created", content: remoteStartSuccess(session), session };
|
|
3480
3846
|
}
|
|
3481
3847
|
async readResumePicker(session) {
|
|
@@ -3496,7 +3862,7 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
3496
3862
|
while (Date.now() < deadline) {
|
|
3497
3863
|
latest = await this.readResumePicker(session);
|
|
3498
3864
|
if (latest && (!previousFingerprint || latest.fingerprint !== previousFingerprint)) return latest;
|
|
3499
|
-
await new Promise((
|
|
3865
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
3500
3866
|
}
|
|
3501
3867
|
return latest;
|
|
3502
3868
|
}
|
|
@@ -3509,7 +3875,7 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
3509
3875
|
let previousFingerprint;
|
|
3510
3876
|
const deadline = Date.now() + 900;
|
|
3511
3877
|
do {
|
|
3512
|
-
await new Promise((
|
|
3878
|
+
await new Promise((resolve2) => setTimeout(resolve2, 120));
|
|
3513
3879
|
await this.poll();
|
|
3514
3880
|
const fingerprint = this.screen?.fingerprint;
|
|
3515
3881
|
if (fingerprint && fingerprint === previousFingerprint) return;
|
|
@@ -3537,7 +3903,7 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
3537
3903
|
}
|
|
3538
3904
|
try {
|
|
3539
3905
|
await execute(session);
|
|
3540
|
-
await new Promise((
|
|
3906
|
+
await new Promise((resolve2) => setTimeout(resolve2, 120));
|
|
3541
3907
|
await this.poll();
|
|
3542
3908
|
const output = this.screen ? tailScreen(this.screen.normalized, 60) : "\u65E0\u6CD5\u8BFB\u53D6\u6700\u65B0\u7EC8\u7AEF\u753B\u9762\u3002";
|
|
3543
3909
|
await this.gateway?.sendMarkdown(chatId, `**\u624B\u52A8\u64CD\u4F5C\uFF1A${operation}**
|
|
@@ -3588,7 +3954,7 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
3588
3954
|
};
|
|
3589
3955
|
}
|
|
3590
3956
|
if (target?.role === "custom-input" || target?.role === "chat") {
|
|
3591
|
-
await new Promise((
|
|
3957
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
3592
3958
|
await this.poll();
|
|
3593
3959
|
if (!before?.interactionId) return { type: "error", content: "\u65E0\u6CD5\u786E\u8BA4\u5F53\u524D\u4EA4\u4E92\uFF0C\u8BF7\u7528 /tail \u68C0\u67E5\u3002" };
|
|
3594
3960
|
this.pendingInteractionInput = {
|
|
@@ -3721,7 +4087,7 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
3721
4087
|
if (focusedIndex === targetIndex) return { ok: true };
|
|
3722
4088
|
const direction = targetIndex > focusedIndex ? "Down" : "Up";
|
|
3723
4089
|
await this.tmux.sendKey(paneId, direction);
|
|
3724
|
-
await new Promise((
|
|
4090
|
+
await new Promise((resolve2) => setTimeout(resolve2, 40));
|
|
3725
4091
|
await this.poll();
|
|
3726
4092
|
const nextFocused = this.screen?.actions.findIndex(({ focused }) => focused) ?? -1;
|
|
3727
4093
|
if (nextFocused === focusedIndex) return { ok: false, error: "terminal focus did not move as expected" };
|
|
@@ -3731,7 +4097,7 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
3731
4097
|
async waitForInteractionChange(interactionId) {
|
|
3732
4098
|
const deadline = Date.now() + 1500;
|
|
3733
4099
|
while (Date.now() < deadline) {
|
|
3734
|
-
await new Promise((
|
|
4100
|
+
await new Promise((resolve2) => setTimeout(resolve2, 75));
|
|
3735
4101
|
await this.poll();
|
|
3736
4102
|
const current = this.screen?.interaction;
|
|
3737
4103
|
if (!current || current.interactionId !== interactionId) return;
|
|
@@ -3740,7 +4106,7 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
3740
4106
|
async waitForControlMarker(interactionId, controlId, marker) {
|
|
3741
4107
|
const deadline = Date.now() + 1500;
|
|
3742
4108
|
while (Date.now() < deadline) {
|
|
3743
|
-
await new Promise((
|
|
4109
|
+
await new Promise((resolve2) => setTimeout(resolve2, 75));
|
|
3744
4110
|
await this.poll();
|
|
3745
4111
|
const current = this.screen?.interaction;
|
|
3746
4112
|
const control = this.screen?.actions.find(({ id }) => id === controlId);
|
|
@@ -3751,7 +4117,7 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
3751
4117
|
const expected = input.trim();
|
|
3752
4118
|
const deadline = Date.now() + 1500;
|
|
3753
4119
|
while (Date.now() < deadline) {
|
|
3754
|
-
await new Promise((
|
|
4120
|
+
await new Promise((resolve2) => setTimeout(resolve2, 75));
|
|
3755
4121
|
await this.poll();
|
|
3756
4122
|
const control = this.screen?.actions.find(({ id }) => id === controlId);
|
|
3757
4123
|
if (this.screen?.interaction?.interactionId === interactionId && control?.inputValue && (control.inputValue === expected || expected.startsWith(control.inputValue))) return;
|
|
@@ -3796,7 +4162,7 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
3796
4162
|
if (!navigation.ok) return navigation;
|
|
3797
4163
|
if (desired.editor) {
|
|
3798
4164
|
if (desired.editor.openKey) await this.tmux.sendKey(paneId, desired.editor.openKey);
|
|
3799
|
-
await new Promise((
|
|
4165
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
3800
4166
|
await this.tmux.sendKey(paneId, "C-u");
|
|
3801
4167
|
await this.tmux.sendKey(paneId, "C-k");
|
|
3802
4168
|
await this.tmux.sendText(paneId, desired.input, false);
|
|
@@ -4078,7 +4444,7 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
4078
4444
|
source: "startup-discovery"
|
|
4079
4445
|
});
|
|
4080
4446
|
}
|
|
4081
|
-
await new Promise((
|
|
4447
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
4082
4448
|
}
|
|
4083
4449
|
const session = this.state.sessions?.[sessionId];
|
|
4084
4450
|
if (!session) return fail(new AppError("SESSION_NOT_FOUND", `session disappeared during startup: ${sessionId}`, { sessionId }));
|
|
@@ -4109,7 +4475,7 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
4109
4475
|
while (Date.now() < deadline) {
|
|
4110
4476
|
const pane = await this.tmux.inspect(session.paneId);
|
|
4111
4477
|
if (!pane || pane.dead) return fail(await this.startupExitedError(session, pane));
|
|
4112
|
-
await new Promise((
|
|
4478
|
+
await new Promise((resolve2) => setTimeout(resolve2, 80));
|
|
4113
4479
|
}
|
|
4114
4480
|
return { ok: true };
|
|
4115
4481
|
}
|
|
@@ -4277,6 +4643,67 @@ ${output}`
|
|
|
4277
4643
|
}
|
|
4278
4644
|
return { ok: true, state: "ready", session };
|
|
4279
4645
|
}
|
|
4646
|
+
async createSessionWorkspaceView(chatId, draft = emptySessionCreateDraft()) {
|
|
4647
|
+
await this.reconcileSessions(true);
|
|
4648
|
+
const latestConfig = await this.store.loadConfig();
|
|
4649
|
+
const discovered = await discoverWorkspaces({
|
|
4650
|
+
workspaceRoots: latestConfig?.workspaceRoots ?? this.config.workspaceRoots,
|
|
4651
|
+
activeSessions: Object.values(this.state.sessions ?? {}),
|
|
4652
|
+
recentWorkspaces: this.state.recentWorkspaces ?? []
|
|
4653
|
+
});
|
|
4654
|
+
for (const warning of discovered.warnings) await this.log(`workspace discovery warning: ${warning}`);
|
|
4655
|
+
const snapshot = this.workspaceSnapshots.create({
|
|
4656
|
+
chatId,
|
|
4657
|
+
ownerOpenId: this.state.ownerOpenId ?? "",
|
|
4658
|
+
candidates: discovered.candidates,
|
|
4659
|
+
warnings: discovered.warnings,
|
|
4660
|
+
partial: discovered.partial
|
|
4661
|
+
});
|
|
4662
|
+
return discovered.candidates.length > 0 ? this.sessionWorkspaceView(snapshot, draft) : this.manualSessionCreateView(snapshot, draft);
|
|
4663
|
+
}
|
|
4664
|
+
sessionWorkspaceView(snapshot, draft) {
|
|
4665
|
+
const visibleCandidates = snapshot.candidates.slice(0, 99);
|
|
4666
|
+
return {
|
|
4667
|
+
mode: "projects",
|
|
4668
|
+
snapshotId: snapshot.id,
|
|
4669
|
+
page: 0,
|
|
4670
|
+
pageCount: 1,
|
|
4671
|
+
hasProjectCandidates: snapshot.candidates.length > 0,
|
|
4672
|
+
candidates: visibleCandidates,
|
|
4673
|
+
warnings: snapshot.warnings,
|
|
4674
|
+
partial: snapshot.partial || visibleCandidates.length < snapshot.candidates.length,
|
|
4675
|
+
draft
|
|
4676
|
+
};
|
|
4677
|
+
}
|
|
4678
|
+
sessionCreateRetryView(snapshot, draft) {
|
|
4679
|
+
return snapshot && snapshot.candidates.length > 0 ? this.sessionWorkspaceView(snapshot, draft) : this.manualSessionCreateView(snapshot, draft);
|
|
4680
|
+
}
|
|
4681
|
+
manualSessionCreateView(snapshot, draft, requestedPage = 0) {
|
|
4682
|
+
const pageCount = Math.max(1, Math.ceil((snapshot?.candidates.length ?? 0) / 20));
|
|
4683
|
+
const page = Math.max(0, Math.min(pageCount - 1, requestedPage));
|
|
4684
|
+
const noCandidates = snapshot && snapshot.candidates.length === 0 ? ["\u5C1A\u672A\u53D1\u73B0\u53EF\u9009\u9879\u76EE\uFF1B\u8BF7\u624B\u52A8\u586B\u5199\u8DEF\u5F84\uFF0C\u6216\u5728\u672C\u673A\u8FD0\u884C lca workspace add ~/workspace\u3002"] : [];
|
|
4685
|
+
return {
|
|
4686
|
+
mode: "manual",
|
|
4687
|
+
snapshotId: snapshot?.id,
|
|
4688
|
+
page,
|
|
4689
|
+
hasProjectCandidates: (snapshot?.candidates.length ?? 0) > 0,
|
|
4690
|
+
pageCount,
|
|
4691
|
+
candidates: [],
|
|
4692
|
+
warnings: [...snapshot?.warnings ?? [], ...noCandidates],
|
|
4693
|
+
partial: snapshot?.partial ?? false,
|
|
4694
|
+
draft: { ...draft, manualCwd: draft.manualCwd ?? (draft.cwd !== SESSION_CREATE_MANUAL_VALUE ? draft.cwd : void 0) }
|
|
4695
|
+
};
|
|
4696
|
+
}
|
|
4697
|
+
async rememberSessionWorkspace(cwd) {
|
|
4698
|
+
this.state = {
|
|
4699
|
+
...this.state,
|
|
4700
|
+
recentWorkspaces: rememberRecentWorkspace(this.state.recentWorkspaces, cwd),
|
|
4701
|
+
updatedAt: Date.now()
|
|
4702
|
+
};
|
|
4703
|
+
await this.store.saveState(this.state).catch((error) => this.log(
|
|
4704
|
+
`failed to persist recent workspace: cwd=${cwd} error=${errorMessage3(error)}`
|
|
4705
|
+
));
|
|
4706
|
+
}
|
|
4280
4707
|
async log(message) {
|
|
4281
4708
|
await appendFile(this.paths.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${message}
|
|
4282
4709
|
`, { mode: 384 });
|
|
@@ -4314,24 +4741,29 @@ function remember(values, value, limit) {
|
|
|
4314
4741
|
values.delete(oldest);
|
|
4315
4742
|
}
|
|
4316
4743
|
}
|
|
4317
|
-
function
|
|
4744
|
+
function sessionCreateDraftFromForm(values) {
|
|
4318
4745
|
const sessionId = formString(values[SESSION_CREATE_NAME_FIELD]);
|
|
4319
4746
|
const agentValue = formString(values[SESSION_CREATE_AGENT_FIELD]);
|
|
4320
|
-
const
|
|
4747
|
+
const projectCwd = formString(values[SESSION_CREATE_PROJECT_FIELD]);
|
|
4748
|
+
const manualCwd = formString(values[SESSION_CREATE_CWD_FIELD]);
|
|
4321
4749
|
const resumeMode = formString(values[SESSION_CREATE_RESUME_FIELD]) || "new";
|
|
4750
|
+
const cwd = projectCwd === SESSION_CREATE_MANUAL_VALUE ? manualCwd : projectCwd || manualCwd;
|
|
4751
|
+
return {
|
|
4752
|
+
sessionId: sessionId || void 0,
|
|
4753
|
+
agent: normalizeAgentId(agentValue) ?? "codex",
|
|
4754
|
+
resumeMode: resumeMode === "picker" ? "picker" : "new",
|
|
4755
|
+
cwd: cwd || void 0,
|
|
4756
|
+
projectCwd: projectCwd || void 0,
|
|
4757
|
+
manualCwd: manualCwd || void 0
|
|
4758
|
+
};
|
|
4759
|
+
}
|
|
4760
|
+
function startRequestFromDraft(draft) {
|
|
4761
|
+
const sessionId = draft.sessionId?.trim() ?? "";
|
|
4322
4762
|
if (!sessionId) return { ok: false, error: "\u8BF7\u586B\u5199 Session \u540D\u79F0\u3002" };
|
|
4323
|
-
|
|
4324
|
-
if (!agent) return { ok: false, error: "\u8BF7\u9009\u62E9\u6709\u6548\u7684 Agent\uFF1Acodex\u3001traex \u6216 claude\u3002" };
|
|
4325
|
-
if (!cwd) return { ok: false, error: "\u8BF7\u586B\u5199\u7EDD\u5BF9\u5DE5\u4F5C\u76EE\u5F55\u3002" };
|
|
4763
|
+
if (!draft.cwd || draft.cwd === SESSION_CREATE_MANUAL_VALUE) return { ok: false, error: "\u8BF7\u9009\u62E9\u6216\u586B\u5199\u9879\u76EE\u76EE\u5F55\u3002" };
|
|
4326
4764
|
let resume;
|
|
4327
|
-
if (resumeMode === "
|
|
4328
|
-
|
|
4329
|
-
}
|
|
4330
|
-
if (resumeMode === "picker") resume = { mode: "picker" };
|
|
4331
|
-
else if (resumeMode !== "new") {
|
|
4332
|
-
return { ok: false, error: "\u65E0\u6CD5\u8BC6\u522B\u542F\u52A8\u65B9\u5F0F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u65B0\u5EFA Session \u8868\u5355\u3002" };
|
|
4333
|
-
}
|
|
4334
|
-
return { ok: true, value: { sessionId, agent, cwd, resume } };
|
|
4765
|
+
if (draft.resumeMode === "picker") resume = { mode: "picker" };
|
|
4766
|
+
return { ok: true, value: { sessionId, agent: draft.agent, cwd: draft.cwd, resume } };
|
|
4335
4767
|
}
|
|
4336
4768
|
function formString(value) {
|
|
4337
4769
|
if (typeof value === "string") return value.trim();
|
|
@@ -4353,7 +4785,7 @@ function remoteError(result2) {
|
|
|
4353
4785
|
const sessionId = typeof context.sessionId === "string" ? context.sessionId : "\u8BE5\u540D\u79F0";
|
|
4354
4786
|
switch (result2.errorCode) {
|
|
4355
4787
|
case "SESSION_EXISTS":
|
|
4356
|
-
return `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\uFF1A\u8BE5 session \u5DF2\u5728\u8FD0\u884C\u3002\u8BF7\u6362\u4E00\u4E2A\u540D\u79F0\uFF0C\u6216\u7528 /sessions \u8FDE\u63A5\u73B0\u6709 session\u3002`;
|
|
4788
|
+
return context.source === "tmux" ? `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\uFF1A\u68C0\u6D4B\u5230\u540C\u540D tmux \u4F1A\u8BDD\uFF0C\u4F46\u5B83\u672A\u767B\u8BB0\u4E3A\u53EF\u8FDE\u63A5\u7684 LCA session\u3002\u8BF7\u6362\u4E00\u4E2A\u540D\u79F0\uFF0C\u6216\u5728\u672C\u673A\u68C0\u67E5 tmux \u4F1A\u8BDD\u3002` : `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\uFF1A\u8BE5 session \u5DF2\u5728\u8FD0\u884C\u3002\u8BF7\u6362\u4E00\u4E2A\u540D\u79F0\uFF0C\u6216\u7528 /sessions \u8FDE\u63A5\u73B0\u6709 session\u3002`;
|
|
4357
4789
|
case "AGENT_SESSION_IN_USE": {
|
|
4358
4790
|
const ownerSessionId = typeof context.ownerSessionId === "string" ? context.ownerSessionId : "\u73B0\u6709 session";
|
|
4359
4791
|
return `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\uFF1A\u8BE5 Agent \u539F\u751F session \u5DF2\u7531 LCA session\u300C${ownerSessionId}\u300D\u8FDE\u63A5\u3002\u8BF7\u7528 /sessions \u8FDE\u63A5\u73B0\u6709 session\u3002`;
|
|
@@ -4429,20 +4861,20 @@ function manualTimestamp() {
|
|
|
4429
4861
|
}
|
|
4430
4862
|
|
|
4431
4863
|
// src/core/paths.ts
|
|
4432
|
-
import { homedir as
|
|
4433
|
-
import { join as
|
|
4864
|
+
import { homedir as homedir4 } from "os";
|
|
4865
|
+
import { join as join3 } from "path";
|
|
4434
4866
|
function resolveAppPaths(root = process.env.LARK_CODING_ASSISTANT_HOME) {
|
|
4435
|
-
const base = root ||
|
|
4867
|
+
const base = root || join3(homedir4(), ".lark-coding-assistant");
|
|
4436
4868
|
return {
|
|
4437
4869
|
root: base,
|
|
4438
|
-
config:
|
|
4439
|
-
secrets:
|
|
4440
|
-
state:
|
|
4441
|
-
logsDir:
|
|
4442
|
-
logFile:
|
|
4443
|
-
runtimeDir:
|
|
4444
|
-
socket:
|
|
4445
|
-
pid:
|
|
4870
|
+
config: join3(base, "config.json"),
|
|
4871
|
+
secrets: join3(base, "secrets.json"),
|
|
4872
|
+
state: join3(base, "state.json"),
|
|
4873
|
+
logsDir: join3(base, "logs"),
|
|
4874
|
+
logFile: join3(base, "logs", "assistant.log"),
|
|
4875
|
+
runtimeDir: join3(base, "runtime"),
|
|
4876
|
+
socket: join3(base, "runtime", "daemon.sock"),
|
|
4877
|
+
pid: join3(base, "runtime", "daemon.pid")
|
|
4446
4878
|
};
|
|
4447
4879
|
}
|
|
4448
4880
|
|