handmux 0.22.0 → 0.23.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 (42) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/bin/handmux.js +29 -28
  4. package/hooks/handmux-notify.sh +2 -4
  5. package/hooks/handmux-write.cjs +2 -24
  6. package/package.json +4 -4
  7. package/public/assets/index-C3N7OkyP.css +32 -0
  8. package/public/assets/index-D1DWcC5u.js +337 -0
  9. package/public/index.html +2 -2
  10. package/src/agents/claude.js +2 -0
  11. package/src/agents/codex.js +60 -13
  12. package/src/agents/index.js +2 -3
  13. package/src/browser/manager.js +37 -11
  14. package/src/browser/publicProxy.js +23 -1
  15. package/src/browser/routes.js +9 -0
  16. package/src/browser/siteVersion.js +124 -0
  17. package/src/claudeEvents.js +70 -27
  18. package/src/cli/codexManaged.js +77 -0
  19. package/src/cli/hookScaffold.js +8 -10
  20. package/src/cli/i18n/en.js +5 -8
  21. package/src/cli/i18n/zh.js +5 -8
  22. package/src/cli/legacyCodexHooks.js +49 -0
  23. package/src/codexAppServer.js +1620 -0
  24. package/src/codexDiff.js +108 -0
  25. package/src/codexTranscriptParse.js +413 -0
  26. package/{hooks/handmux-codex-usage.cjs → src/codexUsageSnapshot.cjs} +3 -15
  27. package/src/httpApi.js +4 -1
  28. package/src/orphans.js +3 -1
  29. package/src/routes/codex.js +473 -0
  30. package/src/routes/sessions.js +7 -2
  31. package/src/routes/system.js +9 -22
  32. package/src/routes/transcript.js +75 -19
  33. package/src/server.js +21 -13
  34. package/src/tmux/commands.js +11 -3
  35. package/src/transcriptReader.js +5 -5
  36. package/src/usage.js +4 -5
  37. package/src/workspace/agentRunner.js +4 -1
  38. package/src/workspace/capture.js +54 -2
  39. package/src/workspace/checkpointer.js +6 -2
  40. package/public/assets/index-CRAlQZDz.css +0 -32
  41. package/public/assets/index-CzlitgRX.js +0 -335
  42. package/src/cli/codexHooks.js +0 -123
package/README.md CHANGED
@@ -68,14 +68,14 @@ handmux start --tunnel cloudflare # instant public URL (cloudflared auto-insta
68
68
  ## Features
69
69
 
70
70
  - **Claude Code / Codex, deeply** — an inbox status ledger, thumb-approve permissions & plans, and stable host-wide usage bars shared by every connected device.
71
- - **Chat view (experimental)** — turn a Claude session into a mobile conversation with Markdown bubbles, colored tool diffs, and tap-to-answer cards; enable it in Settings and switch back to the terminal at any time.
71
+ - **Chat view** — drive Codex CLI through App Server with streaming Markdown bubbles, tool cards, persistent task goals and live status; long replies hold at their beginning for reading, with an explicit jump back to the latest output. Existing panes can switch in place, and messages sent during a turn form an editable server queue. Claude Code chat is an independently enabled experimental option.
72
72
  - **Real-time terminal + desktop keyboard** — stream the same tmux pane on phone and desktop with scrollback, weak-network fallback, direct physical-keyboard input, and native copy/paste.
73
73
  - **Command & chat modes** — switch between direct terminal input and natural-language agent chat, with a customizable quick bar that stays in sync across devices.
74
74
  - **Client reload after updates** — once the server update has finished, use Settings → Reload app to load the new client without quitting and reopening the home-screen app.
75
75
  - **Script push** — notify your phone from any script or CI step with `handmux push`; target all devices, a named session, or a specific device.
76
76
  - **Workspace recovery** — handmux keeps the metadata needed to restore your latest tmux workspace after a computer or tmux-server restart, without replacing existing sessions.
77
77
  - **Git viewer** — changes / commit history / any branch / full-screen colored diff, multi-repo tabs, read-only, never touches your tree.
78
- - **Web Preview for URLs and static folders** — preview URLs by phone or computer proxy, or open an isolated static folder, with phone/desktop layouts and zoom; it embeds pages rather than replacing a full browser.
78
+ - **Web Preview for URLs and static folders** — preview URLs by phone or computer proxy, or open an isolated static folder, with narrow/wide page widths and zoom; computer proxy can also request a mobile or desktop site version. It embeds pages rather than replacing a full browser.
79
79
  - **Docs** — tap a path in the terminal to open it; Markdown rendered, font zoom, sentence-by-sentence read-aloud.
