deskbuddy 0.2.0-beta.26 → 0.2.0-beta.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -716,7 +716,7 @@ var AutoEnabler = class {
716
716
  // src/daemon/index.ts
717
717
  import { randomBytes as randomBytes7 } from "crypto";
718
718
  import { existsSync as existsSync13, statSync as statSync13 } from "fs";
719
- import { basename as basename10, dirname as dirname10, join as join27 } from "path";
719
+ import { basename as basename10, dirname as dirname10, join as join28 } from "path";
720
720
  import WebSocket3 from "ws";
721
721
 
722
722
  // src/core/state.ts
@@ -1677,7 +1677,7 @@ function mapStatus(s2, decision) {
1677
1677
  import { randomUUID } from "crypto";
1678
1678
  import { lstatSync, readlinkSync, realpathSync as realpathSync2 } from "fs";
1679
1679
  import { homedir as homedir2 } from "os";
1680
- import { basename as basename4, dirname as dirname2, join as join10, posix as posix3, resolve as resolve2, win32 as win323 } from "path";
1680
+ import { basename as basename5, dirname as dirname2, join as join10, posix as posix3, resolve as resolve2, win32 as win323 } from "path";
1681
1681
 
1682
1682
  // src/daemon/gate/classify.ts
1683
1683
  import { isAbsolute, relative, resolve, sep } from "path";
@@ -2114,8 +2114,9 @@ var CMD_MATCH_MIN = 6;
2114
2114
  var CMD_MATCH_LEN = 80;
2115
2115
 
2116
2116
  // src/daemon/gate/options.ts
2117
- var OPTION_LABEL_MAX = 120;
2118
- var OPTION_DESC_MAX = 120;
2117
+ var OPTIONS_MAX = 16;
2118
+ var OPTION_LABEL_MAX = 2e3;
2119
+ var OPTION_DESC_MAX = 2e3;
2119
2120
  var ID_RE = /^[a-z0-9_]{1,16}$/;
2120
2121
  var KINDS = /* @__PURE__ */ new Set(["allow", "allow_session", "allow_always", "deny"]);
2121
2122
  function cleanOptionText(s2) {
@@ -2124,9 +2125,8 @@ function cleanOptionText(s2) {
2124
2125
  function checkOptions(set, limits = {}) {
2125
2126
  if (!set || !Array.isArray(set.options)) return null;
2126
2127
  const opts = set.options;
2127
- const unbounded = limits.unbounded === true;
2128
2128
  const anyKinds = limits.anyKinds === true;
2129
- if (opts.length < (anyKinds ? 1 : 2) || !unbounded && opts.length > 4) return null;
2129
+ if (opts.length < (anyKinds ? 1 : 2) || opts.length > OPTIONS_MAX) return null;
2130
2130
  const ids = /* @__PURE__ */ new Set();
2131
2131
  const out = [];
2132
2132
  for (const o of opts) {
@@ -2135,8 +2135,9 @@ function checkOptions(set, limits = {}) {
2135
2135
  if (!KINDS.has(o.kind)) return null;
2136
2136
  if (o.input === true && o.kind !== "deny") return null;
2137
2137
  const label = typeof o.label === "string" ? cleanOptionText(o.label) : "";
2138
- if (!label || !unbounded && label.length > OPTION_LABEL_MAX) return null;
2139
- const desc = typeof o.description === "string" ? unbounded ? cleanOptionText(o.description) : cleanOptionText(o.description).slice(0, OPTION_DESC_MAX) : "";
2138
+ if (!label || label.length > OPTION_LABEL_MAX) return null;
2139
+ const desc = typeof o.description === "string" ? cleanOptionText(o.description) : "";
2140
+ if (desc.length > OPTION_DESC_MAX) return null;
2140
2141
  const a = set.actions?.[o.id];
2141
2142
  if (!a || a.decision !== (o.kind === "deny" ? "deny" : "allow")) return null;
2142
2143
  if (a.defer === true && o.kind !== "deny") return null;
@@ -2417,13 +2418,10 @@ function qoderCnTuiKnown(toolName, toolInput, suggestions) {
2417
2418
  if (!s2 || s2.type !== "setMode" || s2.mode !== "acceptEdits" || s2.destination !== "session") return null;
2418
2419
  const set = qoderSet(...remember("session", QODER_TEXT.session, "allow_session", "\u89C4\u5219\uFF1A\u672C\u4F1A\u8BDD\u5207\u6362\u5230 acceptEdits\uFF08\u81EA\u52A8\u63A5\u53D7\u6587\u4EF6\u7F16\u8F91\uFF09"));
2419
2420
  if (!set) return null;
2420
- return checkOptions(
2421
- {
2422
- options: [...set.options, { id: "editor", label: QODER_TEXT.editor, description: QODER_TEXT.editorDesc, kind: "deny" }],
2423
- actions: { ...set.actions, editor: { decision: "deny", defer: true } }
2424
- },
2425
- { unbounded: true }
2426
- );
2421
+ return checkOptions({
2422
+ options: [...set.options, { id: "editor", label: QODER_TEXT.editor, description: QODER_TEXT.editorDesc, kind: "deny" }],
2423
+ actions: { ...set.actions, editor: { decision: "deny", defer: true } }
2424
+ });
2427
2425
  }
2428
2426
  if (toolName === "WebFetch") {
2429
2427
  const rc = s2 ? singleRule(s2, "WebFetch", "localSettings") : null;
@@ -2443,6 +2441,45 @@ function qoderCnTuiKnown(toolName, toolInput, suggestions) {
2443
2441
  }
2444
2442
  return null;
2445
2443
  }
2444
+ var QODER_SCREEN_UNKNOWN_DESC = "\u8981\u5728\u7535\u8111\u4E0A\u64CD\u4F5C\uFF0C\u8BBE\u5907\u4E0A\u9009\u8FD9\u4E00\u9879\u540E\u8BF7\u5230\u7535\u8111\u4E0A\u7EE7\u7EED";
2445
+ var SCREEN_DENY_RE = /^(?:No\b|Reject\b|Deny\b|Don'?t\b|Do not\b|Cancel\b|拒绝|不)/i;
2446
+ var SCREEN_INPUT_RE = /tell .+ what to do|type something|拒绝并说明/i;
2447
+ var SCREEN_ALLOW_RE = /^(?:Allow\b|Always\b|Yes\b|允许|始终允许|总是允许)/i;
2448
+ function qoderCnScreenOptions(labels, table) {
2449
+ if (!Array.isArray(labels) || labels.length < 2) return null;
2450
+ const main2 = [];
2451
+ const tail3 = [];
2452
+ const ids = /* @__PURE__ */ new Set();
2453
+ const take = (id, n) => ids.has(id) ? `k${n}` : id;
2454
+ for (let i = 0; i < labels.length; i++) {
2455
+ const n = i + 1;
2456
+ const label = cleanOptionText(String(labels[i] ?? ""));
2457
+ if (!label) return null;
2458
+ let pair;
2459
+ const known = table?.options.find((o) => o.label === label);
2460
+ const knownAct = known ? table?.actions[known.id] : void 0;
2461
+ if (label === QODER_TEXT.once) pair = [{ id: take("allow", n), label, kind: "allow" }, { decision: "allow" }];
2462
+ else if (label === QODER_TEXT.no) pair = [{ id: take("no", n), label, kind: "deny" }, { decision: "deny" }];
2463
+ else if (label === QODER_TEXT.rejectInput)
2464
+ pair = [{ id: take("deny", n), label, description: QODER_TEXT.rejectInputDesc, kind: "deny", input: true }, { decision: "deny" }];
2465
+ else if (label === QODER_TEXT.editor)
2466
+ pair = [{ id: take("editor", n), label, description: QODER_TEXT.editorDesc, kind: "deny" }, { decision: "deny", defer: true }];
2467
+ else if (known && knownAct && !knownAct.defer && known.kind !== "allow")
2468
+ pair = [{ ...known, id: take(known.id, n) }, knownAct];
2469
+ else if (SCREEN_DENY_RE.test(label))
2470
+ pair = [{ id: `k${n}`, label, kind: "deny", ...SCREEN_INPUT_RE.test(label) ? { input: true } : {} }, { decision: "deny" }];
2471
+ else if (SCREEN_ALLOW_RE.test(label))
2472
+ pair = [{ id: `k${n}`, label, kind: /session|会话/i.test(label) ? "allow_session" : "allow_always" }, { decision: "allow", screenKey: n }];
2473
+ else pair = [{ id: `k${n}`, label, description: QODER_SCREEN_UNKNOWN_DESC, kind: "deny" }, { decision: "deny", defer: true }];
2474
+ ids.add(pair[0].id);
2475
+ (pair[1].defer ? tail3 : main2).push(pair);
2476
+ }
2477
+ const all = [...main2, ...tail3];
2478
+ const options = all.map(([o]) => o);
2479
+ const actions = {};
2480
+ for (const [o, a] of all) actions[o.id] = a;
2481
+ return checkOptions({ options, actions });
2482
+ }
2446
2483
  var QODER_GUI_TEXT = {
2447
2484
  zh: {
2448
2485
  deny: "\u62D2\u7EDD",
@@ -2518,7 +2555,25 @@ function qoderCnGuiOptions(suggestions, lang) {
2518
2555
  options.push({ id, label: v.label, ...v.desc ? { description: v.desc } : {}, kind: "allow_session" });
2519
2556
  actions[id] = { decision: "allow", updatedPermissions: [s2] };
2520
2557
  }
2521
- return checkOptions({ options, actions }, { unbounded: true });
2558
+ return checkOptions({ options, actions });
2559
+ }
2560
+ var QWENWORK_TEXT = {
2561
+ zh: { deny: "\u62D2\u7EDD", allow: "\u5141\u8BB8" },
2562
+ "zh-TW": { deny: "\u62D2\u7D55", allow: "\u5141\u8A31" },
2563
+ en: { deny: "Deny", allow: "Allow" }
2564
+ };
2565
+ var QWENWORK_CONFIRM_TOOLS = /(^|__)qwenwork_pages_publish$/;
2566
+ function qwenworkOptions(toolName, lang) {
2567
+ if (QWENWORK_CONFIRM_TOOLS.test(toolName)) return codebuddyNoticeOptions();
2568
+ if (lang !== "zh" && lang !== "en" && lang !== "zh-TW") return null;
2569
+ const t = QWENWORK_TEXT[lang];
2570
+ return checkOptions({
2571
+ options: [
2572
+ { id: "deny", label: t.deny, kind: "deny" },
2573
+ { id: "allow", label: t.allow, kind: "allow" }
2574
+ ],
2575
+ actions: { deny: { decision: "deny" }, allow: { decision: "allow" } }
2576
+ });
2522
2577
  }
2523
2578
  function firstRememberOption(set) {
2524
2579
  return set?.options.find((o) => o.kind === "allow_session" || o.kind === "allow_always");
@@ -2598,10 +2653,537 @@ function codebuddyOptions(toolName, toolInput, product, ctx = {}) {
2598
2653
  function codebuddyNoticeOptions() {
2599
2654
  return checkOptions(
2600
2655
  { options: [{ id: "o0", label: CODEBUDDY_TEXT.noticeButton, kind: "deny" }], actions: { o0: { decision: "deny", defer: true } } },
2601
- { anyKinds: true, unbounded: true }
2656
+ { anyKinds: true }
2602
2657
  );
2603
2658
  }
2604
2659
 
2660
+ // src/wrap/answerKeys.ts
2661
+ var KEY_DOWN = "\x1B[B";
2662
+ var KEY_UP = "\x1B[A";
2663
+ var KEY_ENTER = "\r";
2664
+ var ARROW_GAP_MS = 150;
2665
+ var CONFIRM_MS = 4e3;
2666
+ var POLL_MS = 150;
2667
+ var FINGERPRINT_CHARS = 40;
2668
+ var FINGERPRINT_TAIL = 30;
2669
+ var FP_SEP = "\u2026";
2670
+ var CURSOR = "[\u276F>\u203A]";
2671
+ function joined(lines3) {
2672
+ return lines3.join("\n");
2673
+ }
2674
+ function squash(s2) {
2675
+ return s2.replace(/[\s\u2500-\u257F]+/g, "");
2676
+ }
2677
+ function basename4(p) {
2678
+ const parts = p.split(/[\\/]+/).filter(Boolean);
2679
+ return parts[parts.length - 1] ?? "";
2680
+ }
2681
+ function displayFingerprint(toolName, input) {
2682
+ const a = input && typeof input === "object" ? input : {};
2683
+ const s2 = (v) => typeof v === "string" ? v : "";
2684
+ const headTail = (v) => {
2685
+ const q = squash(v);
2686
+ return q.length > FINGERPRINT_CHARS + FINGERPRINT_TAIL ? q.slice(0, FINGERPRINT_CHARS) + FP_SEP + q.slice(-FINGERPRINT_TAIL) : q;
2687
+ };
2688
+ if (toolName === "Bash") return headTail(s2(a.command));
2689
+ if (toolName === "Write") {
2690
+ const name = squash(basename4(s2(a.file_path)));
2691
+ const body = squash(s2(a.content)).slice(0, 20);
2692
+ return name ? body ? name + FP_SEP + body : name : "";
2693
+ }
2694
+ if (["Edit", "MultiEdit", "Read", "NotebookEdit"].includes(toolName)) return squash(basename4(s2(a.file_path) || s2(a.notebook_path)));
2695
+ return headTail(Object.values(a).find((v) => typeof v === "string" && v.trim() !== "") ?? "").slice(0, 200);
2696
+ }
2697
+ var SETTLE_MS = 800;
2698
+ async function settled(io, ok) {
2699
+ if (!ok(io.screen())) return false;
2700
+ await io.sleep(SETTLE_MS);
2701
+ return ok(io.screen());
2702
+ }
2703
+ async function waitUntil(io, ok, ms = CONFIRM_MS) {
2704
+ const loops = Math.max(1, Math.ceil(ms / POLL_MS));
2705
+ for (let i = 0; i < loops; i++) {
2706
+ if (ok(io.screen())) return true;
2707
+ await io.sleep(POLL_MS);
2708
+ }
2709
+ return ok(io.screen());
2710
+ }
2711
+ function permissionRegion(lines3, toolName) {
2712
+ let q = -1;
2713
+ for (let i = lines3.length - 1; i >= 0; i--) {
2714
+ if (/^\s*Do you want to .+\?\s*$/.test(lines3[i])) {
2715
+ q = i;
2716
+ break;
2717
+ }
2718
+ }
2719
+ if (q < 0 || !toolName) return null;
2720
+ const head = new RegExp(`^\\s*\u25CF\\s*${toolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\(`);
2721
+ let h = -1;
2722
+ for (let i = q - 1; i >= 0; i--) {
2723
+ if (head.test(lines3[i])) {
2724
+ h = i;
2725
+ break;
2726
+ }
2727
+ if (/^\s*●/.test(lines3[i])) return null;
2728
+ }
2729
+ if (h < 0) return null;
2730
+ const after = joined(lines3.slice(q + 1, q + 6));
2731
+ if (!new RegExp(`^\\s*(?:${CURSOR}\\s*)?1\\.\\s+Yes\\s*$`, "m").test(after)) return null;
2732
+ if (!/^\s*(?:[❯>›]\s*)?3\.\s+No\b/m.test(after)) return null;
2733
+ return lines3.slice(h, q + 1);
2734
+ }
2735
+ function isPermissionDialog(lines3, toolName, fingerprint2) {
2736
+ if (!fingerprint2) return false;
2737
+ const region = permissionRegion(lines3, toolName);
2738
+ if (!region) return false;
2739
+ const text = squash(joined(region));
2740
+ return fingerprint2.split(FP_SEP).every((part) => !!part && text.includes(squash(part)));
2741
+ }
2742
+ async function answerPermission(io, toolName, fingerprint2, decision) {
2743
+ const ok = (l) => isPermissionDialog(l, toolName, fingerprint2);
2744
+ if (!await settled(io, ok)) return "not_waiting";
2745
+ io.write(decision === "allow" ? "1" : "3");
2746
+ return await waitUntil(io, (l) => !ok(l)) ? "done" : "partial";
2747
+ }
2748
+ var TAB_ROW = /←.*[☐☒].*Submit.*→/;
2749
+ function isQuestionDialog(lines3) {
2750
+ return lines3.some((l) => TAB_ROW.test(l));
2751
+ }
2752
+ function onQuestion(lines3, q) {
2753
+ const start = questionStart(lines3);
2754
+ if (start < 0) return false;
2755
+ const t = squash(q.question);
2756
+ return !!t && squash(joined(lines3.slice(start))).includes(t);
2757
+ }
2758
+ function isReviewPage(lines3) {
2759
+ const t = joined(lines3);
2760
+ return /Review your answers/.test(t) && /Submit answers/.test(t);
2761
+ }
2762
+ function questionStart(lines3) {
2763
+ for (let i = lines3.length - 1; i >= 0; i--) if (TAB_ROW.test(lines3[i])) return i + 1;
2764
+ return -1;
2765
+ }
2766
+ function optionLine(lines3, n, label) {
2767
+ const lab = squash(label).slice(0, 6);
2768
+ const start = questionStart(lines3);
2769
+ if (start < 0) return -1;
2770
+ for (let i = start; i < lines3.length; i++) {
2771
+ const m = lines3[i].match(/^\s*(?:[❯>›]\s*)?[☐☒]?\s*(\d+)\.\s+(.*)$/);
2772
+ if (m && Number(m[1]) === n && squash(m[2]).startsWith(lab)) return i;
2773
+ }
2774
+ return -1;
2775
+ }
2776
+ function checked(lines3, line) {
2777
+ return line >= 0 && /^\s*☒/.test(lines3[line]);
2778
+ }
2779
+ function singleCursor(lines3, q) {
2780
+ const n = q.options.length;
2781
+ const start = questionStart(lines3);
2782
+ if (start < 0) return -1;
2783
+ for (let i = start; i < lines3.length; i++) {
2784
+ const l = lines3[i];
2785
+ const m = l.match(new RegExp(`^\\s*${CURSOR}\\s*(\\d+)\\.\\s+(.*)$`));
2786
+ if (m) {
2787
+ const k = Number(m[1]) - 1;
2788
+ if (k >= 0 && k < n) return squash(m[2]).startsWith(squash(q.options[k].label).slice(0, 6)) ? k : -1;
2789
+ if (k === n) return /^Typesomething/.test(squash(m[2])) ? n : -1;
2790
+ return -1;
2791
+ }
2792
+ if (new RegExp(`^\\s*${CURSOR}\\s*(Next|Submit)\\s*$`).test(l)) return n + 1;
2793
+ }
2794
+ return -1;
2795
+ }
2796
+ async function move(io, delta) {
2797
+ const key = delta > 0 ? KEY_DOWN : KEY_UP;
2798
+ for (let i = 0; i < Math.abs(delta); i++) {
2799
+ io.write(key);
2800
+ await io.sleep(ARROW_GAP_MS);
2801
+ }
2802
+ }
2803
+ async function answerQuestions(io, questions, selections) {
2804
+ if (questions.length === 0 || selections.length !== questions.length) return "not_waiting";
2805
+ for (let qi = 0; qi < questions.length; qi++) {
2806
+ const sel = selections[qi];
2807
+ const q = questions[qi];
2808
+ if (sel.length === 0 || sel.some((i) => !Number.isInteger(i) || i < 0 || i >= q.options.length)) return "not_waiting";
2809
+ if (q.multiSelect !== true && sel.length !== 1) return "not_waiting";
2810
+ }
2811
+ if (!await settled(io, (l) => onQuestion(l, questions[0]))) return "not_waiting";
2812
+ if (questions.some((q) => q.multiSelect === true) && (!io.cursorLine || io.cursorLine() === null)) return "unsupported";
2813
+ let wrote = false;
2814
+ const fail = () => wrote ? "partial" : "not_waiting";
2815
+ for (let qi = 0; qi < questions.length; qi++) {
2816
+ const q = questions[qi];
2817
+ const n = q.options.length;
2818
+ const sel = [...selections[qi]].sort((a, b) => a - b);
2819
+ if (!await waitUntil(io, (l) => onQuestion(l, q))) return fail();
2820
+ if (q.multiSelect === true) {
2821
+ const lines0 = io.screen();
2822
+ for (let i = 0; i < n; i++) if (checked(lines0, optionLine(lines0, i + 1, q.options[i].label))) return fail();
2823
+ const first = optionLine(lines0, 1, q.options[0].label);
2824
+ if (first < 0 || io.cursorLine?.() !== first) return fail();
2825
+ let cur = 0;
2826
+ for (const t of sel) {
2827
+ await move(io, t - cur);
2828
+ cur = t;
2829
+ const onTarget = await waitUntil(io, (l) => {
2830
+ const ln = optionLine(l, t + 1, q.options[t].label);
2831
+ return ln >= 0 && io.cursorLine?.() === ln;
2832
+ });
2833
+ if (!onTarget) return fail();
2834
+ io.write(KEY_ENTER);
2835
+ wrote = true;
2836
+ if (!await waitUntil(io, (l) => checked(l, optionLine(l, t + 1, q.options[t].label)))) return fail();
2837
+ }
2838
+ await move(io, n + 1 - cur);
2839
+ if (!await waitUntil(io, (l) => questionStart(l) >= 0 && new RegExp(`^\\s*${CURSOR}\\s*(Submit|Next)\\s*$`, "m").test(joined(l.slice(questionStart(l)))))) return fail();
2840
+ io.write(KEY_ENTER);
2841
+ wrote = true;
2842
+ } else {
2843
+ const t = sel[0];
2844
+ const cur = singleCursor(io.screen(), q);
2845
+ if (cur < 0) return fail();
2846
+ await move(io, t - cur);
2847
+ if (!await waitUntil(io, (l) => singleCursor(l, q) === t && optionLine(l, t + 1, q.options[t].label) >= 0)) return fail();
2848
+ io.write(KEY_ENTER);
2849
+ wrote = true;
2850
+ }
2851
+ const next = questions[qi + 1];
2852
+ const moved = await waitUntil(io, (l) => next ? onQuestion(l, next) : isReviewPage(l) || !isQuestionDialog(l));
2853
+ if (!moved) return fail();
2854
+ }
2855
+ const s2 = io.screen();
2856
+ if (!isReviewPage(s2)) return isQuestionDialog(s2) ? fail() : "done";
2857
+ if (!new RegExp(`^\\s*${CURSOR}\\s*1\\.\\s+Submit answers`, "m").test(joined(s2))) return fail();
2858
+ io.write(KEY_ENTER);
2859
+ return await waitUntil(io, (l) => !isReviewPage(l) && !isQuestionDialog(l)) ? "done" : "partial";
2860
+ }
2861
+ function stillWaiting(lines3, probe) {
2862
+ if (probe.questions && probe.questions.length > 0) return probe.questions.some((q) => onQuestion(lines3, q)) || isReviewPage(lines3);
2863
+ return isPermissionDialog(lines3, probe.toolName, probe.fingerprint ?? "");
2864
+ }
2865
+
2866
+ // src/wrap/screen.ts
2867
+ var ESC = "\x1B";
2868
+ var PASTE_BEGIN = ESC + "[200~";
2869
+ var PASTE_END = ESC + "[201~";
2870
+ function sanitize(text) {
2871
+ return String(text).replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
2872
+ }
2873
+ var MAX_INJECT_CHARS = 800;
2874
+ var TEXT_TOO_LONG = "text_too_long";
2875
+ function oneLine(text) {
2876
+ return sanitize(text).replace(/\s*\n\s*/g, " ").trim();
2877
+ }
2878
+ function tooLongToInject(text) {
2879
+ return oneLine(text).length > MAX_INJECT_CHARS;
2880
+ }
2881
+ function prepare(text) {
2882
+ return " " + oneLine(text);
2883
+ }
2884
+ var TRUST_RE = /trust (the files in )?this folder|Yes, I trust this folder/i;
2885
+ var PLACEHOLDER_RE = /^Try ".*"$/;
2886
+ function isTerminalReport(s2) {
2887
+ if (!s2) return true;
2888
+ const rest = s2.replace(/\x1b\[[IO]/g, "").replace(/\x1b\[\d+;\d+R/g, "").replace(/\x1b\[[?>][\d;]*\$?[a-zA-Z]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1bP[^\x1b]*\x1b\\/g, "");
2889
+ return rest === "";
2890
+ }
2891
+ var PERMISSION_RE = /Do you want to (proceed|make|create|run|allow)|[❯>]\s*1\. Yes/;
2892
+ function inspectLines(lines3, undimmed) {
2893
+ const text = lines3.join("\n");
2894
+ const trust = TRUST_RE.test(text);
2895
+ const permission = PERMISSION_RE.test(text);
2896
+ const rules = [];
2897
+ lines3.forEach((l, i) => {
2898
+ if (/^─{20,}/.test(l.trim())) rules.push(i);
2899
+ });
2900
+ let input = null;
2901
+ if (rules.length >= 2) {
2902
+ const a = rules[rules.length - 2];
2903
+ const b = rules[rules.length - 1];
2904
+ const src = undimmed && undimmed.length === lines3.length ? undimmed : lines3;
2905
+ input = src.slice(a + 1, b).map((l) => l.trimEnd()).join("\n").replace(/^\s*[❯>]\s?/, "").trimEnd();
2906
+ }
2907
+ const v = { permission, input };
2908
+ if (trust) v.trust = true;
2909
+ if (input !== null && PLACEHOLDER_RE.test(input.trim())) v.placeholder = true;
2910
+ return v;
2911
+ }
2912
+ function boxMatches(view, body) {
2913
+ if (view.input === null || view.permission) return false;
2914
+ const flat2 = (x) => x.replace(/\s+/g, "");
2915
+ return flat2(view.input) === flat2(body) || /\[Pasted text #\d+/.test(view.input);
2916
+ }
2917
+
2918
+ // src/wrap/screenRules.ts
2919
+ var Q_RULE_RE = /^(?:─{20,}|-{20,})$/;
2920
+ var Q_TRUST_RE = /Do you trust the files in this folder\?/;
2921
+ var Q_PERMISSION_RE = /Permission Required|Apply this change\?|\d\. Allow once|\d\. Allow for this session/;
2922
+ var Q_LOGIN_RE = /Sign in to continue|Login with QoderCN|How would you like to authenticate/;
2923
+ var Q_BUSY_RE = /\(esc to cancel\b/;
2924
+ var Q_PLACEHOLDER_RE = /^Type your message or @\S*$/;
2925
+ var Q_FOOTER_RE = /· ctx\b/;
2926
+ function bottomRegion(lines3, rules) {
2927
+ const anchor = rules.length >= 3 ? rules[rules.length - 3] : rules[rules.length - 1];
2928
+ return lines3.slice(Math.max(0, anchor - 1)).join("\n");
2929
+ }
2930
+ function qoderCnRule(lines3) {
2931
+ const rules = [];
2932
+ lines3.forEach((l, i) => {
2933
+ if (Q_RULE_RE.test(l.trim())) rules.push(i);
2934
+ });
2935
+ if (rules.length === 0) return { permission: false, input: null };
2936
+ const text = bottomRegion(lines3, rules);
2937
+ if (Q_TRUST_RE.test(text)) return { permission: false, input: null, trust: true };
2938
+ if (Q_LOGIN_RE.test(text)) return { permission: false, input: null, dialog: "login" };
2939
+ const permission = Q_PERMISSION_RE.test(text);
2940
+ let input = null;
2941
+ if (rules.length >= 2) {
2942
+ const a = rules[rules.length - 2];
2943
+ const b = rules[rules.length - 1];
2944
+ const box = lines3.slice(a + 1, b).map((l) => l.trimEnd());
2945
+ if (box.length > 0 && /^\s*>/.test(box[0])) input = box.join("\n").replace(/^\s*>\s?/, "").trim();
2946
+ }
2947
+ const v = { permission, input };
2948
+ if (Q_BUSY_RE.test(text)) v.busy = true;
2949
+ if (input !== null && Q_PLACEHOLDER_RE.test(input)) v.placeholder = true;
2950
+ else if (input !== null && input.startsWith("/")) v.dialog = "slash-menu";
2951
+ if (!v.dialog && lines3.slice(rules[rules.length - 1] + 1).some((l) => l.trim() !== "" && !Q_FOOTER_RE.test(l))) v.dialog = "popup";
2952
+ return v;
2953
+ }
2954
+ function textRejectFor(tool) {
2955
+ if (tool !== "qoder_cn") return void 0;
2956
+ return (text) => /^[/!]/.test(oneLine(text)) ? "slash_command" : null;
2957
+ }
2958
+ var RULES = {
2959
+ claude_code: inspectLines,
2960
+ qoder_cn: qoderCnRule
2961
+ };
2962
+ function screenRuleFor(tool) {
2963
+ return RULES[tool] ?? (() => null);
2964
+ }
2965
+ var INJECT_READY = {
2966
+ claude_code: true,
2967
+ qoder_cn: true
2968
+ };
2969
+ function injectReady(tool) {
2970
+ return INJECT_READY[tool] === true;
2971
+ }
2972
+
2973
+ // src/wrap/qoderPermBox.ts
2974
+ var TITLE_RE = /^\s*Permission Required\s*$/;
2975
+ var TOOL_RE = /^\s*Tool:\s*(\S.*?)\s*$/;
2976
+ var FILE_RE = /^\s*File:\s*(\S.*?)\s*$/;
2977
+ var OPTION_RE = /^(\s*)(?:([❯>])\s*)?(\d+)\.\s+(\S.*?)\s*$/;
2978
+ var HINT_RE = /\b(?:esc|enter|tab|ctrl|shift)\b|[↑↓←→]|\bto (?:cancel|select|confirm|navigate|choose)\b|按\s*\S+\s*(?:取消|确认|选择)/i;
2979
+ function squash2(s2) {
2980
+ return s2.replace(/[\s─-╿|+]+/g, "");
2981
+ }
2982
+ function slash(p) {
2983
+ return p.replace(/\\/g, "/").replace(/\/+/g, "/");
2984
+ }
2985
+ function qoderBoxFingerprint(toolName, input) {
2986
+ const a = input && typeof input === "object" ? input : {};
2987
+ const s2 = (v) => typeof v === "string" ? v : "";
2988
+ if (toolName === "Bash") {
2989
+ const c = squash2(s2(a.command));
2990
+ return c ? `cmd:${c}` : "";
2991
+ }
2992
+ if (["Write", "Edit", "MultiEdit", "Read", "NotebookEdit"].includes(toolName)) {
2993
+ const p = s2(a.file_path) || s2(a.notebook_path);
2994
+ return p.trim() ? `file:${slash(p.trim())}` : "";
2995
+ }
2996
+ return "";
2997
+ }
2998
+ function parseQoderPermissionBox(lines3) {
2999
+ const view = qoderCnRule(lines3);
3000
+ if (!view || !view.permission || view.trust) return null;
3001
+ let title = -1;
3002
+ for (let i = lines3.length - 1; i >= 0; i--) {
3003
+ if (TITLE_RE.test(lines3[i])) {
3004
+ title = i;
3005
+ break;
3006
+ }
3007
+ }
3008
+ if (title < 0) return null;
3009
+ const region = lines3.slice(title);
3010
+ let tool = "";
3011
+ let file3;
3012
+ for (const l of region) {
3013
+ const m = l.match(TOOL_RE);
3014
+ if (m && !tool) tool = m[1];
3015
+ const f = l.match(FILE_RE);
3016
+ if (f && file3 === void 0) file3 = f[1];
3017
+ }
3018
+ if (!tool) return null;
3019
+ let best = null;
3020
+ let cur = [];
3021
+ const close = () => {
3022
+ if (cur.length >= 2) best = cur;
3023
+ cur = [];
3024
+ };
3025
+ region.forEach((l, idx) => {
3026
+ const m = l.match(OPTION_RE);
3027
+ if (m) {
3028
+ const n = Number(m[3]);
3029
+ if (n === 1) close();
3030
+ else if (cur.length === 0 || n !== cur[cur.length - 1].n + 1) {
3031
+ close();
3032
+ return;
3033
+ }
3034
+ const col = l.length - l.trimStart().length + (m[2] ? m[2].length : 0);
3035
+ cur.push({ n, label: m[4], selected: !!m[2], col: l.indexOf(m[4], col), rows: [idx] });
3036
+ return;
3037
+ }
3038
+ const last = cur[cur.length - 1];
3039
+ if (last && l.trim() !== "" && l.length - l.trimStart().length >= last.col && !HINT_RE.test(l)) {
3040
+ last.label = `${last.label} ${l.trim()}`;
3041
+ last.rows.push(idx);
3042
+ return;
3043
+ }
3044
+ close();
3045
+ });
3046
+ close();
3047
+ const opts = best;
3048
+ if (!opts || opts.filter((o) => o.selected).length !== 1) return null;
3049
+ const optRows = new Set(opts.flatMap((o) => o.rows));
3050
+ return {
3051
+ tool,
3052
+ ...file3 !== void 0 ? { file: file3 } : {},
3053
+ options: opts.map(({ n, label, selected }) => ({ n, label, selected })),
3054
+ body: squash2(region.filter((_, i) => !optRows.has(i)).join("\n"))
3055
+ };
3056
+ }
3057
+ function boxIsFor(box, toolName, fingerprint2) {
3058
+ if (!box || box.tool !== toolName) return false;
3059
+ if (!fingerprint2) return true;
3060
+ if (fingerprint2.startsWith("cmd:")) return box.body.includes(fingerprint2.slice(4));
3061
+ if (fingerprint2.startsWith("file:")) {
3062
+ const full = fingerprint2.slice(5);
3063
+ const shown = box.file ? slash(box.file.trim()) : "";
3064
+ return !!shown && (full === shown || full.endsWith(shown.startsWith("/") ? shown : `/${shown}`));
3065
+ }
3066
+ return false;
3067
+ }
3068
+ function sameLabels(box, labels) {
3069
+ return box.options.length === labels.length && box.options.every((o, i) => o.label === labels[i]);
3070
+ }
3071
+ var QoderBoxTracker = class {
3072
+ #id = 0;
3073
+ #sig = null;
3074
+ #box = null;
3075
+ update(lines3) {
3076
+ const box = lines3 ? parseQoderPermissionBox(lines3) : null;
3077
+ const sig = box ? JSON.stringify([box.tool, box.file ?? "", box.body, box.options.map((o) => o.label)]) : null;
3078
+ if (sig !== null && sig !== this.#sig) this.#id++;
3079
+ this.#sig = sig;
3080
+ this.#box = box;
3081
+ }
3082
+ current() {
3083
+ return this.#box ? { id: this.#id, box: this.#box } : null;
3084
+ }
3085
+ };
3086
+ function snap(io) {
3087
+ io.track.update(io.screen());
3088
+ return io.track.current();
3089
+ }
3090
+ var READ_POLL_MS = 250;
3091
+ var READ_WAIT_MS = 2500;
3092
+ async function readQoderBox(io, toolName, fingerprint2, waitMs = READ_WAIT_MS) {
3093
+ const loops = Math.max(2, Math.ceil(waitMs / READ_POLL_MS) + 1);
3094
+ let prev = null;
3095
+ let prevOther = null;
3096
+ for (let i = 0; i < loops; i++) {
3097
+ const cur = snap(io);
3098
+ if (cur && boxIsFor(cur.box, toolName, fingerprint2)) {
3099
+ prevOther = null;
3100
+ const labels = cur.box.options.map((o) => o.label);
3101
+ const key = JSON.stringify([cur.id, labels]);
3102
+ if (key === prev) return { labels, box: cur.id };
3103
+ prev = key;
3104
+ } else {
3105
+ prev = null;
3106
+ if (cur && prevOther === cur.id) return null;
3107
+ prevOther = cur ? cur.id : null;
3108
+ }
3109
+ if (i < loops - 1) await io.sleep(READ_POLL_MS);
3110
+ }
3111
+ return null;
3112
+ }
3113
+ var KEY_GAP_MS = 120;
3114
+ var CONFIRM_MS2 = 3e3;
3115
+ var SETTLE_MS2 = 300;
3116
+ async function waitUntil2(io, ok, ms = CONFIRM_MS2) {
3117
+ const loops = Math.max(1, Math.ceil(ms / 150));
3118
+ for (let i = 0; i < loops; i++) {
3119
+ if (ok()) return true;
3120
+ await io.sleep(150);
3121
+ }
3122
+ return ok();
3123
+ }
3124
+ var dropAnswerKeys = (s2) => /[\r\n]/.test(s2) || /^[0-9]+$/.test(s2);
3125
+ async function pressQoderOption(io, toolName, fingerprint2, labels, n, boxId) {
3126
+ if (!Number.isInteger(n) || n < 1 || n > labels.length) return "not_waiting";
3127
+ const box = () => {
3128
+ const c = snap(io);
3129
+ return c && c.id === boxId && boxIsFor(c.box, toolName, fingerprint2) && sameLabels(c.box, labels) ? c.box : null;
3130
+ };
3131
+ if (!box()) return "not_waiting";
3132
+ await io.sleep(SETTLE_MS2);
3133
+ const b0 = box();
3134
+ if (!b0) return "not_waiting";
3135
+ const from = b0.options.findIndex((o) => o.selected) + 1;
3136
+ if (from < 1) return "not_waiting";
3137
+ io.hold?.(true);
3138
+ let drop;
3139
+ try {
3140
+ const delta = n - from;
3141
+ const key = delta > 0 ? KEY_DOWN : KEY_UP;
3142
+ const back = delta > 0 ? KEY_UP : KEY_DOWN;
3143
+ let moved = 0;
3144
+ for (let i = 0; i < Math.abs(delta); i++) {
3145
+ io.write(key);
3146
+ moved++;
3147
+ await io.sleep(KEY_GAP_MS);
3148
+ }
3149
+ const onTarget = await waitUntil2(io, () => box()?.options[n - 1].selected === true);
3150
+ if (!onTarget) {
3151
+ if (moved === 0) return "not_waiting";
3152
+ for (let i = 0; i < moved; i++) {
3153
+ io.write(back);
3154
+ await io.sleep(KEY_GAP_MS);
3155
+ }
3156
+ return await waitUntil2(io, () => box()?.options[from - 1].selected === true) ? "restored" : "cursor_moved";
3157
+ }
3158
+ io.write(KEY_ENTER);
3159
+ if (!await waitUntil2(io, () => !box())) return "unconfirmed";
3160
+ drop = dropAnswerKeys;
3161
+ return "done";
3162
+ } finally {
3163
+ io.hold?.(false, drop);
3164
+ }
3165
+ }
3166
+ async function applyQoderAnswer(req, io) {
3167
+ const a = req.answer;
3168
+ if (a.kind !== "qoder-read" && a.kind !== "qoder-key") return null;
3169
+ const id = req.instructionId;
3170
+ if (req.expireAt > 0 && Date.now() > req.expireAt) return { instructionId: id, status: "rejected", reason: "expired" };
3171
+ if (!io.screen()) return { instructionId: id, status: "rejected", reason: "no_screen" };
3172
+ if (a.kind === "qoder-read") {
3173
+ const r2 = await readQoderBox(io, a.toolName, a.fingerprint ?? "");
3174
+ return r2 ? { instructionId: id, status: "started", mode: "window", options: r2.labels, box: r2.box } : { instructionId: id, status: "rejected", reason: "not_waiting" };
3175
+ }
3176
+ let r;
3177
+ try {
3178
+ r = await pressQoderOption(io, a.toolName, a.fingerprint ?? "", a.labels, a.n, a.box);
3179
+ } catch {
3180
+ r = "unconfirmed";
3181
+ }
3182
+ if (r === "done") return { instructionId: id, status: "started", mode: "window" };
3183
+ if (r === "not_waiting" || r === "restored" || r === "cursor_moved") return { instructionId: id, status: "rejected", reason: r };
3184
+ return { instructionId: id, status: "failed", mode: "window", reason: "unconfirmed" };
3185
+ }
3186
+
2605
3187
  // src/daemon/gate/coordinator.ts
2606
3188
  var REMEMBER_TOOLS = /* @__PURE__ */ new Set([
2607
3189
  "claude_code",
@@ -2620,7 +3202,7 @@ var REMEMBER_SCOPE = {
2620
3202
  };
2621
3203
  function rememberScopeOf(tool, ui) {
2622
3204
  if (tool === "claude_code") return "all";
2623
- if (tool === "qoder_cn" && ui === "tui") return "local";
3205
+ if ((tool === "qoder_cn" || tool === "qoder") && ui === "tui") return "local";
2624
3206
  return "session";
2625
3207
  }
2626
3208
  function pickRememberSuggestion(suggestions, opts = {}) {
@@ -2685,12 +3267,14 @@ function rememberUpdatesOf(rec) {
2685
3267
  function windowOptions(tool, toolName, hook, viaGate, host = {}) {
2686
3268
  const ui = host.ui;
2687
3269
  if (!viaGate && hook.deskbuddy_options !== void 0)
2688
- return checkOptions(hook.deskbuddy_options, tool === "workbuddy" ? { anyKinds: true, unbounded: true } : {});
3270
+ return checkOptions(hook.deskbuddy_options, tool === "workbuddy" ? { anyKinds: true } : {});
2689
3271
  if (tool === "claude_code")
2690
3272
  return claudeOptions(toolName, hook.permission_suggestions, { autoModeAllowed: host.ccAutoMode === "on", permissionMode: hook.permission_mode });
2691
3273
  if (tool === "opencode") return opencodeOptions(hook.oc_permission, hook.oc_always, hook.oc_child);
2692
- if (tool === "qoder_cn" && ui === "tui") return qoderCnTuiOptions(toolName, hook.tool_input, hook.permission_suggestions);
2693
- if (tool === "qoder_cn" && ui === "gui") return qoderCnGuiOptions(hook.permission_suggestions, host.lang);
3274
+ const isQoder = tool === "qoder_cn" || tool === "qoder";
3275
+ if (isQoder && ui === "tui") return qoderCnTuiOptions(toolName, hook.tool_input, hook.permission_suggestions);
3276
+ if (isQoder && ui === "gui") return qoderCnGuiOptions(hook.permission_suggestions, host.lang);
3277
+ if (tool === "qwenwork" && !viaGate) return qwenworkOptions(toolName, host.lang);
2694
3278
  if (tool === "workbuddy" && hook.deskbuddy_cb_product !== void 0)
2695
3279
  return codebuddyOptions(toolName, hook.tool_input, hook.deskbuddy_cb_product, { truncated: hook.deskbuddy_cb_truncated === true, sandbox: hook.deskbuddy_cb_box === "sandbox" });
2696
3280
  return null;
@@ -2930,7 +3514,33 @@ var ApprovalCoordinator = class {
2930
3514
  const cardSummary = ownSummary || summary2;
2931
3515
  const inProject = kind === "edit" && !qv && !trust && !!cwd && editInProject(toolName, hook.tool_input, cwdForms(cwd), { real: realpathNearest });
2932
3516
  const remember = qv || trust ? null : tool === "codex" ? codexRemember(hook.deskbuddy_remember) : REMEMBER_TOOLS.has(tool) ? pickRememberSuggestion(hook.permission_suggestions, { edit: EDIT_TOOLS2.has(toolName) || kind === "edit", scope: rememberScopeOf(tool, host.ui) }) : null;
2933
- const optSet = qv || trust ? null : windowOptions(tool, toolName, hook, viaGate, host);
3517
+ let appSet = null;
3518
+ const appByInput = !(typeof hook.tool_use_id === "string" && hook.tool_use_id);
3519
+ const twin = appByInput && this.#d.store.pendingForSession(sessionId).some((r) => r.mode === "window" && r.fingerprint === fingerprint(toolName, hook.tool_input));
3520
+ if (!qv && !trust && !twin && viaGate && (tool === "qoder_cn" || tool === "qoder") && host.ui !== "tui" && this.#d.qoderApp) {
3521
+ appSet = await this.#d.qoderApp.options(tool, hook, signal).catch(() => null);
3522
+ if (signal.aborted) return none;
3523
+ }
3524
+ let optSet = qv || trust ? null : appSet ?? windowOptions(tool, toolName, hook, viaGate, host);
3525
+ let screenBox;
3526
+ const sameToolPending = this.#d.store.pending().some((r) => r.mode === "window" && r.sessionId === sessionId && this.#windows.get(r.approvalId)?.toolName === toolName);
3527
+ if (!qv && !trust && tool === "qoder_cn" && host.ui === "tui" && this.#d.qoderScreen && !sameToolPending) {
3528
+ const fp = qoderBoxFingerprint(toolName, hook.tool_input);
3529
+ let got = null;
3530
+ try {
3531
+ got = await this.#d.qoderScreen.read(sessionId, toolName, fp);
3532
+ } catch {
3533
+ got = null;
3534
+ }
3535
+ const taken = !!got && this.#d.store.pending().some((r) => r.mode === "window" && r.sessionId === sessionId && r.screenBox?.box === got?.box);
3536
+ const s2 = got && !taken ? qoderCnScreenOptions(got.labels, optSet) : null;
3537
+ if (s2 && got) {
3538
+ optSet = s2;
3539
+ screenBox = { toolName, fingerprint: fp, labels: got.labels, box: got.box };
3540
+ } else if (taken) this.#d.log(`Qoder \u6388\u6743\u6846\u5B9E\u4F8B ${got?.box} \u5DF2\u5C5E\u4E8E\u522B\u7684\u5F85\u6279\u51C6\u5361\uFF0C\u6309\u5DE5\u5177\u67E5\u8868`);
3541
+ else if (got) this.#d.log(`Qoder \u6388\u6743\u6846\u539F\u6587\u6620\u5C04\u4E0D\u8FC7\u81EA\u68C0\uFF0C\u6309\u5DE5\u5177\u67E5\u8868\uFF1A${got.labels.join(" / ").slice(0, 200)}`);
3542
+ if (signal.aborted) return none;
3543
+ }
2934
3544
  const rec = {
2935
3545
  approvalId: randomUUID(),
2936
3546
  runId: "",
@@ -2945,6 +3555,7 @@ var ApprovalCoordinator = class {
2945
3555
  createdAt: this.#now(),
2946
3556
  ...remember ? { remember: remember.text, rememberUpdates: remember.updates } : {},
2947
3557
  ...optSet ? { options: optSet.options, optionActions: optSet.actions, ...optSet.pcAllowAmbiguous ? { pcAllowAmbiguous: true } : {} } : {},
3558
+ ...screenBox ? { screenBox } : {},
2948
3559
  ...inProject ? { inProject: true } : {},
2949
3560
  ...qv ? (() => {
2950
3561
  const context = lastAssistantText(hook.transcript_path);
@@ -3299,6 +3910,10 @@ var ApprovalCoordinator = class {
3299
3910
  }
3300
3911
  rec.optionId = optionId;
3301
3912
  }
3913
+ if (action?.screenKey !== void 0 && decision === "allow" && rec.mode === "window") {
3914
+ this.#pressScreenKey(rec, action.screenKey, deviceName, remembered);
3915
+ return;
3916
+ }
3302
3917
  const updates = optionId === void 0 ? rememberUpdatesOf(rec) : null;
3303
3918
  const always = optionId === void 0 && decision === "allow_always" && rec.mode === "window" && updates !== null;
3304
3919
  if (always) remembered = true;
@@ -3327,7 +3942,20 @@ var ApprovalCoordinator = class {
3327
3942
  };
3328
3943
  if (decision === "allow") {
3329
3944
  const perms = action ? action.updatedPermissions : always && updates ? updates : void 0;
3330
- waiter({ decision: "allow", notice, ...perms && perms.length > 0 ? { updatedPermissions: perms } : {}, ...opt });
3945
+ const reply = {
3946
+ decision: "allow",
3947
+ notice,
3948
+ ...perms && perms.length > 0 ? { updatedPermissions: perms } : {},
3949
+ ...action?.updatedInput ? { updatedInput: action.updatedInput } : {},
3950
+ ...opt
3951
+ };
3952
+ const viaApp = action?.qoderApp;
3953
+ if (viaApp && this.#d.qoderApp) {
3954
+ void this.#d.qoderApp.respond(rec.tool, rec.sessionId, viaApp.toolUseId, viaApp.permissionSuggestionId).catch(() => "failed").then((r) => {
3955
+ this.#d.log(`\u7A97\u53E3\u6388\u6743 ${approvalId} \u5207\u6A21\u5F0F\u5EFA\u8BAE\u7ECF App \u56DE\u7B54\uFF1A${r}`);
3956
+ waiter(r === "failed" ? reply : { decision: "none" });
3957
+ });
3958
+ } else waiter(reply);
3331
3959
  } else {
3332
3960
  waiter({
3333
3961
  decision: "deny",
@@ -3358,6 +3986,48 @@ var ApprovalCoordinator = class {
3358
3986
  if (decision === "allow") this.#d.resume(rec, "orphan");
3359
3987
  }
3360
3988
  /** 窗口待批准按「不表态」收掉:本地作废、放掉挂着的 hook(回 none,框留给电脑前的人)。服务端那条已有结果,不撤 */
3989
+ /**
3990
+ * 设备选了只能写键的屏幕选项(spec 2026-10-09 qodercn-tui-screen-options §4.3):
3991
+ * 先把记录标成已决定、取走 waiter(重复下发 / 撤卡扫描都碰不到它),再经窗口通道写键;不管成败 hook 都回 none。
3992
+ * 写不成(框变了 / 电脑上已处理 / hook 已不在)→ 一个键不写,电脑上的框照旧等人,记录改 cancelled
3993
+ */
3994
+ #pressScreenKey(rec, n, deviceName, remembered) {
3995
+ rec.status = "approved";
3996
+ rec.decidedAt = this.#now();
3997
+ rec.consumed = true;
3998
+ this.#windows.delete(rec.approvalId);
3999
+ const waiter = this.#waiters.get(rec.approvalId);
4000
+ this.#waiters.delete(rec.approvalId);
4001
+ const box = rec.screenBox;
4002
+ const qs = this.#d.qoderScreen;
4003
+ const fail = (why) => {
4004
+ rec.status = "cancelled";
4005
+ this.#d.store.save();
4006
+ this.#d.log(`\u7A97\u53E3\u6388\u6743 ${rec.approvalId} \u8BBE\u5907\u9009\u4E86\u7B2C ${n} \u9879\uFF0C${why}\uFF1A\u6CA1\u6709\u56DE\u8F66\uFF0C\u7535\u8111\u4E0A\u7167\u65E7\u7B49\u4EBA\u5904\u7406`);
4007
+ void this.#cancelRemote(rec.approvalId, "\u8BBE\u5907\u4E0A\u7684\u9009\u62E9\u672A\u751F\u6548\uFF0C\u8BF7\u5230\u7535\u8111\u4E0A\u5904\u7406");
4008
+ };
4009
+ this.#d.store.save();
4010
+ if (!waiter || !box || !qs) {
4011
+ fail(!waiter ? "hook \u5DF2\u4E0D\u5728" : "\u6CA1\u6709\u8BFB\u5C4F\u8BB0\u5F55");
4012
+ waiter?.({ decision: "none" });
4013
+ return;
4014
+ }
4015
+ void qs.press(rec.sessionId, box, n).catch(() => "uncertain").then((r) => {
4016
+ if (r === "done") {
4017
+ this.#d.log(`\u7A97\u53E3\u6388\u6743 ${rec.approvalId} \u8BBE\u5907\u9009\u4E86\u7B2C ${n} \u9879\uFF0C\u5DF2\u5199\u952E\u4F5C\u7B54`);
4018
+ try {
4019
+ ;
4020
+ (this.#d.writeNotice ?? writeNotice)(rec.sessionId, windowNotice("allow", rec.summary, deviceName) + (remembered ? "\uFF08\u5DF2\u8BB0\u4F4F\uFF09" : ""));
4021
+ } catch {
4022
+ }
4023
+ } else if (r === "not_written") fail("\u5C4F\u4E0A\u5DF2\u4E0D\u662F\u540C\u4E00\u4E2A\u6388\u6743\u6846");
4024
+ else {
4025
+ this.#d.log(`\u7A97\u53E3\u6388\u6743 ${rec.approvalId} \u8BBE\u5907\u9009\u4E86\u7B2C ${n} \u9879\uFF0C\u5DF2\u5199\u56DE\u8F66\u4F46\u6CA1\u786E\u8BA4\u6846\u5173\u95ED\uFF0C\u7ED3\u679C\u672A\u786E\u8BA4`);
4026
+ void this.#cancelRemote(rec.approvalId, "\u8BBE\u5907\u4E0A\u7684\u9009\u62E9\u53EF\u80FD\u672A\u751F\u6548\uFF0C\u8BF7\u5230\u7535\u8111\u4E0A\u786E\u8BA4");
4027
+ }
4028
+ waiter({ decision: "none" });
4029
+ });
4030
+ }
3361
4031
  #dropWindow(rec) {
3362
4032
  rec.status = "cancelled";
3363
4033
  rec.decidedAt = this.#now();
@@ -3547,7 +4217,7 @@ function realpathNearest(p) {
3547
4217
  }
3548
4218
  const parent = dirname2(cur);
3549
4219
  if (parent === cur) return "";
3550
- tail3.push(basename4(cur));
4220
+ tail3.push(basename5(cur));
3551
4221
  cur = parent;
3552
4222
  }
3553
4223
  return "";
@@ -3906,8 +4576,8 @@ var SeenIds = class _SeenIds {
3906
4576
 
3907
4577
  // src/daemon/qwenworkBridge.ts
3908
4578
  import { closeSync as closeSync4, openSync as openSync4, readdirSync as readdirSync8, readFileSync as readFileSync9, readSync as readSync3 } from "fs";
3909
- import { homedir as homedir3 } from "os";
3910
- import { basename as basename5, dirname as dirname3, join as join15, resolve as resolve3 } from "path";
4579
+ import { homedir as homedir4 } from "os";
4580
+ import { basename as basename6, dirname as dirname3, join as join16, resolve as resolve3 } from "path";
3911
4581
 
3912
4582
  // src/daemon/dumateWatch.ts
3913
4583
  import { execFileSync as execFileSync5, spawn as spawn2 } from "child_process";
@@ -4759,40 +5429,192 @@ var DumateWatch = class {
4759
5429
  this.#reconciling = false;
4760
5430
  }
4761
5431
  }
4762
- };
4763
- function runHook(node, input, agentPid) {
5432
+ };
5433
+ function runHook(node, input, agentPid) {
5434
+ return new Promise((resolve10) => {
5435
+ let child;
5436
+ try {
5437
+ child = spawn2(node, [installedHookPath(), "dumate"], {
5438
+ stdio: ["pipe", "ignore", "ignore"],
5439
+ env: { ...process.env, DESKBUDDY_AGENT_PID: String(agentPid) }
5440
+ });
5441
+ } catch {
5442
+ resolve10();
5443
+ return;
5444
+ }
5445
+ const timer = setTimeout(() => child.kill("SIGKILL"), HOOK_KILL_MS);
5446
+ timer.unref?.();
5447
+ child.on("error", () => {
5448
+ clearTimeout(timer);
5449
+ resolve10();
5450
+ });
5451
+ child.on("close", () => {
5452
+ clearTimeout(timer);
5453
+ resolve10();
5454
+ });
5455
+ child.stdin.on("error", () => {
5456
+ });
5457
+ child.stdin.end(JSON.stringify(input));
5458
+ });
5459
+ }
5460
+
5461
+ // src/daemon/qwenworkLang.ts
5462
+ import { execFile as execFile2 } from "child_process";
5463
+ import { open } from "fs/promises";
5464
+ import { homedir as homedir3 } from "os";
5465
+ import { join as join15 } from "path";
5466
+ var DATA_DIR = "QwenWorkCN";
5467
+ var MAX_SCAN = 16 * 1024 * 1024;
5468
+ var LANG_RE = /(?<![A-Za-z0-9_:.-])language"(zh-TW|zh|en)"/g;
5469
+ var CACHE_MS = 30 * 1e3;
5470
+ function qwenworkUserDataDir() {
5471
+ const forced = process.env.DESKBUDDY_QWENWORK_DATA;
5472
+ if (forced) return forced;
5473
+ if (process.platform === "darwin") return join15(homedir3(), "Library", "Application Support", DATA_DIR);
5474
+ if (process.platform === "win32") return join15(process.env.APPDATA?.trim() || join15(homedir3(), "AppData", "Roaming"), DATA_DIR);
5475
+ return join15(process.env.XDG_CONFIG_HOME?.trim() || join15(homedir3(), ".config"), DATA_DIR);
5476
+ }
5477
+ async function readAll(p) {
5478
+ let fh;
5479
+ try {
5480
+ fh = await open(p, "r");
5481
+ } catch (e) {
5482
+ return e?.code === "ENOENT" ? "missing" : "error";
5483
+ }
5484
+ try {
5485
+ const size = (await fh.stat()).size;
5486
+ if (size > MAX_SCAN) return "error";
5487
+ const buf = Buffer.alloc(size);
5488
+ let off = 0;
5489
+ while (off < size) {
5490
+ const { bytesRead } = await fh.read(buf, off, size - off, off);
5491
+ if (bytesRead <= 0) break;
5492
+ off += bytesRead;
5493
+ }
5494
+ return buf.subarray(0, off);
5495
+ } catch {
5496
+ return "error";
5497
+ } finally {
5498
+ await fh.close().catch(() => {
5499
+ });
5500
+ }
5501
+ }
5502
+ function matches(buf) {
5503
+ const out = [];
5504
+ const s2 = buf.toString("latin1");
5505
+ LANG_RE.lastIndex = 0;
5506
+ let m;
5507
+ while (m = LANG_RE.exec(s2)) out.push(m[1]);
5508
+ return out;
5509
+ }
5510
+ function varint(b, at) {
5511
+ let v = 0;
5512
+ for (let i = 0; i < 9; i++) {
5513
+ if (at + i >= b.length) return null;
5514
+ const c = b[at + i];
5515
+ if (i === 8) return [v * 256 + c, at + 9];
5516
+ v = v * 128 + (c & 127);
5517
+ if (!(c & 128)) return [v, at + i + 1];
5518
+ }
5519
+ return null;
5520
+ }
5521
+ function pageLangs(page, first = false) {
5522
+ const h = first ? 100 : 0;
5523
+ if (page.length < h + 8 || page[h] !== 13) return [];
5524
+ const n = page.readUInt16BE(h + 3);
5525
+ const out = [];
5526
+ for (let i = 0; i < n; i++) {
5527
+ const p = h + 8 + 2 * i;
5528
+ if (p + 2 > page.length) break;
5529
+ const cell = page.readUInt16BE(p);
5530
+ if (cell < h + 8 || cell >= page.length) continue;
5531
+ const len = varint(page, cell);
5532
+ const rowid = len ? varint(page, len[1]) : null;
5533
+ if (!len || !rowid) continue;
5534
+ out.push(...matches(page.subarray(rowid[1], Math.min(page.length, rowid[1] + Math.min(len[0], 256)))));
5535
+ }
5536
+ return out;
5537
+ }
5538
+ function langFromWal(wal) {
5539
+ if (!wal || wal.length < 32) return "";
5540
+ const magic = wal.readUInt32BE(0);
5541
+ if (magic !== 931071618 && magic !== 931071619) return "";
5542
+ const pageSize = wal.readUInt32BE(8);
5543
+ if (pageSize < 512 || pageSize > 65536) return "";
5544
+ const salt1 = wal.readUInt32BE(16);
5545
+ const salt2 = wal.readUInt32BE(20);
5546
+ let last = "";
5547
+ for (let off = 32; off + 24 + pageSize <= wal.length; off += 24 + pageSize) {
5548
+ if (wal.readUInt32BE(off + 8) !== salt1 || wal.readUInt32BE(off + 12) !== salt2) break;
5549
+ const found = pageLangs(wal.subarray(off + 24, off + 24 + pageSize), wal.readUInt32BE(off) === 1);
5550
+ if (found.length > 0) last = found[found.length - 1];
5551
+ }
5552
+ return last;
5553
+ }
5554
+ function langFromDb(db) {
5555
+ if (db && db.length === 0) return "";
5556
+ if (!db || db.length < 100 || db.toString("latin1", 0, 16) !== "SQLite format 3\0") return "invalid";
5557
+ const raw = db.readUInt16BE(16);
5558
+ const pageSize = raw === 1 ? 65536 : raw;
5559
+ if (pageSize < 512 || pageSize > 65536 || (pageSize & pageSize - 1) !== 0) return "invalid";
5560
+ const set = /* @__PURE__ */ new Set();
5561
+ for (let off = 0; off + pageSize <= db.length; off += pageSize) for (const v of pageLangs(db.subarray(off, off + pageSize), off === 0)) set.add(v);
5562
+ if (set.size === 1) return [...set][0];
5563
+ return set.size > 1 ? "ambiguous" : "";
5564
+ }
5565
+ function systemLang() {
4764
5566
  return new Promise((resolve10) => {
4765
- let child;
4766
- try {
4767
- child = spawn2(node, [installedHookPath(), "dumate"], {
4768
- stdio: ["pipe", "ignore", "ignore"],
4769
- env: { ...process.env, DESKBUDDY_AGENT_PID: String(agentPid) }
5567
+ const done = (first) => resolve10(!first ? "" : first.toLowerCase().startsWith("zh") ? "zh" : "en");
5568
+ if (process.platform === "darwin") {
5569
+ execFile2("defaults", ["read", "-g", "AppleLanguages"], { encoding: "utf8", timeout: 3e3 }, (err, out) => {
5570
+ if (err) return done("");
5571
+ const m = /"?([A-Za-z]{2,3}(?:[-_][A-Za-z0-9]+)*)"?/.exec(out);
5572
+ done(m ? m[1] : "");
4770
5573
  });
4771
- } catch {
4772
- resolve10();
4773
5574
  return;
4774
5575
  }
4775
- const timer = setTimeout(() => child.kill("SIGKILL"), HOOK_KILL_MS);
4776
- timer.unref?.();
4777
- child.on("error", () => {
4778
- clearTimeout(timer);
4779
- resolve10();
4780
- });
4781
- child.on("close", () => {
4782
- clearTimeout(timer);
4783
- resolve10();
4784
- });
4785
- child.stdin.on("error", () => {
4786
- });
4787
- child.stdin.end(JSON.stringify(input));
5576
+ try {
5577
+ done(Intl.DateTimeFormat().resolvedOptions().locale || "");
5578
+ } catch {
5579
+ done("");
5580
+ }
4788
5581
  });
4789
5582
  }
5583
+ async function qwenworkLang(deps2 = {}) {
5584
+ const dir3 = (deps2.dataDir ?? qwenworkUserDataDir)();
5585
+ const read = deps2.readFile ?? readAll;
5586
+ const db = join15(dir3, "data", "agents.db");
5587
+ const wal = await read(`${db}-wal`);
5588
+ if (wal === "error") return "";
5589
+ if (wal !== "missing") {
5590
+ const fromWal = langFromWal(wal);
5591
+ if (fromWal) return fromWal;
5592
+ }
5593
+ const main2 = await read(db);
5594
+ if (main2 === "error") return "";
5595
+ if (main2 !== "missing") {
5596
+ const fromDb = langFromDb(main2);
5597
+ if (fromDb === "ambiguous" || fromDb === "invalid") return "";
5598
+ if (fromDb) return fromDb;
5599
+ }
5600
+ if (process.env.DESKBUDDY_QWENWORK_DATA && !deps2.systemLang) return "";
5601
+ return (deps2.systemLang ?? systemLang)();
5602
+ }
5603
+ function cachedQwenworkLang(now = Date.now) {
5604
+ let hit = null;
5605
+ return async () => {
5606
+ if (hit && now() - hit.at < CACHE_MS) return hit.v;
5607
+ const v = await qwenworkLang();
5608
+ hit = { at: now(), v };
5609
+ return v;
5610
+ };
5611
+ }
4790
5612
 
4791
5613
  // src/daemon/qwenworkBridge.ts
4792
5614
  var SUBMIT_TOOL = "qwenwork_task_submit_response";
4793
5615
  var DETAIL_TOOL = "qwenwork_task_get_detail";
4794
5616
  var SEND_TOOL = "qwenwork_task_send_message";
4795
- var POLL_MS = 2e3;
5617
+ var POLL_MS2 = 2e3;
4796
5618
  var REQUEST_TIMEOUT_MS2 = 5e3;
4797
5619
  var EARLY_WINDOW_MS = 10 * 1e3;
4798
5620
  var EARLY_RETRY_MS = 1e3;
@@ -4852,12 +5674,12 @@ function chatIdOf(cwd, root) {
4852
5674
  if (typeof cwd !== "string" || !cwd) return null;
4853
5675
  const dir3 = resolve3(cwd);
4854
5676
  if (!sameDir(dirname3(dir3), resolve3(root))) return null;
4855
- const id = basename5(dir3);
5677
+ const id = basename6(dir3);
4856
5678
  return /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : null;
4857
5679
  }
4858
5680
  function transcriptCwdOf(sessionId, home = qwenworkHome()) {
4859
5681
  if (!/^[A-Za-z0-9_-]{1,128}$/.test(sessionId)) return null;
4860
- const base = join15(home, "projects");
5682
+ const base = join16(home, "projects");
4861
5683
  let dirs;
4862
5684
  try {
4863
5685
  dirs = readdirSync8(base);
@@ -4867,7 +5689,7 @@ function transcriptCwdOf(sessionId, home = qwenworkHome()) {
4867
5689
  for (const d of dirs) {
4868
5690
  let fd;
4869
5691
  try {
4870
- fd = openSync4(join15(base, d, `${sessionId}.jsonl`), "r");
5692
+ fd = openSync4(join16(base, d, `${sessionId}.jsonl`), "r");
4871
5693
  } catch {
4872
5694
  continue;
4873
5695
  }
@@ -5034,6 +5856,7 @@ var QwenWorkMcp = class {
5034
5856
  var QwenWorkBridge = class {
5035
5857
  #d;
5036
5858
  #mcp;
5859
+ #lang;
5037
5860
  /** chatId → 正在等设备的那张卡(同一 chatId 同时只推一张) */
5038
5861
  #cards = /* @__PURE__ */ new Map();
5039
5862
  /** 推过的 (chatId, toolUseId) → 时间:同一道题 / 同一个授权框只推一次 */
@@ -5045,9 +5868,10 @@ var QwenWorkBridge = class {
5045
5868
  #stopped = false;
5046
5869
  constructor(deps2) {
5047
5870
  this.#d = deps2;
5048
- const read = deps2.readConfig ?? (() => readConfigFile(join15(qwenworkHome(), "mcp-adaptor.config")));
5871
+ const read = deps2.readConfig ?? (() => readConfigFile(join16(qwenworkHome(), "mcp-adaptor.config")));
5049
5872
  const f = deps2.fetch ?? globalThis.fetch;
5050
5873
  this.#mcp = new QwenWorkMcp(read, f, deps2.log);
5874
+ this.#lang = deps2.lang ?? cachedQwenworkLang();
5051
5875
  }
5052
5876
  #now() {
5053
5877
  return this.#d.now ? this.#d.now() : Date.now();
@@ -5064,7 +5888,7 @@ var QwenWorkBridge = class {
5064
5888
  async reply(cwd, text, sessionId = "") {
5065
5889
  const notSent = (reason, message) => ({ status: "not_sent", reason, message });
5066
5890
  if (this.#d.enabled && !this.#d.enabled()) return notSent("tool_unsupported", "\u5343\u95EE\u529E\u516C\u6CA1\u6709\u63A5\u5165");
5067
- const root = this.#d.workspaceRoot ?? join15(homedir3(), ".qwenworkcn", "workspace");
5891
+ const root = this.#d.workspaceRoot ?? join16(homedir4(), ".qwenworkcn", "workspace");
5068
5892
  let dir3 = typeof cwd === "string" ? cwd : "";
5069
5893
  if (!dir3 && sessionId) dir3 = (this.#d.transcriptCwd ?? transcriptCwdOf)(sessionId) ?? "";
5070
5894
  if (!dir3) return notSent("session_gone", "\u7535\u8111\u4E0A\u627E\u4E0D\u5230\u8FD9\u4E2A\u4F1A\u8BDD\u7684\u8BB0\u5F55");
@@ -5117,7 +5941,7 @@ var QwenWorkBridge = class {
5117
5941
  toolUseId = typeof det.toolUseId === "string" ? det.toolUseId : "";
5118
5942
  if (!toolName) return;
5119
5943
  } else return;
5120
- const root = this.#d.workspaceRoot ?? join15(homedir3(), ".qwenworkcn", "workspace");
5944
+ const root = this.#d.workspaceRoot ?? join16(homedir4(), ".qwenworkcn", "workspace");
5121
5945
  const chatId = chatIdOf(h.cwd, root);
5122
5946
  if (!chatId) {
5123
5947
  this.#d.log("\u5343\u95EE\u529E\u516C\uFF1A\u8FD9\u9053\u9898 / \u6388\u6743\u4E0D\u5728\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0B\uFF08\u53EF\u80FD\u662F\u5B50\u4EE3\u7406\uFF09\uFF0C\u4E0D\u63A8\u8BBE\u5907");
@@ -5135,6 +5959,14 @@ var QwenWorkBridge = class {
5135
5959
  if (this.#seen.has(key)) return;
5136
5960
  this.#seen.set(key, this.#now());
5137
5961
  this.#replaceOld(chatId, toolUseId);
5962
+ let lang = "";
5963
+ if (kind === "permission") {
5964
+ try {
5965
+ lang = await this.#lang();
5966
+ } catch {
5967
+ lang = "";
5968
+ }
5969
+ }
5138
5970
  const e = await this.#mcp.ensure();
5139
5971
  if (!e.ok) {
5140
5972
  if (e.why !== "unsupported") this.#d.log(`\u5343\u95EE\u529E\u516C\uFF1A${e.message}\uFF0C\u8FD9\u4E00\u6761\u4E0D\u63A8\u8BBE\u5907`);
@@ -5148,6 +5980,7 @@ var QwenWorkBridge = class {
5148
5980
  const gateReq = {
5149
5981
  window: true,
5150
5982
  tool: "qwenwork",
5983
+ ...lang ? { lang } : {},
5151
5984
  hook: {
5152
5985
  hook_event_name: "PermissionRequest",
5153
5986
  session_id: sessionId,
@@ -5240,7 +6073,7 @@ var QwenWorkBridge = class {
5240
6073
  }
5241
6074
  #ensurePoll() {
5242
6075
  if (this.#timer || this.#stopped) return;
5243
- this.#timer = setInterval(() => void this.poll(), this.#d.pollMs ?? POLL_MS);
6076
+ this.#timer = setInterval(() => void this.poll(), this.#d.pollMs ?? POLL_MS2);
5244
6077
  this.#timer.unref?.();
5245
6078
  }
5246
6079
  /**
@@ -5341,21 +6174,21 @@ function sleep(ms) {
5341
6174
  }
5342
6175
 
5343
6176
  // src/daemon/workbuddyApp/keeper.ts
5344
- import { execFile as execFile3, spawn as spawn3 } from "child_process";
6177
+ import { execFile as execFile4, spawn as spawn3 } from "child_process";
5345
6178
  import { existsSync as existsSync8, mkdirSync as mkdirSync13, readdirSync as readdirSync10, readFileSync as readFileSync12, renameSync as renameSync8, writeFileSync as writeFileSync12 } from "fs";
5346
- import { delimiter, join as join18, resolve as resolve4 } from "path";
6179
+ import { delimiter, join as join19, resolve as resolve4 } from "path";
5347
6180
 
5348
6181
  // src/daemon/workbuddyAppDb.ts
5349
- import { execFile as execFile2 } from "child_process";
6182
+ import { execFile as execFile3 } from "child_process";
5350
6183
  import { closeSync as closeSync6, existsSync as existsSync7, mkdirSync as mkdirSync12, openSync as openSync6, readdirSync as readdirSync9, readFileSync as readFileSync11, readSync as readSync5, renameSync as renameSync7, rmSync as rmSync8, statSync as statSync7, writeFileSync as writeFileSync11 } from "fs";
5351
6184
  import { createRequire as createRequire2 } from "module";
5352
- import { basename as basename7, dirname as dirname5, isAbsolute as isAbsolute3, join as join17 } from "path";
6185
+ import { basename as basename8, dirname as dirname5, isAbsolute as isAbsolute3, join as join18 } from "path";
5353
6186
 
5354
6187
  // src/core/title.ts
5355
6188
  import { closeSync as closeSync5, existsSync as existsSync6, openSync as openSync5, readFileSync as readFileSync10, readSync as readSync4, statSync as statSync6 } from "fs";
5356
6189
  import { createRequire } from "module";
5357
- import { homedir as homedir4 } from "os";
5358
- import { basename as basename6, dirname as dirname4, isAbsolute as isAbsolute2, join as join16 } from "path";
6190
+ import { homedir as homedir5 } from "os";
6191
+ import { basename as basename7, dirname as dirname4, isAbsolute as isAbsolute2, join as join17 } from "path";
5359
6192
  var TRANSCRIPT_TAIL_BYTES = 64 * 1024;
5360
6193
  var SESSION_TITLE_MAX = 100;
5361
6194
  function cleanTitle(text) {
@@ -5366,14 +6199,14 @@ function cleanTitle(text) {
5366
6199
  var requireHere = createRequire(import.meta.url);
5367
6200
  var QODER_DATA_DIRS = { qoder_cn: "com.qodercn.app.stable", qoder: "com.qoder.app.stable" };
5368
6201
  var QODER_APP_DIR_ENV = { qoder_cn: "DESKBUDDY_QODER_CN_APP_DIR", qoder: "DESKBUDDY_QODER_APP_DIR" };
5369
- function qoderDbPaths(tool, platform = process.platform, home = homedir4(), env = process.env) {
6202
+ function qoderDbPaths(tool, platform = process.platform, home = homedir5(), env = process.env) {
5370
6203
  let root;
5371
- if (platform === "darwin") root = join16(home, "Library", "Application Support");
5372
- else if (platform === "win32") root = env.APPDATA || join16(home, "AppData", "Roaming");
5373
- else root = env.XDG_CONFIG_HOME || join16(home, ".config");
6204
+ if (platform === "darwin") root = join17(home, "Library", "Application Support");
6205
+ else if (platform === "win32") root = env.APPDATA || join17(home, "AppData", "Roaming");
6206
+ else root = env.XDG_CONFIG_HOME || join17(home, ".config");
5374
6207
  const one = tool ? QODER_DATA_DIRS[tool] : void 0;
5375
6208
  const tools = one ? [tool] : ["qoder_cn", "qoder"];
5376
- return tools.map((t) => join16(env[QODER_APP_DIR_ENV[t] ?? ""] || join16(root, QODER_DATA_DIRS[t] ?? ""), "main.sqlite"));
6209
+ return tools.map((t) => join17(env[QODER_APP_DIR_ENV[t] ?? ""] || join17(root, QODER_DATA_DIRS[t] ?? ""), "main.sqlite"));
5377
6210
  }
5378
6211
 
5379
6212
  // src/daemon/workbuddyAppDb.ts
@@ -5439,12 +6272,12 @@ function findWbTranscript(configDir, sessionId) {
5439
6272
  if (!SESSION_UUID_RE.test(sessionId)) return null;
5440
6273
  let dirs;
5441
6274
  try {
5442
- dirs = readdirSync9(join17(configDir, "projects"));
6275
+ dirs = readdirSync9(join18(configDir, "projects"));
5443
6276
  } catch {
5444
6277
  return null;
5445
6278
  }
5446
6279
  for (const d of dirs) {
5447
- const p = join17(configDir, "projects", d, `${sessionId}.jsonl`);
6280
+ const p = join18(configDir, "projects", d, `${sessionId}.jsonl`);
5448
6281
  if (existsSync7(p)) return p;
5449
6282
  }
5450
6283
  return null;
@@ -5530,8 +6363,8 @@ function wbElectronCandidates(appCliPath) {
5530
6363
  const m = /^(.*?)[\\/]resources[\\/]app\.asar\.unpacked[\\/]/i.exec(appCliPath);
5531
6364
  if (!m) return [];
5532
6365
  const root = m[1];
5533
- if (/\.app[\\/]Contents$/.test(root)) return [join17(root, "MacOS", "Electron"), join17(root, "MacOS", "WorkBuddy")];
5534
- return [join17(root, "WorkBuddy.exe")];
6366
+ if (/\.app[\\/]Contents$/.test(root)) return [join18(root, "MacOS", "Electron"), join18(root, "MacOS", "WorkBuddy")];
6367
+ return [join18(root, "WorkBuddy.exe")];
5535
6368
  }
5536
6369
  function defaultWbElectron(d = defaultLocateDeps()) {
5537
6370
  if ((d.platform ?? process.platform) === "win32") return null;
@@ -5550,14 +6383,14 @@ function defaultAppRunning(platform = process.platform) {
5550
6383
  return new Promise((resolve10) => {
5551
6384
  try {
5552
6385
  if (platform === "darwin") {
5553
- execFile2("pgrep", ["-f", "WorkBuddy.app/Contents/MacOS/"], { timeout: 5e3 }, (err) => {
6386
+ execFile3("pgrep", ["-f", "WorkBuddy.app/Contents/MacOS/"], { timeout: 5e3 }, (err) => {
5554
6387
  const code = err?.code;
5555
6388
  resolve10(!err ? true : code === 1 ? false : true);
5556
6389
  });
5557
6390
  return;
5558
6391
  }
5559
6392
  if (platform === "win32") {
5560
- execFile2(
6393
+ execFile3(
5561
6394
  "tasklist",
5562
6395
  ["/FI", "IMAGENAME eq WorkBuddy.exe", "/FO", "CSV", "/NH"],
5563
6396
  { timeout: 1e4, windowsHide: true, encoding: "utf8" },
@@ -5574,7 +6407,7 @@ function defaultAppRunning(platform = process.platform) {
5574
6407
  var PENDING_MAX = 20;
5575
6408
  var PENDING_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
5576
6409
  function pendingPath() {
5577
- return join17(deskbuddyHome(), "workbuddy-pending.json");
6410
+ return join18(deskbuddyHome(), "workbuddy-pending.json");
5578
6411
  }
5579
6412
  function loadPendingFile() {
5580
6413
  try {
@@ -5604,7 +6437,7 @@ var defaultWbAppDbDeps = () => ({
5604
6437
  electron: () => defaultWbElectron(),
5605
6438
  runElectron: (bin, script, input) => new Promise((resolve10, reject) => {
5606
6439
  const env = { ...process.env, ELECTRON_RUN_AS_NODE: "1" };
5607
- const c = execFile2(
6440
+ const c = execFile3(
5608
6441
  bin,
5609
6442
  ["-e", script],
5610
6443
  { env, timeout: 15e3, windowsHide: true, maxBuffer: 1024 * 1024 },
@@ -5702,7 +6535,7 @@ async function registerNow(job, d) {
5702
6535
  const tp = findWbTranscript(d.configDir(), job.sessionId);
5703
6536
  if (!tp) return "skip no_transcript";
5704
6537
  const info = wbTranscriptInfo(tp);
5705
- const title = info.title || cleanTitle(job.fallbackTitle ?? "") || basename7(job.cwd) || job.cwd;
6538
+ const title = info.title || cleanTitle(job.fallbackTitle ?? "") || basename8(job.cwd) || job.cwd;
5706
6539
  const r = await runDb({ db, sessionId: job.sessionId, cwd: job.cwd, title, createdAt: info.createdAt, updatedAt: info.updatedAt }, d);
5707
6540
  if (r === "no_sqlite") return "skip no_sqlite";
5708
6541
  if (r.status === "schema") return `skip schema ${r.detail ?? ""}`.trim();
@@ -5744,7 +6577,7 @@ function loopbackWsUrl(wsUrl, port) {
5744
6577
  async function connectCdp(port, d = {}) {
5745
6578
  const fetchJson = d.fetchJson ?? defaultFetchJson;
5746
6579
  const target = pickMainTarget(await fetchJson(`http://127.0.0.1:${port}/json/list`, 3e3));
5747
- if (!target?.webSocketDebuggerUrl) throw new Error("\u627E\u4E0D\u5230 WorkBuddy \u4E3B\u7A97\u53E3");
6580
+ if (!target?.webSocketDebuggerUrl) throw new Error("\u627E\u4E0D\u5230 App \u4E3B\u7A97\u53E3");
5748
6581
  const url = loopbackWsUrl(target.webSocketDebuggerUrl, port);
5749
6582
  if (!url) throw new Error("\u4E3B\u7A97\u53E3\u7684\u8C03\u8BD5\u5730\u5740\u4E0D\u5BF9");
5750
6583
  const ws = (d.ws ?? ((u) => new WebSocket(u, { handshakeTimeout: 5e3 })))(url);
@@ -5842,7 +6675,7 @@ var VERIFY_MIN_MS = 30 * 1e3;
5842
6675
  var VERIFY_MAX_MS = 60 * 1e3;
5843
6676
  var VERIFY_STEP_MS = 5 * 1e3;
5844
6677
  var defaultExec2 = (file3, args, timeoutMs = 1e4) => new Promise((done) => {
5845
- execFile3(file3, args, { timeout: timeoutMs, windowsHide: true, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }, (err, stdout6) => {
6678
+ execFile4(file3, args, { timeout: timeoutMs, windowsHide: true, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }, (err, stdout6) => {
5846
6679
  const code = err ? typeof err.code === "number" ? err.code : -1 : 0;
5847
6680
  done({ code, stdout: String(stdout6 ?? "") });
5848
6681
  });
@@ -5892,7 +6725,7 @@ function cleanLaunchEnv(src = process.env, shim = shimDir()) {
5892
6725
  }
5893
6726
  var REMEMBER_EXE_MS = 30 * 60 * 1e3;
5894
6727
  var AWAY_IDLE_MS = 5 * 60 * 1e3;
5895
- var keeperThrottleFile = () => join18(deskbuddyHome(), "wbapp", "keeper.json");
6728
+ var keeperThrottleFile = () => join19(deskbuddyHome(), "wbapp", "keeper.json");
5896
6729
  var filePersist = {
5897
6730
  load: () => {
5898
6731
  try {
@@ -5903,7 +6736,7 @@ var filePersist = {
5903
6736
  },
5904
6737
  save: (s2) => {
5905
6738
  try {
5906
- mkdirSync13(join18(deskbuddyHome(), "wbapp"), { recursive: true });
6739
+ mkdirSync13(join19(deskbuddyHome(), "wbapp"), { recursive: true });
5907
6740
  const f = keeperThrottleFile();
5908
6741
  writeFileSync12(f + ".tmp", JSON.stringify(s2));
5909
6742
  renameSync8(f + ".tmp", f);
@@ -5987,7 +6820,7 @@ function workbuddyBusy(now = Date.now(), dir3 = stateDir(), alive = isAlive) {
5987
6820
  for (const f of readdirSync10(dir3)) {
5988
6821
  if (!f.endsWith(".json")) continue;
5989
6822
  try {
5990
- const s2 = JSON.parse(readFileSync12(join18(dir3, f), "utf8"));
6823
+ const s2 = JSON.parse(readFileSync12(join19(dir3, f), "utf8"));
5991
6824
  if (s2.tool !== "workbuddy" || s2.state !== "watch" && s2.state !== "ask_approval") continue;
5992
6825
  if (typeof s2.pid === "number" && s2.pid > 0) {
5993
6826
  if (alive(s2.pid, s2.pidName)) return true;
@@ -6610,7 +7443,7 @@ function choicesToSet(choices) {
6610
7443
  options.push({ id, label: c.label, kind, ...c.description ? { description: c.description } : {} });
6611
7444
  actions[id] = { decision: kind === "deny" ? "deny" : "allow", native: c.native ?? { reject: true } };
6612
7445
  });
6613
- return checkOptions({ options, actions }, { anyKinds: true, unbounded: true });
7446
+ return checkOptions({ options, actions }, { anyKinds: true });
6614
7447
  }
6615
7448
  function asNative(v) {
6616
7449
  if (!v || typeof v !== "object") return null;
@@ -6642,7 +7475,7 @@ var WB_OWN_TEXT = {
6642
7475
  var str3 = (v) => typeof v === "string" ? v.trim() : "";
6643
7476
  function noticeCard(title, lang, why) {
6644
7477
  const O = WB_OWN_TEXT[lang];
6645
- const set = checkOptions({ options: [{ id: "o0", label: O.noticeButton, kind: "deny" }], actions: { o0: { decision: "deny" } } }, { anyKinds: true, unbounded: true });
7478
+ const set = checkOptions({ options: [{ id: "o0", label: O.noticeButton, kind: "deny" }], actions: { o0: { decision: "deny" } } }, { anyKinds: true });
6646
7479
  return { type: "notice", options: set, summary: `${O.noticePrefix}${title || O.pending}`, why };
6647
7480
  }
6648
7481
  function rawChoices(p, lang) {
@@ -6749,7 +7582,7 @@ function wbCard(p, lang) {
6749
7582
  }
6750
7583
  function fallbackCard(p, lang) {
6751
7584
  const O = WB_OWN_TEXT[lang];
6752
- const set = checkOptions({ options: [{ id: "o0", label: O.reject, kind: "deny" }], actions: { o0: { decision: "deny", native: { reject: true } } } }, { anyKinds: true, unbounded: true });
7585
+ const set = checkOptions({ options: [{ id: "o0", label: O.reject, kind: "deny" }], actions: { o0: { decision: "deny", native: { reject: true } } } }, { anyKinds: true });
6753
7586
  const tool = str3(p.toolName);
6754
7587
  return { type: "options", options: set, toolName: tool || "WorkBuddy", toolInput: {}, summary: tool ? `${tool} ${O.pending}` : O.pending };
6755
7588
  }
@@ -6990,7 +7823,7 @@ function domOptions(labels, lang) {
6990
7823
  const first = options.find((o) => o.kind !== "deny");
6991
7824
  if (first) first.kind = "allow";
6992
7825
  }
6993
- return checkOptions({ options, actions }, { anyKinds: true, unbounded: true });
7826
+ return checkOptions({ options, actions }, { anyKinds: true });
6994
7827
  }
6995
7828
  var WorkbuddyAppWatch = class {
6996
7829
  #d;
@@ -7245,25 +8078,25 @@ var WorkbuddyAppWatch = class {
7245
8078
  }
7246
8079
  }
7247
8080
  async #sdkRound() {
7248
- const snap = await this.#eval(SNAPSHOT);
7249
- if (!snap || !Array.isArray(snap.sessions)) return;
7250
- this.#snapshotBusy = snap.busy === true;
8081
+ const snap2 = await this.#eval(SNAPSHOT);
8082
+ if (!snap2 || !Array.isArray(snap2.sessions)) return;
8083
+ this.#snapshotBusy = snap2.busy === true;
7251
8084
  this.#snapshotBusyAt = this.#now();
7252
8085
  const present = /* @__PURE__ */ new Set();
7253
8086
  const current = /* @__PURE__ */ new Map();
7254
- for (const s2 of snap.sessions) {
8087
+ for (const s2 of snap2.sessions) {
7255
8088
  if (!s2?.id) continue;
7256
8089
  present.add(s2.id);
7257
8090
  if (s2.pending?.id) current.set(s2.id, s2.pending.id);
7258
8091
  }
7259
8092
  this.#withdrawStale(present, current);
7260
8093
  this.#prune();
7261
- const lang = wbLang(snap.locale);
8094
+ const lang = wbLang(snap2.locale);
7262
8095
  for (const sid of [...this.#missingSince.keys()]) {
7263
- const sp = snap.sessions.find((x) => x?.id === sid)?.pending;
8096
+ const sp = snap2.sessions.find((x) => x?.id === sid)?.pending;
7264
8097
  if (!sp || sp.category !== "pendingMissing") this.#missingSince.delete(sid);
7265
8098
  }
7266
- for (const s2 of snap.sessions) {
8099
+ for (const s2 of snap2.sessions) {
7267
8100
  const p = s2?.pending;
7268
8101
  if (!s2?.id || !p?.id) continue;
7269
8102
  const key = `${s2.id}:${p.id}`;
@@ -7273,7 +8106,7 @@ var WorkbuddyAppWatch = class {
7273
8106
  this.#missingSince.set(s2.id, since);
7274
8107
  if (this.#now() - since < MISSING_STABLE_MS) continue;
7275
8108
  }
7276
- if (!lang) this.#skipLog(`lang:${snap.locale}`, `WorkBuddy \u754C\u9762\u8BED\u8A00 ${snap.locale || "(\u672A\u77E5)"} \u6CA1\u6709\u539F\u6587\u8868\uFF0C\u5F85\u786E\u8BA4\u53EA\u63A8\u63D0\u793A\u5361`);
8109
+ if (!lang) this.#skipLog(`lang:${snap2.locale}`, `WorkBuddy \u754C\u9762\u8BED\u8A00 ${snap2.locale || "(\u672A\u77E5)"} \u6CA1\u6709\u539F\u6587\u8868\uFF0C\u5F85\u786E\u8BA4\u53EA\u63A8\u63D0\u793A\u5361`);
7277
8110
  const c = wbCard(p, lang);
7278
8111
  if (c.type === "notice") {
7279
8112
  this.#skipLog(key, `WorkBuddy \u4F1A\u8BDD ${s2.id.slice(0, 8)} \u7684\u5F85\u786E\u8BA4\uFF08${c.why}\uFF09\u5728\u8BBE\u5907\u4E0A\u53EA\u63A8\u63D0\u793A\u5361`);
@@ -7286,11 +8119,11 @@ var WorkbuddyAppWatch = class {
7286
8119
  }
7287
8120
  }
7288
8121
  async #domRound() {
7289
- const snap = await this.#eval(DOM_SNAPSHOT);
7290
- if (!snap) return;
7291
- const sid = snap.sessionId ?? "";
7292
- const labels = Array.isArray(snap.card?.labels) ? snap.card.labels.filter((l) => typeof l === "string") : null;
7293
- const rawText = typeof snap.card?.text === "string" ? snap.card.text.slice(0, 2e3) : "";
8122
+ const snap2 = await this.#eval(DOM_SNAPSHOT);
8123
+ if (!snap2) return;
8124
+ const sid = snap2.sessionId ?? "";
8125
+ const labels = Array.isArray(snap2.card?.labels) ? snap2.card.labels.filter((l) => typeof l === "string") : null;
8126
+ const rawText = typeof snap2.card?.text === "string" ? snap2.card.text.slice(0, 2e3) : "";
7294
8127
  const textNorm = normalizeDomText(rawText);
7295
8128
  const fp = labels ? `dom:${labels.join("|")}::${textNorm}` : "";
7296
8129
  const present = /* @__PURE__ */ new Set();
@@ -7310,9 +8143,9 @@ var WorkbuddyAppWatch = class {
7310
8143
  else this.#domFp.delete(sid);
7311
8144
  }
7312
8145
  if (!sid || !labels) return;
7313
- const lang = wbLang(snap.locale);
8146
+ const lang = wbLang(snap2.locale);
7314
8147
  if (!lang) {
7315
- this.#skipLog(`lang:${snap.locale}`, `WorkBuddy \u754C\u9762\u8BED\u8A00 ${snap.locale || "(\u672A\u77E5)"} \u6CA1\u6709\u539F\u6587\u8868\uFF0C\u5F85\u786E\u8BA4\u4E0D\u63A8\u8BBE\u5907`);
8148
+ this.#skipLog(`lang:${snap2.locale}`, `WorkBuddy \u754C\u9762\u8BED\u8A00 ${snap2.locale || "(\u672A\u77E5)"} \u6CA1\u6709\u539F\u6587\u8868\uFF0C\u5F85\u786E\u8BA4\u4E0D\u63A8\u8BBE\u5907`);
7316
8149
  return;
7317
8150
  }
7318
8151
  const key = `${sid}:${fp}`;
@@ -7463,8 +8296,8 @@ function isQoderDesktopPid(pidName) {
7463
8296
 
7464
8297
  // src/daemon/qoderApp/keeper.ts
7465
8298
  import { existsSync as existsSync9, mkdirSync as mkdirSync14, readdirSync as readdirSync11, readFileSync as readFileSync13, renameSync as renameSync9, writeFileSync as writeFileSync13 } from "fs";
7466
- import { homedir as homedir5 } from "os";
7467
- import { join as join19 } from "path";
8299
+ import { homedir as homedir6 } from "os";
8300
+ import { join as join20 } from "path";
7468
8301
  var WAKE_LAUNCH_GAP_MS2 = 60 * 1e3;
7469
8302
  var RESTART_GAP_MS2 = 30 * 60 * 1e3;
7470
8303
  var RESTART_DAILY_MAX2 = 3;
@@ -7503,7 +8336,7 @@ function toolBusy(tool, now = Date.now(), dir3 = stateDir(), alive = isAlive) {
7503
8336
  for (const f of readdirSync11(dir3)) {
7504
8337
  if (!f.endsWith(".json")) continue;
7505
8338
  try {
7506
- const s2 = JSON.parse(readFileSync13(join19(dir3, f), "utf8"));
8339
+ const s2 = JSON.parse(readFileSync13(join20(dir3, f), "utf8"));
7507
8340
  if (s2.tool !== tool || s2.state !== "watch" && s2.state !== "ask_approval") continue;
7508
8341
  if (typeof s2.pid === "number" && s2.pid > 0) {
7509
8342
  if (alive(s2.pid, s2.pidName)) return true;
@@ -7516,7 +8349,7 @@ function toolBusy(tool, now = Date.now(), dir3 = stateDir(), alive = isAlive) {
7516
8349
  return false;
7517
8350
  }
7518
8351
  function defaultThrottleFile() {
7519
- return join19(deskbuddyHome(), "qoderapp", "keeper.json");
8352
+ return join20(deskbuddyHome(), "qoderapp", "keeper.json");
7520
8353
  }
7521
8354
  var QoderCdpKeeper = class {
7522
8355
  #d;
@@ -7563,7 +8396,7 @@ var QoderCdpKeeper = class {
7563
8396
  all = {};
7564
8397
  }
7565
8398
  all[this.#d.brand.tool] = t;
7566
- mkdirSync14(join19(f, ".."), { recursive: true });
8399
+ mkdirSync14(join20(f, ".."), { recursive: true });
7567
8400
  writeFileSync13(f + ".tmp", JSON.stringify(all));
7568
8401
  renameSync9(f + ".tmp", f);
7569
8402
  } catch {
@@ -7601,7 +8434,7 @@ var QoderCdpKeeper = class {
7601
8434
  if (this.#d.appPath) return this.#d.appPath();
7602
8435
  if (this.#platform() !== "darwin") return null;
7603
8436
  const name = this.#d.brand.macApp.split("/").pop() ?? "";
7604
- for (const p of [this.#d.brand.macApp, join19(homedir5(), "Applications", name)]) if (existsSync9(p)) return p;
8437
+ for (const p of [this.#d.brand.macApp, join20(homedir6(), "Applications", name)]) if (existsSync9(p)) return p;
7605
8438
  return null;
7606
8439
  }
7607
8440
  /** 一轮:健康检查 → 活着核对属主。🔴 平时不重启 App */
@@ -7710,7 +8543,7 @@ var PROBE2 = `(() => {
7710
8543
  return { ok: missing.length === 0, missing };
7711
8544
  })()`;
7712
8545
  function sendExpr(i) {
7713
- const payload = { sessionId: i.sessionId, agentId: "agent:default", inputId: i.inputId, prompt: i.prompt };
8546
+ const payload = { sessionId: i.sessionId, agentId: "agent:default", inputId: i.inputId, prompt: i.prompt, permissionMode: "default" };
7714
8547
  if (i.isNew) {
7715
8548
  payload.turnId = i.inputId;
7716
8549
  payload.executionKind = "local";
@@ -7837,12 +8670,13 @@ var QoderAppConn = class {
7837
8670
  this.#keeperAt = 0;
7838
8671
  this.#retryAt = 0;
7839
8672
  }
7840
- async eval(expr) {
8673
+ /** timeoutMs:设备授权卡查选项要快,单独给短超时(缺省 EVAL_TIMEOUT_MS) */
8674
+ async eval(expr, timeoutMs = EVAL_TIMEOUT_MS) {
7841
8675
  if (!this.available()) return null;
7842
8676
  const c = this.#client;
7843
8677
  if (!c) return null;
7844
8678
  try {
7845
- return await c.evaluate(expr, EVAL_TIMEOUT_MS);
8679
+ return await c.evaluate(expr, timeoutMs);
7846
8680
  } catch (e) {
7847
8681
  this.#d.log(`${this.#d.brand.label} App \u4EFB\u52A1\u811A\u672C\u51FA\u9519\uFF1A${e?.message ?? e}`);
7848
8682
  return null;
@@ -7941,6 +8775,224 @@ var QoderAppConn = class {
7941
8775
  }
7942
8776
  };
7943
8777
 
8778
+ // src/daemon/qoderApp/approval.ts
8779
+ var APP_LOOKUP_TIMEOUT_MS = 3e3;
8780
+ var APP_LOOKUP_RETRY_MS = [250, 500, 1e3];
8781
+ function pendingCardExpr(q) {
8782
+ const want = { sessionId: q.sessionId, toolName: q.toolName, toolUseId: q.toolUseId ?? "", input: q.toolUseId ? null : q.toolInput ?? null };
8783
+ return `(async () => {
8784
+ const q = globalThis.qoderDesktop;
8785
+ if (!q || typeof q.watchChatSessionControl !== 'function' || typeof q.unwatchChatSessionControl !== 'function') return { ok: false, why: 'nobridge' };
8786
+ const NR = new RegExp(${JSON.stringify(NOT_REGISTERED_RE_SRC)}, 'i');
8787
+ const msg = (e) => String((e && e.message) || e).slice(0, 300);
8788
+ const want = ${JSON.stringify(want)};
8789
+ const wid = 'deskbuddy:perm:' + Date.now().toString(36) + ':' + Math.random().toString(36).slice(2);
8790
+ let st = null;
8791
+ try { const r = await q.watchChatSessionControl(want.sessionId, wid); st = r && r.state; }
8792
+ catch (e) { return { ok: false, why: NR.test(msg(e)) ? 'gone' : 'error', message: msg(e) }; }
8793
+ finally { try { await q.unwatchChatSessionControl(want.sessionId, wid); } catch (e) {} }
8794
+ if (!st || typeof st !== 'object') return { ok: false, why: 'error', message: 'no state' };
8795
+ const busy = st.runtimeState === 'running' || st.runtimeState === 'opening' || st.runtimeState === 'waiting-user' || !!st.activeTurn;
8796
+ const list = Array.isArray(st.pendingInteractions) ? st.pendingInteractions.filter((p) => p && p.type === 'permission') : [];
8797
+ const canon = (v) => JSON.stringify(v, (k, x) => (x && typeof x === 'object' && !Array.isArray(x) ? Object.keys(x).sort().reduce((o, k2) => ((o[k2] = x[k2]), o), {}) : x));
8798
+ const hits = want.toolUseId ? list.filter((p) => p.toolUseId === want.toolUseId) : list.filter((p) => p.toolName === want.toolName && canon(p.input) === canon(want.input));
8799
+ if (hits.length === 0) return { ok: false, why: 'not_found', busy };
8800
+ // \u6309\u53C2\u6570\u5339\u914D\uFF1AApp \u91CC\u540C\u540D\u540C\u53C2\u7684\u5F85\u786E\u8BA4\u4E0D\u6B62\u4E00\u6761\uFF08\u4E0D\u7BA1\u662F\u5426\u5DF2\u88AB\u522B\u7684\u8BBE\u5907\u5361\u5BF9\u4E0A\uFF09\u5C31\u5206\u4E0D\u6E05\u662F\u54EA\u4E00\u6B21\u8C03\u7528 \u2192 \u9000\u56DE\u67E5\u8868
8801
+ if (hits.length > 1) return { ok: false, why: 'ambiguous' };
8802
+ const p = hits[0];
8803
+ if (p.toolName !== want.toolName) return { ok: false, why: 'mismatch' };
8804
+ const isI18n = (v) => v && typeof v === 'object' && typeof v.getFixedT === 'function' && typeof v.t === 'function' && typeof v.exists === 'function';
8805
+ const findI18n = () => {
8806
+ const roots = [document.getElementById('root'), ...Array.from(document.body ? document.body.children : [])];
8807
+ for (const el of roots) {
8808
+ if (!el) continue;
8809
+ const ck = Object.keys(el).find((k) => k.startsWith('__reactContainer$'));
8810
+ if (!ck) continue;
8811
+ const stack = [el[ck]];
8812
+ let n = 0;
8813
+ while (stack.length && n < 50000) {
8814
+ const f = stack.pop();
8815
+ if (!f) continue;
8816
+ n++;
8817
+ let h = f.memoizedState, m = 0;
8818
+ while (h && typeof h === 'object' && m < 80) {
8819
+ const ms = h.memoizedState;
8820
+ const c = [ms, Array.isArray(ms) ? ms[0] : null, Array.isArray(ms) && ms[0] ? ms[0].i18n : null];
8821
+ for (const v of c) { const real = v && typeof v === 'object' && isI18n(v.__original) ? v.__original : v; if (isI18n(real)) return real; }
8822
+ h = h.next;
8823
+ m++;
8824
+ }
8825
+ if (f.sibling) stack.push(f.sibling);
8826
+ if (f.child) stack.push(f.child);
8827
+ }
8828
+ }
8829
+ return null;
8830
+ };
8831
+ const input = p.input && typeof p.input === 'object' && !Array.isArray(p.input) ? p.input : {};
8832
+ const mode = typeof input.mode === 'string' ? input.mode : '';
8833
+ if ((p.toolName === 'CollaborationAuthorizeProjectAccess' && mode === 'collaboration_access_authorization') || (p.toolName === 'LarkAuthorize' && mode === 'lark_authorization') || p.toolName === 'EnterPlanMode' || p.toolName === 'ExitPlanMode')
8834
+ return { ok: false, why: 'unsupported', message: p.toolName };
8835
+ const i18n = findI18n();
8836
+ if (!i18n) return { ok: false, why: 'no_i18n' };
8837
+ const T = (k, o) => { if (!i18n.exists(k)) throw new Error('missing ' + k); const s = i18n.t(k, o); if (typeof s !== 'string' || !s) throw new Error('empty ' + k); return s; };
8838
+ const modeLabel = (m) => (m === 'default' ? T('composer.permission.acceptEdits.label') : m === 'acceptEdits' || m === 'auto' || m === 'bypassPermissions' ? T('composer.permission.' + m + '.label') : m);
8839
+ const ruleScope = (r) => ((r.ruleContent && String(r.ruleContent).endsWith(':*') ? String(r.ruleContent).slice(0, -2) : r.ruleContent) || '').trim() || r.toolName;
8840
+ const suggLabel = (s) => s.type === 'mode' ? T('permission.suggestion.mode', { mode: modeLabel(s.mode) })
8841
+ : s.type === 'directories' ? T('permission.suggestion.directories.' + s.operation, { scope: s.directories.join(', ') })
8842
+ : s.operation === 'add' ? T('permission.suggestion.rules.add.' + s.behavior, { scope: s.rules.map(ruleScope).join(', ') })
8843
+ : T('permission.suggestion.rules.' + s.operation, { behavior: T('permission.suggestion.behavior.' + s.behavior), scope: s.rules.map(ruleScope).join(', ') });
8844
+ const items = [];
8845
+ let variant = 'default';
8846
+ try {
8847
+ if (p.toolName === 'dws_bash' && mode === 'pat_authorization') {
8848
+ variant = 'pat';
8849
+ const g = Array.isArray(input.grantOptions) ? input.grantOptions.filter((x) => typeof x === 'string') : [];
8850
+ items.push({ key: 'deny', label: T('permission.deny') });
8851
+ if (!(g.length > 0) || g.includes('once')) items.push({ key: 'patOnce', label: T('permission.pat.allowOnce') });
8852
+ if (g.includes('session')) items.push({ key: 'patSession', label: T('permission.pat.allowSession') });
8853
+ if (g.includes('permanent')) items.push({ key: 'patAlways', label: T('permission.pat.allowAlways') });
8854
+ } else if (p.toolName === 'RequestDirectory' && mode === 'directory_connection') {
8855
+ variant = 'directory';
8856
+ items.push({ key: 'deny', label: T('permission.deny') }, { key: 'allowOnce', label: T('permission.allow') });
8857
+ } else if (p.reason && p.reason.kind === 'repeatedToolCall') {
8858
+ // \u300C\u7EC8\u6B62\u4EFB\u52A1\u300D\u5728\u6E32\u67D3\u5C42\u5C31\u662F onClick=V1 \u2192 respondPermission(deny)\uFF0C\u53EA\u62D2\u8FD9\u4E00\u6B21\u8C03\u7528\uFF08\u4E0E hook deny \u540C\u4E49\uFF09\uFF0C\u5E76\u4E0D\u4E2D\u65AD\u4F1A\u8BDD
8859
+ variant = 'repeated';
8860
+ items.push({ key: 'deny', label: T('permission.repeatedTool.stopTask') }, { key: 'allowOnce', label: T('permission.repeatedTool.allowOnce') });
8861
+ } else {
8862
+ items.push({ key: 'deny', label: T('permission.deny') }, { key: 'allowOnce', label: T('permission.allowOnce') });
8863
+ for (const s of Array.isArray(p.permissionSuggestions) ? p.permissionSuggestions : []) {
8864
+ if (!s || typeof s.id !== 'string') return { ok: false, why: 'bad_suggestion' };
8865
+ items.push({ key: 'suggestion', label: suggLabel(s), suggestion: s });
8866
+ }
8867
+ }
8868
+ } catch (e) {
8869
+ return { ok: false, why: 'no_i18n', message: msg(e) };
8870
+ }
8871
+ return { ok: true, toolUseId: p.toolUseId, toolName: p.toolName, matchedBy: want.toolUseId ? 'id' : 'input', variant, lang: String(i18n.resolvedLanguage || i18n.language || ''), items };
8872
+ })()`;
8873
+ }
8874
+ function respondSuggestionExpr(sessionId, toolUseId, permissionSuggestionId) {
8875
+ const payload = { type: "permission", sessionId, toolUseId, decision: "allow", permissionSuggestionId };
8876
+ return `(async () => {
8877
+ const q = globalThis.qoderDesktop;
8878
+ if (!q || typeof q.respondChatInteraction !== 'function') return { ok: false, why: 'nobridge' };
8879
+ const msg = (e) => String((e && e.message) || e).slice(0, 300);
8880
+ try { await q.respondChatInteraction(${JSON.stringify(payload)}); return { ok: true }; }
8881
+ catch (e) { const m = msg(e); return { ok: false, why: /\u786E\u8BA4\u8BF7\u6C42\u5DF2\u7ECF\u5931\u6548|not pending|no longer pending/i.test(m) ? 'gone' : 'error', message: m }; }
8882
+ })()`;
8883
+ }
8884
+ var RULE_BEHAVIOR_ZH2 = { allow: "\u5141\u8BB8", deny: "\u62D2\u7EDD", ask: "\u8BE2\u95EE" };
8885
+ function sameJson(a, b) {
8886
+ const canon = (v) => JSON.stringify(
8887
+ v,
8888
+ (_k, x) => x && typeof x === "object" && !Array.isArray(x) ? Object.keys(x).sort().reduce((o, k) => (o[k] = x[k], o), {}) : x
8889
+ );
8890
+ return canon(a) === canon(b);
8891
+ }
8892
+ function suggestionUpdate(s2, hookSuggestions) {
8893
+ const m = /^session-(\d+)$/.exec(s2.id);
8894
+ const orig = m && Array.isArray(hookSuggestions) ? hookSuggestions[Number(m[1])] : void 0;
8895
+ const rebuilt = s2.type === "rules" ? { type: s2.operation === "add" ? "addRules" : s2.operation === "replace" ? "replaceRules" : "removeRules", behavior: s2.behavior, rules: s2.rules, destination: "session" } : s2.type === "mode" ? { type: "setMode", mode: s2.mode, destination: "session" } : { type: s2.operation === "add" ? "addDirectories" : "removeDirectories", directories: s2.directories, destination: "session" };
8896
+ if (orig && typeof orig === "object" && orig.destination === "session" && orig.type === rebuilt.type) {
8897
+ const same = s2.type === "rules" ? orig.behavior === s2.behavior && sameJson(orig.rules, s2.rules) : s2.type === "mode" ? orig.mode === s2.mode : sameJson(orig.directories, s2.directories);
8898
+ if (same) return orig;
8899
+ }
8900
+ return rebuilt;
8901
+ }
8902
+ function ruleDesc(s2) {
8903
+ if (s2.type !== "rules" || !Array.isArray(s2.rules) || s2.rules.length === 0) return "";
8904
+ const op = s2.operation === "add" ? "" : s2.operation === "replace" ? "\u66FF\u6362\u4E3A" : "\u79FB\u9664";
8905
+ const scope = s2.rules.map((r) => typeof r.ruleContent === "string" && r.ruleContent ? `${r.toolName}(${r.ruleContent})` : `\u6574\u4E2A ${r.toolName} \u5DE5\u5177`).join("\u3001");
8906
+ const kindText = s2.operation === "add" && s2.behavior === "allow" ? "" : `${RULE_BEHAVIOR_ZH2[s2.behavior] ?? s2.behavior}`;
8907
+ return `${kindText}\u89C4\u5219${op}\uFF1A${scope} \u2192 session`;
8908
+ }
8909
+ function appCardOptions(card, hook) {
8910
+ const options = [];
8911
+ const actions = {};
8912
+ const input = hook.tool_input && typeof hook.tool_input === "object" && !Array.isArray(hook.tool_input) ? hook.tool_input : {};
8913
+ let n = 0;
8914
+ for (const it of card.items) {
8915
+ if (!it || typeof it.label !== "string") return null;
8916
+ if (it.key === "deny") {
8917
+ options.push({ id: "deny", label: it.label, kind: "deny" });
8918
+ actions.deny = { decision: "deny" };
8919
+ } else if (it.key === "allowOnce") {
8920
+ options.push({ id: "allow", label: it.label, kind: "allow" });
8921
+ actions.allow = { decision: "allow" };
8922
+ } else if (it.key === "patOnce" || it.key === "patSession" || it.key === "patAlways") {
8923
+ const [id, kind, grantType] = it.key === "patOnce" ? ["allow", "allow", "once"] : it.key === "patSession" ? ["session", "allow_session", "session"] : ["always", "allow_always", "permanent"];
8924
+ options.push({ id, label: it.label, kind });
8925
+ actions[id] = { decision: "allow", updatedInput: { ...input, grantType } };
8926
+ } else if (it.key === "suggestion" && it.suggestion) {
8927
+ const s2 = it.suggestion;
8928
+ const id = `s${n++}`;
8929
+ const desc = ruleDesc(s2);
8930
+ options.push({ id, label: it.label, ...desc ? { description: desc } : {}, kind: "allow_session" });
8931
+ actions[id] = {
8932
+ decision: "allow",
8933
+ updatedPermissions: [suggestionUpdate(s2, hook.permission_suggestions)],
8934
+ // 🔴 只有按 tool_use_id 精确对上的卡才经 App 回答:按参数匹配到的可能是同名同参的另一次调用,答错对象
8935
+ ...s2.type === "mode" && card.matchedBy === "id" ? { qoderApp: { toolUseId: card.toolUseId, permissionSuggestionId: s2.id } } : {}
8936
+ };
8937
+ } else return null;
8938
+ }
8939
+ return checkOptions({ options, actions });
8940
+ }
8941
+ var QoderAppApprovals = class {
8942
+ #d;
8943
+ constructor(deps2) {
8944
+ this.#d = deps2;
8945
+ }
8946
+ #conn(tool) {
8947
+ if (tool !== "qoder_cn" && tool !== "qoder") return null;
8948
+ try {
8949
+ const c = this.#d.conn(tool);
8950
+ return c && c.available() ? c : null;
8951
+ } catch {
8952
+ return null;
8953
+ }
8954
+ }
8955
+ /**
8956
+ * 这条 hook 请求在 App 里对应的确认卡 → 设备选项。拿不到返回 null(调用方退回查表)。
8957
+ * 会话在 App 里跑着、但还没看到这一条时短暂重看(App 收 worker 的确认请求与 hook 进程起来几乎同时)
8958
+ */
8959
+ async options(tool, hook, signal) {
8960
+ const conn = this.#conn(tool);
8961
+ if (!conn) return null;
8962
+ const sessionId = typeof hook.session_id === "string" ? hook.session_id : "";
8963
+ const toolName = typeof hook.tool_name === "string" ? hook.tool_name : "";
8964
+ if (!sessionId || !toolName) return null;
8965
+ const toolUseId = typeof hook.tool_use_id === "string" && hook.tool_use_id ? hook.tool_use_id : void 0;
8966
+ const expr = pendingCardExpr({ sessionId, toolName, toolUseId, toolInput: hook.tool_input });
8967
+ const sleep3 = this.#d.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
8968
+ let last = "";
8969
+ for (let i = 0; ; i++) {
8970
+ if (signal?.aborted) return null;
8971
+ const r = await conn.eval(expr, APP_LOOKUP_TIMEOUT_MS);
8972
+ if (r && r.ok === true) {
8973
+ const set = appCardOptions(r, hook);
8974
+ if (!set) this.#d.log(`${tool} App \u6388\u6743\u5361\u9009\u9879\u81EA\u68C0\u4E0D\u8FC7\uFF08${r.variant}\uFF09\uFF0C\u9000\u56DE\u67E5\u8868`);
8975
+ return set;
8976
+ }
8977
+ last = r ? `${r.why}${r.message ? `:${r.message}` : ""}` : "timeout";
8978
+ if (!r || r.why !== "not_found" || !r.busy || i >= APP_LOOKUP_RETRY_MS.length) break;
8979
+ await sleep3(APP_LOOKUP_RETRY_MS[i]);
8980
+ }
8981
+ this.#d.log(`${tool} App \u6388\u6743\u5361\u6CA1\u53D6\u5230\u771F\u5B9E\u9009\u9879\uFF08${last}\uFF09\uFF0C\u9000\u56DE\u67E5\u8868`);
8982
+ return null;
8983
+ }
8984
+ /** 设备选了「本次任务使用 X 模式」:经 App 回答。ok = App 接了;gone = 这条已不在待确认(电脑上已处理);failed = 没回成 */
8985
+ async respond(tool, sessionId, toolUseId, permissionSuggestionId) {
8986
+ const conn = this.#conn(tool);
8987
+ if (!conn) return "failed";
8988
+ const r = await conn.eval(respondSuggestionExpr(sessionId, toolUseId, permissionSuggestionId), APP_LOOKUP_TIMEOUT_MS);
8989
+ if (r?.ok === true) return "ok";
8990
+ if (r?.why === "gone") return "gone";
8991
+ this.#d.log(`${tool} App \u56DE\u7B54\u6388\u6743\u5931\u8D25\uFF08${r ? `${r.why}${r.message ? `:${r.message}` : ""}` : "timeout"}\uFF09\uFF0C\u6539\u7531 hook \u56DE\u7B54`);
8992
+ return "failed";
8993
+ }
8994
+ };
8995
+
7944
8996
  // src/daemon/qoderApp/task.ts
7945
8997
  import { createHash as createHash3 } from "crypto";
7946
8998
  import { realpathSync as realpathSync4 } from "fs";
@@ -8859,7 +9911,7 @@ import { dirname as dirname6, sep as sep2 } from "path";
8859
9911
  // src/core/wrapWindows.ts
8860
9912
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync15, readdirSync as readdirSync12, readFileSync as readFileSync14, renameSync as renameSync10, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync14 } from "fs";
8861
9913
  import { tmpdir as tmpdir2 } from "os";
8862
- import { join as join20 } from "path";
9914
+ import { join as join21 } from "path";
8863
9915
  var WINDOW_ID_RE = /^[a-f0-9]{32}$/;
8864
9916
  var WINDOW_EVENTS = [
8865
9917
  "SessionStart",
@@ -8872,10 +9924,10 @@ var WINDOW_EVENTS = [
8872
9924
  "SessionEnd"
8873
9925
  ];
8874
9926
  function windowsDir() {
8875
- return join20(deskbuddyHome(), "windows");
9927
+ return join21(deskbuddyHome(), "windows");
8876
9928
  }
8877
9929
  function bySessionDir() {
8878
- return join20(windowsDir(), "by-session");
9930
+ return join21(windowsDir(), "by-session");
8879
9931
  }
8880
9932
  function safeSid(sessionId) {
8881
9933
  return String(sessionId).replace(/[^A-Za-z0-9_-]/g, "");
@@ -8889,7 +9941,7 @@ function atomicWrite(file3, text) {
8889
9941
  renameSync10(tmp, file3);
8890
9942
  }
8891
9943
  function windowFile(windowId) {
8892
- return join20(windowsDir(), `${windowId}.json`);
9944
+ return join21(windowsDir(), `${windowId}.json`);
8893
9945
  }
8894
9946
  function writeWindow(windowId, rec) {
8895
9947
  if (!WINDOW_ID_RE.test(windowId)) return;
@@ -8925,7 +9977,7 @@ function removeWindow(windowId) {
8925
9977
  return;
8926
9978
  }
8927
9979
  for (const n of names) {
8928
- const f = join20(bySessionDir(), n);
9980
+ const f = join21(bySessionDir(), n);
8929
9981
  try {
8930
9982
  if (readFileSync14(f, "utf8").trim() === windowId) rmSync9(f, { force: true });
8931
9983
  } catch {
@@ -8936,14 +9988,14 @@ function readBySession(sessionId) {
8936
9988
  const safe2 = safeSid(sessionId);
8937
9989
  if (!safe2) return null;
8938
9990
  try {
8939
- const id = readFileSync14(join20(bySessionDir(), safe2), "utf8").trim();
9991
+ const id = readFileSync14(join21(bySessionDir(), safe2), "utf8").trim();
8940
9992
  return WINDOW_ID_RE.test(id) ? id : null;
8941
9993
  } catch {
8942
9994
  return null;
8943
9995
  }
8944
9996
  }
8945
9997
  function lastEventFile(windowId) {
8946
- return join20(windowsDir(), `${windowId}.last`);
9998
+ return join21(windowsDir(), `${windowId}.last`);
8947
9999
  }
8948
10000
  function readLastEvent(windowId) {
8949
10001
  if (!WINDOW_ID_RE.test(windowId)) return null;
@@ -8951,239 +10003,33 @@ function readLastEvent(windowId) {
8951
10003
  const e = JSON.parse(readFileSync14(lastEventFile(windowId), "utf8"));
8952
10004
  return e && typeof e.ev === "string" && typeof e.at === "number" ? { ev: e.ev, sid: String(e.sid ?? ""), at: e.at } : null;
8953
10005
  } catch {
8954
- return null;
8955
- }
8956
- }
8957
- function writeLastEvent(windowId, ev, sid, at) {
8958
- if (!WINDOW_ID_RE.test(windowId) || !WINDOW_EVENTS.includes(ev)) return;
8959
- ensureDir(windowsDir());
8960
- atomicWrite(lastEventFile(windowId), JSON.stringify({ ev, sid, at }));
8961
- }
8962
- function channelPath(windowId, platform = process.platform, home = deskbuddyHome()) {
8963
- if (platform === "win32") return `\\\\.\\pipe\\deskbuddy-${windowId}`;
8964
- const name = `${windowId.slice(0, 16)}.sock`;
8965
- const primary = join20(home, "win", name);
8966
- if (Buffer.byteLength(primary) <= 103) return primary;
8967
- const uid = typeof process.getuid === "function" ? process.getuid() : 0;
8968
- return join20(tmpdir2(), `deskbuddy-${uid}`, name);
8969
- }
8970
- function ensureChannelDir(sockPath) {
8971
- const dir3 = join20(sockPath, "..");
8972
- try {
8973
- mkdirSync15(dir3, { recursive: true, mode: 448 });
8974
- const st = statSync8(dir3);
8975
- if (typeof process.getuid === "function" && st.uid !== process.getuid()) return false;
8976
- chmodSync2(dir3, 448);
8977
- return true;
8978
- } catch {
8979
- return false;
8980
- }
8981
- }
8982
-
8983
- // src/wrap/answerKeys.ts
8984
- var KEY_DOWN = "\x1B[B";
8985
- var KEY_UP = "\x1B[A";
8986
- var KEY_ENTER = "\r";
8987
- var ARROW_GAP_MS = 150;
8988
- var CONFIRM_MS = 4e3;
8989
- var POLL_MS2 = 150;
8990
- var FINGERPRINT_CHARS = 40;
8991
- var FINGERPRINT_TAIL = 30;
8992
- var FP_SEP = "\u2026";
8993
- var CURSOR = "[\u276F>\u203A]";
8994
- function joined(lines3) {
8995
- return lines3.join("\n");
8996
- }
8997
- function squash(s2) {
8998
- return s2.replace(/[\s\u2500-\u257F]+/g, "");
8999
- }
9000
- function basename8(p) {
9001
- const parts = p.split(/[\\/]+/).filter(Boolean);
9002
- return parts[parts.length - 1] ?? "";
9003
- }
9004
- function displayFingerprint(toolName, input) {
9005
- const a = input && typeof input === "object" ? input : {};
9006
- const s2 = (v) => typeof v === "string" ? v : "";
9007
- const headTail = (v) => {
9008
- const q = squash(v);
9009
- return q.length > FINGERPRINT_CHARS + FINGERPRINT_TAIL ? q.slice(0, FINGERPRINT_CHARS) + FP_SEP + q.slice(-FINGERPRINT_TAIL) : q;
9010
- };
9011
- if (toolName === "Bash") return headTail(s2(a.command));
9012
- if (toolName === "Write") {
9013
- const name = squash(basename8(s2(a.file_path)));
9014
- const body = squash(s2(a.content)).slice(0, 20);
9015
- return name ? body ? name + FP_SEP + body : name : "";
9016
- }
9017
- if (["Edit", "MultiEdit", "Read", "NotebookEdit"].includes(toolName)) return squash(basename8(s2(a.file_path) || s2(a.notebook_path)));
9018
- return headTail(Object.values(a).find((v) => typeof v === "string" && v.trim() !== "") ?? "").slice(0, 200);
9019
- }
9020
- var SETTLE_MS = 800;
9021
- async function settled(io, ok) {
9022
- if (!ok(io.screen())) return false;
9023
- await io.sleep(SETTLE_MS);
9024
- return ok(io.screen());
9025
- }
9026
- async function waitUntil(io, ok, ms = CONFIRM_MS) {
9027
- const loops = Math.max(1, Math.ceil(ms / POLL_MS2));
9028
- for (let i = 0; i < loops; i++) {
9029
- if (ok(io.screen())) return true;
9030
- await io.sleep(POLL_MS2);
9031
- }
9032
- return ok(io.screen());
9033
- }
9034
- function permissionRegion(lines3, toolName) {
9035
- let q = -1;
9036
- for (let i = lines3.length - 1; i >= 0; i--) {
9037
- if (/^\s*Do you want to .+\?\s*$/.test(lines3[i])) {
9038
- q = i;
9039
- break;
9040
- }
9041
- }
9042
- if (q < 0 || !toolName) return null;
9043
- const head = new RegExp(`^\\s*\u25CF\\s*${toolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\(`);
9044
- let h = -1;
9045
- for (let i = q - 1; i >= 0; i--) {
9046
- if (head.test(lines3[i])) {
9047
- h = i;
9048
- break;
9049
- }
9050
- if (/^\s*●/.test(lines3[i])) return null;
9051
- }
9052
- if (h < 0) return null;
9053
- const after = joined(lines3.slice(q + 1, q + 6));
9054
- if (!new RegExp(`^\\s*(?:${CURSOR}\\s*)?1\\.\\s+Yes\\s*$`, "m").test(after)) return null;
9055
- if (!/^\s*(?:[❯>›]\s*)?3\.\s+No\b/m.test(after)) return null;
9056
- return lines3.slice(h, q + 1);
9057
- }
9058
- function isPermissionDialog(lines3, toolName, fingerprint2) {
9059
- if (!fingerprint2) return false;
9060
- const region = permissionRegion(lines3, toolName);
9061
- if (!region) return false;
9062
- const text = squash(joined(region));
9063
- return fingerprint2.split(FP_SEP).every((part) => !!part && text.includes(squash(part)));
9064
- }
9065
- async function answerPermission(io, toolName, fingerprint2, decision) {
9066
- const ok = (l) => isPermissionDialog(l, toolName, fingerprint2);
9067
- if (!await settled(io, ok)) return "not_waiting";
9068
- io.write(decision === "allow" ? "1" : "3");
9069
- return await waitUntil(io, (l) => !ok(l)) ? "done" : "partial";
9070
- }
9071
- var TAB_ROW = /←.*[☐☒].*Submit.*→/;
9072
- function isQuestionDialog(lines3) {
9073
- return lines3.some((l) => TAB_ROW.test(l));
9074
- }
9075
- function onQuestion(lines3, q) {
9076
- const start = questionStart(lines3);
9077
- if (start < 0) return false;
9078
- const t = squash(q.question);
9079
- return !!t && squash(joined(lines3.slice(start))).includes(t);
9080
- }
9081
- function isReviewPage(lines3) {
9082
- const t = joined(lines3);
9083
- return /Review your answers/.test(t) && /Submit answers/.test(t);
9084
- }
9085
- function questionStart(lines3) {
9086
- for (let i = lines3.length - 1; i >= 0; i--) if (TAB_ROW.test(lines3[i])) return i + 1;
9087
- return -1;
9088
- }
9089
- function optionLine(lines3, n, label) {
9090
- const lab = squash(label).slice(0, 6);
9091
- const start = questionStart(lines3);
9092
- if (start < 0) return -1;
9093
- for (let i = start; i < lines3.length; i++) {
9094
- const m = lines3[i].match(/^\s*(?:[❯>›]\s*)?[☐☒]?\s*(\d+)\.\s+(.*)$/);
9095
- if (m && Number(m[1]) === n && squash(m[2]).startsWith(lab)) return i;
9096
- }
9097
- return -1;
9098
- }
9099
- function checked(lines3, line) {
9100
- return line >= 0 && /^\s*☒/.test(lines3[line]);
9101
- }
9102
- function singleCursor(lines3, q) {
9103
- const n = q.options.length;
9104
- const start = questionStart(lines3);
9105
- if (start < 0) return -1;
9106
- for (let i = start; i < lines3.length; i++) {
9107
- const l = lines3[i];
9108
- const m = l.match(new RegExp(`^\\s*${CURSOR}\\s*(\\d+)\\.\\s+(.*)$`));
9109
- if (m) {
9110
- const k = Number(m[1]) - 1;
9111
- if (k >= 0 && k < n) return squash(m[2]).startsWith(squash(q.options[k].label).slice(0, 6)) ? k : -1;
9112
- if (k === n) return /^Typesomething/.test(squash(m[2])) ? n : -1;
9113
- return -1;
9114
- }
9115
- if (new RegExp(`^\\s*${CURSOR}\\s*(Next|Submit)\\s*$`).test(l)) return n + 1;
10006
+ return null;
9116
10007
  }
9117
- return -1;
9118
10008
  }
9119
- async function move(io, delta) {
9120
- const key = delta > 0 ? KEY_DOWN : KEY_UP;
9121
- for (let i = 0; i < Math.abs(delta); i++) {
9122
- io.write(key);
9123
- await io.sleep(ARROW_GAP_MS);
9124
- }
10009
+ function writeLastEvent(windowId, ev, sid, at) {
10010
+ if (!WINDOW_ID_RE.test(windowId) || !WINDOW_EVENTS.includes(ev)) return;
10011
+ ensureDir(windowsDir());
10012
+ atomicWrite(lastEventFile(windowId), JSON.stringify({ ev, sid, at }));
9125
10013
  }
9126
- async function answerQuestions(io, questions, selections) {
9127
- if (questions.length === 0 || selections.length !== questions.length) return "not_waiting";
9128
- for (let qi = 0; qi < questions.length; qi++) {
9129
- const sel = selections[qi];
9130
- const q = questions[qi];
9131
- if (sel.length === 0 || sel.some((i) => !Number.isInteger(i) || i < 0 || i >= q.options.length)) return "not_waiting";
9132
- if (q.multiSelect !== true && sel.length !== 1) return "not_waiting";
9133
- }
9134
- if (!await settled(io, (l) => onQuestion(l, questions[0]))) return "not_waiting";
9135
- if (questions.some((q) => q.multiSelect === true) && (!io.cursorLine || io.cursorLine() === null)) return "unsupported";
9136
- let wrote = false;
9137
- const fail = () => wrote ? "partial" : "not_waiting";
9138
- for (let qi = 0; qi < questions.length; qi++) {
9139
- const q = questions[qi];
9140
- const n = q.options.length;
9141
- const sel = [...selections[qi]].sort((a, b) => a - b);
9142
- if (!await waitUntil(io, (l) => onQuestion(l, q))) return fail();
9143
- if (q.multiSelect === true) {
9144
- const lines0 = io.screen();
9145
- for (let i = 0; i < n; i++) if (checked(lines0, optionLine(lines0, i + 1, q.options[i].label))) return fail();
9146
- const first = optionLine(lines0, 1, q.options[0].label);
9147
- if (first < 0 || io.cursorLine?.() !== first) return fail();
9148
- let cur = 0;
9149
- for (const t of sel) {
9150
- await move(io, t - cur);
9151
- cur = t;
9152
- const onTarget = await waitUntil(io, (l) => {
9153
- const ln = optionLine(l, t + 1, q.options[t].label);
9154
- return ln >= 0 && io.cursorLine?.() === ln;
9155
- });
9156
- if (!onTarget) return fail();
9157
- io.write(KEY_ENTER);
9158
- wrote = true;
9159
- if (!await waitUntil(io, (l) => checked(l, optionLine(l, t + 1, q.options[t].label)))) return fail();
9160
- }
9161
- await move(io, n + 1 - cur);
9162
- if (!await waitUntil(io, (l) => questionStart(l) >= 0 && new RegExp(`^\\s*${CURSOR}\\s*(Submit|Next)\\s*$`, "m").test(joined(l.slice(questionStart(l)))))) return fail();
9163
- io.write(KEY_ENTER);
9164
- wrote = true;
9165
- } else {
9166
- const t = sel[0];
9167
- const cur = singleCursor(io.screen(), q);
9168
- if (cur < 0) return fail();
9169
- await move(io, t - cur);
9170
- if (!await waitUntil(io, (l) => singleCursor(l, q) === t && optionLine(l, t + 1, q.options[t].label) >= 0)) return fail();
9171
- io.write(KEY_ENTER);
9172
- wrote = true;
9173
- }
9174
- const next = questions[qi + 1];
9175
- const moved = await waitUntil(io, (l) => next ? onQuestion(l, next) : isReviewPage(l) || !isQuestionDialog(l));
9176
- if (!moved) return fail();
9177
- }
9178
- const s2 = io.screen();
9179
- if (!isReviewPage(s2)) return isQuestionDialog(s2) ? fail() : "done";
9180
- if (!new RegExp(`^\\s*${CURSOR}\\s*1\\.\\s+Submit answers`, "m").test(joined(s2))) return fail();
9181
- io.write(KEY_ENTER);
9182
- return await waitUntil(io, (l) => !isReviewPage(l) && !isQuestionDialog(l)) ? "done" : "partial";
10014
+ function channelPath(windowId, platform = process.platform, home = deskbuddyHome()) {
10015
+ if (platform === "win32") return `\\\\.\\pipe\\deskbuddy-${windowId}`;
10016
+ const name = `${windowId.slice(0, 16)}.sock`;
10017
+ const primary = join21(home, "win", name);
10018
+ if (Buffer.byteLength(primary) <= 103) return primary;
10019
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
10020
+ return join21(tmpdir2(), `deskbuddy-${uid}`, name);
9183
10021
  }
9184
- function stillWaiting(lines3, probe) {
9185
- if (probe.questions && probe.questions.length > 0) return probe.questions.some((q) => onQuestion(lines3, q)) || isReviewPage(lines3);
9186
- return isPermissionDialog(lines3, probe.toolName, probe.fingerprint ?? "");
10022
+ function ensureChannelDir(sockPath) {
10023
+ const dir3 = join21(sockPath, "..");
10024
+ try {
10025
+ mkdirSync15(dir3, { recursive: true, mode: 448 });
10026
+ const st = statSync8(dir3);
10027
+ if (typeof process.getuid === "function" && st.uid !== process.getuid()) return false;
10028
+ chmodSync2(dir3, 448);
10029
+ return true;
10030
+ } catch {
10031
+ return false;
10032
+ }
9187
10033
  }
9188
10034
 
9189
10035
  // src/wrap/channel.ts
@@ -9211,6 +10057,20 @@ function parseAnswer(m) {
9211
10057
  if (a.decision !== "allow" && a.decision !== "deny") return null;
9212
10058
  return { instructionId, sessionId, expireAt, answer: { kind: "permission", toolName, fingerprint: fingerprint2, decision: a.decision } };
9213
10059
  }
10060
+ if (a.kind === "qoder-read" || a.kind === "qoder-key") {
10061
+ const toolName = toolNameOf(a.toolName);
10062
+ const fp = a.fingerprint === void 0 ? void 0 : str4(a.fingerprint, 4e3);
10063
+ if (!toolName || fp === null) return null;
10064
+ const f = fp ? { fingerprint: fp } : {};
10065
+ if (a.kind === "qoder-read") return { instructionId, sessionId, expireAt, answer: { kind: "qoder-read", toolName, ...f } };
10066
+ const labels = Array.isArray(a.labels) ? a.labels : [];
10067
+ if (labels.length < 2 || labels.length > 16 || !labels.every((l) => str4(l, 1e3) !== null)) return null;
10068
+ const n = a.n;
10069
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > labels.length) return null;
10070
+ const box = a.box;
10071
+ if (typeof box !== "number" || !Number.isInteger(box) || box < 1) return null;
10072
+ return { instructionId, sessionId, expireAt, answer: { kind: "qoder-key", toolName, ...f, labels, n, box } };
10073
+ }
9214
10074
  if (a.prompt !== void 0 && a.prompt !== "hooks") return null;
9215
10075
  const hooks = a.prompt === "hooks" ? { prompt: "hooks" } : {};
9216
10076
  if (a.kind === "trust") {
@@ -9319,18 +10179,19 @@ function parseRequest(line) {
9319
10179
  }
9320
10180
  var HEX32 = /^[0-9a-f]{32}$/;
9321
10181
  function replyMac(secret, nonceC, r) {
9322
- return wakeMac(secret, "r", nonceC, r.instructionId, r.status, r.reason ?? "");
10182
+ const extra = r.options !== void 0 || r.box !== void 0 ? [canonicalJson({ options: r.options, box: r.box })] : [];
10183
+ return wakeMac(secret, "r", nonceC, r.instructionId, r.status, r.reason ?? "", ...extra);
9323
10184
  }
9324
10185
  function startChannelServer(path, secret, onRequest, platform = process.platform, onAnswer) {
9325
10186
  return new Promise((resolve10) => {
9326
10187
  const server = createServer3((sock) => {
9327
- let open2 = true;
10188
+ let open3 = true;
9328
10189
  let phase = "hello";
9329
10190
  let nonceC = "";
9330
10191
  let nonceS = "";
9331
10192
  sock.on("error", () => {
9332
10193
  });
9333
- sock.on("close", () => open2 = false);
10194
+ sock.on("close", () => open3 = false);
9334
10195
  const authTimer = setTimeout(() => {
9335
10196
  if (phase === "hello") sock.destroy();
9336
10197
  }, 5e3);
@@ -9375,7 +10236,7 @@ function startChannelServer(path, secret, onRequest, platform = process.platform
9375
10236
  return;
9376
10237
  }
9377
10238
  phase = "done";
9378
- onRequest(req, reply, () => open2);
10239
+ onRequest(req, reply, () => open3);
9379
10240
  } catch {
9380
10241
  sock.destroy();
9381
10242
  }
@@ -9615,10 +10476,10 @@ function toolOfMessage(message) {
9615
10476
  }
9616
10477
  function pendingCall(text, toolName) {
9617
10478
  const { calls, done } = scanTranscript(text);
9618
- const open2 = calls.filter((c) => !done.has(c.callId));
9619
- const last = open2[open2.length - 1];
10479
+ const open3 = calls.filter((c) => !done.has(c.callId));
10480
+ const last = open3[open3.length - 1];
9620
10481
  if (!last || last.name !== toolName) return null;
9621
- if (open2.filter((c) => c.name === toolName).length > 1) return null;
10482
+ if (open3.filter((c) => c.name === toolName).length > 1) return null;
9622
10483
  return last;
9623
10484
  }
9624
10485
  function selectionsOf(input, lists) {
@@ -9896,6 +10757,62 @@ function defaultSend(windowId, rec, req) {
9896
10757
  return sendAnswer(channelPath(windowId), secret, req, { finalTimeoutMs: req.answer.kind === "probe" ? 5e3 : 60 * 1e3 });
9897
10758
  }
9898
10759
 
10760
+ // src/daemon/qoderScreen.ts
10761
+ import { randomUUID as randomUUID2 } from "crypto";
10762
+
10763
+ // src/daemon/window.ts
10764
+ function windowForSession(sessionId, tool) {
10765
+ const id = readBySession(sessionId);
10766
+ if (!id) return null;
10767
+ const rec = readWindow(id);
10768
+ if (!rec || !isAlive(rec.pid)) return null;
10769
+ if (tool && rec.tool && rec.tool !== tool) return null;
10770
+ return id;
10771
+ }
10772
+
10773
+ // src/daemon/qoderScreen.ts
10774
+ var READ_TIMEOUT_MS = 5e3;
10775
+ var PRESS_TIMEOUT_MS = 15e3;
10776
+ var EXPIRE_MS = 3e4;
10777
+ var defaultDeps = {
10778
+ windowFor: windowForSession,
10779
+ readWindow,
10780
+ send: (windowId, req, finalTimeoutMs) => sendAnswer(channelPath(windowId), windowSecret(windowId), req, { finalTimeoutMs })
10781
+ };
10782
+ function qoderScreen(d = defaultDeps) {
10783
+ const target = (sessionId) => {
10784
+ const id = d.windowFor(sessionId, "qoder_cn");
10785
+ if (!id) return null;
10786
+ const rec = d.readWindow(id);
10787
+ return rec && rec.tool === "qoder_cn" && rec.answer !== true ? id : null;
10788
+ };
10789
+ const req = (sessionId, answer) => ({
10790
+ instructionId: randomUUID2().replace(/-/g, ""),
10791
+ sessionId,
10792
+ expireAt: Date.now() + EXPIRE_MS,
10793
+ answer
10794
+ });
10795
+ return {
10796
+ async read(sessionId, toolName, fingerprint2) {
10797
+ const win = target(sessionId);
10798
+ if (!win) return null;
10799
+ const r = await d.send(win, req(sessionId, { kind: "qoder-read", toolName, ...fingerprint2 ? { fingerprint: fingerprint2 } : {} }), READ_TIMEOUT_MS);
10800
+ if (!r || r.status !== "started" || !Array.isArray(r.options) || typeof r.box !== "number" || !Number.isInteger(r.box) || r.box < 1) return null;
10801
+ const labels = r.options.filter((x) => typeof x === "string" && x.length > 0);
10802
+ return labels.length === r.options.length && labels.length >= 2 ? { labels, box: r.box } : null;
10803
+ },
10804
+ async press(sessionId, box, n) {
10805
+ const win = target(sessionId);
10806
+ if (!win) return "not_written";
10807
+ const answer = { kind: "qoder-key", toolName: box.toolName, ...box.fingerprint ? { fingerprint: box.fingerprint } : {}, labels: box.labels, n, box: box.box };
10808
+ const r = await d.send(win, req(sessionId, answer), PRESS_TIMEOUT_MS);
10809
+ if (r?.status === "started") return "done";
10810
+ if (!r || r.status === "rejected" || r.status === "not_here") return "not_written";
10811
+ return "uncertain";
10812
+ }
10813
+ };
10814
+ }
10815
+
9899
10816
  // src/daemon/codebuddyTerminal.ts
9900
10817
  import { closeSync as closeSync8, openSync as openSync8, readSync as readSync7, statSync as statSync10 } from "fs";
9901
10818
  var ANNOUNCE_TTL_MS = 5 * 1e3;
@@ -10172,12 +11089,12 @@ var CodebuddyTerminal = class {
10172
11089
 
10173
11090
  // src/wrap/trustPrompt.ts
10174
11091
  import { existsSync as existsSync10, realpathSync as realpathSync7, statSync as statSync11 } from "fs";
10175
- import { homedir as homedir6 } from "os";
10176
- import { dirname as dirname7, join as join21, resolve as resolve7 } from "path";
11092
+ import { homedir as homedir7 } from "os";
11093
+ import { dirname as dirname7, join as join22, resolve as resolve7 } from "path";
10177
11094
  var TRUST_TOOLS = /* @__PURE__ */ new Set(["claude_code", "workbuddy", "codex"]);
10178
11095
  var CURSOR_CHARS = "\u276F>\u203A";
10179
11096
  var BOX = /[─-╿|]/g;
10180
- function squash2(s2) {
11097
+ function squash3(s2) {
10181
11098
  return s2.replace(BOX, "").replace(/\s+/g, "");
10182
11099
  }
10183
11100
  function squashLines(lines3) {
@@ -10330,7 +11247,7 @@ function pathShown(v, forms) {
10330
11247
  const body = v.body.toLowerCase();
10331
11248
  const breaks = v.breaks ?? [];
10332
11249
  return forms.some((f) => {
10333
- const q = squash2(f).toLowerCase();
11250
+ const q = squash3(f).toLowerCase();
10334
11251
  if (!q) return false;
10335
11252
  for (let i = body.indexOf(q); i >= 0; i = body.indexOf(q, i + 1)) {
10336
11253
  const end = i + q.length;
@@ -10339,7 +11256,7 @@ function pathShown(v, forms) {
10339
11256
  return false;
10340
11257
  });
10341
11258
  }
10342
- function trustCwdForms(cwd, real2 = realpathSync7, home = homedir6()) {
11259
+ function trustCwdForms(cwd, real2 = realpathSync7, home = homedir7()) {
10343
11260
  const out = /* @__PURE__ */ new Set();
10344
11261
  const add = (p) => {
10345
11262
  if (!p) return;
@@ -10363,7 +11280,7 @@ function gitStat(p) {
10363
11280
  return null;
10364
11281
  }
10365
11282
  }
10366
- function findRepoScope(tool, cwd, stat2 = gitStat, home = homedir6(), real2 = realpathSync7) {
11283
+ function findRepoScope(tool, cwd, stat2 = gitStat, home = homedir7(), real2 = realpathSync7) {
10367
11284
  if (tool !== "claude_code" && tool !== "codex" || !cwd) return null;
10368
11285
  const logical = resolve7(cwd);
10369
11286
  const a = scanRepo(logical, stat2, home);
@@ -10381,7 +11298,7 @@ function scanRepo(start, stat2, home) {
10381
11298
  let d = start.replace(/[\\/]+$/, "") || start;
10382
11299
  let aboveHome = false;
10383
11300
  for (let i = 0; i < 64; i++) {
10384
- const g = stat2(join21(d, ".git"));
11301
+ const g = stat2(join22(d, ".git"));
10385
11302
  if (g) {
10386
11303
  if (aboveHome) return { kind: "unknown" };
10387
11304
  if (g === "file") return { kind: "worktree", at: d };
@@ -10421,11 +11338,11 @@ function trustAnswerable(v, forms) {
10421
11338
  if (v.kind === "hooks") return true;
10422
11339
  return !widenedBeyond(v, forms) && pathShown(v, forms);
10423
11340
  }
10424
- var SETTLE_MS2 = 800;
10425
- var CONFIRM_MS2 = 4e3;
11341
+ var SETTLE_MS3 = 800;
11342
+ var CONFIRM_MS3 = 4e3;
10426
11343
  var POLL_MS4 = 150;
10427
11344
  var ARROW_GAP_MS2 = 150;
10428
- async function waitUntil2(io, ok, ms = CONFIRM_MS2) {
11345
+ async function waitUntil3(io, ok, ms = CONFIRM_MS3) {
10429
11346
  const loops = Math.max(1, Math.ceil(ms / POLL_MS4));
10430
11347
  for (let i = 0; i < loops; i++) {
10431
11348
  if (ok(io.screen())) return true;
@@ -10441,7 +11358,7 @@ async function answerTrust(io, tool, decision, forms, kind = "trust", repoRoot)
10441
11358
  };
10442
11359
  const v0 = view(io.screen());
10443
11360
  if (!v0) return "not_waiting";
10444
- await io.sleep(SETTLE_MS2);
11361
+ await io.sleep(SETTLE_MS3);
10445
11362
  const v = view(io.screen());
10446
11363
  if (!v || v.cursor !== 0) return "not_waiting";
10447
11364
  const role = decision === "allow" ? "folder" : "no";
@@ -10456,7 +11373,7 @@ async function answerTrust(io, tool, decision, forms, kind = "trust", repoRoot)
10456
11373
  wrote = true;
10457
11374
  await io.sleep(ARROW_GAP_MS2);
10458
11375
  }
10459
- const onTarget = await waitUntil2(io, (l) => {
11376
+ const onTarget = await waitUntil3(io, (l) => {
10460
11377
  const w = view(l);
10461
11378
  return !!w && w.cursor >= 0 && w.options[w.cursor].role === role;
10462
11379
  });
@@ -10465,7 +11382,7 @@ async function answerTrust(io, tool, decision, forms, kind = "trust", repoRoot)
10465
11382
  } finally {
10466
11383
  io.hold?.(false);
10467
11384
  }
10468
- return await waitUntil2(io, (l) => !inspectTrust(l, tool, kind)) ? "done" : "partial";
11385
+ return await waitUntil3(io, (l) => !inspectTrust(l, tool, kind)) ? "done" : "partial";
10469
11386
  }
10470
11387
  function trustWaiting(lines3, tool, kind = "trust") {
10471
11388
  return inspectTrust(lines3, tool, kind) !== null;
@@ -11972,9 +12889,9 @@ function codexQuestionInput(p) {
11972
12889
 
11973
12890
  // src/core/codexRemote.ts
11974
12891
  import { mkdirSync as mkdirSync16, renameSync as renameSync11, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "fs";
11975
- import { join as join22 } from "path";
12892
+ import { join as join23 } from "path";
11976
12893
  function codexRemotePath() {
11977
- return join22(deskbuddyHome(), "codex-remote.json");
12894
+ return join23(deskbuddyHome(), "codex-remote.json");
11978
12895
  }
11979
12896
  function writeCodexRemote(threads, pid = process.pid, now = Date.now()) {
11980
12897
  try {
@@ -11992,71 +12909,9 @@ function removeCodexRemote() {
11992
12909
  }
11993
12910
  }
11994
12911
 
11995
- // src/wrap/screen.ts
11996
- var ESC = "\x1B";
11997
- var PASTE_BEGIN = ESC + "[200~";
11998
- var PASTE_END = ESC + "[201~";
11999
- function sanitize(text) {
12000
- return String(text).replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
12001
- }
12002
- var MAX_INJECT_CHARS = 800;
12003
- var TEXT_TOO_LONG = "text_too_long";
12004
- function oneLine(text) {
12005
- return sanitize(text).replace(/\s*\n\s*/g, " ").trim();
12006
- }
12007
- function tooLongToInject(text) {
12008
- return oneLine(text).length > MAX_INJECT_CHARS;
12009
- }
12010
- function prepare(text) {
12011
- return " " + oneLine(text);
12012
- }
12013
- var TRUST_RE = /trust (the files in )?this folder|Yes, I trust this folder/i;
12014
- var PLACEHOLDER_RE = /^Try ".*"$/;
12015
- function isTerminalReport(s2) {
12016
- if (!s2) return true;
12017
- const rest = s2.replace(/\x1b\[[IO]/g, "").replace(/\x1b\[\d+;\d+R/g, "").replace(/\x1b\[[?>][\d;]*\$?[a-zA-Z]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1bP[^\x1b]*\x1b\\/g, "");
12018
- return rest === "";
12019
- }
12020
- var PERMISSION_RE = /Do you want to (proceed|make|create|run|allow)|[❯>]\s*1\. Yes/;
12021
- function inspectLines(lines3, undimmed) {
12022
- const text = lines3.join("\n");
12023
- const trust = TRUST_RE.test(text);
12024
- const permission = PERMISSION_RE.test(text);
12025
- const rules = [];
12026
- lines3.forEach((l, i) => {
12027
- if (/^─{20,}/.test(l.trim())) rules.push(i);
12028
- });
12029
- let input = null;
12030
- if (rules.length >= 2) {
12031
- const a = rules[rules.length - 2];
12032
- const b = rules[rules.length - 1];
12033
- const src = undimmed && undimmed.length === lines3.length ? undimmed : lines3;
12034
- input = src.slice(a + 1, b).map((l) => l.trimEnd()).join("\n").replace(/^\s*[❯>]\s?/, "").trimEnd();
12035
- }
12036
- const v = { permission, input };
12037
- if (trust) v.trust = true;
12038
- if (input !== null && PLACEHOLDER_RE.test(input.trim())) v.placeholder = true;
12039
- return v;
12040
- }
12041
- function boxMatches(view, body) {
12042
- if (view.input === null || view.permission) return false;
12043
- const flat2 = (x) => x.replace(/\s+/g, "");
12044
- return flat2(view.input) === flat2(body) || /\[Pasted text #\d+/.test(view.input);
12045
- }
12046
-
12047
- // src/daemon/window.ts
12048
- function windowForSession(sessionId, tool) {
12049
- const id = readBySession(sessionId);
12050
- if (!id) return null;
12051
- const rec = readWindow(id);
12052
- if (!rec || !isAlive(rec.pid)) return null;
12053
- if (tool && rec.tool && rec.tool !== tool) return null;
12054
- return id;
12055
- }
12056
-
12057
12912
  // src/core/interrupt.ts
12058
12913
  import { closeSync as closeSync9, openSync as openSync9, readdirSync as readdirSync13, readSync as readSync8, statSync as statSync12 } from "fs";
12059
- import { dirname as dirname8, join as join23 } from "path";
12914
+ import { dirname as dirname8, join as join24 } from "path";
12060
12915
  var INTERRUPT_TOOLS = ["claude_code", "qoder_cn", "qoder"];
12061
12916
  function transcriptHome(tool) {
12062
12917
  if (tool === "claude_code") return claudeHome();
@@ -12162,17 +13017,17 @@ function claudeTranscriptFile(sessionId, cwd, home = claudeHome()) {
12162
13017
  }
12163
13018
  };
12164
13019
  if (cwd) {
12165
- const f = join23(home, "projects", projectDirName(cwd), name);
13020
+ const f = join24(home, "projects", projectDirName(cwd), name);
12166
13021
  if (exists(f)) return f;
12167
13022
  }
12168
13023
  let dirs = [];
12169
13024
  try {
12170
- dirs = readdirSync13(join23(home, "projects"));
13025
+ dirs = readdirSync13(join24(home, "projects"));
12171
13026
  } catch {
12172
13027
  return null;
12173
13028
  }
12174
13029
  for (const d of dirs) {
12175
- const f = join23(home, "projects", d, name);
13030
+ const f = join24(home, "projects", d, name);
12176
13031
  if (exists(f)) return f;
12177
13032
  }
12178
13033
  return null;
@@ -12190,12 +13045,12 @@ function shouldSettleInterrupt(st, markerAt, now) {
12190
13045
  }
12191
13046
 
12192
13047
  // src/daemon/qoderAppDb.ts
12193
- import { execFile as execFile4 } from "child_process";
13048
+ import { execFile as execFile5 } from "child_process";
12194
13049
  import { createHash as createHash4 } from "crypto";
12195
13050
  import { existsSync as existsSync11 } from "fs";
12196
- import { access, open, readdir, stat } from "fs/promises";
13051
+ import { access, open as open2, readdir, stat } from "fs/promises";
12197
13052
  import { createRequire as createRequire3 } from "module";
12198
- import { basename as basename9, dirname as dirname9, isAbsolute as isAbsolute4, join as join24 } from "path";
13053
+ import { basename as basename9, dirname as dirname9, isAbsolute as isAbsolute4, join as join25 } from "path";
12199
13054
  var QODER_APP_TOOLS = ["qoder_cn", "qoder"];
12200
13055
  var DB_SCRIPT = String.raw`
12201
13056
  const { DatabaseSync } = require('node:sqlite');
@@ -12272,7 +13127,7 @@ function qoderConfigDir(tool) {
12272
13127
  }
12273
13128
  async function findTranscript(configDir, sessionId) {
12274
13129
  if (!SESSION_UUID_RE.test(sessionId)) return null;
12275
- const root = join24(configDir, "projects");
13130
+ const root = join25(configDir, "projects");
12276
13131
  let dirs;
12277
13132
  try {
12278
13133
  dirs = await readdir(root);
@@ -12280,7 +13135,7 @@ async function findTranscript(configDir, sessionId) {
12280
13135
  return null;
12281
13136
  }
12282
13137
  for (const d of dirs) {
12283
- const p = join24(root, d, `${sessionId}.jsonl`);
13138
+ const p = join25(root, d, `${sessionId}.jsonl`);
12284
13139
  try {
12285
13140
  await access(p);
12286
13141
  return p;
@@ -12291,7 +13146,7 @@ async function findTranscript(configDir, sessionId) {
12291
13146
  }
12292
13147
  var SLICE2 = 64 * 1024;
12293
13148
  async function readSlice2(p, from) {
12294
- const fh = await open(p, "r");
13149
+ const fh = await open2(p, "r");
12295
13150
  try {
12296
13151
  const size = (await fh.stat()).size;
12297
13152
  const len = Math.min(size, SLICE2);
@@ -12373,7 +13228,7 @@ var defaultQoderAppDeps = () => ({
12373
13228
  },
12374
13229
  runElectron: (bin, script, input) => new Promise((resolve10, reject) => {
12375
13230
  const env = { ...process.env, ELECTRON_RUN_AS_NODE: "1" };
12376
- const c = execFile4(
13231
+ const c = execFile5(
12377
13232
  bin,
12378
13233
  ["-e", script],
12379
13234
  { env, timeout: 15e3, windowsHide: true, maxBuffer: 1024 * 1024 },
@@ -12852,17 +13707,17 @@ function startWakeServer(path, hub, secret, platform = process.platform) {
12852
13707
 
12853
13708
  // src/core/appended.ts
12854
13709
  import { chmodSync as chmodSync4, mkdirSync as mkdirSync17, readFileSync as readFileSync16, renameSync as renameSync12, rmSync as rmSync13, writeFileSync as writeFileSync16 } from "fs";
12855
- import { join as join25 } from "path";
13710
+ import { join as join26 } from "path";
12856
13711
  var APPENDED_MAX_ITEMS = 5;
12857
13712
  var APPENDED_TEXT_MAX = 500;
12858
13713
  var APPENDED_RESULT_MAX = 800;
12859
13714
  var APPENDED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
12860
13715
  function appendedDir() {
12861
- return join25(deskbuddyHome(), "appended");
13716
+ return join26(deskbuddyHome(), "appended");
12862
13717
  }
12863
13718
  function appendedPath(sessionId) {
12864
13719
  const safe2 = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
12865
- return safe2 ? join25(appendedDir(), `${safe2}.json`) : null;
13720
+ return safe2 ? join26(appendedDir(), `${safe2}.json`) : null;
12866
13721
  }
12867
13722
  function readAppended(sessionId) {
12868
13723
  const file3 = appendedPath(sessionId);
@@ -12892,13 +13747,13 @@ function writeAppended(sessionId, item) {
12892
13747
  var RUNNING_TTL_MS = 60 * 60 * 1e3;
12893
13748
  function runningPath(sessionId) {
12894
13749
  const safe2 = sessionId.replace(/[^A-Za-z0-9_-]/g, "");
12895
- return safe2 ? join25(deskbuddyHome(), "running", `${safe2}.json`) : null;
13750
+ return safe2 ? join26(deskbuddyHome(), "running", `${safe2}.json`) : null;
12896
13751
  }
12897
13752
  function writeRunning(sessionId, now = Date.now()) {
12898
13753
  const file3 = runningPath(sessionId);
12899
13754
  if (!file3) return;
12900
13755
  try {
12901
- mkdirSync17(join25(deskbuddyHome(), "running"), { recursive: true, mode: 448 });
13756
+ mkdirSync17(join26(deskbuddyHome(), "running"), { recursive: true, mode: 448 });
12902
13757
  writeFileSync16(`${file3}.tmp`, JSON.stringify({ at: now, pid: process.pid }), { encoding: "utf8", mode: 384 });
12903
13758
  renameSync12(`${file3}.tmp`, file3);
12904
13759
  } catch {
@@ -12946,7 +13801,7 @@ function resultSummary(tool, stdout6) {
12946
13801
 
12947
13802
  // src/core/dshMailbox.ts
12948
13803
  import { chmodSync as chmodSync5, existsSync as existsSync12, mkdirSync as mkdirSync18, readdirSync as readdirSync14, readFileSync as readFileSync17, renameSync as renameSync13, rmSync as rmSync14, writeFileSync as writeFileSync17 } from "fs";
12949
- import { join as join26 } from "path";
13804
+ import { join as join27 } from "path";
12950
13805
  var DSH_HEARTBEAT_FRESH_MS = 5 * 1e3;
12951
13806
  var DSH_ACK_TIMEOUT_MS = 8 * 1e3;
12952
13807
  var ACK_POLL_MS = 200;
@@ -12957,10 +13812,10 @@ var REVOKE_RETRY_MS = 50;
12957
13812
  var SID_RE3 = /^[A-Za-z0-9_-]{1,128}$/;
12958
13813
  var IID_RE = /^[A-Za-z0-9_-]{1,128}$/;
12959
13814
  function dshMailboxDir(home = deskbuddyHome(), channel = "dsh") {
12960
- return join26(home, channel);
13815
+ return join27(home, channel);
12961
13816
  }
12962
13817
  function sub(home, name, channel = "dsh") {
12963
- return join26(dshMailboxDir(home, channel), name);
13818
+ return join27(dshMailboxDir(home, channel), name);
12964
13819
  }
12965
13820
  function ensureDir2(p) {
12966
13821
  mkdirSync18(p, { recursive: true, mode: 448 });
@@ -13000,7 +13855,7 @@ function liveHeartbeats(sessionId, d0 = {}) {
13000
13855
  if (!/^\d+\.json$/.test(n)) continue;
13001
13856
  let hb;
13002
13857
  try {
13003
- hb = JSON.parse(readFileSync17(join26(dir3, n), "utf8"));
13858
+ hb = JSON.parse(readFileSync17(join27(dir3, n), "utf8"));
13004
13859
  } catch {
13005
13860
  continue;
13006
13861
  }
@@ -13024,8 +13879,8 @@ async function dshDeliver(sessionId, msg, d0 = {}) {
13024
13879
  const ackDir = sub(d.home, "ack", d.channel);
13025
13880
  ensureDir2(inbox);
13026
13881
  ensureDir2(ackDir);
13027
- const letter = join26(inbox, `${sessionId}.${msg.instructionId}.json`);
13028
- const ackPath = join26(ackDir, `${msg.instructionId}.json`);
13882
+ const letter = join27(inbox, `${sessionId}.${msg.instructionId}.json`);
13883
+ const ackPath = join27(ackDir, `${msg.instructionId}.json`);
13029
13884
  if (existsSync12(letter)) return { ok: false, reason: "inbox_busy" };
13030
13885
  const writtenAt = d.now();
13031
13886
  writeJson(letter, { instructionId: msg.instructionId, text: msg.text, at: writtenAt, ...d0.addressed ? { pid: hb.pid } : {} });
@@ -13098,7 +13953,7 @@ function clearDir(inbox) {
13098
13953
  let n = 0;
13099
13954
  for (const f of names) {
13100
13955
  try {
13101
- rmSync14(join26(inbox, f), { force: true });
13956
+ rmSync14(join27(inbox, f), { force: true });
13102
13957
  n++;
13103
13958
  } catch {
13104
13959
  }
@@ -13164,7 +14019,7 @@ function defaultCodexAppServers() {
13164
14019
  }
13165
14020
  function opencodeReady() {
13166
14021
  const mod = ocPluginModule(installedHookPath());
13167
- return isOcPluginInstalled(opencodePluginPath()) && existsSync13(mod) && existsSync13(join27(dirname10(mod), "ocTranslate.mjs"));
14022
+ return isOcPluginInstalled(opencodePluginPath()) && existsSync13(mod) && existsSync13(join28(dirname10(mod), "ocTranslate.mjs"));
13168
14023
  }
13169
14024
  var INTERNAL_PROMPT_MARK = "[DeskBuddy]";
13170
14025
  function orphanResumeText(summary2) {
@@ -13370,7 +14225,10 @@ var Daemon = class {
13370
14225
  policy: () => loadConfig().approvalPolicy ?? "commands",
13371
14226
  resume: (r, kind) => this.#scheduleResume(r, kind),
13372
14227
  log,
13373
- now: this.#deps.now
14228
+ now: this.#deps.now,
14229
+ qoderScreen: qoderScreen(),
14230
+ // Qoder / Qoder CN 桌面 App 里的确认卡用 App 的真实选项(连接在下面按品牌建好,这里按需取)
14231
+ qoderApp: new QoderAppApprovals({ conn: (tool) => this.#qoderConns.find((c) => c.tool === tool) ?? null, log })
13374
14232
  });
13375
14233
  this.#workbuddy = new WorkbuddyBridge({
13376
14234
  gate: (req, signal) => this.#coord.onGate(req, signal),
@@ -15309,7 +16167,7 @@ async function runDisable(tools, opts = {}) {
15309
16167
 
15310
16168
  // src/commands/install.ts
15311
16169
  import { mkdirSync as mkdirSync19, rmSync as rmSync16, writeFileSync as writeFileSync19 } from "fs";
15312
- import { join as join28 } from "path";
16170
+ import { join as join29 } from "path";
15313
16171
 
15314
16172
  // src/commands/setup.ts
15315
16173
  import qrcode from "qrcode-terminal";
@@ -15317,9 +16175,9 @@ import qrcode from "qrcode-terminal";
15317
16175
  // src/commands/remoteCwd.ts
15318
16176
  import { existsSync as existsSync14, statSync as statSync14 } from "fs";
15319
16177
  import { createInterface } from "readline";
15320
- import { homedir as homedir7 } from "os";
16178
+ import { homedir as homedir8 } from "os";
15321
16179
  import { isAbsolute as isAbsolute5, resolve as resolve8 } from "path";
15322
- function normalizePath(raw, home = homedir7()) {
16180
+ function normalizePath(raw, home = homedir8()) {
15323
16181
  const s2 = (raw || "").trim();
15324
16182
  if (!s2) return null;
15325
16183
  const expanded = s2 === "~" ? home : s2.startsWith("~/") || process.platform === "win32" && s2.startsWith("~\\") ? resolve8(home, s2.slice(2)) : s2;
@@ -15526,7 +16384,7 @@ function inSandbox(env) {
15526
16384
  }
15527
16385
  function precheckWritable(dirs) {
15528
16386
  for (const dir3 of dirs) {
15529
- const probe = join28(dir3, `.deskbuddy-probe-${process.pid}`);
16387
+ const probe = join29(dir3, `.deskbuddy-probe-${process.pid}`);
15530
16388
  try {
15531
16389
  mkdirSync19(dir3, { recursive: true });
15532
16390
  writeFileSync19(probe, "");
@@ -15936,7 +16794,7 @@ async function runAutoSwitch(opts) {
15936
16794
  // src/wrap/run.ts
15937
16795
  import { randomBytes as randomBytes10 } from "crypto";
15938
16796
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync21, statSync as statSync16, truncateSync as truncateSync3 } from "fs";
15939
- import { join as join31 } from "path";
16797
+ import { join as join32 } from "path";
15940
16798
  import { StringDecoder as StringDecoder2 } from "string_decoder";
15941
16799
 
15942
16800
  // src/wrap/injector.ts
@@ -16405,67 +17263,12 @@ function runDirect(real2, args, env = process.env) {
16405
17263
  });
16406
17264
  }
16407
17265
 
16408
- // src/wrap/screenRules.ts
16409
- var Q_RULE_RE = /^(?:─{20,}|-{20,})$/;
16410
- var Q_TRUST_RE = /Do you trust the files in this folder\?/;
16411
- var Q_PERMISSION_RE = /Permission Required|Apply this change\?|\d\. Allow once|\d\. Allow for this session/;
16412
- var Q_LOGIN_RE = /Sign in to continue|Login with QoderCN|How would you like to authenticate/;
16413
- var Q_BUSY_RE = /\(esc to cancel\b/;
16414
- var Q_PLACEHOLDER_RE = /^Type your message or @\S*$/;
16415
- var Q_FOOTER_RE = /· ctx\b/;
16416
- function bottomRegion(lines3, rules) {
16417
- const anchor = rules.length >= 3 ? rules[rules.length - 3] : rules[rules.length - 1];
16418
- return lines3.slice(Math.max(0, anchor - 1)).join("\n");
16419
- }
16420
- function qoderCnRule(lines3) {
16421
- const rules = [];
16422
- lines3.forEach((l, i) => {
16423
- if (Q_RULE_RE.test(l.trim())) rules.push(i);
16424
- });
16425
- if (rules.length === 0) return { permission: false, input: null };
16426
- const text = bottomRegion(lines3, rules);
16427
- if (Q_TRUST_RE.test(text)) return { permission: false, input: null, trust: true };
16428
- if (Q_LOGIN_RE.test(text)) return { permission: false, input: null, dialog: "login" };
16429
- const permission = Q_PERMISSION_RE.test(text);
16430
- let input = null;
16431
- if (rules.length >= 2) {
16432
- const a = rules[rules.length - 2];
16433
- const b = rules[rules.length - 1];
16434
- const box = lines3.slice(a + 1, b).map((l) => l.trimEnd());
16435
- if (box.length > 0 && /^\s*>/.test(box[0])) input = box.join("\n").replace(/^\s*>\s?/, "").trim();
16436
- }
16437
- const v = { permission, input };
16438
- if (Q_BUSY_RE.test(text)) v.busy = true;
16439
- if (input !== null && Q_PLACEHOLDER_RE.test(input)) v.placeholder = true;
16440
- else if (input !== null && input.startsWith("/")) v.dialog = "slash-menu";
16441
- if (!v.dialog && lines3.slice(rules[rules.length - 1] + 1).some((l) => l.trim() !== "" && !Q_FOOTER_RE.test(l))) v.dialog = "popup";
16442
- return v;
16443
- }
16444
- function textRejectFor(tool) {
16445
- if (tool !== "qoder_cn") return void 0;
16446
- return (text) => /^[/!]/.test(oneLine(text)) ? "slash_command" : null;
16447
- }
16448
- var RULES = {
16449
- claude_code: inspectLines,
16450
- qoder_cn: qoderCnRule
16451
- };
16452
- function screenRuleFor(tool) {
16453
- return RULES[tool] ?? (() => null);
16454
- }
16455
- var INJECT_READY = {
16456
- claude_code: true,
16457
- qoder_cn: true
16458
- };
16459
- function injectReady(tool) {
16460
- return INJECT_READY[tool] === true;
16461
- }
16462
-
16463
17266
  // src/wrap/codexLaunch.ts
16464
17267
  import { execFileSync as execFileSync7, spawn as spawn6 } from "child_process";
16465
17268
  import { closeSync as closeSync10, mkdirSync as mkdirSync20, openSync as openSync10, readdirSync as readdirSync15, readFileSync as readFileSync19, rmSync as rmSync18, statSync as statSync15, truncateSync as truncateSync2, writeFileSync as writeFileSync20, renameSync as renameSync14 } from "fs";
16466
17269
  import { request as request2 } from "http";
16467
17270
  import { createServer as createServer5 } from "net";
16468
- import { join as join29 } from "path";
17271
+ import { join as join30 } from "path";
16469
17272
  var TAKEOVER_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "--config", "-m", "--model", "-s", "--sandbox", "-a", "--ask-for-approval", "-C", "--cd", "--enable", "--disable"]);
16470
17273
  var SHORT_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "-m", "-s", "-a", "-C"]);
16471
17274
  var TAKEOVER_FLAGS = /* @__PURE__ */ new Set(["--search", "--no-alt-screen", "--dangerously-bypass-approvals-and-sandbox"]);
@@ -16634,10 +17437,10 @@ async function runCodexWindow(real2, args, d) {
16634
17437
  }
16635
17438
  var MAX_LOG_BYTES2 = 1024 * 1024;
16636
17439
  function appServerLogPath() {
16637
- return join29(deskbuddyHome(), "codex-app-server.log");
17440
+ return join30(deskbuddyHome(), "codex-app-server.log");
16638
17441
  }
16639
17442
  function windowsDir2() {
16640
- return join29(deskbuddyHome(), "codex-windows");
17443
+ return join30(deskbuddyHome(), "codex-windows");
16641
17444
  }
16642
17445
  function pidAlive5(pid) {
16643
17446
  try {
@@ -16727,7 +17530,7 @@ function killOwnServer(pid, url, args = freshArgs, kill = (p) => killServer(p))
16727
17530
  if (a && isAppServerArgs(a, url)) kill(pid);
16728
17531
  }
16729
17532
  function recordPath(rec) {
16730
- return join29(windowsDir2(), `${rec.server}.json`);
17533
+ return join30(windowsDir2(), `${rec.server}.json`);
16731
17534
  }
16732
17535
  function writeRecord(rec) {
16733
17536
  try {
@@ -16752,7 +17555,7 @@ function reapWindows(dir3 = windowsDir2(), alive = pidAlive5, args = freshArgs,
16752
17555
  return;
16753
17556
  }
16754
17557
  for (const n of names) {
16755
- const p = join29(dir3, n);
17558
+ const p = join30(dir3, n);
16756
17559
  let rec;
16757
17560
  try {
16758
17561
  rec = JSON.parse(readFileSync19(p, "utf8"));
@@ -16775,7 +17578,7 @@ function reapWindows(dir3 = windowsDir2(), alive = pidAlive5, args = freshArgs,
16775
17578
  function launchLog(line) {
16776
17579
  try {
16777
17580
  mkdirSync20(deskbuddyHome(), { recursive: true });
16778
- writeFileSync20(join29(deskbuddyHome(), "wrap.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [codex ${process.pid}] ${line}
17581
+ writeFileSync20(join30(deskbuddyHome(), "wrap.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [codex ${process.pid}] ${line}
16779
17582
  `, { flag: "a" });
16780
17583
  } catch {
16781
17584
  }
@@ -16798,9 +17601,9 @@ function defaultCodexWindowDeps(base) {
16798
17601
  }
16799
17602
 
16800
17603
  // src/wrap/promptLaunch.ts
16801
- import { randomBytes as randomBytes8, randomUUID as randomUUID2 } from "crypto";
16802
- import { homedir as homedir8 } from "os";
16803
- import { join as join30 } from "path";
17604
+ import { randomBytes as randomBytes8, randomUUID as randomUUID3 } from "crypto";
17605
+ import { homedir as homedir9 } from "os";
17606
+ import { join as join31 } from "path";
16804
17607
  var FIRST_LINE_WAIT_MS2 = 2 * 60 * 1e3;
16805
17608
  function promptArgs(text) {
16806
17609
  return text ? ["--", prepare(text)] : [];
@@ -16808,7 +17611,7 @@ function promptArgs(text) {
16808
17611
  function promptEnv(tool, env, launcherPid = process.pid, answerWindowId = "") {
16809
17612
  const out = { ...env };
16810
17613
  if (tool === "workbuddy") {
16811
- if (!out.CODEBUDDY_CONFIG_DIR) out.CODEBUDDY_CONFIG_DIR = join30(homedir8(), ".workbuddy");
17614
+ if (!out.CODEBUDDY_CONFIG_DIR) out.CODEBUDDY_CONFIG_DIR = join31(homedir9(), ".workbuddy");
16812
17615
  out.DESKBUDDY_LAUNCHER_PID = String(launcherPid);
16813
17616
  out.DESKBUDDY_LAUNCHED_TOOL = tool;
16814
17617
  if (answerWindowId) out.DESKBUDDY_LAUNCHED_WINDOW = answerWindowId;
@@ -16915,7 +17718,7 @@ async function runPromptLaunch(tool, windowId, cwd, d) {
16915
17718
  d.log(`[${windowId.slice(0, 8)}] \u6CA1\u7B49\u5230\u9996\u53E5\uFF0C\u8D77\u4E00\u4E2A\u7A7A\u7684 ${tool}`);
16916
17719
  }
16917
17720
  const answerWindowId = tool === "workbuddy" || tool === "codex" ? randomBytes8(16).toString("hex") : "";
16918
- const sid = REGISTER_TOOLS.has(tool) ? (d.newSessionId ?? randomUUID2)() : "";
17721
+ const sid = REGISTER_TOOLS.has(tool) ? (d.newSessionId ?? randomUUID3)() : "";
16919
17722
  const env = tool === "qoder_cn" ? { ...promptEnv(tool, d.env), ...real2.env ?? {} } : promptEnv(tool, d.env, process.pid, answerWindowId);
16920
17723
  const args = [...launchArgs(tool, d.platform, void 0, sid), ...promptArgs(text)];
16921
17724
  const early = sid ? startEarlyRegister(tool, sid, cwd, text, d) : null;
@@ -17073,6 +17876,7 @@ async function applyAnswer(req, io, trust) {
17073
17876
  const id = req.instructionId;
17074
17877
  if (req.expireAt > 0 && Date.now() > req.expireAt) return { instructionId: id, status: "rejected", reason: "expired" };
17075
17878
  const a = req.answer;
17879
+ if (a.kind === "qoder-read" || a.kind === "qoder-key") return { instructionId: id, status: "rejected", reason: "unsupported" };
17076
17880
  if ((a.kind === "trust" || a.kind === "probe" && a.trust) && !trust) return { instructionId: id, status: "rejected", reason: "unsupported" };
17077
17881
  if (a.kind === "probe" && a.login) {
17078
17882
  if (!io.loginOnScreen) return { instructionId: id, status: "rejected", reason: "unsupported" };
@@ -17310,8 +18114,8 @@ async function launchTranscriptReady(tool, sessionId) {
17310
18114
  return false;
17311
18115
  }
17312
18116
  }
17313
- var defaultDeps = { qoder: (j) => syncQoderApp(j), workbuddy: (j) => registerWorkbuddySession(j) };
17314
- async function launchRegister(tool, sessionId, cwd, firstLine, final, d = defaultDeps) {
18117
+ var defaultDeps2 = { qoder: (j) => syncQoderApp(j), workbuddy: (j) => registerWorkbuddySession(j) };
18118
+ async function launchRegister(tool, sessionId, cwd, firstLine, final, d = defaultDeps2) {
17315
18119
  if (tool === "workbuddy") return d.workbuddy({ sessionId, cwd, fallbackTitle: firstLine ?? "" });
17316
18120
  if (tool !== "qoder_cn") return "skip tool";
17317
18121
  const r = await d.qoder({ tool, kind: "register", sessionId, cwd, fallbackTitle: firstLine ?? "" });
@@ -17339,7 +18143,7 @@ function childEnv(src, windowId, takeover = true) {
17339
18143
  }
17340
18144
  function wrapLog(line) {
17341
18145
  try {
17342
- const p = join31(deskbuddyHome(), "wrap.log");
18146
+ const p = join32(deskbuddyHome(), "wrap.log");
17343
18147
  mkdirSync21(deskbuddyHome(), { recursive: true });
17344
18148
  try {
17345
18149
  if (statSync16(p).size > MAX_LOG_BYTES3) truncateSync3(p, 0);
@@ -17643,17 +18447,29 @@ function runAttached(tool, p, term0, windowId, deps2, launched = false, takeover
17643
18447
  }
17644
18448
  };
17645
18449
  let holdDepth = 0;
17646
- const holdKeys = (on) => {
18450
+ let holdDrop;
18451
+ const holdKeys = (on, drop) => {
17647
18452
  if (on) {
17648
18453
  holdDepth++;
17649
18454
  held = held ?? [];
17650
18455
  return;
17651
18456
  }
18457
+ if (drop) holdDrop = drop;
17652
18458
  holdDepth = Math.max(0, holdDepth - 1);
17653
18459
  if (holdDepth > 0) return;
17654
18460
  const h = held ?? [];
18461
+ const d = holdDrop;
17655
18462
  held = null;
17656
- for (const s2 of h) toPty(s2);
18463
+ holdDrop = void 0;
18464
+ for (const s2 of h) if (!d || !d(s2)) toPty(s2);
18465
+ };
18466
+ const qoderBoxes = takeover && tool === "qoder_cn" ? new QoderBoxTracker() : null;
18467
+ let boxTimer = null;
18468
+ const trackBoxes = () => {
18469
+ try {
18470
+ qoderBoxes?.update(term && !finished ? screenLines(term) : null);
18471
+ } catch {
18472
+ }
17657
18473
  };
17658
18474
  const injector = new Injector({
17659
18475
  now: () => Date.now(),
@@ -17681,6 +18497,13 @@ function runAttached(tool, p, term0, windowId, deps2, launched = false, takeover
17681
18497
  }
17682
18498
  }
17683
18499
  injector.noteOutput();
18500
+ if (qoderBoxes && !boxTimer) {
18501
+ boxTimer = setTimeout(() => {
18502
+ boxTimer = null;
18503
+ trackBoxes();
18504
+ }, 30);
18505
+ boxTimer.unref?.();
18506
+ }
17684
18507
  });
17685
18508
  const onStdin = (b) => {
17686
18509
  const s2 = typeof b === "string" ? b : decoder.write(b);
@@ -17762,10 +18585,32 @@ function runAttached(tool, p, term0, windowId, deps2, launched = false, takeover
17762
18585
  }
17763
18586
  },
17764
18587
  deps2.platform,
17765
- // 设备答复只收信任框(授权框走 hook 的 PermissionRequest,不经这里);用户自己开的窗口一律不收
18588
+ // 设备答复收信任框,以及 Qoder CN 授权框的读屏 / 写键(spec 2026-10-09 qodercn-tui-screen-options);
18589
+ // 其余授权框走 hook 的 PermissionRequest,不经这里
17766
18590
  (req, reply) => {
17767
18591
  reply({ instructionId: req.instructionId, status: "ack" });
17768
18592
  const a = req.answer;
18593
+ if (a.kind === "qoder-read" || a.kind === "qoder-key") {
18594
+ if (!qoderBoxes) {
18595
+ reply({ instructionId: req.instructionId, status: "rejected", reason: "unsupported" });
18596
+ return;
18597
+ }
18598
+ const io = {
18599
+ screen: () => term && !finished ? screenLines(term) : null,
18600
+ write: toPty,
18601
+ sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
18602
+ hold: holdKeys,
18603
+ track: qoderBoxes
18604
+ };
18605
+ const run = () => applyQoderAnswer(req, io).then((r) => {
18606
+ const out = r ?? { instructionId: req.instructionId, status: "rejected", reason: "unsupported" };
18607
+ if (a.kind === "qoder-key") deps2.log(`[${windowId.slice(0, 8)}] \u8BBE\u5907\u6388\u6743\u9009\u9879\u7B2C ${a.n} \u9879\u5199\u952E\uFF1A${out.status}${out.reason ? " " + out.reason : ""}`);
18608
+ reply(out);
18609
+ });
18610
+ if (a.kind === "qoder-read") void run().catch(() => reply({ instructionId: req.instructionId, status: "rejected", reason: "no_screen" }));
18611
+ else answerChain = answerChain.then(run).catch(() => reply({ instructionId: req.instructionId, status: "failed", mode: "window", reason: "partial" }));
18612
+ return;
18613
+ }
17769
18614
  if (trust && (a.kind === "trust" || a.kind === "probe" && a.trust)) trust.noteContact(a.prompt ?? "trust");
17770
18615
  if (!trustCtx || !(a.kind === "trust" || a.kind === "probe" && a.trust)) {
17771
18616
  reply({ instructionId: req.instructionId, status: "rejected", reason: "unsupported" });
@@ -17799,6 +18644,7 @@ function runAttached(tool, p, term0, windowId, deps2, launched = false, takeover
17799
18644
  if (takeover) timer = setInterval(() => {
17800
18645
  injector.tick().catch(() => {
17801
18646
  });
18647
+ trackBoxes();
17802
18648
  try {
17803
18649
  withdraw?.tick();
17804
18650
  } catch {