deskbuddy 0.2.0-beta.46 → 0.2.0-beta.47

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.
@@ -203,7 +203,10 @@ function loadConfig() {
203
203
  cozeApp: typeof parsed.cozeApp === "boolean" ? parsed.cozeApp : void 0,
204
204
  cozeCdpPort: portOrUndefined(parsed.cozeCdpPort),
205
205
  chatgptAppPort: typeof parsed.chatgptAppPort === "boolean" ? parsed.chatgptAppPort : void 0,
206
- chatgptCdpPort: portOrUndefined(parsed.chatgptCdpPort)
206
+ chatgptCdpPort: portOrUndefined(parsed.chatgptCdpPort),
207
+ focusSessionOnRemote: typeof parsed.focusSessionOnRemote === "boolean" ? parsed.focusSessionOnRemote : void 0,
208
+ focusSessionOnRemoteWindows: typeof parsed.focusSessionOnRemoteWindows === "boolean" ? parsed.focusSessionOnRemoteWindows : void 0,
209
+ focusSessionInApp: typeof parsed.focusSessionInApp === "boolean" ? parsed.focusSessionInApp : void 0
207
210
  };
208
211
  } catch {
209
212
  return fallback;
@@ -6338,6 +6341,52 @@ var WsCdpClient = class {
6338
6341
 
6339
6342
  // src/daemon/workbuddyApp/keeper.ts
6340
6343
  import { posix as posix2, win32 as win326 } from "path";
6344
+
6345
+ // src/daemon/userIdle.ts
6346
+ function parseHidIdleMs(out) {
6347
+ const m = /"HIDIdleTime"\s*=\s*(\d+)/.exec(out);
6348
+ return m ? Math.floor(Number(m[1]) / 1e6) : null;
6349
+ }
6350
+ var WIN_PRESENCE_PS = String.raw`Add-Type @"
6351
+ using System; using System.Runtime.InteropServices;
6352
+ public class DbUserIdle {
6353
+ [StructLayout(LayoutKind.Sequential)] public struct LII { public uint cbSize; public uint dwTime; }
6354
+ [DllImport("user32.dll")] public static extern bool GetLastInputInfo(ref LII p);
6355
+ [DllImport("kernel32.dll")] public static extern uint GetTickCount();
6356
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
6357
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
6358
+ }
6359
+ "@
6360
+ $l = New-Object DbUserIdle+LII; $l.cbSize = 8
6361
+ if (-not [DbUserIdle]::GetLastInputInfo([ref]$l)) { exit 1 }
6362
+ $idle = (([uint64][DbUserIdle]::GetTickCount()) - [uint64]$l.dwTime + 4294967296) % 4294967296
6363
+ $fp = [uint32]0; [void][DbUserIdle]::GetWindowThreadProcessId([DbUserIdle]::GetForegroundWindow(), [ref]$fp)
6364
+ $n = ''; try { $n = (Get-Process -Id $fp -ErrorAction Stop).ProcessName } catch {}
6365
+ Write-Output ("{0}|{1}" -f $idle, $n)`;
6366
+ function parseWinPresence(code, stdout2) {
6367
+ const m = /^(\d+)\|(.*)$/m.exec(stdout2.trim());
6368
+ if (code !== 0 || !m) return null;
6369
+ return { idleMs: Number(m[1]), front: m[2].trim() };
6370
+ }
6371
+ async function macIdleMs(exec) {
6372
+ const r = await exec("ioreg", ["-c", "IOHIDSystem", "-d", "4"]);
6373
+ return r.code === 0 ? parseHidIdleMs(r.stdout) : null;
6374
+ }
6375
+ async function winPresence(exec) {
6376
+ const r = await exec("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", WIN_PRESENCE_PS], 15e3);
6377
+ return parseWinPresence(r.code, r.stdout);
6378
+ }
6379
+ async function userIdleMs(exec, platform = process.platform) {
6380
+ try {
6381
+ if (platform === "darwin") return await macIdleMs(exec);
6382
+ if (platform === "win32") return (await winPresence(exec))?.idleMs ?? null;
6383
+ } catch {
6384
+ return null;
6385
+ }
6386
+ return null;
6387
+ }
6388
+
6389
+ // src/daemon/workbuddyApp/keeper.ts
6341
6390
  var DEFAULT_CDP_PORT = 39217;
6342
6391
  var ENV_NAME = "WORKBUDDY_REMOTE_DEBUGGING_PORT";
6343
6392
  var RESTART_GAP_MS = 30 * 60 * 1e3;
@@ -6418,26 +6467,6 @@ var filePersist = {
6418
6467
  }
6419
6468
  }
6420
6469
  };
6421
- function parseHidIdleMs(out) {
6422
- const m = /"HIDIdleTime"\s*=\s*(\d+)/.exec(out);
6423
- return m ? Math.floor(Number(m[1]) / 1e6) : null;
6424
- }
6425
- var WIN_PRESENCE_PS = String.raw`Add-Type @"
6426
- using System; using System.Runtime.InteropServices;
6427
- public class DbUserIdle {
6428
- [StructLayout(LayoutKind.Sequential)] public struct LII { public uint cbSize; public uint dwTime; }
6429
- [DllImport("user32.dll")] public static extern bool GetLastInputInfo(ref LII p);
6430
- [DllImport("kernel32.dll")] public static extern uint GetTickCount();
6431
- [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
6432
- [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
6433
- }
6434
- "@
6435
- $l = New-Object DbUserIdle+LII; $l.cbSize = 8
6436
- if (-not [DbUserIdle]::GetLastInputInfo([ref]$l)) { exit 1 }
6437
- $idle = (([uint64][DbUserIdle]::GetTickCount()) - [uint64]$l.dwTime + 4294967296) % 4294967296
6438
- $fp = [uint32]0; [void][DbUserIdle]::GetWindowThreadProcessId([DbUserIdle]::GetForegroundWindow(), [ref]$fp)
6439
- $n = ''; try { $n = (Get-Process -Id $fp -ErrorAction Stop).ProcessName } catch {}
6440
- Write-Output ("{0}|{1}" -f $idle, $n)`;
6441
6470
  var WAKE_LAUNCH_GAP_MS = 60 * 1e3;
6442
6471
  var APP_PATH_TTL_MS = 5 * 60 * 1e3;