80
80
  - **Mobile select & copy** — long-press to select in the terminal, drag iOS-style handles to fine-tune, copy the selection / a whole line / a whole paragraph.
81
81
  - **Files both ways** — multi-select upload from the chat box, download, share in, copy any absolute path.
package/README.zh-CN.md CHANGED
@@ -68,14 +68,14 @@ handmux start --tunnel cloudflare # 即时公网地址(自动装 cloudflared)
68
68
  ## 功能一览
69
69
 
70
70
  - **Claude Code / Codex 深度**——收件箱状态台账、拇指批授权批计划,以及所有设备共享的本机最新 agent 用量。
71
- - **对话视图(实验性)**——用 Markdown 气泡、彩色工具 diff 和点按回答卡片把 Claude 会话变成移动端对话,可在设置中开启并随时切回终端。
71
+ - **对话视图**——通过 App Server 用流式 Markdown 气泡、工具卡、持久任务目标和实时状态操作 Codex CLI;长回答会稳定在开头供阅读,用户可随时回到最新内容;支持现有 pane 原位接入和回复期间可编辑的服务端消息队列。Claude Code 对话是独立开启的实验性功能。
72
72
  - **实时终端 + 电脑键盘**——在手机和电脑上实时操控同一个 tmux pane,支持历史滚动、弱网回退、物理键盘直输和原生复制粘贴。
73
73
  - **命令 / 聊天双模式**——在终端直输与自然语言对话间切换,并用跨设备同步的快捷栏快速操作。
74
74
  - **更新后重新加载客户端**——服务端更新完成后,在设置里点「重新加载应用」即可载入新版客户端,无需退出主屏应用再重新打开。
75
75
  - **脚本推送**——用 `handmux push` 从脚本或 CI 步骤推消息到手机,可指定全部设备、某个会话或某台设备。
76
76
  - **工作区恢复**——handmux 静默保存必要元数据,在电脑或 tmux server 重启后恢复最新工作区,绝不替换现有会话。
77
77
  - **Git 查看器**——改动 / 提交历史 / 任意分支 / 全屏彩色 diff,多仓库分页,只读不动工作区。
78
- - **网页预览器**——用手机直连或电脑代理预览网址,也可隔离打开静态目录,支持手机 / 电脑视图和缩放;它只嵌入网页,不替代完整浏览器。
78
+ - **网页预览器**——用手机直连或电脑代理预览网址,也可隔离打开静态目录,支持窄屏 / 宽屏和缩放;电脑代理还可向网站请求手机版或电脑版。它只嵌入网页,不替代完整浏览器。
79
79
  - **文档**——终端里点路径即开;Markdown 排版、字号缩放、逐句高亮朗读。
80
80
  - **选中 · 拷贝**——终端里长按选中,拖 iOS 式手柄精调,一键拷贝选区 / 整行 / 整段。
81
81
  - **文件双向传**——聊天框多选上传、下载、系统分享进来、复制绝对路径。
package/bin/handmux.js CHANGED
@@ -34,7 +34,7 @@ import { scanSupervisorPids, terminateSupervisorPids } from '../src/cli/supervis
34
34
  import { runSetup } from '../src/cli/setupWizard.js';
35
35
  import { commitShortcuts, reportShortcutCommit, runShortcutEditor } from '../src/cli/shortcutEditor.js';
36
36
  import { hooksStatus, installHooks, uninstallHooks } from '../src/cli/claudeHooks.js';
37
- import { codexHooksStatus, installCodexHooks, uninstallCodexHooks } from '../src/cli/codexHooks.js';
37
+ import { removeLegacyCodexHooks } from '../src/cli/legacyCodexHooks.js';
38
38
  import { statusLineStatus, installStatusLine, uninstallStatusLine, composeHint, refreshStatusLineScript } from '../src/cli/statusLine.js';
39
39
  import { claudeUsagePath } from '../src/usage.js';
40
40
  import { probe } from '../src/cli/probe.js';
@@ -42,6 +42,7 @@ import { notifyUpdate, runUpdateCheck, isBrewInstall, PKG_NAME } from '../src/cl
42
42
  import { t, initLocale, setLocale } from '../src/cli/i18n/index.js';
43
43
  import { runPush } from '../src/cli/pushCmd.js';
