@co0ontty/wand 1.6.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/git-worktree.d.ts +12 -0
- package/dist/git-worktree.js +43 -0
- package/dist/message-parser.d.ts +1 -1
- package/dist/message-parser.js +275 -1
- package/dist/process-manager.d.ts +6 -3
- package/dist/process-manager.js +135 -81
- package/dist/pty-text-utils.js +79 -29
- package/dist/server-session-routes.js +39 -7
- package/dist/server.js +4 -1
- package/dist/session-logger.d.ts +2 -0
- package/dist/session-logger.js +23 -0
- package/dist/storage.js +80 -18
- package/dist/structured-session-manager.d.ts +5 -0
- package/dist/structured-session-manager.js +107 -43
- package/dist/types.d.ts +14 -0
- package/dist/web-ui/content/scripts.js +1468 -301
- package/dist/web-ui/content/styles.css +135 -0
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SessionSnapshot } from "./types.js";
|
|
2
|
+
interface WorktreeSetupOptions {
|
|
3
|
+
cwd: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
}
|
|
6
|
+
interface WorktreeSetupResult {
|
|
7
|
+
cwd: string;
|
|
8
|
+
worktreeEnabled: boolean;
|
|
9
|
+
worktree: NonNullable<SessionSnapshot["worktree"]>;
|
|
10
|
+
}
|
|
11
|
+
export declare function prepareSessionWorktree(options: WorktreeSetupOptions): WorktreeSetupResult;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function runGit(args, cwd) {
|
|
5
|
+
return execFileSync("git", args, {
|
|
6
|
+
cwd,
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9
|
+
}).trim();
|
|
10
|
+
}
|
|
11
|
+
function sanitizeBranchSegment(value) {
|
|
12
|
+
return value
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
15
|
+
.replace(/^-+|-+$/g, "")
|
|
16
|
+
.slice(0, 48) || "session";
|
|
17
|
+
}
|
|
18
|
+
function getCurrentBranch(repoRoot) {
|
|
19
|
+
const branch = runGit(["branch", "--show-current"], repoRoot);
|
|
20
|
+
return branch || "master";
|
|
21
|
+
}
|
|
22
|
+
export function prepareSessionWorktree(options) {
|
|
23
|
+
const resolvedCwd = path.resolve(options.cwd);
|
|
24
|
+
const repoRoot = runGit(["rev-parse", "--show-toplevel"], resolvedCwd);
|
|
25
|
+
if (!repoRoot || !existsSync(repoRoot)) {
|
|
26
|
+
throw new Error("当前目录不在 git 仓库中,无法启用 worktree 模式。");
|
|
27
|
+
}
|
|
28
|
+
const baseBranch = getCurrentBranch(repoRoot);
|
|
29
|
+
const branchSuffix = sanitizeBranchSegment(options.sessionId.split("-")[0] || options.sessionId);
|
|
30
|
+
const branchName = `wand/${sanitizeBranchSegment(baseBranch)}-${branchSuffix}`;
|
|
31
|
+
const worktreesRoot = path.join(repoRoot, ".wand-worktrees");
|
|
32
|
+
const worktreePath = path.join(worktreesRoot, branchName.replace(/\//g, "-"));
|
|
33
|
+
mkdirSync(worktreesRoot, { recursive: true });
|
|
34
|
+
runGit(["worktree", "add", "-b", branchName, worktreePath, "HEAD"], repoRoot);
|
|
35
|
+
return {
|
|
36
|
+
cwd: worktreePath,
|
|
37
|
+
worktreeEnabled: true,
|
|
38
|
+
worktree: {
|
|
39
|
+
branch: branchName,
|
|
40
|
+
path: worktreePath,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/message-parser.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ChatMessage } from "./types.js";
|
|
2
|
-
export declare function parseMessages(output: string): ChatMessage[];
|
|
2
|
+
export declare function parseMessages(output: string, command?: string): ChatMessage[];
|
package/dist/message-parser.js
CHANGED
|
@@ -1,8 +1,282 @@
|
|
|
1
1
|
import { stripAnsi, isNoiseLine } from "./pty-text-utils.js";
|
|
2
|
-
|
|
2
|
+
function isCodexCommand(command) {
|
|
3
|
+
return /^codex\b/.test((command ?? "").trim());
|
|
4
|
+
}
|
|
5
|
+
const CODEX_FOOTER_RE = /\bgpt-\d+(?:\.\d+)?(?:\s+[a-z0-9.-]+)?\s+·\s+\d+%\s+left\s+·\s+(?:\/|~\/).+/i;
|
|
6
|
+
const CODEX_ACTIVITY_RE = /^(?:thinking|working|running|planning|applying|reading|searching|inspecting|reviewing|summarizing|editing|updating|writing|completed)\b/i;
|
|
7
|
+
function stripCodexSegment(raw) {
|
|
8
|
+
return raw
|
|
9
|
+
.replace(/\x1b\][^\x07]*(\x07|\x1b\\)/g, "")
|
|
10
|
+
.replace(/\x1b\[(\d+)C/g, (_match, count) => " ".repeat(Number(count) || 1))
|
|
11
|
+
.replace(/\x1b\[[0-9;?]*[AB]/g, "\n")
|
|
12
|
+
.replace(/\x1b\[[0-9;?]*[su]/g, "")
|
|
13
|
+
.replace(/\x1b\[[0-9;?]*[HfJKr]/g, "\n")
|
|
14
|
+
.replace(/\x1bM/g, "\n")
|
|
15
|
+
.replace(/\x1b\[[0-9;?]*[ST]/g, "\n")
|
|
16
|
+
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "")
|
|
17
|
+
.replace(/\x1b[><=ePX^_]/g, "")
|
|
18
|
+
.replace(/[\u00a0\u200b-\u200d\ufeff]/g, " ")
|
|
19
|
+
// eslint-disable-next-line no-control-regex
|
|
20
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
|
|
21
|
+
.replace(/[ \t]+\n/g, "\n");
|
|
22
|
+
}
|
|
23
|
+
function normalizeCodexText(text) {
|
|
24
|
+
return text
|
|
25
|
+
.replace(/\s+/g, " ")
|
|
26
|
+
.replace(/[M]+$/g, "")
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
function isLikelyAssistantTailArtifact(longer, shorter) {
|
|
30
|
+
if (!longer.startsWith(shorter))
|
|
31
|
+
return false;
|
|
32
|
+
const suffix = longer.slice(shorter.length);
|
|
33
|
+
return /^[a-z]{1,4}$/i.test(suffix);
|
|
34
|
+
}
|
|
35
|
+
function parseCodexMessages(output) {
|
|
36
|
+
const messages = [];
|
|
37
|
+
function normalizeCodexAssistantLine(line) {
|
|
38
|
+
return line
|
|
39
|
+
.replace(/^[•◦·]\s*/, "")
|
|
40
|
+
.replace(/^⏺\s+/, "")
|
|
41
|
+
.replace(/^│\s*/, "")
|
|
42
|
+
.trim();
|
|
43
|
+
}
|
|
44
|
+
function normalizeCodexPromptLine(line) {
|
|
45
|
+
return line
|
|
46
|
+
.replace(/^›\s*/, "")
|
|
47
|
+
.replace(/^>\s*/, "")
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
50
|
+
function shouldIgnoreCodexLine(line) {
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
if (!trimmed)
|
|
53
|
+
return true;
|
|
54
|
+
if (isNoiseLine(trimmed))
|
|
55
|
+
return true;
|
|
56
|
+
if (CODEX_FOOTER_RE.test(trimmed))
|
|
57
|
+
return true;
|
|
58
|
+
if (/^[╭╰│┌└┐┘├┤┬┴┼─═]/.test(trimmed))
|
|
59
|
+
return true;
|
|
60
|
+
if (/^\[>[0-9;?]*u$/i.test(trimmed))
|
|
61
|
+
return true;
|
|
62
|
+
if (/^M+$/i.test(trimmed))
|
|
63
|
+
return true;
|
|
64
|
+
if (/^(?:OpenAI Codex|Codex)\b/i.test(trimmed))
|
|
65
|
+
return true;
|
|
66
|
+
if (/^(?:tokens?|context window|remaining context|approvals?|sandbox|provider|session id):\s*/i.test(trimmed))
|
|
67
|
+
return true;
|
|
68
|
+
if (/^(?:thinking|working)\s*(?:\.\.\.|…)?$/i.test(trimmed))
|
|
69
|
+
return true;
|
|
70
|
+
if (/^[•◦·]\s+(?:thinking|working|running|planning|applying|reading|searching|inspecting|reviewing|summarizing|editing|updating|writing|completed)\b/i.test(trimmed))
|
|
71
|
+
return true;
|
|
72
|
+
if (/^(?:model|directory|tip|context|cwd|path):\s+/i.test(trimmed))
|
|
73
|
+
return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function extractCodexPromptCandidate(line) {
|
|
77
|
+
const trimmed = line.trim();
|
|
78
|
+
if (!/^›(?:\s|$)/.test(trimmed))
|
|
79
|
+
return null;
|
|
80
|
+
if (CODEX_FOOTER_RE.test(trimmed))
|
|
81
|
+
return null;
|
|
82
|
+
const prompt = normalizeCodexText(normalizeCodexPromptLine(trimmed));
|
|
83
|
+
if (!prompt || shouldIgnoreCodexLine(prompt))
|
|
84
|
+
return null;
|
|
85
|
+
return prompt;
|
|
86
|
+
}
|
|
87
|
+
function extractCodexAssistantCandidate(line) {
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
if (!/^[•◦·⏺]/.test(trimmed))
|
|
90
|
+
return null;
|
|
91
|
+
let assistant = normalizeCodexAssistantLine(trimmed);
|
|
92
|
+
if (!assistant || /^[•◦·⏺]$/.test(assistant))
|
|
93
|
+
return null;
|
|
94
|
+
assistant = assistant.replace(/\s*\(\d+[smh]?\s*•\s*esc to interrupt\)[\s\S]*$/i, "");
|
|
95
|
+
assistant = assistant.replace(/(?:[a-z]{1,6})?›[\s\S]*$/, "");
|
|
96
|
+
assistant = assistant.replace(/\s{2,}gpt-\d[\s\S]*$/i, "");
|
|
97
|
+
assistant = assistant.replace(/\b(?:OpenAI Codex|model:|directory:|Tip:)\b[\s\S]*$/i, "");
|
|
98
|
+
assistant = normalizeCodexText(assistant);
|
|
99
|
+
if (!assistant || assistant.length < 2 || CODEX_ACTIVITY_RE.test(assistant) || shouldIgnoreCodexLine(assistant)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return assistant;
|
|
103
|
+
}
|
|
104
|
+
function extractCodexEchoCandidate(line) {
|
|
105
|
+
const trimmed = normalizeCodexText(line);
|
|
106
|
+
if (!trimmed || shouldIgnoreCodexLine(trimmed))
|
|
107
|
+
return null;
|
|
108
|
+
if (/^[•◦·⏺›]/.test(trimmed))
|
|
109
|
+
return null;
|
|
110
|
+
if (/^[\[\]<>0-9;?]+u?$/i.test(trimmed))
|
|
111
|
+
return null;
|
|
112
|
+
if (/^[╭╰│┌└┐┘├┤┬┴┼─═]/.test(trimmed))
|
|
113
|
+
return null;
|
|
114
|
+
if (trimmed.length > 500)
|
|
115
|
+
return null;
|
|
116
|
+
return trimmed;
|
|
117
|
+
}
|
|
118
|
+
function collectCodexCandidates() {
|
|
119
|
+
const candidates = [];
|
|
120
|
+
let order = 0;
|
|
121
|
+
for (const rawSegment of output.replace(/\r\n?/g, "\n").split("\n")) {
|
|
122
|
+
const cleanedSegment = stripCodexSegment(rawSegment);
|
|
123
|
+
const pieces = cleanedSegment.split("\n");
|
|
124
|
+
for (const piece of pieces) {
|
|
125
|
+
const line = piece.trim();
|
|
126
|
+
if (!line)
|
|
127
|
+
continue;
|
|
128
|
+
const prompt = extractCodexPromptCandidate(line);
|
|
129
|
+
if (prompt) {
|
|
130
|
+
candidates.push({ kind: "user", order, text: prompt });
|
|
131
|
+
order += 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const assistant = extractCodexAssistantCandidate(line);
|
|
135
|
+
if (assistant) {
|
|
136
|
+
candidates.push({ kind: "assistant", order, text: assistant });
|
|
137
|
+
order += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const echo = extractCodexEchoCandidate(line);
|
|
141
|
+
if (echo) {
|
|
142
|
+
candidates.push({ kind: "echo", order, text: echo });
|
|
143
|
+
order += 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return candidates.filter((candidate, index, list) => {
|
|
148
|
+
const previous = list[index - 1];
|
|
149
|
+
return !previous || previous.kind !== candidate.kind || previous.text !== candidate.text;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function coalesceAssistantLines(lines) {
|
|
153
|
+
const collected = [];
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
const normalized = normalizeCodexText(line);
|
|
156
|
+
if (!normalized || normalized.length < 2 || shouldIgnoreCodexLine(normalized))
|
|
157
|
+
continue;
|
|
158
|
+
const previous = collected[collected.length - 1];
|
|
159
|
+
if (!previous) {
|
|
160
|
+
collected.push(normalized);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (normalized === previous)
|
|
164
|
+
continue;
|
|
165
|
+
if (normalized.startsWith(previous)) {
|
|
166
|
+
collected[collected.length - 1] = normalized;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (previous.startsWith(normalized)) {
|
|
170
|
+
if (isLikelyAssistantTailArtifact(previous, normalized)) {
|
|
171
|
+
collected[collected.length - 1] = normalized;
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
collected.push(normalized);
|
|
176
|
+
}
|
|
177
|
+
return collected.join("\n").trim();
|
|
178
|
+
}
|
|
179
|
+
function extractVisiblePrompt(lines) {
|
|
180
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
181
|
+
const line = lines[index].trim();
|
|
182
|
+
if (!line)
|
|
183
|
+
continue;
|
|
184
|
+
const inlinePrompt = extractCodexPromptCandidate(line);
|
|
185
|
+
if (inlinePrompt)
|
|
186
|
+
return inlinePrompt;
|
|
187
|
+
if (line === "›") {
|
|
188
|
+
for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
|
|
189
|
+
const nextLine = normalizeCodexText(lines[nextIndex]);
|
|
190
|
+
if (!nextLine || CODEX_FOOTER_RE.test(nextLine) || shouldIgnoreCodexLine(nextLine))
|
|
191
|
+
continue;
|
|
192
|
+
return nextLine;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
function extractVisibleAssistantLines(lines) {
|
|
199
|
+
const assistantLines = [];
|
|
200
|
+
let collecting = false;
|
|
201
|
+
for (const rawLine of lines) {
|
|
202
|
+
const line = rawLine.trim();
|
|
203
|
+
if (!line) {
|
|
204
|
+
if (collecting)
|
|
205
|
+
break;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const assistant = extractCodexAssistantCandidate(line);
|
|
209
|
+
if (assistant) {
|
|
210
|
+
assistantLines.push(assistant);
|
|
211
|
+
collecting = true;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (collecting) {
|
|
215
|
+
if (line === "›" ||
|
|
216
|
+
/^›(?:\s|$)/.test(line) ||
|
|
217
|
+
CODEX_FOOTER_RE.test(line) ||
|
|
218
|
+
shouldIgnoreCodexLine(line)) {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
assistantLines.push(normalizeCodexText(line));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return assistantLines;
|
|
225
|
+
}
|
|
226
|
+
const candidates = collectCodexCandidates();
|
|
227
|
+
const explicitUsers = candidates.filter((candidate) => candidate.kind === "user");
|
|
228
|
+
const assistantCandidates = candidates.filter((candidate) => candidate.kind === "assistant");
|
|
229
|
+
const echoCandidates = candidates.filter((candidate) => candidate.kind === "echo");
|
|
230
|
+
const strippedOutput = stripAnsi(output);
|
|
231
|
+
const strippedLines = strippedOutput.split("\n").map((line) => line.trimEnd());
|
|
232
|
+
const visiblePrompt = extractVisiblePrompt(strippedLines);
|
|
233
|
+
const latestExplicitUser = explicitUsers[explicitUsers.length - 1] ?? null;
|
|
234
|
+
const echoedUserCandidates = echoCandidates
|
|
235
|
+
.map((candidate) => candidate.text)
|
|
236
|
+
.filter((text) => text.length >= 3);
|
|
237
|
+
const latestEchoUser = [...echoedUserCandidates].reverse().find((text) => text !== visiblePrompt) ?? echoedUserCandidates[echoedUserCandidates.length - 1] ?? null;
|
|
238
|
+
const currentUser = latestExplicitUser?.text ?? latestEchoUser;
|
|
239
|
+
const rawAssistantLines = assistantCandidates
|
|
240
|
+
.filter((candidate) => !latestExplicitUser || candidate.order > latestExplicitUser.order)
|
|
241
|
+
.map((candidate) => candidate.text);
|
|
242
|
+
const visibleAssistantFallback = [...strippedOutput.matchAll(/^[ \t]*[•◦·⏺][ \t]*(.+)$/gm)]
|
|
243
|
+
.map((match) => normalizeCodexText(match[1] ?? ""))
|
|
244
|
+
.filter((line) => (!!line
|
|
245
|
+
&& !CODEX_ACTIVITY_RE.test(line)
|
|
246
|
+
&& !CODEX_FOOTER_RE.test(line)
|
|
247
|
+
&& !/\b(?:OpenAI Codex|model:|directory:|Tip:|esc to interrupt)\b/i.test(line)));
|
|
248
|
+
const assistant = coalesceAssistantLines(rawAssistantLines)
|
|
249
|
+
|| coalesceAssistantLines(extractVisibleAssistantLines(strippedLines))
|
|
250
|
+
|| visibleAssistantFallback[visibleAssistantFallback.length - 1]
|
|
251
|
+
|| null;
|
|
252
|
+
if (currentUser) {
|
|
253
|
+
messages.push({ role: "user", content: currentUser });
|
|
254
|
+
}
|
|
255
|
+
if (assistant) {
|
|
256
|
+
messages.push({ role: "assistant", content: assistant });
|
|
257
|
+
}
|
|
258
|
+
if (!messages.length && latestExplicitUser) {
|
|
259
|
+
messages.push({ role: "user", content: latestExplicitUser.text });
|
|
260
|
+
}
|
|
261
|
+
else if (!messages.length && latestEchoUser) {
|
|
262
|
+
messages.push({ role: "user", content: latestEchoUser });
|
|
263
|
+
}
|
|
264
|
+
const deduped = [];
|
|
265
|
+
for (const message of messages) {
|
|
266
|
+
const previous = deduped[deduped.length - 1];
|
|
267
|
+
if (!previous || previous.role !== message.role || previous.content !== message.content) {
|
|
268
|
+
deduped.push(message);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return deduped;
|
|
272
|
+
}
|
|
273
|
+
export function parseMessages(output, command) {
|
|
3
274
|
const messages = [];
|
|
4
275
|
if (!output)
|
|
5
276
|
return messages;
|
|
277
|
+
if (isCodexCommand(command)) {
|
|
278
|
+
return parseCodexMessages(output);
|
|
279
|
+
}
|
|
6
280
|
const stripped = stripAnsi(output).replace(/\r/g, "\n");
|
|
7
281
|
const lines = stripped.split("\n");
|
|
8
282
|
const cleaned = lines.filter((line) => !isNoiseLine(line.trim()));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { WandStorage } from "./storage.js";
|
|
3
|
-
import { ExecutionMode, ProcessEventHandler, SessionSnapshot, WandConfig } from "./types.js";
|
|
3
|
+
import { ExecutionMode, ProcessEventHandler, SessionProvider, SessionSnapshot, WandConfig } from "./types.js";
|
|
4
4
|
export type { ProcessEvent, ProcessEventHandler } from "./types.js";
|
|
5
5
|
/** Human-readable task information for the UI */
|
|
6
6
|
export interface TaskInfo {
|
|
@@ -32,8 +32,8 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
32
32
|
private readonly lifecycleManager;
|
|
33
33
|
/** Per-session debounce timers for throttled persist calls */
|
|
34
34
|
private readonly persistDebounceTimers;
|
|
35
|
-
/** Last persisted message
|
|
36
|
-
private readonly
|
|
35
|
+
/** Last persisted message state per session — used to skip redundant message writes */
|
|
36
|
+
private readonly lastPersistedMessageState;
|
|
37
37
|
constructor(config: WandConfig, storage: WandStorage, configDir?: string);
|
|
38
38
|
on(event: "process", listener: ProcessEventHandler): this;
|
|
39
39
|
private emitEvent;
|
|
@@ -41,6 +41,8 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
41
41
|
start(command: string, cwd: string | undefined, mode: ExecutionMode, initialInput?: string, opts?: {
|
|
42
42
|
resumedFromSessionId?: string;
|
|
43
43
|
autoRecovered?: boolean;
|
|
44
|
+
worktreeEnabled?: boolean;
|
|
45
|
+
provider?: SessionProvider;
|
|
44
46
|
}): SessionSnapshot;
|
|
45
47
|
list(): SessionSnapshot[];
|
|
46
48
|
hasClaudeSessionFile(cwd: string, claudeSessionId: string): boolean;
|
|
@@ -52,6 +54,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
52
54
|
cwd: string;
|
|
53
55
|
}[]): number;
|
|
54
56
|
get(id: string): SessionSnapshot | null;
|
|
57
|
+
getPtyTranscript(id: string): string | null;
|
|
55
58
|
sendInput(id: string, input: string, view?: "chat" | "terminal", shortcutKey?: string): SessionSnapshot;
|
|
56
59
|
/** Emit a task event for a session, debounced to avoid flooding */
|
|
57
60
|
private emitTask;
|