@unlinearity/cli-wechat-bridge 1.1.4 → 1.1.6
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 +153 -131
- package/bin/_run-entry.mjs +20 -17
- package/dist/bridge/bridge-adapters.claude.js +429 -11
- package/dist/bridge/bridge-adapters.codex.js +441 -22
- package/dist/bridge/bridge-adapters.core.js +2 -1
- package/dist/bridge/bridge-adapters.opencode.js +392 -53
- package/dist/bridge/bridge-adapters.pi.js +33 -7
- package/dist/bridge/bridge-adapters.shared.js +2 -1
- package/dist/bridge/bridge-controller.js +3 -0
- package/dist/bridge/bridge-final-reply.js +2 -65
- package/dist/bridge/bridge-session-resume.js +151 -0
- package/dist/bridge/bridge-state.js +1 -1
- package/dist/bridge/bridge-utils.js +30 -101
- package/dist/bridge/claude-hooks.js +1 -0
- package/dist/bridge/wechat-bridge.js +256 -211
- package/dist/bridge/wechat-forwarding.js +2 -58
- package/dist/bridge/wechat-outbound-queue.js +2 -0
- package/dist/channels/wechat/bridge-final-reply.js +65 -0
- package/dist/channels/wechat/channel-message.js +24 -0
- package/dist/channels/wechat/index.js +5 -0
- package/dist/channels/wechat/wechat-channel-port.js +67 -0
- package/dist/channels/wechat/wechat-forwarding.js +79 -0
- package/dist/channels/wechat/wechat-outbound-queue.js +72 -0
- package/dist/companion/codex-remote-client.js +291 -32
- package/dist/companion/codex-visible-client-link.js +122 -0
- package/dist/companion/local-companion-link.js +26 -0
- package/dist/companion/local-companion-start.js +0 -1
- package/dist/companion/opencode-tui-bridge-plugin.js +73 -0
- package/dist/companion/pi-tui-bridge-extension.js +42 -3
- package/dist/core/bridge-control.js +32 -0
- package/dist/core/bridge-event-forwarder.js +50 -0
- package/dist/core/bridge-message-router.js +32 -0
- package/dist/core/channel-types.js +1 -0
- package/dist/core/text-utils.js +63 -0
- package/dist/daemon/wechat-daemon.js +242 -159
- package/dist/i18n/messages-en.js +2 -2
- package/dist/i18n/messages-zh.js +2 -2
- package/package.json +2 -6
- package/bin/wechat-claude-start.mjs +0 -10
- package/bin/wechat-codex-start.mjs +0 -10
- package/bin/wechat-opencode-start.mjs +0 -10
- package/bin/wechat-pi-start.mjs +0 -10
|
@@ -1,65 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export async function forwardWechatFinalReply(params) {
|
|
4
|
-
const { adapter, rawText, sender, onEmptyVisibleReply } = params;
|
|
5
|
-
const parsed = parseWechatFinalReply(rawText);
|
|
6
|
-
const sanitizedText = sanitizeWechatFinalReplyText(adapter, parsed.visibleText);
|
|
7
|
-
const visibleText = formatFinalReplyMessage(adapter, sanitizedText).trim();
|
|
8
|
-
if (visibleText) {
|
|
9
|
-
// Send long replies in bounded chunks: a single oversized sendmessage call
|
|
10
|
-
// can be rejected by the WeChat API, silently losing the whole reply.
|
|
11
|
-
const chunks = splitWechatTextIntoChunks(visibleText);
|
|
12
|
-
for (let index = 0; index < chunks.length; index += 1) {
|
|
13
|
-
const sent = await sender.sendText(chunks[index]);
|
|
14
|
-
if (sent === false) {
|
|
15
|
-
// The send channel is failing (e.g. an expired context token).
|
|
16
|
-
// Report what was dropped instead of ending mid-reply silently;
|
|
17
|
-
// this notice itself is best effort and may also fail.
|
|
18
|
-
const remainingChunks = chunks.length - index - 1;
|
|
19
|
-
if (remainingChunks > 0 || parsed.attachments.length > 0) {
|
|
20
|
-
const parts = [];
|
|
21
|
-
if (remainingChunks > 0) {
|
|
22
|
-
parts.push(`${remainingChunks} reply chunk(s)`);
|
|
23
|
-
}
|
|
24
|
-
if (parsed.attachments.length > 0) {
|
|
25
|
-
parts.push(`${parsed.attachments.length} attachment(s)`);
|
|
26
|
-
}
|
|
27
|
-
await sender.sendText(`[bridge] Reply delivery was interrupted; ${parts.join(" and ")} could not be sent. Send a new message to retry.`);
|
|
28
|
-
}
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
else if (adapter === "opencode" && parsed.visibleText.trim()) {
|
|
34
|
-
onEmptyVisibleReply?.({
|
|
35
|
-
adapter,
|
|
36
|
-
rawVisibleText: parsed.visibleText,
|
|
37
|
-
});
|
|
38
|
-
const sent = await sender.sendText(OPENCODE_EMPTY_VISIBLE_REPLY_MESSAGE);
|
|
39
|
-
if (sent === false) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
for (const attachment of parsed.attachments) {
|
|
44
|
-
try {
|
|
45
|
-
switch (attachment.kind) {
|
|
46
|
-
case "image":
|
|
47
|
-
await sender.sendImage(attachment.path);
|
|
48
|
-
break;
|
|
49
|
-
case "file":
|
|
50
|
-
await sender.sendFile(attachment.path);
|
|
51
|
-
break;
|
|
52
|
-
case "voice":
|
|
53
|
-
await sender.sendVoice(attachment.path);
|
|
54
|
-
break;
|
|
55
|
-
case "video":
|
|
56
|
-
await sender.sendVideo(attachment.path);
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
const errorText = error instanceof Error ? error.message : String(error ?? "unknown error");
|
|
62
|
-
await sender.sendText(`Failed to send ${attachment.kind} attachment: ${attachment.path}\n${errorText}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
1
|
+
/** @deprecated Import from ../channels/wechat/bridge-final-reply.ts instead. */
|
|
2
|
+
export * from "../channels/wechat/bridge-final-reply.js";
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { formatResumeSessionList, formatSessionSwitchMessage, } from "./bridge-utils.js";
|
|
2
|
+
export const RESUME_SESSION_LIST_LIMIT = 8;
|
|
3
|
+
export const RESUME_SESSION_SNAPSHOT_TTL_MS = 5 * 60 * 1_000;
|
|
4
|
+
const RESUME_SESSION_PREFIX_SCAN_LIMIT = 100;
|
|
5
|
+
function describeError(error) {
|
|
6
|
+
return error instanceof Error ? error.message : String(error);
|
|
7
|
+
}
|
|
8
|
+
function adapterDisplayName(adapter) {
|
|
9
|
+
switch (adapter) {
|
|
10
|
+
case "codex":
|
|
11
|
+
return "Codex";
|
|
12
|
+
case "claude":
|
|
13
|
+
return "Claude";
|
|
14
|
+
case "opencode":
|
|
15
|
+
return "OpenCode";
|
|
16
|
+
case "pi":
|
|
17
|
+
return "Pi";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function adapterSessionNoun(adapter) {
|
|
21
|
+
return adapter === "codex" ? "thread" : "session";
|
|
22
|
+
}
|
|
23
|
+
export function isWechatResumeEnabled(adapter) {
|
|
24
|
+
return (adapter === "codex" ||
|
|
25
|
+
adapter === "claude" ||
|
|
26
|
+
adapter === "opencode" ||
|
|
27
|
+
adapter === "pi");
|
|
28
|
+
}
|
|
29
|
+
export function shouldForwardSessionSwitchEvent(reason) {
|
|
30
|
+
return reason !== "wechat_resume";
|
|
31
|
+
}
|
|
32
|
+
export class ResumeSessionCoordinator {
|
|
33
|
+
adapter;
|
|
34
|
+
runtime;
|
|
35
|
+
now;
|
|
36
|
+
snapshot = null;
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.adapter = options.adapter;
|
|
39
|
+
this.runtime = options.runtime;
|
|
40
|
+
this.now = options.now ?? Date.now;
|
|
41
|
+
}
|
|
42
|
+
clear() {
|
|
43
|
+
this.snapshot = null;
|
|
44
|
+
}
|
|
45
|
+
async execute(target) {
|
|
46
|
+
const normalizedTarget = target?.trim() ?? "";
|
|
47
|
+
if (!normalizedTarget) {
|
|
48
|
+
const candidates = await this.listCandidates(RESUME_SESSION_LIST_LIMIT);
|
|
49
|
+
this.snapshot = {
|
|
50
|
+
createdAtMs: this.now(),
|
|
51
|
+
candidates,
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
kind: "list",
|
|
55
|
+
message: formatResumeSessionList({
|
|
56
|
+
adapter: this.adapter,
|
|
57
|
+
candidates,
|
|
58
|
+
currentSessionId: this.runtime.getState().sharedSessionId ??
|
|
59
|
+
this.runtime.getState().sharedThreadId,
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const candidate = await this.resolveCandidate(normalizedTarget);
|
|
64
|
+
const currentSessionId = this.runtime.getState().sharedSessionId ??
|
|
65
|
+
this.runtime.getState().sharedThreadId;
|
|
66
|
+
if (candidate.sessionId === currentSessionId) {
|
|
67
|
+
return {
|
|
68
|
+
kind: "already_active",
|
|
69
|
+
sessionId: candidate.sessionId,
|
|
70
|
+
message: `${adapterDisplayName(this.adapter)} ${adapterSessionNoun(this.adapter)} ${candidate.sessionId.slice(0, 12)} is already active.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
await this.runtime.resumeSession(candidate.sessionId);
|
|
74
|
+
return {
|
|
75
|
+
kind: "resumed",
|
|
76
|
+
sessionId: candidate.sessionId,
|
|
77
|
+
message: formatSessionSwitchMessage({
|
|
78
|
+
adapter: this.adapter,
|
|
79
|
+
sessionId: candidate.sessionId,
|
|
80
|
+
source: "wechat",
|
|
81
|
+
reason: "wechat_resume",
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async resolveCandidate(target) {
|
|
86
|
+
if (/^\d+$/.test(target)) {
|
|
87
|
+
const snapshot = this.getCurrentSnapshot();
|
|
88
|
+
if (!snapshot) {
|
|
89
|
+
throw new Error("The recent session list has expired. Send /resume again before choosing a number.");
|
|
90
|
+
}
|
|
91
|
+
const index = Number(target) - 1;
|
|
92
|
+
const candidate = snapshot.candidates[index];
|
|
93
|
+
if (!candidate) {
|
|
94
|
+
throw new Error(`Resume selection ${target} is outside the displayed range. Send /resume again to refresh the list.`);
|
|
95
|
+
}
|
|
96
|
+
return candidate;
|
|
97
|
+
}
|
|
98
|
+
const snapshot = this.getCurrentSnapshot();
|
|
99
|
+
const snapshotMatch = this.findIdMatch(snapshot?.candidates ?? [], target);
|
|
100
|
+
if (snapshotMatch) {
|
|
101
|
+
return snapshotMatch;
|
|
102
|
+
}
|
|
103
|
+
const candidates = await this.listCandidates(RESUME_SESSION_PREFIX_SCAN_LIMIT);
|
|
104
|
+
const candidate = this.findIdMatch(candidates, target);
|
|
105
|
+
if (candidate) {
|
|
106
|
+
return candidate;
|
|
107
|
+
}
|
|
108
|
+
// A complete ID may be older than the recent candidate window. The
|
|
109
|
+
// adapter remains responsible for validating its existence and cwd.
|
|
110
|
+
return {
|
|
111
|
+
sessionId: target,
|
|
112
|
+
title: target,
|
|
113
|
+
lastUpdatedAt: new Date(0).toISOString(),
|
|
114
|
+
source: this.adapter,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
findIdMatch(candidates, target) {
|
|
118
|
+
const normalizedTarget = target.toLowerCase();
|
|
119
|
+
const exact = candidates.find((candidate) => candidate.sessionId.toLowerCase() === normalizedTarget);
|
|
120
|
+
if (exact) {
|
|
121
|
+
return exact;
|
|
122
|
+
}
|
|
123
|
+
const prefixMatches = candidates.filter((candidate) => candidate.sessionId.toLowerCase().startsWith(normalizedTarget));
|
|
124
|
+
if (prefixMatches.length > 1) {
|
|
125
|
+
throw new Error(`Session ID prefix ${target} is ambiguous. Send /resume again and use a longer ID prefix.`);
|
|
126
|
+
}
|
|
127
|
+
return prefixMatches[0] ?? null;
|
|
128
|
+
}
|
|
129
|
+
getCurrentSnapshot() {
|
|
130
|
+
if (!this.snapshot) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (this.now() - this.snapshot.createdAtMs > RESUME_SESSION_SNAPSHOT_TTL_MS) {
|
|
134
|
+
this.snapshot = null;
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return this.snapshot;
|
|
138
|
+
}
|
|
139
|
+
async listCandidates(limit) {
|
|
140
|
+
try {
|
|
141
|
+
return await this.runtime.listResumeSessions(limit);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const message = describeError(error);
|
|
145
|
+
if (/^Failed to list\s/i.test(message)) {
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`Failed to list ${adapterDisplayName(this.adapter)} ${adapterSessionNoun(this.adapter)}s: ${message}`, { cause: error });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureWorkspaceChannelDir, ensureChannelDataDir, } from "../wechat/channel-config.js";
|
|
3
|
-
import { buildInstanceId } from "
|
|
3
|
+
import { buildInstanceId } from "../core/text-utils.js";
|
|
4
4
|
import { writeJsonFileAtomic } from "../utils/atomic-file.js";
|
|
5
5
|
import { getProcessRecordByPid, isWechatBridgeCommandLine, } from "./bridge-process-reaper.js";
|
|
6
6
|
const ORPHAN_LOCK_RECLAIM_TIMEOUT_MS = 2_000;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { parseBridgeControlCommand, } from "../core/bridge-control.js";
|
|
5
|
+
import { normalizeOutput, summarizeOutput, truncatePreview, } from "../core/text-utils.js";
|
|
6
|
+
export { buildInstanceId, buildOneTimeCode, formatDuration, normalizeOutput, nowIso, stripAnsi, summarizeOutput, truncatePreview, } from "../core/text-utils.js";
|
|
7
7
|
// Messages older than bridge start minus this grace window are treated as
|
|
8
8
|
// pre-start backlog and skipped. The window must absorb realistic clock skew
|
|
9
9
|
// between the local machine and WeChat server timestamps: with a small value,
|
|
@@ -99,29 +99,6 @@ const INLINE_REFERENCE_ONLY_FILE_EXTENSIONS = new Set([
|
|
|
99
99
|
const INLINE_MAAS_URL_RE = /https?:\/\/[^\s]*?\/([A-Za-z]:\\.+?(?:\.\s*[A-Za-z0-9]{2,8})+)(?:\?[^\n]*)?/g;
|
|
100
100
|
const INLINE_WINDOWS_PATH_RE = /(^|[^\w])`?([A-Za-z]:\\(?:[^\\/:*?"<>|\r\n`]+\\)*[^\\/:*?"<>|\r\n`]+?(?:\.\s*[A-Za-z0-9]{2,8})+)`?(?=$|[^\w])/gm;
|
|
101
101
|
const INLINE_HOME_RELATIVE_PATH_RE = /(^|[^\w])`?((?:~[\\/])?(?:Desktop|Documents|Downloads|Pictures|Videos|Music)[\\/](?:[^\\/:*?"<>|\r\n`]+[\\/])*[^\\/:*?"<>|\r\n`]+?(?:\.\s*[A-Za-z0-9]{2,8})+)`?(?=$|[^\w])/gim;
|
|
102
|
-
export function nowIso() {
|
|
103
|
-
return new Date().toISOString();
|
|
104
|
-
}
|
|
105
|
-
export function stripAnsi(text) {
|
|
106
|
-
return text.replace(ANSI_ESCAPE_RE, "");
|
|
107
|
-
}
|
|
108
|
-
export function normalizeOutput(text) {
|
|
109
|
-
return stripAnsi(text)
|
|
110
|
-
// eslint-disable-next-line no-control-regex
|
|
111
|
-
.replace(/\u0000/g, "")
|
|
112
|
-
.replace(/\r\n/g, "\n")
|
|
113
|
-
.replace(/\r/g, "\n");
|
|
114
|
-
}
|
|
115
|
-
export function truncatePreview(text, maxLength = 140) {
|
|
116
|
-
const normalized = normalizeOutput(text).trim().replace(/\s+/g, " ");
|
|
117
|
-
if (!normalized) {
|
|
118
|
-
return "(empty)";
|
|
119
|
-
}
|
|
120
|
-
if (normalized.length <= maxLength) {
|
|
121
|
-
return normalized;
|
|
122
|
-
}
|
|
123
|
-
return `${normalized.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
124
|
-
}
|
|
125
102
|
export function isThinkingForwardEnabled() {
|
|
126
103
|
if (process.env.CLI_BRIDGE_THINKING_FORWARD === "1") {
|
|
127
104
|
return true;
|
|
@@ -150,51 +127,8 @@ export function formatThinkingForWechat(text, maxLength = 500) {
|
|
|
150
127
|
}
|
|
151
128
|
return `${normalized.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
152
129
|
}
|
|
153
|
-
export function buildOneTimeCode(length = 6) {
|
|
154
|
-
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
155
|
-
let code = "";
|
|
156
|
-
while (code.length < length) {
|
|
157
|
-
code += alphabet[Math.floor(Math.random() * alphabet.length)];
|
|
158
|
-
}
|
|
159
|
-
return code;
|
|
160
|
-
}
|
|
161
|
-
export function buildInstanceId() {
|
|
162
|
-
return `bridge-${Date.now().toString(36)}-${buildOneTimeCode(6).toLowerCase()}`;
|
|
163
|
-
}
|
|
164
130
|
export function parseSystemCommand(text) {
|
|
165
|
-
|
|
166
|
-
if (!trimmed.startsWith("/")) {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
const [rawCommand, ...rest] = trimmed.split(/\s+/);
|
|
170
|
-
if (!rawCommand) {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
const command = rawCommand.toLowerCase();
|
|
174
|
-
const argument = rest.join(" ").trim();
|
|
175
|
-
switch (command) {
|
|
176
|
-
case "/status":
|
|
177
|
-
return { type: "status" };
|
|
178
|
-
case "/resume":
|
|
179
|
-
return argument ? { type: "resume", target: argument } : { type: "resume" };
|
|
180
|
-
case "/new":
|
|
181
|
-
case "/new-session":
|
|
182
|
-
return { type: "new_session" };
|
|
183
|
-
case "/stop":
|
|
184
|
-
return { type: "stop" };
|
|
185
|
-
case "/reset":
|
|
186
|
-
return { type: "reset" };
|
|
187
|
-
case "/confirm":
|
|
188
|
-
case "/yes":
|
|
189
|
-
return { type: "confirm" };
|
|
190
|
-
case "/deny":
|
|
191
|
-
case "/no":
|
|
192
|
-
return { type: "deny" };
|
|
193
|
-
case "/answer":
|
|
194
|
-
return argument ? { type: "answer", raw: argument } : null;
|
|
195
|
-
default:
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
131
|
+
return parseBridgeControlCommand(text);
|
|
198
132
|
}
|
|
199
133
|
export function parseWechatControlCommand(text, options) {
|
|
200
134
|
const systemCommand = parseSystemCommand(text);
|
|
@@ -474,32 +408,6 @@ export function detectCliApproval(text) {
|
|
|
474
408
|
denyInput: matched.denyInput,
|
|
475
409
|
};
|
|
476
410
|
}
|
|
477
|
-
export function formatDuration(durationMs) {
|
|
478
|
-
if (!Number.isFinite(durationMs) || durationMs < 0) {
|
|
479
|
-
return "0s";
|
|
480
|
-
}
|
|
481
|
-
const totalSeconds = Math.floor(durationMs / 1000);
|
|
482
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
483
|
-
const seconds = totalSeconds % 60;
|
|
484
|
-
if (!minutes) {
|
|
485
|
-
return `${seconds}s`;
|
|
486
|
-
}
|
|
487
|
-
return `${minutes}m ${seconds}s`;
|
|
488
|
-
}
|
|
489
|
-
export function summarizeOutput(text, maxLength = 280) {
|
|
490
|
-
const normalized = normalizeOutput(text)
|
|
491
|
-
.split("\n")
|
|
492
|
-
.map((line) => line.trim())
|
|
493
|
-
.filter(Boolean);
|
|
494
|
-
if (!normalized.length) {
|
|
495
|
-
return "(no output)";
|
|
496
|
-
}
|
|
497
|
-
const summary = normalized.slice(-6).join("\n");
|
|
498
|
-
if (summary.length <= maxLength) {
|
|
499
|
-
return summary;
|
|
500
|
-
}
|
|
501
|
-
return summary.slice(summary.length - maxLength);
|
|
502
|
-
}
|
|
503
411
|
export function formatStatusReport(bridgeState, adapterState) {
|
|
504
412
|
const pending = bridgeState.pendingConfirmation;
|
|
505
413
|
const pendingUserInput = bridgeState.pendingUserInput;
|
|
@@ -568,6 +476,20 @@ export function formatSessionSwitchMessage(params) {
|
|
|
568
476
|
return `OpenCode session switched to ${shortSessionId}.`;
|
|
569
477
|
}
|
|
570
478
|
}
|
|
479
|
+
if (params.adapter === "pi") {
|
|
480
|
+
switch (params.reason) {
|
|
481
|
+
case "local_follow":
|
|
482
|
+
case "local_session_fallback":
|
|
483
|
+
case "local_turn":
|
|
484
|
+
return `Pi session switched to ${shortSessionId} from the local terminal.`;
|
|
485
|
+
case "wechat_resume":
|
|
486
|
+
return `Pi session switched to ${shortSessionId} from WeChat.`;
|
|
487
|
+
case "startup_restore":
|
|
488
|
+
return `Pi restored shared session ${shortSessionId} on startup.`;
|
|
489
|
+
default:
|
|
490
|
+
return `Pi session switched to ${shortSessionId}.`;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
571
493
|
switch (params.reason) {
|
|
572
494
|
case "local_follow":
|
|
573
495
|
case "local_session_fallback":
|
|
@@ -596,17 +518,24 @@ export function formatResumeSessionList(params) {
|
|
|
596
518
|
? "No saved Codex threads were found for this working directory."
|
|
597
519
|
: adapter === "opencode"
|
|
598
520
|
? "No saved OpenCode sessions were found for this working directory."
|
|
599
|
-
:
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
521
|
+
: adapter === "pi"
|
|
522
|
+
? "No saved Pi sessions were found for this working directory."
|
|
523
|
+
: "No saved Claude sessions were found for this working directory.";
|
|
524
|
+
}
|
|
525
|
+
const title = adapter === "codex"
|
|
526
|
+
? "Recent Codex threads:"
|
|
527
|
+
: adapter === "opencode"
|
|
528
|
+
? "Recent OpenCode sessions:"
|
|
529
|
+
: adapter === "pi"
|
|
530
|
+
? "Recent Pi sessions:"
|
|
531
|
+
: "Recent Claude sessions:";
|
|
603
532
|
return [
|
|
604
533
|
title,
|
|
605
534
|
...candidates.map((candidate, index) => {
|
|
606
535
|
const marker = currentSessionId && candidate.sessionId === currentSessionId ? " [current]" : "";
|
|
607
536
|
return `${index + 1}. ${candidate.title} (${candidate.lastUpdatedAt}, ${candidate.sessionId.slice(0, 12)})${marker}`;
|
|
608
537
|
}),
|
|
609
|
-
|
|
538
|
+
"Reply with /resume <number> or /resume <unique-id-prefix>.",
|
|
610
539
|
].join("\n");
|
|
611
540
|
}
|
|
612
541
|
export function formatResumeThreadList(candidates, currentThreadId) {
|