@ycodium-ai/plugin-conversation 0.2.3244 → 0.2.3250

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
@@ -3,51 +3,9 @@ import { definePlugin } from "@ycodium-ai/plugin-api";
3
3
 
4
4
  // src/sessionMap/activate.ts
5
5
  import * as NodeCrypto from "node:crypto";
6
- import {
7
- TURN_PROGRESS_TRIGGER_KIND,
8
- projectOrNull
9
- } from "@ycodium-ai/plugin-api";
6
+ import { TURN_PROGRESS_TRIGGER_KIND } from "@ycodium-ai/plugin-api";
10
7
 
11
8
  // src/sessionMap/actions.ts
12
- function createCommands(input) {
13
- return [
14
- {
15
- requestId: 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
- },
25
- turnCommand({
26
- threadId: input.threadId,
27
- text: input.text,
28
- messageId: input.messageId,
29
- commandId: input.turnCommandId,
30
- runtimeMode: input.runtimeMode,
31
- interactionMode: input.interactionMode,
32
- modelSelection: input.modelSelection
33
- })
34
- ];
35
- }
36
- function turnCommand(input) {
37
- return {
38
- requestId: input.commandId,
39
- threadId: input.threadId,
40
- message: {
41
- messageId: input.messageId,
42
- role: "user",
43
- text: input.text,
44
- attachments: []
45
- },
46
- runtimeMode: input.runtimeMode,
47
- interactionMode: input.interactionMode,
48
- ...input.modelSelection == null ? {} : { modelSelection: input.modelSelection }
49
- };
50
- }
51
9
  function forkCommand(input) {
52
10
  return {
53
11
  requestId: input.commandId,
@@ -56,11 +14,6 @@ function forkCommand(input) {
56
14
  sourceMessageId: input.sourceMessageId
57
15
  };
58
16
  }
59
- function titleFromText(text2) {
60
- const line = text2.trim().split("\n")[0] ?? "";
61
- const title = line.trim().slice(0, 80);
62
- return title.length > 0 ? title : "New session";
63
- }
64
17
  function forkPointOf(value) {
65
18
  if (typeof value !== "object" || value === null) return null;
66
19
  const point = value;
@@ -73,13 +26,12 @@ function forkPointOf(value) {
73
26
  // src/ui/sessionMap/limits.ts
74
27
  var MAX_CARD_TEXT = 8e3;
75
28
  var MAX_PREVIEW_TEXT = 480;
76
- var MAX_DETAIL_MESSAGES = 40;
77
- var MAX_DETAIL_TEXT = 32e3;
78
29
  var MAX_TURNS_PER_THREAD = 48;
79
30
  var MAX_TOOLS_PER_TURN = 12;
80
31
  var MAX_TOOL_LABEL = 180;
81
32
  var THREAD_PAGE_SIZE = 200;
82
33
  var PROJECTION_NAME = "card";
34
+ var MAX_LOAD_PER_CALL = 60;
83
35
 
84
36
  // src/ui/sessionMap/text.ts
85
37
  function clipText(value, limit = MAX_CARD_TEXT) {
@@ -127,22 +79,8 @@ function canvasJson(state) {
127
79
  return `${JSON.stringify(state)}
128
80
  `;
129
81
  }
130
- function admitThreads(state, threadIds) {
131
- const seen = new Set(state.seenThreadIds);
132
- const hidden = new Set(state.hiddenThreadIds);
133
- let changed = false;
134
- for (const threadId of threadIds) {
135
- if (threadId.length === 0 || seen.has(threadId)) continue;
136
- seen.add(threadId);
137
- changed = true;
138
- if (!state.autoProjection) hidden.add(threadId);
139
- }
140
- if (!changed) return state;
141
- return {
142
- ...state,
143
- seenThreadIds: [...seen].slice(-MAX_IDS),
144
- hiddenThreadIds: [...hidden].slice(-MAX_IDS)
145
- };
82
+ function showArchived(state, show) {
83
+ return state.showArchived === show ? state : { ...state, showArchived: show };
146
84
  }
147
85
  function hideThread(state, threadId) {
148
86
  if (state.hiddenThreadIds.includes(threadId)) return state;
@@ -152,34 +90,6 @@ function restoreThread(state, threadId) {
152
90
  if (!state.hiddenThreadIds.includes(threadId)) return state;
153
91
  return { ...state, hiddenThreadIds: state.hiddenThreadIds.filter((id) => id !== threadId) };
154
92
  }
155
- function clearPositions(state) {
156
- if (state.positions.length === 0) return state;
157
- return { ...state, positions: [] };
158
- }
159
- function rememberPosition(state, position) {
160
- const next = state.positions.filter((item) => item.cardId !== position.cardId);
161
- next.push({
162
- cardId: position.cardId,
163
- x: finite(position.x),
164
- y: finite(position.y)
165
- });
166
- return { ...state, positions: next.slice(-MAX_POSITIONS) };
167
- }
168
- function patchCanvas(state, patch) {
169
- const anchors = { ...state.anchors };
170
- if (patch.anchor === null) {
171
- } else if (patch.anchor !== void 0) {
172
- if (patch.anchor.cardId.length === 0) delete anchors[patch.anchor.threadId];
173
- else anchors[patch.anchor.threadId] = patch.anchor.cardId.slice(0, 200);
174
- }
175
- return {
176
- ...state,
177
- autoProjection: patch.autoProjection ?? state.autoProjection,
178
- workspaceTitle: patch.workspaceTitle === void 0 ? state.workspaceTitle : patch.workspaceTitle.trim().slice(0, MAX_TITLE),
179
- showArchived: patch.showArchived ?? state.showArchived,
180
- anchors
181
- };
182
- }
183
93
  function readRevision(value) {
184
94
  return typeof value === "number" && Number.isInteger(value) && value >= 0 && value < 1e9 ? value : 0;
185
95
  }
@@ -486,15 +396,28 @@ var TranscriptCache = class {
486
396
  }
487
397
  };
488
398
 
399
+ // src/sessionMap/turnOutline.ts
400
+ function buildTurnOutline(messages, failedIds, waitingId) {
401
+ const failed = new Set(failedIds);
402
+ const users = messages.filter((message) => message.role === "user");
403
+ return users.map((message, index) => ({
404
+ messageId: message.messageId,
405
+ ordinal: index + 1,
406
+ ...failed.has(message.messageId) ? { failed: true } : {},
407
+ ...message.messageId === waitingId ? { waiting: true } : {}
408
+ }));
409
+ }
410
+
489
411
  // src/sessionMap/runtime.ts
490
- async function listSessionShells(ctx) {
412
+ async function listSessionShells(ctx, projectId) {
491
413
  const shells = [];
492
414
  let cursor = null;
493
415
  for (; ; ) {
494
416
  const page = await ctx.threads.list({
495
417
  limit: THREAD_PAGE_SIZE,
496
418
  cursor,
497
- archived: "include"
419
+ archived: "include",
420
+ ...projectId === void 0 ? {} : { projectId }
498
421
  });
499
422
  shells.push(...page.items);
500
423
  if (page.nextCursor === null || page.nextCursor === cursor) return shells;
@@ -507,19 +430,19 @@ function threadFromShell(shell) {
507
430
  threadId: shell.threadId,
508
431
  projectId: shell.projectId,
509
432
  title: shell.title,
433
+ createdAt: shell.createdAt,
510
434
  updatedAt: shell.updatedAt,
511
435
  archived: shell.archivedAt !== null,
512
436
  running: runningOf(shell.sessionStatus),
437
+ status: statusOfShell(shell.status),
513
438
  sessionStatus: shell.sessionStatus,
514
- forkedFrom: forkPointOf(shell.forkedFrom),
515
- runtimeMode: shell.runtimeMode,
516
- interactionMode: shell.interactionMode,
517
- modelSelection: shell.modelSelection
439
+ forkedFrom: forkPointOf(shell.forkedFrom)
518
440
  },
519
441
  messages: [],
520
442
  parentMessages: null,
521
443
  tools: [],
522
- textLimit: MAX_PREVIEW_TEXT
444
+ textLimit: MAX_PREVIEW_TEXT,
445
+ loaded: false
523
446
  });
524
447
  }
525
448
  function projectThread(input) {
@@ -532,13 +455,22 @@ function projectThread(input) {
532
455
  threadId: input.execution.threadId,
533
456
  projectId: input.execution.projectId,
534
457
  title: input.execution.title,
458
+ createdAt: input.execution.createdAt,
535
459
  updatedAt: input.execution.updatedAt,
536
460
  archived: input.execution.archived,
537
461
  running: input.execution.running,
462
+ status: input.execution.status,
463
+ turnCount: own.filter((message) => message.role === "user").length,
464
+ loaded: input.loaded ?? true,
538
465
  forkedFrom: input.execution.forkedFrom,
539
466
  inheritedMessageCount: inherited,
540
467
  cutWalkedBack: false,
541
- turns: buildTurns(own, input.tools, input.execution.running, input.textLimit)
468
+ turns: buildTurns(own, input.tools, input.execution.running, input.textLimit),
469
+ turnOutline: buildTurnOutline(
470
+ own,
471
+ input.execution.failedMessageIds ?? [],
472
+ input.execution.status === "waiting" ? input.execution.waitingMessageId ?? own.findLast((message) => message.role === "user")?.messageId ?? null : null
473
+ )
542
474
  };
543
475
  }
544
476
  function boardSnapshot(input) {
@@ -548,6 +480,14 @@ function boardSnapshot(input) {
548
480
  const thread = byId.get(threadId);
549
481
  return thread === void 0 ? [] : [{ threadId, title: thread.title }];
550
482
  });
483
+ const sources = /* @__PURE__ */ new Map();
484
+ for (const thread of input.threads) {
485
+ const fork = thread.forkedFrom;
486
+ if (fork === null) continue;
487
+ const ids = sources.get(fork.sourceThreadId) ?? /* @__PURE__ */ new Set();
488
+ ids.add(fork.sourceMessageId);
489
+ sources.set(fork.sourceThreadId, ids);
490
+ }
551
491
  const visible = input.threads.filter((thread) => {
552
492
  if (hidden.has(thread.threadId)) return false;
553
493
  if (!input.canvas.showArchived && thread.archived) return false;
@@ -556,20 +496,25 @@ function boardSnapshot(input) {
556
496
  const ranked = [...visible].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
557
497
  return {
558
498
  projects: input.projects,
559
- threads: ranked.map(previewThread),
499
+ threads: ranked.map(
500
+ (thread) => previewThread({
501
+ ...thread,
502
+ ...thread.turnOutline === void 0 ? {} : {
503
+ turnOutline: thread.turnOutline.map(
504
+ (turn) => sources.get(thread.threadId)?.has(turn.messageId) ? { ...turn, forkSource: true } : turn
505
+ )
506
+ }
507
+ })
508
+ ),
560
509
  hiddenThreadIds: input.canvas.hiddenThreadIds,
561
510
  hiddenThreads,
562
- anchors: input.canvas.anchors,
563
- workspaceTitle: input.canvas.workspaceTitle,
564
- autoProjection: input.canvas.autoProjection,
565
- showArchived: input.canvas.showArchived,
566
- positions: input.canvas.positions
511
+ showArchived: input.canvas.showArchived
567
512
  };
568
513
  }
569
514
  function previewThread(thread) {
570
515
  return {
571
516
  ...thread,
572
- turns: thread.turns.map((turn) => {
517
+ turns: thread.turns.slice(-3).map((turn) => {
573
518
  const question = clipText(turn.question, MAX_PREVIEW_TEXT);
574
519
  const answer = clipText(turn.answer, MAX_PREVIEW_TEXT);
575
520
  return {
@@ -582,6 +527,34 @@ function previewThread(thread) {
582
527
  })
583
528
  };
584
529
  }
530
+ function statusOfShell(status) {
531
+ switch (status) {
532
+ case "needs-approval":
533
+ case "needs-input":
534
+ case "plan-ready":
535
+ return "waiting";
536
+ case "running":
537
+ case "background":
538
+ return "running";
539
+ case "failed":
540
+ return "failed";
541
+ case "stopped":
542
+ case "idle":
543
+ return "done";
544
+ default: {
545
+ const unreachable = status;
546
+ return unreachable;
547
+ }
548
+ }
549
+ }
550
+ function statusOfExecution(execution, running) {
551
+ if (execution.hasPendingApprovals || execution.hasPendingUserInput) return "waiting";
552
+ if (running) return "running";
553
+ if (execution.latestTurn?.state === "error" || execution.session?.status === "error") {
554
+ return "failed";
555
+ }
556
+ return "done";
557
+ }
585
558
  function runningOf(status) {
586
559
  return status === "starting" || status === "running";
587
560
  }
@@ -591,14 +564,22 @@ function executionRow(execution) {
591
564
  threadId: execution.threadId,
592
565
  projectId: execution.projectId,
593
566
  title: execution.title,
567
+ createdAt: execution.createdAt,
594
568
  updatedAt: execution.updatedAt,
595
569
  archived: execution.archivedAt !== null,
596
570
  running,
571
+ status: statusOfExecution(execution, running),
597
572
  sessionStatus: execution.session?.status ?? null,
598
573
  forkedFrom: forkPointOf(execution.forkedFrom),
599
- runtimeMode: execution.runtimeMode,
600
- interactionMode: execution.interactionMode,
601
- modelSelection: execution.modelSelection
574
+ failedMessageIds: [
575
+ ...(execution.turnIntents ?? []).flatMap(
576
+ (intent) => intent.turn?.state === "error" ? [intent.turn.userMessageId] : []
577
+ ),
578
+ ...execution.latestTurn?.state === "error" && execution.latestTurn.userMessageId !== void 0 ? [execution.latestTurn.userMessageId] : []
579
+ ],
580
+ waitingMessageId: (execution.turnIntents ?? []).find(
581
+ (intent) => intent.turn?.turnId != null && intent.turn.turnId === execution.session?.activeTurnId
582
+ )?.turn?.userMessageId ?? execution.latestTurn?.userMessageId ?? null
602
583
  };
603
584
  }
604
585
  async function executionOf(ctx, threadId) {
@@ -607,11 +588,109 @@ async function executionOf(ctx, threadId) {
607
588
  return executionRow(execution);
608
589
  }
609
590
 
591
+ // src/sessionMap/mapDirectory.ts
592
+ var MapDirectory = class {
593
+ rows = /* @__PURE__ */ new Map();
594
+ projects = /* @__PURE__ */ new Map();
595
+ sources = /* @__PURE__ */ new Map();
596
+ ready = /* @__PURE__ */ new Set();
597
+ flights = /* @__PURE__ */ new Map();
598
+ ctx;
599
+ constructor(ctx) {
600
+ this.ctx = ctx;
601
+ }
602
+ isReady(projectId) {
603
+ return this.ready.has(null) || this.ready.has(projectId);
604
+ }
605
+ ensure(projectId) {
606
+ if (this.isReady(projectId)) return Promise.resolve();
607
+ const pending = this.flights.get(projectId);
608
+ if (pending !== void 0) return pending;
609
+ const flight = listSessionShells(this.ctx, projectId ?? void 0).then((shells) => {
610
+ for (const shell of shells) this.set(threadFromShell(shell));
611
+ this.ready.add(projectId);
612
+ }).finally(() => this.flights.delete(projectId));
613
+ this.flights.set(projectId, flight);
614
+ return flight;
615
+ }
616
+ set(thread) {
617
+ const previous = this.rows.get(thread.threadId);
618
+ if (previous?.forkedFrom != null) this.removeSource(previous);
619
+ if (previous !== void 0 && previous.projectId !== thread.projectId)
620
+ this.projects.get(previous.projectId)?.delete(thread.threadId);
621
+ this.rows.set(thread.threadId, thread);
622
+ const ids = this.projects.get(thread.projectId) ?? /* @__PURE__ */ new Set();
623
+ ids.add(thread.threadId);
624
+ this.projects.set(thread.projectId, ids);
625
+ if (thread.forkedFrom !== null) {
626
+ const fork = thread.forkedFrom;
627
+ const messages = this.sources.get(fork.sourceThreadId) ?? /* @__PURE__ */ new Map();
628
+ const children = messages.get(fork.sourceMessageId) ?? /* @__PURE__ */ new Set();
629
+ children.add(thread.threadId);
630
+ messages.set(fork.sourceMessageId, children);
631
+ this.sources.set(fork.sourceThreadId, messages);
632
+ }
633
+ }
634
+ decorate(thread) {
635
+ const sources = this.sources.get(thread.threadId);
636
+ if (sources === void 0 || thread.turnOutline === void 0) return thread;
637
+ return {
638
+ ...thread,
639
+ turnOutline: thread.turnOutline.map(
640
+ (turn) => sources.has(turn.messageId) ? { ...turn, forkSource: true } : turn
641
+ )
642
+ };
643
+ }
644
+ get(threadId) {
645
+ return this.rows.get(threadId);
646
+ }
647
+ list(projectId) {
648
+ if (projectId === null) return [...this.rows.values()];
649
+ return [...this.projects.get(projectId) ?? []].flatMap((id) => {
650
+ const row = this.rows.get(id);
651
+ return row === void 0 ? [] : [row];
652
+ });
653
+ }
654
+ delete(threadId) {
655
+ const row = this.rows.get(threadId);
656
+ if (row === void 0) return null;
657
+ this.removeSource(row);
658
+ this.rows.delete(threadId);
659
+ this.projects.get(row.projectId)?.delete(threadId);
660
+ return row.projectId;
661
+ }
662
+ removeSource(thread) {
663
+ const fork = thread.forkedFrom;
664
+ if (fork === null) return;
665
+ const messages = this.sources.get(fork.sourceThreadId);
666
+ const children = messages?.get(fork.sourceMessageId);
667
+ children?.delete(thread.threadId);
668
+ if (children?.size === 0) messages?.delete(fork.sourceMessageId);
669
+ if (messages?.size === 0) this.sources.delete(fork.sourceThreadId);
670
+ }
671
+ };
672
+
673
+ // src/sessionMap/turnCards.ts
674
+ function turnCards(messages, ids, tools, running) {
675
+ const wanted = new Set(ids.slice(0, 60));
676
+ const starts = messages.flatMap((message, index) => message.role === "user" ? [index] : []);
677
+ return starts.flatMap((start, index) => {
678
+ const message = messages[start];
679
+ if (message === void 0 || !wanted.has(message.messageId)) return [];
680
+ const latest = index === starts.length - 1;
681
+ return buildTurns(
682
+ messages.slice(start, starts[index + 1]),
683
+ latest ? tools : [],
684
+ latest && running,
685
+ MAX_PREVIEW_TEXT
686
+ );
687
+ });
688
+ }
689
+
610
690
  // src/sessionMap/threadReads.ts
611
691
  import {
612
692
  isThreadNotFound,
613
693
  threadMessagesNewestFirst,
614
- threadOrNull,
615
694
  THREAD_ACTIVITIES_LIST_LIMIT,
616
695
  TOOL_LIFECYCLE_ACTIVITY_KINDS
617
696
  } from "@ycodium-ai/plugin-api";
@@ -697,37 +776,16 @@ async function readToolTitles(threads, threadId) {
697
776
  }
698
777
  return titles;
699
778
  }
700
- async function readDetail(threads, threadId) {
701
- const thread = await threadOrNull(threads, threadId);
702
- if (thread === null) return { found: false, messages: [] };
703
- let latest;
704
- try {
705
- latest = await threads.messages.list({
706
- threadId,
707
- limit: MAX_DETAIL_MESSAGES,
708
- includeTotal: true
709
- });
710
- } catch (error) {
711
- if (isThreadNotFound(error)) return { found: false, messages: [] };
712
- throw error;
713
- }
714
- return {
715
- found: true,
716
- title: thread.title,
717
- omitted: Math.max(0, (latest.total ?? 0) - latest.items.length),
718
- messages: latest.items.toReversed().map((message) => {
719
- const clipped = clipText(message.text, MAX_DETAIL_TEXT);
720
- return {
721
- messageId: message.id,
722
- role: message.role,
723
- text: clipped.text,
724
- truncated: clipped.truncated
725
- };
726
- })
727
- };
728
- }
729
779
 
730
780
  // src/sessionMap/mapLive.ts
781
+ var WAITING_ACTIVITY_KINDS = /* @__PURE__ */ new Set([
782
+ "approval.requested",
783
+ "approval.resolved",
784
+ "provider.approval.respond.failed",
785
+ "user-input.requested",
786
+ "user-input.resolved",
787
+ "provider.user-input.respond.failed"
788
+ ]);
731
789
  function createSessionMapHub(ctx, canvas, options = {}) {
732
790
  return new SessionMapHub(ctx, canvas, options.timers !== false);
733
791
  }
@@ -738,14 +796,15 @@ var SessionMapHub = class {
738
796
  tools = /* @__PURE__ */ new Map();
739
797
  toolTitles = /* @__PURE__ */ new Map();
740
798
  executions = /* @__PURE__ */ new Map();
799
+ executionGenerations = /* @__PURE__ */ new Map();
741
800
  live = /* @__PURE__ */ new Map();
742
801
  pendingMessage = /* @__PURE__ */ new Map();
743
802
  queued = /* @__PURE__ */ new Set();
744
- removed = /* @__PURE__ */ new Set();
745
- emitters = /* @__PURE__ */ new Set();
746
- shells = null;
803
+ removed = /* @__PURE__ */ new Map();
804
+ emitters = /* @__PURE__ */ new Map();
805
+ pendingFull = /* @__PURE__ */ new Set();
806
+ directory;
747
807
  projects = null;
748
- directoryFlight = null;
749
808
  tail = Promise.resolve();
750
809
  wantFull = false;
751
810
  dueTimer = null;
@@ -756,13 +815,16 @@ var SessionMapHub = class {
756
815
  this.ctx = ctx;
757
816
  this.canvas = canvas;
758
817
  this.timers = timers;
818
+ this.directory = new MapDirectory(ctx);
759
819
  }
760
- subscribe(emit) {
761
- this.emitters.add(emit);
820
+ subscribe(emit, projectId = null) {
821
+ this.emitters.set(emit, projectId);
822
+ this.pendingFull.add(emit);
762
823
  const deferred = this.gate.takeDeferred();
763
- this.enqueue(deferred, true);
824
+ this.enqueue(deferred, false);
764
825
  return () => {
765
826
  this.emitters.delete(emit);
827
+ this.pendingFull.delete(emit);
766
828
  };
767
829
  }
768
830
  onMessage(threadId, event, now = Date.now()) {
@@ -780,8 +842,12 @@ var SessionMapHub = class {
780
842
  const threadId = event.threadId;
781
843
  if (progress === null || threadId === null || threadId.length === 0) return;
782
844
  const cached = this.executions.get(threadId);
783
- if (cached !== void 0 && cached.running !== (progress.phase !== "ended")) {
784
- this.executions.set(threadId, { ...cached, running: progress.phase !== "ended" });
845
+ const running = progress.phase !== "ended";
846
+ if (!running || cached === void 0) this.invalidateExecution(threadId);
847
+ else if (cached !== void 0 && !cached.running) {
848
+ this.invalidateExecution(threadId);
849
+ const status = cached.status === "waiting" ? "waiting" : "running";
850
+ this.executions.set(threadId, { ...cached, running, status });
785
851
  }
786
852
  const known = this.toolTitles.get(threadId);
787
853
  this.tools.set(
@@ -801,28 +867,58 @@ var SessionMapHub = class {
801
867
  }
802
868
  onDirectory(type, threadId) {
803
869
  if (type === "thread.deleted") {
870
+ const projectId = this.directory.delete(threadId);
804
871
  this.drop(threadId);
805
- this.removed.add(threadId);
806
- if (this.shells !== null) {
807
- this.shells = this.shells.filter((shell) => shell.threadId !== threadId);
808
- }
872
+ if (projectId !== null) this.removed.set(threadId, projectId);
809
873
  if (this.emitters.size > 0) this.enqueue([], false);
810
874
  return;
811
875
  }
812
- this.shells = null;
813
- this.projects = null;
814
876
  if (threadId.length > 0) {
815
877
  this.transcripts.invalidate(threadId);
816
- this.executions.delete(threadId);
878
+ this.invalidateExecution(threadId);
817
879
  }
818
880
  if (threadId.length > 0) this.note(threadId, Date.now(), false);
819
881
  else if (this.emitters.size > 0) this.enqueue([], true);
820
882
  }
883
+ /** 只有审批与提问的发出、答复会改「等你」;别的活动(工具进展等)不重读执行。 */
884
+ onActivity(threadId, payload, now = Date.now()) {
885
+ if (!isRecord(payload) || !isRecord(payload.activity)) return;
886
+ if (WAITING_ACTIVITY_KINDS.has(readString(payload.activity.kind))) this.onStatus(threadId, now);
887
+ }
888
+ /** 审批、提问或会话状态变了:卡片状态要跟着变,执行行重读。 */
889
+ onStatus(threadId, now = Date.now()) {
890
+ if (threadId.length === 0) return;
891
+ this.invalidateExecution(threadId);
892
+ this.note(threadId, now, false);
893
+ }
894
+ /** 屏幕上的卡片只有目录那几样:读它们的正文。读过的不再读。 */
895
+ load(threadIds, now = Date.now()) {
896
+ for (const threadId of threadIds.slice(0, MAX_LOAD_PER_CALL)) {
897
+ if (threadId.length === 0 || this.cards.has(threadId)) continue;
898
+ this.note(threadId, now, true);
899
+ }
900
+ }
821
901
  /** 用户命令:立刻读这一条,不管节流窗口。 */
822
902
  touch(threadId, now = Date.now()) {
823
- this.executions.delete(threadId);
903
+ this.invalidateExecution(threadId);
824
904
  this.note(threadId, now, true);
825
905
  }
906
+ async readTurns(threadId, messageIds) {
907
+ const execution = await this.executionFor(threadId);
908
+ if (execution === null) return { threadId, turns: [] };
909
+ const messages = await this.loadMessages(threadId);
910
+ const parent = execution.forkedFrom === null ? null : await this.loadParent(execution.forkedFrom.sourceThreadId);
911
+ const inherited = inheritedMessageCount(parent, execution.forkedFrom?.sourceMessageId ?? null);
912
+ return {
913
+ threadId,
914
+ turns: turnCards(
915
+ messages.slice(inherited),
916
+ messageIds,
917
+ this.tools.get(threadId) ?? [],
918
+ execution.running
919
+ )
920
+ };
921
+ }
826
922
  canvasChanged() {
827
923
  this.enqueue([], true);
828
924
  }
@@ -876,16 +972,49 @@ var SessionMapHub = class {
876
972
  const full = this.wantFull;
877
973
  this.queued.clear();
878
974
  this.wantFull = false;
975
+ for (const scope of new Set(this.emitters.values())) await this.directory.ensure(scope);
879
976
  for (const id of ids) await this.projectStored(id);
880
977
  if (this.emitters.size === 0) return;
881
- if (full || this.shells === null) await this.emitFull();
882
- else if (ids.length > 0 || this.removed.size > 0) this.emitPatch(ids);
978
+ const fresh = /* @__PURE__ */ new Set();
979
+ for (const [emit, scope] of this.emitters) {
980
+ if (!this.directory.isReady(scope)) continue;
981
+ if (full || this.pendingFull.has(emit)) fresh.add(emit);
982
+ this.pendingFull.delete(emit);
983
+ }
984
+ if (fresh.size > 0) await this.emitFull(fresh);
985
+ if (ids.length > 0 || this.removed.size > 0)
986
+ this.emitPatch(ids, /* @__PURE__ */ new Set([...fresh, ...this.pendingFull]));
883
987
  }
884
988
  async projectStored(threadId) {
989
+ const generation = this.executionGenerations.get(threadId) ?? 0;
990
+ const execution = await this.executionFor(threadId);
991
+ if (generation !== (this.executionGenerations.get(threadId) ?? 0))
992
+ return this.projectStored(threadId);
993
+ if (execution !== null) {
994
+ this.directory.set(
995
+ projectThread({
996
+ execution,
997
+ messages: [],
998
+ parentMessages: null,
999
+ tools: [],
1000
+ textLimit: MAX_CARD_TEXT,
1001
+ loaded: false
1002
+ })
1003
+ );
1004
+ if (![...this.emitters.values()].some(
1005
+ (scope) => scope === null || scope === execution.projectId
1006
+ )) {
1007
+ this.cards.delete(threadId);
1008
+ return;
1009
+ }
1010
+ }
885
1011
  const projected = await this.projectOne(threadId);
1012
+ if (generation !== (this.executionGenerations.get(threadId) ?? 0))
1013
+ return this.projectStored(threadId);
886
1014
  if (projected === null) {
1015
+ const projectId = this.directory.delete(threadId);
887
1016
  this.drop(threadId);
888
- this.removed.add(threadId);
1017
+ if (projectId !== null) this.removed.set(threadId, projectId);
889
1018
  return;
890
1019
  }
891
1020
  this.cards.set(threadId, projected);
@@ -924,11 +1053,19 @@ var SessionMapHub = class {
924
1053
  return applyLiveAnswer(projected, this.transcripts.get(threadId) ?? messages, live, answer);
925
1054
  }
926
1055
  async executionFor(threadId) {
927
- const cached = this.executions.get(threadId);
928
- if (cached !== void 0) return cached;
929
- const row = await executionOf(this.ctx, threadId);
930
- if (row !== null) this.executions.set(threadId, row);
931
- return row;
1056
+ for (; ; ) {
1057
+ const cached = this.executions.get(threadId);
1058
+ if (cached !== void 0) return cached;
1059
+ const generation = this.executionGenerations.get(threadId) ?? 0;
1060
+ const row = await executionOf(this.ctx, threadId);
1061
+ if (generation !== (this.executionGenerations.get(threadId) ?? 0)) continue;
1062
+ if (row !== null) this.executions.set(threadId, row);
1063
+ return row;
1064
+ }
1065
+ }
1066
+ invalidateExecution(threadId) {
1067
+ this.executions.delete(threadId);
1068
+ this.executionGenerations.set(threadId, (this.executionGenerations.get(threadId) ?? 0) + 1);
932
1069
  }
933
1070
  async seedTranscript(threadId) {
934
1071
  if (this.transcripts.has(threadId)) return;
@@ -965,62 +1102,55 @@ var SessionMapHub = class {
965
1102
  }
966
1103
  return this.transcripts.get(threadId) ?? [];
967
1104
  }
968
- async emitFull() {
969
- await this.ensureDirectory();
970
- const shells = this.shells ?? [];
971
- const ids = shells.map((shell) => shell.threadId);
972
- const current = await this.canvas.read();
973
- const state = admitThreads(current, ids) === current ? current : await this.canvas.update((latest) => admitThreads(latest, ids));
974
- const known = new Set(ids);
975
- for (const threadId of this.cards.keys()) {
976
- if (!known.has(threadId)) this.drop(threadId);
1105
+ async emitFull(targets) {
1106
+ const state = await this.canvas.read();
1107
+ if ([...targets].some((emit) => this.emitters.get(emit) === null) && this.projects === null) {
1108
+ this.projects = (await this.ctx.projects.list({ repositoryIdentity: "skip" })).map(
1109
+ (project) => ({
1110
+ projectId: project.id,
1111
+ title: project.title,
1112
+ workspaceRoot: project.workspaceRoot,
1113
+ archived: project.archivedAt != null
1114
+ })
1115
+ );
1116
+ }
1117
+ for (const emit of targets) {
1118
+ const scope = this.emitters.get(emit);
1119
+ if (scope === void 0) continue;
1120
+ const threads = this.directory.list(scope).map((row) => this.cards.get(row.threadId) ?? row);
1121
+ const ids = new Set(threads.map((thread) => thread.threadId));
1122
+ emit(
1123
+ boardSnapshot({
1124
+ projects: scope === null ? this.projects ?? [] : [],
1125
+ threads,
1126
+ canvas: scope === null ? state : { ...state, hiddenThreadIds: state.hiddenThreadIds.filter((id) => ids.has(id)) }
1127
+ })
1128
+ );
977
1129
  }
978
- const threads = shells.map((shell) => this.cards.get(shell.threadId) ?? threadFromShell(shell));
979
- const frame = boardSnapshot({
980
- projects: this.projects ?? [],
981
- threads,
982
- canvas: state
983
- });
984
- this.removed.clear();
985
- for (const emit of this.emitters) emit(frame);
986
1130
  }
987
- emitPatch(ids) {
988
- const removedThreadIds = [...this.removed];
1131
+ emitPatch(ids, skip) {
1132
+ const removed = new Map(this.removed);
989
1133
  this.removed.clear();
990
1134
  const threads = ids.flatMap((id) => {
991
1135
  const card = this.cards.get(id);
992
- return card === void 0 ? [] : [card];
1136
+ return card === void 0 ? [] : [previewThread(this.directory.decorate(card))];
993
1137
  });
994
- const frame = { patch: true, threads, removedThreadIds };
995
- for (const emit of this.emitters) emit(frame);
996
- }
997
- ensureDirectory() {
998
- if (this.shells !== null && this.projects !== null) return Promise.resolve();
999
- if (this.directoryFlight !== null) return this.directoryFlight;
1000
- this.directoryFlight = this.loadDirectory().finally(() => {
1001
- this.directoryFlight = null;
1002
- });
1003
- return this.directoryFlight;
1004
- }
1005
- async loadDirectory() {
1006
- const shells = await listSessionShells(this.ctx);
1007
- const projects = (await this.ctx.projects.list({ repositoryIdentity: "skip" })).map(
1008
- (project) => ({
1009
- projectId: project.id,
1010
- title: project.title,
1011
- workspaceRoot: project.workspaceRoot,
1012
- archived: project.archivedAt != null
1013
- })
1014
- );
1015
- this.shells = shells;
1016
- this.projects = projects;
1138
+ for (const [emit, scope] of Array.from(this.emitters)) {
1139
+ if (skip.has(emit) || !this.emitters.has(emit)) continue;
1140
+ const scopedThreads = scope === null ? threads : threads.filter((thread) => thread.projectId === scope);
1141
+ const removedThreadIds = [...removed].flatMap(
1142
+ ([id, projectId]) => scope === null || projectId === scope ? [id] : []
1143
+ );
1144
+ if (scopedThreads.length > 0 || removedThreadIds.length > 0)
1145
+ emit({ patch: true, threads: scopedThreads, removedThreadIds });
1146
+ }
1017
1147
  }
1018
1148
  drop(threadId) {
1019
1149
  this.cards.delete(threadId);
1020
1150
  this.transcripts.invalidate(threadId);
1021
1151
  this.tools.delete(threadId);
1022
1152
  this.toolTitles.delete(threadId);
1023
- this.executions.delete(threadId);
1153
+ this.invalidateExecution(threadId);
1024
1154
  this.live.delete(threadId);
1025
1155
  this.pendingMessage.delete(threadId);
1026
1156
  }
@@ -1049,7 +1179,9 @@ function activateSessionMap(ctx) {
1049
1179
  "thread.deleted",
1050
1180
  "thread.message-sent",
1051
1181
  "thread.archived",
1052
- "thread.unarchived"
1182
+ "thread.unarchived",
1183
+ "thread.activity-appended",
1184
+ "thread.session-set"
1053
1185
  ],
1054
1186
  handle: (event) => {
1055
1187
  const threadId = event.aggregateKind === "thread" ? event.aggregateId : "";
@@ -1057,6 +1189,14 @@ function activateSessionMap(ctx) {
1057
1189
  hub.onMessage(threadId, event);
1058
1190
  return;
1059
1191
  }
1192
+ if (event.type === "thread.session-set") {
1193
+ hub.onStatus(threadId);
1194
+ return;
1195
+ }
1196
+ if (event.type === "thread.activity-appended") {
1197
+ hub.onActivity(threadId, event.payload);
1198
+ return;
1199
+ }
1060
1200
  hub.onDirectory(event.type, threadId);
1061
1201
  }
1062
1202
  });
@@ -1077,16 +1217,54 @@ function activateSessionMap(ctx) {
1077
1217
  return { ok: true };
1078
1218
  }
1079
1219
  });
1220
+ ctx.command({
1221
+ name: "load",
1222
+ description: "Read the sessions whose cards are on screen.",
1223
+ scope: "read",
1224
+ payloadSchema: {
1225
+ type: "object",
1226
+ properties: { threadIds: { type: "array", items: { type: "string" } } },
1227
+ required: ["threadIds"]
1228
+ },
1229
+ handle: async (payload) => {
1230
+ const ids = payload["threadIds"];
1231
+ hub.load(Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : []);
1232
+ return { ok: true };
1233
+ }
1234
+ });
1235
+ ctx.command({
1236
+ name: "turns",
1237
+ description: "Read short text for the turn cards on screen.",
1238
+ scope: "read",
1239
+ payloadSchema: {
1240
+ type: "object",
1241
+ properties: {
1242
+ threadId: { type: "string" },
1243
+ messageIds: { type: "array", maxItems: 60, items: { type: "string" } }
1244
+ },
1245
+ required: ["threadId", "messageIds"]
1246
+ },
1247
+ handle: async (payload) => {
1248
+ const ids = payload["messageIds"];
1249
+ return hub.readTurns(
1250
+ readString(payload["threadId"]),
1251
+ Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : []
1252
+ );
1253
+ }
1254
+ });
1080
1255
  ctx.command({
1081
1256
  name: "snapshot",
1082
1257
  description: "Read the session map.",
1083
1258
  scope: "read",
1084
- payloadSchema: { type: "object", properties: {} },
1085
- handle: async () => {
1259
+ payloadSchema: { type: "object", properties: { projectId: { type: "string" } } },
1260
+ handle: async (payload) => {
1086
1261
  const frames = [];
1087
- const stop = hub.subscribe((frame) => {
1088
- frames.push(frame);
1089
- });
1262
+ const stop = hub.subscribe(
1263
+ (frame) => {
1264
+ frames.push(frame);
1265
+ },
1266
+ readString(payload["projectId"]) || null
1267
+ );
1090
1268
  await hub.settled();
1091
1269
  stop();
1092
1270
  return frames.at(-1) ?? { projects: [], threads: [] };
@@ -1097,11 +1275,14 @@ function activateSessionMap(ctx) {
1097
1275
  description: "Follow the session map.",
1098
1276
  scope: "read",
1099
1277
  buffer: { latest: 1 },
1100
- payloadSchema: { type: "object", properties: {} },
1101
- handle: async (_payload, emit, signal) => {
1102
- const stop = hub.subscribe((frame) => {
1103
- emit(frame);
1104
- });
1278
+ payloadSchema: { type: "object", properties: { projectId: { type: "string" } } },
1279
+ handle: async (payload, emit, signal) => {
1280
+ const stop = hub.subscribe(
1281
+ (frame) => {
1282
+ emit(frame);
1283
+ },
1284
+ readString(payload["projectId"]) || null
1285
+ );
1105
1286
  try {
1106
1287
  await abort(signal);
1107
1288
  } finally {
@@ -1109,63 +1290,16 @@ function activateSessionMap(ctx) {
1109
1290
  }
1110
1291
  }
1111
1292
  });
1112
- ctx.command({
1113
- name: "detail",
1114
- description: "Read one session for the detail pane.",
1115
- scope: "read",
1116
- payloadSchema: {
1117
- type: "object",
1118
- properties: { threadId: { type: "string" } },
1119
- required: ["threadId"]
1120
- },
1121
- handle: async (payload) => readDetail(ctx.threads, readString(payload["threadId"]))
1122
- });
1123
1293
  ctx.command({
1124
1294
  name: "preferences",
1125
1295
  description: "Read or update canvas preferences.",
1126
1296
  scope: "operate",
1127
1297
  payloadSchema: { type: "object", properties: { patch: {} } },
1128
1298
  handle: async (payload) => {
1129
- const patch = payload["patch"];
1130
- const next = patch === void 0 ? await canvas.read() : await canvas.update((state) => patchCanvas(state, preferencePatch(patch)));
1299
+ const show = preferencePatch(payload["patch"]);
1300
+ const next = show === null ? await canvas.read() : await canvas.update((state) => showArchived(state, show));
1131
1301
  hub.canvasChanged();
1132
- return next;
1133
- }
1134
- });
1135
- ctx.command({
1136
- name: "move",
1137
- description: "Save a card position.",
1138
- scope: "operate",
1139
- payloadSchema: {
1140
- type: "object",
1141
- properties: {
1142
- cardId: { type: "string" },
1143
- x: { type: "number" },
1144
- y: { type: "number" }
1145
- },
1146
- required: ["cardId", "x", "y"]
1147
- },
1148
- handle: async (payload) => {
1149
- const next = await canvas.update(
1150
- (state) => rememberPosition(state, {
1151
- cardId: readString(payload["cardId"]),
1152
- x: typeof payload["x"] === "number" ? payload["x"] : 0,
1153
- y: typeof payload["y"] === "number" ? payload["y"] : 0
1154
- })
1155
- );
1156
- hub.canvasChanged();
1157
- return { positions: next.positions };
1158
- }
1159
- });
1160
- ctx.command({
1161
- name: "tidy",
1162
- description: "Drop saved card positions so the map uses the fork layout.",
1163
- scope: "operate",
1164
- payloadSchema: { type: "object", properties: {} },
1165
- handle: async () => {
1166
- await canvas.update(clearPositions);
1167
- hub.canvasChanged();
1168
- return { ok: true };
1302
+ return { showArchived: next.showArchived };
1169
1303
  }
1170
1304
  });
1171
1305
  ctx.command({
@@ -1199,117 +1333,31 @@ function activateSessionMap(ctx) {
1199
1333
  return { ok: true };
1200
1334
  }
1201
1335
  });
1202
- ctx.command({
1203
- name: "create",
1204
- description: "Open a session in a project and send the first message.",
1205
- scope: "operate",
1206
- payloadSchema: {
1207
- type: "object",
1208
- properties: {
1209
- projectId: { type: "string" },
1210
- text: { type: "string" }
1211
- },
1212
- required: ["projectId", "text"]
1213
- },
1214
- handle: async (payload) => createSession(
1215
- ctx,
1216
- readString(payload["projectId"]),
1217
- readString(payload["text"]),
1218
- (id) => hub.touch(id)
1219
- )
1220
- });
1221
- ctx.command({
1222
- name: "send",
1223
- description: "Send a follow-up into an existing session.",
1224
- scope: "operate",
1225
- payloadSchema: {
1226
- type: "object",
1227
- properties: {
1228
- threadId: { type: "string" },
1229
- text: { type: "string" }
1230
- },
1231
- required: ["threadId", "text"]
1232
- },
1233
- handle: async (payload) => sendFollowUp(
1234
- ctx,
1235
- readString(payload["threadId"]),
1236
- readString(payload["text"]),
1237
- (id) => hub.touch(id)
1238
- )
1239
- });
1240
1336
  ctx.command({
1241
1337
  name: "fork",
1242
- description: "Fork at a user message, then send the draft.",
1338
+ description: "Fork at a user message. The first message is written in the new session.",
1243
1339
  scope: "operate",
1244
1340
  payloadSchema: {
1245
1341
  type: "object",
1246
1342
  properties: {
1247
1343
  threadId: { type: "string" },
1248
- messageId: { type: "string" },
1249
- text: { type: "string" }
1344
+ messageId: { type: "string" }
1250
1345
  },
1251
- required: ["threadId", "messageId", "text"]
1346
+ required: ["threadId", "messageId"]
1252
1347
  },
1253
1348
  handle: async (payload) => forkSession(
1254
1349
  ctx,
1255
1350
  readString(payload["threadId"]),
1256
1351
  readString(payload["messageId"]),
1257
- readString(payload["text"]),
1258
1352
  (id) => hub.touch(id)
1259
1353
  )
1260
1354
  });
1261
1355
  }
1262
- async function createSession(ctx, projectId, text2, refresh) {
1263
- const trimmed = text2.trim();
1264
- if (trimmed.length === 0) return { ok: false, code: "empty" };
1265
- const project = await projectOrNull(ctx.projects, projectId);
1266
- if (project === null) return { ok: false, code: "missing-project" };
1267
- const selection = await ctx.providers.selection.resolve({ base: project.defaultModelSelection });
1268
- const threadId = NodeCrypto.randomUUID();
1269
- const [create, start] = createCommands({
1270
- projectId,
1271
- title: titleFromText(trimmed),
1272
- text: trimmed,
1273
- modelSelection: selection.modelSelection,
1274
- runtimeMode: "full-access",
1275
- interactionMode: "default",
1276
- threadId,
1277
- commandId: NodeCrypto.randomUUID(),
1278
- messageId: NodeCrypto.randomUUID(),
1279
- turnCommandId: NodeCrypto.randomUUID()
1280
- });
1281
- await ctx.threads.create(create);
1282
- await ctx.threads.turns.start(start);
1283
- refresh(threadId);
1284
- return { ok: true, threadId, walkedBack: false };
1285
- }
1286
- async function sendFollowUp(ctx, threadId, text2, refresh) {
1287
- const trimmed = text2.trim();
1288
- if (trimmed.length === 0) return { ok: false, code: "empty" };
1289
- const execution = await executionOf(ctx, threadId);
1290
- if (execution === null) return { ok: false, code: "missing-thread" };
1291
- await ctx.threads.turns.start(
1292
- turnCommand({
1293
- threadId,
1294
- text: trimmed,
1295
- messageId: NodeCrypto.randomUUID(),
1296
- commandId: NodeCrypto.randomUUID(),
1297
- runtimeMode: execution.runtimeMode,
1298
- interactionMode: execution.interactionMode,
1299
- modelSelection: execution.modelSelection
1300
- })
1301
- );
1302
- refresh(threadId);
1303
- return { ok: true, threadId, walkedBack: false };
1304
- }
1305
- async function forkSession(ctx, threadId, messageId, text2, refresh) {
1306
- const trimmed = text2.trim();
1307
- if (trimmed.length === 0) return { ok: false, code: "empty" };
1356
+ async function forkSession(ctx, threadId, messageId, refresh) {
1308
1357
  const transcript = await readTranscript(ctx.threads, threadId);
1309
1358
  if (transcript === null) return { ok: false, code: "missing-thread" };
1310
1359
  const anchor = resolveForkAnchor(transcript, messageId);
1311
1360
  if (anchor === null) return { ok: false, code: "no-user-message" };
1312
- const execution = await executionOf(ctx, threadId);
1313
1361
  const childId = NodeCrypto.randomUUID();
1314
1362
  await ctx.threads.fork(
1315
1363
  forkCommand({
@@ -1319,33 +1367,13 @@ async function forkSession(ctx, threadId, messageId, text2, refresh) {
1319
1367
  commandId: NodeCrypto.randomUUID()
1320
1368
  })
1321
1369
  );
1322
- await ctx.threads.turns.start(
1323
- turnCommand({
1324
- threadId: childId,
1325
- text: trimmed,
1326
- messageId: NodeCrypto.randomUUID(),
1327
- commandId: NodeCrypto.randomUUID(),
1328
- runtimeMode: execution?.runtimeMode ?? "full-access",
1329
- interactionMode: execution?.interactionMode ?? "default",
1330
- modelSelection: execution === null ? null : execution.modelSelection
1331
- })
1332
- );
1333
1370
  refresh(threadId);
1334
1371
  refresh(childId);
1335
1372
  return { ok: true, threadId: childId, walkedBack: anchor.walkedBack };
1336
1373
  }
1337
1374
  function preferencePatch(value) {
1338
- if (!isRecord(value)) return {};
1339
- const anchor = isRecord(value.anchor) ? {
1340
- threadId: readString(value.anchor.threadId),
1341
- cardId: readString(value.anchor.cardId)
1342
- } : void 0;
1343
- return {
1344
- ...typeof value.autoProjection === "boolean" ? { autoProjection: value.autoProjection } : {},
1345
- ...typeof value.workspaceTitle === "string" ? { workspaceTitle: value.workspaceTitle } : {},
1346
- ...typeof value.showArchived === "boolean" ? { showArchived: value.showArchived } : {},
1347
- ...anchor === void 0 ? {} : { anchor }
1348
- };
1375
+ if (!isRecord(value) || typeof value.showArchived !== "boolean") return null;
1376
+ return value.showArchived;
1349
1377
  }
1350
1378
  function abort(signal) {
1351
1379
  return new Promise((resolve) => {
@@ -1360,7 +1388,7 @@ function abort(signal) {
1360
1388
  // src/index.ts
1361
1389
  var index_default = definePlugin({
1362
1390
  id: "conversation",
1363
- apiVersion: 27,
1391
+ apiVersion: 28,
1364
1392
  activate(ctx) {
1365
1393
  activateSessionMap(ctx);
1366
1394
  }