@ycodium-ai/plugin-conversation 0.2.2938 → 0.2.2944

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/index.js CHANGED
@@ -1,9 +1,1324 @@
1
1
  // src/index.ts
2
2
  import { definePlugin } from "@ycodium-ai/plugin-api";
3
+
4
+ // src/sessionMap/activate.ts
5
+ import * as NodeCrypto from "node:crypto";
6
+ import {
7
+ TURN_PROGRESS_TRIGGER_KIND
8
+ } from "@ycodium-ai/plugin-api";
9
+
10
+ // src/sessionMap/actions.ts
11
+ function createCommands(input) {
12
+ return [
13
+ {
14
+ type: "thread.create",
15
+ commandId: input.commandId,
16
+ threadId: input.threadId,
17
+ projectId: input.projectId,
18
+ title: input.title,
19
+ modelSelection: input.modelSelection,
20
+ runtimeMode: input.runtimeMode,
21
+ interactionMode: input.interactionMode,
22
+ branch: null,
23
+ worktreePath: null,
24
+ createdAt: input.now
25
+ },
26
+ turnCommand({
27
+ threadId: input.threadId,
28
+ text: input.text,
29
+ now: input.now,
30
+ messageId: input.messageId,
31
+ commandId: input.turnCommandId,
32
+ runtimeMode: input.runtimeMode,
33
+ interactionMode: input.interactionMode,
34
+ modelSelection: input.modelSelection
35
+ })
36
+ ];
37
+ }
38
+ function turnCommand(input) {
39
+ return {
40
+ type: "thread.turn.start",
41
+ commandId: input.commandId,
42
+ threadId: input.threadId,
43
+ message: {
44
+ messageId: input.messageId,
45
+ role: "user",
46
+ text: input.text,
47
+ attachments: []
48
+ },
49
+ runtimeMode: input.runtimeMode,
50
+ interactionMode: input.interactionMode,
51
+ createdAt: input.now,
52
+ ...input.modelSelection == null ? {} : { modelSelection: input.modelSelection }
53
+ };
54
+ }
55
+ function forkCommand(input) {
56
+ return {
57
+ type: "thread.fork",
58
+ commandId: input.commandId,
59
+ threadId: input.threadId,
60
+ sourceThreadId: input.sourceThreadId,
61
+ sourceMessageId: input.sourceMessageId,
62
+ createdAt: input.now
63
+ };
64
+ }
65
+ function titleFromText(text2) {
66
+ const line = text2.trim().split("\n")[0] ?? "";
67
+ const title = line.trim().slice(0, 80);
68
+ return title.length > 0 ? title : "New session";
69
+ }
70
+ function forkPointOf(value) {
71
+ if (typeof value !== "object" || value === null) return null;
72
+ const point = value;
73
+ if (typeof point.sourceThreadId !== "string" || typeof point.sourceMessageId !== "string") {
74
+ return null;
75
+ }
76
+ return { sourceThreadId: point.sourceThreadId, sourceMessageId: point.sourceMessageId };
77
+ }
78
+
79
+ // src/ui/sessionMap/limits.ts
80
+ var MAX_CARD_TEXT = 8e3;
81
+ var MAX_PREVIEW_TEXT = 480;
82
+ var MAX_DETAIL_MESSAGES = 40;
83
+ var MAX_DETAIL_TEXT = 32e3;
84
+ var MAX_TURNS_PER_THREAD = 48;
85
+ var MAX_TOOLS_PER_TURN = 12;
86
+ var MAX_TOOL_LABEL = 180;
87
+ var THREAD_PAGE_SIZE = 200;
88
+ var PROJECTION_NAME = "card";
89
+
90
+ // src/ui/sessionMap/text.ts
91
+ function clipText(value, limit = MAX_CARD_TEXT) {
92
+ const text2 = value.replaceAll("\0", "");
93
+ if (text2.length <= limit) return { text: text2, truncated: false };
94
+ return { text: text2.slice(0, limit), truncated: true };
95
+ }
96
+ function isRecord(value) {
97
+ return typeof value === "object" && value !== null && !Array.isArray(value);
98
+ }
99
+ function readString(value) {
100
+ return typeof value === "string" ? value : "";
101
+ }
102
+
103
+ // src/ui/sessionMap/canvas.ts
104
+ var EMPTY_CANVAS = {
105
+ version: 1,
106
+ revision: 0,
107
+ autoProjection: true,
108
+ workspaceTitle: "",
109
+ showArchived: false,
110
+ hiddenThreadIds: [],
111
+ seenThreadIds: [],
112
+ anchors: {},
113
+ positions: []
114
+ };
115
+ var MAX_TITLE = 120;
116
+ var MAX_POSITIONS = 4e3;
117
+ var MAX_IDS = 2e4;
118
+ function parseCanvas(value) {
119
+ if (!isRecord(value)) return EMPTY_CANVAS;
120
+ return {
121
+ version: 1,
122
+ revision: readRevision(value.revision),
123
+ autoProjection: value.autoProjection !== false,
124
+ workspaceTitle: readString(value.workspaceTitle).trim().slice(0, MAX_TITLE),
125
+ showArchived: value.showArchived === true,
126
+ hiddenThreadIds: readIds(value.hiddenThreadIds),
127
+ seenThreadIds: readIds(value.seenThreadIds),
128
+ anchors: readAnchors(value.anchors),
129
+ positions: readPositions(value.positions)
130
+ };
131
+ }
132
+ function canvasJson(state) {
133
+ return `${JSON.stringify(state)}
134
+ `;
135
+ }
136
+ function admitThreads(state, threadIds) {
137
+ const seen = new Set(state.seenThreadIds);
138
+ const hidden = new Set(state.hiddenThreadIds);
139
+ let changed = false;
140
+ for (const threadId of threadIds) {
141
+ if (threadId.length === 0 || seen.has(threadId)) continue;
142
+ seen.add(threadId);
143
+ changed = true;
144
+ if (!state.autoProjection) hidden.add(threadId);
145
+ }
146
+ if (!changed) return state;
147
+ return {
148
+ ...state,
149
+ seenThreadIds: [...seen].slice(-MAX_IDS),
150
+ hiddenThreadIds: [...hidden].slice(-MAX_IDS)
151
+ };
152
+ }
153
+ function hideThread(state, threadId) {
154
+ if (state.hiddenThreadIds.includes(threadId)) return state;
155
+ return { ...state, hiddenThreadIds: [...state.hiddenThreadIds, threadId].slice(-MAX_IDS) };
156
+ }
157
+ function restoreThread(state, threadId) {
158
+ if (!state.hiddenThreadIds.includes(threadId)) return state;
159
+ return { ...state, hiddenThreadIds: state.hiddenThreadIds.filter((id) => id !== threadId) };
160
+ }
161
+ function clearPositions(state) {
162
+ if (state.positions.length === 0) return state;
163
+ return { ...state, positions: [] };
164
+ }
165
+ function rememberPosition(state, position) {
166
+ const next = state.positions.filter((item) => item.cardId !== position.cardId);
167
+ next.push({
168
+ cardId: position.cardId,
169
+ x: finite(position.x),
170
+ y: finite(position.y)
171
+ });
172
+ return { ...state, positions: next.slice(-MAX_POSITIONS) };
173
+ }
174
+ function patchCanvas(state, patch) {
175
+ const anchors = { ...state.anchors };
176
+ if (patch.anchor === null) {
177
+ } else if (patch.anchor !== void 0) {
178
+ if (patch.anchor.cardId.length === 0) delete anchors[patch.anchor.threadId];
179
+ else anchors[patch.anchor.threadId] = patch.anchor.cardId.slice(0, 200);
180
+ }
181
+ return {
182
+ ...state,
183
+ autoProjection: patch.autoProjection ?? state.autoProjection,
184
+ workspaceTitle: patch.workspaceTitle === void 0 ? state.workspaceTitle : patch.workspaceTitle.trim().slice(0, MAX_TITLE),
185
+ showArchived: patch.showArchived ?? state.showArchived,
186
+ anchors
187
+ };
188
+ }
189
+ function readRevision(value) {
190
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value < 1e9 ? value : 0;
191
+ }
192
+ function readIds(value) {
193
+ if (!Array.isArray(value)) return [];
194
+ const ids = [];
195
+ for (const item of value) {
196
+ if (typeof item !== "string" || item.trim().length === 0 || ids.includes(item)) continue;
197
+ ids.push(item);
198
+ if (ids.length === MAX_IDS) break;
199
+ }
200
+ return ids;
201
+ }
202
+ function readAnchors(value) {
203
+ if (!isRecord(value)) return {};
204
+ const anchors = {};
205
+ for (const [threadId, cardId] of Object.entries(value)) {
206
+ if (threadId.trim().length === 0 || typeof cardId !== "string" || cardId.length === 0) continue;
207
+ anchors[threadId] = cardId.slice(0, 200);
208
+ }
209
+ return anchors;
210
+ }
211
+ function readPositions(value) {
212
+ if (!Array.isArray(value)) return [];
213
+ const positions = [];
214
+ for (const item of value) {
215
+ if (!isRecord(item) || typeof item.cardId !== "string") continue;
216
+ positions.push({ cardId: item.cardId, x: finite(item.x), y: finite(item.y) });
217
+ if (positions.length === MAX_POSITIONS) break;
218
+ }
219
+ return positions;
220
+ }
221
+ function finite(value) {
222
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
223
+ }
224
+
225
+ // src/sessionMap/canvasFile.ts
226
+ import * as NodeFSP from "node:fs/promises";
227
+ import * as NodePath from "node:path";
228
+ var MAX_ATTEMPTS = 8;
229
+ var STALE_LOCK_MS = 5e3;
230
+ function openCanvasFile(dataDir) {
231
+ const file = NodePath.join(dataDir, "workspaces.json");
232
+ const lock = `${file}.lock`;
233
+ let chain = Promise.resolve();
234
+ const read = async () => {
235
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
236
+ const stamp = await readStamp(file);
237
+ if (stamp !== null) return stamp.state;
238
+ }
239
+ return EMPTY_CANVAS;
240
+ };
241
+ const update = (change) => withLock(lock, async () => {
242
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
243
+ const before = await readStamp(file);
244
+ if (before === null) continue;
245
+ const next = {
246
+ ...change(before.state),
247
+ version: 1,
248
+ revision: before.revision + 1
249
+ };
250
+ const raced = await readStamp(file);
251
+ if (raced === null || raced.mtimeMs !== before.mtimeMs || raced.revision !== before.revision) {
252
+ continue;
253
+ }
254
+ await NodeFSP.mkdir(dataDir, { recursive: true });
255
+ const temporary = `${file}.${process.pid}.${attempt}.tmp`;
256
+ await NodeFSP.writeFile(temporary, canvasJson(next), "utf8");
257
+ const again = await readStamp(file);
258
+ if (again === null || again.mtimeMs !== before.mtimeMs || again.revision !== before.revision) {
259
+ await NodeFSP.rm(temporary, { force: true });
260
+ continue;
261
+ }
262
+ await NodeFSP.rename(temporary, file);
263
+ return next;
264
+ }
265
+ throw new Error("canvas file changed while it was being saved");
266
+ });
267
+ return {
268
+ read,
269
+ update(change) {
270
+ const run = chain.then(() => update(change));
271
+ chain = run.then(
272
+ () => void 0,
273
+ () => void 0
274
+ );
275
+ return run;
276
+ }
277
+ };
278
+ }
279
+ async function readStamp(file) {
280
+ let stat2;
281
+ try {
282
+ stat2 = await NodeFSP.stat(file);
283
+ } catch (error) {
284
+ if (isCode(error, "ENOENT")) return { mtimeMs: 0, revision: 0, state: EMPTY_CANVAS };
285
+ throw error;
286
+ }
287
+ const text2 = await NodeFSP.readFile(file, "utf8");
288
+ let again;
289
+ try {
290
+ again = await NodeFSP.stat(file);
291
+ } catch (error) {
292
+ if (isCode(error, "ENOENT")) return null;
293
+ throw error;
294
+ }
295
+ if (again.mtimeMs !== stat2.mtimeMs || again.size !== stat2.size) return null;
296
+ let state = EMPTY_CANVAS;
297
+ try {
298
+ state = parseCanvas(JSON.parse(text2));
299
+ } catch {
300
+ state = EMPTY_CANVAS;
301
+ }
302
+ return { mtimeMs: again.mtimeMs, revision: state.revision, state };
303
+ }
304
+ async function withLock(lock, body) {
305
+ await NodeFSP.mkdir(NodePath.dirname(lock), { recursive: true });
306
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
307
+ try {
308
+ const handle = await NodeFSP.open(lock, "wx");
309
+ try {
310
+ return await body();
311
+ } finally {
312
+ await handle.close();
313
+ await NodeFSP.rm(lock, { force: true });
314
+ }
315
+ } catch (error) {
316
+ if (!isCode(error, "EEXIST")) throw error;
317
+ await stealStaleLock(lock);
318
+ await delay(20 * (attempt + 1));
319
+ }
320
+ }
321
+ throw new Error("canvas file is locked");
322
+ }
323
+ async function stealStaleLock(lock) {
324
+ try {
325
+ const stat2 = await NodeFSP.stat(lock);
326
+ if (Date.now() - stat2.mtimeMs > STALE_LOCK_MS) await NodeFSP.rm(lock, { force: true });
327
+ } catch (error) {
328
+ if (!isCode(error, "ENOENT")) throw error;
329
+ }
330
+ }
331
+ function delay(ms) {
332
+ return new Promise((resolve) => {
333
+ setTimeout(resolve, ms);
334
+ });
335
+ }
336
+ function isCode(error, code) {
337
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
338
+ }
339
+
340
+ // src/ui/sessionMap/graph.ts
341
+ function resolveForkAnchor(messages, anchorId) {
342
+ const index = messages.findIndex((message) => message.messageId === anchorId);
343
+ if (index < 0) return null;
344
+ if (messages[index]?.role === "user") return { messageId: anchorId, walkedBack: false };
345
+ for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
346
+ const message = messages[cursor];
347
+ if (message?.role === "user") return { messageId: message.messageId, walkedBack: true };
348
+ }
349
+ return null;
350
+ }
351
+ function inheritedMessageCount(parent, sourceMessageId) {
352
+ if (parent === null || sourceMessageId === null) return 0;
353
+ const index = parent.findIndex((message) => message.messageId === sourceMessageId);
354
+ return index < 0 ? 0 : index;
355
+ }
356
+ function buildTurns(messages, tools, running, textLimit) {
357
+ const own = messages;
358
+ const turns = [];
359
+ for (let index = 0; index < own.length; index += 1) {
360
+ const message = own[index];
361
+ if (message === void 0 || message.role !== "user") continue;
362
+ const replies = [];
363
+ const errors = [];
364
+ for (let cursor = index + 1; cursor < own.length; cursor += 1) {
365
+ const next = own[cursor];
366
+ if (next === void 0 || next.role === "user") break;
367
+ if (next.role === "assistant") replies.push(next);
368
+ else if (next.text.trim().length > 0) errors.push(next);
369
+ }
370
+ const question = clipText(message.text, textLimit);
371
+ const answerSource = replies.at(-1);
372
+ const answer = clipText(answerSource?.text ?? "", textLimit);
373
+ const errorSource = errors.at(-1);
374
+ turns.push({
375
+ messageId: message.messageId,
376
+ question: question.text,
377
+ questionTruncated: question.truncated,
378
+ answer: answer.text,
379
+ answerTruncated: answer.truncated,
380
+ error: errorSource === void 0 ? null : clipText(errorSource.text, textLimit).text,
381
+ tools: [],
382
+ running: false
383
+ });
384
+ }
385
+ const kept = turns.slice(-MAX_TURNS_PER_THREAD);
386
+ const latest = kept.at(-1);
387
+ if (latest === void 0) return turns;
388
+ const withTools = {
389
+ ...latest,
390
+ tools: tools.slice(-MAX_TOOLS_PER_TURN).map((tool) => ({
391
+ callId: tool.callId,
392
+ title: tool.title.slice(0, MAX_TOOL_LABEL),
393
+ status: tool.status
394
+ })),
395
+ running
396
+ };
397
+ return [...kept.slice(0, -1), withTools];
398
+ }
399
+
400
+ // src/sessionMap/mapLive.ts
401
+ import {
402
+ readTurnProgress
403
+ } from "@ycodium-ai/plugin-api";
404
+
405
+ // src/ui/sessionMap/liveAnswer.ts
406
+ function liveSegmentForTurn(messages, segment) {
407
+ if (segment === void 0) return null;
408
+ let lastUser = -1;
409
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
410
+ if (messages[index]?.role === "user") {
411
+ lastUser = index;
412
+ break;
413
+ }
414
+ }
415
+ if (lastUser < 0) return null;
416
+ for (let index = lastUser + 1; index < messages.length; index += 1) {
417
+ const message = messages[index];
418
+ if (message?.messageId === segment.messageId && message.role === "assistant") return segment;
419
+ }
420
+ return null;
421
+ }
422
+ function applyLiveAnswer(thread, messages, segment, answer) {
423
+ const live = liveSegmentForTurn(messages, segment);
424
+ if (live === null || answer === null || thread.turns.length === 0) return thread;
425
+ const turns = thread.turns.slice();
426
+ const last = turns.at(-1);
427
+ if (last === void 0) return thread;
428
+ turns[turns.length - 1] = {
429
+ ...last,
430
+ answer: answer.text,
431
+ answerTruncated: answer.truncated,
432
+ running: live.streaming
433
+ };
434
+ return { ...thread, running: thread.running || live.streaming, turns };
435
+ }
436
+
437
+ // src/ui/sessionMap/toolStrip.ts
438
+ function record(value) {
439
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
440
+ return value;
441
+ }
442
+ function text(value) {
443
+ return typeof value === "string" ? value.trim() : "";
444
+ }
445
+ function toolTitleFromActivity(payload) {
446
+ const root = record(payload);
447
+ if (root === null) return "";
448
+ const data = record(root.data) ?? root;
449
+ const toolLabel = record(data.toolLabel) ?? record(root.toolLabel);
450
+ if (toolLabel !== null) {
451
+ if (toolLabel.descriptive === false) return "";
452
+ const detail = text(toolLabel.detail);
453
+ if (detail.length > 0) return detail;
454
+ return text(toolLabel.title);
455
+ }
456
+ return text(root.title);
457
+ }
458
+ function titlesByCallId(activities) {
459
+ const titles = /* @__PURE__ */ new Map();
460
+ for (const activity of activities) {
461
+ const root = record(activity.payload);
462
+ if (root === null) continue;
463
+ const data = record(root.data) ?? root;
464
+ const callId = text(data.toolCallId) || text(root.toolCallId);
465
+ if (callId.length === 0) continue;
466
+ const title = toolTitleFromActivity(activity.payload);
467
+ if (title.length > 0) titles.set(callId, title);
468
+ }
469
+ return titles;
470
+ }
471
+
472
+ // src/sessionMap/refreshGate.ts
473
+ var CARD_REFRESH_MS = 800;
474
+ var CardRefreshGate = class {
475
+ lastAt = /* @__PURE__ */ new Map();
476
+ waiting = /* @__PURE__ */ new Set();
477
+ deferred = /* @__PURE__ */ new Set();
478
+ note(input) {
479
+ if (input.threadId.length === 0) return { run: [], dueAt: null };
480
+ if (input.subscribers === 0 && !input.immediate) {
481
+ this.deferred.add(input.threadId);
482
+ return { run: [], dueAt: null };
483
+ }
484
+ this.deferred.delete(input.threadId);
485
+ this.waiting.add(input.threadId);
486
+ if (input.immediate) return this.flush(input.now);
487
+ const last = this.lastAt.get(input.threadId);
488
+ if (last === void 0 || input.now - last >= CARD_REFRESH_MS) return this.flush(input.now);
489
+ return { run: [], dueAt: last + CARD_REFRESH_MS };
490
+ }
491
+ flush(now) {
492
+ const run = [...this.waiting];
493
+ this.waiting.clear();
494
+ for (const id of run) this.lastAt.set(id, now);
495
+ return { run, dueAt: null };
496
+ }
497
+ takeDeferred() {
498
+ const ids = [...this.deferred];
499
+ this.deferred.clear();
500
+ return ids;
501
+ }
502
+ };
503
+ var TranscriptCache = class {
504
+ byThread = /* @__PURE__ */ new Map();
505
+ has(threadId) {
506
+ return this.byThread.has(threadId);
507
+ }
508
+ get(threadId) {
509
+ return this.byThread.get(threadId) ?? null;
510
+ }
511
+ seed(threadId, messages) {
512
+ this.byThread.set(
513
+ threadId,
514
+ messages.map((message) => ({ ...message }))
515
+ );
516
+ }
517
+ upsert(threadId, message) {
518
+ const list = this.byThread.get(threadId);
519
+ if (list === void 0) return false;
520
+ const index = list.findIndex((item) => item.messageId === message.messageId);
521
+ if (index < 0) list.push({ ...message });
522
+ else list[index] = { ...message };
523
+ return true;
524
+ }
525
+ invalidate(threadId) {
526
+ this.byThread.delete(threadId);
527
+ }
528
+ };
529
+
530
+ // src/sessionMap/runtime.ts
531
+ async function listSessionShells(ctx) {
532
+ const shells = [];
533
+ let cursor = null;
534
+ for (; ; ) {
535
+ const page = await ctx.projections.listThreads({
536
+ limit: THREAD_PAGE_SIZE,
537
+ cursor,
538
+ includeArchived: true
539
+ });
540
+ shells.push(...page.threads);
541
+ if (page.nextCursor === null || page.nextCursor === cursor) return shells;
542
+ cursor = page.nextCursor;
543
+ }
544
+ }
545
+ function threadFromShell(shell) {
546
+ return projectThread({
547
+ execution: {
548
+ threadId: shell.threadId,
549
+ projectId: shell.projectId,
550
+ title: shell.title,
551
+ updatedAt: shell.updatedAt,
552
+ archived: shell.archivedAt !== null,
553
+ running: runningOf(shell.sessionStatus),
554
+ sessionStatus: shell.sessionStatus,
555
+ forkedFrom: forkPointOf(shell.forkedFrom),
556
+ runtimeMode: shell.runtimeMode,
557
+ interactionMode: shell.interactionMode,
558
+ modelSelection: shell.modelSelection
559
+ },
560
+ messages: [],
561
+ parentMessages: null,
562
+ tools: [],
563
+ textLimit: MAX_PREVIEW_TEXT
564
+ });
565
+ }
566
+ function projectThread(input) {
567
+ const inherited = inheritedMessageCount(
568
+ input.parentMessages,
569
+ input.execution.forkedFrom?.sourceMessageId ?? null
570
+ );
571
+ const own = input.messages.slice(inherited);
572
+ return {
573
+ threadId: input.execution.threadId,
574
+ projectId: input.execution.projectId,
575
+ title: input.execution.title,
576
+ updatedAt: input.execution.updatedAt,
577
+ archived: input.execution.archived,
578
+ running: input.execution.running,
579
+ forkedFrom: input.execution.forkedFrom,
580
+ inheritedMessageCount: inherited,
581
+ cutWalkedBack: false,
582
+ turns: buildTurns(own, input.tools, input.execution.running, input.textLimit)
583
+ };
584
+ }
585
+ function boardSnapshot(input) {
586
+ const byId = new Map(input.threads.map((thread) => [thread.threadId, thread]));
587
+ const hidden = new Set(input.canvas.hiddenThreadIds);
588
+ const hiddenThreads = input.canvas.hiddenThreadIds.flatMap((threadId) => {
589
+ const thread = byId.get(threadId);
590
+ return thread === void 0 ? [] : [{ threadId, title: thread.title }];
591
+ });
592
+ const visible = input.threads.filter((thread) => {
593
+ if (hidden.has(thread.threadId)) return false;
594
+ if (!input.canvas.showArchived && thread.archived) return false;
595
+ return true;
596
+ });
597
+ const ranked = [...visible].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
598
+ return {
599
+ projects: input.projects,
600
+ threads: ranked.map(previewThread),
601
+ hiddenThreadIds: input.canvas.hiddenThreadIds,
602
+ hiddenThreads,
603
+ anchors: input.canvas.anchors,
604
+ workspaceTitle: input.canvas.workspaceTitle,
605
+ autoProjection: input.canvas.autoProjection,
606
+ showArchived: input.canvas.showArchived,
607
+ positions: input.canvas.positions
608
+ };
609
+ }
610
+ function previewThread(thread) {
611
+ return {
612
+ ...thread,
613
+ turns: thread.turns.map((turn) => {
614
+ const question = clipText(turn.question, MAX_PREVIEW_TEXT);
615
+ const answer = clipText(turn.answer, MAX_PREVIEW_TEXT);
616
+ return {
617
+ ...turn,
618
+ question: question.text,
619
+ questionTruncated: turn.questionTruncated || question.truncated,
620
+ answer: answer.text,
621
+ answerTruncated: turn.answerTruncated || answer.truncated
622
+ };
623
+ })
624
+ };
625
+ }
626
+ function runningOf(status) {
627
+ return status === "starting" || status === "running";
628
+ }
629
+ function executionRow(execution) {
630
+ const running = runningOf(execution.session?.status ?? null) || execution.latestTurn?.state === "running";
631
+ return {
632
+ threadId: execution.threadId,
633
+ projectId: execution.projectId,
634
+ title: execution.title,
635
+ updatedAt: execution.updatedAt,
636
+ archived: execution.archivedAt !== null,
637
+ running,
638
+ sessionStatus: execution.session?.status ?? null,
639
+ forkedFrom: forkPointOf(execution.forkedFrom),
640
+ runtimeMode: execution.runtimeMode,
641
+ interactionMode: execution.interactionMode,
642
+ modelSelection: execution.modelSelection
643
+ };
644
+ }
645
+ async function executionOf(ctx, threadId) {
646
+ const execution = await ctx.projections.getThreadExecution(threadId);
647
+ if (execution === null) return null;
648
+ return executionRow(execution);
649
+ }
650
+
651
+ // src/sessionMap/mapLive.ts
652
+ function createSessionMapHub(ctx, canvas, options = {}) {
653
+ return new SessionMapHub(ctx, canvas, options.timers !== false);
654
+ }
655
+ var SessionMapHub = class {
656
+ gate = new CardRefreshGate();
657
+ transcripts = new TranscriptCache();
658
+ cards = /* @__PURE__ */ new Map();
659
+ tools = /* @__PURE__ */ new Map();
660
+ toolTitles = /* @__PURE__ */ new Map();
661
+ executions = /* @__PURE__ */ new Map();
662
+ live = /* @__PURE__ */ new Map();
663
+ pendingMessage = /* @__PURE__ */ new Map();
664
+ queued = /* @__PURE__ */ new Set();
665
+ removed = /* @__PURE__ */ new Set();
666
+ emitters = /* @__PURE__ */ new Set();
667
+ shells = null;
668
+ projects = null;
669
+ directoryFlight = null;
670
+ tail = Promise.resolve();
671
+ wantFull = false;
672
+ dueTimer = null;
673
+ ctx;
674
+ canvas;
675
+ timers;
676
+ constructor(ctx, canvas, timers) {
677
+ this.ctx = ctx;
678
+ this.canvas = canvas;
679
+ this.timers = timers;
680
+ }
681
+ subscribe(emit) {
682
+ this.emitters.add(emit);
683
+ const deferred = this.gate.takeDeferred();
684
+ this.enqueue(deferred, true);
685
+ return () => {
686
+ this.emitters.delete(emit);
687
+ };
688
+ }
689
+ onMessage(threadId, event, now = Date.now()) {
690
+ if (threadId.length === 0) return;
691
+ const messageId = messageIdOf(event);
692
+ if (messageId !== null) {
693
+ const pending = this.pendingMessage.get(threadId) ?? /* @__PURE__ */ new Set();
694
+ pending.add(messageId);
695
+ this.pendingMessage.set(threadId, pending);
696
+ }
697
+ this.note(threadId, now, false);
698
+ }
699
+ onProgress(event, now = Date.now()) {
700
+ const progress = readTurnProgress(event);
701
+ const threadId = event.threadId;
702
+ if (progress === null || threadId === null || threadId.length === 0) return;
703
+ const cached = this.executions.get(threadId);
704
+ if (cached !== void 0 && cached.running !== (progress.phase !== "ended")) {
705
+ this.executions.set(threadId, { ...cached, running: progress.phase !== "ended" });
706
+ }
707
+ const known = this.toolTitles.get(threadId);
708
+ this.tools.set(
709
+ threadId,
710
+ progress.tools.map((tool) => ({
711
+ callId: tool.toolCallId,
712
+ title: known?.get(tool.toolCallId) ?? "",
713
+ status: tool.status
714
+ }))
715
+ );
716
+ if (progress.phase === "ended") this.live.delete(threadId);
717
+ else {
718
+ const segment = progress.segments.at(-1);
719
+ if (segment !== void 0) this.live.set(threadId, segment);
720
+ }
721
+ this.note(threadId, now, false);
722
+ }
723
+ onDirectory(type, threadId) {
724
+ if (type === "thread.deleted") {
725
+ this.drop(threadId);
726
+ this.removed.add(threadId);
727
+ if (this.shells !== null) {
728
+ this.shells = this.shells.filter((shell) => shell.threadId !== threadId);
729
+ }
730
+ if (this.emitters.size > 0) this.enqueue([], false);
731
+ return;
732
+ }
733
+ this.shells = null;
734
+ this.projects = null;
735
+ if (threadId.length > 0) {
736
+ this.transcripts.invalidate(threadId);
737
+ this.executions.delete(threadId);
738
+ }
739
+ if (threadId.length > 0) this.note(threadId, Date.now(), false);
740
+ else if (this.emitters.size > 0) this.enqueue([], true);
741
+ }
742
+ /** 用户命令:立刻读这一条,不管节流窗口。 */
743
+ touch(threadId, now = Date.now()) {
744
+ this.executions.delete(threadId);
745
+ this.note(threadId, now, true);
746
+ }
747
+ canvasChanged() {
748
+ this.enqueue([], true);
749
+ }
750
+ flushDue(now) {
751
+ this.clearTimer();
752
+ const step = this.gate.flush(now);
753
+ if (step.run.length > 0) this.enqueue(step.run, false);
754
+ }
755
+ settled() {
756
+ return this.tail;
757
+ }
758
+ stop() {
759
+ this.clearTimer();
760
+ }
761
+ note(threadId, now, immediate) {
762
+ const step = this.gate.note({
763
+ threadId,
764
+ now,
765
+ subscribers: this.emitters.size,
766
+ immediate
767
+ });
768
+ if (step.run.length > 0) this.enqueue(step.run, false);
769
+ if (step.dueAt !== null && this.timers) this.arm(step.dueAt);
770
+ }
771
+ arm(dueAt) {
772
+ if (this.dueTimer !== null) return;
773
+ const wait = Math.max(0, dueAt - Date.now());
774
+ this.dueTimer = setTimeout(() => {
775
+ this.dueTimer = null;
776
+ this.flushDue(Date.now());
777
+ }, wait);
778
+ }
779
+ clearTimer() {
780
+ if (this.dueTimer === null) return;
781
+ clearTimeout(this.dueTimer);
782
+ this.dueTimer = null;
783
+ }
784
+ enqueue(ids, full) {
785
+ for (const id of ids) {
786
+ if (id.length > 0) this.queued.add(id);
787
+ }
788
+ if (full) this.wantFull = true;
789
+ this.tail = this.tail.then(() => this.drain()).catch((error) => {
790
+ this.ctx.log.warn("session map projection skipped a thread", {
791
+ detail: error instanceof Error ? error.message : "unknown"
792
+ });
793
+ });
794
+ }
795
+ async drain() {
796
+ const ids = [...this.queued];
797
+ const full = this.wantFull;
798
+ this.queued.clear();
799
+ this.wantFull = false;
800
+ for (const id of ids) await this.projectStored(id);
801
+ if (this.emitters.size === 0) return;
802
+ if (full || this.shells === null) await this.emitFull();
803
+ else if (ids.length > 0 || this.removed.size > 0) this.emitPatch(ids);
804
+ }
805
+ async projectStored(threadId) {
806
+ const projected = await this.projectOne(threadId);
807
+ if (projected === null) {
808
+ this.drop(threadId);
809
+ this.removed.add(threadId);
810
+ return;
811
+ }
812
+ this.cards.set(threadId, projected);
813
+ await this.ctx.sessionProjections.set({
814
+ name: PROJECTION_NAME,
815
+ threadId,
816
+ value: jsonValue(projected)
817
+ });
818
+ }
819
+ async projectOne(threadId) {
820
+ const execution = await this.executionFor(threadId);
821
+ if (execution === null) return null;
822
+ const messages = await this.loadMessages(threadId);
823
+ let parentMessages = null;
824
+ if (execution.forkedFrom !== null) {
825
+ parentMessages = await this.loadParent(execution.forkedFrom.sourceThreadId);
826
+ }
827
+ const segment = this.live.get(threadId);
828
+ const live = segment === void 0 ? void 0 : segment;
829
+ let answer = null;
830
+ if (live !== void 0) {
831
+ const message = await this.ctx.projections.getThreadMessage(live.messageId);
832
+ if (message !== null) {
833
+ this.transcripts.upsert(threadId, messageOf(message));
834
+ const clipped = clipText(message.text, MAX_CARD_TEXT);
835
+ answer = { text: clipped.text, truncated: clipped.truncated };
836
+ }
837
+ }
838
+ const projected = projectThread({
839
+ execution,
840
+ messages: this.transcripts.get(threadId) ?? messages,
841
+ parentMessages,
842
+ tools: this.tools.get(threadId) ?? [],
843
+ textLimit: MAX_CARD_TEXT
844
+ });
845
+ return applyLiveAnswer(projected, this.transcripts.get(threadId) ?? messages, live, answer);
846
+ }
847
+ async executionFor(threadId) {
848
+ const cached = this.executions.get(threadId);
849
+ if (cached !== void 0) return cached;
850
+ const row = await executionOf(this.ctx, threadId);
851
+ if (row !== null) this.executions.set(threadId, row);
852
+ return row;
853
+ }
854
+ async seedTranscript(threadId) {
855
+ if (this.transcripts.has(threadId)) return;
856
+ const thread = await this.ctx.projections.getThread(threadId, { includeActivities: true });
857
+ this.transcripts.seed(threadId, transcriptOf(thread));
858
+ this.rememberTitles(threadId, titlesByCallId(thread?.activities ?? []));
859
+ }
860
+ rememberTitles(threadId, titles) {
861
+ if (titles.size === 0) return;
862
+ const known = this.toolTitles.get(threadId) ?? /* @__PURE__ */ new Map();
863
+ for (const [id, title] of titles) known.set(id, title);
864
+ this.toolTitles.set(threadId, known);
865
+ const tools = this.tools.get(threadId);
866
+ if (tools === void 0) return;
867
+ this.tools.set(
868
+ threadId,
869
+ tools.map((tool) => ({ ...tool, title: known.get(tool.callId) ?? tool.title }))
870
+ );
871
+ }
872
+ async loadMessages(threadId) {
873
+ const pending = this.pendingMessage.get(threadId);
874
+ this.pendingMessage.delete(threadId);
875
+ await this.seedTranscript(threadId);
876
+ if (pending !== void 0) {
877
+ for (const messageId of pending) {
878
+ const message = await this.ctx.projections.getThreadMessage(messageId);
879
+ if (message !== null) this.transcripts.upsert(threadId, messageOf(message));
880
+ }
881
+ }
882
+ return this.transcripts.get(threadId) ?? [];
883
+ }
884
+ async loadParent(threadId) {
885
+ if (!this.transcripts.has(threadId)) {
886
+ const thread = await this.ctx.projections.getThread(threadId);
887
+ this.transcripts.seed(threadId, transcriptOf(thread));
888
+ }
889
+ return this.transcripts.get(threadId) ?? [];
890
+ }
891
+ async emitFull() {
892
+ await this.ensureDirectory();
893
+ const shells = this.shells ?? [];
894
+ const ids = shells.map((shell) => shell.threadId);
895
+ const current = await this.canvas.read();
896
+ const state = admitThreads(current, ids) === current ? current : await this.canvas.update((latest) => admitThreads(latest, ids));
897
+ const known = new Set(ids);
898
+ for (const threadId of this.cards.keys()) {
899
+ if (!known.has(threadId)) this.drop(threadId);
900
+ }
901
+ const threads = shells.map((shell) => this.cards.get(shell.threadId) ?? threadFromShell(shell));
902
+ const frame = boardSnapshot({
903
+ projects: this.projects ?? [],
904
+ threads,
905
+ canvas: state
906
+ });
907
+ this.removed.clear();
908
+ for (const emit of this.emitters) emit(frame);
909
+ }
910
+ emitPatch(ids) {
911
+ const removedThreadIds = [...this.removed];
912
+ this.removed.clear();
913
+ const threads = ids.flatMap((id) => {
914
+ const card = this.cards.get(id);
915
+ return card === void 0 ? [] : [card];
916
+ });
917
+ const frame = { patch: true, threads, removedThreadIds };
918
+ for (const emit of this.emitters) emit(frame);
919
+ }
920
+ ensureDirectory() {
921
+ if (this.shells !== null && this.projects !== null) return Promise.resolve();
922
+ if (this.directoryFlight !== null) return this.directoryFlight;
923
+ this.directoryFlight = this.loadDirectory().finally(() => {
924
+ this.directoryFlight = null;
925
+ });
926
+ return this.directoryFlight;
927
+ }
928
+ async loadDirectory() {
929
+ const shells = await listSessionShells(this.ctx);
930
+ const projects = (await this.ctx.projections.listProjects()).map((project) => ({
931
+ projectId: project.projectId,
932
+ title: project.title,
933
+ workspaceRoot: project.workspaceRoot,
934
+ archived: project.archivedAt != null
935
+ }));
936
+ this.shells = shells;
937
+ this.projects = projects;
938
+ }
939
+ drop(threadId) {
940
+ this.cards.delete(threadId);
941
+ this.transcripts.invalidate(threadId);
942
+ this.tools.delete(threadId);
943
+ this.toolTitles.delete(threadId);
944
+ this.executions.delete(threadId);
945
+ this.live.delete(threadId);
946
+ this.pendingMessage.delete(threadId);
947
+ }
948
+ };
949
+ function messageIdOf(event) {
950
+ if (!isRecord(event.payload)) return null;
951
+ const messageId = readString(event.payload.messageId);
952
+ return messageId.length > 0 ? messageId : null;
953
+ }
954
+ function transcriptOf(thread) {
955
+ if (thread === null) return [];
956
+ return thread.messages.map(messageOf);
957
+ }
958
+ function messageOf(message) {
959
+ return { messageId: message.messageId, role: message.role, text: message.text };
960
+ }
961
+ function jsonValue(value) {
962
+ return JSON.parse(JSON.stringify(value));
963
+ }
964
+
965
+ // src/sessionMap/activate.ts
966
+ function activateSessionMap(ctx) {
967
+ const canvas = openCanvasFile(ctx.paths.dataDir);
968
+ const hub = createSessionMapHub(ctx, canvas);
969
+ ctx.sessionProjections.register({ name: PROJECTION_NAME });
970
+ ctx.reactor({
971
+ name: "project-cards",
972
+ events: [
973
+ "thread.created",
974
+ "thread.deleted",
975
+ "thread.message-sent",
976
+ "thread.archived",
977
+ "thread.unarchived"
978
+ ],
979
+ handle: (event) => {
980
+ const threadId = event.aggregateKind === "thread" ? event.aggregateId : "";
981
+ if (event.type === "thread.message-sent") {
982
+ hub.onMessage(threadId, event);
983
+ return;
984
+ }
985
+ hub.onDirectory(event.type, threadId);
986
+ }
987
+ });
988
+ ctx.events.on(TURN_PROGRESS_TRIGGER_KIND, (event) => {
989
+ hub.onProgress(event);
990
+ });
991
+ ctx.command({
992
+ name: "focus",
993
+ description: "Load one session onto the map.",
994
+ scope: "read",
995
+ payloadSchema: {
996
+ type: "object",
997
+ properties: { threadId: { type: "string" } },
998
+ required: ["threadId"]
999
+ },
1000
+ handle: async (payload) => {
1001
+ hub.touch(readString(payload["threadId"]));
1002
+ return { ok: true };
1003
+ }
1004
+ });
1005
+ ctx.command({
1006
+ name: "snapshot",
1007
+ description: "Read the session map.",
1008
+ scope: "read",
1009
+ payloadSchema: { type: "object", properties: {} },
1010
+ handle: async () => {
1011
+ const frames = [];
1012
+ const stop = hub.subscribe((frame) => {
1013
+ frames.push(frame);
1014
+ });
1015
+ await hub.settled();
1016
+ stop();
1017
+ return frames.at(-1) ?? { projects: [], threads: [] };
1018
+ }
1019
+ });
1020
+ ctx.stream({
1021
+ name: "live",
1022
+ description: "Follow the session map.",
1023
+ scope: "read",
1024
+ buffer: { latest: 1 },
1025
+ payloadSchema: { type: "object", properties: {} },
1026
+ handle: async (_payload, emit, signal) => {
1027
+ const stop = hub.subscribe((frame) => {
1028
+ emit(frame);
1029
+ });
1030
+ try {
1031
+ await abort(signal);
1032
+ } finally {
1033
+ stop();
1034
+ }
1035
+ }
1036
+ });
1037
+ ctx.command({
1038
+ name: "detail",
1039
+ description: "Read one session for the detail pane.",
1040
+ scope: "read",
1041
+ payloadSchema: {
1042
+ type: "object",
1043
+ properties: { threadId: { type: "string" } },
1044
+ required: ["threadId"]
1045
+ },
1046
+ handle: async (payload) => detailOf(ctx, readString(payload["threadId"]))
1047
+ });
1048
+ ctx.command({
1049
+ name: "preferences",
1050
+ description: "Read or update canvas preferences.",
1051
+ scope: "operate",
1052
+ payloadSchema: { type: "object", properties: { patch: {} } },
1053
+ handle: async (payload) => {
1054
+ const patch = payload["patch"];
1055
+ const next = patch === void 0 ? await canvas.read() : await canvas.update((state) => patchCanvas(state, preferencePatch(patch)));
1056
+ hub.canvasChanged();
1057
+ return next;
1058
+ }
1059
+ });
1060
+ ctx.command({
1061
+ name: "move",
1062
+ description: "Save a card position.",
1063
+ scope: "operate",
1064
+ payloadSchema: {
1065
+ type: "object",
1066
+ properties: {
1067
+ cardId: { type: "string" },
1068
+ x: { type: "number" },
1069
+ y: { type: "number" }
1070
+ },
1071
+ required: ["cardId", "x", "y"]
1072
+ },
1073
+ handle: async (payload) => {
1074
+ const next = await canvas.update(
1075
+ (state) => rememberPosition(state, {
1076
+ cardId: readString(payload["cardId"]),
1077
+ x: typeof payload["x"] === "number" ? payload["x"] : 0,
1078
+ y: typeof payload["y"] === "number" ? payload["y"] : 0
1079
+ })
1080
+ );
1081
+ hub.canvasChanged();
1082
+ return { positions: next.positions };
1083
+ }
1084
+ });
1085
+ ctx.command({
1086
+ name: "tidy",
1087
+ description: "Drop saved card positions so the map uses the fork layout.",
1088
+ scope: "operate",
1089
+ payloadSchema: { type: "object", properties: {} },
1090
+ handle: async () => {
1091
+ await canvas.update(clearPositions);
1092
+ hub.canvasChanged();
1093
+ return { ok: true };
1094
+ }
1095
+ });
1096
+ ctx.command({
1097
+ name: "hide",
1098
+ description: "Hide a session card without deleting the session.",
1099
+ scope: "operate",
1100
+ payloadSchema: {
1101
+ type: "object",
1102
+ properties: { threadId: { type: "string" } },
1103
+ required: ["threadId"]
1104
+ },
1105
+ handle: async (payload) => {
1106
+ const threadId = readString(payload["threadId"]);
1107
+ await canvas.update((state) => hideThread(state, threadId));
1108
+ hub.canvasChanged();
1109
+ return { ok: true };
1110
+ }
1111
+ });
1112
+ ctx.command({
1113
+ name: "restore",
1114
+ description: "Show a hidden session card again.",
1115
+ scope: "operate",
1116
+ payloadSchema: {
1117
+ type: "object",
1118
+ properties: { threadId: { type: "string" } },
1119
+ required: ["threadId"]
1120
+ },
1121
+ handle: async (payload) => {
1122
+ await canvas.update((state) => restoreThread(state, readString(payload["threadId"])));
1123
+ hub.canvasChanged();
1124
+ return { ok: true };
1125
+ }
1126
+ });
1127
+ ctx.command({
1128
+ name: "create",
1129
+ description: "Open a session in a project and send the first message.",
1130
+ scope: "operate",
1131
+ payloadSchema: {
1132
+ type: "object",
1133
+ properties: {
1134
+ projectId: { type: "string" },
1135
+ text: { type: "string" }
1136
+ },
1137
+ required: ["projectId", "text"]
1138
+ },
1139
+ handle: async (payload) => createSession(
1140
+ ctx,
1141
+ readString(payload["projectId"]),
1142
+ readString(payload["text"]),
1143
+ (id) => hub.touch(id)
1144
+ )
1145
+ });
1146
+ ctx.command({
1147
+ name: "send",
1148
+ description: "Send a follow-up into an existing session.",
1149
+ scope: "operate",
1150
+ payloadSchema: {
1151
+ type: "object",
1152
+ properties: {
1153
+ threadId: { type: "string" },
1154
+ text: { type: "string" }
1155
+ },
1156
+ required: ["threadId", "text"]
1157
+ },
1158
+ handle: async (payload) => sendFollowUp(
1159
+ ctx,
1160
+ readString(payload["threadId"]),
1161
+ readString(payload["text"]),
1162
+ (id) => hub.touch(id)
1163
+ )
1164
+ });
1165
+ ctx.command({
1166
+ name: "fork",
1167
+ description: "Fork at a user message, then send the draft.",
1168
+ scope: "operate",
1169
+ payloadSchema: {
1170
+ type: "object",
1171
+ properties: {
1172
+ threadId: { type: "string" },
1173
+ messageId: { type: "string" },
1174
+ text: { type: "string" }
1175
+ },
1176
+ required: ["threadId", "messageId", "text"]
1177
+ },
1178
+ handle: async (payload) => forkSession(
1179
+ ctx,
1180
+ readString(payload["threadId"]),
1181
+ readString(payload["messageId"]),
1182
+ readString(payload["text"]),
1183
+ (id) => hub.touch(id)
1184
+ )
1185
+ });
1186
+ }
1187
+ function transcriptOf2(thread) {
1188
+ if (thread === null) return [];
1189
+ return thread.messages.map(messageOf2);
1190
+ }
1191
+ function messageOf2(message) {
1192
+ return { messageId: message.messageId, role: message.role, text: message.text };
1193
+ }
1194
+ async function detailOf(ctx, threadId) {
1195
+ const thread = await ctx.projections.getThread(threadId);
1196
+ if (thread === null) return { found: false, messages: [] };
1197
+ const tail = thread.messages.slice(-MAX_DETAIL_MESSAGES);
1198
+ return {
1199
+ found: true,
1200
+ title: thread.title,
1201
+ omitted: Math.max(0, thread.messages.length - tail.length),
1202
+ messages: tail.map((message) => {
1203
+ const clipped = clipText(message.text, MAX_DETAIL_TEXT);
1204
+ return {
1205
+ messageId: message.messageId,
1206
+ role: message.role,
1207
+ text: clipped.text,
1208
+ truncated: clipped.truncated
1209
+ };
1210
+ })
1211
+ };
1212
+ }
1213
+ async function createSession(ctx, projectId, text2, refresh) {
1214
+ const trimmed = text2.trim();
1215
+ if (trimmed.length === 0) return { ok: false, code: "empty" };
1216
+ const project = await ctx.projections.getProject(projectId);
1217
+ if (project === null) return { ok: false, code: "missing-project" };
1218
+ const selection = await ctx.providers.resolveSelection({ base: project.defaultModelSelection });
1219
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1220
+ const threadId = NodeCrypto.randomUUID();
1221
+ const commands = createCommands({
1222
+ projectId,
1223
+ title: titleFromText(trimmed),
1224
+ text: trimmed,
1225
+ modelSelection: selection.modelSelection,
1226
+ runtimeMode: "full-access",
1227
+ interactionMode: "default",
1228
+ now,
1229
+ threadId,
1230
+ commandId: NodeCrypto.randomUUID(),
1231
+ messageId: NodeCrypto.randomUUID(),
1232
+ turnCommandId: NodeCrypto.randomUUID()
1233
+ });
1234
+ for (const command of commands) await ctx.commands.dispatch(command);
1235
+ refresh(threadId);
1236
+ return { ok: true, threadId, walkedBack: false };
1237
+ }
1238
+ async function sendFollowUp(ctx, threadId, text2, refresh) {
1239
+ const trimmed = text2.trim();
1240
+ if (trimmed.length === 0) return { ok: false, code: "empty" };
1241
+ const execution = await executionOf(ctx, threadId);
1242
+ if (execution === null) return { ok: false, code: "missing-thread" };
1243
+ await ctx.commands.dispatch(
1244
+ turnCommand({
1245
+ threadId,
1246
+ text: trimmed,
1247
+ now: (/* @__PURE__ */ new Date()).toISOString(),
1248
+ messageId: NodeCrypto.randomUUID(),
1249
+ commandId: NodeCrypto.randomUUID(),
1250
+ runtimeMode: execution.runtimeMode,
1251
+ interactionMode: execution.interactionMode,
1252
+ modelSelection: execution.modelSelection
1253
+ })
1254
+ );
1255
+ refresh(threadId);
1256
+ return { ok: true, threadId, walkedBack: false };
1257
+ }
1258
+ async function forkSession(ctx, threadId, messageId, text2, refresh) {
1259
+ const trimmed = text2.trim();
1260
+ if (trimmed.length === 0) return { ok: false, code: "empty" };
1261
+ const thread = await ctx.projections.getThread(threadId);
1262
+ if (thread === null) return { ok: false, code: "missing-thread" };
1263
+ const anchor = resolveForkAnchor(transcriptOf2(thread), messageId);
1264
+ if (anchor === null) return { ok: false, code: "no-user-message" };
1265
+ const execution = await executionOf(ctx, threadId);
1266
+ const childId = NodeCrypto.randomUUID();
1267
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1268
+ await ctx.commands.dispatch(
1269
+ forkCommand({
1270
+ sourceThreadId: threadId,
1271
+ sourceMessageId: anchor.messageId,
1272
+ now,
1273
+ threadId: childId,
1274
+ commandId: NodeCrypto.randomUUID()
1275
+ })
1276
+ );
1277
+ await ctx.commands.dispatch(
1278
+ turnCommand({
1279
+ threadId: childId,
1280
+ text: trimmed,
1281
+ now,
1282
+ messageId: NodeCrypto.randomUUID(),
1283
+ commandId: NodeCrypto.randomUUID(),
1284
+ runtimeMode: execution?.runtimeMode ?? "full-access",
1285
+ interactionMode: execution?.interactionMode ?? "default",
1286
+ modelSelection: execution === null ? null : execution.modelSelection
1287
+ })
1288
+ );
1289
+ refresh(threadId);
1290
+ refresh(childId);
1291
+ return { ok: true, threadId: childId, walkedBack: anchor.walkedBack };
1292
+ }
1293
+ function preferencePatch(value) {
1294
+ if (!isRecord(value)) return {};
1295
+ const anchor = isRecord(value.anchor) ? {
1296
+ threadId: readString(value.anchor.threadId),
1297
+ cardId: readString(value.anchor.cardId)
1298
+ } : void 0;
1299
+ return {
1300
+ ...typeof value.autoProjection === "boolean" ? { autoProjection: value.autoProjection } : {},
1301
+ ...typeof value.workspaceTitle === "string" ? { workspaceTitle: value.workspaceTitle } : {},
1302
+ ...typeof value.showArchived === "boolean" ? { showArchived: value.showArchived } : {},
1303
+ ...anchor === void 0 ? {} : { anchor }
1304
+ };
1305
+ }
1306
+ function abort(signal) {
1307
+ return new Promise((resolve) => {
1308
+ if (signal.aborted) {
1309
+ resolve();
1310
+ return;
1311
+ }
1312
+ signal.addEventListener("abort", () => resolve(), { once: true });
1313
+ });
1314
+ }
1315
+
1316
+ // src/index.ts
3
1317
  var index_default = definePlugin({
4
1318
  id: "conversation",
5
1319
  apiVersion: 3,
6
- activate: () => {
1320
+ activate(ctx) {
1321
+ activateSessionMap(ctx);
7
1322
  }
8
1323
  });
9
1324
  export {