deskbuddy 0.2.0-beta.47 → 0.2.0-beta.48

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.
@@ -8,6 +8,8 @@ import { randomUUID } from "crypto";
8
8
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
9
9
  import { homedir, hostname } from "os";
10
10
  import { dirname, join, resolve } from "path";
11
+ var DEFAULT_FOCUS_IDLE_SECONDS = 5;
12
+ var MAX_FOCUS_IDLE_SECONDS = 600;
11
13
  function defaultRemoteCwd(home = homedir()) {
12
14
  return process.env.DESKBUDDY_DEFAULT_REMOTE_CWD || join(home, "DeskBuddy");
13
15
  }
@@ -206,7 +208,8 @@ function loadConfig() {
206
208
  chatgptCdpPort: portOrUndefined(parsed.chatgptCdpPort),
207
209
  focusSessionOnRemote: typeof parsed.focusSessionOnRemote === "boolean" ? parsed.focusSessionOnRemote : void 0,
208
210
  focusSessionOnRemoteWindows: typeof parsed.focusSessionOnRemoteWindows === "boolean" ? parsed.focusSessionOnRemoteWindows : void 0,
209
- focusSessionInApp: typeof parsed.focusSessionInApp === "boolean" ? parsed.focusSessionInApp : void 0
211
+ focusSessionInApp: typeof parsed.focusSessionInApp === "boolean" ? parsed.focusSessionInApp : void 0,
212
+ focusIdleSeconds: typeof parsed.focusIdleSeconds === "number" && Number.isInteger(parsed.focusIdleSeconds) && parsed.focusIdleSeconds >= 0 && parsed.focusIdleSeconds <= MAX_FOCUS_IDLE_SECONDS ? parsed.focusIdleSeconds : void 0
210
213
  };
