Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/mobile/src/features/threads/use-composer-command-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
ThreadPullRequestLink,
} from "@t3tools/contracts";
import {
composerThreadQuery,
type ComposerEnvironmentCandidate,
matchComposerEnvironmentItems,
matchComposerThreadItems,
Expand Down Expand Up @@ -346,6 +347,14 @@ export function useComposerCommandMenu({
trigger?.kind === "path" ? trigger.query : "",
{ titlesOnly: true },
);
const threadQuery = trigger?.kind === "path" ? composerThreadQuery(trigger.query) : "";
useThreadSearch(
trigger?.kind === "path" && environmentId && threadQuery !== trigger.query
? [environmentId]
: [],
threadQuery,
{ titlesOnly: true },
);
const pullRequestSearch = useComposerPullRequestSearch({
environmentId,
projectId: pullRequestProjectId,
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/components/chat/useComposerMenuItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
threadPullRequestKeyOf,
} from "@t3tools/client-runtime/thread-pull-requests";
import {
composerThreadQuery,
matchComposerEnvironmentItems,
matchComposerThreadItems,
} from "@t3tools/client-runtime/composerThreadItems";
Expand Down Expand Up @@ -125,6 +126,12 @@ export function useComposerMenuItems({
const environmentThreadShells = useVisibleThreadShells(isPathTrigger);
// Mounts older settled threads matching the mention, which shells may leave out.
useThreadSearch(isPathTrigger ? [environmentId] : [], pathTriggerQuery, { titlesOnly: true });
const threadQuery = composerThreadQuery(pathTriggerQuery);
useThreadSearch(
isPathTrigger && threadQuery !== pathTriggerQuery ? [environmentId] : [],
Comment thread
maria-rcks marked this conversation as resolved.
Resolved
threadQuery,
{ titlesOnly: true },
);
// The user's machines feed `@` machine matches; the catalog summary is cheap and stable.
const environmentIdentities = useEnvironmentIdentities();
const environmentMachines = useEnvironmentMachines();
Expand Down
41 changes: 41 additions & 0 deletions packages/client-runtime/src/composerThreadItems.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,44 @@ describe("matchComposerEnvironmentItems", () => {
).toEqual([]);
});
});

it("reads dashes, underscores, and dots in a mention as spaces", () => {
const match = (query: string) =>
matchComposerThreadItems({
shells: [
shell("follow-up", "Review PR 195 follow-up: tests", {
updatedAt: "2026-10-22T00:00:00.000Z",
}),
shell("exact", "Review PR 195"),
shell("other", "Review PR 194"),
],
environmentId,
linkedEnvironments: new Map(),
excludeThreadId: null,
query,
now: Date.parse("2026-10-23T00:00:00.000Z"),
}).map((item) => item.thread.threadId);
expect(match("review-pr-195")).toEqual(["exact", "follow-up"]);
expect(match("review_pr.195")).toEqual(["exact", "follow-up"]);
});

it("preserves literal thread ids and punctuation in exact titles", () => {
const match = (query: string, shells: ReadonlyArray<ComposerThreadCandidate>) =>
matchComposerThreadItems({
shells,
environmentId,
linkedEnvironments: new Map(),
excludeThreadId: null,
query,
now: Date.parse("2026-10-23T00:00:00.000Z"),
}).map((item) => item.thread.threadId);
expect(match("review-pr-195", [shell("review-pr-195", "Unrelated title")])).toEqual([
"review-pr-195",
]);
expect(
match("node.js", [
shell("prefix", "Node.js guide", { updatedAt: "2026-10-22T00:00:00.000Z" }),
shell("exact", "Node.js"),
]),
).toEqual(["exact", "prefix"]);
});
22 changes: 16 additions & 6 deletions packages/client-runtime/src/composerThreadItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export interface ComposerThreadItem {
readonly description: string;
}

/** A mention cannot hold a space, so dashes, underscores, and dots stand in for one. */
export function composerThreadQuery(query: string): string {
return query.replace(/[-_.]+/g, " ");
}

/**
* Threads to reference from the composer, matched and ranked like the command
* palette, without subagent or side chat threads. The agent reads threads on its own server
Expand All @@ -40,6 +45,7 @@ export function matchComposerThreadItems(input: {
now: number;
}): ComposerThreadItem[] {
const query = foldSearchQuery(input.query);
const normalizedQuery = foldSearchQuery(composerThreadQuery(input.query));
if (query.length === 0) return [];
const candidates = input.shells.filter(
(shell) =>
Expand All @@ -49,12 +55,16 @@ export function matchComposerThreadItems(input: {
shell.archivedAt === null &&
isListedThreadLineage(shell.lineage),
);
return rankSearchItems(candidates, (shell, options) =>
scoreThreadSearch(query, threadSearchSubject(shell, null), input.now, {
...options,
picks: false,
}),
)
return rankSearchItems(candidates, (shell, options) => {
const subject = threadSearchSubject(shell, null);
const searchOptions = { ...options, picks: false };
const literal = scoreThreadSearch(query, subject, input.now, searchOptions);
if (query === normalizedQuery || normalizedQuery.length === 0) return literal;
const normalized = scoreThreadSearch(normalizedQuery, subject, input.now, searchOptions);
return literal !== null && (normalized === null || literal.score >= normalized.score)
? literal
: normalized;
})
.slice(0, COMPOSER_THREAD_RESULT_LIMIT)
.map(({ item: shell }) => {
const machine =
Expand Down
Loading