| 1 | import type { EnvironmentId, ScopedThreadRef, ThreadId } from "@t3tools/contracts"; |
| 2 | |
| 3 | const COMPOSER_THREAD_RESULT_LIMIT = 5; |
| 4 | |
| 5 | export 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 | |
| 13 | export 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 | */ |
| 26 | export 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 | } |