lark-coding-assistant 0.2.2 → 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.
@@ -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((resolve, reject) => {
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
- resolve({ stdout, stderr });
23
+ resolve2({ stdout, stderr });
24
24
  }
25
25
  );
26
26
  });
27
27
  }
28
28
  function runFileWithInput(file, args, input) {
29
- return new Promise((resolve, reject) => {
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) resolve({ stdout, stderr });
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, 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
- return { ...state, sessions };
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
- if (!isAbsolute(request.cwd)) {
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) || pane.dead) continue;
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 = homedir()) {
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
- function sessionCreateCard() {
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: "codex",
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: "new",
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.rememberSessionCreateFormAction(event.messageId, event.chatId);
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") {
@@ -2179,8 +2300,13 @@ var LarkGateway = class {
2179
2300
  return;
2180
2301
  }
2181
2302
  if (result2.type === "session-create-form") {
2182
- const sent = await this.sendMessage(event.chatId, { card: sessionCreateCard() });
2183
- this.rememberSessionCreateFormAction(sent.messageId, event.chatId);
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);
2184
2310
  const sourceCard = action.kind === "session-create" && result2.sessions ? sessionPickerCard(event.chatId, result2.sessions, result2.activeSessionId, this.signer, void 0, false) : sessionCreateOpenedCard();
2185
2311
  await this.updateCardAfterAction(event.messageId, sourceCard).catch(async (error) => {
2186
2312
  const detail = cardErrorDetail(error);
@@ -2227,7 +2353,7 @@ var LarkGateway = class {
2227
2353
  const retryDelays = [0, 300, 800, 1600];
2228
2354
  let lastError;
2229
2355
  for (const delay of retryDelays) {
2230
- if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
2356
+ if (delay > 0) await new Promise((resolve2) => setTimeout(resolve2, delay));
2231
2357
  try {
2232
2358
  await this.channel.updateCard(messageId, card2);
2233
2359
  return;
@@ -2318,18 +2444,19 @@ var LarkGateway = class {
2318
2444
  [MANUAL_SUBMIT_ACTION, this.signer.sign({ ...common, action: MANUAL_SUBMIT_ACTION }, 10 * 6e4)]
2319
2445
  ]));
2320
2446
  }
2321
- rememberSessionCreateFormAction(messageId, chatId) {
2322
- this.formActions.set(messageId, /* @__PURE__ */ new Map([[
2323
- SESSION_CREATE_SUBMIT_ACTION,
2324
- this.signer.sign({
2325
- kind: "session-create",
2326
- agent: "codex",
2327
- action: "submit",
2328
- paneId: "",
2329
- fingerprint: "create",
2330
- chatId
2331
- }, 10 * 6e4)
2332
- ]]));
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);
2333
2460
  }
2334
2461
  rememberFormActions(messageId, chatId, paneId, screen, agent) {
2335
2462
  if (screen.interaction?.semantics?.activation !== "toggle") {
@@ -2365,9 +2492,9 @@ var LarkGateway = class {
2365
2492
  sendStartupConflict(chatId, request, owner) {
2366
2493
  return this.sendMessage(chatId, { card: startupConflictCard(chatId, requestSession(request), owner, this.signer) });
2367
2494
  }
2368
- async sendSessionCreate(chatId) {
2369
- const result2 = await this.sendMessage(chatId, { card: sessionCreateCard() });
2370
- this.rememberSessionCreateFormAction(result2.messageId, chatId);
2495
+ async sendSessionCreate(chatId, view2) {
2496
+ const result2 = await this.sendMessage(chatId, { card: sessionCreateCard(view2) });
2497
+ this.rememberSessionCreateFormActions(result2.messageId, chatId, view2);
2371
2498
  return result2;
2372
2499
  }
2373
2500
  sendSessionStartupFailure(chatId, failure) {
@@ -2640,6 +2767,152 @@ function redactSecrets(value) {
2640
2767
  ).replace(/([?&](?:api[_-]?key|access[_-]?token|token|secret|signature)=)[^&#\s]+/gi, "$1[REDACTED]");
2641
2768
  }
2642
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
+
2643
2916
  // src/daemon/server.ts
2644
2917
  var AssistantDaemon = class {
2645
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") {
@@ -2681,6 +2954,7 @@ var AssistantDaemon = class {
2681
2954
  pendingAgentSessionClaims = /* @__PURE__ */ new Map();
2682
2955
  pendingResumePickers = /* @__PURE__ */ new Map();
2683
2956
  pendingStartupConflicts = /* @__PURE__ */ new Map();
2957
+ workspaceSnapshots = new WorkspaceSnapshotStore();
2684
2958
  pendingInteractionInput;
2685
2959
  attachAttempts = /* @__PURE__ */ new Map();
2686
2960
  server = createServer((socket) => this.handleSocket(socket));
@@ -2707,9 +2981,9 @@ var AssistantDaemon = class {
2707
2981
  try {
2708
2982
  await this.reconcileSessions(true);
2709
2983
  await rm(this.paths.socket, { force: true });
2710
- await new Promise((resolve, reject) => {
2984
+ await new Promise((resolve2, reject) => {
2711
2985
  this.server.once("error", reject);
2712
- this.server.listen(this.paths.socket, () => resolve());
2986
+ this.server.listen(this.paths.socket, () => resolve2());
2713
2987
  });
2714
2988
  await chmod3(this.paths.socket, 384);
2715
2989
  this.gateway = this.gatewayFactory(config, secrets, {
@@ -2732,7 +3006,7 @@ var AssistantDaemon = class {
2732
3006
  if (this.timer) clearTimeout(this.timer);
2733
3007
  await this.pollInFlight?.catch(() => void 0);
2734
3008
  await this.gateway?.disconnect().catch(() => void 0);
2735
- await new Promise((resolve) => this.server.close(() => resolve()));
3009
+ await new Promise((resolve2) => this.server.close(() => resolve2()));
2736
3010
  await this.releaseRuntimeFiles();
2737
3011
  }
2738
3012
  async acquireRuntimeFiles() {
@@ -2819,7 +3093,8 @@ var AssistantDaemon = class {
2819
3093
  }
2820
3094
  async startSession(sessionId, cwd, agentId, resume) {
2821
3095
  try {
2822
- await validateStartSessionRequest({ sessionId, cwd, agent: agentId, resume });
3096
+ const validated = await validateStartSessionRequest({ sessionId, cwd, agent: agentId, resume });
3097
+ cwd = validated.cwd;
2823
3098
  } catch (error) {
2824
3099
  return fail(error);
2825
3100
  }
@@ -2947,6 +3222,7 @@ var AssistantDaemon = class {
2947
3222
  await this.tmux.preserveOnExit(session.sessionName, false).catch((error) => this.log(
2948
3223
  `failed to disable startup preservation for ${sessionId}: ${errorMessage3(error)}`
2949
3224
  ));
3225
+ await this.rememberSessionWorkspace(session.cwd);
2950
3226
  }
2951
3227
  await this.log(
2952
3228
  `session created: session=${session.id} agent=${session.agent} pane=${session.paneId} active=${this.state.activeSessionId === session.id}`
@@ -3031,7 +3307,7 @@ var AssistantDaemon = class {
3031
3307
  if (message.senderId !== this.state.ownerOpenId || message.chatId !== this.state.boundChatId) return;
3032
3308
  if (text === "/start") {
3033
3309
  try {
3034
- await this.gateway?.sendSessionCreate(message.chatId);
3310
+ await this.gateway?.sendSessionCreate(message.chatId, await this.createSessionWorkspaceView(message.chatId));
3035
3311
  } catch (error) {
3036
3312
  await this.log(`session create card failed: ${errorMessage3(error)}`);
3037
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");
@@ -3296,7 +3572,11 @@ ${escapeFence2(output).slice(-6800)}
3296
3572
  }
3297
3573
  if (action.kind === "session-stop") return this.handleSessionStopAction(action);
3298
3574
  if (action.kind === "session-start-error") {
3299
- if (action.action === "create") return { type: "session-create-form", content: "\u8BF7\u586B\u5199\u542F\u52A8\u4FE1\u606F\u3002" };
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
+ };
3300
3580
  if (action.action === "sessions") {
3301
3581
  await this.reconcileSessions(true);
3302
3582
  return this.sessionPickerActionResult("\u5DF2\u53D1\u9001\u6700\u65B0 Sessions\u3002");
@@ -3310,15 +3590,53 @@ ${escapeFence2(output).slice(-6800)}
3310
3590
  return {
3311
3591
  type: "session-create-form",
3312
3592
  content: "\u8BF7\u586B\u5199\u542F\u52A8\u4FE1\u606F\u3002",
3593
+ view: await this.createSessionWorkspaceView(event.chatId),
3313
3594
  sessions: Object.values(this.state.sessions ?? {}),
3314
3595
  activeSessionId: this.state.activeSessionId
3315
3596
  };
3316
3597
  }
3317
- if (action.action !== "submit" || !event.action.formValue) {
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) {
3318
3608
  return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u65B0\u5EFA Session \u8868\u5355\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 /sessions\u3002" };
3319
3609
  }
3320
- const request = startRequestFromForm(event.action.formValue);
3321
- if (!request.ok) return { type: "error", content: request.error };
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
+ }
3322
3640
  const result3 = await this.startRemoteSession(request.value);
3323
3641
  if (!result3.ok) return larkStartupError(result3.error, request.value);
3324
3642
  if (result3.state === "picker") {
@@ -3523,6 +3841,7 @@ ${escapeFence2(output).slice(-6800)}
3523
3841
  await this.tmux.preserveOnExit(session.sessionName, false).catch(() => void 0);
3524
3842
  const selected = await this.useSession(sessionId);
3525
3843
  if (!selected.ok) return { type: "error", content: remoteError(selected) };
3844
+ await this.rememberSessionWorkspace(session.cwd);
3526
3845
  return { type: "session-created", content: remoteStartSuccess(session), session };
3527
3846
  }
3528
3847
  async readResumePicker(session) {
@@ -3543,7 +3862,7 @@ ${escapeFence2(output).slice(-6800)}
3543
3862
  while (Date.now() < deadline) {
3544
3863
  latest = await this.readResumePicker(session);
3545
3864
  if (latest && (!previousFingerprint || latest.fingerprint !== previousFingerprint)) return latest;
3546
- await new Promise((resolve) => setTimeout(resolve, 100));
3865
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
3547
3866
  }
3548
3867
  return latest;
3549
3868
  }
@@ -3556,7 +3875,7 @@ ${escapeFence2(output).slice(-6800)}
3556
3875
  let previousFingerprint;
3557
3876
  const deadline = Date.now() + 900;
3558
3877
  do {
3559
- await new Promise((resolve) => setTimeout(resolve, 120));
3878
+ await new Promise((resolve2) => setTimeout(resolve2, 120));
3560
3879
  await this.poll();
3561
3880
  const fingerprint = this.screen?.fingerprint;
3562
3881
  if (fingerprint && fingerprint === previousFingerprint) return;
@@ -3584,7 +3903,7 @@ ${escapeFence2(output).slice(-6800)}
3584
3903
  }
3585
3904
  try {
3586
3905
  await execute(session);
3587
- await new Promise((resolve) => setTimeout(resolve, 120));
3906
+ await new Promise((resolve2) => setTimeout(resolve2, 120));
3588
3907
  await this.poll();
3589
3908
  const output = this.screen ? tailScreen(this.screen.normalized, 60) : "\u65E0\u6CD5\u8BFB\u53D6\u6700\u65B0\u7EC8\u7AEF\u753B\u9762\u3002";
3590
3909
  await this.gateway?.sendMarkdown(chatId, `**\u624B\u52A8\u64CD\u4F5C\uFF1A${operation}**
@@ -3635,7 +3954,7 @@ ${escapeFence2(output).slice(-6500)}
3635
3954
  };
3636
3955
  }
3637
3956
  if (target?.role === "custom-input" || target?.role === "chat") {
3638
- await new Promise((resolve) => setTimeout(resolve, 100));
3957
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
3639
3958
  await this.poll();
3640
3959
  if (!before?.interactionId) return { type: "error", content: "\u65E0\u6CD5\u786E\u8BA4\u5F53\u524D\u4EA4\u4E92\uFF0C\u8BF7\u7528 /tail \u68C0\u67E5\u3002" };
3641
3960
  this.pendingInteractionInput = {
@@ -3768,7 +4087,7 @@ ${escapeFence2(output).slice(-6500)}
3768
4087
  if (focusedIndex === targetIndex) return { ok: true };
3769
4088
  const direction = targetIndex > focusedIndex ? "Down" : "Up";
3770
4089
  await this.tmux.sendKey(paneId, direction);
3771
- await new Promise((resolve) => setTimeout(resolve, 40));
4090
+ await new Promise((resolve2) => setTimeout(resolve2, 40));
3772
4091
  await this.poll();
3773
4092
  const nextFocused = this.screen?.actions.findIndex(({ focused }) => focused) ?? -1;
3774
4093
  if (nextFocused === focusedIndex) return { ok: false, error: "terminal focus did not move as expected" };
@@ -3778,7 +4097,7 @@ ${escapeFence2(output).slice(-6500)}
3778
4097
  async waitForInteractionChange(interactionId) {
3779
4098
  const deadline = Date.now() + 1500;
3780
4099
  while (Date.now() < deadline) {
3781
- await new Promise((resolve) => setTimeout(resolve, 75));
4100
+ await new Promise((resolve2) => setTimeout(resolve2, 75));
3782
4101
  await this.poll();
3783
4102
  const current = this.screen?.interaction;
3784
4103
  if (!current || current.interactionId !== interactionId) return;
@@ -3787,7 +4106,7 @@ ${escapeFence2(output).slice(-6500)}
3787
4106
  async waitForControlMarker(interactionId, controlId, marker) {
3788
4107
  const deadline = Date.now() + 1500;
3789
4108
  while (Date.now() < deadline) {
3790
- await new Promise((resolve) => setTimeout(resolve, 75));
4109
+ await new Promise((resolve2) => setTimeout(resolve2, 75));
3791
4110
  await this.poll();
3792
4111
  const current = this.screen?.interaction;
3793
4112
  const control = this.screen?.actions.find(({ id }) => id === controlId);
@@ -3798,7 +4117,7 @@ ${escapeFence2(output).slice(-6500)}
3798
4117
  const expected = input.trim();
3799
4118
  const deadline = Date.now() + 1500;
3800
4119
  while (Date.now() < deadline) {
3801
- await new Promise((resolve) => setTimeout(resolve, 75));
4120
+ await new Promise((resolve2) => setTimeout(resolve2, 75));
3802
4121
  await this.poll();
3803
4122
  const control = this.screen?.actions.find(({ id }) => id === controlId);
3804
4123
  if (this.screen?.interaction?.interactionId === interactionId && control?.inputValue && (control.inputValue === expected || expected.startsWith(control.inputValue))) return;
@@ -3843,7 +4162,7 @@ ${escapeFence2(output).slice(-6500)}
3843
4162
  if (!navigation.ok) return navigation;
3844
4163
  if (desired.editor) {
3845
4164
  if (desired.editor.openKey) await this.tmux.sendKey(paneId, desired.editor.openKey);
3846
- await new Promise((resolve) => setTimeout(resolve, 100));
4165
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
3847
4166
  await this.tmux.sendKey(paneId, "C-u");
3848
4167
  await this.tmux.sendKey(paneId, "C-k");
3849
4168
  await this.tmux.sendText(paneId, desired.input, false);
@@ -4125,7 +4444,7 @@ ${escapeFence2(output).slice(-6500)}
4125
4444
  source: "startup-discovery"
4126
4445
  });
4127
4446
  }
4128
- await new Promise((resolve) => setTimeout(resolve, 100));
4447
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
4129
4448
  }
4130
4449
  const session = this.state.sessions?.[sessionId];
4131
4450
  if (!session) return fail(new AppError("SESSION_NOT_FOUND", `session disappeared during startup: ${sessionId}`, { sessionId }));
@@ -4156,7 +4475,7 @@ ${escapeFence2(output).slice(-6500)}
4156
4475
  while (Date.now() < deadline) {
4157
4476
  const pane = await this.tmux.inspect(session.paneId);
4158
4477
  if (!pane || pane.dead) return fail(await this.startupExitedError(session, pane));
4159
- await new Promise((resolve) => setTimeout(resolve, 80));
4478
+ await new Promise((resolve2) => setTimeout(resolve2, 80));
4160
4479
  }
4161
4480
  return { ok: true };
4162
4481
  }
@@ -4324,6 +4643,67 @@ ${output}`
4324
4643
  }
4325
4644
  return { ok: true, state: "ready", session };
4326
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
+ }
4327
4707
  async log(message) {
4328
4708
  await appendFile(this.paths.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${message}
4329
4709
  `, { mode: 384 });
@@ -4361,24 +4741,29 @@ function remember(values, value, limit) {
4361
4741
  values.delete(oldest);
4362
4742
  }
4363
4743
  }
4364
- function startRequestFromForm(values) {
4744
+ function sessionCreateDraftFromForm(values) {
4365
4745
  const sessionId = formString(values[SESSION_CREATE_NAME_FIELD]);
4366
4746
  const agentValue = formString(values[SESSION_CREATE_AGENT_FIELD]);
4367
- const cwd = formString(values[SESSION_CREATE_CWD_FIELD]);
4747
+ const projectCwd = formString(values[SESSION_CREATE_PROJECT_FIELD]);
4748
+ const manualCwd = formString(values[SESSION_CREATE_CWD_FIELD]);
4368
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() ?? "";
4369
4762
  if (!sessionId) return { ok: false, error: "\u8BF7\u586B\u5199 Session \u540D\u79F0\u3002" };
4370
- const agent = normalizeAgentId(agentValue);
4371
- if (!agent) return { ok: false, error: "\u8BF7\u9009\u62E9\u6709\u6548\u7684 Agent\uFF1Acodex\u3001traex \u6216 claude\u3002" };
4372
- 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" };
4373
4764
  let resume;
4374
- if (resumeMode === "last") {
4375
- return { ok: false, error: "\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" };
4376
- }
4377
- if (resumeMode === "picker") resume = { mode: "picker" };
4378
- else if (resumeMode !== "new") {
4379
- return { ok: false, error: "\u65E0\u6CD5\u8BC6\u522B\u542F\u52A8\u65B9\u5F0F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u65B0\u5EFA Session \u8868\u5355\u3002" };
4380
- }
4381
- 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 } };
4382
4767
  }
4383
4768
  function formString(value) {
4384
4769
  if (typeof value === "string") return value.trim();
@@ -4400,7 +4785,7 @@ function remoteError(result2) {
4400
4785
  const sessionId = typeof context.sessionId === "string" ? context.sessionId : "\u8BE5\u540D\u79F0";
4401
4786
  switch (result2.errorCode) {
4402
4787
  case "SESSION_EXISTS":
4403
- 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`;
4404
4789
  case "AGENT_SESSION_IN_USE": {
4405
4790
  const ownerSessionId = typeof context.ownerSessionId === "string" ? context.ownerSessionId : "\u73B0\u6709 session";
4406
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`;
@@ -4476,20 +4861,20 @@ function manualTimestamp() {
4476
4861
  }
4477
4862
 
4478
4863
  // src/core/paths.ts
4479
- import { homedir as homedir2 } from "os";
4480
- import { join as join2 } from "path";
4864
+ import { homedir as homedir4 } from "os";
4865
+ import { join as join3 } from "path";
4481
4866
  function resolveAppPaths(root = process.env.LARK_CODING_ASSISTANT_HOME) {
4482
- const base = root || join2(homedir2(), ".lark-coding-assistant");
4867
+ const base = root || join3(homedir4(), ".lark-coding-assistant");
4483
4868
  return {
4484
4869
  root: base,
4485
- config: join2(base, "config.json"),
4486
- secrets: join2(base, "secrets.json"),
4487
- state: join2(base, "state.json"),
4488
- logsDir: join2(base, "logs"),
4489
- logFile: join2(base, "logs", "assistant.log"),
4490
- runtimeDir: join2(base, "runtime"),
4491
- socket: join2(base, "runtime", "daemon.sock"),
4492
- pid: join2(base, "runtime", "daemon.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")
4493
4878
  };
4494
4879
  }
4495
4880