handmux 0.25.2 → 0.26.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.
@@ -48,8 +48,8 @@
48
48
  @keyframes bootDot { 0%,100% { opacity: .22; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-3px); } }
49
49
  @media (prefers-reduced-motion: reduce) { .boot-glow, .boot-dots i { animation: none; opacity: .8; } }
50
50
  </style>
51
- <script type="module" crossorigin src="/assets/index-DH4DnhbV.js"></script>
52
- <link rel="stylesheet" crossorigin href="/assets/index-DPBerEuM.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" crossorigin href="/assets/index-DPBerEuM.css"></noscript>
51
+ <script type="module" crossorigin src="/assets/index-KDa5PCoi.js"></script>
52
+ <link rel="stylesheet" crossorigin href="/assets/index-BVFKs5dN.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" crossorigin href="/assets/index-BVFKs5dN.css"></noscript>
53
53
  </head>
54
54
  <body>
55
55
  <div id="boot-splash" aria-hidden="true">
@@ -6,6 +6,7 @@ import { createCodexNativeInboxSource, } from '../agents/nativeInboxSources.js';
6
6
  import { createCodexConversationAdapter } from '../agents/codexConversation.js';
7
7
  import { createCodexInteractionAdapter } from '../agents/codexInteraction.js';
8
8
  import { createPiConversationActivityReader, createPiConversationAdapter, } from '../agents/piConversation.js';
9
+ import { createPiConversationContextAdapter } from '../agents/piConversationContext.js';
9
10
  import { createPiSessionControlAdapter } from '../agents/piSessionControl.js';
10
11
  import { createCodexSessionControlAdapter } from '../agents/codexSessionControl.js';
11
12
  import { createCodexConversationControls } from '../agents/codexConversationControls.js';
@@ -128,6 +129,7 @@ export function createBuiltinAgentRuntime({ home, claudeEvents, claudeProjectsRo
128
129
  ...(piSessionsRoot === undefined ? {} : { sessionsRoot: piSessionsRoot }),
129
130
  }),
130
131
  conversationActivity: createPiConversationActivityReader(context.bridge),
132
+ conversationContext: createPiConversationContextAdapter({ host: context.bridge }),
131
133
  sessionControl: createPiSessionControlAdapter({ host: context.bridge }),