211
214
  } catch {
212
215
  return fallback;
@@ -1714,10 +1717,19 @@ var CODEX_EVENTS = [
1714
1717
  "SessionEnd",
1715
1718
  "PermissionRequest"
1716
1719
  ];
1717
- var CODEX_TRUST_NOTICE = "\u26A0\uFE0F \u4E0B\u6B21\u542F\u52A8 Codex \u65F6\u4F1A\u8981\u6C42\u4F60\u786E\u8BA4\u4FE1\u4EFB\u8FD9\u4E2A hook\uFF0C\u8BF7\u9009\u62E9\u540C\u610F\u2014\u2014\u4E0D\u786E\u8BA4\u7684\u8BDD Codex \u4E0D\u4F1A\u6267\u884C\u5B83\u3002\n DeskBuddy \u5347\u7EA7\uFF08\u542B\u81EA\u52A8\u5347\u7EA7\uFF09\u65B0\u589E hook \u6761\u76EE\u65F6\uFF0CCodex \u53EA\u4F1A\u5C31\u65B0\u6761\u76EE\u518D\u95EE\u4E00\u6B21\uFF0C\u540C\u6837\u9009\u540C\u610F\uFF1B\u539F\u6709\u6761\u76EE\u4E0D\u53D7\u5F71\u54CD\u3001\u72B6\u6001\u7167\u5E38\u4E0A\u62A5\u3002\n Codex \u7ECF `--remote` \u8FDE\u5E38\u9A7B app-server \u4F7F\u7528\u65F6\uFF0C\u7A97\u53E3\u91CC\u7684\u6388\u6743\u6846\u4E5F\u80FD\u5728\u8BBE\u5907\u4E0A\u786E\u8BA4\uFF08\u5B88\u62A4\u8FDB\u7A0B\u5728\u8DD1\u65F6\u751F\u6548\uFF09";
1720
+ var CODEX_TRUST_NOTICE = "\u26A0\uFE0F \u4E0B\u6B21\u542F\u52A8 Codex \u65F6\u4F1A\u8981\u6C42\u4F60\u786E\u8BA4\u4FE1\u4EFB\u8FD9\u4E2A hook\uFF0C\u8BF7\u9009\u62E9\u540C\u610F\u2014\u2014\u4E0D\u786E\u8BA4\u7684\u8BDD Codex \u4E0D\u4F1A\u6267\u884C\u5B83\u3002\n \u6BCF\u6761 hook \u90FD\u5E26\u300CDeskBuddy\uFF1A\u2026\u300D\u540D\u5B57\uFF0C\u5BA1\u67E5\u6846\u91CC\u80FD\u770B\u5230\u5B83\u662F\u505A\u4EC0\u4E48\u7684\u3002\n DeskBuddy \u5347\u7EA7\uFF08\u542B\u81EA\u52A8\u5347\u7EA7\uFF09\u65B0\u589E\u6216\u6539\u52A8 hook \u6761\u76EE\u65F6\uFF0CCodex \u4F1A\u5C31\u8FD9\u4E9B\u6761\u76EE\u518D\u95EE\u4E00\u6B21\uFF0C\u540C\u6837\u9009\u540C\u610F\uFF1B\u786E\u8BA4\u524D\u8FD9\u4E9B\u6761\u76EE\u4E0D\u6267\u884C\u3002\n Codex \u7ECF `--remote` \u8FDE\u5E38\u9A7B app-server \u4F7F\u7528\u65F6\uFF0C\u7A97\u53E3\u91CC\u7684\u6388\u6743\u6846\u4E5F\u80FD\u5728\u8BBE\u5907\u4E0A\u786E\u8BA4\uFF08\u5B88\u62A4\u8FDB\u7A0B\u5728\u8DD1\u65F6\u751F\u6548\uFF09";
1718
1721
  function codexConfigPath() {
1719
1722
  return join7(codexHome(), "config.toml");
1720
1723
  }
1724
+ var CODEX_HOOK_LABELS = {
1725
+ UserPromptSubmit: "DeskBuddy\uFF1A\u628A\u4F60\u53D1\u7684\u6D88\u606F\u540C\u6B65\u5230\u684C\u9762\u8BBE\u5907",
1726
+ PreToolUse: "DeskBuddy\uFF1A\u628A\u6B63\u5728\u6267\u884C\u7684\u64CD\u4F5C\u540C\u6B65\u5230\u684C\u9762\u8BBE\u5907",
1727
+ PostToolUse: "DeskBuddy\uFF1A\u628A\u64CD\u4F5C\u7ED3\u679C\u540C\u6B65\u5230\u684C\u9762\u8BBE\u5907",
1728
+ Stop: "DeskBuddy\uFF1A\u4EFB\u52A1\u5B8C\u6210\u65F6\u901A\u77E5\u684C\u9762\u8BBE\u5907",
1729
+ SessionEnd: "DeskBuddy\uFF1A\u4F1A\u8BDD\u7ED3\u675F\u65F6\u901A\u77E5\u684C\u9762\u8BBE\u5907",
1730
+ PermissionRequest: "DeskBuddy\uFF1A\u628A\u6388\u6743\u8BF7\u6C42\u540C\u6B65\u5230\u684C\u9762\u8BBE\u5907"
1731
+ };
1732
+ var CODEX_APPENDED_LABEL = "DeskBuddy\uFF1A\u8BFB\u53D6\u4F60\u5728\u684C\u9762\u8BBE\u5907\u4E0A\u8FFD\u52A0\u7684\u6307\u4EE4";
1721
1733
  var CODEX_APPENDED_TIMEOUT_S = 3;
1722
1734
  function renderBlock(hookPath, events, tool, platform) {
1723
1735
  const script = platform === "win32" ? winPsPath(hookPath) : hookPath;
@@ -1725,9 +1737,10 @@ function renderBlock(hookPath, events, tool, platform) {
1725
1737
  const appended = JSON.stringify(`node ${script} ${tool} --appended`);
1726
1738
  const lines = [CODEX_MARKER_BEGIN];
1727
1739
  for (const event of events) {
1728
- lines.push(`[[hooks.${event}]]`, 'matcher = ""', "hooks = [", ` { type = "command", command = ${command}, async = true, timeoutSec = 5 }`);
1740
+ const label2 = JSON.stringify(CODEX_HOOK_LABELS[event] ?? `DeskBuddy\uFF1A${event} \u65F6\u540C\u6B65\u5230\u684C\u9762\u8BBE\u5907`);
1741
+ lines.push(`[[hooks.${event}]]`, 'matcher = ""', "hooks = [", ` { type = "command", command = ${command}, async = true, timeoutSec = 5, statusMessage = ${label2} }`);
1729
1742
  if (event === "UserPromptSubmit") {
1730
- lines.push(` , { type = "command", command = ${appended}, async = false, timeout = ${CODEX_APPENDED_TIMEOUT_S} }`);
1743
+ lines.push(` , { type = "command", command = ${appended}, async = false, timeout = ${CODEX_APPENDED_TIMEOUT_S}, statusMessage = ${JSON.stringify(CODEX_APPENDED_LABEL)} }`);
1731
1744
  }
1732
1745
  lines.push("]", "");
1733
1746
  }
@@ -7225,6 +7238,16 @@ var TYPE_TEXT = "text";
7225
7238
  var TYPE_SUMMARY = "summary";
7226
7239
  var MAX_TEXT_LENGTH = 2e3;
7227
7240
  var MAX_ID_LENGTH = 64;
7241
+ function parseFocusOverride(d) {
7242
+ const o = {};
7243
+ const e = typeof d.focusEnabled === "string" ? d.focusEnabled.trim() : d.focusEnabled;
7244
+ if (e === true || e === "true") o.enabled = true;
7245
+ else if (e === false || e === "false") o.enabled = false;
7246
+ const raw = d.focusIdleSec;
7247
+ const sec = typeof raw === "number" ? raw : typeof raw === "string" && /^\d{1,3}$/.test(raw.trim()) ? Number(raw.trim()) : NaN;
7248
+ if (Number.isInteger(sec) && sec >= 0 && sec <= 600) o.idleMs = sec * 1e3;
7249
+ return o.enabled === void 0 && o.idleMs === void 0 ? void 0 : o;
7250
+ }
7228
7251
  function str2(v) {
7229
7252
  return typeof v === "string" ? v : "";
7230
7253
  }
@@ -7261,6 +7284,10 @@ function parseCommand(frame) {
7261
7284
  const dirRef = !sessionId && /^[0-9a-f]{16}$/.test(rawDir) ? rawDir : "";
7262
7285
  const cmd = { instructionId, type, sessionId, tool, text, expireAt: parseMillis(d.expireAt), dirRef };
7263
7286
  if (type === TYPE_SUMMARY) cmd.kind = "summary";
7287
+ else {
7288
+ const focus = parseFocusOverride(d);
7289
+ if (focus) cmd.focus = focus;
7290
+ }
7264
7291
  return cmd;
7265
7292
  }
7266
7293
  var TYPE_APPROVAL = "approval";
@@ -11563,7 +11590,7 @@ var defaultLoginOpenDeps = () => ({
11563
11590
  });
11564
11591
  async function installCodebuddyHooks(log) {
11565
11592
  try {
11566
- const { installTools: installTools2 } = await import("./enable-OICEMNUF.js");
11593
+ const { installTools: installTools2 } = await import("./enable-W4R6MJGE.js");
11567
11594
  installTools2(["workbuddy"], () => {
11568
11595
  }, null, { auto: true });
11569
11596
  } catch (e) {
@@ -12077,7 +12104,7 @@ function defaultOfferDeps(log) {
12077
12104
  }),
12078
12105
  openLogin: () => defaultOpenLogin(platform),
12079
12106
  installHooks: async () => {
12080
- const { installTools: installTools2 } = await import("./enable-OICEMNUF.js");
12107
+ const { installTools: installTools2 } = await import("./enable-W4R6MJGE.js");
12081
12108
  installTools2(["workbuddy"], (l) => log(`[WorkBuddy \u72EC\u7ACB\u7248] ${l}`), null, { auto: true });
12082
12109
  },
12083
12110
  log: (l) => log(`[WorkBuddy \u72EC\u7ACB\u7248] ${l}`),
@@ -12317,6 +12344,8 @@ async function runEnable(tools, opts = {}) {
12317
12344
  }
12318
12345
 
12319
12346
  export {
12347
+ DEFAULT_FOCUS_IDLE_SECONDS,
12348
+ MAX_FOCUS_IDLE_SECONDS,
12320
12349
  defaultRemoteCwd,
12321
12350
  ensureDefaultRemoteCwd,
12322
12351
  remoteCwdReady,
package/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  COZE_TOOL,
19
19
  CdpKeeper,
20
20
  DEFAULT_CDP_PORT,
21
+ DEFAULT_FOCUS_IDLE_SECONDS,
21
22
  DM_DEFAULT_CDP_PORT,
22
23
  DUMATE_BRAND,
23
24
  DumateKeeper,
@@ -27,6 +28,7 @@ import {
27
28
  LAUNCH_TOOLS,
28
29
  LIMITED_NOTE,
29
30
  LOCATE_EVERY_MS,
31
+ MAX_FOCUS_IDLE_SECONDS,
30
32
  MAX_TEXT_LENGTH,
31
33
  NOT_REGISTERED_RE_SRC,
32
34
  PROBE,
@@ -279,22 +281,25 @@ import {
279
281
  writeState,
280
282
  writeWakeMark,
281
283
  writeWakeSecret
282
- } from "./chunk-JPCZVBK6.js";
284
+ } from "./chunk-XWSJEGGS.js";
283
285
 
284
286
  // src/commands/config.ts
285
287
  var BOOL_KEYS = {
286
288
  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
+ def: true,
290
+ sticky: true,
291
+ 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\u5728\u7A7A\u95F2\u95E8\u69DB\u5185\u7528\u8FC7\u952E\u9F20\u65F6\u4E0D\u5207\uFF09",
289
292
  off: "\u5DF2\u5173\u95ED\uFF1A\u8BBE\u5907\u56DE\u590D / \u65B0\u5EFA\u4EFB\u52A1\u540E\u4E0D\u518D\u628A App \u8C03\u5230\u524D\u53F0"
290
293
  },
291
294
  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",
295
+ def: null,
296
+ sticky: true,
297
+ on: "\u5DF2\u6253\u5F00\uFF1AWindows \u4E0A\u7528\u6DF1\u94FE\u5207\u4F1A\u8BDD\uFF08App \u6CA1\u6CE8\u518C\u534F\u8BAE\u7684\u7167\u6837\u4E0D\u5207\uFF09",
294
298
  off: "\u5DF2\u5173\u95ED\uFF1AWindows \u4E0A\u4E0D\u7528\u6DF1\u94FE\u5207\u4F1A\u8BDD"
295
299
  },
296
300
  focusSessionInApp: {
297
301
  def: true,
302
+ sticky: false,
298
303
  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
304
  off: "\u5DF2\u5173\u95ED\uFF1A\u8BBE\u5907\u56DE\u590D opencode \u4F1A\u8BDD\u540E\u4E0D\u5207\u6362\u754C\u9762\u91CC\u7684\u4F1A\u8BDD"
300
305
  }
@@ -304,7 +309,12 @@ var USAGE = [
304
309
  "\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
310
  " deskbuddy config get transcript",
306
311
  ` deskbuddy config set <${Object.keys(BOOL_KEYS).join("|")}> true|false`,
307
- " deskbuddy config get <\u540C\u4E0A>"
312
+ " deskbuddy config get <\u540C\u4E0A>",
313
+ " deskbuddy config set focusSessionOnRemoteWindows auto \uFF08Windows \u4E0D\u5355\u72EC\u8BBE\uFF0C\u8DDF\u968F focusSessionOnRemote\uFF0C\u9ED8\u8BA4\u5C31\u662F\u8FD9\u6837\uFF09",
314
+ ` deskbuddy config set focusIdleSeconds <0\uFF5E${MAX_FOCUS_IDLE_SECONDS}> \uFF08\u5207\u4F1A\u8BDD\u7684\u952E\u9F20\u7A7A\u95F2\u95E8\u69DB\u79D2\u6570\uFF0C\u9ED8\u8BA4 ${DEFAULT_FOCUS_IDLE_SECONDS}\uFF1B0 = \u4E0D\u770B\u952E\u9F20\uFF09`,
315
+ " deskbuddy config set focusIdleSeconds default \uFF08\u5220\u6389\u672C\u5730\u95E8\u69DB\uFF0C\u6062\u590D\u9ED8\u8BA4\uFF09",
316
+ " deskbuddy config get focusIdleSeconds",
317
+ " \uFF08App \u5BF9\u8FD9\u53F0\u7535\u8111 / \u8BBE\u5907\u8BBE\u8FC7\u5207\u4F1A\u8BDD\u5F00\u5173\u6216\u95E8\u69DB\u65F6\uFF0C\u4EE5 App \u4E3A\u51C6\uFF09"
308
318
  ].join("\n");
309
319
  function runConfig(args, out = (s2) => process.stdout.write(s2 + "\n")) {
310
320
  const [verb, key, value] = args;
@@ -313,6 +323,7 @@ function runConfig(args, out = (s2) => process.stdout.write(s2 + "\n")) {
313
323
  return 1;
314
324
  }
315
325
  if (key === "transcript") return transcript(verb, value, out);
326
+ if (key === "focusIdleSeconds") return idleSeconds(verb, value, out);
316
327
  if (!isBoolKey(key)) {
317
328
  out(USAGE);
318
329
  return 1;
@@ -320,19 +331,46 @@ function runConfig(args, out = (s2) => process.stdout.write(s2 + "\n")) {
320
331
  const spec = BOOL_KEYS[key];
321
332
  const cfg = loadConfig();
322
333
  if (verb === "get") {
323
- out(String(cfg[key] ?? spec.def));
334
+ out(String(cfg[key] ?? spec.def ?? "auto"));
335
+ return 0;
336
+ }
337
+ if (spec.def === null && value === "auto") {
338
+ saveConfig({ ...cfg, [key]: void 0 });
339
+ out("\u5DF2\u6539\u4E3A\u8DDF\u968F focusSessionOnRemote\uFF08\u5B88\u62A4\u8FDB\u7A0B\u4E0B\u4E00\u6761\u6307\u4EE4\u751F\u6548\uFF09");
324
340
  return 0;
325
341
  }
326
342
  if (value !== "true" && value !== "false") {
327
- out("\u53D6\u503C\u53EA\u80FD\u662F true \u6216 false");
343
+ out(spec.def === null ? "\u53D6\u503C\u53EA\u80FD\u662F true\u3001false \u6216 auto" : "\u53D6\u503C\u53EA\u80FD\u662F true \u6216 false");
328
344
  return 1;
329
345
  }
330
346
  const v = value === "true";
331
- const next = { ...cfg, [key]: v === spec.def ? void 0 : v };
347
+ const next = { ...cfg, [key]: !spec.sticky && v === spec.def ? void 0 : v };
332
348
  saveConfig(next);
333
349
  out(`${v ? spec.on : spec.off}\uFF08\u5B88\u62A4\u8FDB\u7A0B\u4E0B\u4E00\u6761\u6307\u4EE4\u751F\u6548\uFF09`);
334
350
  return 0;
335
351
  }
352
+ function idleSeconds(verb, value, out) {
353
+ const cfg = loadConfig();
354
+ if (verb === "get") {
355
+ out(String(cfg.focusIdleSeconds ?? DEFAULT_FOCUS_IDLE_SECONDS));
356
+ return 0;
357
+ }
358
+ if (value === "default") {
359
+ saveConfig({ ...cfg, focusIdleSeconds: void 0 });
360
+ out(`\u5DF2\u6062\u590D\u9ED8\u8BA4 ${DEFAULT_FOCUS_IDLE_SECONDS} \u79D2\uFF08App \u8BBE\u8FC7\u95E8\u69DB\u65F6\u4EE5 App \u4E3A\u51C6\uFF1B\u5B88\u62A4\u8FDB\u7A0B\u4E0B\u4E00\u6761\u6307\u4EE4\u751F\u6548\uFF09`);
361
+ return 0;
362
+ }
363
+ const n = value !== void 0 && /^\d{1,3}$/.test(value) ? Number(value) : NaN;
364
+ if (!Number.isInteger(n) || n < 0 || n > MAX_FOCUS_IDLE_SECONDS) {
365
+ out(`\u53D6\u503C\u53EA\u80FD\u662F 0\uFF5E${MAX_FOCUS_IDLE_SECONDS} \u7684\u6574\u6570\uFF08\u79D2\uFF09\uFF0C\u6216 default \u6062\u590D\u9ED8\u8BA4`);
366
+ return 1;
367
+ }
368
+ saveConfig({ ...cfg, focusIdleSeconds: n });
369
+ out(
370
+ `${n === 0 ? "\u5DF2\u8BBE\u4E3A 0\uFF1A\u8BBE\u5907\u56DE\u590D / \u65B0\u5EFA\u4EFB\u52A1\u540E\u4E0D\u770B\u952E\u9F20\uFF0C\u6536\u5230\u5C31\u5207\u5230\u8BE5\u4F1A\u8BDD" : `\u5DF2\u8BBE\u4E3A ${n} \u79D2\uFF1A\u4F60 ${n} \u79D2\u5185\u7528\u8FC7\u952E\u9F20\u65F6\u4E0D\u5207\u4F1A\u8BDD`}\uFF08App \u8BBE\u8FC7\u95E8\u69DB\u65F6\u4EE5 App \u4E3A\u51C6\uFF1B\u5B88\u62A4\u8FDB\u7A0B\u4E0B\u4E00\u6761\u6307\u4EE4\u751F\u6548\uFF09`
371
+ );
372
+ return 0;
373
+ }
336
374
  function transcript(verb, value, out) {
337
375
  const cfg = loadConfig();
338
376
  if (verb === "get") {
@@ -2280,10 +2318,18 @@ var TitleRefresh = class {
2280
2318
  import { basename as basename2 } from "path";
2281
2319
  var KEEPALIVE_MS = 2 * 60 * 1e3;
2282
2320
  var MAX_SESSIONS = 20;
2283
- var CAPS = ["cmd_result", "window_inject", "resume_ended", "reply_idle_window", "turns", "summary", ...SUMMARY_CAPS, ...TRANSCRIPT_CAPS];
2321
+ var CAPS = ["cmd_result", "window_inject", "resume_ended", "reply_idle_window", "turns", "summary", "focus_cfg", ...SUMMARY_CAPS, ...TRANSCRIPT_CAPS];
2284
2322
  function capsFor(cfg) {
2285
2323
  return cfg.transcript === false ? CAPS.filter((c) => !TRANSCRIPT_CAPS.includes(c)) : [...CAPS];
2286
2324
  }
2325
+ function focusLocalFor(cfg) {
2326
+ const o = {};
2327
+ if (typeof cfg.focusSessionOnRemote === "boolean") o.enabled = cfg.focusSessionOnRemote;
2328
+ const n = cfg.focusIdleSeconds;
2329
+ if (typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= 600) o.idleSeconds = n;
2330
+ if (typeof cfg.focusSessionOnRemoteWindows === "boolean") o.windowsEnabled = cfg.focusSessionOnRemoteWindows;
2331
+ return o;
2332
+ }
2287
2333
  function turnsOf(s2) {
2288
2334
  return Number.isInteger(s2.turns) && s2.turns >= 0 ? s2.turns : void 0;
2289
2335
  }
@@ -2305,7 +2351,7 @@ function buildSnapshot(states, tool, cfg, nowMs, remote) {
2305
2351
  if (turns !== void 0) o.turns = turns;
2306
2352
  return o;
2307
2353
  });
2308
- return { clientId: cfg.clientId, hostName: cfg.hostName, tool, snapshotAt: nowMs, sessions, caps: capsFor(cfg) };
2354
+ return { clientId: cfg.clientId, hostName: cfg.hostName, tool, snapshotAt: nowMs, sessions, caps: capsFor(cfg), focusLocal: focusLocalFor(cfg) };
2309
2355
  }
2310
2356
 
2311
2357
  // src/core/dirs.ts
@@ -5982,7 +6028,7 @@ function sleep(ms) {
5982
6028
  // src/daemon/focusSession.ts
5983
6029
  import { execFile as execFile3 } from "child_process";
5984
6030
  import { open as open2 } from "fs/promises";
5985
- var FOCUS_IDLE_MIN_MS = 10 * 1e3;
6031
+ var DEFAULT_FOCUS_IDLE_MS = DEFAULT_FOCUS_IDLE_SECONDS * 1e3;
5986
6032
  var FOCUS_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
5987
6033
  var okId = (id) => typeof id === "string" && FOCUS_ID_RE.test(id);
5988
6034
  function workbuddyChatUrl(sessionId) {
@@ -6006,8 +6052,19 @@ function focusScheme(t) {
6006
6052
  return t.kind === "workbuddy" ? "workbuddy" : t.kind === "codex" ? "codex" : "qwenwork-cn";
6007
6053
  }
6008
6054
  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 };
6055
+ function focusSettings(cfg, override) {
6056
+ const app = override?.enabled;
6057
+ const onRemote = app ?? cfg.focusSessionOnRemote !== false;
6058
+ const local = cfg.focusIdleSeconds;
6059
+ const localMs = Number.isInteger(local) && local >= 0 && local <= MAX_FOCUS_IDLE_SECONDS ? local * 1e3 : void 0;
6060
+ const ov = override?.idleMs;
6061
+ const appMs = Number.isInteger(ov) && ov >= 0 && ov <= MAX_FOCUS_IDLE_SECONDS * 1e3 ? ov : void 0;
6062
+ return {
6063
+ onRemote,
6064
+ onRemoteWindows: app ?? (typeof cfg.focusSessionOnRemoteWindows === "boolean" ? cfg.focusSessionOnRemoteWindows : onRemote),
6065
+ inApp: app ?? cfg.focusSessionInApp !== false,
6066
+ idleMinMs: appMs ?? localMs ?? DEFAULT_FOCUS_IDLE_MS
6067
+ };
6011
6068
  }
6012
6069
  var APP_ORIGINATOR_RE = /"originator"\s*:\s*"(Codex Desktop|codex_work_desktop)"/;
6013
6070
  function isChatgptAppRolloutHead(head) {
@@ -6070,6 +6127,52 @@ function defaultProtocolRegistered(scheme) {
6070
6127
  if (skipSystem() || !/^[a-z][a-z0-9+.-]{0,31}$/.test(scheme)) return Promise.resolve(false);
6071
6128
  return execOk("reg.exe", ["query", `HKCR\\${scheme}`, "/v", "URL Protocol"], 5e3);
6072
6129
  }
6130
+ var WIN_PROCESS_NAMES = {
6131
+ workbuddy: ["WorkBuddy"],
6132
+ qwenwork: ["QwenWorkCN"],
6133
+ codex: ["ChatGPT (Beta)", "ChatGPT"]
6134
+ };
6135
+ var WIN_RAISE_DELAY_MS = 1500;
6136
+ var WIN_RAISE_TIMEOUT_MS = 8e3;
6137
+ function tasklistHas(stdout6, names) {
6138
+ const want = new Set(names.map((n) => n.toLowerCase()));
6139
+ for (const line of stdout6.split(/\r?\n/)) {
6140
+ const m = /^"([^"]+)"/.exec(line.trim());
6141
+ if (m && want.has(m[1].replace(/\.exe$/i, "").toLowerCase())) return true;
6142
+ }
6143
+ return false;
6144
+ }
6145
+ async function defaultAppRunning(names, exec = defaultExec) {
6146
+ if (skipSystem()) return null;
6147
+ const r = await exec("tasklist", ["/FO", "CSV", "/NH"], 1e4);
6148
+ return r.code === 0 ? tasklistHas(r.stdout, names) : null;
6149
+ }
6150
+ var PROC_NAME_RE = /^[A-Za-z0-9 ()._-]{1,64}$/;
6151
+ function winRaiseScript(names) {
6152
+ if (names.length === 0 || !names.every((n) => PROC_NAME_RE.test(n))) return null;
6153
+ const list = names.map((n) => `'${n}'`).join(",");
6154
+ return String.raw`Add-Type @"
6155
+ using System; using System.Runtime.InteropServices;
6156
+ public class DbFocusWin {
6157
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
6158
+ [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
6159
+ [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
6160
+ [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, UIntPtr extra);
6161
+ }
6162
+ "@
6163
+ $p = Get-Process -Name ${list} -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero -and $_.MainWindowTitle } | Select-Object -First 1
6164
+ if (-not $p) { Write-Output 'nowin'; exit 0 }
6165
+ $h = $p.MainWindowHandle
6166
+ [DbFocusWin]::keybd_event(0x12, 0, 0, [UIntPtr]::Zero); [DbFocusWin]::keybd_event(0x12, 0, 2, [UIntPtr]::Zero)
6167
+ if ([DbFocusWin]::IsIconic($h)) { [void][DbFocusWin]::ShowWindow($h, 9) }
6168
+ Write-Output ("fg={0}" -f [DbFocusWin]::SetForegroundWindow($h))`;
6169
+ }
6170
+ async function defaultRaiseWindow(names, exec = defaultExec) {
6171
+ const script = winRaiseScript(names);
6172
+ if (skipSystem() || !script) return false;
6173
+ const r = await exec("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], WIN_RAISE_TIMEOUT_MS);
6174
+ return r.code === 0 && /fg=True/i.test(r.stdout);
6175
+ }
6073
6176
  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
6177
  var DONE_MAX = 200;
6075
6178
  var IDLE_REFRESH_MAC_MS = 5 * 1e3;
@@ -6081,6 +6184,11 @@ var SessionFocuser = class {
6081
6184
  /** 后台取到的键鼠空闲(opencode 切会话只读它,投递永不等取空闲,评审 73f95f0) */
6082
6185
  #idleCache = null;
6083
6186
  #lastInAppUse = Number.NEGATIVE_INFINITY;
6187
+ /** 最近一次 opencode 投递带的 App 设置:后台刷新据它判开关 / 门槛(App 开了而本地关着时也要刷) */
6188
+ #lastInAppOverride;
6189
+ /** Windows 调前台:是否有一轮在等 / 在跑;等着处理的最新目标 */
6190
+ #raiseBusy = false;
6191
+ #raiseNext = null;
6084
6192
  #idleTimer = null;
6085
6193
  #refreshing = false;
6086
6194
  constructor(d) {
@@ -6091,12 +6199,13 @@ var SessionFocuser = class {
6091
6199
  return this.#platform() === "win32" ? IDLE_REFRESH_WIN_MS : IDLE_REFRESH_MAC_MS;
6092
6200
  }
6093
6201
  /**
6094
- * 后台刷新一次空闲缓存:只在 focusSessionInApp 开着、且最近 10 分钟内有过 opencode 投递时取;同一时刻只取一次。
6095
- * 不活跃了就停掉定时器(下次投递再起)
6202
+ * 后台刷新一次空闲缓存:只在 App 内切换开着、门槛不是 0、且最近 10 分钟内有过 opencode 投递时取(开关 / 门槛按最近一次投递带的 App 设置);
6203
+ * 同一时刻只取一次。不活跃了就停掉定时器(下次投递再起)
6096
6204
  */
6097
6205
  async refreshIdle() {
6098
6206
  if (this.#refreshing) return;
6099
- if (!this.inAppEnabled() || this.#now() - this.#lastInAppUse > IDLE_ACTIVE_WINDOW_MS) {
6207
+ const s2 = focusSettings(this.#cfg(), this.#lastInAppOverride);
6208
+ if (!s2.inApp || s2.idleMinMs === 0 || this.#now() - this.#lastInAppUse > IDLE_ACTIVE_WINDOW_MS) {
6100
6209
  this.#stopIdleTimer();
6101
6210
  return;
6102
6211
  }
@@ -6122,16 +6231,22 @@ var SessionFocuser = class {
6122
6231
  return c.ms;
6123
6232
  }
6124
6233
  /** opencode 这类 App 内切换(不激活)开着吗 */
6125
- inAppEnabled() {
6126
- return focusSettings(this.#cfg()).inApp;
6234
+ inAppEnabled(override) {
6235
+ return focusSettings(this.#cfg(), override).inApp;
6127
6236
  }
6128
6237
  /**
6129
- * opencode 这类 App 内切换现在能不能做:开关开着,且用户没在用电脑(键鼠空闲 ≥ FOCUS_IDLE_MIN_MS;读不到也不切)。
6238
+ * opencode 这类 App 内切换现在能不能做:开关开着,且用户没在用电脑(键鼠空闲 ≥ 门槛;读不到也不切)。
6239
+ * 门槛 0 → 不看键鼠直接切(不要求有缓存、不起取空闲定时器)。
6130
6240
  * 🔴 同步、只读后台缓存,绝不在投递路径上取空闲(Windows 要起 PowerShell)。缓存没有 / 过期 → 这次不切;
6131
6241
  * 同时记下「最近有 opencode 投递」并确保后台刷新在跑(第一次投递时还没有缓存,从下一条起生效)。跳过时记一行
6132
6242
  */
6133
- inAppReady(instructionId, name = "opencode") {
6134
- if (!this.inAppEnabled()) return false;
6243
+ inAppReady(instructionId, override, name = "opencode") {
6244
+ const s2 = focusSettings(this.#cfg(), override);
6245
+ this.#lastInAppOverride = override ?? void 0;
6246
+ if (!s2.inApp || s2.idleMinMs === 0) {
6247
+ this.#stopIdleTimer();
6248
+ return s2.inApp;
6249
+ }
6135
6250
  this.#lastInAppUse = this.#now();
6136
6251
  if (!this.#idleTimer && this.#d.timers !== false) {
6137
6252
  void this.refreshIdle().catch(() => {
@@ -6141,29 +6256,28 @@ var SessionFocuser = class {
6141
6256
  this.#idleTimer.unref?.();
6142
6257
  }
6143
6258
  const idle = this.#cachedIdle();
6144
- if (idle === null || idle < FOCUS_IDLE_MIN_MS) {
6259
+ if (idle === null || idle < s2.idleMinMs) {
6145
6260
  this.#d.log(`\u6307\u4EE4 ${instructionId} \u4E0D\u5728 ${name} \u754C\u9762\u91CC\u5207\u4F1A\u8BDD\uFF1A${idleWhy(idle)}`);
6146
6261
  return false;
6147
6262
  }
6148
6263
  return true;
6149
6264
  }
6150
6265
  /** 深链切会话在这台电脑上会生效吗(开关 + 平台)。调用方据此决定要不要先做查找类的准备工作 */
6151
- remoteEnabled() {
6152
- const s2 = focusSettings(this.#cfg());
6153
- if (!s2.onRemote) return false;
6266
+ remoteEnabled(override) {
6267
+ const s2 = focusSettings(this.#cfg(), override);
6154
6268
  const p = this.#platform();
6155
- return p === "darwin" || p === "win32" && s2.onRemoteWindows;
6269
+ return p === "darwin" && s2.onRemote || p === "win32" && s2.onRemoteWindows;
6156
6270
  }
6157
6271
  /**
6158
6272
  * 设备回复 / 新建任务交出去之后调:开关、平台、id、用户在不在打字都过了才打开深链。永不抛。
6159
6273
  * instructionId 只用来去重和记日志;不记会话内容
6160
6274
  */
6161
- async focus(instructionId, t) {
6275
+ async focus(instructionId, t, override) {
6162
6276
  if (this.#done.has(instructionId)) return "duplicate";
6163
- const s2 = focusSettings(this.#cfg());
6164
- if (!s2.onRemote) return "disabled";
6277
+ const s2 = focusSettings(this.#cfg(), override);
6165
6278
  const p = this.#platform();
6166
- if (p !== "darwin" && !(p === "win32" && s2.onRemoteWindows)) return "platform";
6279
+ if (p !== "darwin" && p !== "win32") return "platform";
6280
+ if (!(p === "win32" ? s2.onRemoteWindows : s2.onRemote)) return "disabled";
6167
6281
  const url = focusUrl(t);
6168
6282
  const name = TOOL_NAME[t.kind];
6169
6283
  if (!url) {
@@ -6182,11 +6296,25 @@ var SessionFocuser = class {
6182
6296
  this.#d.log(`\u6307\u4EE4 ${instructionId} \u5207\u5230 ${name} \u4F1A\u8BDD\u5931\u8D25\uFF1A\u672C\u673A\u6CA1\u6CE8\u518C ${focusScheme(t)}:// \u534F\u8BAE`);
6183
6297
  return "protocol_missing";
6184
6298
  }
6299
+ if (t.kind === "codex") {
6300
+ let running = null;
6301
+ try {
6302
+ running = await (this.#d.appRunning ?? ((n) => defaultAppRunning(n)))(WIN_PROCESS_NAMES.codex);
6303
+ } catch {
6304
+ running = null;
6305
+ }
6306
+ if (running === false) {
6307
+ this.#d.log(`\u6307\u4EE4 ${instructionId} \u4E0D\u5207\u5230 ${name} \u4F1A\u8BDD\uFF1AChatGPT App \u6CA1\u5F00\uFF0C\u4E0D\u5207`);
6308
+ return "app_not_running";
6309
+ }
6310
+ }
6185
6311
  }
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";
6312
+ if (s2.idleMinMs > 0) {
6313
+ const idle = await this.#idle();
6314
+ if (idle === null || idle < s2.idleMinMs) {
6315
+ this.#d.log(`\u6307\u4EE4 ${instructionId} \u4E0D\u5207\u5230 ${name} \u4F1A\u8BDD\uFF1A${idleWhy(idle)}`);
6316
+ return "user_active";
6317
+ }
6190
6318
  }
6191
6319
  let ok = false;
6192
6320
  try {
@@ -6195,8 +6323,36 @@ var SessionFocuser = class {
6195
6323
  ok = false;
6196
6324
  }
6197
6325
  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`);
6326
+ if (ok && p === "win32") await this.#raise(instructionId, t, name);
6198
6327
  return ok ? "opened" : "failed";
6199
6328
  }
6329
+ /**
6330
+ * Windows:深链打开后约 1.5 秒把 App 窗口调到前台(前台锁下深链本身不一定能把窗口带上来)。失败只记日志,永不抛。
6331
+ * 🔴 同一时刻只跑一个:已有在等 / 在跑的,只记下最新目标(等待中被后来的覆盖),跑完再处理,不并发起多个 PowerShell(评审)
6332
+ */
6333
+ async #raise(instructionId, t, name) {
6334
+ this.#raiseNext = { instructionId, t, name };
6335
+ if (this.#raiseBusy) return;
6336
+ this.#raiseBusy = true;
6337
+ try {
6338
+ while (this.#raiseNext) {
6339
+ let ok = false;
6340
+ let job = null;
6341
+ try {
6342
+ await (this.#d.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))))(WIN_RAISE_DELAY_MS);
6343
+ job = this.#raiseNext;
6344
+ this.#raiseNext = null;
6345
+ if (!job) break;
6346
+ ok = await (this.#d.raiseWindow ?? ((n) => defaultRaiseWindow(n)))(WIN_PROCESS_NAMES[job.t.kind]);
6347
+ } catch {
6348
+ ok = false;
6349
+ }
6350
+ if (job && !ok) this.#d.log(`\u6307\u4EE4 ${job.instructionId} \u5DF2\u6253\u5F00 ${job.name} \u6DF1\u94FE\uFF0C\u4F46\u6CA1\u80FD\u628A\u7A97\u53E3\u8C03\u5230\u524D\u53F0`);
6351
+ }
6352
+ } finally {
6353
+ this.#raiseBusy = false;
6354
+ }
6355
+ }
6200
6356
  /** 键鼠空闲毫秒(Windows 起一次 PowerShell);取不到 null。永不抛 */
6201
6357
  async #idle() {
6202
6358
  const p = this.#platform();
@@ -18985,17 +19141,17 @@ var Daemon = class {
18985
19141
  #focusAfter(cmd, target) {
18986
19142
  if (isSummary(cmd)) return;
18987
19143
  try {
18988
- if (!this.#deps.focuser.remoteEnabled()) return;
19144
+ if (!this.#deps.focuser.remoteEnabled(cmd.focus)) return;
18989
19145
  } catch {
18990
19146
  return;
18991
19147
  }
18992
- void Promise.resolve().then(target).then((t) => t ? this.#deps.focuser.focus(cmd.instructionId, t) : null).catch(() => {
19148
+ void Promise.resolve().then(target).then((t) => t ? this.#deps.focuser.focus(cmd.instructionId, t, cmd.focus) : null).catch(() => {
18993
19149
  });
18994
19150
  }
18995
- /** opencode 这类 App 内切会话(不激活):开关开着且用户没在用电脑 */
19151
+ /** opencode 这类 App 内切会话(不激活):开关开着且用户没在用电脑(开关 / 门槛先看指令帧带的 App 设置) */
18996
19152
  #focusInApp(cmd) {
18997
19153
  try {
18998
- return this.#deps.focuser.inAppReady(cmd.instructionId);
19154
+ return this.#deps.focuser.inAppReady(cmd.instructionId, cmd.focus);
18999
19155
  } catch {
19000
19156
  return false;
19001
19157
  }
@@ -22768,8 +22924,11 @@ var USAGE2 = `deskbuddy \u2014\u2014 \u628A AI \u7F16\u7801\u667A\u80FD\u4F53\u6
22768
22924
  deskbuddy logout [--yes] \u79FB\u9664 hook \u5E76\u6E05\u9664\u672C\u673A\u6570\u636E
22769
22925
  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
22926
  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
22927
+ App \u8C03\u5230\u524D\u53F0\uFF0C\u9ED8\u8BA4\u5F00\uFF09\u3001focusSessionInApp\uFF08opencode \u5185\u5207\u6362\uFF0C\u9ED8\u8BA4\u5F00\uFF09\u3001
22928
+ focusSessionOnRemoteWindows\uFF08Windows \u5355\u72EC\u8BBE\uFF0C\u4E0D\u8BBE = \u8DDF\u968F
22929
+ focusSessionOnRemote\uFF1Bauto \u6062\u590D\u8DDF\u968F\uFF09
22930
+ deskbuddy config set focusIdleSeconds <0\uFF5E600|default> \u5207\u4F1A\u8BDD\u524D\u7684\u952E\u9F20\u7A7A\u95F2\u95E8\u69DB\u79D2\u6570\uFF08\u9ED8\u8BA4 5\uFF1B0 = \u4E0D\u770B\u952E\u9F20\uFF09
22931
+ App \u5BF9\u8FD9\u53F0\u7535\u8111 / \u8BBE\u5907\u8BBE\u8FC7\u5F00\u5173\u6216\u95E8\u69DB\u65F6\uFF0C\u4EE5 App \u4E3A\u51C6
22773
22932
  deskbuddy workbuddy-setup [--yes] \u5B89\u88C5 WorkBuddy \u7684\u72EC\u7ACB\u7248\u547D\u4EE4\u884C\uFF08CodeBuddy Code\uFF09\uFF0C\u88C5\u4E86\u624D\u80FD\u5728
22774
22933
  \u8BBE\u5907\u4E0A\u65B0\u5EFA / \u56DE\u590D WorkBuddy \u4EFB\u52A1\uFF08--yes \u4E0D\u5F39\u6846\u76F4\u63A5\u88C5\uFF09
22775
22934
  deskbuddy wrap <tool> [\u53C2\u6570\u2026] \uFF08\u57AB\u7247\u5185\u90E8\u7528\uFF09\u7ECF DeskBuddy \u6253\u5F00\u667A\u80FD\u4F53\u7A97\u53E3\uFF0C
@@ -5,7 +5,7 @@ import {
5
5
  installTools,
6
6
  parseTools,
7
7
  runEnable
8
- } from "./chunk-JPCZVBK6.js";
8
+ } from "./chunk-XWSJEGGS.js";
9
9
  export {
10
10
  defaultShimEnvForInstall,
11
11
  installShims,
package/dist/hook.cjs CHANGED
@@ -31,11 +31,22 @@ const SUMMARY_TOOLS = ['claude_code', 'codex', 'qoder', 'qoder_cn', 'opencode',
31
31
  // transcript:<tool> 与 src/core/transcriptTools.ts 的 TRANSCRIPT_TOOLS 同一份(有对齐用例)
32
32
  const TRANSCRIPT_TOOLS = ['claude_code', 'codex', 'qoder', 'qoder_cn', 'workbuddy', 'qwenwork', 'opencode', 'dumate', 'deepseek_harness']
33
33
  const TRANSCRIPT_CAPS = ['transcript'].concat(TRANSCRIPT_TOOLS.map((t) => 'transcript:' + t))
34
- const CAPS = ['cmd_result', 'window_inject', 'resume_ended', 'reply_idle_window', 'turns', 'summary'].concat(SUMMARY_TOOLS.map((t) => 'summary:' + t), TRANSCRIPT_CAPS)
34
+ const CAPS = ['cmd_result', 'window_inject', 'resume_ended', 'reply_idle_window', 'turns', 'summary', 'focus_cfg'].concat(SUMMARY_TOOLS.map((t) => 'summary:' + t), TRANSCRIPT_CAPS)
35
35
  /** 本地硬开关 transcript=false 时不声明 transcript caps(与 src/core/snapshot.ts 的 capsFor 一致) */
36
36
  function capsFor(cfg) {
37
37
  return cfg && cfg.transcript === false ? CAPS.filter((c) => TRANSCRIPT_CAPS.indexOf(c) === -1) : CAPS.slice()
38
38
  }
39
+
40
+ /** 本机显式写过的切会话设置,没写的项不带(spec 2026-10-13 focus-remote-config §2.4;与 src/core/snapshot.ts 的 focusLocalFor 一致) */
41
+ function focusLocalFor(cfg) {
42
+ const o = {}
43
+ if (!cfg) return o
44
+ if (typeof cfg.focusSessionOnRemote === 'boolean') o.enabled = cfg.focusSessionOnRemote
45
+ const n = cfg.focusIdleSeconds
46
+ if (typeof n === 'number' && Number.isInteger(n) && n >= 0 && n <= 600) o.idleSeconds = n
47
+ if (typeof cfg.focusSessionOnRemoteWindows === 'boolean') o.windowsEnabled = cfg.focusSessionOnRemoteWindows
48
+ return o
49
+ }
39
50
  /** 状态文件里保留的最近事件条数,与 src/core/state.ts 的 RECENT_MAX 一致 */
40
51
  const RECENT_MAX = 8
41
52
  const REPORT_TIMEOUT_MS = 2000
@@ -285,6 +296,10 @@ function loadConfig() {
285
296
  apiBase: process.env.DESKBUDDY_API_BASE || parsed.apiBase || DEFAULT_API_BASE,
286
297
  // 本地硬开关(spec 2026-10-11 transcript-sync §5):只有显式 false 才关
287
298
  transcript: parsed.transcript === false ? false : undefined,
299
+ // 快照带的 focusLocal 用(与 src/core/config.ts 同样只认合法值)
300
+ focusSessionOnRemote: typeof parsed.focusSessionOnRemote === 'boolean' ? parsed.focusSessionOnRemote : undefined,
301
+ focusSessionOnRemoteWindows: typeof parsed.focusSessionOnRemoteWindows === 'boolean' ? parsed.focusSessionOnRemoteWindows : undefined,
302
+ focusIdleSeconds: typeof parsed.focusIdleSeconds === 'number' ? parsed.focusIdleSeconds : undefined,
288
303
  }
289
304
  } catch (e) {
290
305
  return fallback
@@ -1219,7 +1234,7 @@ function buildSnapshot(states, tool, cfg, nowMs, remote) {
1219
1234
  if (turnsOf(s) !== undefined) o.turns = s.turns
1220
1235
  return o
1221
1236
  })
1222
- return { clientId: cfg.clientId, hostName: cfg.hostName, tool: tool, snapshotAt: nowMs, sessions: sessions, caps: capsFor(cfg) }
1237
+ return { clientId: cfg.clientId, hostName: cfg.hostName, tool: tool, snapshotAt: nowMs, sessions: sessions, caps: capsFor(cfg), focusLocal: focusLocalFor(cfg) }
1223
1238
  }
1224
1239
 
1225
1240
  // ---------- 上报 ----------
@@ -3013,6 +3028,7 @@ module.exports = {
3013
3028
  parseWinProcJson,
3014
3029
  CAPS,
3015
3030
  capsFor,
3031
+ focusLocalFor,
3016
3032
  turnEvents,
3017
3033
  turnsOf,
3018
3034
  setTurns,
@@ -548,3 +548,33 @@
548
548
  2026-10-13T10:34:18.075Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
549
549
  2026-10-13T10:34:18.075Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
550
550
  2026-10-13T10:34:22.944Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
551
+ 2026-10-13T12:11:57.052Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
552
+ 2026-10-13T12:12:02.307Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
553
+ 2026-10-13T12:12:02.307Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
554
+ 2026-10-13T12:12:02.419Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
555
+ 2026-10-13T12:12:02.419Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
556
+ 2026-10-13T12:12:06.051Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
557
+ 2026-10-13T12:46:22.157Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
558
+ 2026-10-13T12:46:27.749Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
559
+ 2026-10-13T12:46:27.749Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
560
+ 2026-10-13T12:46:27.968Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
561
+ 2026-10-13T12:46:27.968Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
562
+ 2026-10-13T12:46:40.616Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
563
+ 2026-10-13T12:55:40.939Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
564
+ 2026-10-13T12:55:47.599Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
565
+ 2026-10-13T12:55:47.599Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
566
+ 2026-10-13T12:55:47.850Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
567
+ 2026-10-13T12:55:47.850Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
568
+ 2026-10-13T12:55:58.486Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
569
+ 2026-10-13T13:13:26.197Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
570
+ 2026-10-13T13:13:33.512Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
571
+ 2026-10-13T13:13:33.512Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
572
+ 2026-10-13T13:13:34.084Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
573
+ 2026-10-13T13:13:34.085Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
574
+ 2026-10-13T13:13:44.931Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
575
+ 2026-10-13T13:17:21.262Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
576
+ 2026-10-13T13:17:26.871Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
577
+ 2026-10-13T13:17:26.871Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
578
+ 2026-10-13T13:17:27.063Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
579
+ 2026-10-13T13:17:27.063Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
580
+ 2026-10-13T13:17:30.209Z [opencode] 后台任务里的提问 que_r2 没法在设备上回答(后台任务不能提问,请在电脑上继续),已拒绝
package/hook/hook.cjs CHANGED
@@ -31,11 +31,22 @@ const SUMMARY_TOOLS = ['claude_code', 'codex', 'qoder', 'qoder_cn', 'opencode',
31
31
  // transcript:<tool> 与 src/core/transcriptTools.ts 的 TRANSCRIPT_TOOLS 同一份(有对齐用例)
32
32
  const TRANSCRIPT_TOOLS = ['claude_code', 'codex', 'qoder', 'qoder_cn', 'workbuddy', 'qwenwork', 'opencode', 'dumate', 'deepseek_harness']
33
33
  const TRANSCRIPT_CAPS = ['transcript'].concat(TRANSCRIPT_TOOLS.map((t) => 'transcript:' + t))
34
- const CAPS = ['cmd_result', 'window_inject', 'resume_ended', 'reply_idle_window', 'turns', 'summary'].concat(SUMMARY_TOOLS.map((t) => 'summary:' + t), TRANSCRIPT_CAPS)
34
+ const CAPS = ['cmd_result', 'window_inject', 'resume_ended', 'reply_idle_window', 'turns', 'summary', 'focus_cfg'].concat(SUMMARY_TOOLS.map((t) => 'summary:' + t), TRANSCRIPT_CAPS)
35
35
  /** 本地硬开关 transcript=false 时不声明 transcript caps(与 src/core/snapshot.ts 的 capsFor 一致) */
36
36
  function capsFor(cfg) {
37
37
  return cfg && cfg.transcript === false ? CAPS.filter((c) => TRANSCRIPT_CAPS.indexOf(c) === -1) : CAPS.slice()
38
38
  }
39
+
40
+ /** 本机显式写过的切会话设置,没写的项不带(spec 2026-10-13 focus-remote-config §2.4;与 src/core/snapshot.ts 的 focusLocalFor 一致) */
41
+ function focusLocalFor(cfg) {
42
+ const o = {}
43
+ if (!cfg) return o
44
+ if (typeof cfg.focusSessionOnRemote === 'boolean') o.enabled = cfg.focusSessionOnRemote
45
+ const n = cfg.focusIdleSeconds
46
+ if (typeof n === 'number' && Number.isInteger(n) && n >= 0 && n <= 600) o.idleSeconds = n
47
+ if (typeof cfg.focusSessionOnRemoteWindows === 'boolean') o.windowsEnabled = cfg.focusSessionOnRemoteWindows
48
+ return o
49
+ }
39
50
  /** 状态文件里保留的最近事件条数,与 src/core/state.ts 的 RECENT_MAX 一致 */
40
51
  const RECENT_MAX = 8
41
52
  const REPORT_TIMEOUT_MS = 2000
@@ -285,6 +296,10 @@ function loadConfig() {
285
296
  apiBase: process.env.DESKBUDDY_API_BASE || parsed.apiBase || DEFAULT_API_BASE,
286
297
  // 本地硬开关(spec 2026-10-11 transcript-sync §5):只有显式 false 才关
287
298
  transcript: parsed.transcript === false ? false : undefined,
299
+ // 快照带的 focusLocal 用(与 src/core/config.ts 同样只认合法值)
300
+ focusSessionOnRemote: typeof parsed.focusSessionOnRemote === 'boolean' ? parsed.focusSessionOnRemote : undefined,
301
+ focusSessionOnRemoteWindows: typeof parsed.focusSessionOnRemoteWindows === 'boolean' ? parsed.focusSessionOnRemoteWindows : undefined,
302
+ focusIdleSeconds: typeof parsed.focusIdleSeconds === 'number' ? parsed.focusIdleSeconds : undefined,
288
303
  }
289
304
  } catch (e) {
290
305
  return fallback
@@ -1219,7 +1234,7 @@ function buildSnapshot(states, tool, cfg, nowMs, remote) {
1219
1234
  if (turnsOf(s) !== undefined) o.turns = s.turns
1220
1235
  return o
1221
1236
  })
1222
- return { clientId: cfg.clientId, hostName: cfg.hostName, tool: tool, snapshotAt: nowMs, sessions: sessions, caps: capsFor(cfg) }
1237
+ return { clientId: cfg.clientId, hostName: cfg.hostName, tool: tool, snapshotAt: nowMs, sessions: sessions, caps: capsFor(cfg), focusLocal: focusLocalFor(cfg) }
1223
1238
  }
1224
1239
 
1225
1240
  // ---------- 上报 ----------
@@ -3013,6 +3028,7 @@ module.exports = {
3013
3028
  parseWinProcJson,
3014
3029
  CAPS,
3015
3030
  capsFor,
3031
+ focusLocalFor,
3016
3032
  turnEvents,
3017
3033
  turnsOf,
3018
3034
  setTurns,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deskbuddy",
3
- "version": "0.2.0-beta.47",
3
+ "version": "0.2.0-beta.48",
4
4
  "description": "把 Claude Code、Codex 等 AI 编码智能体每个会话窗口的状态,实时同步到桌面硬件屏幕",
5
5
  "keywords": [
6
6
  "ai",