handmux 0.22.1 → 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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/handmux.js +29 -28
- package/hooks/handmux-notify.sh +2 -4
- package/hooks/handmux-write.cjs +2 -24
- package/package.json +3 -3
- package/public/assets/index-C3N7OkyP.css +32 -0
- package/public/assets/index-D1DWcC5u.js +337 -0
- package/public/index.html +2 -2
- package/src/agents/claude.js +2 -0
- package/src/agents/codex.js +60 -13
- package/src/agents/index.js +2 -3
- package/src/claudeEvents.js +70 -27
- package/src/cli/codexManaged.js +77 -0
- package/src/cli/hookScaffold.js +8 -10
- package/src/cli/i18n/en.js +5 -8
- package/src/cli/i18n/zh.js +5 -8
- package/src/cli/legacyCodexHooks.js +49 -0
- package/src/codexAppServer.js +1620 -0
- package/src/codexDiff.js +108 -0
- package/src/codexTranscriptParse.js +413 -0
- package/{hooks/handmux-codex-usage.cjs → src/codexUsageSnapshot.cjs} +3 -15
- package/src/httpApi.js +4 -1
- package/src/orphans.js +3 -1
- package/src/routes/codex.js +473 -0
- package/src/routes/sessions.js +7 -2
- package/src/routes/system.js +9 -22
- package/src/routes/transcript.js +75 -19
- package/src/server.js +21 -13
- package/src/tmux/commands.js +11 -3
- package/src/transcriptReader.js +5 -5
- package/src/usage.js +4 -5
- package/src/workspace/agentRunner.js +4 -1
- package/src/workspace/capture.js +54 -2
- package/src/workspace/checkpointer.js +6 -2
- package/public/assets/index-ClOByNCn.js +0 -335
- package/public/assets/index-rjrtUwoJ.css +0 -32
- package/src/cli/codexHooks.js +0 -123
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ 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
|
|
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.
|
package/README.zh-CN.md
CHANGED
|
@@ -68,7 +68,7 @@ handmux start --tunnel cloudflare # 即时公网地址(自动装 cloudflared)
|
|
|
68
68
|
## 功能一览
|
|
69
69
|
|
|
70
70
|
- **Claude Code / Codex 深度**——收件箱状态台账、拇指批授权批计划,以及所有设备共享的本机最新 agent 用量。
|
|
71
|
-
-
|
|
71
|
+
- **对话视图**——通过 App Server 用流式 Markdown 气泡、工具卡、持久任务目标和实时状态操作 Codex CLI;长回答会稳定在开头供阅读,用户可随时回到最新内容;支持现有 pane 原位接入和回复期间可编辑的服务端消息队列。Claude Code 对话是独立开启的实验性功能。
|
|
72
72
|
- **实时终端 + 电脑键盘**——在手机和电脑上实时操控同一个 tmux pane,支持历史滚动、弱网回退、物理键盘直输和原生复制粘贴。
|
|
73
73
|
- **命令 / 聊天双模式**——在终端直输与自然语言对话间切换,并用跨设备同步的快捷栏快速操作。
|
|
74
74
|
- **更新后重新加载客户端**——服务端更新完成后,在设置里点「重新加载应用」即可载入新版客户端,无需退出主屏应用再重新打开。
|
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 {
|
|
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
|
-
//
|
|
484
|
-
|
|
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
|
-
|
|
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
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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`
|
|
566
|
-
//
|
|
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
|
-
|
|
571
|
-
|
|
570
|
+
removeLegacyCodexHooks(HOME);
|
|
571
|
+
if (hooksStatus(HOME) === 'no-claude') {
|
|
572
|
+
console.log(t('hooks.noClaude'));
|
|
572
573
|
return;
|
|
573
574
|
}
|
|
574
|
-
if (
|
|
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
|
-
|
|
581
|
+
removeLegacyCodexHooks(HOME);
|
|
581
582
|
uninstallStatusLine(HOME);
|
|
582
583
|
console.log(t('hooks.removed'));
|
|
583
584
|
return;
|
package/hooks/handmux-notify.sh
CHANGED
|
@@ -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
|
-
#
|
|
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"
|
|
21
|
+
node "$(dirname "$0")/handmux-write.cjs" "$FILE" "$PANE" "$1" "$TS" "$HOST" 2>/dev/null || true
|
|
24
22
|
exit 0
|
package/hooks/handmux-write.cjs
CHANGED
|
@@ -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 = ''
|
|
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
|
-
|
|
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.
|
|
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"},
|
|
65
66
|
{"version":"0.22.1","date":"2026-08-04","zh":"网页宽度与网站版本分开控制","en":"Separate page width and site-version controls"},
|
|
66
67
|
{"version":"0.22.0","date":"2026-08-03","zh":"全屏设置 · 窗口与分屏管理更清晰","en":"Full-screen Settings · clearer window and pane management"},
|
|
67
68
|
{"version":"0.21.0","date":"2026-08-03","zh":"网页预览:手机直连、电脑代理与静态目录","en":"Web preview: direct, computer proxy, and static folders"},
|
|
68
69
|
{"version":"0.20.3","date":"2026-08-01","zh":"实时终端显示更稳定流畅","en":"More stable, smoother live terminals"},
|
|
69
70
|
{"version":"0.20.2","date":"2026-07-27","zh":"实时终端长时间运行更流畅","en":"Smoother long-running live terminals"},
|
|
70
71
|
{"version":"0.20.1","date":"2026-07-26","zh":"聊天发送修复 · 电脑键盘不中断","en":"Reliable chat sending · uninterrupted desktop input"},
|
|
71
|
-
{"version":"0.20.0","date":"2026-07-26","zh":"终端实时推送 · 弱网自动回退","en":"Live terminal pushing · automatic network fallback"}
|
|
72
|
-
{"version":"0.19.0","date":"2026-07-25","zh":"电脑物理键盘直输 · Window 切换更流畅","en":"Desktop keyboard input · faster window switching"}
|
|
72
|
+
{"version":"0.20.0","date":"2026-07-26","zh":"终端实时推送 · 弱网自动回退","en":"Live terminal pushing · automatic network fallback"}
|
|
73
73
|
]
|
|
74
74
|
}
|