1import type { EnvironmentId, ScopedThreadRef, ThreadId } from "@t3tools/contracts";
2
3const COMPOSER_THREAD_RESULT_LIMIT = 5;
4
5export interface ComposerThreadCandidate {
6 readonly environmentId: EnvironmentId;
7 readonly id: ThreadId;
8 readonly title: string;
9 readonly updatedAt: string;
10 readonly archivedAt: string | null;
11}
12
13export interface ComposerThreadItem {
14 readonly id: string;
15 readonly type: "thread";
16 readonly thread: ScopedThreadRef;
17 readonly label: string;
18 readonly description: string;
19}
20
21/**
22 * Threads the `@` picker offers next to file paths. The agent can only read threads on its
23 * own server, so candidates stay within the composer's environment. A query is required:
24 * bare `@` stays a file picker.
25 */
26export function matchComposerThreadItems(input: {
27 shells: ReadonlyArray<ComposerThreadCandidate>;
28 environmentId: EnvironmentId;
29 excludeThreadId: ThreadId | null;
30 query: string;
31}): ComposerThreadItem[] {
32 const query = input.query.trim().toLowerCase();
33 if (query.length === 0) return [];
34 return input.shells
35 .filter(
36 (shell) =>
37 shell.environmentId === input.environmentId &&
38 shell.id !== input.excludeThreadId &&
39 shell.archivedAt === null &&
40 shell.title.toLowerCase().includes(query),
41 )
42 .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
43 .slice(0, COMPOSER_THREAD_RESULT_LIMIT)
44 .map((shell) => ({
45 id: `thread:${shell.environmentId}:${shell.id}`,
46 type: "thread",
47 thread: { environmentId: shell.environmentId, threadId: shell.id },
48 label: shell.title,
49 description: "Thread",
50 }));
51}