Skip to content
Closed
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
21 changes: 14 additions & 7 deletions apps/mobile/src/features/pull-requests/usePullRequestThreads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,20 @@ export function usePullRequestThreads(
filePath,
);
const draft = getComposerDraftSnapshot(draftKey);
const attached = draft.context?.records.some(
(record) =>
record.kind === "review-comment" &&
"pullRequest" in record &&
record.pullRequest?.url === pr.url &&
record.filePath === context.filePath,
);
const attached = draft.context?.records.some((record) => {
if (context.kind === "pull-request" && record.kind === "pull-request") {
return "url" in record && record.url === pr.url;
}
if (context.kind === "review-comment" && record.kind === "review-comment") {
return (
"pullRequest" in record &&
"filePath" in record &&
record.pullRequest?.url === pr.url &&
record.filePath === context.filePath
);
}
return false;
});
if (
!attached &&
!insertComposerDraftContext(
Expand Down
45 changes: 45 additions & 0 deletions apps/mobile/src/features/settings/ThreadSettingsSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export function SettingsLabsRouteScreen() {
setting="linkedEnvironmentsEnabled"
description="Let agents on a machine work on your other machines, and theirs on it. Turn it on for each machine. While off, nothing new starts over a link; running tasks finish."
/>
<MentionIndexBetaSection />
<MachineBetaSection
title="Fast worktree checkout"
icon="bolt.circle"
Expand Down Expand Up @@ -299,3 +300,47 @@ function MachineBetaSection({
</View>
);
}

/** Mention index is per connected machine; it does not need a capability gate. */
function MentionIndexBetaSection() {
const { environments } = useEnvironments();
const connected = environments.filter(
(environment) => environment.connection.phase === "connected",
);
const writable = useEnvironmentsWithScope(connected, AuthSettingsWriteScope);
const updateSettings = useAtomCommand(serverEnvironment.updateSettings, {
label: "mentionIndexEnabled beta update",
reportFailure: true,
});
if (connected.length === 0) return null;
return (
<View className="gap-3">
<SettingsSection title="Beta: Mention index">
{connected.map((environment) => {
const policy = environment.serverConfig?.managedPolicy;
const managed = isManagedSettingPath(policy, "mentionIndexEnabled");
return (
<SettingsSwitchRow
key={environment.environmentId}
icon="magnifyingglass"
label={environment.label}
{...(managed ? { subtitle: `Managed by ${policy?.organization}.` } : {})}
value={environment.serverConfig?.settings.mentionIndexEnabled === true}
disabled={managed || !writable.has(environment.environmentId)}
onValueChange={(value) =>
void updateSettings({
environmentId: environment.environmentId,
input: { patch: { mentionIndexEnabled: value } },
})
}
/>
);
})}
</SettingsSection>
<Text className="px-2 text-sm text-foreground-muted">
Index pull requests, issues, threads, Jira, and Figma for composer mentions. While it is
off, @ and # stay classic.
</Text>
</View>
);
}
77 changes: 76 additions & 1 deletion apps/mobile/src/features/threads/ComposerCommandPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,37 @@ export type ComposerCommandItem =
readonly label: string;
readonly description: string;
}
| {
readonly id: string;
readonly type: "jira";
readonly key: string;
readonly url: string;
readonly summary: string;
readonly label: string;
readonly description: string;
}
| {
readonly id: string;
readonly type: "figma";
readonly fileKey: string;
readonly url: string;
readonly name: string;
readonly hasThumbnail: boolean;
readonly label: string;
readonly description: string;
readonly thumbnailUri?: string;
}
| {
readonly id: string;
readonly type: "issue";
readonly number: number;
readonly title: string;
readonly url: string;
readonly repository: string;
readonly state: "open" | "closed";
readonly label: string;
readonly description: string;
}
| {
readonly id: string;
readonly type: "environment";
Expand Down Expand Up @@ -131,6 +162,12 @@ function itemIcon(item: ComposerCommandItem): AppSymbolName | null {
return null;
case "thread":
return "text.bubble";
case "jira":
return "tag";
case "figma":
return "photo";
case "issue":
return "exclamationmark.circle";
case "environment":
return ENVIRONMENT_MACHINE_SYMBOLS[item.machine];
}
Expand All @@ -143,25 +180,63 @@ function groupLabel(
switch (triggerKind) {
case "pull-request":
return "Pull requests";
case "hash":
return "Pull requests and issues";
case "jira":
return "Jira issues";
case "figma":
return "Figma designs";
case "issue":
return "GitHub issues";
case "thread":
return "Threads";
case "slash-command":
return "Commands";
case "skill":
return "Skills";
case "path":
return items.every((item) => item.type === "path") ? "Files" : "Mentions";
case "file":
return "Files";
default:
return null;
}
}

function emptyText(triggerKind: ComposerTriggerKind | null, isLoading: boolean): string {
if (isLoading) {
return triggerKind === "path" ? "Searching files…" : "Loading…";
return triggerKind === "path"
? "Searching files…"
: triggerKind === "hash"
? "Finding pull requests and issues…"
: triggerKind === "jira"
? "Finding Jira issues…"
: triggerKind === "figma"
? "Finding Figma designs…"
: triggerKind === "issue"
? "Finding GitHub issues…"
: triggerKind === "thread"
? "Finding threads…"
: triggerKind === "file"
? "Searching files…"
: "Loading…";
}
switch (triggerKind) {
case "pull-request":
return "No matching pull requests.";
case "hash":
return "No matching pull requests or issues.";
case "jira":
return "No matching Jira issues.";
case "figma":
return "No matching Figma designs.";
case "issue":
return "No matching GitHub issues.";
case "thread":
return "No matching threads.";
case "path":
return "No matching threads, machines, or files.";
case "file":
return "No matching files or folders.";
case "skill":
return "No skills found.";
Expand Down
9 changes: 8 additions & 1 deletion apps/mobile/src/features/threads/ThreadComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps
hasCompactableConversation: props.hasCompactableConversation,
onChangeDraftMessage: props.onChangeDraftMessage,
offersUsageLimits: usageLimitsOffered,
mentionIndexEnabled: props.serverConfig?.settings.mentionIndexEnabled === true,
onUsageLimits:
usageLimitsOffered && draftAttachments.length === 0 ? openUsageLimits : undefined,
modelOptions: mentionModelOptions,
Expand Down Expand Up @@ -839,7 +840,13 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps
{props.draftControls}
{!isSubmissionLocked &&
composerMenu.trigger &&
(composerMenu.items.length > 0 || composerMenu.trigger.kind === "pull-request") ? (
(composerMenu.items.length > 0 ||
composerMenu.trigger.kind === "pull-request" ||
composerMenu.trigger.kind === "hash" ||
composerMenu.trigger.kind === "jira" ||
composerMenu.trigger.kind === "figma" ||
composerMenu.trigger.kind === "issue" ||
composerMenu.trigger.kind === "thread") ? (
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
<ComposerPopoverAnchor>
<ComposerCommandPopover
items={composerMenu.items}
Expand Down
20 changes: 17 additions & 3 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { hostedThreadLink, parseThreadLinkHref } from "@t3tools/shared/threadLin
import { PROMPT_LINK_MAX_PROMPT_LENGTH } from "@t3tools/shared/promptLinks";
import {
parseComposerContextHref,
parseExternalComposerContextHref,
collectComposerContextReferences,
replaceComposerContextReferences,
formatComposerContextReference,
Expand Down Expand Up @@ -2353,10 +2354,22 @@ function UserMessageContent(props: UserMessageContentProps) {
const threadIcons = useThreadChipIcons(props.environmentId, liveText, props.context?.records);
const text = replaceComposerContextReferences(liveText, (ref) => {
const available = props.context?.records.some((record) => record.contextId === ref.contextId);
// Keep plain URL markdown when there is no matching record (not a chip).
if (!available && ref.url !== undefined) return ref.source;
return `[${ref.label}${available ? "" : " (unavailable)"}](t3-context://v1/${ref.kind}/${ref.contextId})`;
});
const parseContextHref = (href: string) => {
const opaque = parseComposerContextHref(href);
if (opaque) return opaque;
const records = props.context?.records;
if (!records || records.length === 0) return null;
const external = parseExternalComposerContextHref(href);
return external && records.some((record) => record.contextId === external.contextId)
? external
: null;
};
const onLinkPress = (href: string) => {
const reference = parseComposerContextHref(href);
const reference = parseContextHref(href);
if (!reference) return props.linkHandlers.onLinkPress?.(href);
const record = props.context?.records.find(
(record) => record.contextId === reference.contextId,
Expand Down Expand Up @@ -2395,7 +2408,7 @@ function UserMessageContent(props: UserMessageContentProps) {
...props.linkHandlers,
onLinkPress,
fileContextMenu: (href) => {
const reference = parseComposerContextHref(href);
const reference = parseContextHref(href);
if (!reference) return props.linkHandlers.fileContextMenu(href);
const record = props.context?.records.find(
(item) => item.contextId === reference.contextId,
Expand All @@ -2409,7 +2422,7 @@ function UserMessageContent(props: UserMessageContentProps) {
};
},
onFileContextMenuAction: (href, actionId) => {
const reference = parseComposerContextHref(href);
const reference = parseContextHref(href);
if (!reference) return props.linkHandlers.onFileContextMenuAction(href, actionId);
if (actionId === "open-context") return onLinkPress(href);
const record = props.context?.records.find(
Expand All @@ -2420,6 +2433,7 @@ function UserMessageContent(props: UserMessageContentProps) {
formatComposerContextReference({
...reference,
label: record.label,
...("url" in record ? { url: record.url } : {}),
}),
{
version: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ vi.mock("react-native", () => ({ Alert: { alert: vi.fn() } }));
vi.mock("../../state/queries", () => ({
useComposerPathSearch: () => ({ entries: [], isPending: false }),
useComposerPullRequestSearch: () => ({ entries: [], isPending: false, error: null }),
useDebouncedValue: <A>(value: A) => value,
}));
vi.mock("../../state/query", () => ({
useEnvironmentQuery: () => ({ data: null, error: null, isPending: false }),
}));
vi.mock("../../state/pull-requests", () => ({
mentionIndexEnvironment: { search: () => null },
}));
vi.mock("../../state/use-composer-drafts", () => ({
getComposerDraftSnapshot: vi.fn(),
Expand Down
Loading
Loading