44
44
  import { runWorkspaceCommand } from '../src/cli/workspaceCmd.js';
45
+ import { runManagedCodex } from '../src/cli/codexManaged.js';
45
46
 
46
47
  const HOME = homedir();
47
48
  const SELF = fileURLToPath(import.meta.url);
@@ -146,6 +147,7 @@ async function main() {
146
147
  case 'status': await status(); process.exit(process.exitCode || 0);
147
148
  case 'logs': return logs();
148
149
  case 'push': process.exitCode = await pushCmd(); return;
150
+ case 'codex': return codexCmd();
149
151
  case 'restore': process.exitCode = await runWorkspaceCommand({ flags, positionals, unknownShortFlags, home: HOME }); return;
150
152
  case 'config': return configCmd();
151
153
  case 'setup': return setupCmd();
@@ -180,6 +182,15 @@ function version() {
180
182
  console.log(currentVersion());
181
183
  }
182
184
 
185
+ async function codexCmd() {
186
+ removeLegacyCodexHooks(HOME);
187
+ try { process.exitCode = await runManagedCodex(process.argv.slice(3), { home: HOME }); }
188
+ catch (error) {
189
+ console.error(`[handmux] ${error?.message || error}`);
190
+ process.exitCode = 1;
191
+ }
192
+ }
193
+
183
194
  function currentVersion() { return requireOpt('../package.json').version; }
184
195
 
185
196
  // `handmux update` (alias `upgrade`) — run the plain global install for the user. We don't self-patch or
@@ -471,6 +482,7 @@ function supervisorArgs(cfg) {
471
482
  }
472
483
 
473
484
  async function setupCmd() {
485
+ removeLegacyCodexHooks(HOME);
474
486
  const target = flags.config ? path.resolve(flags.config) : configPath(HOME);
475
487
  // Is an instance already up? Then the run-action reads "Save & restart" and applying means a real restart
476
488
  // (a running supervisor won't pick up the new config on its own). Captured before the interactive setup so
@@ -480,11 +492,10 @@ async function setupCmd() {
480
492
  const res = await runSetup({ home: HOME, target, running });
481
493
  if (!res) { process.exit(2); }
482
494
  const { cfg, start: doStart } = res; // hub's "save & start/restart" carries the intent — no separate confirm
483
- // Offer to enable the inbox hooks when an agent is present but not yet wired (Claude 'absent', or Codex
484
- // 'absent'). installAgentHooks() then wires every present agent (idempotent for any already installed).
485
- const offerHooks = hooksStatus(HOME) === 'absent' || codexHooksStatus(HOME) === 'absent';
495
+ // Codex uses App Server and needs no hooks. Offer this only when Claude Code is present but not wired.
496
+ const offerHooks = hooksStatus(HOME) === 'absent';
486
497
  if (offerHooks && await confirm(t('hooks.confirmEnable'))) {
487
- installAgentHooks();
498
+ installClaudeHooks();
488
499
  }
489
500
  await maybeOfferStatusLine();
490
501
  if (doStart) {
@@ -543,41 +554,31 @@ async function maybeOfferStatusLine() {
543
554
  }
544
555
  }
545
556
 
546
- // Install the inbox hooks for every coding agent present on this host (Claude Code, Codex the state file
547
- // is shared, entries are agent-tagged). Each is opt-in by the mere presence of its config dir. Prints a
548
- // per-agent line and returns how many were wired, so callers can gate the "reload" hint. Codex's single
549
- // `notify` slot may already hold the user's OWN program — we never clobber it, we warn.
550
- function installAgentHooks() {
551
- let installed = 0;
552
- if (hooksStatus(HOME) !== 'no-claude') {
553
- installHooks(HOME, { srcDir: HOOKS_SRC, stateFile: claudeStatePath(HOME) });
554
- console.log(t('hooks.installedClaude'));
555
- installed++;
556
- }
557
- if (codexHooksStatus(HOME) !== 'no-codex') {
558
- installCodexHooks(HOME, { srcDir: HOOKS_SRC, stateFile: claudeStatePath(HOME) });
559
- console.log(t('hooks.installedCodex'));
560
- installed++;
561
- }
562
- return installed;
557
+ // Install Claude Code lifecycle hooks for inbox state and push. Codex never enters this path.
558
+ function installClaudeHooks() {
559
+ if (hooksStatus(HOME) === 'no-claude') return false;
560
+ installHooks(HOME, { srcDir: HOOKS_SRC, stateFile: claudeStatePath(HOME) });
561
+ console.log(t('hooks.installedClaude'));
562
+ return true;
563
563
  }
564
564
 
565
- // `handmux hooks install|uninstall` opt-in wiring of the coding-agent lifecycle hooks that drive the
566
- // inbox/push. Never creates ~/.claude or ~/.codex; if neither agent is present we say so and exit 0.
565
+ // `handmux hooks install|uninstall` controls Claude Code lifecycle hooks. Codex is App Server-only;
566
+ // uninstall also removes the exact Handmux Codex hook block left by older releases.
567
567
  async function hooksCmd() {
568
568
  const sub = process.argv[3];
569
569
  if (sub === 'install') {
570
- if (hooksStatus(HOME) === 'no-claude' && codexHooksStatus(HOME) === 'no-codex') {
571
- console.log(t('hooks.noAgents'));
570
+ removeLegacyCodexHooks(HOME);
571
+ if (hooksStatus(HOME) === 'no-claude') {
572
+ console.log(t('hooks.noClaude'));
572
573
  return;
573
574
  }
574
- if (installAgentHooks() > 0) console.log(t('hooks.installedHint'));
575
+ if (installClaudeHooks()) console.log(t('hooks.installedHint'));
575
576
  await maybeOfferStatusLine();
576
577
  return;
577
578
  }
578
579
  if (sub === 'uninstall') {
579
580
  uninstallHooks(HOME);
580
- uninstallCodexHooks(HOME);
581
+ removeLegacyCodexHooks(HOME);
581
582
  uninstallStatusLine(HOME);
582
583
  console.log(t('hooks.removed'));
583
584
  return;
@@ -1,9 +1,7 @@
1
1
  #!/bin/sh
2
2
  # handmux 上报 hook. $1 = stop | notify | prompt | end | resume | permreq | compacting | compact | stopfail.
3
3
  # (compacting/compact/stopfail 是版本门控事件:PreCompact/PostCompact/StopFailure,只在够新的 Claude 上注册。)
4
- # $2 = agent id(留空=claude;
5
- # Codex 传 'codex')—— Claude 与 Codex 的 hook payload 字段一致(stdin JSON),所以共用这一个脚本。
6
- # stdin = agent 原始 payload(JSON).
4
+ # stdin = Claude Code 原始 payload(JSON).
7
5
  # (resume = PostToolUse on AskUserQuestion/ExitPlanMode:答完选项/批准计划 → 状态翻回进行中、带所选项。)
8
6
  # (permreq = PermissionRequest:真实弹框一出现就发、带 tool_name → 比 permission_prompt 早亮「需要你」。)
9
7
  # 只做一件事:把本次事件写进一个本地 JSON 状态文件(键=tmux pane,值=该 pane 最新事件)。不联网、
@@ -20,5 +18,5 @@ TS=$(perl -MTime::HiRes -e 'printf "%.0f", Time::HiRes::time()*1000' 2>/dev/null
20
18
  HOST=$(hostname 2>/dev/null || printf '')
21
19
  # payload 经 stdin 原样流给 node(不在 shell 里转义,避免坏数据);pane 含 '%' 直接进 JSON 字段,
22
20
  # 不再进 URL → 彻底告别旧的 "%110 被 url-decode 丢弃" 那类坑。
23
- node "$(dirname "$0")/handmux-write.cjs" "$FILE" "$PANE" "$1" "$TS" "$HOST" "$2" 2>/dev/null || true
21
+ node "$(dirname "$0")/handmux-write.cjs" "$FILE" "$PANE" "$1" "$TS" "$HOST" 2>/dev/null || true
24
22
  exit 0
@@ -14,9 +14,8 @@
14
14
  // or corrupt the file. Best-effort throughout and silent — the hook is fire-and-forget and must never
15
15
  // fail Claude (the shell wrapper swallows errors and always exits 0).
16
16
  const fs = require('node:fs');
17
- const path = require('node:path');
18
17
 
19
- const [, , file, pane, src, ts, host = '', agent = ''] = process.argv;
18
+ const [, , file, pane, src, ts, host = ''] = process.argv;
20
19
  if (!file || !pane || !src) process.exit(0);
21
20
 
22
21
  let payload = {};
@@ -58,20 +57,8 @@ function update() {
58
57
  const curSid = obj[pane] && obj[pane].payload && obj[pane].payload.session_id;
59
58
  const endSid = payload && payload.session_id;
60
59
  if (!curSid || !endSid || curSid === endSid) delete obj[pane];
61
- } else if (src === 'resume' && agent === 'codex') {
62
- // Codex fires PostToolUse on EVERY tool call, so its resume exists purely to un-stick a pane from 需要你
63
- // back to 进行中 after the user approved a PermissionRequest. Apply it ONLY as that transition — a mid-
64
- // turn tool call (pane already 进行中 / 已完成) is a no-op, so we don't rewrite the entry on every
65
- // command (the load Claude's matcher avoids). Claude's resume — no agent arg — is unaffected.
66
- const prev = obj[pane];
67
- const prevPerm = prev && (prev.src === 'permreq'
68
- || (prev.src === 'notify' && (prev.payload || {}).notification_type === 'permission_prompt'));
69
- if (!prevPerm) { return; }
70
- obj[pane] = { ts: Number(ts) || 0, src, host, payload, agent };
71
60
  } else {
72
- // agent tag lets the server dispatch classify + liveness per agent (Codex passes 'codex'); omitted for
73
- // Claude so legacy entries stay byte-identical and default to claude server-side.
74
- obj[pane] = { ts: Number(ts) || 0, src, host, payload, ...(agent ? { agent } : {}) };
61
+ obj[pane] = { ts: Number(ts) || 0, src, host, payload };
75
62
  }
76
63
  const tmp = `${file}.${process.pid}.tmp`;
77
64
  fs.writeFileSync(tmp, JSON.stringify(obj));
@@ -89,12 +76,3 @@ for (let i = 0; i < 60 && !held; i++) { // ~0.9s budget, t
89
76
  }
90
77
  try { update(); } catch { /* best effort */ }
91
78
  if (held) { try { fs.unlinkSync(lock); } catch { /* ignore */ } }
92
-
93
- // Codex hook payloads include the exact rollout transcript_path. Capture its latest token_count into a
94
- // machine-wide snapshot after the inbox update; failures are isolated so usage can never break the hook.
95
- if (agent === 'codex' && typeof payload.transcript_path === 'string') {
96
- try {
97
- const { captureTranscript } = require('./handmux-codex-usage.cjs');
98
- captureTranscript(payload.transcript_path, path.join(path.dirname(file), 'codex-usage.json'));
99
- } catch { /* best effort */ }
100
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handmux",
3
- "version": "0.22.0",
3
+ "version": "0.23.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",
@@ -62,13 +62,13 @@
62
62
  "vitest": "^2.0.0"
63
63
  },
64
64
  "whatsNew": [
65
+ {"version":"0.23.0","date":"2026-08-09","zh":"Codex 托管对话正式上线","en":"Managed Codex chat is here"},
66
+ {"version":"0.22.1","date":"2026-08-04","zh":"网页宽度与网站版本分开控制","en":"Separate page width and site-version controls"},
65
67
  {"version":"0.22.0","date":"2026-08-03","zh":"全屏设置 · 窗口与分屏管理更清晰","en":"Full-screen Settings · clearer window and pane management"},
66
68
  {"version":"0.21.0","date":"2026-08-03","zh":"网页预览:手机直连、电脑代理与静态目录","en":"Web preview: direct, computer proxy, and static folders"},
67
69
  {"version":"0.20.3","date":"2026-08-01","zh":"实时终端显示更稳定流畅","en":"More stable, smoother live terminals"},
68
70
  {"version":"0.20.2","date":"2026-07-27","zh":"实时终端长时间运行更流畅","en":"Smoother long-running live terminals"},
69
71
  {"version":"0.20.1","date":"2026-07-26","zh":"聊天发送修复 · 电脑键盘不中断","en":"Reliable chat sending · uninterrupted desktop input"},
70
- {"version":"0.20.0","date":"2026-07-26","zh":"终端实时推送 · 弱网自动回退","en":"Live terminal pushing · automatic network fallback"},
71
- {"version":"0.19.0","date":"2026-07-25","zh":"电脑物理键盘直输 · Window 切换更流畅","en":"Desktop keyboard input · faster window switching"},
72
- {"version":"0.18.0","date":"2026-07-22","zh":"Tmux 工作区恢复 · 快捷栏自由定制","en":"Tmux workspace recovery · customizable shortcuts"}
72
+ {"version":"0.20.0","date":"2026-07-26","zh":"终端实时推送 · 弱网自动回退","en":"Live terminal pushing · automatic network fallback"}
73
73
  ]
74
74
  }