132
134
  start: () => {
133
135
  inbox.start();
@@ -5,7 +5,7 @@ import { createTranscriptParser } from '../transcriptParse.js';
5
5
  import { transcriptReader } from '../transcriptReader.js';
6
6
  import { projectsDir } from './claude.js';
7
7
  import { isSessionUuid } from './scanUtils.js';
8
- import { clipConversationText as clipped, safeProviderPathLabel, sanitizeToolInputWithMetadata, sanitizeToolResultText, } from './conversationProjectionSafety.js';
8
+ import { clipConversationText as clipped, safeProviderDiffPath, sanitizeToolInputWithMetadata, sanitizeToolResultText, } from './conversationProjectionSafety.js';
9
9
  function isRun(target) {
10
10
  return 'runId' in target;
11
11
  }
@@ -151,7 +151,7 @@ function toolItems(message, sessionId, id, tool) {
151
151
  const input = tool.input && typeof tool.input === 'object' && !Array.isArray(tool.input)
152
152
  ? tool.input : {};
153
153
  const rawPath = input.file_path ?? input.path;
154
- const providerPath = safeProviderPathLabel(rawPath);
154
+ const providerPath = safeProviderDiffPath(rawPath);
155
155
  const patch = diffPatch(tool);
156
156
  items.push({
157
157
  ...itemBase(message, sessionId, `${id}:diff`),
@@ -5,7 +5,7 @@ import { createCodexTranscriptParser, } from '../codexTranscriptParse.js';
5
5
  import { projectCodexConversationMutation } from '../codexConversationProjection.js';
6
6
  import { parseCodexStreamEvent } from '../codexStreamProtocol.js';
7
7
  import { resolveCodexRollout, sessionsDir } from './codex.js';
8
- import { clipConversationText, safeProviderPathLabel, sanitizeToolInputWithMetadata, sanitizeToolResultText, } from './conversationProjectionSafety.js';
8
+ import { clipConversationText, safeProviderDiffPath, sanitizeToolInputWithMetadata, sanitizeToolResultText, } from './conversationProjectionSafety.js';
9
9
  const DURABLE_QUIET_MS = 100;
10
10
  const DEFAULT_DURABLE_SETTLE_TIMEOUT_MS = 15_000;
11
11
  const MAX_OPENING_NATIVE_EVENTS = 4_096;
@@ -381,7 +381,7 @@ function projectDurableMessage(message, sessionId) {
381
381
  && !Array.isArray(message.tool.input) ? message.tool.input : {};
382
382
  if (message.tool.diff) {
383
383
  const rawPath = rawInput.file_path ?? rawInput.path;
384
- const path = safeProviderPathLabel(rawPath);
384
+ const path = safeProviderDiffPath(rawPath);
385
385
  const patch = typeof rawInput.patch === 'string'
386
386
  ? sanitizeToolResultText(rawInput.patch) : undefined;
387
387
  items.push({
@@ -33,8 +33,11 @@ function permissionMode(settings) {
33
33
  if ((sandbox === 'dangerFullAccess' || sandbox === 'danger-full-access') && approval === 'never') {
34
34
  return 'full-access';
35
35
  }
36
+ if (approval === 'on-request'
37
+ && (reviewer === 'auto_review' || reviewer === 'guardian_subagent'))
38
+ return 'auto-review';
36
39
  if ((sandbox === 'workspaceWrite' || sandbox === 'workspace-write') && approval === 'on-request') {
37
- return reviewer === 'auto_review' || reviewer === 'guardian_subagent' ? 'auto-review' : 'default';
40
+ return reviewer === undefined || reviewer === 'user' ? 'default' : 'custom';
38
41
  }
39
42
  return 'custom';
40
43
  }
@@ -127,6 +127,13 @@ export function safeProviderPathLabel(value) {
127
127
  return undefined;
128
128
  return path;
129
129
  }
130
+ export function safeProviderDiffPath(value) {
131
+ const label = safeProviderPathLabel(value);
132
+ if (label === undefined || label.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(label)) {
133
+ return undefined;
134
+ }
135
+ return label;
136
+ }
130
137
  function urlContainsPrivateData(value) {
131
138
  try {
132
139
  const url = new URL(value);
@@ -22,6 +22,7 @@ export const piAgentAdapter = {
22
22
  capabilities: {
23
23
  inbox: { apiVersion: 1 },
24
24
  conversation: { apiVersion: 1, experimental: true },
25
+ conversationContext: { apiVersion: 1 },
25
26
  sessionControl: { apiVersion: 1 },
26
27
  },
27
28
  presentation: { iconId: 'pi' },
@@ -0,0 +1,49 @@
1
+ const PI_CONTEXT_IMPLEMENTATION_VERSION = 7;
2
+ function record(value) {
3
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
4
+ ? value : null;
5
+ }
6
+ function finite(value) {
7
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
8
+ }
9
+ function contextSnapshot(value) {
10
+ const snapshot = record(value);
11
+ if (!snapshot || !['idle', 'working', 'waiting', 'compacting', 'unknown']
12
+ .includes(String(snapshot.activity))) {
13
+ throw new Error('Pi Extension returned an invalid Conversation Context snapshot');
14
+ }
15
+ const hasUsedTokens = snapshot.usedTokens !== undefined;
16
+ const hasTotalTokens = snapshot.totalTokens !== undefined;
17
+ if (hasUsedTokens !== hasTotalTokens
18
+ || (hasUsedTokens && (!finite(snapshot.usedTokens)
19
+ || !finite(snapshot.totalTokens) || snapshot.totalTokens <= 0))) {
20
+ throw new Error('Pi Extension returned invalid context usage');
21
+ }
22
+ if (snapshot.cwd !== undefined
23
+ && (typeof snapshot.cwd !== 'string' || !snapshot.cwd.trim())) {
24
+ throw new Error('Pi Extension returned an invalid working directory');
25
+ }
26
+ return {
27
+ activity: snapshot.activity,
28
+ ...(hasUsedTokens ? {
29
+ usedTokens: snapshot.usedTokens,
30
+ totalTokens: snapshot.totalTokens,
31
+ } : {}),
32
+ ...(typeof snapshot.cwd === 'string' ? { cwd: snapshot.cwd } : {}),
33
+ };
34
+ }
35
+ export function createPiConversationContextAdapter({ host, }) {
36
+ if (!host || typeof host.request !== 'function') {
37
+ throw new TypeError('Pi Conversation Context adapter requires LocalAgentBridgeHost');
38
+ }
39
+ return {
40
+ apiVersion: 1,
41
+ async read(run) {
42
+ // Context is optional for pre-v7 Connectors. Avoid probing an unregistered handler so their
43
+ // otherwise healthy Conversation and Inbox capabilities remain usable until Pi is reloaded.
44
+ if ((run.ref.implementationVersion ?? 1) < PI_CONTEXT_IMPLEMENTATION_VERSION)
45
+ return null;
46
+ return contextSnapshot(await host.request(run, 'conversation', 'context', {}, { timeoutMs: 8_000, signal: run.signal }));
47
+ },
48
+ };
49
+ }
@@ -1,9 +1,12 @@
1
1
  import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
2
3
  import path from 'node:path';
3
4
  import { homedir } from 'node:os';
4
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
6
  const MANAGED_MARK = '// handmux-managed-pi-extension:v1';
6
7
  const ENTRY_MARK = '// handmux-entry:';
8
+ const NATIVE_IMPORT_MODULE_URL = 'data:text/javascript;base64,'
9
+ + 'ZXhwb3J0IGZ1bmN0aW9uIGltcG9ydFBpQ29ubmVjdG9yKHNwZWNpZmllcikgeyByZXR1cm4gaW1wb3J0KHNwZWNpZmllcik7IH0K';
7
10
  export class PiExtensionInstallError extends Error {
8
11
  code;
9
12
  constructor(code, message) {
@@ -59,11 +62,18 @@ function expectedEntryUrl(entryFile) {
59
62
  }
60
63
  function wrapper(entryFile) {
61
64
  const entryUrl = expectedEntryUrl(entryFile);
65
+ const fingerprint = createHash('sha256').update(fs.readFileSync(entryFile)).digest('hex');
66
+ const importUrl = `${entryUrl}?handmux=${fingerprint}`;
62
67
  return [
63
68
  MANAGED_MARK,
64
69
  `${ENTRY_MARK}${entryUrl}`,
65
70
  '// This tiny wrapper is owned by Handmux. Pi loads it from its documented global extension path.',
66
- `export { default } from ${JSON.stringify(entryUrl)};`,
71
+ '// Native ESM import preserves the fingerprint that Pi\'s jiti strips from static re-exports.',
72
+ `import { importPiConnector } from ${JSON.stringify(NATIVE_IMPORT_MODULE_URL)};`,
73
+ 'export default async function handmux(api) {',
74
+ ` const connector = await importPiConnector(${JSON.stringify(importUrl)});`,
75
+ ' return connector.default(api);',
76
+ '}',
67
77
  '',
68
78
  ].join('\n');
69
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handmux",
3
- "version": "0.25.2",
3
+ "version": "0.26.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.26.0","date":"2026-09-04","zh":"对话精细复制 · Pi 上下文状态","en":"Precise chat copy · Pi context status"},
78
79
  {"version":"0.25.2","date":"2026-09-02","zh":"修复 Agent 对话断流、排队与工具路径","en":"Fixed Agent chat streams, queues, and tool paths"},
79
80
  {"version":"0.25.1","date":"2026-09-02","zh":"修复 Codex 幽灵任务与对话空白","en":"Fixed Codex ghost tasks and blank chats"},
80
81
  {"version":"0.25.0","date":"2026-09-01","zh":"Pi 原生接入 · 多 Agent 对话更可靠","en":"Native Pi support · more reliable agent chat"},
81
82
  {"version":"0.24.0","date":"2026-08-11","zh":"Codex 任务进度与 Goal 原生上线","en":"Native Codex task progress and Goals"},
82
83
  {"version":"0.23.0","date":"2026-08-09","zh":"Codex 托管对话正式上线","en":"Managed Codex chat is here"},
83
84
  {"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.22.0","date":"2026-08-03","zh":"全屏设置 · 窗口与分屏管理更清晰","en":"Full-screen Settings · clearer window and pane management"}
86
86
  ]
87
87
  }