handmux 0.23.0 → 0.25.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 +31 -8
- package/README.zh-CN.md +31 -8
- package/bin/handmux.js +14 -688
- package/bin/node-version.js +20 -0
- package/dist/bin/handmux.js +1015 -0
- package/dist/connectors/bridgeClient.js +385 -0
- package/dist/connectors/claude/index.js +476 -0
- package/dist/connectors/pi/bridgeClient.js +8 -0
- package/dist/connectors/pi/index.js +851 -0
- package/dist/hooks/handmux-notify.sh +64 -0
- package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
- package/dist/hooks/handmux-write.cjs +264 -0
- package/dist/package.json +127 -0
- package/dist/public/assets/index-CTrqTqtT.css +32 -0
- package/dist/public/assets/index-DmjeMSQA.js +346 -0
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
- package/{public → dist/public}/index.html +5 -4
- package/dist/src/agent-runtime/adapter.js +204 -0
- package/dist/src/agent-runtime/bridge.js +1006 -0
- package/dist/src/agent-runtime/bridgeStore.js +25 -0
- package/dist/src/agent-runtime/bridgeTransport.js +619 -0
- package/dist/src/agent-runtime/bridgeTypes.js +13 -0
- package/dist/src/agent-runtime/builtinRuntime.js +193 -0
- package/dist/src/agent-runtime/conversation.js +2179 -0
- package/dist/src/agent-runtime/conversationActivation.js +75 -0
- package/dist/src/agent-runtime/conversationActivity.js +69 -0
- package/dist/src/agent-runtime/conversationControls.js +206 -0
- package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
- package/dist/src/agent-runtime/conversationStore.js +352 -0
- package/dist/src/agent-runtime/conversationTypes.js +1 -0
- package/dist/src/agent-runtime/conversationValidation.js +339 -0
- package/dist/src/agent-runtime/inbox.js +973 -0
- package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
- package/dist/src/agent-runtime/inboxStore.js +25 -0
- package/dist/src/agent-runtime/inboxTypes.js +1 -0
- package/dist/src/agent-runtime/interaction.js +829 -0
- package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
- package/dist/src/agent-runtime/interactionStore.js +18 -0
- package/dist/src/agent-runtime/interactionTypes.js +1 -0
- package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
- package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
- package/dist/src/agent-runtime/resourceTypes.js +1 -0
- package/dist/src/agent-runtime/resources.js +245 -0
- package/dist/src/agent-runtime/run.js +328 -0
- package/dist/src/agent-runtime/runtime.js +793 -0
- package/dist/src/agent-runtime/sessionControl.js +164 -0
- package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
- package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
- package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
- package/dist/src/agents/claude.js +203 -0
- package/dist/src/agents/claudeConversation.js +362 -0
- package/dist/src/agents/claudeInteraction.js +134 -0
- package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
- package/dist/src/agents/codex.js +262 -0
- package/dist/src/agents/codexConversation.js +1103 -0
- package/dist/src/agents/codexConversationActivation.js +121 -0
- package/dist/src/agents/codexConversationControls.js +256 -0
- package/dist/src/agents/codexInteraction.js +306 -0
- package/dist/src/agents/codexSessionControl.js +118 -0
- package/dist/src/agents/codexSubscriptionUsage.js +313 -0
- package/dist/src/agents/codexTerminalControl.js +23 -0
- package/dist/src/agents/conversationProjectionSafety.js +416 -0
- package/dist/src/agents/index.js +39 -0
- package/dist/src/agents/nativeInbox.js +264 -0
- package/dist/src/agents/nativeInboxSources.js +115 -0
- package/dist/src/agents/pi.js +28 -0
- package/dist/src/agents/piConversation.js +523 -0
- package/dist/src/agents/piConversationHistory.js +502 -0
- package/dist/src/agents/piInboxBridge.js +368 -0
- package/dist/src/agents/piSessionControl.js +32 -0
- package/dist/src/agents/processIdentity.js +94 -0
- package/dist/src/agents/scanUtils.js +262 -0
- package/dist/src/apiAccountProviders.js +194 -0
- package/dist/src/apiAccounts.js +592 -0
- package/dist/src/apiErrors.js +86 -0
- package/{src → dist/src}/appName.js +12 -13
- package/dist/src/asr/iflyConfig.js +7 -0
- package/dist/src/asr/iflySign.js +14 -0
- package/dist/src/auth.js +27 -0
- package/dist/src/browser/bootstrap.js +72 -0
- package/dist/src/browser/cookieProfiles.js +413 -0
- package/dist/src/browser/coordinator.js +51 -0
- package/dist/src/browser/hammerheadCompat.js +104 -0
- package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
- package/dist/src/browser/manager.js +560 -0
- package/dist/src/browser/originLabel.js +15 -0
- package/dist/src/browser/profilePersistence.js +235 -0
- package/dist/src/browser/publicProxy.js +176 -0
- package/dist/src/browser/routes.js +159 -0
- package/dist/src/browser/sessionStore.js +72 -0
- package/dist/src/browser/siteVersion.js +121 -0
- package/dist/src/browser/targetPolicy.js +163 -0
- package/{src → dist/src}/browser/worker.js +14 -18
- package/dist/src/browser/workerClient.js +286 -0
- package/dist/src/browser/workerServer.js +91 -0
- package/dist/src/captureBackground.js +95 -0
- package/dist/src/claudeEvents.js +525 -0
- package/dist/src/cli/agentIntegration.js +131 -0
- package/dist/src/cli/agentLauncher.js +26 -0
- package/dist/src/cli/claudeHooks.js +290 -0
- package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
- package/dist/src/cli/cloudflared.js +106 -0
- package/dist/src/cli/codexManaged.js +80 -0
- package/{src → dist/src}/cli/cpolarUrl.js +8 -9
- package/dist/src/cli/drivers.js +98 -0
- package/dist/src/cli/hookScaffold.js +39 -0
- package/dist/src/cli/i18n/en.js +392 -0
- package/{src → dist/src}/cli/i18n/index.js +27 -25
- package/dist/src/cli/i18n/zh.js +391 -0
- package/dist/src/cli/legacyCodexHooks.js +52 -0
- package/{src → dist/src}/cli/natappUrl.js +2 -3
- package/dist/src/cli/options.js +245 -0
- package/dist/src/cli/piExtension.js +155 -0
- package/dist/src/cli/probe.js +16 -0
- package/{src → dist/src}/cli/prompt.js +4 -6
- package/dist/src/cli/pushCmd.js +102 -0
- package/{src → dist/src}/cli/qr.js +19 -19
- package/dist/src/cli/service.js +154 -0
- package/dist/src/cli/setupModel.js +194 -0
- package/dist/src/cli/setupWizard.js +647 -0
- package/dist/src/cli/shortcutEditor.js +351 -0
- package/dist/src/cli/sshTunnel.js +18 -0
- package/dist/src/cli/state.js +117 -0
- package/dist/src/cli/statusLine.js +115 -0
- package/dist/src/cli/supervisor.js +278 -0
- package/dist/src/cli/supervisorHealth.js +21 -0
- package/dist/src/cli/supervisorLaunch.js +148 -0
- package/dist/src/cli/supervisorProcesses.js +62 -0
- package/dist/src/cli/supervisorState.js +62 -0
- package/dist/src/cli/tmuxVersion.js +53 -0
- package/{src → dist/src}/cli/tunlite.js +7 -10
- package/dist/src/cli/tunnelClients.js +72 -0
- package/{src → dist/src}/cli/updateCheck.js +98 -69
- package/{src → dist/src}/cli/urlHost.js +8 -4
- package/dist/src/cli/workspaceCmd.js +347 -0
- package/dist/src/codexAppServer.js +3436 -0
- package/dist/src/codexConversationProjection.js +199 -0
- package/dist/src/codexDiff.js +118 -0
- package/dist/src/codexMessageIdentity.js +18 -0
- package/dist/src/codexPlan.js +30 -0
- package/dist/src/codexQueueProtocol.js +137 -0
- package/dist/src/codexStreamProtocol.js +207 -0
- package/dist/src/codexToolProtocol.js +68 -0
- package/dist/src/codexTranscriptParse.js +835 -0
- package/dist/src/codexUsageSnapshot.js +228 -0
- package/dist/src/config.js +8 -0
- package/{src → dist/src}/docPath.js +22 -24
- package/dist/src/docs.js +337 -0
- package/dist/src/git.js +284 -0
- package/dist/src/healthProtocol.js +97 -0
- package/dist/src/httpApi.js +96 -0
- package/dist/src/jsonStore.js +15 -0
- package/{src → dist/src}/keyNames.js +3 -3
- package/dist/src/notifications.js +111 -0
- package/dist/src/orphans.js +167 -0
- package/dist/src/paneInput.js +42 -0
- package/dist/src/pendingPrompt.js +194 -0
- package/dist/src/previewServer.js +202 -0
- package/dist/src/previews.js +179 -0
- package/dist/src/privateStateStore.js +124 -0
- package/dist/src/projectTask/backup.js +224 -0
- package/dist/src/projectTask/lock.js +108 -0
- package/dist/src/projectTask/migrations.js +90 -0
- package/dist/src/projectTask/routes.js +155 -0
- package/dist/src/projectTask/runtime.js +182 -0
- package/dist/src/projectTask/schema.js +56 -0
- package/dist/src/projectTask/store.js +457 -0
- package/dist/src/push.js +224 -0
- package/dist/src/routes/agents.js +631 -0
- package/dist/src/routes/apiAccounts.js +82 -0
- package/dist/src/routes/files.js +230 -0
- package/dist/src/routes/git.js +89 -0
- package/dist/src/routes/health.js +16 -0
- package/dist/src/routes/notifications.js +16 -0
- package/dist/src/routes/previews.js +43 -0
- package/dist/src/routes/push.js +145 -0
- package/dist/src/routes/sessions.js +244 -0
- package/dist/src/routes/system.js +220 -0
- package/dist/src/routes/terminal.js +244 -0
- package/dist/src/routes/workspace.js +267 -0
- package/dist/src/server.js +315 -0
- package/dist/src/shortcutConfig.js +83 -0
- package/{src → dist/src}/staticCache.js +5 -3
- package/{src → dist/src}/staticCompression.js +5 -6
- package/dist/src/terminalStream.js +441 -0
- package/{src → dist/src}/tmux/commands.js +185 -172
- package/dist/src/tmux/format.js +47 -0
- package/dist/src/transcriptParse.js +261 -0
- package/dist/src/transcriptReader.js +209 -0
- package/{src → dist/src}/trimCapture.js +11 -11
- package/dist/src/uploadTypes.js +31 -0
- package/dist/src/urlPolicy.js +18 -0
- package/dist/src/usagePaths.js +15 -0
- package/dist/src/workspace/agentRunner.js +301 -0
- package/dist/src/workspace/atomicJson.js +31 -0
- package/dist/src/workspace/capture.js +139 -0
- package/dist/src/workspace/checkpointer.js +228 -0
- package/dist/src/workspace/environment.js +71 -0
- package/dist/src/workspace/lock.js +137 -0
- package/dist/src/workspace/mapping.js +128 -0
- package/dist/src/workspace/operations.js +271 -0
- package/dist/src/workspace/paths.js +15 -0
- package/dist/src/workspace/planner.js +292 -0
- package/dist/src/workspace/restore.js +363 -0
- package/dist/src/workspace/runtime.js +318 -0
- package/dist/src/workspace/schema.js +156 -0
- package/dist/src/workspace/store.js +464 -0
- package/dist/src/workspace/tmuxAdapter.js +704 -0
- package/dist/src/workspaceProtocol.js +158 -0
- package/package.json +30 -17
- package/hooks/handmux-notify.sh +0 -22
- package/hooks/handmux-write.cjs +0 -78
- package/public/assets/index-C3N7OkyP.css +0 -32
- package/public/assets/index-D1DWcC5u.js +0 -337
- package/src/.gitkeep +0 -0
- package/src/agents/claude.js +0 -159
- package/src/agents/codex.js +0 -168
- package/src/agents/index.js +0 -21
- package/src/agents/processIdentity.js +0 -72
- package/src/agents/scanUtils.js +0 -225
- package/src/asr/iflyConfig.js +0 -10
- package/src/asr/iflySign.js +0 -16
- package/src/auth.js +0 -30
- package/src/browser/bootstrap.js +0 -68
- package/src/browser/cookieProfiles.js +0 -403
- package/src/browser/coordinator.js +0 -47
- package/src/browser/hammerheadCompat.js +0 -86
- package/src/browser/hammerheadRedirectCompat.js +0 -38
- package/src/browser/manager.js +0 -556
- package/src/browser/originLabel.js +0 -17
- package/src/browser/profilePersistence.js +0 -241
- package/src/browser/publicProxy.js +0 -175
- package/src/browser/routes.js +0 -154
- package/src/browser/sessionStore.js +0 -78
- package/src/browser/siteVersion.js +0 -124
- package/src/browser/targetPolicy.js +0 -137
- package/src/browser/workerClient.js +0 -253
- package/src/browser/workerServer.js +0 -98
- package/src/captureBackground.js +0 -93
- package/src/claudeEvents.js +0 -399
- package/src/cli/claudeHooks.js +0 -206
- package/src/cli/cloudflared.js +0 -99
- package/src/cli/codexManaged.js +0 -77
- package/src/cli/drivers.js +0 -97
- package/src/cli/hookScaffold.js +0 -33
- package/src/cli/i18n/en.js +0 -385
- package/src/cli/i18n/zh.js +0 -384
- package/src/cli/legacyCodexHooks.js +0 -49
- package/src/cli/options.js +0 -211
- package/src/cli/probe.js +0 -16
- package/src/cli/pushCmd.js +0 -60
- package/src/cli/service.js +0 -126
- package/src/cli/setupModel.js +0 -154
- package/src/cli/setupWizard.js +0 -506
- package/src/cli/shortcutEditor.js +0 -288
- package/src/cli/sshTunnel.js +0 -12
- package/src/cli/state.js +0 -90
- package/src/cli/statusLine.js +0 -90
- package/src/cli/supervisor.js +0 -183
- package/src/cli/supervisorProcesses.js +0 -57
- package/src/cli/tmuxVersion.js +0 -49
- package/src/cli/tunnelClients.js +0 -62
- package/src/cli/workspaceCmd.js +0 -300
- package/src/codexAppServer.js +0 -1620
- package/src/codexDiff.js +0 -108
- package/src/codexTranscriptParse.js +0 -413
- package/src/codexUsageSnapshot.cjs +0 -129
- package/src/config.js +0 -9
- package/src/docs.js +0 -228
- package/src/git.js +0 -208
- package/src/httpApi.js +0 -72
- package/src/jsonStore.js +0 -24
- package/src/notifications.js +0 -68
- package/src/orphans.js +0 -152
- package/src/pendingPrompt.js +0 -163
- package/src/previewServer.js +0 -168
- package/src/previews.js +0 -116
- package/src/push.js +0 -183
- package/src/routes/codex.js +0 -473
- package/src/routes/files.js +0 -172
- package/src/routes/git.js +0 -57
- package/src/routes/notifications.js +0 -19
- package/src/routes/previews.js +0 -35
- package/src/routes/push.js +0 -119
- package/src/routes/sessions.js +0 -168
- package/src/routes/system.js +0 -132
- package/src/routes/terminal.js +0 -210
- package/src/routes/transcript.js +0 -171
- package/src/routes/workspace.js +0 -267
- package/src/server.js +0 -186
- package/src/shortcutConfig.js +0 -75
- package/src/terminalStream.js +0 -391
- package/src/tmux/format.js +0 -45
- package/src/transcriptParse.js +0 -180
- package/src/transcriptReader.js +0 -101
- package/src/uploadTypes.js +0 -30
- package/src/urlPolicy.js +0 -14
- package/src/usage.js +0 -260
- package/src/workspace/agentRunner.js +0 -188
- package/src/workspace/atomicJson.js +0 -32
- package/src/workspace/capture.js +0 -139
- package/src/workspace/checkpointer.js +0 -187
- package/src/workspace/environment.js +0 -75
- package/src/workspace/lock.js +0 -128
- package/src/workspace/mapping.js +0 -124
- package/src/workspace/operations.js +0 -252
- package/src/workspace/paths.js +0 -16
- package/src/workspace/planner.js +0 -247
- package/src/workspace/restore.js +0 -279
- package/src/workspace/runtime.js +0 -314
- package/src/workspace/schema.js +0 -136
- package/src/workspace/store.js +0 -382
- package/src/workspace/tmuxAdapter.js +0 -554
- /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
- /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
- /package/{public → dist/public}/icons/badge-96.png +0 -0
- /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-192.png +0 -0
- /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-512.png +0 -0
- /package/{public → dist/public}/icons/logo.svg +0 -0
- /package/{public → dist/public}/manifest.webmanifest +0 -0
- /package/{public → dist/public}/offline.html +0 -0
- /package/{public → dist/public}/sw.js +0 -0
- /package/{src → dist/src}/browser/protocol.js +0 -0
- /package/{src → dist/src}/cli/cfNamed.js +0 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { promises as fsp } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { sanitizeToolInputWithMetadata, sanitizeToolResultText, } from './conversationProjectionSafety.js';
|
|
5
|
+
const MAX_SESSION_BYTES = 64 * 1024 * 1024;
|
|
6
|
+
const MAX_TEXT_BYTES = 240 * 1024;
|
|
7
|
+
const ENTRY_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
8
|
+
function record(value) {
|
|
9
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
10
|
+
? value : null;
|
|
11
|
+
}
|
|
12
|
+
/** Alias Pi's command tools onto the shared Codex command presentation contract. */
|
|
13
|
+
export function normalizePiCommandTool(name, input) {
|
|
14
|
+
if (name !== 'bash' && name !== 'shell')
|
|
15
|
+
return { name, input };
|
|
16
|
+
const args = record(input);
|
|
17
|
+
if (!args)
|
|
18
|
+
return { name, input };
|
|
19
|
+
const command = typeof args.command === 'string' ? args.command
|
|
20
|
+
: name === 'shell' && typeof args.cmd === 'string' ? args.cmd : undefined;
|
|
21
|
+
if (command === undefined)
|
|
22
|
+
return { name, input };
|
|
23
|
+
const { command: _command, ...rest } = args;
|
|
24
|
+
return { name: 'exec_command', input: { ...rest, cmd: command } };
|
|
25
|
+
}
|
|
26
|
+
function time(entry, message) {
|
|
27
|
+
if (typeof message?.timestamp === 'number' && Number.isFinite(message.timestamp))
|
|
28
|
+
return message.timestamp;
|
|
29
|
+
if (typeof entry.timestamp !== 'string')
|
|
30
|
+
return undefined;
|
|
31
|
+
const parsed = Date.parse(entry.timestamp);
|
|
32
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
33
|
+
}
|
|
34
|
+
function cappedText(value, maxBytes = MAX_TEXT_BYTES) {
|
|
35
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
const originalBytes = Buffer.byteLength(value);
|
|
38
|
+
if (originalBytes <= maxBytes)
|
|
39
|
+
return { text: value, truncated: false, originalBytes };
|
|
40
|
+
if (maxBytes <= 0)
|
|
41
|
+
return { text: '', truncated: true, originalBytes };
|
|
42
|
+
let text = Buffer.from(value).subarray(0, maxBytes).toString('utf8');
|
|
43
|
+
while (Buffer.byteLength(text) > maxBytes)
|
|
44
|
+
text = text.slice(0, -1);
|
|
45
|
+
return { text, truncated: true, originalBytes };
|
|
46
|
+
}
|
|
47
|
+
function content(value, sanitizeToolResult = false) {
|
|
48
|
+
const raw = typeof value === 'string' ? [{ type: 'text', text: value }]
|
|
49
|
+
: Array.isArray(value) ? value : [];
|
|
50
|
+
const blocks = [];
|
|
51
|
+
let imageCount = 0;
|
|
52
|
+
let truncated = false;
|
|
53
|
+
let redacted = false;
|
|
54
|
+
let originalBytes = 0;
|
|
55
|
+
let remainingBytes = MAX_TEXT_BYTES;
|
|
56
|
+
for (const candidate of raw) {
|
|
57
|
+
const block = record(candidate);
|
|
58
|
+
if (block?.type === 'image') {
|
|
59
|
+
imageCount += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (block?.type !== 'text')
|
|
63
|
+
continue;
|
|
64
|
+
const rawText = typeof block.text === 'string' ? block.text : null;
|
|
65
|
+
const safe = rawText !== null && sanitizeToolResult ? sanitizeToolResultText(rawText) : null;
|
|
66
|
+
const normalized = cappedText(safe?.text ?? block.text, remainingBytes);
|
|
67
|
+
if (!normalized)
|
|
68
|
+
continue;
|
|
69
|
+
if (normalized.text) {
|
|
70
|
+
blocks.push({ type: 'text', text: normalized.text });
|
|
71
|
+
remainingBytes -= Buffer.byteLength(normalized.text);
|
|
72
|
+
}
|
|
73
|
+
truncated ||= normalized.truncated || safe?.truncated === true;
|
|
74
|
+
redacted ||= safe?.redacted === true;
|
|
75
|
+
originalBytes += safe?.originalBytes ?? normalized.originalBytes;
|
|
76
|
+
}
|
|
77
|
+
return { blocks, imageCount, truncated, redacted, originalBytes };
|
|
78
|
+
}
|
|
79
|
+
function base(entry, sessionId, id = `pi:${entry.id}`, message) {
|
|
80
|
+
const createdAt = time(entry, message);
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
sessionId,
|
|
84
|
+
status: 'complete',
|
|
85
|
+
...(createdAt === undefined ? {} : { sourceCreatedAt: createdAt }),
|
|
86
|
+
extensions: { 'pi.entryId': entry.id },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function textStatus(value) {
|
|
90
|
+
return value.truncated ? {
|
|
91
|
+
status: 'truncated',
|
|
92
|
+
truncation: { reason: 'size_limit', originalBytes: value.originalBytes },
|
|
93
|
+
} : {};
|
|
94
|
+
}
|
|
95
|
+
function imageNotice(entry, sessionId, count, suffix) {
|
|
96
|
+
return count > 0 ? {
|
|
97
|
+
...base(entry, sessionId, `pi:${entry.id}:${suffix}`),
|
|
98
|
+
kind: 'notice', level: 'info', code: 'image_unavailable',
|
|
99
|
+
message: count === 1 ? 'Image attachment is available in the Pi terminal'
|
|
100
|
+
: `${count} image attachments are available in the Pi terminal`,
|
|
101
|
+
} : null;
|
|
102
|
+
}
|
|
103
|
+
function projectMessage(entry, sessionId) {
|
|
104
|
+
const message = record(entry.message);
|
|
105
|
+
if (!message || typeof message.role !== 'string')
|
|
106
|
+
return [];
|
|
107
|
+
if (message.role === 'user') {
|
|
108
|
+
const projected = content(message.content);
|
|
109
|
+
const items = projected.blocks.length ? [{
|
|
110
|
+
...base(entry, sessionId, undefined, message), ...textStatus(projected),
|
|
111
|
+
kind: 'message', role: 'user', content: projected.blocks,
|
|
112
|
+
}] : [];
|
|
113
|
+
const notice = imageNotice(entry, sessionId, projected.imageCount, 'images');
|
|
114
|
+
if (notice)
|
|
115
|
+
items.push(notice);
|
|
116
|
+
return items;
|
|
117
|
+
}
|
|
118
|
+
if (message.role === 'assistant') {
|
|
119
|
+
const raw = Array.isArray(message.content) ? message.content : [];
|
|
120
|
+
const textBlocks = content(raw);
|
|
121
|
+
const items = [];
|
|
122
|
+
if (textBlocks.blocks.length) {
|
|
123
|
+
const failed = message.stopReason === 'error' || message.stopReason === 'aborted';
|
|
124
|
+
const errorMessage = message.stopReason === 'aborted'
|
|
125
|
+
? 'Pi generation was interrupted' : 'Pi generation failed';
|
|
126
|
+
items.push({
|
|
127
|
+
...base(entry, sessionId, undefined, message),
|
|
128
|
+
...(failed ? {
|
|
129
|
+
status: 'error',
|
|
130
|
+
error: { code: message.stopReason === 'aborted' ? 'interrupted' : 'provider_error', message: errorMessage },
|
|
131
|
+
} : textStatus(textBlocks)),
|
|
132
|
+
kind: 'message', role: 'assistant', content: textBlocks.blocks,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else if (message.stopReason === 'error' || message.stopReason === 'aborted') {
|
|
136
|
+
items.push({
|
|
137
|
+
...base(entry, sessionId, undefined, message),
|
|
138
|
+
status: 'error',
|
|
139
|
+
error: {
|
|
140
|
+
code: message.stopReason === 'aborted' ? 'interrupted' : 'provider_error',
|
|
141
|
+
message: message.stopReason === 'aborted'
|
|
142
|
+
? 'Pi generation was interrupted' : 'Pi generation failed',
|
|
143
|
+
},
|
|
144
|
+
kind: 'notice', level: 'error', code: 'generation_failed',
|
|
145
|
+
message: message.stopReason === 'aborted'
|
|
146
|
+
? 'Pi generation was interrupted' : 'Pi generation failed',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
let tool = 0;
|
|
150
|
+
for (const candidate of raw) {
|
|
151
|
+
const block = record(candidate);
|
|
152
|
+
// Pi persists raw thinking blocks without a user-visible summary marker. They are deliberately
|
|
153
|
+
// omitted rather than relabelled as reasoning_summary.
|
|
154
|
+
if (block?.type === 'toolCall' && typeof block.id === 'string'
|
|
155
|
+
&& ENTRY_ID_RE.test(block.id) && typeof block.name === 'string' && block.name) {
|
|
156
|
+
const normalizedTool = normalizePiCommandTool(block.name, block.arguments);
|
|
157
|
+
const inputProjection = sanitizeToolInputWithMetadata(normalizedTool.input);
|
|
158
|
+
const input = inputProjection.value;
|
|
159
|
+
items.push({
|
|
160
|
+
...base(entry, sessionId, `pi:${entry.id}:tool-${tool++}`, message),
|
|
161
|
+
kind: 'tool_call', callId: `pi:${block.id}`, name: normalizedTool.name,
|
|
162
|
+
...(input === undefined ? {} : { input }),
|
|
163
|
+
...(inputProjection.truncated ? {
|
|
164
|
+
status: 'truncated',
|
|
165
|
+
truncation: { reason: 'size_limit', originalBytes: inputProjection.originalBytes },
|
|
166
|
+
} : {}),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const notice = imageNotice(entry, sessionId, textBlocks.imageCount, 'images');
|
|
171
|
+
if (notice)
|
|
172
|
+
items.push(notice);
|
|
173
|
+
return items;
|
|
174
|
+
}
|
|
175
|
+
if (message.role === 'toolResult' && typeof message.toolCallId === 'string') {
|
|
176
|
+
const projected = content(message.content, true);
|
|
177
|
+
const items = [{
|
|
178
|
+
...base(entry, sessionId, undefined, message), ...textStatus(projected),
|
|
179
|
+
kind: 'tool_result', callId: `pi:${message.toolCallId}`, content: projected.blocks,
|
|
180
|
+
...(message.isError === true ? { isError: true } : {}),
|
|
181
|
+
}];
|
|
182
|
+
const notice = imageNotice(entry, sessionId, projected.imageCount, 'images');
|
|
183
|
+
if (notice)
|
|
184
|
+
items.push(notice);
|
|
185
|
+
return items;
|
|
186
|
+
}
|
|
187
|
+
if (message.role === 'bashExecution') {
|
|
188
|
+
const callId = `pi:${entry.id}:bash`;
|
|
189
|
+
const safeOutput = sanitizeToolResultText(String(message.output ?? ''));
|
|
190
|
+
const output = safeOutput.text ? safeOutput
|
|
191
|
+
: {
|
|
192
|
+
text: '', truncated: safeOutput.truncated, redacted: safeOutput.redacted,
|
|
193
|
+
originalBytes: safeOutput.originalBytes,
|
|
194
|
+
};
|
|
195
|
+
const normalizedTool = normalizePiCommandTool('bash', { command: String(message.command ?? '') });
|
|
196
|
+
const inputProjection = sanitizeToolInputWithMetadata(normalizedTool.input);
|
|
197
|
+
const input = inputProjection.value;
|
|
198
|
+
return [{
|
|
199
|
+
...base(entry, sessionId, `pi:${entry.id}:bash-call`, message),
|
|
200
|
+
kind: 'tool_call', callId, name: normalizedTool.name,
|
|
201
|
+
...(input === undefined ? {} : { input }),
|
|
202
|
+
...(inputProjection.truncated ? {
|
|
203
|
+
status: 'truncated',
|
|
204
|
+
truncation: { reason: 'size_limit', originalBytes: inputProjection.originalBytes },
|
|
205
|
+
} : {}),
|
|
206
|
+
}, {
|
|
207
|
+
...base(entry, sessionId, `pi:${entry.id}:bash-result`, message), ...textStatus(output),
|
|
208
|
+
kind: 'tool_result', callId, content: output.text ? [{ type: 'text', text: output.text }] : [],
|
|
209
|
+
...((typeof message.exitCode === 'number' && message.exitCode !== 0) || message.cancelled === true
|
|
210
|
+
? { isError: true } : {}),
|
|
211
|
+
}];
|
|
212
|
+
}
|
|
213
|
+
if (message.role === 'custom' && message.display === true) {
|
|
214
|
+
const projected = content(message.content);
|
|
215
|
+
const text = projected.blocks.filter((block) => block.type === 'text').map((block) => block.text).join('\n');
|
|
216
|
+
return text ? [{
|
|
217
|
+
...base(entry, sessionId, undefined, message), ...textStatus(projected),
|
|
218
|
+
kind: 'notice', level: 'info', code: typeof message.customType === 'string'
|
|
219
|
+
&& message.customType.length <= 256 ? message.customType : 'custom_message', message: text,
|
|
220
|
+
}] : [];
|
|
221
|
+
}
|
|
222
|
+
if (message.role === 'branchSummary' || message.role === 'compactionSummary') {
|
|
223
|
+
const summary = cappedText(message.summary);
|
|
224
|
+
return summary ? [{
|
|
225
|
+
...base(entry, sessionId, undefined, message), ...textStatus(summary),
|
|
226
|
+
kind: 'compaction', summary: summary.text,
|
|
227
|
+
}] : [];
|
|
228
|
+
}
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
function projectEntry(entry, sessionId) {
|
|
232
|
+
if (entry.type === 'message')
|
|
233
|
+
return projectMessage(entry, sessionId);
|
|
234
|
+
if (entry.type === 'compaction' || entry.type === 'branch_summary') {
|
|
235
|
+
const summary = cappedText(entry.summary);
|
|
236
|
+
return [{
|
|
237
|
+
...base(entry, sessionId), ...(summary ? textStatus(summary) : {}),
|
|
238
|
+
kind: 'compaction', ...(summary ? { summary: summary.text } : {}),
|
|
239
|
+
}];
|
|
240
|
+
}
|
|
241
|
+
if (entry.type === 'custom_message' && entry.display === true) {
|
|
242
|
+
return projectMessage({ ...entry, type: 'message', message: {
|
|
243
|
+
role: 'custom', customType: entry.customType, content: entry.content,
|
|
244
|
+
display: true, timestamp: Date.parse(entry.timestamp ?? ''),
|
|
245
|
+
} }, sessionId);
|
|
246
|
+
}
|
|
247
|
+
if (entry.type === 'model_change' && typeof entry.provider === 'string'
|
|
248
|
+
&& typeof entry.modelId === 'string') {
|
|
249
|
+
return [{
|
|
250
|
+
...base(entry, sessionId), kind: 'notice', level: 'info', code: 'model_changed',
|
|
251
|
+
message: `Model changed to ${entry.provider}/${entry.modelId}`,
|
|
252
|
+
}];
|
|
253
|
+
}
|
|
254
|
+
if (entry.type === 'thinking_level_change' && typeof entry.thinkingLevel === 'string') {
|
|
255
|
+
return [{
|
|
256
|
+
...base(entry, sessionId), kind: 'notice', level: 'info', code: 'thinking_level_changed',
|
|
257
|
+
message: `Thinking level changed to ${entry.thinkingLevel}`,
|
|
258
|
+
}];
|
|
259
|
+
}
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
function visibleText(item) {
|
|
263
|
+
if (item.kind !== 'message' || item.role !== 'user')
|
|
264
|
+
return undefined;
|
|
265
|
+
const value = item.content.flatMap((block) => block.type === 'text' ? [block.text] : []).join('\n');
|
|
266
|
+
return value || undefined;
|
|
267
|
+
}
|
|
268
|
+
function mergeLiveItems(durable, live) {
|
|
269
|
+
const ids = new Set(durable.map((item) => item.id));
|
|
270
|
+
const merged = [...durable];
|
|
271
|
+
const unmatchedDurableUsers = new Set(durable.flatMap((item, index) => (item.kind === 'message' && item.role === 'user' ? [index] : [])));
|
|
272
|
+
for (const item of live) {
|
|
273
|
+
if (ids.has(item.id))
|
|
274
|
+
continue;
|
|
275
|
+
const pendingRequest = item.extensions?.['pi.pendingClientRequestId'];
|
|
276
|
+
const pendingNativeText = item.extensions?.['pi.pendingNativeText'];
|
|
277
|
+
const duplicateIndex = typeof pendingRequest === 'string' && item.sourceCreatedAt !== undefined
|
|
278
|
+
? [...unmatchedDurableUsers].find((index) => {
|
|
279
|
+
const candidate = durable[index];
|
|
280
|
+
return candidate !== undefined
|
|
281
|
+
&& candidate.kind === 'message' && candidate.role === 'user'
|
|
282
|
+
&& candidate.sourceCreatedAt === item.sourceCreatedAt
|
|
283
|
+
&& visibleText(candidate) === (typeof pendingNativeText === 'string'
|
|
284
|
+
? pendingNativeText : visibleText(item));
|
|
285
|
+
}) : undefined;
|
|
286
|
+
if (duplicateIndex !== undefined) {
|
|
287
|
+
unmatchedDurableUsers.delete(duplicateIndex);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
ids.add(item.id);
|
|
291
|
+
merged.push(structuredClone(item));
|
|
292
|
+
}
|
|
293
|
+
return merged;
|
|
294
|
+
}
|
|
295
|
+
export function projectPiLiveItems(values, sessionId) {
|
|
296
|
+
const seen = new Set();
|
|
297
|
+
return values.flatMap((candidate) => {
|
|
298
|
+
const value = record(candidate);
|
|
299
|
+
if (!value || typeof value.type !== 'string' || typeof value.id !== 'string'
|
|
300
|
+
|| !ENTRY_ID_RE.test(value.id) || seen.has(value.id))
|
|
301
|
+
return [];
|
|
302
|
+
const parentId = value.parentId;
|
|
303
|
+
if (parentId !== null && (typeof parentId !== 'string' || !ENTRY_ID_RE.test(parentId)))
|
|
304
|
+
return [];
|
|
305
|
+
seen.add(value.id);
|
|
306
|
+
return projectEntry({ ...value, type: value.type, id: value.id, parentId }, sessionId);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
export async function resolvePiSessionFile(root, sessionId) {
|
|
310
|
+
const suffix = `_${sessionId}.jsonl`;
|
|
311
|
+
let found = null;
|
|
312
|
+
async function scan(directory, depth) {
|
|
313
|
+
if (found || depth > 2)
|
|
314
|
+
return;
|
|
315
|
+
let entries;
|
|
316
|
+
try {
|
|
317
|
+
entries = await fsp.readdir(directory, { withFileTypes: true });
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
for (const entry of entries) {
|
|
323
|
+
if (found)
|
|
324
|
+
return;
|
|
325
|
+
const candidate = path.join(directory, entry.name);
|
|
326
|
+
if (entry.isDirectory())
|
|
327
|
+
await scan(candidate, depth + 1);
|
|
328
|
+
else if (entry.isFile() && entry.name.endsWith(suffix))
|
|
329
|
+
found = candidate;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
await scan(root, 0);
|
|
333
|
+
return found;
|
|
334
|
+
}
|
|
335
|
+
async function loadBranch(file, sessionId, requestedLeaf) {
|
|
336
|
+
const stat = await fsp.stat(file);
|
|
337
|
+
if (!stat.isFile() || stat.size > MAX_SESSION_BYTES)
|
|
338
|
+
throw new Error('Pi session file is invalid or too large');
|
|
339
|
+
const lines = (await fsp.readFile(file, 'utf8')).split('\n').filter(Boolean);
|
|
340
|
+
const header = record(JSON.parse(lines.shift() ?? 'null'));
|
|
341
|
+
if (!header || header.type !== 'session' || header.id !== sessionId
|
|
342
|
+
|| !Number.isSafeInteger(header.version) || header.version < 1 || header.version > 3) {
|
|
343
|
+
throw new Error('Pi session header is invalid');
|
|
344
|
+
}
|
|
345
|
+
const entries = [];
|
|
346
|
+
const byId = new Map();
|
|
347
|
+
let previousId = null;
|
|
348
|
+
for (const line of lines) {
|
|
349
|
+
const value = record(JSON.parse(line));
|
|
350
|
+
if (!value || typeof value.type !== 'string' || typeof value.id !== 'string'
|
|
351
|
+
|| !ENTRY_ID_RE.test(value.id) || byId.has(value.id))
|
|
352
|
+
throw new Error('Pi session entry is invalid');
|
|
353
|
+
const parentId = header.version === 1 ? previousId : value.parentId;
|
|
354
|
+
if (parentId !== null && (typeof parentId !== 'string' || !ENTRY_ID_RE.test(parentId))) {
|
|
355
|
+
throw new Error('Pi session parent is invalid');
|
|
356
|
+
}
|
|
357
|
+
const entry = { ...value, type: value.type, id: value.id, parentId };
|
|
358
|
+
entries.push(entry);
|
|
359
|
+
byId.set(entry.id, entry);
|
|
360
|
+
previousId = entry.id;
|
|
361
|
+
}
|
|
362
|
+
if (requestedLeaf && requestedLeaf !== 'root' && !byId.has(requestedLeaf)) {
|
|
363
|
+
throw new Error('Pi active leaf is not readable yet');
|
|
364
|
+
}
|
|
365
|
+
const leafId = requestedLeaf ?? entries.at(-1)?.id ?? 'root';
|
|
366
|
+
if (leafId === 'root')
|
|
367
|
+
return { leafId, entries: [] };
|
|
368
|
+
const branch = [];
|
|
369
|
+
const visited = new Set();
|
|
370
|
+
let current = byId.get(leafId);
|
|
371
|
+
while (current) {
|
|
372
|
+
if (visited.has(current.id))
|
|
373
|
+
throw new Error('Pi session tree contains a cycle');
|
|
374
|
+
visited.add(current.id);
|
|
375
|
+
branch.push(current);
|
|
376
|
+
if (current.parentId === null)
|
|
377
|
+
break;
|
|
378
|
+
current = byId.get(current.parentId);
|
|
379
|
+
if (!current)
|
|
380
|
+
throw new Error('Pi session tree contains a missing parent');
|
|
381
|
+
}
|
|
382
|
+
branch.reverse();
|
|
383
|
+
return { leafId, entries: branch };
|
|
384
|
+
}
|
|
385
|
+
export class PiConversationHistory {
|
|
386
|
+
#sessionsRoot;
|
|
387
|
+
#resolveFile;
|
|
388
|
+
#activeLeaves = new Map();
|
|
389
|
+
#activeViews = new Map();
|
|
390
|
+
#liveSnapshots = new Map();
|
|
391
|
+
constructor({ sessionsRoot, resolveFile = resolvePiSessionFile }) {
|
|
392
|
+
this.#sessionsRoot = sessionsRoot;
|
|
393
|
+
this.#resolveFile = resolveFile;
|
|
394
|
+
}
|
|
395
|
+
setActiveLeaf(sessionId, leafId) {
|
|
396
|
+
if (!ENTRY_ID_RE.test(leafId))
|
|
397
|
+
throw new Error('Invalid Pi active leaf');
|
|
398
|
+
this.#activeLeaves.set(sessionId, leafId);
|
|
399
|
+
this.#activeViews.delete(sessionId);
|
|
400
|
+
}
|
|
401
|
+
setActiveView(sessionId, leafId, sourceViewId) {
|
|
402
|
+
if (!ENTRY_ID_RE.test(leafId) || typeof sourceViewId !== 'string'
|
|
403
|
+
|| sourceViewId.length === 0 || sourceViewId.length > 256) {
|
|
404
|
+
throw new Error('Invalid Pi active view');
|
|
405
|
+
}
|
|
406
|
+
this.#activeLeaves.set(sessionId, leafId);
|
|
407
|
+
this.#activeViews.set(sessionId, sourceViewId);
|
|
408
|
+
}
|
|
409
|
+
beginLive(sessionId, runId) {
|
|
410
|
+
const current = this.#liveSnapshots.get(sessionId);
|
|
411
|
+
if (current?.runId === runId)
|
|
412
|
+
return;
|
|
413
|
+
const sourceViewId = `pi-live:${runId}`;
|
|
414
|
+
this.#liveSnapshots.set(sessionId, {
|
|
415
|
+
runId, sourceViewId, items: [],
|
|
416
|
+
});
|
|
417
|
+
this.#activeLeaves.delete(sessionId);
|
|
418
|
+
this.#activeViews.delete(sessionId);
|
|
419
|
+
}
|
|
420
|
+
setLiveSnapshot(sessionId, runId, leafId, snapshot, sourceViewId) {
|
|
421
|
+
const current = this.#liveSnapshots.get(sessionId);
|
|
422
|
+
if (!current || current.runId !== runId || !ENTRY_ID_RE.test(leafId)
|
|
423
|
+
|| (sourceViewId !== undefined && (sourceViewId.length === 0 || sourceViewId.length > 256))
|
|
424
|
+
|| (snapshot.sessionFile !== undefined && (!path.isAbsolute(snapshot.sessionFile)
|
|
425
|
+
|| snapshot.sessionFile.length > 4096))
|
|
426
|
+
|| (snapshot.implementationVersion !== undefined
|
|
427
|
+
&& (!Number.isSafeInteger(snapshot.implementationVersion)
|
|
428
|
+
|| snapshot.implementationVersion <= 0))) {
|
|
429
|
+
throw new Error('Invalid Pi live snapshot');
|
|
430
|
+
}
|
|
431
|
+
this.#liveSnapshots.set(sessionId, {
|
|
432
|
+
runId,
|
|
433
|
+
leafId,
|
|
434
|
+
sourceViewId: sourceViewId ?? current.sourceViewId,
|
|
435
|
+
...(snapshot.sessionFile === undefined ? {} : { sessionFile: snapshot.sessionFile }),
|
|
436
|
+
...(snapshot.implementationVersion === undefined
|
|
437
|
+
? (current.implementationVersion === undefined
|
|
438
|
+
? {} : { implementationVersion: current.implementationVersion })
|
|
439
|
+
: { implementationVersion: snapshot.implementationVersion }),
|
|
440
|
+
items: snapshot.items ? structuredClone(snapshot.items) : current.items,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async discover(sessionId) {
|
|
444
|
+
const snapshot = await this.#snapshot(sessionId);
|
|
445
|
+
if (!snapshot)
|
|
446
|
+
return null;
|
|
447
|
+
const live = this.#liveSnapshots.get(sessionId);
|
|
448
|
+
return {
|
|
449
|
+
sourceViewId: this.#sourceViewId(sessionId, snapshot.leafId),
|
|
450
|
+
...(live?.implementationVersion === undefined
|
|
451
|
+
? {} : { implementationVersion: live.implementationVersion }),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
async readPage(session, request) {
|
|
455
|
+
const snapshot = await this.#snapshot(session.sessionId);
|
|
456
|
+
if (!snapshot)
|
|
457
|
+
throw new Error('Pi session is unavailable');
|
|
458
|
+
const all = snapshot.items;
|
|
459
|
+
let end = all.length;
|
|
460
|
+
if (request.beforeSourceCursor !== undefined) {
|
|
461
|
+
if (!/^\d+$/.test(request.beforeSourceCursor))
|
|
462
|
+
throw new Error('Invalid Pi source cursor');
|
|
463
|
+
const cursor = Number(request.beforeSourceCursor);
|
|
464
|
+
if (!Number.isSafeInteger(cursor))
|
|
465
|
+
throw new Error('Invalid Pi source cursor');
|
|
466
|
+
end = Math.min(cursor, all.length);
|
|
467
|
+
}
|
|
468
|
+
const start = Math.max(0, end - request.limit);
|
|
469
|
+
return {
|
|
470
|
+
sessionId: session.sessionId,
|
|
471
|
+
sourceViewId: this.#sourceViewId(session.sessionId, snapshot.leafId),
|
|
472
|
+
sourceHistoryToken: createHash('sha256').update(JSON.stringify(all)).digest('hex'),
|
|
473
|
+
items: all.slice(start, end),
|
|
474
|
+
...(start > 0 ? { previousSourceCursor: String(start) } : {}),
|
|
475
|
+
hasMore: start > 0,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
async #snapshot(sessionId) {
|
|
479
|
+
const live = this.#liveSnapshots.get(sessionId);
|
|
480
|
+
const file = live?.sessionFile ?? await this.#resolveFile(this.#sessionsRoot, sessionId);
|
|
481
|
+
if (file) {
|
|
482
|
+
try {
|
|
483
|
+
const branch = await loadBranch(file, sessionId, live?.leafId ?? this.#activeLeaves.get(sessionId));
|
|
484
|
+
return {
|
|
485
|
+
leafId: branch.leafId,
|
|
486
|
+
items: mergeLiveItems(branch.entries.flatMap((entry) => projectEntry(entry, sessionId)), live?.items ?? []),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
const code = error !== null && typeof error === 'object'
|
|
491
|
+
? error.code : undefined;
|
|
492
|
+
if (!live || code !== 'ENOENT')
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return live ? { leafId: live.leafId ?? 'root', items: structuredClone(live.items) } : null;
|
|
497
|
+
}
|
|
498
|
+
#sourceViewId(sessionId, leafId) {
|
|
499
|
+
return this.#liveSnapshots.get(sessionId)?.sourceViewId
|
|
500
|
+
?? this.#activeViews.get(sessionId) ?? `pi-leaf:${leafId}`;
|
|
501
|
+
}
|
|
502
|
+
}
|