leduo-patrol 2.2.1 → 2.2.3
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 +11 -2
- package/dist/server/__tests__/acp-session.test.js +92 -0
- package/dist/server/__tests__/activity-monitor.test.js +13 -1
- package/dist/server/__tests__/session-manager.test.js +215 -1
- package/dist/server/acp-session.js +476 -0
- package/dist/server/activity-monitor.js +22 -7
- package/dist/server/index.js +54 -1
- package/dist/server/session-manager.js +1117 -121
- package/dist/web/assets/index-B-YXVUoQ.css +1 -0
- package/dist/web/assets/index-Bu0K7QgY.js +21 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -1
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/LICENSE +191 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/README.md +53 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.d.ts +823 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.js +965 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.test.d.ts +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.test.js +839 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/acp.test.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/agent.d.ts +2 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/agent.js +225 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/agent.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/client.d.ts +2 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/client.js +130 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/examples/client.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.d.ts +35 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js +5 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/index.d.ts +27 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/index.js +28 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/index.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.d.ts +2870 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.js +3 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.d.ts +5333 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js +1554 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/stream.d.ts +24 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/stream.js +64 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/dist/stream.js.map +1 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/package.json +66 -0
- package/vendor/claude-code-acp/node_modules/@agentclientprotocol/sdk/schema/schema.json +4125 -0
- package/vendor/claude-code-acp/node_modules/@types/node/LICENSE +21 -0
- package/vendor/claude-code-acp/node_modules/@types/node/README.md +15 -0
- package/vendor/claude-code-acp/node_modules/@types/node/assert/strict.d.ts +105 -0
- package/vendor/claude-code-acp/node_modules/@types/node/assert.d.ts +955 -0
- package/vendor/claude-code-acp/node_modules/@types/node/async_hooks.d.ts +623 -0
- package/vendor/claude-code-acp/node_modules/@types/node/buffer.buffer.d.ts +466 -0
- package/vendor/claude-code-acp/node_modules/@types/node/buffer.d.ts +1810 -0
- package/vendor/claude-code-acp/node_modules/@types/node/child_process.d.ts +1428 -0
- package/vendor/claude-code-acp/node_modules/@types/node/cluster.d.ts +486 -0
- package/vendor/claude-code-acp/node_modules/@types/node/compatibility/iterators.d.ts +21 -0
- package/vendor/claude-code-acp/node_modules/@types/node/console.d.ts +151 -0
- package/vendor/claude-code-acp/node_modules/@types/node/constants.d.ts +20 -0
- package/vendor/claude-code-acp/node_modules/@types/node/crypto.d.ts +4065 -0
- package/vendor/claude-code-acp/node_modules/@types/node/dgram.d.ts +564 -0
- package/vendor/claude-code-acp/node_modules/@types/node/diagnostics_channel.d.ts +576 -0
- package/vendor/claude-code-acp/node_modules/@types/node/dns/promises.d.ts +503 -0
- package/vendor/claude-code-acp/node_modules/@types/node/dns.d.ts +922 -0
- package/vendor/claude-code-acp/node_modules/@types/node/domain.d.ts +166 -0
- package/vendor/claude-code-acp/node_modules/@types/node/events.d.ts +1054 -0
- package/vendor/claude-code-acp/node_modules/@types/node/fs/promises.d.ts +1329 -0
- package/vendor/claude-code-acp/node_modules/@types/node/fs.d.ts +4676 -0
- package/vendor/claude-code-acp/node_modules/@types/node/globals.d.ts +150 -0
- package/vendor/claude-code-acp/node_modules/@types/node/globals.typedarray.d.ts +101 -0
- package/vendor/claude-code-acp/node_modules/@types/node/http.d.ts +2167 -0
- package/vendor/claude-code-acp/node_modules/@types/node/http2.d.ts +2480 -0
- package/vendor/claude-code-acp/node_modules/@types/node/https.d.ts +405 -0
- package/vendor/claude-code-acp/node_modules/@types/node/index.d.ts +115 -0
- package/vendor/claude-code-acp/node_modules/@types/node/inspector/promises.d.ts +41 -0
- package/vendor/claude-code-acp/node_modules/@types/node/inspector.d.ts +224 -0
- package/vendor/claude-code-acp/node_modules/@types/node/inspector.generated.d.ts +4226 -0
- package/vendor/claude-code-acp/node_modules/@types/node/module.d.ts +819 -0
- package/vendor/claude-code-acp/node_modules/@types/node/net.d.ts +933 -0
- package/vendor/claude-code-acp/node_modules/@types/node/os.d.ts +507 -0
- package/vendor/claude-code-acp/node_modules/@types/node/package.json +155 -0
- package/vendor/claude-code-acp/node_modules/@types/node/path/posix.d.ts +8 -0
- package/vendor/claude-code-acp/node_modules/@types/node/path/win32.d.ts +8 -0
- package/vendor/claude-code-acp/node_modules/@types/node/path.d.ts +187 -0
- package/vendor/claude-code-acp/node_modules/@types/node/perf_hooks.d.ts +643 -0
- package/vendor/claude-code-acp/node_modules/@types/node/process.d.ts +2161 -0
- package/vendor/claude-code-acp/node_modules/@types/node/punycode.d.ts +117 -0
- package/vendor/claude-code-acp/node_modules/@types/node/querystring.d.ts +152 -0
- package/vendor/claude-code-acp/node_modules/@types/node/quic.d.ts +910 -0
- package/vendor/claude-code-acp/node_modules/@types/node/readline/promises.d.ts +161 -0
- package/vendor/claude-code-acp/node_modules/@types/node/readline.d.ts +541 -0
- package/vendor/claude-code-acp/node_modules/@types/node/repl.d.ts +415 -0
- package/vendor/claude-code-acp/node_modules/@types/node/sea.d.ts +162 -0
- package/vendor/claude-code-acp/node_modules/@types/node/sqlite.d.ts +955 -0
- package/vendor/claude-code-acp/node_modules/@types/node/stream/consumers.d.ts +38 -0
- package/vendor/claude-code-acp/node_modules/@types/node/stream/promises.d.ts +211 -0
- package/vendor/claude-code-acp/node_modules/@types/node/stream/web.d.ts +296 -0
- package/vendor/claude-code-acp/node_modules/@types/node/stream.d.ts +1760 -0
- package/vendor/claude-code-acp/node_modules/@types/node/string_decoder.d.ts +67 -0
- package/vendor/claude-code-acp/node_modules/@types/node/test/reporters.d.ts +96 -0
- package/vendor/claude-code-acp/node_modules/@types/node/test.d.ts +2240 -0
- package/vendor/claude-code-acp/node_modules/@types/node/timers/promises.d.ts +108 -0
- package/vendor/claude-code-acp/node_modules/@types/node/timers.d.ts +159 -0
- package/vendor/claude-code-acp/node_modules/@types/node/tls.d.ts +1198 -0
- package/vendor/claude-code-acp/node_modules/@types/node/trace_events.d.ts +197 -0
- package/vendor/claude-code-acp/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +462 -0
- package/vendor/claude-code-acp/node_modules/@types/node/ts5.6/compatibility/float16array.d.ts +71 -0
- package/vendor/claude-code-acp/node_modules/@types/node/ts5.6/globals.typedarray.d.ts +36 -0
- package/vendor/claude-code-acp/node_modules/@types/node/ts5.6/index.d.ts +117 -0
- package/vendor/claude-code-acp/node_modules/@types/node/ts5.7/compatibility/float16array.d.ts +72 -0
- package/vendor/claude-code-acp/node_modules/@types/node/ts5.7/index.d.ts +117 -0
- package/vendor/claude-code-acp/node_modules/@types/node/tty.d.ts +250 -0
- package/vendor/claude-code-acp/node_modules/@types/node/url.d.ts +519 -0
- package/vendor/claude-code-acp/node_modules/@types/node/util/types.d.ts +558 -0
- package/vendor/claude-code-acp/node_modules/@types/node/util.d.ts +1662 -0
- package/vendor/claude-code-acp/node_modules/@types/node/v8.d.ts +983 -0
- package/vendor/claude-code-acp/node_modules/@types/node/vm.d.ts +1208 -0
- package/vendor/claude-code-acp/node_modules/@types/node/wasi.d.ts +202 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/abortcontroller.d.ts +59 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/blob.d.ts +23 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/console.d.ts +9 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/crypto.d.ts +39 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/domexception.d.ts +68 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/encoding.d.ts +11 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/events.d.ts +106 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/fetch.d.ts +69 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/importmeta.d.ts +13 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/messaging.d.ts +23 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/navigator.d.ts +25 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/performance.d.ts +45 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/storage.d.ts +24 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/streams.d.ts +115 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/timers.d.ts +44 -0
- package/vendor/claude-code-acp/node_modules/@types/node/web-globals/url.d.ts +24 -0
- package/vendor/claude-code-acp/node_modules/@types/node/worker_threads.d.ts +717 -0
- package/vendor/claude-code-acp/node_modules/@types/node/zlib.d.ts +618 -0
- package/vendor/claude-code-acp/node_modules/undici-types/LICENSE +21 -0
- package/vendor/claude-code-acp/node_modules/undici-types/README.md +6 -0
- package/vendor/claude-code-acp/node_modules/undici-types/agent.d.ts +32 -0
- package/vendor/claude-code-acp/node_modules/undici-types/api.d.ts +43 -0
- package/vendor/claude-code-acp/node_modules/undici-types/balanced-pool.d.ts +29 -0
- package/vendor/claude-code-acp/node_modules/undici-types/cache-interceptor.d.ts +172 -0
- package/vendor/claude-code-acp/node_modules/undici-types/cache.d.ts +36 -0
- package/vendor/claude-code-acp/node_modules/undici-types/client-stats.d.ts +15 -0
- package/vendor/claude-code-acp/node_modules/undici-types/client.d.ts +108 -0
- package/vendor/claude-code-acp/node_modules/undici-types/connector.d.ts +34 -0
- package/vendor/claude-code-acp/node_modules/undici-types/content-type.d.ts +21 -0
- package/vendor/claude-code-acp/node_modules/undici-types/cookies.d.ts +30 -0
- package/vendor/claude-code-acp/node_modules/undici-types/diagnostics-channel.d.ts +74 -0
- package/vendor/claude-code-acp/node_modules/undici-types/dispatcher.d.ts +276 -0
- package/vendor/claude-code-acp/node_modules/undici-types/env-http-proxy-agent.d.ts +22 -0
- package/vendor/claude-code-acp/node_modules/undici-types/errors.d.ts +161 -0
- package/vendor/claude-code-acp/node_modules/undici-types/eventsource.d.ts +66 -0
- package/vendor/claude-code-acp/node_modules/undici-types/fetch.d.ts +211 -0
- package/vendor/claude-code-acp/node_modules/undici-types/formdata.d.ts +108 -0
- package/vendor/claude-code-acp/node_modules/undici-types/global-dispatcher.d.ts +9 -0
- package/vendor/claude-code-acp/node_modules/undici-types/global-origin.d.ts +7 -0
- package/vendor/claude-code-acp/node_modules/undici-types/h2c-client.d.ts +73 -0
- package/vendor/claude-code-acp/node_modules/undici-types/handlers.d.ts +15 -0
- package/vendor/claude-code-acp/node_modules/undici-types/header.d.ts +160 -0
- package/vendor/claude-code-acp/node_modules/undici-types/index.d.ts +80 -0
- package/vendor/claude-code-acp/node_modules/undici-types/interceptors.d.ts +39 -0
- package/vendor/claude-code-acp/node_modules/undici-types/mock-agent.d.ts +68 -0
- package/vendor/claude-code-acp/node_modules/undici-types/mock-call-history.d.ts +111 -0
- package/vendor/claude-code-acp/node_modules/undici-types/mock-client.d.ts +27 -0
- package/vendor/claude-code-acp/node_modules/undici-types/mock-errors.d.ts +12 -0
- package/vendor/claude-code-acp/node_modules/undici-types/mock-interceptor.d.ts +94 -0
- package/vendor/claude-code-acp/node_modules/undici-types/mock-pool.d.ts +27 -0
- package/vendor/claude-code-acp/node_modules/undici-types/package.json +55 -0
- package/vendor/claude-code-acp/node_modules/undici-types/patch.d.ts +29 -0
- package/vendor/claude-code-acp/node_modules/undici-types/pool-stats.d.ts +19 -0
- package/vendor/claude-code-acp/node_modules/undici-types/pool.d.ts +41 -0
- package/vendor/claude-code-acp/node_modules/undici-types/proxy-agent.d.ts +29 -0
- package/vendor/claude-code-acp/node_modules/undici-types/readable.d.ts +68 -0
- package/vendor/claude-code-acp/node_modules/undici-types/retry-agent.d.ts +8 -0
- package/vendor/claude-code-acp/node_modules/undici-types/retry-handler.d.ts +125 -0
- package/vendor/claude-code-acp/node_modules/undici-types/snapshot-agent.d.ts +109 -0
- package/vendor/claude-code-acp/node_modules/undici-types/util.d.ts +18 -0
- package/vendor/claude-code-acp/node_modules/undici-types/utility.d.ts +7 -0
- package/vendor/claude-code-acp/node_modules/undici-types/webidl.d.ts +341 -0
- package/vendor/claude-code-acp/node_modules/undici-types/websocket.d.ts +186 -0
- package/dist/web/assets/index-B5Dh2E8j.css +0 -1
- package/dist/web/assets/index-xPPPaEde.js +0 -13
|
@@ -4,35 +4,37 @@ import { mkdir, readFile, writeFile, access, readdir, open, stat } from "node:fs
|
|
|
4
4
|
import { watch } from "node:fs";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { ClaudeCliSession } from "./claude-cli-session.js";
|
|
7
|
+
import { ClaudeAcpSession } from "./acp-session.js";
|
|
7
8
|
import { ActivityMonitor, projectDirPath } from "./activity-monitor.js";
|
|
8
9
|
export class SessionManager {
|
|
10
|
+
static INITIAL_TIMELINE_WINDOW = 120;
|
|
11
|
+
static HISTORY_PAGE_SIZE = 120;
|
|
12
|
+
static OUTPUT_BUFFER_MAX = 256 * 1024;
|
|
13
|
+
static DISCOVERY_POLL_MS = 1000;
|
|
14
|
+
static DISCOVERY_MAX_POLLS = 60;
|
|
15
|
+
static HISTORY_DEBOUNCE_MS = 200;
|
|
16
|
+
static HISTORY_POLL_MS = 3000;
|
|
9
17
|
allowedRoots;
|
|
10
18
|
claudeBin;
|
|
19
|
+
agentBinPath;
|
|
11
20
|
allowSkipPermissions;
|
|
12
21
|
stateFilePath;
|
|
13
22
|
sessions = new Map();
|
|
14
23
|
listeners = new Set();
|
|
15
24
|
activityMonitor;
|
|
16
|
-
/** Reverse index: Claude sessionId → clientSessionId */
|
|
17
25
|
sessionIdIndex = new Map();
|
|
18
|
-
|
|
26
|
+
askUserQuestionMap = new Map();
|
|
19
27
|
discoveryTimers = new Map();
|
|
20
|
-
persistTimer = null;
|
|
21
|
-
/** Maximum bytes of PTY output to keep per session for replay on reconnect. */
|
|
22
|
-
static OUTPUT_BUFFER_MAX = 256 * 1024;
|
|
23
|
-
static DISCOVERY_POLL_MS = 1000;
|
|
24
|
-
static DISCOVERY_MAX_POLLS = 60; // ~1 minute
|
|
25
|
-
// history.jsonl monitoring for /clear detection
|
|
26
28
|
historyFilePath;
|
|
27
29
|
historyWatcher = null;
|
|
28
30
|
historyPollTimer = null;
|
|
29
31
|
historyDebounceTimer = null;
|
|
30
32
|
lastHistorySize = 0;
|
|
31
|
-
|
|
32
|
-
static HISTORY_POLL_MS = 3000;
|
|
33
|
+
persistTimer = null;
|
|
33
34
|
constructor(options) {
|
|
34
35
|
this.allowedRoots = options.allowedRoots;
|
|
35
36
|
this.claudeBin = options.claudeBin;
|
|
37
|
+
this.agentBinPath = options.agentBinPath;
|
|
36
38
|
this.allowSkipPermissions = options.allowSkipPermissions ?? false;
|
|
37
39
|
this.stateFilePath = path.join(os.homedir(), ".leduo-patrol", "state.json");
|
|
38
40
|
this.historyFilePath = path.join(process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude"), "history.jsonl");
|
|
@@ -44,12 +46,13 @@ export class SessionManager {
|
|
|
44
46
|
if (!entry)
|
|
45
47
|
return;
|
|
46
48
|
entry.snapshot.activityState = activityState;
|
|
49
|
+
entry.snapshot.updatedAt = new Date().toISOString();
|
|
50
|
+
this.schedulePersist();
|
|
47
51
|
this.emit({
|
|
48
52
|
type: "session_activity",
|
|
49
53
|
payload: { clientSessionId, activityState },
|
|
50
54
|
});
|
|
51
55
|
});
|
|
52
|
-
// Set up plan session detection callback
|
|
53
56
|
this.activityMonitor.onPlanSessionDetected = (newSessionId, promptId, workspacePath) => {
|
|
54
57
|
this.handlePlanSessionDetected(newSessionId, promptId, workspacePath);
|
|
55
58
|
};
|
|
@@ -60,7 +63,6 @@ export class SessionManager {
|
|
|
60
63
|
for (const persisted of persistedState.sessions) {
|
|
61
64
|
if (!(await this.isRestorableWorkspace(persisted.workspacePath))) {
|
|
62
65
|
skippedPersistedSessions = true;
|
|
63
|
-
console.warn(`[SessionManager] Skipping persisted session with unavailable workspace: ${persisted.workspacePath}`);
|
|
64
66
|
continue;
|
|
65
67
|
}
|
|
66
68
|
const snapshot = {
|
|
@@ -70,26 +72,37 @@ export class SessionManager {
|
|
|
70
72
|
connectionState: "connecting",
|
|
71
73
|
activityState: "idle",
|
|
72
74
|
sessionId: persisted.sessionId,
|
|
75
|
+
engine: persisted.engine === "acp" ? "acp" : "cli",
|
|
76
|
+
switchable: true,
|
|
73
77
|
updatedAt: persisted.updatedAt,
|
|
74
78
|
allowSkipPermissions: persisted.allowSkipPermissions,
|
|
79
|
+
acp: persisted.engine === "acp"
|
|
80
|
+
? createEmptyAcpState(persisted.acpDefaultModeId, persisted.acpCurrentModeId)
|
|
81
|
+
: undefined,
|
|
75
82
|
};
|
|
76
|
-
|
|
83
|
+
const entry = {
|
|
77
84
|
snapshot,
|
|
78
85
|
cliSession: null,
|
|
86
|
+
cliExitExpected: false,
|
|
87
|
+
acpSession: null,
|
|
88
|
+
acpFullTimeline: [],
|
|
79
89
|
outputBuffer: "",
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
this.
|
|
90
|
+
switchInProgress: false,
|
|
91
|
+
};
|
|
92
|
+
this.sessions.set(snapshot.clientSessionId, entry);
|
|
93
|
+
if (snapshot.sessionId) {
|
|
94
|
+
this.sessionIdIndex.set(snapshot.sessionId, snapshot.clientSessionId);
|
|
95
|
+
this.activityMonitor.watch(snapshot.sessionId, snapshot.workspacePath);
|
|
96
|
+
}
|
|
83
97
|
}
|
|
84
98
|
if (skippedPersistedSessions) {
|
|
85
99
|
await this.writePersistedState().catch(() => undefined);
|
|
86
100
|
}
|
|
87
101
|
for (const entry of this.sessions.values()) {
|
|
88
|
-
this.
|
|
102
|
+
this.startEngine(entry, Boolean(entry.snapshot.sessionId)).catch((error) => {
|
|
89
103
|
this.handleManagerError(entry.snapshot.clientSessionId, error);
|
|
90
104
|
});
|
|
91
105
|
}
|
|
92
|
-
// Start monitoring ~/.claude/history.jsonl for /clear commands
|
|
93
106
|
await this.startHistoryMonitor();
|
|
94
107
|
}
|
|
95
108
|
subscribe(listener) {
|
|
@@ -101,29 +114,46 @@ export class SessionManager {
|
|
|
101
114
|
getStateSnapshot() {
|
|
102
115
|
return {
|
|
103
116
|
sessions: [...this.sessions.values()]
|
|
104
|
-
.map((entry) => entry
|
|
117
|
+
.map((entry) => this.snapshotForEvent(entry))
|
|
105
118
|
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)),
|
|
106
119
|
};
|
|
107
120
|
}
|
|
121
|
+
getAvailableEngines() {
|
|
122
|
+
return this.agentBinPath ? ["cli", "acp"] : ["cli"];
|
|
123
|
+
}
|
|
124
|
+
getSessionHistory(clientSessionId, before, limit = SessionManager.HISTORY_PAGE_SIZE) {
|
|
125
|
+
const entry = this.getEntry(clientSessionId);
|
|
126
|
+
if (!entry.snapshot.acp) {
|
|
127
|
+
throw new Error("Session history is only available for ACP sessions.");
|
|
128
|
+
}
|
|
129
|
+
const fullTimeline = this.ensureFullTimeline(entry);
|
|
130
|
+
const normalizedLimit = Number.isFinite(limit) ? limit : SessionManager.HISTORY_PAGE_SIZE;
|
|
131
|
+
const normalizedBefore = Number.isFinite(before) ? before : fullTimeline.length;
|
|
132
|
+
const safeLimit = Math.max(1, Math.min(normalizedLimit, SessionManager.HISTORY_PAGE_SIZE));
|
|
133
|
+
const safeBefore = Math.max(0, Math.min(normalizedBefore, fullTimeline.length));
|
|
134
|
+
const start = Math.max(0, safeBefore - safeLimit);
|
|
135
|
+
return {
|
|
136
|
+
items: fullTimeline.slice(start, safeBefore),
|
|
137
|
+
start,
|
|
138
|
+
total: fullTimeline.length,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
108
141
|
getSessionWorkspacePath(clientSessionId) {
|
|
109
142
|
return this.getEntry(clientSessionId).snapshot.workspacePath;
|
|
110
143
|
}
|
|
111
|
-
async createSession(requestedWorkspacePath, requestedTitle, allowSkipPermissions) {
|
|
144
|
+
async createSession(requestedWorkspacePath, requestedTitle, allowSkipPermissions, engine = "cli") {
|
|
145
|
+
if (engine === "acp" && !this.agentBinPath) {
|
|
146
|
+
throw new Error("ACP engine is unavailable. Check LEDUO_PATROL_AGENT_BIN or bundled claude-code-acp.");
|
|
147
|
+
}
|
|
112
148
|
const resolvedWorkspacePath = await this.resolveRequestedWorkspace(requestedWorkspacePath);
|
|
113
149
|
const existingEntry = [...this.sessions.values()].find((entry) => entry.snapshot.workspacePath === resolvedWorkspacePath);
|
|
114
150
|
if (existingEntry) {
|
|
115
151
|
this.emit({
|
|
116
152
|
type: "session_registered",
|
|
117
|
-
payload:
|
|
118
|
-
clientSessionId: existingEntry.snapshot.clientSessionId,
|
|
119
|
-
title: existingEntry.snapshot.title,
|
|
120
|
-
workspacePath: existingEntry.snapshot.workspacePath,
|
|
121
|
-
sessionId: existingEntry.snapshot.sessionId,
|
|
122
|
-
},
|
|
153
|
+
payload: this.snapshotForEvent(existingEntry),
|
|
123
154
|
});
|
|
124
155
|
return existingEntry.snapshot;
|
|
125
156
|
}
|
|
126
|
-
const sessionId = randomUUID();
|
|
127
157
|
const effectiveAllowSkipPermissions = allowSkipPermissions ?? this.allowSkipPermissions;
|
|
128
158
|
const snapshot = {
|
|
129
159
|
clientSessionId: randomUUID(),
|
|
@@ -131,51 +161,204 @@ export class SessionManager {
|
|
|
131
161
|
workspacePath: resolvedWorkspacePath,
|
|
132
162
|
connectionState: "connecting",
|
|
133
163
|
activityState: "idle",
|
|
134
|
-
sessionId,
|
|
164
|
+
sessionId: engine === "cli" ? randomUUID() : "",
|
|
165
|
+
engine,
|
|
166
|
+
switchable: true,
|
|
135
167
|
updatedAt: new Date().toISOString(),
|
|
136
168
|
allowSkipPermissions: effectiveAllowSkipPermissions,
|
|
169
|
+
acp: engine === "acp" ? createEmptyAcpState() : undefined,
|
|
137
170
|
};
|
|
138
171
|
const entry = {
|
|
139
172
|
snapshot,
|
|
140
173
|
cliSession: null,
|
|
174
|
+
cliExitExpected: false,
|
|
175
|
+
acpSession: null,
|
|
176
|
+
acpFullTimeline: [],
|
|
141
177
|
outputBuffer: "",
|
|
178
|
+
switchInProgress: false,
|
|
142
179
|
};
|
|
143
180
|
this.sessions.set(snapshot.clientSessionId, entry);
|
|
144
|
-
|
|
145
|
-
|
|
181
|
+
if (snapshot.sessionId) {
|
|
182
|
+
this.sessionIdIndex.set(snapshot.sessionId, snapshot.clientSessionId);
|
|
183
|
+
this.activityMonitor.watch(snapshot.sessionId, resolvedWorkspacePath);
|
|
184
|
+
}
|
|
146
185
|
this.schedulePersist();
|
|
147
186
|
this.emit({
|
|
148
187
|
type: "session_registered",
|
|
149
|
-
payload:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
188
|
+
payload: this.snapshotForEvent(entry),
|
|
189
|
+
});
|
|
190
|
+
try {
|
|
191
|
+
await this.startEngine(entry, false);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
entry.snapshot.updatedAt = new Date().toISOString();
|
|
195
|
+
this.schedulePersist();
|
|
196
|
+
this.emit({
|
|
197
|
+
type: "session_updated",
|
|
198
|
+
payload: this.snapshotForEvent(entry),
|
|
199
|
+
});
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
this.emit({
|
|
203
|
+
type: "session_updated",
|
|
204
|
+
payload: this.snapshotForEvent(entry),
|
|
155
205
|
});
|
|
156
|
-
await this.startCliSession(entry, false);
|
|
157
206
|
return snapshot;
|
|
158
207
|
}
|
|
208
|
+
async switchEngine(clientSessionId, engine) {
|
|
209
|
+
const entry = this.getEntry(clientSessionId);
|
|
210
|
+
if (entry.snapshot.engine === engine) {
|
|
211
|
+
return entry.snapshot;
|
|
212
|
+
}
|
|
213
|
+
if (engine === "acp" && !this.agentBinPath) {
|
|
214
|
+
throw new Error("ACP engine is unavailable. Check LEDUO_PATROL_AGENT_BIN or bundled claude-code-acp.");
|
|
215
|
+
}
|
|
216
|
+
const blockedReason = this.getSwitchBlockedReason(entry);
|
|
217
|
+
if (blockedReason) {
|
|
218
|
+
throw new Error(`Session is not switchable: ${blockedReason}`);
|
|
219
|
+
}
|
|
220
|
+
const previousEngine = entry.snapshot.engine;
|
|
221
|
+
entry.switchInProgress = true;
|
|
222
|
+
try {
|
|
223
|
+
await this.stopEngine(entry, "switch");
|
|
224
|
+
entry.snapshot.engine = engine;
|
|
225
|
+
if (engine === "acp" && !entry.snapshot.acp) {
|
|
226
|
+
entry.snapshot.acp = createEmptyAcpState();
|
|
227
|
+
}
|
|
228
|
+
entry.snapshot.connectionState = "connecting";
|
|
229
|
+
entry.snapshot.updatedAt = new Date().toISOString();
|
|
230
|
+
this.schedulePersist();
|
|
231
|
+
await this.startEngine(entry, Boolean(entry.snapshot.sessionId));
|
|
232
|
+
entry.switchInProgress = false;
|
|
233
|
+
this.emit({
|
|
234
|
+
type: "session_updated",
|
|
235
|
+
payload: this.snapshotForEvent(entry),
|
|
236
|
+
});
|
|
237
|
+
return entry.snapshot;
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
entry.snapshot.engine = previousEngine;
|
|
241
|
+
entry.snapshot.connectionState = "connecting";
|
|
242
|
+
try {
|
|
243
|
+
await this.startEngine(entry, Boolean(entry.snapshot.sessionId));
|
|
244
|
+
}
|
|
245
|
+
catch (rollbackError) {
|
|
246
|
+
entry.switchInProgress = false;
|
|
247
|
+
this.handleManagerError(clientSessionId, rollbackError);
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
entry.switchInProgress = false;
|
|
251
|
+
this.emit({
|
|
252
|
+
type: "session_updated",
|
|
253
|
+
payload: this.snapshotForEvent(entry),
|
|
254
|
+
});
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
159
258
|
writeToSession(clientSessionId, data) {
|
|
160
259
|
const entry = this.getEntry(clientSessionId);
|
|
260
|
+
if (entry.snapshot.engine !== "cli") {
|
|
261
|
+
throw new Error("CLI input is only available for CLI sessions.");
|
|
262
|
+
}
|
|
161
263
|
entry.cliSession?.write(data);
|
|
162
264
|
}
|
|
163
265
|
resizeCliSession(clientSessionId, cols, rows) {
|
|
164
266
|
const entry = this.getEntry(clientSessionId);
|
|
267
|
+
if (entry.snapshot.engine !== "cli") {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
165
270
|
entry.cliSession?.resize(cols, rows);
|
|
166
271
|
}
|
|
167
|
-
/** Return buffered PTY output so a reconnecting client can replay history. */
|
|
168
272
|
getSessionOutputBuffer(clientSessionId) {
|
|
169
273
|
const entry = this.getEntry(clientSessionId);
|
|
170
274
|
return entry.outputBuffer;
|
|
171
275
|
}
|
|
276
|
+
async prompt(clientSessionId, text, modeId, images) {
|
|
277
|
+
const entry = this.getEntry(clientSessionId);
|
|
278
|
+
if (entry.snapshot.engine !== "acp") {
|
|
279
|
+
throw new Error("Prompting is only available in ACP mode.");
|
|
280
|
+
}
|
|
281
|
+
if (!entry.acpSession) {
|
|
282
|
+
await this.startEngine(entry, Boolean(entry.snapshot.sessionId));
|
|
283
|
+
}
|
|
284
|
+
const acpState = this.ensureAcpState(entry);
|
|
285
|
+
const effectiveModeId = modeId || acpState.defaultModeId;
|
|
286
|
+
if (effectiveModeId) {
|
|
287
|
+
await entry.acpSession?.setMode(effectiveModeId);
|
|
288
|
+
acpState.currentModeId = effectiveModeId;
|
|
289
|
+
}
|
|
290
|
+
entry.snapshot.updatedAt = new Date().toISOString();
|
|
291
|
+
this.schedulePersist();
|
|
292
|
+
await entry.acpSession?.prompt(text, images);
|
|
293
|
+
}
|
|
294
|
+
async setSessionMode(clientSessionId, modeId) {
|
|
295
|
+
const entry = this.getEntry(clientSessionId);
|
|
296
|
+
if (entry.snapshot.engine !== "acp") {
|
|
297
|
+
throw new Error("Session modes are only available in ACP mode.");
|
|
298
|
+
}
|
|
299
|
+
if (!modeId) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!entry.acpSession) {
|
|
303
|
+
await this.startEngine(entry, Boolean(entry.snapshot.sessionId));
|
|
304
|
+
}
|
|
305
|
+
const acpState = this.ensureAcpState(entry);
|
|
306
|
+
await entry.acpSession?.setMode(modeId);
|
|
307
|
+
acpState.defaultModeId = modeId;
|
|
308
|
+
acpState.currentModeId = modeId;
|
|
309
|
+
entry.snapshot.updatedAt = new Date().toISOString();
|
|
310
|
+
this.schedulePersist();
|
|
311
|
+
this.emit({
|
|
312
|
+
type: "session_mode_changed",
|
|
313
|
+
payload: {
|
|
314
|
+
clientSessionId,
|
|
315
|
+
defaultModeId: acpState.defaultModeId,
|
|
316
|
+
currentModeId: acpState.currentModeId,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
async cancel(clientSessionId) {
|
|
321
|
+
const entry = this.getEntry(clientSessionId);
|
|
322
|
+
if (entry.snapshot.engine !== "acp") {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
await entry.acpSession?.cancel();
|
|
326
|
+
}
|
|
327
|
+
async resolvePermission(clientSessionId, requestId, optionId, note) {
|
|
328
|
+
const entry = this.getEntry(clientSessionId);
|
|
329
|
+
if (entry.snapshot.engine !== "acp") {
|
|
330
|
+
throw new Error("Permissions are only available in ACP mode.");
|
|
331
|
+
}
|
|
332
|
+
await entry.acpSession?.resolvePermission(requestId, optionId, note);
|
|
333
|
+
}
|
|
334
|
+
async answerQuestion(clientSessionId, questionId, answer) {
|
|
335
|
+
const entry = this.getEntry(clientSessionId);
|
|
336
|
+
if (entry.snapshot.engine !== "acp") {
|
|
337
|
+
throw new Error("Questions are only available in ACP mode.");
|
|
338
|
+
}
|
|
339
|
+
const mappedPermission = this.askUserQuestionMap.get(questionId);
|
|
340
|
+
if (mappedPermission) {
|
|
341
|
+
const siblingIds = [];
|
|
342
|
+
for (const [qId, mapping] of this.askUserQuestionMap.entries()) {
|
|
343
|
+
if (mapping.requestId === mappedPermission.requestId) {
|
|
344
|
+
siblingIds.push(qId);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
for (const qId of siblingIds) {
|
|
348
|
+
this.askUserQuestionMap.delete(qId);
|
|
349
|
+
}
|
|
350
|
+
await entry.acpSession?.resolvePermission(mappedPermission.requestId, "deny", answer);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
await entry.acpSession?.answerQuestion(questionId, answer);
|
|
354
|
+
}
|
|
172
355
|
async closeSession(clientSessionId) {
|
|
173
356
|
const entry = this.sessions.get(clientSessionId);
|
|
174
357
|
if (!entry) {
|
|
175
358
|
return;
|
|
176
359
|
}
|
|
177
360
|
this.clearDiscoveryTimer(clientSessionId);
|
|
178
|
-
|
|
361
|
+
await this.stopEngine(entry, "close");
|
|
179
362
|
this.activityMonitor.unwatch(entry.snapshot.sessionId);
|
|
180
363
|
this.sessionIdIndex.delete(entry.snapshot.sessionId);
|
|
181
364
|
this.sessions.delete(clientSessionId);
|
|
@@ -185,8 +368,36 @@ export class SessionManager {
|
|
|
185
368
|
payload: { clientSessionId },
|
|
186
369
|
});
|
|
187
370
|
}
|
|
371
|
+
async startEngine(entry, resume) {
|
|
372
|
+
entry.snapshot.connectionState = "connecting";
|
|
373
|
+
entry.snapshot.updatedAt = new Date().toISOString();
|
|
374
|
+
this.schedulePersist();
|
|
375
|
+
if (entry.snapshot.engine === "cli") {
|
|
376
|
+
await this.startCliSession(entry, resume);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
await this.startAcpSession(entry, resume);
|
|
380
|
+
}
|
|
381
|
+
async stopEngine(entry, reason) {
|
|
382
|
+
if (reason === "switch") {
|
|
383
|
+
entry.outputBuffer = "";
|
|
384
|
+
}
|
|
385
|
+
if (entry.cliSession) {
|
|
386
|
+
entry.cliExitExpected = true;
|
|
387
|
+
entry.cliSession.kill();
|
|
388
|
+
entry.cliSession = null;
|
|
389
|
+
}
|
|
390
|
+
if (entry.acpSession) {
|
|
391
|
+
await entry.acpSession.dispose();
|
|
392
|
+
entry.acpSession = null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
188
395
|
async startCliSession(entry, resume) {
|
|
189
396
|
const { snapshot } = entry;
|
|
397
|
+
if (!snapshot.sessionId) {
|
|
398
|
+
snapshot.sessionId = randomUUID();
|
|
399
|
+
this.bindSessionId(snapshot.clientSessionId, "", snapshot.sessionId, snapshot.workspacePath, false);
|
|
400
|
+
}
|
|
190
401
|
try {
|
|
191
402
|
const cliSession = new ClaudeCliSession({
|
|
192
403
|
workspacePath: snapshot.workspacePath,
|
|
@@ -196,8 +407,8 @@ export class SessionManager {
|
|
|
196
407
|
allowSkipPermissions: snapshot.allowSkipPermissions,
|
|
197
408
|
});
|
|
198
409
|
entry.cliSession = cliSession;
|
|
410
|
+
entry.cliExitExpected = false;
|
|
199
411
|
cliSession.on("output", (data) => {
|
|
200
|
-
// Append to ring buffer so reconnecting clients can replay history
|
|
201
412
|
entry.outputBuffer += data;
|
|
202
413
|
if (entry.outputBuffer.length > SessionManager.OUTPUT_BUFFER_MAX) {
|
|
203
414
|
entry.outputBuffer = entry.outputBuffer.slice(-SessionManager.OUTPUT_BUFFER_MAX);
|
|
@@ -208,6 +419,12 @@ export class SessionManager {
|
|
|
208
419
|
});
|
|
209
420
|
});
|
|
210
421
|
cliSession.on("exit", (exitCode) => {
|
|
422
|
+
const expected = entry.cliExitExpected;
|
|
423
|
+
entry.cliExitExpected = false;
|
|
424
|
+
entry.cliSession = null;
|
|
425
|
+
if (expected) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
211
428
|
this.clearDiscoveryTimer(snapshot.clientSessionId);
|
|
212
429
|
snapshot.connectionState = "error";
|
|
213
430
|
snapshot.updatedAt = new Date().toISOString();
|
|
@@ -226,11 +443,559 @@ export class SessionManager {
|
|
|
226
443
|
throw error;
|
|
227
444
|
}
|
|
228
445
|
}
|
|
446
|
+
async startAcpSession(entry, resume) {
|
|
447
|
+
if (!this.agentBinPath) {
|
|
448
|
+
throw new Error("ACP engine is unavailable.");
|
|
449
|
+
}
|
|
450
|
+
const acpSession = new ClaudeAcpSession({
|
|
451
|
+
workspacePath: entry.snapshot.workspacePath,
|
|
452
|
+
agentBinPath: this.agentBinPath,
|
|
453
|
+
claudeBin: this.claudeBin,
|
|
454
|
+
onEvent: (event) => this.handleAcpSessionEvent(entry.snapshot.clientSessionId, event),
|
|
455
|
+
});
|
|
456
|
+
entry.acpSession = acpSession;
|
|
457
|
+
await acpSession.connect();
|
|
458
|
+
if (resume && entry.snapshot.sessionId) {
|
|
459
|
+
entry.acpFullTimeline = [];
|
|
460
|
+
this.syncVisibleTimeline(entry);
|
|
461
|
+
const restorableSessionId = await acpSession.findRestorableSession(entry.snapshot.sessionId);
|
|
462
|
+
if (restorableSessionId) {
|
|
463
|
+
if (restorableSessionId !== entry.snapshot.sessionId) {
|
|
464
|
+
this.bindSessionId(entry.snapshot.clientSessionId, entry.snapshot.sessionId, restorableSessionId, entry.snapshot.workspacePath, true);
|
|
465
|
+
}
|
|
466
|
+
await acpSession.loadSession(restorableSessionId);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
await acpSession.ensureSession();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
await acpSession.ensureSession();
|
|
474
|
+
}
|
|
475
|
+
const acpState = this.ensureAcpState(entry);
|
|
476
|
+
if (acpState.defaultModeId && acpState.currentModeId !== acpState.defaultModeId) {
|
|
477
|
+
await acpSession.setMode(acpState.defaultModeId);
|
|
478
|
+
acpState.currentModeId = acpState.defaultModeId;
|
|
479
|
+
}
|
|
480
|
+
entry.snapshot.connectionState = "connected";
|
|
481
|
+
entry.snapshot.updatedAt = new Date().toISOString();
|
|
482
|
+
this.schedulePersist();
|
|
483
|
+
}
|
|
484
|
+
handleAcpSessionEvent(clientSessionId, event) {
|
|
485
|
+
const entry = this.sessions.get(clientSessionId);
|
|
486
|
+
if (!entry) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const acpState = this.ensureAcpState(entry);
|
|
490
|
+
let shouldEmitFullSnapshot = false;
|
|
491
|
+
switch (event.type) {
|
|
492
|
+
case "ready":
|
|
493
|
+
entry.snapshot.connectionState = "connected";
|
|
494
|
+
entry.snapshot.workspacePath = event.payload.workspacePath;
|
|
495
|
+
this.appendTimeline(entry, {
|
|
496
|
+
id: randomUUID(),
|
|
497
|
+
kind: "system",
|
|
498
|
+
title: "Claude ACP 已连接",
|
|
499
|
+
body: event.payload.workspacePath,
|
|
500
|
+
});
|
|
501
|
+
shouldEmitFullSnapshot = true;
|
|
502
|
+
break;
|
|
503
|
+
case "session_created":
|
|
504
|
+
this.bindSessionId(clientSessionId, entry.snapshot.sessionId, event.payload.sessionId, entry.snapshot.workspacePath, false);
|
|
505
|
+
entry.snapshot.connectionState = "connected";
|
|
506
|
+
acpState.modes = event.payload.modes;
|
|
507
|
+
acpState.currentModeId = acpState.currentModeId || acpState.defaultModeId || "default";
|
|
508
|
+
this.appendTimeline(entry, {
|
|
509
|
+
id: randomUUID(),
|
|
510
|
+
kind: "system",
|
|
511
|
+
title: "会话已创建",
|
|
512
|
+
body: event.payload.sessionId,
|
|
513
|
+
meta: labelForMode(acpState.currentModeId || acpState.defaultModeId),
|
|
514
|
+
});
|
|
515
|
+
shouldEmitFullSnapshot = true;
|
|
516
|
+
break;
|
|
517
|
+
case "session_restored":
|
|
518
|
+
this.bindSessionId(clientSessionId, entry.snapshot.sessionId, event.payload.sessionId, entry.snapshot.workspacePath, false);
|
|
519
|
+
entry.snapshot.connectionState = "connected";
|
|
520
|
+
acpState.modes = event.payload.modes;
|
|
521
|
+
this.appendTimeline(entry, {
|
|
522
|
+
id: randomUUID(),
|
|
523
|
+
kind: "system",
|
|
524
|
+
title: "会话已恢复",
|
|
525
|
+
body: event.payload.sessionId,
|
|
526
|
+
meta: labelForMode(acpState.currentModeId || acpState.defaultModeId),
|
|
527
|
+
});
|
|
528
|
+
shouldEmitFullSnapshot = true;
|
|
529
|
+
break;
|
|
530
|
+
case "prompt_started":
|
|
531
|
+
acpState.busy = true;
|
|
532
|
+
this.appendTimeline(entry, {
|
|
533
|
+
id: event.payload.promptId,
|
|
534
|
+
kind: "user",
|
|
535
|
+
title: "你",
|
|
536
|
+
body: event.payload.text,
|
|
537
|
+
});
|
|
538
|
+
break;
|
|
539
|
+
case "prompt_finished":
|
|
540
|
+
acpState.busy = false;
|
|
541
|
+
this.appendTimeline(entry, {
|
|
542
|
+
id: randomUUID(),
|
|
543
|
+
kind: "system",
|
|
544
|
+
title: "本轮完成",
|
|
545
|
+
body: event.payload.stopReason,
|
|
546
|
+
});
|
|
547
|
+
break;
|
|
548
|
+
case "session_update":
|
|
549
|
+
this.consumeSessionUpdate(entry, event.payload);
|
|
550
|
+
break;
|
|
551
|
+
case "permission_requested": {
|
|
552
|
+
const normalizedTitle = normalizeAcpToolTitle(event.payload.toolCall.title) || undefined;
|
|
553
|
+
if (isAskUserQuestionTitle(normalizedTitle)) {
|
|
554
|
+
const rawInput = asRecord(event.payload.toolCall.rawInput);
|
|
555
|
+
const rawQuestions = Array.isArray(rawInput?.questions) ? rawInput.questions : [];
|
|
556
|
+
const parsedQuestions = [];
|
|
557
|
+
if (rawQuestions.length > 0) {
|
|
558
|
+
for (const rawQ of rawQuestions) {
|
|
559
|
+
const q = asRecord(rawQ);
|
|
560
|
+
if (!q)
|
|
561
|
+
continue;
|
|
562
|
+
const questionStr = typeof q.question === "string" ? q.question : "";
|
|
563
|
+
const headerStr = typeof q.header === "string" ? q.header : undefined;
|
|
564
|
+
const rawOpts = Array.isArray(q.options) ? q.options : [];
|
|
565
|
+
const options = rawOpts
|
|
566
|
+
.map((opt) => {
|
|
567
|
+
const o = asRecord(opt);
|
|
568
|
+
if (!o)
|
|
569
|
+
return null;
|
|
570
|
+
const label = typeof o.label === "string" ? o.label : "";
|
|
571
|
+
const description = typeof o.description === "string" ? o.description : undefined;
|
|
572
|
+
return label ? { id: label, label, description } : null;
|
|
573
|
+
})
|
|
574
|
+
.filter((o) => o !== null);
|
|
575
|
+
parsedQuestions.push({
|
|
576
|
+
question: questionStr,
|
|
577
|
+
header: headerStr,
|
|
578
|
+
options,
|
|
579
|
+
allowCustomAnswer: true,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
const questionText = typeof rawInput?.question === "string" ? rawInput.question : "";
|
|
585
|
+
parsedQuestions.push({
|
|
586
|
+
question: questionText,
|
|
587
|
+
header: undefined,
|
|
588
|
+
options: [],
|
|
589
|
+
allowCustomAnswer: true,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
const groupId = randomUUID();
|
|
593
|
+
const questionIds = [];
|
|
594
|
+
for (const pq of parsedQuestions) {
|
|
595
|
+
const questionId = randomUUID();
|
|
596
|
+
questionIds.push(questionId);
|
|
597
|
+
const questionSnapshot = {
|
|
598
|
+
clientSessionId,
|
|
599
|
+
questionId,
|
|
600
|
+
groupId,
|
|
601
|
+
question: pq.question,
|
|
602
|
+
header: pq.header,
|
|
603
|
+
options: pq.options,
|
|
604
|
+
allowCustomAnswer: true,
|
|
605
|
+
};
|
|
606
|
+
acpState.questions.push(questionSnapshot);
|
|
607
|
+
this.appendTimeline(entry, {
|
|
608
|
+
id: questionId,
|
|
609
|
+
kind: "system",
|
|
610
|
+
title: "提问",
|
|
611
|
+
body: pq.header ? `【${pq.header}】${pq.question}` : pq.question,
|
|
612
|
+
meta: "pending",
|
|
613
|
+
});
|
|
614
|
+
this.askUserQuestionMap.set(questionId, {
|
|
615
|
+
clientSessionId,
|
|
616
|
+
requestId: event.payload.requestId,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
entry.snapshot.updatedAt = new Date().toISOString();
|
|
620
|
+
this.schedulePersist();
|
|
621
|
+
for (let i = 0; i < parsedQuestions.length; i += 1) {
|
|
622
|
+
const pq = parsedQuestions[i];
|
|
623
|
+
this.emit({
|
|
624
|
+
type: "question_requested",
|
|
625
|
+
payload: {
|
|
626
|
+
clientSessionId,
|
|
627
|
+
questionId: questionIds[i],
|
|
628
|
+
groupId,
|
|
629
|
+
question: pq.question,
|
|
630
|
+
header: pq.header,
|
|
631
|
+
options: pq.options,
|
|
632
|
+
allowCustomAnswer: true,
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const permission = {
|
|
639
|
+
clientSessionId,
|
|
640
|
+
requestId: event.payload.requestId,
|
|
641
|
+
toolCall: {
|
|
642
|
+
toolCallId: event.payload.toolCall.toolCallId,
|
|
643
|
+
title: normalizedTitle,
|
|
644
|
+
status: event.payload.toolCall.status ?? undefined,
|
|
645
|
+
rawInput: event.payload.toolCall.rawInput,
|
|
646
|
+
},
|
|
647
|
+
options: event.payload.options.map((option) => ({
|
|
648
|
+
optionId: option.optionId,
|
|
649
|
+
name: option.name,
|
|
650
|
+
kind: option.kind,
|
|
651
|
+
})),
|
|
652
|
+
};
|
|
653
|
+
acpState.permissions.push(permission);
|
|
654
|
+
this.appendTimeline(entry, {
|
|
655
|
+
id: event.payload.requestId,
|
|
656
|
+
kind: "tool",
|
|
657
|
+
title: summarizeToolTitle(normalizedTitle, event.payload.toolCall.rawInput, event.payload.toolCall.toolCallId),
|
|
658
|
+
body: formatToolDetails({
|
|
659
|
+
toolCallId: event.payload.toolCall.toolCallId,
|
|
660
|
+
title: normalizedTitle,
|
|
661
|
+
status: event.payload.toolCall.status,
|
|
662
|
+
rawInput: event.payload.toolCall.rawInput,
|
|
663
|
+
}),
|
|
664
|
+
meta: event.payload.toolCall.status ?? "pending",
|
|
665
|
+
});
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
case "permission_resolved":
|
|
669
|
+
acpState.permissions = acpState.permissions.filter((permission) => permission.requestId !== event.payload.requestId);
|
|
670
|
+
entry.snapshot.acp = { ...acpState };
|
|
671
|
+
break;
|
|
672
|
+
case "question_requested": {
|
|
673
|
+
const questionSnapshot = {
|
|
674
|
+
clientSessionId,
|
|
675
|
+
questionId: event.payload.questionId,
|
|
676
|
+
question: event.payload.question,
|
|
677
|
+
options: event.payload.options.map((opt) => ({
|
|
678
|
+
id: opt.id,
|
|
679
|
+
label: opt.label,
|
|
680
|
+
})),
|
|
681
|
+
allowCustomAnswer: event.payload.allowCustomAnswer,
|
|
682
|
+
};
|
|
683
|
+
acpState.questions.push(questionSnapshot);
|
|
684
|
+
this.appendTimeline(entry, {
|
|
685
|
+
id: event.payload.questionId,
|
|
686
|
+
kind: "system",
|
|
687
|
+
title: "提问",
|
|
688
|
+
body: event.payload.question,
|
|
689
|
+
meta: "pending",
|
|
690
|
+
});
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
case "question_answered":
|
|
694
|
+
acpState.questions = acpState.questions.filter((question) => question.questionId !== event.payload.questionId);
|
|
695
|
+
entry.snapshot.acp = { ...acpState };
|
|
696
|
+
break;
|
|
697
|
+
case "error": {
|
|
698
|
+
const editChangeMessage = formatEditToolChangeMessage(event.payload.message);
|
|
699
|
+
if (editChangeMessage) {
|
|
700
|
+
this.appendTimeline(entry, {
|
|
701
|
+
id: randomUUID(),
|
|
702
|
+
kind: "tool",
|
|
703
|
+
title: editChangeMessage.title,
|
|
704
|
+
body: editChangeMessage.body,
|
|
705
|
+
meta: "completed",
|
|
706
|
+
});
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
if (event.payload.fatal) {
|
|
710
|
+
acpState.busy = false;
|
|
711
|
+
entry.snapshot.connectionState = "error";
|
|
712
|
+
shouldEmitFullSnapshot = true;
|
|
713
|
+
}
|
|
714
|
+
this.appendTimeline(entry, {
|
|
715
|
+
id: randomUUID(),
|
|
716
|
+
kind: "error",
|
|
717
|
+
title: event.payload.fatal ? "错误" : "警告",
|
|
718
|
+
body: event.payload.message,
|
|
719
|
+
});
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
entry.snapshot.updatedAt = new Date().toISOString();
|
|
724
|
+
this.schedulePersist();
|
|
725
|
+
if (shouldEmitFullSnapshot) {
|
|
726
|
+
this.emitSessionUpdated(entry);
|
|
727
|
+
}
|
|
728
|
+
switch (event.type) {
|
|
729
|
+
case "prompt_started":
|
|
730
|
+
this.emit({
|
|
731
|
+
type: "prompt_started",
|
|
732
|
+
payload: {
|
|
733
|
+
clientSessionId,
|
|
734
|
+
promptId: event.payload.promptId,
|
|
735
|
+
text: event.payload.text,
|
|
736
|
+
},
|
|
737
|
+
});
|
|
738
|
+
break;
|
|
739
|
+
case "prompt_finished":
|
|
740
|
+
this.emit({
|
|
741
|
+
type: "prompt_finished",
|
|
742
|
+
payload: {
|
|
743
|
+
clientSessionId,
|
|
744
|
+
promptId: event.payload.promptId,
|
|
745
|
+
stopReason: event.payload.stopReason,
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
break;
|
|
749
|
+
case "session_update":
|
|
750
|
+
this.emit({
|
|
751
|
+
type: "session_update",
|
|
752
|
+
payload: {
|
|
753
|
+
clientSessionId,
|
|
754
|
+
...event.payload,
|
|
755
|
+
sessionUpdate: event.payload.sessionUpdate,
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
break;
|
|
759
|
+
case "permission_requested":
|
|
760
|
+
this.emit({
|
|
761
|
+
type: "permission_requested",
|
|
762
|
+
payload: {
|
|
763
|
+
clientSessionId,
|
|
764
|
+
requestId: event.payload.requestId,
|
|
765
|
+
toolCall: {
|
|
766
|
+
toolCallId: event.payload.toolCall.toolCallId,
|
|
767
|
+
title: event.payload.toolCall.title ?? undefined,
|
|
768
|
+
status: event.payload.toolCall.status ?? undefined,
|
|
769
|
+
rawInput: event.payload.toolCall.rawInput,
|
|
770
|
+
},
|
|
771
|
+
options: event.payload.options.map((option) => ({
|
|
772
|
+
optionId: option.optionId,
|
|
773
|
+
name: option.name,
|
|
774
|
+
kind: option.kind,
|
|
775
|
+
})),
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
break;
|
|
779
|
+
case "permission_resolved":
|
|
780
|
+
this.emit({
|
|
781
|
+
type: "permission_resolved",
|
|
782
|
+
payload: {
|
|
783
|
+
clientSessionId,
|
|
784
|
+
requestId: event.payload.requestId,
|
|
785
|
+
optionId: event.payload.optionId,
|
|
786
|
+
},
|
|
787
|
+
});
|
|
788
|
+
break;
|
|
789
|
+
case "question_requested":
|
|
790
|
+
this.emit({
|
|
791
|
+
type: "question_requested",
|
|
792
|
+
payload: {
|
|
793
|
+
clientSessionId,
|
|
794
|
+
questionId: event.payload.questionId,
|
|
795
|
+
question: event.payload.question,
|
|
796
|
+
options: event.payload.options.map((option) => ({
|
|
797
|
+
id: option.id,
|
|
798
|
+
label: option.label,
|
|
799
|
+
})),
|
|
800
|
+
allowCustomAnswer: event.payload.allowCustomAnswer,
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
break;
|
|
804
|
+
case "question_answered":
|
|
805
|
+
this.emit({
|
|
806
|
+
type: "question_answered",
|
|
807
|
+
payload: {
|
|
808
|
+
clientSessionId,
|
|
809
|
+
questionId: event.payload.questionId,
|
|
810
|
+
answer: event.payload.answer,
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
break;
|
|
814
|
+
case "error":
|
|
815
|
+
this.emit({
|
|
816
|
+
type: "error",
|
|
817
|
+
payload: {
|
|
818
|
+
clientSessionId,
|
|
819
|
+
message: event.payload.message,
|
|
820
|
+
fatal: event.payload.fatal,
|
|
821
|
+
},
|
|
822
|
+
});
|
|
823
|
+
break;
|
|
824
|
+
default:
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
consumeSessionUpdate(entry, update) {
|
|
829
|
+
const acpState = this.ensureAcpState(entry);
|
|
830
|
+
switch (update.sessionUpdate) {
|
|
831
|
+
case "available_commands_update":
|
|
832
|
+
acpState.availableCommands = normalizeAvailableCommandsSnapshot(update.availableCommands ?? update.supportedCommands ?? update.commands);
|
|
833
|
+
entry.snapshot.acp = { ...acpState };
|
|
834
|
+
break;
|
|
835
|
+
case "agent_message_chunk": {
|
|
836
|
+
const chunkText = extractChunkText(update.content);
|
|
837
|
+
if (chunkText) {
|
|
838
|
+
const parentId = extractParentToolCallId(update);
|
|
839
|
+
this.appendTextChunk(entry, "agent", "Claude", chunkText, parentId);
|
|
840
|
+
}
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
case "agent_thought_chunk": {
|
|
844
|
+
const chunkText = extractChunkText(update.content);
|
|
845
|
+
if (chunkText) {
|
|
846
|
+
const parentId = extractParentToolCallId(update);
|
|
847
|
+
this.appendTextChunk(entry, "thought", "思路", chunkText, parentId);
|
|
848
|
+
}
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
case "tool_call":
|
|
852
|
+
case "tool_call_update": {
|
|
853
|
+
const claudeCodeMeta = asRecord(asRecord(update._meta)?.claudeCode);
|
|
854
|
+
const metaToolName = typeof claudeCodeMeta?.toolName === "string" ? claudeCodeMeta.toolName : undefined;
|
|
855
|
+
const normalizedTitle = normalizeAcpToolTitle(update.title) || normalizeAcpToolTitle(metaToolName) || undefined;
|
|
856
|
+
const parentToolCallId = extractParentToolCallId(update);
|
|
857
|
+
const effectiveStatus = isAskUserQuestionTitle(normalizedTitle) && update.status === "failed"
|
|
858
|
+
? "completed"
|
|
859
|
+
: update.status;
|
|
860
|
+
this.appendTimeline(entry, {
|
|
861
|
+
id: randomUUID(),
|
|
862
|
+
kind: "tool",
|
|
863
|
+
title: summarizeToolTitle(normalizedTitle, update.rawInput, update.toolCallId),
|
|
864
|
+
body: formatToolDetails({
|
|
865
|
+
toolCallId: update.toolCallId,
|
|
866
|
+
title: normalizedTitle,
|
|
867
|
+
status: effectiveStatus,
|
|
868
|
+
rawInput: update.rawInput,
|
|
869
|
+
rawOutput: update.rawOutput,
|
|
870
|
+
parentToolCallId,
|
|
871
|
+
}),
|
|
872
|
+
meta: String(effectiveStatus ?? update.sessionUpdate),
|
|
873
|
+
});
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
case "plan": {
|
|
877
|
+
const parentToolCallId = extractParentToolCallId(update);
|
|
878
|
+
this.appendTimeline(entry, {
|
|
879
|
+
id: randomUUID(),
|
|
880
|
+
kind: "system",
|
|
881
|
+
title: "执行计划",
|
|
882
|
+
body: stringifyMaybe(update.entries ?? update),
|
|
883
|
+
parentToolCallId,
|
|
884
|
+
});
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
case "current_mode_update":
|
|
888
|
+
acpState.currentModeId = String(update.currentModeId ?? acpState.currentModeId ?? "default");
|
|
889
|
+
this.appendTimeline(entry, {
|
|
890
|
+
id: randomUUID(),
|
|
891
|
+
kind: "system",
|
|
892
|
+
title: "模式切换",
|
|
893
|
+
body: String(update.currentModeId ?? "unknown"),
|
|
894
|
+
});
|
|
895
|
+
break;
|
|
896
|
+
default:
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
ensureAcpState(entry) {
|
|
901
|
+
if (!entry.snapshot.acp) {
|
|
902
|
+
entry.snapshot.acp = createEmptyAcpState();
|
|
903
|
+
}
|
|
904
|
+
return entry.snapshot.acp;
|
|
905
|
+
}
|
|
906
|
+
appendTextChunk(entry, kind, title, text, parentToolCallId) {
|
|
907
|
+
const fullTimeline = this.ensureFullTimeline(entry);
|
|
908
|
+
const lastItem = fullTimeline.at(-1);
|
|
909
|
+
if (lastItem &&
|
|
910
|
+
lastItem.kind === kind &&
|
|
911
|
+
lastItem.title === title &&
|
|
912
|
+
!lastItem.meta &&
|
|
913
|
+
(lastItem.parentToolCallId ?? undefined) === parentToolCallId) {
|
|
914
|
+
lastItem.body += text;
|
|
915
|
+
this.syncVisibleTimeline(entry);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
this.appendTimeline(entry, {
|
|
919
|
+
id: randomUUID(),
|
|
920
|
+
kind,
|
|
921
|
+
title,
|
|
922
|
+
body: text,
|
|
923
|
+
parentToolCallId,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
appendTimeline(entry, item) {
|
|
927
|
+
this.ensureFullTimeline(entry).push(item);
|
|
928
|
+
this.syncVisibleTimeline(entry);
|
|
929
|
+
}
|
|
930
|
+
syncVisibleTimeline(entry) {
|
|
931
|
+
const acpState = this.ensureAcpState(entry);
|
|
932
|
+
const fullTimeline = this.ensureFullTimeline(entry);
|
|
933
|
+
const total = fullTimeline.length;
|
|
934
|
+
const start = Math.max(0, total - SessionManager.INITIAL_TIMELINE_WINDOW);
|
|
935
|
+
acpState.timeline = fullTimeline.slice(start);
|
|
936
|
+
acpState.historyTotal = total;
|
|
937
|
+
acpState.historyStart = start;
|
|
938
|
+
}
|
|
939
|
+
ensureFullTimeline(entry) {
|
|
940
|
+
if (!Array.isArray(entry.acpFullTimeline)) {
|
|
941
|
+
entry.acpFullTimeline = [];
|
|
942
|
+
}
|
|
943
|
+
return entry.acpFullTimeline;
|
|
944
|
+
}
|
|
229
945
|
emit(event) {
|
|
230
946
|
for (const listener of this.listeners) {
|
|
231
947
|
listener(event);
|
|
232
948
|
}
|
|
233
949
|
}
|
|
950
|
+
emitSessionUpdated(entry) {
|
|
951
|
+
this.emit({
|
|
952
|
+
type: "session_updated",
|
|
953
|
+
payload: this.snapshotForEvent(entry),
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
snapshotForEvent(entry) {
|
|
957
|
+
const blockedReason = this.getSwitchBlockedReason(entry);
|
|
958
|
+
const snapshot = structuredClone(entry.snapshot);
|
|
959
|
+
snapshot.switchable = !blockedReason;
|
|
960
|
+
snapshot.switchBlockedReason = blockedReason ?? undefined;
|
|
961
|
+
return snapshot;
|
|
962
|
+
}
|
|
963
|
+
getSwitchBlockedReason(entry) {
|
|
964
|
+
if (entry.switchInProgress) {
|
|
965
|
+
return "切换中";
|
|
966
|
+
}
|
|
967
|
+
if (entry.snapshot.connectionState === "connecting") {
|
|
968
|
+
return "连接中";
|
|
969
|
+
}
|
|
970
|
+
if (entry.snapshot.engine === "cli") {
|
|
971
|
+
if (entry.snapshot.activityState === "running")
|
|
972
|
+
return "运行中";
|
|
973
|
+
if (entry.snapshot.activityState === "pending")
|
|
974
|
+
return "待处理";
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
const acpState = entry.snapshot.acp;
|
|
978
|
+
if (!acpState) {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
if (acpState.permissions.length > 0)
|
|
982
|
+
return "待审批";
|
|
983
|
+
if (acpState.questions.length > 0)
|
|
984
|
+
return "待提问";
|
|
985
|
+
if (this.isAcpBusy(acpState))
|
|
986
|
+
return "运行中";
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
isAcpBusy(acpState) {
|
|
990
|
+
if (!acpState.busy) {
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
const latestItem = acpState.timeline.at(-1);
|
|
994
|
+
if (latestItem?.kind === "system" && latestItem.title === "本轮完成") {
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
return true;
|
|
998
|
+
}
|
|
234
999
|
schedulePersist() {
|
|
235
1000
|
if (this.persistTimer) {
|
|
236
1001
|
clearTimeout(this.persistTimer);
|
|
@@ -246,8 +1011,11 @@ export class SessionManager {
|
|
|
246
1011
|
title: session.title,
|
|
247
1012
|
workspacePath: session.workspacePath,
|
|
248
1013
|
sessionId: session.sessionId,
|
|
1014
|
+
engine: session.engine,
|
|
249
1015
|
updatedAt: session.updatedAt,
|
|
250
1016
|
allowSkipPermissions: session.allowSkipPermissions,
|
|
1017
|
+
acpDefaultModeId: session.acp?.defaultModeId,
|
|
1018
|
+
acpCurrentModeId: session.acp?.currentModeId,
|
|
251
1019
|
}));
|
|
252
1020
|
await mkdir(path.dirname(this.stateFilePath), { recursive: true });
|
|
253
1021
|
await writeFile(this.stateFilePath, JSON.stringify({ sessions: persistedSessions }, null, 2), "utf8");
|
|
@@ -297,43 +1065,59 @@ export class SessionManager {
|
|
|
297
1065
|
return false;
|
|
298
1066
|
}
|
|
299
1067
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
1068
|
+
bindSessionId(clientSessionId, oldSessionId, newSessionId, workspacePath, emitChangeEvent) {
|
|
1069
|
+
if (!newSessionId || oldSessionId === newSessionId) {
|
|
1070
|
+
if (newSessionId) {
|
|
1071
|
+
this.sessionIdIndex.set(newSessionId, clientSessionId);
|
|
1072
|
+
}
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
if (oldSessionId) {
|
|
1076
|
+
this.sessionIdIndex.delete(oldSessionId);
|
|
1077
|
+
this.activityMonitor.switchWatch(oldSessionId, newSessionId, workspacePath);
|
|
1078
|
+
}
|
|
1079
|
+
else {
|
|
1080
|
+
this.activityMonitor.watch(newSessionId, workspacePath);
|
|
1081
|
+
}
|
|
1082
|
+
this.sessionIdIndex.set(newSessionId, clientSessionId);
|
|
1083
|
+
const entry = this.sessions.get(clientSessionId);
|
|
1084
|
+
if (entry) {
|
|
1085
|
+
entry.snapshot.sessionId = newSessionId;
|
|
1086
|
+
entry.snapshot.updatedAt = new Date().toISOString();
|
|
1087
|
+
this.schedulePersist();
|
|
1088
|
+
}
|
|
1089
|
+
if (emitChangeEvent) {
|
|
1090
|
+
this.emit({
|
|
1091
|
+
type: "session_id_updated",
|
|
1092
|
+
payload: { clientSessionId, newSessionId },
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
303
1096
|
handlePlanSessionDetected(newSessionId, promptId, workspacePath) {
|
|
304
1097
|
const oldSessionId = this.activityMonitor.getSessionIdByPromptId(promptId);
|
|
305
1098
|
if (!oldSessionId) {
|
|
306
|
-
console.log(`[SessionManager] handlePlanSessionDetected: no oldSessionId found for promptId ${promptId}`);
|
|
307
1099
|
return;
|
|
308
1100
|
}
|
|
309
1101
|
const clientSessionId = this.sessionIdIndex.get(oldSessionId);
|
|
310
1102
|
if (!clientSessionId) {
|
|
311
|
-
console.log(`[SessionManager] handlePlanSessionDetected: no clientSessionId found for oldSessionId ${oldSessionId}`);
|
|
312
1103
|
return;
|
|
313
1104
|
}
|
|
314
|
-
console.log(`[SessionManager] Plan session detected: ${oldSessionId} → ${newSessionId} (promptId=${promptId})`);
|
|
315
1105
|
this.completeSessionSwitch(clientSessionId, oldSessionId, newSessionId, workspacePath);
|
|
316
1106
|
}
|
|
317
1107
|
handleSessionClear(oldSessionId, workspacePath) {
|
|
318
1108
|
const clientSessionId = this.sessionIdIndex.get(oldSessionId);
|
|
319
1109
|
if (!clientSessionId) {
|
|
320
|
-
console.log(`[SessionManager] handleSessionClear: no clientSessionId found for ${oldSessionId}`);
|
|
321
1110
|
return;
|
|
322
1111
|
}
|
|
323
1112
|
const entry = this.sessions.get(clientSessionId);
|
|
324
1113
|
if (!entry) {
|
|
325
|
-
console.log(`[SessionManager] handleSessionClear: no entry found for clientSessionId ${clientSessionId}`);
|
|
326
1114
|
return;
|
|
327
1115
|
}
|
|
328
|
-
console.log(`[SessionManager] handleSessionClear: oldSessionId=${oldSessionId}, clientSessionId=${clientSessionId}, workspace=${workspacePath}`);
|
|
329
|
-
// Immediately mark as idle — the old session is done
|
|
330
1116
|
entry.snapshot.activityState = "idle";
|
|
331
1117
|
this.emit({
|
|
332
1118
|
type: "session_activity",
|
|
333
1119
|
payload: { clientSessionId, activityState: "idle" },
|
|
334
1120
|
});
|
|
335
|
-
// Delay 500ms before starting discovery — the new session file is created
|
|
336
|
-
// almost simultaneously with /clear in history.jsonl.
|
|
337
1121
|
const delayTimer = setTimeout(() => {
|
|
338
1122
|
this.startNewSessionDiscovery(clientSessionId, oldSessionId, workspacePath);
|
|
339
1123
|
}, 500);
|
|
@@ -342,44 +1126,40 @@ export class SessionManager {
|
|
|
342
1126
|
startNewSessionDiscovery(clientSessionId, oldSessionId, workspacePath) {
|
|
343
1127
|
const dirPath = projectDirPath(workspacePath);
|
|
344
1128
|
let pollCount = 0;
|
|
345
|
-
console.log(`[SessionManager] startNewSessionDiscovery: dir=${dirPath}, oldSession=${oldSessionId}`);
|
|
346
1129
|
const tryFind = async () => {
|
|
347
1130
|
try {
|
|
348
|
-
const files = (await readdir(dirPath)).filter((
|
|
349
|
-
// Stat each file (except old session) to find the most recently modified ones
|
|
1131
|
+
const files = (await readdir(dirPath)).filter((file) => file.endsWith(".jsonl"));
|
|
350
1132
|
const candidates = [];
|
|
351
|
-
for (const
|
|
352
|
-
const sid =
|
|
1133
|
+
for (const file of files) {
|
|
1134
|
+
const sid = file.replace(/\.jsonl$/, "");
|
|
353
1135
|
if (sid === oldSessionId)
|
|
354
1136
|
continue;
|
|
355
1137
|
try {
|
|
356
|
-
const
|
|
357
|
-
candidates.push({ name:
|
|
1138
|
+
const fileStat = await stat(path.join(dirPath, file));
|
|
1139
|
+
candidates.push({ name: file, mtimeMs: fileStat.mtimeMs });
|
|
358
1140
|
}
|
|
359
1141
|
catch {
|
|
360
|
-
//
|
|
1142
|
+
// ignore unreadable candidate
|
|
361
1143
|
}
|
|
362
1144
|
}
|
|
363
|
-
candidates.sort((
|
|
364
|
-
// Check the top 3 newest files for /clear content
|
|
1145
|
+
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
365
1146
|
for (const candidate of candidates.slice(0, 3)) {
|
|
366
1147
|
const filePath = path.join(dirPath, candidate.name);
|
|
367
1148
|
let fd;
|
|
368
1149
|
try {
|
|
369
1150
|
fd = await open(filePath, "r");
|
|
370
|
-
const
|
|
371
|
-
const { bytesRead } = await fd.read(
|
|
372
|
-
const head =
|
|
1151
|
+
const buffer = Buffer.alloc(4096);
|
|
1152
|
+
const { bytesRead } = await fd.read(buffer, 0, 4096, 0);
|
|
1153
|
+
const head = buffer.toString("utf8", 0, bytesRead);
|
|
373
1154
|
if (head.includes("<command-name>/clear</command-name>")) {
|
|
374
1155
|
const newSessionId = candidate.name.replace(/\.jsonl$/, "");
|
|
375
|
-
console.log(`[SessionManager] discovery confirmed new session: ${candidate.name} (contains /clear)`);
|
|
376
1156
|
this.clearDiscoveryTimer(clientSessionId);
|
|
377
1157
|
this.completeSessionSwitch(clientSessionId, oldSessionId, newSessionId, workspacePath);
|
|
378
1158
|
return true;
|
|
379
1159
|
}
|
|
380
1160
|
}
|
|
381
1161
|
catch {
|
|
382
|
-
//
|
|
1162
|
+
// ignore
|
|
383
1163
|
}
|
|
384
1164
|
finally {
|
|
385
1165
|
await fd?.close();
|
|
@@ -387,18 +1167,16 @@ export class SessionManager {
|
|
|
387
1167
|
}
|
|
388
1168
|
}
|
|
389
1169
|
catch {
|
|
390
|
-
//
|
|
1170
|
+
// ignore
|
|
391
1171
|
}
|
|
392
1172
|
return false;
|
|
393
1173
|
};
|
|
394
|
-
// Immediate first attempt, then retry every 1s up to ~1 minute
|
|
395
1174
|
tryFind().then((found) => {
|
|
396
1175
|
if (found)
|
|
397
1176
|
return;
|
|
398
1177
|
const timer = setInterval(async () => {
|
|
399
|
-
pollCount
|
|
1178
|
+
pollCount += 1;
|
|
400
1179
|
if (pollCount > SessionManager.DISCOVERY_MAX_POLLS) {
|
|
401
|
-
console.log(`[SessionManager] discovery timed out for ${oldSessionId}`);
|
|
402
1180
|
this.clearDiscoveryTimer(clientSessionId);
|
|
403
1181
|
return;
|
|
404
1182
|
}
|
|
@@ -411,22 +1189,9 @@ export class SessionManager {
|
|
|
411
1189
|
const entry = this.sessions.get(clientSessionId);
|
|
412
1190
|
if (!entry)
|
|
413
1191
|
return;
|
|
414
|
-
|
|
415
|
-
// Update reverse index
|
|
416
|
-
this.sessionIdIndex.delete(oldSessionId);
|
|
417
|
-
this.sessionIdIndex.set(newSessionId, clientSessionId);
|
|
418
|
-
// Update snapshot
|
|
419
|
-
entry.snapshot.sessionId = newSessionId;
|
|
1192
|
+
this.bindSessionId(clientSessionId, oldSessionId, newSessionId, workspacePath, true);
|
|
420
1193
|
entry.snapshot.updatedAt = new Date().toISOString();
|
|
421
|
-
// Switch activity monitor to watch the new JSONL file
|
|
422
|
-
this.activityMonitor.switchWatch(oldSessionId, newSessionId, workspacePath);
|
|
423
|
-
// Persist so --resume uses the new session ID
|
|
424
1194
|
this.schedulePersist();
|
|
425
|
-
// Notify frontend
|
|
426
|
-
this.emit({
|
|
427
|
-
type: "session_id_updated",
|
|
428
|
-
payload: { clientSessionId, newSessionId },
|
|
429
|
-
});
|
|
430
1195
|
}
|
|
431
1196
|
clearDiscoveryTimer(clientSessionId) {
|
|
432
1197
|
const timer = this.discoveryTimers.get(clientSessionId);
|
|
@@ -435,25 +1200,19 @@ export class SessionManager {
|
|
|
435
1200
|
this.discoveryTimers.delete(clientSessionId);
|
|
436
1201
|
}
|
|
437
1202
|
}
|
|
438
|
-
// ---------------------------------------------------------------------------
|
|
439
|
-
// history.jsonl monitoring — detect /clear commands globally
|
|
440
|
-
// ---------------------------------------------------------------------------
|
|
441
1203
|
async startHistoryMonitor() {
|
|
442
|
-
// Get initial file size so we only process new lines
|
|
443
1204
|
try {
|
|
444
|
-
const
|
|
445
|
-
this.lastHistorySize =
|
|
1205
|
+
const historyStats = await stat(this.historyFilePath);
|
|
1206
|
+
this.lastHistorySize = historyStats.size;
|
|
446
1207
|
}
|
|
447
1208
|
catch {
|
|
448
1209
|
this.lastHistorySize = 0;
|
|
449
1210
|
}
|
|
450
|
-
console.log(`[SessionManager] Watching ${this.historyFilePath} for /clear commands (initial size=${this.lastHistorySize})`);
|
|
451
1211
|
try {
|
|
452
1212
|
this.historyWatcher = watch(this.historyFilePath, () => {
|
|
453
1213
|
this.scheduleHistoryCheck();
|
|
454
1214
|
});
|
|
455
1215
|
this.historyWatcher.on("error", () => {
|
|
456
|
-
// Fall back to polling if the watcher fails
|
|
457
1216
|
this.historyWatcher?.close();
|
|
458
1217
|
this.historyWatcher = null;
|
|
459
1218
|
if (!this.historyPollTimer) {
|
|
@@ -464,7 +1223,6 @@ export class SessionManager {
|
|
|
464
1223
|
});
|
|
465
1224
|
}
|
|
466
1225
|
catch {
|
|
467
|
-
// Watcher unavailable — use polling
|
|
468
1226
|
this.historyPollTimer = setInterval(() => {
|
|
469
1227
|
this.checkHistoryUpdates().catch(() => undefined);
|
|
470
1228
|
}, SessionManager.HISTORY_POLL_MS);
|
|
@@ -481,68 +1239,54 @@ export class SessionManager {
|
|
|
481
1239
|
async checkHistoryUpdates() {
|
|
482
1240
|
let fd;
|
|
483
1241
|
try {
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
this.lastHistorySize = stats.size;
|
|
1242
|
+
const historyStats = await stat(this.historyFilePath);
|
|
1243
|
+
if (historyStats.size < this.lastHistorySize) {
|
|
1244
|
+
this.lastHistorySize = historyStats.size;
|
|
488
1245
|
return;
|
|
489
1246
|
}
|
|
490
|
-
|
|
491
|
-
if (stats.size === this.lastHistorySize)
|
|
1247
|
+
if (historyStats.size === this.lastHistorySize)
|
|
492
1248
|
return;
|
|
493
|
-
const readSize =
|
|
1249
|
+
const readSize = historyStats.size - this.lastHistorySize;
|
|
494
1250
|
fd = await open(this.historyFilePath, "r");
|
|
495
1251
|
const buffer = Buffer.alloc(readSize);
|
|
496
1252
|
await fd.read(buffer, 0, readSize, this.lastHistorySize);
|
|
497
|
-
this.lastHistorySize =
|
|
1253
|
+
this.lastHistorySize = historyStats.size;
|
|
498
1254
|
const newText = buffer.toString("utf8");
|
|
499
|
-
const lines = newText.split("\n").filter((
|
|
1255
|
+
const lines = newText.split("\n").filter((line) => line.trim().length > 0);
|
|
500
1256
|
for (const line of lines) {
|
|
501
1257
|
try {
|
|
502
1258
|
const entry = JSON.parse(line);
|
|
503
1259
|
if (entry.display === "/clear" && entry.sessionId) {
|
|
504
|
-
// Check if we're tracking this session
|
|
505
1260
|
const clientSessionId = this.sessionIdIndex.get(entry.sessionId);
|
|
506
1261
|
if (clientSessionId) {
|
|
507
1262
|
const managed = this.sessions.get(clientSessionId);
|
|
508
1263
|
if (managed) {
|
|
509
|
-
console.log(`[SessionManager] /clear detected in history.jsonl for session ${entry.sessionId} (client=${clientSessionId})`);
|
|
510
1264
|
this.handleSessionClear(entry.sessionId, managed.snapshot.workspacePath);
|
|
511
1265
|
}
|
|
512
1266
|
}
|
|
513
1267
|
}
|
|
514
1268
|
}
|
|
515
1269
|
catch {
|
|
516
|
-
//
|
|
1270
|
+
// ignore malformed line
|
|
517
1271
|
}
|
|
518
1272
|
}
|
|
519
1273
|
}
|
|
520
1274
|
catch {
|
|
521
|
-
//
|
|
1275
|
+
// ignore
|
|
522
1276
|
}
|
|
523
1277
|
finally {
|
|
524
1278
|
await fd?.close();
|
|
525
1279
|
}
|
|
526
1280
|
}
|
|
527
|
-
stopHistoryMonitor() {
|
|
528
|
-
if (this.historyWatcher) {
|
|
529
|
-
this.historyWatcher.close();
|
|
530
|
-
this.historyWatcher = null;
|
|
531
|
-
}
|
|
532
|
-
if (this.historyPollTimer) {
|
|
533
|
-
clearInterval(this.historyPollTimer);
|
|
534
|
-
this.historyPollTimer = null;
|
|
535
|
-
}
|
|
536
|
-
if (this.historyDebounceTimer) {
|
|
537
|
-
clearTimeout(this.historyDebounceTimer);
|
|
538
|
-
this.historyDebounceTimer = null;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
1281
|
handleManagerError(clientSessionId, error) {
|
|
542
1282
|
const entry = this.sessions.get(clientSessionId);
|
|
543
1283
|
if (!entry) {
|
|
544
1284
|
return;
|
|
545
1285
|
}
|
|
1286
|
+
const acpState = entry.snapshot.acp;
|
|
1287
|
+
if (acpState) {
|
|
1288
|
+
acpState.busy = false;
|
|
1289
|
+
}
|
|
546
1290
|
entry.snapshot.connectionState = "error";
|
|
547
1291
|
entry.snapshot.updatedAt = new Date().toISOString();
|
|
548
1292
|
this.schedulePersist();
|
|
@@ -556,6 +1300,169 @@ export class SessionManager {
|
|
|
556
1300
|
});
|
|
557
1301
|
}
|
|
558
1302
|
}
|
|
1303
|
+
function createEmptyAcpState(defaultModeId = "default", currentModeId = defaultModeId) {
|
|
1304
|
+
return {
|
|
1305
|
+
modes: [],
|
|
1306
|
+
defaultModeId,
|
|
1307
|
+
currentModeId,
|
|
1308
|
+
busy: false,
|
|
1309
|
+
timeline: [],
|
|
1310
|
+
historyTotal: 0,
|
|
1311
|
+
historyStart: 0,
|
|
1312
|
+
permissions: [],
|
|
1313
|
+
questions: [],
|
|
1314
|
+
availableCommands: [],
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
function stringifyMaybe(value) {
|
|
1318
|
+
if (typeof value === "string") {
|
|
1319
|
+
return value;
|
|
1320
|
+
}
|
|
1321
|
+
return JSON.stringify(value, null, 2);
|
|
1322
|
+
}
|
|
1323
|
+
function formatToolDetails(details) {
|
|
1324
|
+
return stringifyMaybe({
|
|
1325
|
+
toolCallId: details.toolCallId,
|
|
1326
|
+
title: details.title,
|
|
1327
|
+
status: details.status,
|
|
1328
|
+
rawInput: details.rawInput,
|
|
1329
|
+
rawOutput: details.rawOutput,
|
|
1330
|
+
...(details.parentToolCallId ? { parentToolCallId: details.parentToolCallId } : undefined),
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
function normalizeAcpToolTitle(rawTitle) {
|
|
1334
|
+
if (typeof rawTitle !== "string")
|
|
1335
|
+
return "";
|
|
1336
|
+
return rawTitle.replace(/^mcp__acp__/i, "");
|
|
1337
|
+
}
|
|
1338
|
+
function summarizeToolTitle(rawTitle, rawInput, rawToolCallId) {
|
|
1339
|
+
const title = normalizeAcpToolTitle(rawTitle).trim();
|
|
1340
|
+
const record = asRecord(rawInput) ?? asRecord(tryParseJson(rawInput));
|
|
1341
|
+
const normalizedTitle = title.toLowerCase();
|
|
1342
|
+
const isSubagent = normalizedTitle.includes("subagent") || normalizedTitle === "task" || normalizedTitle.includes(" task");
|
|
1343
|
+
if (isSubagent) {
|
|
1344
|
+
const summary = readSubagentSummary(record);
|
|
1345
|
+
if (summary) {
|
|
1346
|
+
return `${title || "Task"} · ${summary}`;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
if (title && !/^工具\s+tool_/.test(title) && !/^tool_/.test(title)) {
|
|
1350
|
+
return title;
|
|
1351
|
+
}
|
|
1352
|
+
const command = typeof record?.command === "string"
|
|
1353
|
+
? record.command
|
|
1354
|
+
: Array.isArray(record?.cmd)
|
|
1355
|
+
? record.cmd.filter((part) => typeof part === "string").join(" ")
|
|
1356
|
+
: null;
|
|
1357
|
+
const pathValue = typeof record?.path === "string"
|
|
1358
|
+
? record.path
|
|
1359
|
+
: typeof record?.filePath === "string"
|
|
1360
|
+
? record.filePath
|
|
1361
|
+
: typeof record?.cwd === "string"
|
|
1362
|
+
? record.cwd
|
|
1363
|
+
: null;
|
|
1364
|
+
const description = typeof record?.description === "string" ? record.description : null;
|
|
1365
|
+
const args = Array.isArray(record?.args) ? record.args.filter((part) => typeof part === "string").join(" ") : null;
|
|
1366
|
+
const summary = [command, pathValue, description, args].filter(Boolean).join(" · ");
|
|
1367
|
+
if (summary) {
|
|
1368
|
+
return summary;
|
|
1369
|
+
}
|
|
1370
|
+
if (title) {
|
|
1371
|
+
return title;
|
|
1372
|
+
}
|
|
1373
|
+
return typeof rawToolCallId === "string" ? `工具 ${rawToolCallId}` : "工具调用";
|
|
1374
|
+
}
|
|
1375
|
+
function readSubagentSummary(record) {
|
|
1376
|
+
if (!record) {
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
for (const key of ["title", "description"]) {
|
|
1380
|
+
const value = record[key];
|
|
1381
|
+
if (typeof value === "string" && value.trim()) {
|
|
1382
|
+
return value.trim();
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
for (const key of ["rawInput", "input", "args", "payload", "params"]) {
|
|
1386
|
+
if (!(key in record)) {
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
const nestedRecord = asRecord(record[key]) ?? asRecord(tryParseJson(record[key]));
|
|
1390
|
+
const nested = readSubagentSummary(nestedRecord);
|
|
1391
|
+
if (nested) {
|
|
1392
|
+
return nested;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
function tryParseJson(value) {
|
|
1398
|
+
if (typeof value !== "string") {
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
const trimmed = value.trim();
|
|
1402
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
1403
|
+
return null;
|
|
1404
|
+
}
|
|
1405
|
+
try {
|
|
1406
|
+
return JSON.parse(trimmed);
|
|
1407
|
+
}
|
|
1408
|
+
catch {
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function asRecord(value) {
|
|
1413
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1414
|
+
}
|
|
1415
|
+
function extractParentToolCallId(update) {
|
|
1416
|
+
const meta = asRecord(update._meta);
|
|
1417
|
+
const claudeCode = asRecord(meta?.claudeCode);
|
|
1418
|
+
const parentId = claudeCode?.parentToolUseId;
|
|
1419
|
+
return typeof parentId === "string" && parentId ? parentId : undefined;
|
|
1420
|
+
}
|
|
1421
|
+
function formatEditToolChangeMessage(message) {
|
|
1422
|
+
const parsed = tryParseJson(message);
|
|
1423
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
1424
|
+
return null;
|
|
1425
|
+
}
|
|
1426
|
+
const files = parsed
|
|
1427
|
+
.map((entry) => asRecord(entry))
|
|
1428
|
+
.filter((entry) => Boolean(entry))
|
|
1429
|
+
.map((entry) => {
|
|
1430
|
+
const filePath = typeof entry.newFileName === "string"
|
|
1431
|
+
? entry.newFileName
|
|
1432
|
+
: typeof entry.oldFileName === "string"
|
|
1433
|
+
? entry.oldFileName
|
|
1434
|
+
: typeof entry.index === "string"
|
|
1435
|
+
? entry.index
|
|
1436
|
+
: "";
|
|
1437
|
+
const hunks = Array.isArray(entry.hunks) ? entry.hunks.length : 0;
|
|
1438
|
+
return { filePath, hunks };
|
|
1439
|
+
})
|
|
1440
|
+
.filter((entry) => entry.filePath);
|
|
1441
|
+
if (files.length === 0) {
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
const lines = files.map((entry) => `- ${entry.filePath}${entry.hunks > 0 ? `(${entry.hunks} 处修改)` : ""}`);
|
|
1445
|
+
return {
|
|
1446
|
+
title: `Edit 已修改 ${files.length} 个文件`,
|
|
1447
|
+
body: `Edit 工具已更新以下文件:\n${lines.join("\n")}`,
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function labelForMode(modeId) {
|
|
1451
|
+
switch (modeId) {
|
|
1452
|
+
case "default":
|
|
1453
|
+
return "Default";
|
|
1454
|
+
case "acceptEdits":
|
|
1455
|
+
return "Accept Edits";
|
|
1456
|
+
case "plan":
|
|
1457
|
+
return "Plan";
|
|
1458
|
+
case "dontAsk":
|
|
1459
|
+
return "Don't Ask";
|
|
1460
|
+
case "bypassPermissions":
|
|
1461
|
+
return "Bypass Permissions";
|
|
1462
|
+
default:
|
|
1463
|
+
return modeId || "默认模式";
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
559
1466
|
function formatError(error) {
|
|
560
1467
|
if (error instanceof Error) {
|
|
561
1468
|
return error.message;
|
|
@@ -567,6 +1474,95 @@ function formatError(error) {
|
|
|
567
1474
|
return String(error);
|
|
568
1475
|
}
|
|
569
1476
|
}
|
|
1477
|
+
function extractChunkText(content) {
|
|
1478
|
+
if (!content) {
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
if (Array.isArray(content)) {
|
|
1482
|
+
const joined = content.map((item) => extractChunkText(item)).filter((item) => Boolean(item)).join("\n");
|
|
1483
|
+
return joined || null;
|
|
1484
|
+
}
|
|
1485
|
+
const record = asRecord(content);
|
|
1486
|
+
if (!record) {
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
if (typeof record.text === "string" && record.text.trim()) {
|
|
1490
|
+
return record.text;
|
|
1491
|
+
}
|
|
1492
|
+
if (record.type === "resource") {
|
|
1493
|
+
const resource = asRecord(record.resource);
|
|
1494
|
+
if (typeof resource?.text === "string" && resource.text.trim()) {
|
|
1495
|
+
return resource.text;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
if (record.type === "resource_link") {
|
|
1499
|
+
const uri = typeof record.uri === "string" ? record.uri : "";
|
|
1500
|
+
return uri ? `[resource] ${uri}` : "[resource]";
|
|
1501
|
+
}
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
function normalizeAvailableCommandsSnapshot(rawValue) {
|
|
1505
|
+
if (!Array.isArray(rawValue)) {
|
|
1506
|
+
return [];
|
|
1507
|
+
}
|
|
1508
|
+
const seen = new Set();
|
|
1509
|
+
const normalized = [];
|
|
1510
|
+
for (const item of rawValue) {
|
|
1511
|
+
if (typeof item === "string") {
|
|
1512
|
+
const name = normalizeCommandName(item);
|
|
1513
|
+
if (!name || seen.has(name)) {
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
seen.add(name);
|
|
1517
|
+
normalized.push({ name, description: "", inputType: "unstructured" });
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
const record = asRecord(item);
|
|
1521
|
+
const rawName = typeof record?.name === "string"
|
|
1522
|
+
? record.name
|
|
1523
|
+
: typeof record?.command === "string"
|
|
1524
|
+
? record.command
|
|
1525
|
+
: "";
|
|
1526
|
+
const name = normalizeCommandName(rawName);
|
|
1527
|
+
if (!name || seen.has(name)) {
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
seen.add(name);
|
|
1531
|
+
normalized.push({
|
|
1532
|
+
name,
|
|
1533
|
+
description: typeof record?.description === "string"
|
|
1534
|
+
? record.description.trim()
|
|
1535
|
+
: typeof record?.title === "string"
|
|
1536
|
+
? record.title.trim()
|
|
1537
|
+
: "",
|
|
1538
|
+
inputType: "unstructured",
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
return normalized.sort((left, right) => left.name.localeCompare(right.name, "zh-Hans-CN"));
|
|
1542
|
+
}
|
|
1543
|
+
function normalizeCommandName(rawName) {
|
|
1544
|
+
const trimmed = rawName.trim();
|
|
1545
|
+
if (!trimmed) {
|
|
1546
|
+
return "";
|
|
1547
|
+
}
|
|
1548
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
1549
|
+
}
|
|
1550
|
+
function isAskUserQuestionTitle(title) {
|
|
1551
|
+
if (!title)
|
|
1552
|
+
return false;
|
|
1553
|
+
const lower = title.toLowerCase();
|
|
1554
|
+
return lower === "askuserquestion" || lower.startsWith("askuserquestion ");
|
|
1555
|
+
}
|
|
570
1556
|
export const sessionManagerTestables = {
|
|
1557
|
+
stringifyMaybe,
|
|
1558
|
+
formatToolDetails,
|
|
1559
|
+
summarizeToolTitle,
|
|
1560
|
+
normalizeAcpToolTitle,
|
|
1561
|
+
isAskUserQuestionTitle,
|
|
1562
|
+
asRecord,
|
|
1563
|
+
extractChunkText,
|
|
1564
|
+
normalizeAvailableCommandsSnapshot,
|
|
1565
|
+
formatEditToolChangeMessage,
|
|
1566
|
+
labelForMode,
|
|
571
1567
|
formatError,
|
|
572
1568
|
};
|