6443
6472
  function defaultWorkbuddyAppPath() {
@@ -6692,7 +6721,7 @@ var CdpKeeper = class {
6692
6721
  if (this.#d.userAway) return await this.#d.userAway();
6693
6722
  const exec = this.#exec();
6694
6723
  if (this.#platform() === "darwin") {
6695
- const idle = parseHidIdleMs((await exec("ioreg", ["-c", "IOHIDSystem", "-d", "4"])).stdout);
6724
+ const idle = await macIdleMs(exec);
6696
6725
  if (idle === null || idle < AWAY_IDLE_MS) return false;
6697
6726
  const asn = (await exec("lsappinfo", ["front"])).stdout.trim();
6698
6727
  if (!asn) return false;
@@ -6700,10 +6729,9 @@ var CdpKeeper = class {
6700
6729
  return !!name.trim() && !/WorkBuddy/i.test(name);
6701
6730
  }
6702
6731
  if (this.#platform() === "win32") {
6703
- const r = await exec("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", WIN_PRESENCE_PS], 15e3);
6704
- const m = /^(\d+)\|(.*)$/m.exec(r.stdout.trim());
6705
- if (r.code !== 0 || !m) return false;
6706
- return Number(m[1]) >= AWAY_IDLE_MS && !/^WorkBuddy$/i.test(m[2].trim());
6732
+ const p = await winPresence(exec);
6733
+ if (!p) return false;
6734
+ return p.idleMs >= AWAY_IDLE_MS && !/^WorkBuddy$/i.test(p.front);
6707
6735
  }
6708
6736
  } catch {
6709
6737
  return false;
@@ -9023,10 +9051,6 @@ function hasMainProc(psOut, brand) {
9023
9051
  }
9024
9052
  return false;
9025
9053
  }
9026
- function parseHidIdleMs2(out) {
9027
- const m = /"HIDIdleTime"\s*=\s*(\d+)/.exec(out);
9028
- return m ? Math.floor(Number(m[1]) / 1e6) : null;
9029
- }
9030
9054
  function parseLsappBundle(out) {
9031
9055
  const m = /"CFBundleIdentifier"\s*=\s*"([^"]+)"/.exec(out);
9032
9056
  return m ? m[1] : null;
@@ -9307,10 +9331,9 @@ var QoderCdpKeeper = class {
9307
9331
  async userAway() {
9308
9332
  if (this.#isWin() && !this.#d.frontBundle && !this.#d.idleMs) {
9309
9333
  try {
9310
- const r = await this.#exec()("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", WIN_PRESENCE_PS], 15e3);
9311
- const m = /^(\d+)\|(.*)$/m.exec(r.stdout.trim());
9312
- if (r.code !== 0 || !m) return false;
9313
- return Number(m[1]) >= USER_IDLE_MS && !!m[2].trim() && !this.#d.brand.win.fgName.test(m[2].trim());
9334
+ const p = await winPresence(this.#exec());
9335
+ if (!p) return false;
9336
+ return p.idleMs >= USER_IDLE_MS && !!p.front && !this.#d.brand.win.fgName.test(p.front);
9314
9337
  } catch {
9315
9338
  return false;
9316
9339
  }
@@ -9333,8 +9356,7 @@ var QoderCdpKeeper = class {
9333
9356
  return parseLsappBundle((await exec("lsappinfo", ["info", "-only", "bundleid", asn])).stdout);
9334
9357
  }
9335
9358
  async #defaultIdle() {
9336
- const r = await this.#exec()("ioreg", ["-c", "IOHIDSystem", "-d", "4"]);
9337
- return r.code === 0 ? parseHidIdleMs2(r.stdout) : null;
9359
+ return macIdleMs(this.#exec());
9338
9360
  }
9339
9361
  /** 主进程(macOS;只认唯一一个;没有 / 认不出 / 多个 → null) */
9340
9362
  async #brandMain() {
@@ -11541,7 +11563,7 @@ var defaultLoginOpenDeps = () => ({
11541
11563
  });
11542
11564
  async function installCodebuddyHooks(log) {
11543
11565
  try {
11544
- const { installTools: installTools2 } = await import("./enable-XTCQ6IFF.js");
11566
+ const { installTools: installTools2 } = await import("./enable-OICEMNUF.js");
11545
11567
  installTools2(["workbuddy"], () => {
11546
11568
  }, null, { auto: true });
11547
11569
  } catch (e) {
@@ -12055,7 +12077,7 @@ function defaultOfferDeps(log) {
12055
12077
  }),
12056
12078
  openLogin: () => defaultOpenLogin(platform),
12057
12079
  installHooks: async () => {
12058
- const { installTools: installTools2 } = await import("./enable-XTCQ6IFF.js");
12080
+ const { installTools: installTools2 } = await import("./enable-OICEMNUF.js");
12059
12081
  installTools2(["workbuddy"], (l) => log(`[WorkBuddy \u72EC\u7ACB\u7248] ${l}`), null, { auto: true });
12060
12082
  },
12061
12083
  log: (l) => log(`[WorkBuddy \u72EC\u7ACB\u7248] ${l}`),
@@ -12483,6 +12505,7 @@ export {
12483
12505
  registerWorkbuddySession,
12484
12506
  flushPendingWorkbuddy,
12485
12507
  connectCdp,
12508
+ userIdleMs,
12486
12509
  DEFAULT_CDP_PORT,
12487
12510
  defaultExec,
12488
12511
  APP_PORT_HUNG_REASON,
package/dist/cli.js CHANGED
@@ -255,6 +255,7 @@ import {
255
255
  uninstallAutostart,
256
256
  uninstallShim,
257
257
  unmarkWbAppSession,
258
+ userIdleMs,
258
259
  versionCliPath,
259
260
  versionDir,
260
261
  waitModeFor,
@@ -278,15 +279,61 @@ import {
278
279
  writeState,
279
280
  writeWakeMark,
280
281
  writeWakeSecret
281
- } from "./chunk-Y6D6FMMF.js";
282
+ } from "./chunk-JPCZVBK6.js";
282
283
 
283
284
  // src/commands/config.ts
285
+ var BOOL_KEYS = {
286
+ focusSessionOnRemote: {
287
+ def: false,
288
+ on: "\u5DF2\u6253\u5F00\uFF1A\u8BBE\u5907\u56DE\u590D / \u65B0\u5EFA\u4EFB\u52A1\u540E\uFF0C\u628A WorkBuddy\u3001ChatGPT\uFF08Codex\uFF09\u3001\u5343\u95EE\u529E\u516C\u5207\u5230\u8BE5\u4F1A\u8BDD\u5E76\u8C03\u5230\u524D\u53F0\uFF08\u4F60 10 \u79D2\u5185\u5728\u7528\u952E\u9F20\u65F6\u4E0D\u5207\uFF09",
289
+ off: "\u5DF2\u5173\u95ED\uFF1A\u8BBE\u5907\u56DE\u590D / \u65B0\u5EFA\u4EFB\u52A1\u540E\u4E0D\u518D\u628A App \u8C03\u5230\u524D\u53F0"
290
+ },
291
+ focusSessionOnRemoteWindows: {
292
+ def: false,
293
+ on: "\u5DF2\u6253\u5F00\uFF1AWindows \u4E0A\u4E5F\u7528\u6DF1\u94FE\u5207\u4F1A\u8BDD\uFF08\u8FD8\u987B\u6253\u5F00 focusSessionOnRemote\uFF1BApp \u6CA1\u6CE8\u518C\u534F\u8BAE\u7684\u7167\u6837\u4E0D\u5207\uFF09",
294
+ off: "\u5DF2\u5173\u95ED\uFF1AWindows \u4E0A\u4E0D\u7528\u6DF1\u94FE\u5207\u4F1A\u8BDD"
295
+ },
296
+ focusSessionInApp: {
297
+ def: true,
298
+ on: "\u5DF2\u6253\u5F00\uFF1A\u8BBE\u5907\u56DE\u590D opencode \u4F1A\u8BDD\u540E\uFF0C\u7EC8\u7AEF\u754C\u9762\u91CC\u5207\u5230\u8BE5\u4F1A\u8BDD\uFF08\u4E0D\u62A2\u7126\u70B9\uFF09",
299
+ off: "\u5DF2\u5173\u95ED\uFF1A\u8BBE\u5907\u56DE\u590D opencode \u4F1A\u8BDD\u540E\u4E0D\u5207\u6362\u754C\u9762\u91CC\u7684\u4F1A\u8BDD"
300
+ }
301
+ };
302
+ var isBoolKey = (k) => !!k && Object.prototype.hasOwnProperty.call(BOOL_KEYS, k);
303
+ var USAGE = [
304
+ "\u7528\u6CD5\uFF1Adeskbuddy config set transcript on|off \uFF08\u4F1A\u8BDD\u5BF9\u8BDD\u540C\u6B65\u7684\u672C\u5730\u5F00\u5173\uFF0C\u9ED8\u8BA4 on\uFF09",
305
+ " deskbuddy config get transcript",
306
+ ` deskbuddy config set <${Object.keys(BOOL_KEYS).join("|")}> true|false`,
307
+ " deskbuddy config get <\u540C\u4E0A>"
308
+ ].join("\n");
284
309
  function runConfig(args, out = (s2) => process.stdout.write(s2 + "\n")) {
285
310
  const [verb, key, value] = args;
286
- if (key !== "transcript" || verb !== "set" && verb !== "get") {
287
- out("\u7528\u6CD5\uFF1Adeskbuddy config set transcript on|off \uFF08\u4F1A\u8BDD\u5BF9\u8BDD\u540C\u6B65\u7684\u672C\u5730\u5F00\u5173\uFF0C\u9ED8\u8BA4 on\uFF09\n deskbuddy config get transcript");
311
+ if (verb !== "set" && verb !== "get") {
312
+ out(USAGE);
313
+ return 1;
314
+ }
315
+ if (key === "transcript") return transcript(verb, value, out);
316
+ if (!isBoolKey(key)) {
317
+ out(USAGE);
288
318
  return 1;
289
319
  }
320
+ const spec = BOOL_KEYS[key];
321
+ const cfg = loadConfig();
322
+ if (verb === "get") {
323
+ out(String(cfg[key] ?? spec.def));
324
+ return 0;
325
+ }
326
+ if (value !== "true" && value !== "false") {
327
+ out("\u53D6\u503C\u53EA\u80FD\u662F true \u6216 false");
328
+ return 1;
329
+ }
330
+ const v = value === "true";
331
+ const next = { ...cfg, [key]: v === spec.def ? void 0 : v };
332
+ saveConfig(next);
333
+ out(`${v ? spec.on : spec.off}\uFF08\u5B88\u62A4\u8FDB\u7A0B\u4E0B\u4E00\u6761\u6307\u4EE4\u751F\u6548\uFF09`);
334
+ return 0;
335
+ }
336
+ function transcript(verb, value, out) {
290
337
  const cfg = loadConfig();
291
338
  if (verb === "get") {
292
339
  out(cfg.transcript === false ? "off" : "on");
@@ -818,6 +865,7 @@ import WebSocket2 from "ws";
818
865
 
819
866
  // src/core/transcript/locate.ts
820
867
  import { readdirSync as readdirSync2 } from "fs";
868
+ import { readdir } from "fs/promises";
821
869
  import { join as join3 } from "path";
822
870
  function findCodexRollout(codexHome2, sessionId, maxDays = 120) {
823
871
  if (!/^[0-9A-Za-z-]{8,64}$/.test(sessionId)) return null;
@@ -848,6 +896,35 @@ function findCodexRollout(codexHome2, sessionId, maxDays = 120) {
848
896
  }
849
897
  return null;
850
898
  }
899
+ async function findCodexRolloutAsync(codexHome2, sessionId, maxDays = 14) {
900
+ if (!/^[0-9A-Za-z-]{8,64}$/.test(sessionId)) return null;
901
+ const tail4 = `-${sessionId}.jsonl`;
902
+ const sorted = async (dir2) => {
903
+ try {
904
+ return (await readdir(dir2)).filter((n) => /^\d+$/.test(n)).sort().reverse();
905
+ } catch {
906
+ return [];
907
+ }
908
+ };
909
+ const root = join3(codexHome2, "sessions");
910
+ let days = 0;
911
+ for (const y of await sorted(root)) {
912
+ for (const m of await sorted(join3(root, y))) {
913
+ for (const d of await sorted(join3(root, y, m))) {
914
+ if (++days > maxDays) return null;
915
+ let names = [];
916
+ try {
917
+ names = await readdir(join3(root, y, m, d));
918
+ } catch {
919
+ continue;
920
+ }
921
+ const hit = names.find((n) => n.startsWith("rollout-") && n.endsWith(tail4));
922
+ if (hit) return join3(root, y, m, d, hit);
923
+ }
924
+ }
925
+ }
926
+ return null;
927
+ }
851
928
 
852
929
  // src/daemon/transcriptSync.ts
853
930
  import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readdirSync as readdirSync3, readFileSync as readFileSync2, readSync, renameSync, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -5260,7 +5337,19 @@ function createResult(text, isError) {
5260
5337
  if (!v || v.success !== true || isError) return { status: "failed", reason: "window_failed", message: `\u5343\u95EE\u529E\u516C\u6CA1\u5EFA\u6210\u4EFB\u52A1\uFF1A${msg.slice(0, 120)}` };
5261
5338
  const chatId = typeof v.chatId === "string" ? v.chatId : "";
5262
5339
  if (!/^[A-Za-z0-9_-]{1,64}$/.test(chatId)) return { status: "failed", reason: "window_failed", message: "\u5343\u95EE\u529E\u516C\u5EFA\u4E86\u4EFB\u52A1\u4F46\u6CA1\u56DE\u4EFB\u52A1\u53F7" };
5263
- return { ok: true, chatId };
5340
+ const subChatId = typeof v.subChatId === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(v.subChatId) ? v.subChatId : "";
5341
+ return subChatId ? { ok: true, chatId, subChatId } : { ok: true, chatId };
5342
+ }
5343
+ function qwenworkChatIdOfSession(sessionId, cwd, root = join14(qwenworkDataHome(), "workspace"), transcriptCwd = transcriptCwdOf) {
5344
+ let dir2 = typeof cwd === "string" ? cwd : "";
5345
+ if (!dir2 && sessionId) {
5346
+ try {
5347
+ dir2 = transcriptCwd(sessionId) ?? "";
5348
+ } catch {
5349
+ dir2 = "";
5350
+ }
5351
+ }
5352
+ return dir2 ? chatIdOf(dir2, root) : null;
5264
5353
  }
5265
5354
  function findQwenworkSession(chatId, root, dir2 = stateDir(), sinceMs = 0) {
5266
5355
  let files;
@@ -5590,7 +5679,7 @@ var QwenWorkBridge = class {
5590
5679
  }
5591
5680
  const sessionId = await this.#waitSession(c.chatId, startedAt);
5592
5681
  this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A\u8BBE\u5907\u65B0\u5EFA\u4EFB\u52A1 ${c.chatId.slice(0, 8)}${sessionId ? `\uFF0C\u5185\u6838\u4F1A\u8BDD ${sessionId.slice(0, 8)}` : "\uFF0C\u8FD8\u6CA1\u7B49\u5230\u5185\u6838\u4F1A\u8BDD\u53F7"}`);
5593
- return { status: "started", chatId: c.chatId, sessionId };
5682
+ return { status: "started", chatId: c.chatId, sessionId, ...c.subChatId ? { subChatId: c.subChatId } : {} };
5594
5683
  }
5595
5684
  async #waitSession(chatId, sinceMs) {
5596
5685
  const root = this.#d.workspaceRoot ?? join14(homedir3(), ".qwenworkcn", "workspace");
@@ -5890,6 +5979,252 @@ function sleep(ms) {
5890
5979
  return new Promise((r) => setTimeout(r, ms).unref?.());
5891
5980
  }
5892
5981
 
5982
+ // src/daemon/focusSession.ts
5983
+ import { execFile as execFile3 } from "child_process";
5984
+ import { open as open2 } from "fs/promises";
5985
+ var FOCUS_IDLE_MIN_MS = 10 * 1e3;
5986
+ var FOCUS_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
5987
+ var okId = (id) => typeof id === "string" && FOCUS_ID_RE.test(id);
5988
+ function workbuddyChatUrl(sessionId) {
5989
+ return okId(sessionId) ? `workbuddy://chat/${sessionId}` : null;
5990
+ }
5991
+ function codexThreadUrl(threadId) {
5992
+ return okId(threadId) ? `codex://threads/${threadId}` : null;
5993
+ }
5994
+ function qwenworkChatUrl(chatId, subChatId) {
5995
+ if (!okId(chatId)) return null;
5996
+ const q = new URLSearchParams({ chatId });
5997
+ if (subChatId && okId(subChatId)) q.set("subChatId", subChatId);
5998
+ return `qwenwork-cn://notification-click?${q.toString()}`;
5999
+ }
6000
+ function focusUrl(t) {
6001
+ if (t.kind === "workbuddy") return workbuddyChatUrl(t.sessionId);
6002
+ if (t.kind === "codex") return codexThreadUrl(t.threadId);
6003
+ return qwenworkChatUrl(t.chatId, t.subChatId);
6004
+ }
6005
+ function focusScheme(t) {
6006
+ return t.kind === "workbuddy" ? "workbuddy" : t.kind === "codex" ? "codex" : "qwenwork-cn";
6007
+ }
6008
+ var TOOL_NAME = { workbuddy: "WorkBuddy", codex: "ChatGPT\uFF08Codex\uFF09", qwenwork: "\u5343\u95EE\u529E\u516C" };
6009
+ function focusSettings(cfg) {
6010
+ return { onRemote: cfg.focusSessionOnRemote === true, onRemoteWindows: cfg.focusSessionOnRemoteWindows === true, inApp: cfg.focusSessionInApp !== false };
6011
+ }
6012
+ var APP_ORIGINATOR_RE = /"originator"\s*:\s*"(Codex Desktop|codex_work_desktop)"/;
6013
+ function isChatgptAppRolloutHead(head) {
6014
+ const first = head.split("\n", 1)[0] ?? "";
6015
+ return /"type"\s*:\s*"session_meta"/.test(first) && APP_ORIGINATOR_RE.test(first);
6016
+ }
6017
+ async function readHead(path, bytes = 16 * 1024) {
6018
+ let fh = null;
6019
+ try {
6020
+ fh = await open2(path, "r");
6021
+ const buf = Buffer.alloc(bytes);
6022
+ const { bytesRead } = await fh.read(buf, 0, bytes, 0);
6023
+ return buf.subarray(0, bytesRead).toString("utf8");
6024
+ } catch {
6025
+ return null;
6026
+ } finally {
6027
+ await fh?.close().catch(() => {
6028
+ });
6029
+ }
6030
+ }
6031
+ var CODEX_OWNER_MAX_DAYS = 14;
6032
+ async function codexThreadInChatgptApp(threadId, d = {}) {
6033
+ if (!okId(threadId)) return false;
6034
+ try {
6035
+ if (d.seenByApp?.(threadId)) return true;
6036
+ } catch {
6037
+ }
6038
+ let file = null;
6039
+ try {
6040
+ file = await (d.rollout ?? ((id) => findCodexRolloutAsync(codexHome(), id, CODEX_OWNER_MAX_DAYS)))(threadId);
6041
+ } catch {
6042
+ file = null;
6043
+ }
6044
+ if (!file) return false;
6045
+ let head = null;
6046
+ try {
6047
+ head = await (d.readHead ?? readHead)(file);
6048
+ } catch {
6049
+ head = null;
6050
+ }
6051
+ return !!head && isChatgptAppRolloutHead(head);
6052
+ }
6053
+ var skipSystem = () => process.env.DESKBUDDY_SKIP_FOCUS_OPEN === "1";
6054
+ function execOk(file, args, timeoutMs = 1e4) {
6055
+ return new Promise((done) => {
6056
+ try {
6057
+ execFile3(file, args, { timeout: timeoutMs, windowsHide: true }, (err) => done(!err));
6058
+ } catch {
6059
+ done(false);
6060
+ }
6061
+ });
6062
+ }
6063
+ function defaultOpenUrl(url, platform = process.platform) {
6064
+ if (skipSystem()) return Promise.resolve(false);
6065
+ if (platform === "darwin") return execOk("open", [url]);
6066
+ if (platform === "win32") return execOk("rundll32.exe", ["url.dll,FileProtocolHandler", url]);
6067
+ return Promise.resolve(false);
6068
+ }
6069
+ function defaultProtocolRegistered(scheme) {
6070
+ if (skipSystem() || !/^[a-z][a-z0-9+.-]{0,31}$/.test(scheme)) return Promise.resolve(false);
6071
+ return execOk("reg.exe", ["query", `HKCR\\${scheme}`, "/v", "URL Protocol"], 5e3);
6072
+ }
6073
+ var idleWhy = (idle) => idle === null ? "\u8BFB\u4E0D\u5230\u952E\u9F20\u7A7A\u95F2\u65F6\u957F" : `\u7528\u6237\u6B63\u5728\u7528\u7535\u8111\uFF08${Math.round(idle / 1e3)} \u79D2\u524D\u78B0\u8FC7\u952E\u9F20\uFF09`;
6074
+ var DONE_MAX = 200;
6075
+ var IDLE_REFRESH_MAC_MS = 5 * 1e3;
6076
+ var IDLE_REFRESH_WIN_MS = 30 * 1e3;
6077
+ var IDLE_ACTIVE_WINDOW_MS = 10 * 60 * 1e3;
6078
+ var SessionFocuser = class {
6079
+ #d;
6080
+ #done = /* @__PURE__ */ new Set();
6081
+ /** 后台取到的键鼠空闲(opencode 切会话只读它,投递永不等取空闲,评审 73f95f0) */
6082
+ #idleCache = null;
6083
+ #lastInAppUse = Number.NEGATIVE_INFINITY;
6084
+ #idleTimer = null;
6085
+ #refreshing = false;
6086
+ constructor(d) {
6087
+ this.#d = d;
6088
+ }
6089
+ /** 刷新周期:Windows 30 秒,其余 5 秒 */
6090
+ idleRefreshMs() {
6091
+ return this.#platform() === "win32" ? IDLE_REFRESH_WIN_MS : IDLE_REFRESH_MAC_MS;
6092
+ }
6093
+ /**
6094
+ * 后台刷新一次空闲缓存:只在 focusSessionInApp 开着、且最近 10 分钟内有过 opencode 投递时取;同一时刻只取一次。
6095
+ * 不活跃了就停掉定时器(下次投递再起)
6096
+ */
6097
+ async refreshIdle() {
6098
+ if (this.#refreshing) return;
6099
+ if (!this.inAppEnabled() || this.#now() - this.#lastInAppUse > IDLE_ACTIVE_WINDOW_MS) {
6100
+ this.#stopIdleTimer();
6101
+ return;
6102
+ }
6103
+ this.#refreshing = true;
6104
+ try {
6105
+ const ms = await this.#idle();
6106
+ this.#idleCache = ms === null ? null : { ms, at: this.#now() };
6107
+ } finally {
6108
+ this.#refreshing = false;
6109
+ }
6110
+ }
6111
+ stop() {
6112
+ this.#stopIdleTimer();
6113
+ }
6114
+ #stopIdleTimer() {
6115
+ if (this.#idleTimer) clearInterval(this.#idleTimer);
6116
+ this.#idleTimer = null;
6117
+ }
6118
+ /** 缓存里的空闲:没有、或超过刷新周期 2 倍 → null(当读不到) */
6119
+ #cachedIdle() {
6120
+ const c = this.#idleCache;
6121
+ if (!c || this.#now() - c.at > 2 * this.idleRefreshMs()) return null;
6122
+ return c.ms;
6123
+ }
6124
+ /** opencode 这类 App 内切换(不激活)开着吗 */
6125
+ inAppEnabled() {
6126
+ return focusSettings(this.#cfg()).inApp;
6127
+ }
6128
+ /**
6129
+ * opencode 这类 App 内切换现在能不能做:开关开着,且用户没在用电脑(键鼠空闲 ≥ FOCUS_IDLE_MIN_MS;读不到也不切)。
6130
+ * 🔴 同步、只读后台缓存,绝不在投递路径上取空闲(Windows 要起 PowerShell)。缓存没有 / 过期 → 这次不切;
6131
+ * 同时记下「最近有 opencode 投递」并确保后台刷新在跑(第一次投递时还没有缓存,从下一条起生效)。跳过时记一行
6132
+ */
6133
+ inAppReady(instructionId, name = "opencode") {
6134
+ if (!this.inAppEnabled()) return false;
6135
+ this.#lastInAppUse = this.#now();
6136
+ if (!this.#idleTimer && this.#d.timers !== false) {
6137
+ void this.refreshIdle().catch(() => {
6138
+ });
6139
+ this.#idleTimer = setInterval(() => void this.refreshIdle().catch(() => {
6140
+ }), this.idleRefreshMs());
6141
+ this.#idleTimer.unref?.();
6142
+ }
6143
+ const idle = this.#cachedIdle();
6144
+ if (idle === null || idle < FOCUS_IDLE_MIN_MS) {
6145
+ this.#d.log(`\u6307\u4EE4 ${instructionId} \u4E0D\u5728 ${name} \u754C\u9762\u91CC\u5207\u4F1A\u8BDD\uFF1A${idleWhy(idle)}`);
6146
+ return false;
6147
+ }
6148
+ return true;
6149
+ }
6150
+ /** 深链切会话在这台电脑上会生效吗(开关 + 平台)。调用方据此决定要不要先做查找类的准备工作 */
6151
+ remoteEnabled() {
6152
+ const s2 = focusSettings(this.#cfg());
6153
+ if (!s2.onRemote) return false;
6154
+ const p = this.#platform();
6155
+ return p === "darwin" || p === "win32" && s2.onRemoteWindows;
6156
+ }
6157
+ /**
6158
+ * 设备回复 / 新建任务交出去之后调:开关、平台、id、用户在不在打字都过了才打开深链。永不抛。
6159
+ * instructionId 只用来去重和记日志;不记会话内容
6160
+ */
6161
+ async focus(instructionId, t) {
6162
+ if (this.#done.has(instructionId)) return "duplicate";
6163
+ const s2 = focusSettings(this.#cfg());
6164
+ if (!s2.onRemote) return "disabled";
6165
+ const p = this.#platform();
6166
+ if (p !== "darwin" && !(p === "win32" && s2.onRemoteWindows)) return "platform";
6167
+ const url = focusUrl(t);
6168
+ const name = TOOL_NAME[t.kind];
6169
+ if (!url) {
6170
+ this.#d.log(`\u6307\u4EE4 ${instructionId} \u5207\u5230 ${name} \u4F1A\u8BDD\uFF1A\u4F1A\u8BDD\u53F7\u4E0D\u5408\u6CD5\uFF0C\u4E0D\u5207`);
6171
+ return "bad_id";
6172
+ }
6173
+ this.#remember(instructionId);
6174
+ if (p === "win32") {
6175
+ let reg = false;
6176
+ try {
6177
+ reg = await (this.#d.protocolRegistered ?? defaultProtocolRegistered)(focusScheme(t));
6178
+ } catch {
6179
+ reg = false;
6180
+ }
6181
+ if (!reg) {
6182
+ this.#d.log(`\u6307\u4EE4 ${instructionId} \u5207\u5230 ${name} \u4F1A\u8BDD\u5931\u8D25\uFF1A\u672C\u673A\u6CA1\u6CE8\u518C ${focusScheme(t)}:// \u534F\u8BAE`);
6183
+ return "protocol_missing";
6184
+ }
6185
+ }
6186
+ const idle = await this.#idle();
6187
+ if (idle === null || idle < FOCUS_IDLE_MIN_MS) {
6188
+ this.#d.log(`\u6307\u4EE4 ${instructionId} \u4E0D\u5207\u5230 ${name} \u4F1A\u8BDD\uFF1A${idleWhy(idle)}`);
6189
+ return "user_active";
6190
+ }
6191
+ let ok = false;
6192
+ try {
6193
+ ok = await (this.#d.openUrl ?? ((u) => defaultOpenUrl(u, p)))(url);
6194
+ } catch {
6195
+ ok = false;
6196
+ }
6197
+ this.#d.log(ok ? `\u6307\u4EE4 ${instructionId} \u5DF2\u5207\u5230 ${name} \u4F1A\u8BDD\uFF08\u6DF1\u94FE\uFF09` : `\u6307\u4EE4 ${instructionId} \u5207\u5230 ${name} \u4F1A\u8BDD\u5931\u8D25\uFF1A\u6DF1\u94FE\u6CA1\u6253\u5F00`);
6198
+ return ok ? "opened" : "failed";
6199
+ }
6200
+ /** 键鼠空闲毫秒(Windows 起一次 PowerShell);取不到 null。永不抛 */
6201
+ async #idle() {
6202
+ const p = this.#platform();
6203
+ try {
6204
+ return await (this.#d.idleMs ?? (() => skipSystem() ? Promise.resolve(null) : userIdleMs(defaultExec, p)))();
6205
+ } catch {
6206
+ return null;
6207
+ }
6208
+ }
6209
+ #remember(id) {
6210
+ this.#done.add(id);
6211
+ if (this.#done.size > DONE_MAX) this.#done.delete(this.#done.values().next().value);
6212
+ }
6213
+ #cfg() {
6214
+ try {
6215
+ return (this.#d.config ?? loadConfig)();
6216
+ } catch {
6217
+ return {};
6218
+ }
6219
+ }
6220
+ #now() {
6221
+ return this.#d.now ? this.#d.now() : Date.now();
6222
+ }
6223
+ #platform() {
6224
+ return this.#d.platform ?? process.platform;
6225
+ }
6226
+ };
6227
+
5893
6228
  // src/daemon/qwenworkApp/watch.ts
5894
6229
  import { homedir as homedir4 } from "os";
5895
6230
  import { join as join15 } from "path";
@@ -10715,13 +11050,13 @@ function replyMac(secret, nonceC, r) {
10715
11050
  function startChannelServer(path, secret, onRequest, platform = process.platform, onAnswer) {
10716
11051
  return new Promise((resolve8) => {
10717
11052
  const server = createServer3((sock) => {
10718
- let open3 = true;
11053
+ let open4 = true;
10719
11054
  let phase = "hello";
10720
11055
  let nonceC = "";
10721
11056
  let nonceS = "";
10722
11057
  sock.on("error", () => {
10723
11058
  });
10724
- sock.on("close", () => open3 = false);
11059
+ sock.on("close", () => open4 = false);
10725
11060
  const authTimer = setTimeout(() => {
10726
11061
  if (phase === "hello") sock.destroy();
10727
11062
  }, 5e3);
@@ -10766,7 +11101,7 @@ function startChannelServer(path, secret, onRequest, platform = process.platform
10766
11101
  return;
10767
11102
  }
10768
11103
  phase = "done";
10769
- onRequest(req, reply, () => open3);
11104
+ onRequest(req, reply, () => open4);
10770
11105
  } catch {
10771
11106
  sock.destroy();
10772
11107
  }
@@ -11006,10 +11341,10 @@ function toolOfMessage(message) {
11006
11341
  }
11007
11342
  function pendingCall(text, toolName) {
11008
11343
  const { calls, done } = scanTranscript(text);
11009
- const open3 = calls.filter((c) => !done.has(c.callId));
11010
- const last = open3[open3.length - 1];
11344
+ const open4 = calls.filter((c) => !done.has(c.callId));
11345
+ const last = open4[open4.length - 1];
11011
11346
  if (!last || last.name !== toolName) return null;
11012
- if (open3.filter((c) => c.name === toolName).length > 1) return null;
11347
+ if (open4.filter((c) => c.name === toolName).length > 1) return null;
11013
11348
  return last;
11014
11349
  }
11015
11350
  function selectionsOf(input, lists) {
@@ -11088,17 +11423,17 @@ var WorkbuddyBridge = class {
11088
11423
  if (h.hook_event_name !== "Notification" || h.notification_type !== "permission_prompt") return;
11089
11424
  const sessionId = typeof h.session_id === "string" ? h.session_id : "";
11090
11425
  const windowId = typeof h.deskbuddy_window === "string" ? h.deskbuddy_window : "";
11091
- const transcript = typeof h.transcript_path === "string" ? h.transcript_path : "";
11092
- if (!sessionId || !/^[a-f0-9]{32}$/.test(windowId) || !transcript) return;
11426
+ const transcript2 = typeof h.transcript_path === "string" ? h.transcript_path : "";
11427
+ if (!sessionId || !/^[a-f0-9]{32}$/.test(windowId) || !transcript2) return;
11093
11428
  const rec = this.#window(windowId);
11094
11429
  if (!rec) return;
11095
11430
  const toolName = toolOfMessage(h.message);
11096
11431
  if (!toolName) return;
11097
- if (!(this.#d.transcriptOk ?? ((p) => transcriptAllowed(p)))(transcript)) {
11432
+ if (!(this.#d.transcriptOk ?? ((p) => transcriptAllowed(p)))(transcript2)) {
11098
11433
  this.#d.log(`WorkBuddy\uFF1A\u4F1A\u8BDD ${sessionId.slice(0, 8)} \u7684\u8BB0\u5F55\u4E0D\u5728 WorkBuddy \u914D\u7F6E\u76EE\u5F55\u4E0B\uFF0C\u4E0D\u63A8\u8BBE\u5907`);
11099
11434
  return;
11100
11435
  }
11101
- const text = this.#read(transcript);
11436
+ const text = this.#read(transcript2);
11102
11437
  const call = text ? pendingCall(text, toolName) : null;
11103
11438
  if (!call) {
11104
11439
  this.#d.log(`WorkBuddy\uFF1A\u4F1A\u8BDD ${sessionId.slice(0, 8)} \u7684 ${toolName} \u5728\u8BB0\u5F55\u91CC\u627E\u4E0D\u5230\u5F85\u7B54\u7684\u90A3\u4E00\u6B65\uFF0C\u4E0D\u63A8\u8BBE\u5907`);
@@ -11128,7 +11463,7 @@ var WorkbuddyBridge = class {
11128
11463
  this.#cards.delete(sessionId);
11129
11464
  old.ac.abort();
11130
11465
  }
11131
- const card = { key, sessionId, windowId, callId: call.callId, toolName, fingerprint: fingerprint2, input: call.input, transcript, ac: new AbortController(), misses: 0 };
11466
+ const card = { key, sessionId, windowId, callId: call.callId, toolName, fingerprint: fingerprint2, input: call.input, transcript: transcript2, ac: new AbortController(), misses: 0 };
11132
11467
  this.#cards.set(sessionId, card);
11133
11468
  this.#ensurePoll();
11134
11469
  const gateReq = {
@@ -11140,7 +11475,7 @@ var WorkbuddyBridge = class {
11140
11475
  cwd: typeof h.cwd === "string" ? h.cwd : rec.cwd,
11141
11476
  tool_name: toolName,
11142
11477
  tool_input: call.input,
11143
- transcript_path: transcript
11478
+ transcript_path: transcript2
11144
11479
  }
11145
11480
  };
11146
11481
  let reply;
@@ -11609,9 +11944,9 @@ var CodebuddyTerminal = class {
11609
11944
  const onAbort = () => ac.abort(signal.reason);
11610
11945
  signal.addEventListener("abort", onAbort, { once: true });
11611
11946
  let timer;
11612
- const transcript = sess ? sess.transcript : "";
11613
- if (transcript && (this.#d.transcriptOk ?? ((p) => transcriptAllowed(p)))(transcript)) {
11614
- const read = () => (this.#d.readTranscript ?? tail3)(transcript);
11947
+ const transcript2 = sess ? sess.transcript : "";
11948
+ if (transcript2 && (this.#d.transcriptOk ?? ((p) => transcriptAllowed(p)))(transcript2)) {
11949
+ const read = () => (this.#d.readTranscript ?? tail3)(transcript2);
11615
11950
  let callId = a ? a.toolUseId : "";
11616
11951
  if (!callId) {
11617
11952
  const t0 = read();
@@ -12416,7 +12751,7 @@ function pidAlive4(pid) {
12416
12751
  }
12417
12752
 
12418
12753
  // src/daemon/dumateApp/transport.ts
12419
- import { execFile as execFile3 } from "child_process";
12754
+ import { execFile as execFile4 } from "child_process";
12420
12755
  import { win32 as win323 } from "path";
12421
12756
 
12422
12757
  // src/daemon/dumateApp/page.ts
@@ -12636,7 +12971,7 @@ function isWinAppMain(r) {
12636
12971
  }
12637
12972
  function execOut(file, args, timeoutMs) {
12638
12973
  return new Promise((done) => {
12639
- execFile3(file, args, { timeout: timeoutMs, windowsHide: true, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }, (err, stdout6) => done(err ? null : String(stdout6 ?? "")));
12974
+ execFile4(file, args, { timeout: timeoutMs, windowsHide: true, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }, (err, stdout6) => done(err ? null : String(stdout6 ?? "")));
12640
12975
  });
12641
12976
  }
12642
12977
  async function defaultBackend(platform = process.platform) {
@@ -14181,6 +14516,7 @@ function codexQuestionInput(p) {
14181
14516
  var INJECT_VERSION = 2;
14182
14517
  var QUEUE_MAX2 = 500;
14183
14518
  var RESOLVED_MAX = 200;
14519
+ var SEEN_THREADS_MAX = 500;
14184
14520
  var KEEPER_EVERY_MS5 = 15 * 1e3;
14185
14521
  var LAUNCH_CHECK_EVERY_MS2 = 2 * 1e3;
14186
14522
  var CONNECT_BACKOFF_MIN_MS5 = 15 * 1e3;
@@ -14285,6 +14621,8 @@ var ChatgptAppBridge = class {
14285
14621
  #stopped = false;
14286
14622
  /** 会话 → 最近一次状态(thread/status/changed);有 active 的 = App 忙 */
14287
14623
  #status = /* @__PURE__ */ new Map();
14624
+ /** 页面里见过的线程(设备回复后切会话时判「这个线程属于 App」,spec 2026-10-13 focus-session §2.2);断开不清,最多 SEEN_THREADS_MAX 条 */
14625
+ #seenThreads = /* @__PURE__ */ new Set();
14288
14626
  constructor(deps2) {
14289
14627
  this.#d = deps2;
14290
14628
  }
@@ -14299,6 +14637,15 @@ var ChatgptAppBridge = class {
14299
14637
  for (const t of this.#status.values()) if (t === "active") return true;
14300
14638
  return false;
14301
14639
  }
14640
+ /** 页面桥收到过这个线程的动静吗(线程属于 ChatGPT App) */
14641
+ seenThread(threadId) {
14642
+ return this.#seenThreads.has(threadId);
14643
+ }
14644
+ #noteThread(threadId) {
14645
+ this.#seenThreads.delete(threadId);
14646
+ this.#seenThreads.add(threadId);
14647
+ if (this.#seenThreads.size > SEEN_THREADS_MAX) this.#seenThreads.delete(this.#seenThreads.values().next().value);
14648
+ }
14302
14649
  /** 等设备答复的卡有几张 */
14303
14650
  pending() {
14304
14651
  return this.#channel?.pending() ?? 0;
@@ -14437,11 +14784,14 @@ var ChatgptAppBridge = class {
14437
14784
  const id = req.id;
14438
14785
  if (typeof req.method !== "string" || !REQUEST_METHODS.includes(req.method)) return;
14439
14786
  if (typeof id !== "number" && typeof id !== "string") return;
14787
+ const tid = req.params && typeof req.params === "object" ? req.params.threadId : void 0;
14788
+ if (typeof tid === "string" && tid) this.#noteThread(tid);
14440
14789
  ch.onMessage({ id, method: req.method, params: req.params });
14441
14790
  return;
14442
14791
  }
14443
14792
  if (m.type === "mcp-notification" && typeof m.method === "string") {
14444
14793
  const p = m.params && typeof m.params === "object" ? m.params : {};
14794
+ if (typeof p.threadId === "string" && p.threadId) this.#noteThread(p.threadId);
14445
14795
  if (m.method === "thread/status/changed" && typeof p.threadId === "string") {
14446
14796
  const type2 = (p.status ?? {}).type;
14447
14797
  if (typeof type2 === "string") this.#status.set(p.threadId, type2);
@@ -14821,10 +15171,10 @@ function codexInterruptedAt(file) {
14821
15171
  }
14822
15172
 
14823
15173
  // src/daemon/qoderAppDb.ts
14824
- import { execFile as execFile4 } from "child_process";
15174
+ import { execFile as execFile5 } from "child_process";
14825
15175
  import { createHash as createHash3 } from "crypto";
14826
15176
  import { existsSync as existsSync6 } from "fs";
14827
- import { access, open as open2, readdir, stat } from "fs/promises";
15177
+ import { access, open as open3, readdir as readdir2, stat } from "fs/promises";
14828
15178
  import { createRequire } from "module";
14829
15179
  import { basename as basename8, dirname as dirname7, isAbsolute as isAbsolute3, join as join24 } from "path";
14830
15180
  var QODER_APP_TOOLS = ["qoder_cn", "qoder"];
@@ -14906,7 +15256,7 @@ async function findTranscript(configDir, sessionId) {
14906
15256
  const root = join24(configDir, "projects");
14907
15257
  let dirs;
14908
15258
  try {
14909
- dirs = await readdir(root);
15259
+ dirs = await readdir2(root);
14910
15260
  } catch {
14911
15261
  return null;
14912
15262
  }
@@ -14922,7 +15272,7 @@ async function findTranscript(configDir, sessionId) {
14922
15272
  }
14923
15273
  var SLICE = 64 * 1024;
14924
15274
  async function readSlice(p, from) {
14925
- const fh = await open2(p, "r");
15275
+ const fh = await open3(p, "r");
14926
15276
  try {
14927
15277
  const size = (await fh.stat()).size;
14928
15278
  const len = Math.min(size, SLICE);
@@ -15004,7 +15354,7 @@ var defaultQoderAppDeps = () => ({
15004
15354
  },
15005
15355
  runElectron: (bin, script, input) => new Promise((resolve8, reject) => {
15006
15356
  const env = { ...process.env, ELECTRON_RUN_AS_NODE: "1" };
15007
- const c = execFile4(
15357
+ const c = execFile5(
15008
15358
  bin,
15009
15359
  ["-e", script],
15010
15360
  { env, timeout: 15e3, windowsHide: true, maxBuffer: 1024 * 1024 },
@@ -15889,7 +16239,7 @@ function scheduleCcTemplateCheck(deps2 = defaultCcCheckDeps, delayMs = CC_CHECK_
15889
16239
  }
15890
16240
 
15891
16241
  // src/daemon/opencodeVersion.ts
15892
- import { execFile as execFile5 } from "child_process";
16242
+ import { execFile as execFile6 } from "child_process";
15893
16243
  import { statSync as statSync15 } from "fs";
15894
16244
  var OPENCODE_PROMPT_MIN_VERSION = "1.2.25";
15895
16245
  function parseOpencodeVersion(out) {
@@ -15960,7 +16310,7 @@ var defaultOpencodeVersion = createOpencodeVersion({
15960
16310
  },
15961
16311
  run: (loc) => new Promise((resolve8) => {
15962
16312
  try {
15963
- execFile5(
16313
+ execFile6(
15964
16314
  loc.file,
15965
16315
  [...loc.prefixArgs, "--version"],
15966
16316
  { encoding: "utf8", windowsHide: true, timeout: PROBE_TIMEOUT_MS, env: { ...scrubAgentEnv({ ...process.env }), ...loc.env ?? {} } },
@@ -16559,7 +16909,9 @@ async function dshDeliver(sessionId, msg, d0 = {}) {
16559
16909
  text: msg.text,
16560
16910
  at: writtenAt,
16561
16911
  ...d0.addressed ? { pid: hb.pid } : {},
16562
- ...msg.summary ? { summary: true, replyWaitMs: msg.replyWaitMs ?? DSH_REPLY_WAIT_MS } : {}
16912
+ ...msg.summary ? { summary: true, replyWaitMs: msg.replyWaitMs ?? DSH_REPLY_WAIT_MS } : {},
16913
+ // opencode:投递成功后在终端界面里切到这个会话(spec 2026-10-13 focus-session §1);dsh 插件不认这个字段
16914
+ ...msg.focus ? { focus: true } : {}
16563
16915
  });
16564
16916
  const sleep3 = d0.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
16565
16917
  let deadline = writtenAt + (d0.timeoutMs ?? DSH_ACK_TIMEOUT_MS);
@@ -16960,7 +17312,9 @@ var Daemon = class {
16960
17312
  if (offer) void offer.maybeAsk("runtime").catch(() => {
16961
17313
  });
16962
17314
  }),
16963
- workbuddyAppSync: deps2.workbuddyAppSync ?? ((job) => registerWorkbuddySession(job))
17315
+ workbuddyAppSync: deps2.workbuddyAppSync ?? ((job) => registerWorkbuddySession(job)),
17316
+ focuser: deps2.focuser ?? new SessionFocuser({ log }),
17317
+ codexInApp: deps2.codexInApp ?? ((tid) => codexThreadInChatgptApp(tid, { seenByApp: (id) => this.#chatgptApp?.seenThread(id) === true }))
16964
17318
  };
16965
17319
  this.#wake = new WakeHub({
16966
17320
  now: this.#deps.now,
@@ -17623,6 +17977,7 @@ var Daemon = class {
17623
17977
  stop() {
17624
17978
  this.#stopped = true;
17625
17979
  this.#clearIdle();
17980
+ this.#deps.focuser.stop?.();
17626
17981
  if (this.#windowScan) clearInterval(this.#windowScan);
17627
17982
  if (this.#aliveReport) clearInterval(this.#aliveReport);
17628
17983
  if (this.#memLog) clearInterval(this.#memLog);
@@ -18077,7 +18432,10 @@ var Daemon = class {
18077
18432
  */
18078
18433
  #toWbApp(cmd, sid, cwd) {
18079
18434
  const key = sid ? `wb:${sid}` : `wbnew:${cmd.instructionId}`;
18080
- const report = (status, sessionId, reason) => this.#report(cmd, sessionId, status, "window", reason ?? "");
18435
+ const report = (status, sessionId, reason) => {
18436
+ this.#report(cmd, sessionId, status, "window", reason ?? "");
18437
+ if (status === "started" && sessionId) this.#focusAfter(cmd, () => ({ kind: "workbuddy", sessionId }));
18438
+ };
18081
18439
  const run = async () => {
18082
18440
  let r;
18083
18441
  try {
@@ -18445,14 +18803,16 @@ var Daemon = class {
18445
18803
  */
18446
18804
  async #toOcWindow(cmd, sid) {
18447
18805
  let ack;
18806
+ const focus = !isSummary(cmd) && this.#focusInApp(cmd);
18448
18807
  try {
18449
- ack = await this.#deps.ocDeliver(sid, { instructionId: cmd.instructionId, text: cmd.text, ...isSummary(cmd) ? { summary: true } : {} });
18808
+ ack = await this.#deps.ocDeliver(sid, { instructionId: cmd.instructionId, text: cmd.text, ...isSummary(cmd) ? { summary: true } : {}, ...focus ? { focus: true } : {} });
18450
18809
  } catch (e) {
18451
18810
  log(`\u6307\u4EE4 ${cmd.instructionId} \u6295\u9012\u7ED9 opencode \u51FA\u9519\uFF1A${e.message}`);
18452
18811
  ack = { ok: false, reason: "deliver_error" };
18453
18812
  }
18454
18813
  if (ack.ok) {
18455
18814
  log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u7ECF\u63D2\u4EF6\u53D1\u8FDB opencode \u4F1A\u8BDD ${sid.slice(0, 12)}${ack.busy ? "\uFF08\u6B63\u5FD9\uFF0C\u6392\u8FDB\u4E0B\u4E00\u8F6E\uFF09" : ""} len=${cmd.text.length}`);
18815
+ if (focus) log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u8BF7\u63D2\u4EF6\u5728 opencode \u754C\u9762\u91CC\u5207\u5230\u8FD9\u4E2A\u4F1A\u8BDD\uFF08\u4F4E\u7248\u672C\u63D2\u4EF6\u81EA\u5DF1\u8DF3\u8FC7\uFF09`);
18456
18816
  if (isSummary(cmd)) this.#summaryWatch(cmd, sid, "ocMailbox");
18457
18817
  this.#report(cmd, sid, ack.busy ? "queued" : "started", "window");
18458
18818
  return;
@@ -18536,6 +18896,12 @@ var Daemon = class {
18536
18896
  log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u53D1\u8FDB${name}\u4F1A\u8BDD ${sid.slice(0, 12)}${r.status === "queued" ? "\uFF08\u6B63\u5FD9\uFF0C\u6392\u8FDB\u4E0B\u4E00\u8F6E\uFF09" : ""} len=${cmd.text.length}`);
18537
18897
  if (isSummary(cmd) && tool === "dumate") this.#summaryWatch(cmd, sid, "dumateHttp");
18538
18898
  this.#report(cmd, sid, r.status, "window");
18899
+ if (tool === "qwenwork")
18900
+ this.#focusAfter(cmd, () => {
18901
+ const chatId = qwenworkChatIdOfSession(sid, readState(sid)?.cwd ?? "");
18902
+ if (!chatId) log(`\u6307\u4EE4 ${cmd.instructionId} \u5343\u95EE\u529E\u516C\u4F1A\u8BDD ${sid.slice(0, 8)} \u53CD\u67E5\u4E0D\u5230\u4EFB\u52A1\u53F7\uFF0C\u4E0D\u5207`);
18903
+ return chatId ? { kind: "qwenwork", chatId } : null;
18904
+ });
18539
18905
  }
18540
18906
  /** 扣子:设备回复写进 App 里的会话。started(mode=window);确定没发 → rejected + 原因;可能已发 → failed/window(🔴 不重发) */
18541
18907
  async #toCoze(cmd, sid) {
@@ -18609,6 +18975,30 @@ var Daemon = class {
18609
18975
  }
18610
18976
  log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u5728\u5343\u95EE\u529E\u516C\u91CC\u5EFA\u4EFB\u52A1 ${r.chatId.slice(0, 8)}${r.sessionId ? ` \u4F1A\u8BDD ${r.sessionId.slice(0, 12)}` : ""} len=${cmd.text.length}`);
18611
18977
  this.#report(cmd, r.sessionId, "started", "window");
18978
+ const created = r;
18979
+ this.#focusAfter(cmd, () => ({ kind: "qwenwork", chatId: created.chatId, ...created.subChatId ? { subChatId: created.subChatId } : {} }));
18980
+ }
18981
+ /**
18982
+ * 设备回复 / 新建任务交出去之后切到该会话(spec 2026-10-13 focus-session):只在这里(设备指令的分发路径)调,本机用户自己发起的不会走到。
18983
+ * 深链开关没开 / 平台不支持时不做反查等准备工作;任务总结不切。不阻塞回报、永不抛
18984
+ */
18985
+ #focusAfter(cmd, target) {
18986
+ if (isSummary(cmd)) return;
18987
+ try {
18988
+ if (!this.#deps.focuser.remoteEnabled()) return;
18989
+ } catch {
18990
+ return;
18991
+ }
18992
+ void Promise.resolve().then(target).then((t) => t ? this.#deps.focuser.focus(cmd.instructionId, t) : null).catch(() => {
18993
+ });
18994
+ }
18995
+ /** opencode 这类 App 内切会话(不激活):开关开着且用户没在用电脑 */
18996
+ #focusInApp(cmd) {
18997
+ try {
18998
+ return this.#deps.focuser.inAppReady(cmd.instructionId);
18999
+ } catch {
19000
+ return false;
19001
+ }
18612
19002
  }
18613
19003
  /** 回报一条指令的处理结果。首发 2 秒超时,失败按退避重发结果(ResultReporter,不重新执行);seen 去重命中的不会走到这里 */
18614
19004
  /** 指令结果回报:失败按退避重发(只重发结果、不重新执行),同一条指令只留最新一份 */
@@ -18627,6 +19017,12 @@ var Daemon = class {
18627
19017
  at: String(this.#deps.now())
18628
19018
  };
18629
19019
  if (status === "done") payload.output = clipOutput(output ?? "");
19020
+ if (cmd.tool === "codex" && cmd.sessionId && sessionId && (status === "started" || status === "queued"))
19021
+ this.#focusAfter(cmd, async () => {
19022
+ if (await this.#deps.codexInApp(sessionId)) return { kind: "codex", threadId: sessionId };
19023
+ log(`\u6307\u4EE4 ${cmd.instructionId} Codex \u7EBF\u7A0B ${sessionId.slice(0, 8)} \u4E0D\u662F ChatGPT App \u91CC\u7684\uFF0C\u4E0D\u5207`);
19024
+ return null;
19025
+ });
18630
19026
  if (isSummary(cmd) && status !== "started" && status !== "queued") this.#ptyWatch.drop(cmd.instructionId);
18631
19027
  if (isSummary(cmd) && (status === "started" || status === "queued") && mode === "window" && SUMMARY_TRANSCRIPT_TOOLS.has(cmd.tool) && !this.#ptyWatch.has(cmd.instructionId))
18632
19028
  this.#summaryWatch(cmd, sessionId);
@@ -22356,7 +22752,7 @@ async function runWorkbuddySetup(opts = {}) {
22356
22752
  }
22357
22753
 
22358
22754
  // src/cli.ts
22359
- var USAGE = `deskbuddy \u2014\u2014 \u628A AI \u7F16\u7801\u667A\u80FD\u4F53\u6BCF\u4E2A\u4F1A\u8BDD\u7A97\u53E3\u7684\u72B6\u6001\u540C\u6B65\u5230\u684C\u9762\u786C\u4EF6\u5C4F\u5E55
22755
+ var USAGE2 = `deskbuddy \u2014\u2014 \u628A AI \u7F16\u7801\u667A\u80FD\u4F53\u6BCF\u4E2A\u4F1A\u8BDD\u7A97\u53E3\u7684\u72B6\u6001\u540C\u6B65\u5230\u684C\u9762\u786C\u4EF6\u5C4F\u5E55
22360
22756
 
22361
22757
  \u7528\u6CD5:
22362
22758
  deskbuddy install --code <\u5B89\u88C5\u7801> \u4E00\u952E\u63A5\u5165\uFF1A\u7528 App\u300C\u6DFB\u52A0\u7535\u8111\u300D\u7ED9\u7684\u5B89\u88C5\u7801\u7ED1\u5B9A\u8D26\u53F7\uFF0C
@@ -22371,6 +22767,9 @@ var USAGE = `deskbuddy \u2014\u2014 \u628A AI \u7F16\u7801\u667A\u80FD\u4F53\u6B
22371
22767
  deskbuddy status \u770B\u91C7\u96C6\u5230\u4E86\u4EC0\u4E48\u3001\u4E0A\u62A5\u4E86\u54EA\u4E9B\u5B57\u6BB5
22372
22768
  deskbuddy logout [--yes] \u79FB\u9664 hook \u5E76\u6E05\u9664\u672C\u673A\u6570\u636E
22373
22769
  deskbuddy config set transcript on|off \u4F1A\u8BDD\u5BF9\u8BDD\u540C\u6B65\u7684\u672C\u5730\u5F00\u5173\uFF08\u9ED8\u8BA4 on\uFF1Boff \u540E\u4E0D\u4E0A\u4F20\u4EFB\u4F55\u5BF9\u8BDD\u6B63\u6587\uFF09
22770
+ deskbuddy config set <\u5F00\u5173> true|false \u8BBE\u5907\u56DE\u590D / \u65B0\u5EFA\u4EFB\u52A1\u540E\u5207\u5230\u8BE5\u4F1A\u8BDD\uFF1AfocusSessionOnRemote\uFF08\u6DF1\u94FE\u628A
22771
+ App \u8C03\u5230\u524D\u53F0\uFF0C\u9ED8\u8BA4\u5173\uFF09\u3001focusSessionInApp\uFF08opencode \u5185\u5207\u6362\uFF0C\u9ED8\u8BA4\u5F00\uFF09\u3001
22772
+ focusSessionOnRemoteWindows\uFF08Windows \u4E5F\u7528\u6DF1\u94FE\uFF0C\u9ED8\u8BA4\u5173\uFF09
22374
22773
  deskbuddy workbuddy-setup [--yes] \u5B89\u88C5 WorkBuddy \u7684\u72EC\u7ACB\u7248\u547D\u4EE4\u884C\uFF08CodeBuddy Code\uFF09\uFF0C\u88C5\u4E86\u624D\u80FD\u5728
22375
22774
  \u8BBE\u5907\u4E0A\u65B0\u5EFA / \u56DE\u590D WorkBuddy \u4EFB\u52A1\uFF08--yes \u4E0D\u5F39\u6846\u76F4\u63A5\u88C5\uFF09
22376
22775
  deskbuddy wrap <tool> [\u53C2\u6570\u2026] \uFF08\u57AB\u7247\u5185\u90E8\u7528\uFF09\u7ECF DeskBuddy \u6253\u5F00\u667A\u80FD\u4F53\u7A97\u53E3\uFF0C
@@ -22395,7 +22794,7 @@ async function main(argv) {
22395
22794
  case "-h":
22396
22795
  case "--help":
22397
22796
  case "help":
22398
- process.stdout.write(USAGE);
22797
+ process.stdout.write(USAGE2);
22399
22798
  return 0;
22400
22799
  case "-v":
22401
22800
  case "--version":
@@ -22426,7 +22825,7 @@ async function main(argv) {
22426
22825
  default:
22427
22826
  process.stderr.write(`\u672A\u77E5\u547D\u4EE4\uFF1A${cmd}
22428
22827
 
22429
- ${USAGE}`);
22828
+ ${USAGE2}`);
22430
22829
  return 1;
22431
22830
  }
22432
22831
  }
@@ -5,7 +5,7 @@ import {
5
5
  installTools,
6
6
  parseTools,
7
7
  runEnable
8
- } from "./chunk-Y6D6FMMF.js";
8
+ } from "./chunk-JPCZVBK6.js";
9
9
  export {
10
10
  defaultShimEnvForInstall,
11
11
  installShims,
@@ -134,11 +134,27 @@ export const OC_PERMISSION_REPLY_MIN = '1.18.32'
134
134
  * 版本读不出 / 格式不对 / 低于已验证版本 → false
135
135
  */
136
136
  export function supportsPermissionReply(version) {
137
+ return ocVersionAtLeast(version, OC_PERMISSION_REPLY_MIN)
138
+ }
139
+
140
+ /**
141
+ * `POST /tui/select-session`(事件 tui.session.select,让终端界面切到某个会话)最早出现的版本:
142
+ * 逐版核对 npm 包 @opencode-ai/sdk 的生成代码,1.0.224 还没有、1.1.1 起有(spec 2026-10-13 focus-session §1)
143
+ */
144
+ export const OC_SELECT_SESSION_MIN = '1.1.1'
145
+
146
+ /** 这个 opencode 版本有没有 /tui/select-session。读不出 / 格式不对 / 低于下限 → false(不认识的路由可能被转发到 app.opencode.ai,不能试探着打) */
147
+ export function supportsSelectSession(version) {
148
+ return ocVersionAtLeast(version, OC_SELECT_SESSION_MIN)
149
+ }
150
+
151
+ /** version ≥ min(只比 主.次.修订);读不出 → false */
152
+ export function ocVersionAtLeast(version, min) {
137
153
  const m = typeof version === 'string' ? version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)/) : null
138
154
  if (!m) return false
139
- const min = OC_PERMISSION_REPLY_MIN.split('.').map(Number)
155
+ const lo = String(min).split('.').map(Number)
140
156
  const v = [Number(m[1]), Number(m[2]), Number(m[3])]
141
- for (let i = 0; i < 3; i++) if (v[i] !== min[i]) return v[i] > min[i]
157
+ for (let i = 0; i < 3; i++) if (v[i] !== lo[i]) return v[i] > lo[i]
142
158
  return true
143
159
  }
144
160
 
package/dist/opencode.mjs CHANGED
@@ -25,10 +25,10 @@ import { createHash, randomBytes } from 'node:crypto'
25
25
  import { appendFileSync, chmodSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
26
26
  import { dirname, join } from 'node:path'
27
27
  import { fileURLToPath } from 'node:url'
28
- import { alwaysSuggestions, askToCc, isPlainQuestion, ocReplyOf, questionToCc, rememberTitle, supportsPermissionReply, toCcTool } from './ocTranslate.mjs'
28
+ import { alwaysSuggestions, askToCc, isPlainQuestion, ocReplyOf, questionToCc, rememberTitle, supportsPermissionReply, supportsSelectSession, toCcTool } from './ocTranslate.mjs'
29
29
 
30
30
  // 翻译函数搬到了 ocTranslate.mjs(与 DumateWatch 共用),这里转出去保持原来的导出
31
- export { alwaysSuggestions, askToCc, isPlainQuestion, ocReplyOf, questionToCc, rememberTitle, supportsPermissionReply, toCcTool }
31
+ export { alwaysSuggestions, askToCc, isPlainQuestion, ocReplyOf, questionToCc, rememberTitle, supportsPermissionReply, supportsSelectSession, toCcTool }
32
32
 
33
33
  const HERE = dirname(fileURLToPath(import.meta.url))
34
34
  const HOOK = join(HERE, 'hook.cjs')
@@ -322,9 +322,48 @@ export function startOcMailbox(opts) {
322
322
  }
323
323
  void tick()
324
324
  }
325
+ /**
326
+ * 设备回复发进来之后在终端界面里切到这个会话(spec 2026-10-13 focus-session §1):不激活窗口、不抢焦点,只换 TUI 里显示的会话。
327
+ * 版本门控:先问一次 /global/health 的版本(进程内记住),低于 OC_SELECT_SESSION_MIN 或读不出就不打这个路由(不认识的路由可能被转发出去)
328
+ */
329
+ let ocVersion = null // null = 还没问到
330
+ const http = () => client && (client._client || client.client)
331
+ const selectSession =
332
+ opts.selectSession ||
333
+ (async (sid, d) => {
334
+ const h = http()
335
+ if (ocVersion === null) {
336
+ try {
337
+ const r = h && typeof h.get === 'function' ? await withTimeout(h.get({ url: '/global/health' }), OC_STATUS_WAIT_MS) : null
338
+ // 超时 / 出错不记,下次再问
339
+ if (r && r !== 'timeout') ocVersion = r.data && typeof r.data.version === 'string' ? r.data.version : ''
340
+ } catch (_) {
341
+ /* 下次再问 */
342
+ }
343
+ }
344
+ if (!supportsSelectSession(ocVersion)) {
345
+ log(`opencode 版本 ${ocVersion || '读不出'}:没有 /tui/select-session,不切会话`)
346
+ return 'old_version'
347
+ }
348
+ if (!h || typeof h.post !== 'function') return 'failed'
349
+ try {
350
+ const r = await withTimeout(
351
+ h.post({ url: '/tui/select-session', query: { directory: d }, body: { sessionID: sid }, headers: { 'Content-Type': 'application/json' } }),
352
+ OC_PROMPT_WAIT_MS
353
+ )
354
+ if (r === 'timeout' || !sdkOk(r)) {
355
+ log(`切到会话 ${sid.slice(0, 12)} 失败:${r === 'timeout' ? '超时' : `status=${(r && r.response && r.response.status) || '?'}`}`)
356
+ return 'failed'
357
+ }
358
+ return 'ok'
359
+ } catch (e) {
360
+ log(`切到会话 ${sid.slice(0, 12)} 出错:${(e && e.message) || e}`)
361
+ return 'failed'
362
+ }
363
+ })
325
364
  /** 会话 → 这个会话上一封信的处理链:同一会话的信串行发,前一封 promptAsync 返回了才发下一封(保证先后) */
326
365
  const chains = new Map()
327
- const enqueue = (sid, iid, text, at, replyWaitMs) => {
366
+ const enqueue = (sid, iid, text, at, replyWaitMs, focus) => {
328
367
  const prev = chains.get(sid) || Promise.resolve()
329
368
  const next = prev
330
369
  .then(() => deliver(sid, text, at + OC_ACK_BUDGET_MS))
@@ -332,6 +371,8 @@ export function startOcMailbox(opts) {
332
371
  (ack) => {
333
372
  writeAck(iid, ack)
334
373
  if (ack && ack.ok && replyWaitMs > 0) watchReply(sid, iid, at + replyWaitMs)
374
+ // 回执先写(不拖慢 daemon),再切会话;切不了不影响这一轮
375
+ if (ack && ack.ok && focus) void selectSession(sid, dirs.get(sid) || directory).catch(() => {})
335
376
  },
336
377
  () => writeAck(iid, { ok: false, reason: 'prompt_failed' })
337
378
  )
@@ -389,10 +430,12 @@ export function startOcMailbox(opts) {
389
430
  continue
390
431
  }
391
432
  const replyWaitMs = letter.summary === true && typeof letter.replyWaitMs === 'number' && letter.replyWaitMs > 0 ? Math.min(letter.replyWaitMs, OC_REPLY_MAX_MS) : 0
392
- got.push({ sid, iid, at, text: typeof letter.text === 'string' ? letter.text : '', replyWaitMs })
433
+ // 任务总结不切会话(daemon 也不会给总结带 focus,这里再兜一层)
434
+ const focus = letter.focus === true && letter.summary !== true
435
+ got.push({ sid, iid, at, text: typeof letter.text === 'string' ? letter.text : '', replyWaitMs, focus })
393
436
  }
394
437
  got.sort((a, b) => a.at - b.at)
395
- for (const x of got) enqueue(x.sid, x.iid, x.text, x.at, x.replyWaitMs)
438
+ for (const x of got) enqueue(x.sid, x.iid, x.text, x.at, x.replyWaitMs, x.focus)
396
439
  }
397
440
  let t1 = null
398
441
  let t2 = null
@@ -536,3 +536,15 @@
536
536
  2026-10-13T08:46:16.267Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
537
537
  2026-10-13T08:46:16.267Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
538
538
  2026-10-13T08:46:21.157Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
539
+ 2026-10-13T09:14:17.159Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
540
+ 2026-10-13T09:14:22.181Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
541
+ 2026-10-13T09:14:22.181Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
542
+ 2026-10-13T09:14:22.290Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
543
+ 2026-10-13T09:14:22.291Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
544
+ 2026-10-13T09:14:25.500Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
545
+ 2026-10-13T10:34:12.495Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
546
+ 2026-10-13T10:34:17.855Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
547
+ 2026-10-13T10:34:17.856Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
548
+ 2026-10-13T10:34:18.075Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
549
+ 2026-10-13T10:34:18.075Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
550
+ 2026-10-13T10:34:22.944Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
@@ -7,3 +7,6 @@ export function questionToCc(props: unknown): { tool_name: string; tool_input: {
7
7
  export function rememberTitle(titles: Map<string, string>, info: unknown): void
8
8
  export function ocReplyOf(out: unknown): { reply: 'once' | 'always' | 'reject'; message?: string } | null
9
9
  export function supportsPermissionReply(version: unknown): boolean
10
+ export const OC_SELECT_SESSION_MIN: string
11
+ export function supportsSelectSession(version: unknown): boolean
12
+ export function ocVersionAtLeast(version: unknown, min: string): boolean
@@ -134,11 +134,27 @@ export const OC_PERMISSION_REPLY_MIN = '1.18.32'
134
134
  * 版本读不出 / 格式不对 / 低于已验证版本 → false
135
135
  */
136
136
  export function supportsPermissionReply(version) {
137
+ return ocVersionAtLeast(version, OC_PERMISSION_REPLY_MIN)
138
+ }
139
+
140
+ /**
141
+ * `POST /tui/select-session`(事件 tui.session.select,让终端界面切到某个会话)最早出现的版本:
142
+ * 逐版核对 npm 包 @opencode-ai/sdk 的生成代码,1.0.224 还没有、1.1.1 起有(spec 2026-10-13 focus-session §1)
143
+ */
144
+ export const OC_SELECT_SESSION_MIN = '1.1.1'
145
+
146
+ /** 这个 opencode 版本有没有 /tui/select-session。读不出 / 格式不对 / 低于下限 → false(不认识的路由可能被转发到 app.opencode.ai,不能试探着打) */
147
+ export function supportsSelectSession(version) {
148
+ return ocVersionAtLeast(version, OC_SELECT_SESSION_MIN)
149
+ }
150
+
151
+ /** version ≥ min(只比 主.次.修订);读不出 → false */
152
+ export function ocVersionAtLeast(version, min) {
137
153
  const m = typeof version === 'string' ? version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)/) : null
138
154
  if (!m) return false
139
- const min = OC_PERMISSION_REPLY_MIN.split('.').map(Number)
155
+ const lo = String(min).split('.').map(Number)
140
156
  const v = [Number(m[1]), Number(m[2]), Number(m[3])]
141
- for (let i = 0; i < 3; i++) if (v[i] !== min[i]) return v[i] > min[i]
157
+ for (let i = 0; i < 3; i++) if (v[i] !== lo[i]) return v[i] > lo[i]
142
158
  return true
143
159
  }
144
160
 
@@ -14,6 +14,7 @@ export function questionToCc(props: unknown): { tool_name: string; tool_input: {
14
14
  export function isPlainQuestion(props: unknown, callTool: unknown): boolean
15
15
  export function ocReplyOf(out: unknown): { reply: 'once' | 'always' | 'reject'; message?: string } | null
16
16
  export function supportsPermissionReply(version: unknown): boolean
17
+ export function supportsSelectSession(version: unknown): boolean
17
18
  export const OC_STALE_LETTER_MS: number
18
19
  export const OC_LETTER_RE: RegExp
19
20
  export const OC_MAILBOX_KEY: symbol
@@ -29,7 +30,17 @@ export interface OcMailboxHandle {
29
30
  pid: number
30
31
  token: string
31
32
  }
32
- export function startOcMailbox(opts: { client: unknown; directory?: string; home?: string; pid?: number; now?: () => number; log?: (m: string) => void; timers?: boolean }): OcMailboxHandle
33
+ export function startOcMailbox(opts: {
34
+ client: unknown
35
+ directory?: string
36
+ home?: string
37
+ pid?: number
38
+ now?: () => number
39
+ log?: (m: string) => void
40
+ timers?: boolean
41
+ /** 投递成功后在终端界面里切到这个会话(测试注入;缺省先问 /global/health 的版本,够版本才打 /tui/select-session) */
42
+ selectSession?: (sessionId: string, directory: string) => Promise<'ok' | 'old_version' | 'failed'>
43
+ }): OcMailboxHandle
33
44
  export function ensureOcMailbox(ctx: unknown, g?: Record<symbol, unknown>, log?: (m: string) => void): OcMailboxHandle | null
34
45
  export const OC_ACK_BUDGET_MS: number
35
46
  export function isTuiProcess(argv?: string[]): boolean
package/hook/opencode.mjs CHANGED
@@ -25,10 +25,10 @@ import { createHash, randomBytes } from 'node:crypto'
25
25
  import { appendFileSync, chmodSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
26
26
  import { dirname, join } from 'node:path'
27
27
  import { fileURLToPath } from 'node:url'
28
- import { alwaysSuggestions, askToCc, isPlainQuestion, ocReplyOf, questionToCc, rememberTitle, supportsPermissionReply, toCcTool } from './ocTranslate.mjs'
28
+ import { alwaysSuggestions, askToCc, isPlainQuestion, ocReplyOf, questionToCc, rememberTitle, supportsPermissionReply, supportsSelectSession, toCcTool } from './ocTranslate.mjs'
29
29
 
30
30
  // 翻译函数搬到了 ocTranslate.mjs(与 DumateWatch 共用),这里转出去保持原来的导出
31
- export { alwaysSuggestions, askToCc, isPlainQuestion, ocReplyOf, questionToCc, rememberTitle, supportsPermissionReply, toCcTool }
31
+ export { alwaysSuggestions, askToCc, isPlainQuestion, ocReplyOf, questionToCc, rememberTitle, supportsPermissionReply, supportsSelectSession, toCcTool }
32
32
 
33
33
  const HERE = dirname(fileURLToPath(import.meta.url))
34
34
  const HOOK = join(HERE, 'hook.cjs')
@@ -322,9 +322,48 @@ export function startOcMailbox(opts) {
322
322
  }
323
323
  void tick()
324
324
  }
325
+ /**
326
+ * 设备回复发进来之后在终端界面里切到这个会话(spec 2026-10-13 focus-session §1):不激活窗口、不抢焦点,只换 TUI 里显示的会话。
327
+ * 版本门控:先问一次 /global/health 的版本(进程内记住),低于 OC_SELECT_SESSION_MIN 或读不出就不打这个路由(不认识的路由可能被转发出去)
328
+ */
329
+ let ocVersion = null // null = 还没问到
330
+ const http = () => client && (client._client || client.client)
331
+ const selectSession =
332
+ opts.selectSession ||
333
+ (async (sid, d) => {
334
+ const h = http()
335
+ if (ocVersion === null) {
336
+ try {
337
+ const r = h && typeof h.get === 'function' ? await withTimeout(h.get({ url: '/global/health' }), OC_STATUS_WAIT_MS) : null
338
+ // 超时 / 出错不记,下次再问
339
+ if (r && r !== 'timeout') ocVersion = r.data && typeof r.data.version === 'string' ? r.data.version : ''
340
+ } catch (_) {
341
+ /* 下次再问 */
342
+ }
343
+ }
344
+ if (!supportsSelectSession(ocVersion)) {
345
+ log(`opencode 版本 ${ocVersion || '读不出'}:没有 /tui/select-session,不切会话`)
346
+ return 'old_version'
347
+ }
348
+ if (!h || typeof h.post !== 'function') return 'failed'
349
+ try {
350
+ const r = await withTimeout(
351
+ h.post({ url: '/tui/select-session', query: { directory: d }, body: { sessionID: sid }, headers: { 'Content-Type': 'application/json' } }),
352
+ OC_PROMPT_WAIT_MS
353
+ )
354
+ if (r === 'timeout' || !sdkOk(r)) {
355
+ log(`切到会话 ${sid.slice(0, 12)} 失败:${r === 'timeout' ? '超时' : `status=${(r && r.response && r.response.status) || '?'}`}`)
356
+ return 'failed'
357
+ }
358
+ return 'ok'
359
+ } catch (e) {
360
+ log(`切到会话 ${sid.slice(0, 12)} 出错:${(e && e.message) || e}`)
361
+ return 'failed'
362
+ }
363
+ })
325
364
  /** 会话 → 这个会话上一封信的处理链:同一会话的信串行发,前一封 promptAsync 返回了才发下一封(保证先后) */
326
365
  const chains = new Map()
327
- const enqueue = (sid, iid, text, at, replyWaitMs) => {
366
+ const enqueue = (sid, iid, text, at, replyWaitMs, focus) => {
328
367
  const prev = chains.get(sid) || Promise.resolve()
329
368
  const next = prev
330
369
  .then(() => deliver(sid, text, at + OC_ACK_BUDGET_MS))
@@ -332,6 +371,8 @@ export function startOcMailbox(opts) {
332
371
  (ack) => {
333
372
  writeAck(iid, ack)
334
373
  if (ack && ack.ok && replyWaitMs > 0) watchReply(sid, iid, at + replyWaitMs)
374
+ // 回执先写(不拖慢 daemon),再切会话;切不了不影响这一轮
375
+ if (ack && ack.ok && focus) void selectSession(sid, dirs.get(sid) || directory).catch(() => {})
335
376
  },
336
377
  () => writeAck(iid, { ok: false, reason: 'prompt_failed' })
337
378
  )
@@ -389,10 +430,12 @@ export function startOcMailbox(opts) {
389
430
  continue
390
431
  }
391
432
  const replyWaitMs = letter.summary === true && typeof letter.replyWaitMs === 'number' && letter.replyWaitMs > 0 ? Math.min(letter.replyWaitMs, OC_REPLY_MAX_MS) : 0
392
- got.push({ sid, iid, at, text: typeof letter.text === 'string' ? letter.text : '', replyWaitMs })
433
+ // 任务总结不切会话(daemon 也不会给总结带 focus,这里再兜一层)
434
+ const focus = letter.focus === true && letter.summary !== true
435
+ got.push({ sid, iid, at, text: typeof letter.text === 'string' ? letter.text : '', replyWaitMs, focus })
393
436
  }
394
437
  got.sort((a, b) => a.at - b.at)
395
- for (const x of got) enqueue(x.sid, x.iid, x.text, x.at, x.replyWaitMs)
438
+ for (const x of got) enqueue(x.sid, x.iid, x.text, x.at, x.replyWaitMs, x.focus)
396
439
  }
397
440
  let t1 = null
398
441
  let t2 = null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deskbuddy",
3
- "version": "0.2.0-beta.46",
3
+ "version": "0.2.0-beta.47",
4
4
  "description": "把 Claude Code、Codex 等 AI 编码智能体每个会话窗口的状态,实时同步到桌面硬件屏幕",
5
5
  "keywords": [
6
6
  "ai",