handmux 0.29.2 → 0.31.0

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.
Files changed (116) hide show
  1. package/dist/connectors/claude/index.js +33 -562
  2. package/dist/connectors/codebuddy/index.js +49 -0
  3. package/dist/connectors/hookBridge.js +560 -0
  4. package/dist/hooks/handmux-codebuddy-notify.sh +116 -0
  5. package/dist/hooks/handmux-notify.sh +2 -0
  6. package/dist/hooks/handmux-write.cjs +72 -5
  7. package/dist/package.json +13 -13
  8. package/dist/public/assets/_basePickBy-BzC0P9Wg.js +1 -0
  9. package/dist/public/assets/_baseUniq-DNOfPYVY.js +1 -0
  10. package/dist/public/assets/abnfDiagram-O67JEVCF-BsRDIQEB.js +1 -0
  11. package/dist/public/assets/arc-DLtLfMnX.js +1 -0
  12. package/dist/public/assets/architectureDiagram-NJMV4G6O-DoIXhWqZ.js +36 -0
  13. package/dist/public/assets/blockDiagram-BEXU5L5S-W0HiOcVA.js +140 -0
  14. package/dist/public/assets/c4Diagram-YGBWAQC7-72uAUMvX.js +38 -0
  15. package/dist/public/assets/channel-CzALSnb0.js +1 -0
  16. package/dist/public/assets/chunk-2Q5K7J3B-B9wSRFQ0.js +1 -0
  17. package/dist/public/assets/chunk-3FUC2YCW-ryphp4s0.js +2 -0
  18. package/dist/public/assets/chunk-5VM5RSS4-Cm4KeKby.js +15 -0
  19. package/dist/public/assets/chunk-GTNCS2PH-Du4_i7r3.js +88 -0
  20. package/dist/public/assets/chunk-GWA4HPMP-CL1biVIA.js +1 -0
  21. package/dist/public/assets/chunk-JWPE2WC7-B8eAVfuB.js +1 -0
  22. package/dist/public/assets/chunk-NETBCI7D-D273GzYg.js +1 -0
  23. package/dist/public/assets/chunk-WEXAMYUT-DipTfRbc.js +1 -0
  24. package/dist/public/assets/chunk-XXDRQBXY-D4SqXrQ8.js +1 -0
  25. package/dist/public/assets/classDiagram-v2-NBCMYWYE-PrpnIXcA.js +217 -0
  26. package/dist/public/assets/cose-bilkent-JH36ORCC-BfcYMYTa.js +1 -0
  27. package/dist/public/assets/cynefin-OW5HDTMX-Du659bmz.js +178 -0
  28. package/dist/public/assets/cynefinDiagram-VND7K2PF-DR_2Copy.js +62 -0
  29. package/dist/public/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  30. package/dist/public/assets/dagre-6A5THRUB-COsChaV5.js +4 -0
  31. package/dist/public/assets/defaultLocale-CrowFXzY.js +1 -0
  32. package/dist/public/assets/diagram-22UHCM2B-Cx2T1HvL.js +200 -0
  33. package/dist/public/assets/diagram-3UASUU5V-Cp04LOka.js +24 -0
  34. package/dist/public/assets/diagram-ATOU4E4O-DlqFSshY.js +3 -0
  35. package/dist/public/assets/diagram-CDSNMT55-goq40ctA.js +30 -0
  36. package/dist/public/assets/diagram-MLGK6HIB-Bdzjsy4r.js +24 -0
  37. package/dist/public/assets/diagram-MPIPVDR6-DtU7cM6P.js +41 -0
  38. package/dist/public/assets/ebnfDiagram-ZINNZB2B-XyVCfmED.js +1 -0
  39. package/dist/public/assets/elk-276RUBZZ-aHVIw-2x.js +27 -0
  40. package/dist/public/assets/erDiagram-OPXOYQCR-Bq_PhTIL.js +99 -0
  41. package/dist/public/assets/flowDiagram-KWPJA3E3-WuqnDSiF.js +213 -0
  42. package/dist/public/assets/ganttDiagram-FUAMR5RP-DZ4lXzkC.js +292 -0
  43. package/dist/public/assets/gitGraphDiagram-X574FWY7-t6l07b9g.js +106 -0
  44. package/dist/public/assets/graph-jWsD5Ro-.js +1 -0
  45. package/dist/public/assets/index-DiYff8oR.css +32 -0
  46. package/dist/public/assets/index-YZNK723o.js +203 -0
  47. package/dist/public/assets/infoDiagram-VRGFBTTK-iUGqoOFp.js +2 -0
  48. package/dist/public/assets/init-Gi6I4Gst.js +1 -0
  49. package/dist/public/assets/ishikawaDiagram-OU5B5YK6-B0TmoUOK.js +70 -0
  50. package/dist/public/assets/journeyDiagram-ZHPQQLJL-CQE61MCu.js +139 -0
  51. package/dist/public/assets/kanban-definition-PNTS6WVX-D2MWCUyS.js +89 -0
  52. package/dist/public/assets/katex-C5jXJg4s.js +257 -0
  53. package/dist/public/assets/linear-WKGxVNP1.js +1 -0
  54. package/dist/public/assets/mermaid.core-DPEwUsaj.js +311 -0
  55. package/dist/public/assets/mindmap-definition-NLK3R4M7-DsYEsBsI.js +96 -0
  56. package/dist/public/assets/ordinal-Cboi1Yqb.js +1 -0
  57. package/dist/public/assets/pegDiagram-GJSIUBJH-BS52Fbxt.js +1 -0
  58. package/dist/public/assets/pieDiagram-5QR66LMP-D4o8g7nW.js +39 -0
  59. package/dist/public/assets/quadrantDiagram-O4NWA36T-DnYUxWll.js +7 -0
  60. package/dist/public/assets/railroadDiagram-XR7U4H2S-BKy_7ULV.js +1 -0
  61. package/dist/public/assets/requirementDiagram-PLB6GJNP-Ce5F4u8K.js +84 -0
  62. package/dist/public/assets/sankeyDiagram-IPEJSGJF-CCJDrut_.js +40 -0
  63. package/dist/public/assets/sequenceDiagram-PO4LG4MO-D5E8ISM1.js +169 -0
  64. package/dist/public/assets/sizeCapture-INFHLROL-BW3KmxDN.js +1 -0
  65. package/dist/public/assets/stateDiagram-v2-GCMORJYK-Bw2ZzQOE.js +291 -0
  66. package/dist/public/assets/swimlanes-2SLR337P-CLJ-6Ex_.js +1 -0
  67. package/dist/public/assets/swimlanesDiagram-TC7HE7FX-BD8rQfV7.js +8 -0
  68. package/dist/public/assets/timeline-definition-EJHVYXUP-C_lFJ-4W.js +120 -0
  69. package/dist/public/assets/usecaseDiagram-POWQR4AR-DWXawL2A.js +370 -0
  70. package/dist/public/assets/vennDiagram-UO4OBE2U-D4O5oZal.js +34 -0
  71. package/dist/public/assets/wardleyDiagram-VNRHLVJA-B6HElk__.js +78 -0
  72. package/dist/public/assets/xychartDiagram-PMCCYNJV-C5OAXnJy.js +7 -0
  73. package/dist/public/index.html +2 -2
  74. package/dist/src/agent-runtime/adapter.js +54 -10
  75. package/dist/src/agent-runtime/builtinRuntime.js +67 -6
  76. package/dist/src/agent-runtime/conversation.js +49 -9
  77. package/dist/src/agent-runtime/interaction.js +26 -12
  78. package/dist/src/agent-runtime/interactionLiveHub.js +4 -0
  79. package/dist/src/agent-runtime/runtime.js +83 -27
  80. package/dist/src/agent-runtime/tmuxRuntime.js +36 -6
  81. package/dist/src/agents/claude.js +2 -19
  82. package/dist/src/agents/claudeInteraction.js +2 -132
  83. package/dist/src/agents/claudePaneInput.js +22 -22
  84. package/dist/src/agents/codebuddy.js +100 -0
  85. package/dist/src/agents/codebuddyConversation.js +414 -0
  86. package/dist/src/agents/codebuddyInteraction.js +4 -0
  87. package/dist/src/agents/codebuddyNativeTail.js +206 -0
  88. package/dist/src/agents/codebuddyPaneInput.js +76 -0
  89. package/dist/src/agents/codebuddyTranscriptParse.js +178 -0
  90. package/dist/src/agents/hookEvents.js +143 -0
  91. package/dist/src/agents/hookInteraction.js +178 -0
  92. package/dist/src/agents/index.js +2 -1
  93. package/dist/src/claudeEvents.js +4 -5
  94. package/dist/src/cli/agentIntegration.js +31 -2
  95. package/dist/src/cli/codebuddyHooks.js +286 -0
  96. package/dist/src/cli/hookScaffold.js +25 -12
  97. package/dist/src/cli/i18n/en.js +1 -1
  98. package/dist/src/cli/i18n/zh.js +1 -1
  99. package/dist/src/cli/state.js +4 -0
  100. package/dist/src/cli/supervisor.js +2 -1
  101. package/dist/src/codebuddyEvents.js +151 -0
  102. package/dist/src/docs.js +21 -7
  103. package/dist/src/logDetail.js +27 -0
  104. package/dist/src/paneInput.js +107 -10
  105. package/dist/src/pendingPrompt.js +21 -5
  106. package/dist/src/routes/files.js +4 -1
  107. package/dist/src/routes/system.js +12 -2
  108. package/dist/src/server.js +69 -8
  109. package/dist/src/trimCapture.js +1 -1
  110. package/dist/src/workspace/checkpointer.js +48 -4
  111. package/dist/src/workspace/lock.js +54 -10
  112. package/dist/src/workspace/runtime.js +23 -8
  113. package/dist/src/workspaceProtocol.js +1 -0
  114. package/package.json +4 -4
  115. package/dist/public/assets/index-2ghkmGbT.js +0 -356
  116. package/dist/public/assets/index-BjnnFTLC.css +0 -32
