handmux 0.25.2 → 0.27.1
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 +20 -93
- package/README.zh-CN.md +20 -93
- package/dist/connectors/pi/index.js +16 -1
- package/dist/package.json +19 -19
- package/dist/public/assets/index-Cdrh2CUp.css +32 -0
- package/dist/public/assets/index-VT8s4ELL.js +356 -0
- package/dist/public/assets/pcm-worklet-NzlBQ6-t.js +1 -0
- package/dist/public/index.html +2 -2
- package/dist/src/agent-runtime/adapter.js +2 -0
- package/dist/src/agent-runtime/builtinRuntime.js +5 -1
- package/dist/src/agent-runtime/conversationActivation.js +136 -4
- package/dist/src/agent-runtime/run.js +23 -2
- package/dist/src/agent-runtime/runtime.js +174 -15
- package/dist/src/agent-runtime/tmuxRuntime.js +3 -7
- package/dist/src/agents/claudeConversation.js +2 -2
- package/dist/src/agents/codex.js +78 -10
- package/dist/src/agents/codexActivationReceipt.js +149 -0
- package/dist/src/agents/codexConversation.js +61 -22
- package/dist/src/agents/codexConversationActivation.js +265 -65
- package/dist/src/agents/codexConversationControls.js +4 -1
- package/dist/src/agents/codexOpenSession.js +164 -0
- package/dist/src/agents/conversationProjectionSafety.js +7 -0
- package/dist/src/agents/nativeInbox.js +24 -56
- package/dist/src/agents/pi.js +1 -0
- package/dist/src/agents/piConversationContext.js +49 -0
- package/dist/src/agents/scanUtils.js +7 -1
- package/dist/src/apiErrors.js +10 -1
- package/dist/src/asr/config.js +29 -0
- package/dist/src/asr/providerRegistry.js +19 -0
- package/dist/src/asr/providers/tencent.js +135 -0
- package/dist/src/asr/providers/xfyun.js +73 -0
- package/dist/src/asr/tencentSentence.js +111 -0
- package/dist/src/asr/tencentSign.js +20 -0
- package/dist/src/asr/verify.js +80 -0
- package/dist/src/cli/codexManaged.js +13 -2
- package/dist/src/cli/i18n/en.js +16 -1
- package/dist/src/cli/i18n/zh.js +16 -1
- package/dist/src/cli/options.js +29 -2
- package/dist/src/cli/piExtension.js +11 -1
- package/dist/src/cli/setupModel.js +49 -5
- package/dist/src/cli/setupWizard.js +149 -29
- package/dist/src/cli/supervisor.js +24 -8
- package/dist/src/cli/supervisorLaunch.js +26 -0
- package/dist/src/codexAppServer.js +41 -4
- package/dist/src/routes/agents.js +51 -14
- package/dist/src/routes/system.js +109 -10
- package/dist/src/server.js +11 -2
- package/dist/src/terminalStream.js +2 -26
- package/dist/src/tmux/commands.js +14 -0
- package/dist/src/tmux/controlProtocol.js +26 -0
- package/dist/src/tmux/paneOutputCapture.js +180 -0
- package/package.json +5 -5
- package/dist/public/assets/index-DH4DnhbV.js +0 -346
- package/dist/public/assets/index-DPBerEuM.css +0 -32
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +0 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { decodeControlData } from './controlProtocol.js';
|
|
3
|
+
const START_TIMEOUT_MS = 5_000;
|
|
4
|
+
const DEFAULT_MAX_BYTES = 256 * 1024;
|
|
5
|
+
const defaultSpawnControl = (command, args, options) => (spawn(command, [...args], options));
|
|
6
|
+
export class TmuxPaneOutputCapture {
|
|
7
|
+
#pane;
|
|
8
|
+
#maxBytes;
|
|
9
|
+
#child;
|
|
10
|
+
#attached;
|
|
11
|
+
#startTimer;
|
|
12
|
+
#resolveAttached;
|
|
13
|
+
#rejectAttached;
|
|
14
|
+
#buffer = Buffer.alloc(0);
|
|
15
|
+
#response;
|
|
16
|
+
#pending = null;
|
|
17
|
+
#chunks = [];
|
|
18
|
+
#bytes = 0;
|
|
19
|
+
#armed = false;
|
|
20
|
+
#overflow = false;
|
|
21
|
+
#failed = false;
|
|
22
|
+
#closed = false;
|
|
23
|
+
#lastError = '';
|
|
24
|
+
constructor({ pane, session, maxBytes = DEFAULT_MAX_BYTES, spawnControl = defaultSpawnControl, }) {
|
|
25
|
+
if (!/^%\d+$/.test(pane) || !/^\$\d+$/.test(session)) {
|
|
26
|
+
throw new TypeError('invalid tmux pane output capture target');
|
|
27
|
+
}
|
|
28
|
+
this.#pane = pane;
|
|
29
|
+
this.#maxBytes = Math.max(1, Math.trunc(maxBytes));
|
|
30
|
+
this.#attached = new Promise((resolve, reject) => {
|
|
31
|
+
this.#resolveAttached = resolve;
|
|
32
|
+
this.#rejectAttached = reject;
|
|
33
|
+
});
|
|
34
|
+
this.#startTimer = setTimeout(() => this.#fail(new Error('tmux control mode attach timed out')), START_TIMEOUT_MS);
|
|
35
|
+
this.#startTimer.unref?.();
|
|
36
|
+
this.#child = spawnControl('tmux', ['-C', 'attach-session', '-E', '-t', session], {
|
|
37
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
38
|
+
});
|
|
39
|
+
this.#child.stdout.on('data', (chunk) => this.#onChunk(chunk));
|
|
40
|
+
this.#child.stderr.on('data', (chunk) => { this.#lastError += chunk.toString('utf8'); });
|
|
41
|
+
this.#child.stdin.on('error', (error) => this.#fail(error));
|
|
42
|
+
this.#child.on('error', (error) => this.#fail(error));
|
|
43
|
+
this.#child.on('exit', (code) => {
|
|
44
|
+
if (!this.#closed)
|
|
45
|
+
this.#fail(new Error(this.#lastError || `tmux control mode exited (${code ?? 'unknown'})`));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async start() {
|
|
49
|
+
await this.#attached;
|
|
50
|
+
}
|
|
51
|
+
async sendKey(key) {
|
|
52
|
+
if (!/^[A-Za-z0-9-]+$/.test(key))
|
|
53
|
+
throw new TypeError('invalid tmux key');
|
|
54
|
+
await this.#attached;
|
|
55
|
+
await this.#request(`send-keys -t ${this.#pane} ${key}`);
|
|
56
|
+
}
|
|
57
|
+
output() {
|
|
58
|
+
if (!this.#armed || this.#failed || this.#overflow)
|
|
59
|
+
return null;
|
|
60
|
+
return Buffer.concat(this.#chunks, this.#bytes);
|
|
61
|
+
}
|
|
62
|
+
outputFrames() {
|
|
63
|
+
if (!this.#armed || this.#failed || this.#overflow)
|
|
64
|
+
return null;
|
|
65
|
+
return this.#chunks.map((chunk) => Buffer.from(chunk));
|
|
66
|
+
}
|
|
67
|
+
close() {
|
|
68
|
+
if (this.#closed)
|
|
69
|
+
return;
|
|
70
|
+
this.#closed = true;
|
|
71
|
+
clearTimeout(this.#startTimer);
|
|
72
|
+
const error = new Error('tmux control output capture closed');
|
|
73
|
+
this.#rejectAttached(error);
|
|
74
|
+
this.#pending?.reject(error);
|
|
75
|
+
this.#response?.reject(error);
|
|
76
|
+
this.#pending = null;
|
|
77
|
+
this.#response = undefined;
|
|
78
|
+
try {
|
|
79
|
+
this.#child.kill();
|
|
80
|
+
}
|
|
81
|
+
catch { /* already gone */ }
|
|
82
|
+
}
|
|
83
|
+
#onChunk(chunk) {
|
|
84
|
+
this.#buffer = Buffer.concat([this.#buffer, chunk]);
|
|
85
|
+
for (;;) {
|
|
86
|
+
const newline = this.#buffer.indexOf(0x0a);
|
|
87
|
+
if (newline < 0)
|
|
88
|
+
return;
|
|
89
|
+
const line = this.#buffer.subarray(0, newline);
|
|
90
|
+
this.#buffer = this.#buffer.subarray(newline + 1);
|
|
91
|
+
this.#onLine(line.at(-1) === 0x0d ? line.subarray(0, -1) : line);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
#onLine(line) {
|
|
95
|
+
if (line.subarray(0, 8).toString('ascii') === '%output ') {
|
|
96
|
+
const split = line.indexOf(0x20, 8);
|
|
97
|
+
if (!this.#armed || split < 0
|
|
98
|
+
|| line.subarray(8, split).toString('ascii') !== this.#pane)
|
|
99
|
+
return;
|
|
100
|
+
const output = decodeControlData(line.subarray(split + 1));
|
|
101
|
+
if (this.#bytes + output.length > this.#maxBytes) {
|
|
102
|
+
this.#overflow = true;
|
|
103
|
+
this.#chunks = [];
|
|
104
|
+
this.#bytes = 0;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!this.#overflow) {
|
|
108
|
+
this.#chunks.push(output);
|
|
109
|
+
this.#bytes += output.length;
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (line.subarray(0, 7).toString('ascii') === '%begin ') {
|
|
114
|
+
this.#response = this.#pending;
|
|
115
|
+
this.#pending = null;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const ended = line.subarray(0, 5).toString('ascii') === '%end ';
|
|
119
|
+
const errored = line.subarray(0, 7).toString('ascii') === '%error ';
|
|
120
|
+
if (ended || errored) {
|
|
121
|
+
const response = this.#response;
|
|
122
|
+
this.#response = undefined;
|
|
123
|
+
if (response) {
|
|
124
|
+
if (errored)
|
|
125
|
+
response.reject(new Error('tmux control command failed'));
|
|
126
|
+
else {
|
|
127
|
+
// tmux never inserts notifications inside a command output block. Arming synchronously at
|
|
128
|
+
// this %end makes every subsequently parsed %output byte provably newer than this C-c.
|
|
129
|
+
this.#chunks = [];
|
|
130
|
+
this.#bytes = 0;
|
|
131
|
+
this.#overflow = false;
|
|
132
|
+
this.#armed = true;
|
|
133
|
+
response.resolve();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (line.subarray(0, 17).toString('ascii') === '%session-changed ') {
|
|
139
|
+
clearTimeout(this.#startTimer);
|
|
140
|
+
this.#resolveAttached();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (line.subarray(0, 5).toString('ascii') === '%exit') {
|
|
144
|
+
this.#fail(new Error('tmux control mode detached'));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
#fail(error) {
|
|
148
|
+
if (this.#failed || this.#closed)
|
|
149
|
+
return;
|
|
150
|
+
this.#failed = true;
|
|
151
|
+
this.#closed = true;
|
|
152
|
+
clearTimeout(this.#startTimer);
|
|
153
|
+
this.#rejectAttached(error);
|
|
154
|
+
this.#pending?.reject(error);
|
|
155
|
+
this.#response?.reject(error);
|
|
156
|
+
this.#pending = null;
|
|
157
|
+
this.#response = undefined;
|
|
158
|
+
this.#chunks = [];
|
|
159
|
+
this.#bytes = 0;
|
|
160
|
+
try {
|
|
161
|
+
this.#child.kill();
|
|
162
|
+
}
|
|
163
|
+
catch { /* already gone */ }
|
|
164
|
+
}
|
|
165
|
+
#request(command) {
|
|
166
|
+
if (this.#closed || this.#failed) {
|
|
167
|
+
return Promise.reject(new Error('tmux control output capture is unavailable'));
|
|
168
|
+
}
|
|
169
|
+
if (this.#pending || this.#response) {
|
|
170
|
+
return Promise.reject(new Error('tmux control command already pending'));
|
|
171
|
+
}
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
this.#pending = { resolve, reject };
|
|
174
|
+
this.#child.stdin.write(`${command}\n`, (error) => {
|
|
175
|
+
if (error)
|
|
176
|
+
this.#fail(error);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handmux",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.1",
|
|
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.27.1","date":"2026-09-08","zh":"腾讯语音 · 终端光标修复 · Codex 对话恢复","en":"Tencent voice · terminal cursor fixes · Codex chat recovery"},
|
|
79
|
+
{"version":"0.27.0","date":"2026-09-05","zh":"腾讯语音输入 · 终端输入更准确","en":"Tencent voice input · more accurate terminal typing"},
|
|
80
|
+
{"version":"0.26.0","date":"2026-09-04","zh":"对话精细复制 · Pi 上下文状态","en":"Precise chat copy · Pi context status"},
|
|
78
81
|
{"version":"0.25.2","date":"2026-09-02","zh":"修复 Agent 对话断流、排队与工具路径","en":"Fixed Agent chat streams, queues, and tool paths"},
|
|
79
82
|
{"version":"0.25.1","date":"2026-09-02","zh":"修复 Codex 幽灵任务与对话空白","en":"Fixed Codex ghost tasks and blank chats"},
|
|
80
83
|
{"version":"0.25.0","date":"2026-09-01","zh":"Pi 原生接入 · 多 Agent 对话更可靠","en":"Native Pi support · more reliable agent chat"},
|
|
81
84
|
{"version":"0.24.0","date":"2026-08-11","zh":"Codex 任务进度与 Goal 原生上线","en":"Native Codex task progress and Goals"},
|
|
82
|
-
{"version":"0.23.0","date":"2026-08-09","zh":"Codex 托管对话正式上线","en":"Managed Codex chat is here"}
|
|
83
|
-
{"version":"0.22.1","date":"2026-08-04","zh":"网页宽度与网站版本分开控制","en":"Separate page width and site-version controls"},
|
|
84
|
-
{"version":"0.22.0","date":"2026-08-03","zh":"全屏设置 · 窗口与分屏管理更清晰","en":"Full-screen Settings · clearer window and pane management"},
|
|
85
|
-
{"version":"0.21.0","date":"2026-08-03","zh":"网页预览:手机直连、电脑代理与静态目录","en":"Web preview: direct, computer proxy, and static folders"}
|
|
85
|
+
{"version":"0.23.0","date":"2026-08-09","zh":"Codex 托管对话正式上线","en":"Managed Codex chat is here"}
|
|
86
86
|
]
|
|
87
87
|
}
|