@@ -17,13 +17,19 @@ function defaultProcessAlive(pid) {
17
17
  }
18
18
  }
19
19
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
20
- function ownerLabel(value) {
20
+ /** Who a lock record says is holding it, or null when the record says nothing usable. */
21
+ export function lockOwnerLabel(value) {
21
22
  const owner = asRecord(value);
22
23
  if (!owner)
23
- return 'unknown owner';
24
- const operation = typeof owner.operationId === 'string' && owner.operationId ? owner.operationId : 'unknown operation';
25
- const pid = Number.isInteger(owner.pid) ? owner.pid : 'unknown';
26
- return `${operation} (pid ${pid})`;
24
+ return null;
25
+ const operation = typeof owner.operationId === 'string' && owner.operationId ? owner.operationId : null;
26
+ const pid = Number.isInteger(owner.pid) ? owner.pid : null;
27
+ if (!operation && pid === null)
28
+ return null;
29
+ return `${operation || 'unknown operation'} (pid ${pid ?? 'unknown'})`;
30
+ }
31
+ function ownerLabel(value) {
32
+ return lockOwnerLabel(value) ?? 'unknown owner';
27
33
  }
28
34
  export function createWorkspaceLock({ dir, fs = fsp, pid = process.pid, now = Date.now, isProcessAlive = defaultProcessAlive, wait = delay, timeoutMs = 5_000, retryMs = 50, staleGraceMs = 5_000, randomUUID = crypto.randomUUID, }) {
29
35
  if (typeof dir !== 'string' || !dir)
@@ -46,21 +52,59 @@ export function createWorkspaceLock({ dir, fs = fsp, pid = process.pid, now = Da
46
52
  return { value: null, mtimeMs: stat.mtimeMs };
47
53
  }
48
54
  }
55
+ async function reclaimUnreadableOwner(record) {
56
+ // The record is missing, truncated, or malformed — the shape a holder leaves when it dies between
57
+ // creating the directory and writing its record. Age is the directory's own, which is when it appeared.
58
+ // `rmdir` only removes an EMPTY directory, so this can never take the lock from a holder that has
59
+ // already written its record; an empty directory is by definition nobody's. The worst a contender can
60
+ // do to a holder is hit the microseconds before that holder's own record exists, and that holder then
61
+ // fails loudly instead of silently sharing the lock.
62
+ try {
63
+ await fs.rmdir(dir);
64
+ return true;
65
+ }
66
+ catch (error) {
67
+ if (errorCode(error) === 'ENOENT')
68
+ return true;
69
+ if (errorCode(error) !== 'ENOTEMPTY')
70
+ return false;
71
+ }
72
+ // Not empty after all: a record exists but cannot be read. Move the whole directory aside. The
73
+ // destination comes from the observed mtime, never from the unvalidated record, so no path is built
74
+ // from content we could not verify — and because the directory is not empty, the tombstone it leaves
75
+ // cannot be replaced by a lock taken after this look.
76
+ const staleDir = `${dir}.stale.unreadable-${Math.round(record.mtimeMs)}`;
77
+ try {
78
+ await fs.rename(dir, staleDir);
79
+ }
80
+ catch (error) {
81
+ if (errorCode(error) === 'ENOENT')
82
+ return true;
83
+ return false;
84
+ }
85
+ return true;
86
+ }
49
87
  async function reclaimIfStale(record) {
50
88
  if (!record)
51
89
  return true;
52
90
  const owner = asRecord(record.value);
53
- if (!owner || typeof owner.token !== 'string' || !SAFE_TOKEN.test(owner.token))
54
- return false;
55
- const startedAt = typeof owner.startedAt === 'string' ? Date.parse(owner.startedAt) : Number.NaN;
91
+ const candidate = owner?.token;
92
+ const token = typeof candidate === 'string' && SAFE_TOKEN.test(candidate) ? candidate : null;
93
+ // An unusable record carries no trustworthy clock, so its age comes from the directory itself.
94
+ const startedAt = token && owner && typeof owner.startedAt === 'string' ? Date.parse(owner.startedAt) : Number.NaN;
56
95
  const age = now() - (Number.isFinite(startedAt) ? startedAt : record.mtimeMs);
96
+ // The grace period is what keeps a contender out of the window between a holder's `mkdir` and the
97
+ // record it is about to write.
57
98
  if (age < staleGraceMs)
58
99
  return false;
59
- if (typeof owner.pid === 'number' && Number.isInteger(owner.pid) && await isProcessAlive(owner.pid))
100
+ const pid = owner && typeof owner.pid === 'number' && Number.isInteger(owner.pid) ? owner.pid : null;
101
+ if (pid !== null && await isProcessAlive(pid))
60
102
  return false;
103
+ if (!token)
104
+ return reclaimUnreadableOwner(record);
61
105
  // Every contender that observed this owner uses the SAME destination. The winner leaves the renamed
62
106
  // directory as a tombstone, so a loser cannot later rename a newly-created lock out of the way.
63
- const staleDir = `${dir}.stale.${owner.token}`;
107
+ const staleDir = `${dir}.stale.${token}`;
64
108
  try {
65
109
  await fs.rename(dir, staleDir);
66
110
  }
@@ -7,6 +7,10 @@ import { buildRecoveryMapping, } from './mapping.js';
7
7
  const recordOf = (value) => (value && typeof value === 'object' && !Array.isArray(value)
8
8
  ? value : null);
9
9
  const errorMessage = (error) => error instanceof Error ? error.message : String(error);
10
+ // A restore holds the writer lock for as long as it runs, and that is normal — so a lock somebody else is
11
+ // holding only means the workspace stopped being captured once it has been held far longer than a restore
12
+ // takes. Nothing here steals the lock; it changes what the status reports.
13
+ const WRITER_BLOCK_THRESHOLD_MS = 5 * 60_000;
10
14
  function unwrapCheckpoint(result) {
11
15
  const record = recordOf(result);
12
16
  if (record?.status !== 'ok' || !record.value) {
@@ -192,17 +196,28 @@ export function createWorkspaceRuntime({ store, tmux, lock, checkpointer, now =
192
196
  catch {
193
197
  return { status: 'degraded', lastSuccessfulCaptureAt: null, errorCode: 'live-unavailable' };
194
198
  }
199
+ if (live.status !== 'ok' && live.status !== 'empty') {
200
+ return {
201
+ status: 'degraded',
202
+ lastSuccessfulCaptureAt: null,
203
+ errorCode: live.status === 'corrupt' ? 'live-corrupt' : 'live-unavailable',
204
+ };
205
+ }
206
+ // A healthy live copy is not protection: if the writer lock has been out of reach for long enough, the
207
+ // copy is simply the last one that was ever written, and capture is what has stopped.
208
+ const blocked = checkpointer.blocked?.();
209
+ if (blocked && Number.isFinite(blocked.since) && now() - blocked.since >= WRITER_BLOCK_THRESHOLD_MS) {
210
+ return {
211
+ status: 'degraded',
212
+ lastSuccessfulCaptureAt: live.status === 'ok' ? live.value.capturedAt || null : null,
213
+ errorCode: 'writer-locked',
214
+ blockedBy: blocked.owner,
215
+ };
216
+ }
195
217
  if (live.status === 'ok') {
196
218
  return { status: 'protected', lastSuccessfulCaptureAt: live.value.capturedAt || null, errorCode: null };
197
219
  }
198
- if (live.status === 'empty') {
199
- return { status: 'unprotected', lastSuccessfulCaptureAt: null, errorCode: null };
200
- }
201
- return {
202
- status: 'degraded',
203
- lastSuccessfulCaptureAt: null,
204
- errorCode: live.status === 'corrupt' ? 'live-corrupt' : 'live-unavailable',
205
- };
220
+ return { status: 'unprotected', lastSuccessfulCaptureAt: null, errorCode: null };
206
221
  }
207
222
  async function performRestore(operationId, request, onProgress, onRunning, onTerminal) {
208
223
  let result;
@@ -42,6 +42,7 @@ export function parseWorkspaceProtectionStatus(value) {
42
42
  ...optionalField('status', memberOf(PROTECTION_STATUSES, status.status)),
43
43
  ...optionalField('lastSuccessfulCaptureAt', optionalString(status.lastSuccessfulCaptureAt)),
44
44
  ...optionalField('errorCode', optionalString(status.errorCode)),
45
+ ...optionalField('blockedBy', optionalString(status.blockedBy)),
45
46
  };
46
47
  }
47
48
  export function parseWorkspaceRestoreStart(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handmux",
3
- "version": "0.29.2",
3
+ "version": "0.31.0",
4
4
  "description": "A mobile vibe-coding cockpit — built on tmux: drive your live session, Claude Code / Codex — anything a terminal can run — from your phone.",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -75,13 +75,13 @@
75
75
  "vitest": "^2.0.0"
76
76
  },
77
77
  "whatsNew": [
78
+ {"version":"0.31.0","date":"2026-09-22","zh":"CodeBuddy 对话接入 · Agent 状态更可靠","en":"CodeBuddy chat support · more reliable Agent state"},
79
+ {"version":"0.30.0","date":"2026-09-19","zh":"文档预览全面升级:朗读、目录、查找、图表","en":"A full document reader: read-aloud, outline, search, diagrams"},
78
80
  {"version":"0.29.2","date":"2026-09-18","zh":"修复 Linux 下 Claude 事件上报的兼容问题","en":"Compatibility fix for Claude event reporting on Linux"},
79
81
  {"version":"0.29.1","date":"2026-09-18","zh":"修复 Claude 对话入口丢失 · 长时间运行不再变卡","en":"Fix lost Claude chat entries · no slowdown after long runs"},
80
82
  {"version":"0.29.0","date":"2026-09-16","zh":"可信设备与可信地址保护","en":"Trusted device and address protection"},
81
83
  {"version":"0.28.0","date":"2026-09-11","zh":"Claude 对话更可靠 · 工具图片预览","en":"More reliable Claude chat · tool image previews"},
82
84
  {"version":"0.27.2","date":"2026-09-08","zh":"修复多标签页偶发鉴权弹窗","en":"Fix intermittent authentication prompts across tabs"},
83
- {"version":"0.27.1","date":"2026-09-08","zh":"腾讯语音 · 终端光标修复 · Codex 对话恢复","en":"Tencent voice · terminal cursor fixes · Codex chat recovery"},
84
- {"version":"0.27.0","date":"2026-09-05","zh":"腾讯语音输入 · 终端输入更准确","en":"Tencent voice input · more accurate terminal typing"},
85
- {"version":"0.26.0","date":"2026-09-04","zh":"对话精细复制 · Pi 上下文状态","en":"Precise chat copy · Pi context status"}
85
+ {"version":"0.27.1","date":"2026-09-08","zh":"腾讯语音 · 终端光标修复 · Codex 对话恢复","en":"Tencent voice · terminal cursor fixes · Codex chat recovery"}
86
86
  ]
87
87
  }