Skip to content
Merged
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
25 changes: 21 additions & 4 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,6 @@ export function HomeScreen(props: HomeScreenProps) {
)
? [props.selectedEnvironmentId]
: [];
const threadSearch = useThreadSearch(searchEnvironmentIds, props.searchQuery);
const threadSearchMatchByKey = useThreadListSearchMatches(props.threads, threadSearch);
const handleSwipeableWillOpen = (methods: SwipeableMethods) => {
if (openSwipeableRef.current !== methods) {
openSwipeableRef.current?.close();
Expand Down Expand Up @@ -380,6 +378,14 @@ export function HomeScreen(props: HomeScreenProps) {

const v2ProjectScopeKey = props.selectedProjectKey;
const v2ScopedProjectGroup = findHomeProjectScope(projectScopes, v2ProjectScopeKey);
const threadSearch = useThreadSearch(
searchEnvironmentIds,
props.searchQuery,
v2ScopedProjectGroup === null
? {}
: { projectIds: v2ScopedProjectGroup.projectRefs.map((ref) => ref.projectId) },
);
const threadSearchMatchByKey = useThreadListSearchMatches(props.threads, threadSearch);
const v2ProjectTitleByProjectKey = new Map(
projectScopes.flatMap((scope) =>
scope.projectRefs.map(
Expand Down Expand Up @@ -447,6 +453,7 @@ export function HomeScreen(props: HomeScreenProps) {
layout: threadListV2Layout,
items: threadListV2Items,
contentOnlyThreadKeys,
olderSettledShelf,
} = useThreadListV2({
threads: props.threads,
pendingTasks: props.pendingTasks,
Expand Down Expand Up @@ -658,7 +665,9 @@ export function HomeScreen(props: HomeScreenProps) {
};

const hasAnyThreads =
props.threads.some((thread) => thread.archivedAt === null) || props.pendingTasks.length > 0;
props.threads.some((thread) => thread.archivedAt === null) ||
props.pendingTasks.length > 0 ||
threadListV2Layout.settledCount > 0;
const selectedEnvironmentLabel =
props.selectedEnvironmentId === null
? null
Expand Down Expand Up @@ -806,7 +815,15 @@ export function HomeScreen(props: HomeScreenProps) {
settledShelfExpanded && threadListV2Layout.hiddenSettledCount > 0 ? (
<ThreadListV2ShowMoreRow
hiddenCount={threadListV2Layout.hiddenSettledCount}
onPress={showMoreSettled}
{...(olderSettledShelf.loading
? { state: "loading" as const }
: olderSettledShelf.failed
? { state: "failed" as const }
: {})}
onPress={() => {
olderSettledShelf.retry();
showMoreSettled();
}}
/>
) : null
}
Expand Down
16 changes: 15 additions & 1 deletion apps/mobile/src/features/keyboard/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,21 @@ export function CommandPalette(props: {
.map((environment) => environment.environmentId),
[environments],
);
const search = useThreadSearch(searchEnvironmentIds, query.startsWith(">") ? "" : query);
// The palette matches environment names when there are several; older rows must match the same.
const environmentLabels =
environments.length > 1
? Object.fromEntries(
searchEnvironmentIds.flatMap((environmentId) => {
const label = savedConnectionsById[environmentId]?.environmentLabel;
return label === undefined ? [] : [[environmentId, label]];
}),
)
: undefined;
const search = useThreadSearch(
searchEnvironmentIds,
query.startsWith(">") ? "" : query,
environmentLabels === undefined ? {} : { environmentLabels },
);
const contentMatchByKey = useMemo(
() =>
new Map(
Expand Down
86 changes: 69 additions & 17 deletions apps/mobile/src/features/prompt-links/usePromptLinkResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as Dialog from "../../components/ui/Dialog";
import { StackActions, useNavigation } from "@react-navigation/native";
import { availableScratchWorkspaceRoot } from "@t3tools/client-runtime/operations/projects";
import {
findPromptLinkProject,
findPromptLinkProjects,
findPromptLinkPullRequestThread,
resolvePromptLinkModelSelection,
} from "@t3tools/client-runtime/prompt-links";
Expand All @@ -11,7 +11,7 @@ import {
squashAtomCommandFailure,
} from "@t3tools/client-runtime/state/runtime";
import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell";
import type { EnvironmentId } from "@t3tools/contracts";
import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
import {
parseNewThreadLink,
type LinkedPrompt,
Expand Down Expand Up @@ -63,6 +63,9 @@ export function usePromptLinkResolver(params: Readonly<Record<string, unknown>>
const loadPullRequest = useAtomQueryRunner(composerPullRequests.detail, {
reportFailure: false,
});
const loadLinkedThreads = useAtomQueryRunner(composerPullRequests.linkedThreadsUncached, {
reportFailure: false,
});
const search = promptLinkSearchFromParams(params);
// Wait for cold-launch shells or existing repositories may be offered for cloning.
const waitingForShells =
Expand All @@ -89,6 +92,61 @@ export function usePromptLinkResolver(params: Readonly<Record<string, unknown>>
return resolved?.exact !== true;
};

const openThread = (
thread: { readonly environmentId: EnvironmentId; readonly id: ThreadId },
prompt: LinkedDraftRequest["prompt"],
) => {
(navigation.getParent() ?? navigation).dispatch(
StackActions.replace("Thread", {
environmentId: String(thread.environmentId),
threadId: String(thread.id),
}),
);
if (prompt) {
void applyLinkedPromptToThreadDraft(
scopedThreadKey(thread.environmentId, thread.id),
prompt,
promptLinkNotice({ hiddenCharacters: prompt.hiddenCharacters }),
);
}
};

// A pull request's thread can be an older settled thread no shell holds, so ask every
// connected environment with the repository. A lookup on another one would wait forever.
const findServerPullRequestThread = async (
projects: ReadonlyArray<EnvironmentProject>,
pullRequest: PromptLinkPullRequest,
) => {
const connected = new Set(
connectedEnvironments
.filter((environment) => environment.connectionState === "connected")
.map((environment) => environment.environmentId),
);
const lookups = projects
.filter((project) => connected.has(project.environmentId))
.map(async (project) => {
const linked = await loadLinkedThreads({
environmentId: project.environmentId,
input: {
projectId: project.id,
repository: promptLinkRepositoryName(pullRequest),
number: pullRequest.number,
},
});
const thread =
linked._tag === "Success"
? linked.value.threads.find((candidate) => candidate.archivedAt === null)
: undefined;
return thread && { environmentId: project.environmentId, id: thread.id };
});
// The most recent project wins without waiting for the lookups after it.
for (const lookup of lookups) {
const thread = await lookup;
if (thread) return thread;
}
return undefined;
};

const openProjectDraft = async (project: EnvironmentProject, request: LinkedDraftRequest) => {
try {
await waitForComposerDraftsLoaded();
Expand Down Expand Up @@ -234,19 +292,7 @@ export function usePromptLinkResolver(params: Readonly<Record<string, unknown>>
if (link.kind === "pull-request") {
const thread = findPromptLinkPullRequestThread(threads, link.pullRequest);
if (thread) {
(navigation.getParent() ?? navigation).dispatch(
StackActions.replace("Thread", {
environmentId: String(thread.environmentId),
threadId: String(thread.id),
}),
);
if (link.prompt) {
void applyLinkedPromptToThreadDraft(
scopedThreadKey(thread.environmentId, thread.id),
link.prompt,
promptLinkNotice({ hiddenCharacters: link.prompt.hiddenCharacters }),
);
}
openThread(thread, link.prompt);
return;
}
}
Expand All @@ -263,14 +309,20 @@ export function usePromptLinkResolver(params: Readonly<Record<string, unknown>>
}
const activityOf = (project: EnvironmentProject) =>
lastActivity.get(`${project.environmentId}:${project.id}`) ?? "";
const project = findPromptLinkProject(
const projects = findPromptLinkProjects(
flow.projectScopes
.flatMap((scope) => scope.projects)
.sort((left, right) => activityOf(right).localeCompare(activityOf(left))),
repository,
);
const project = projects[0];
if (project) {
void openProjectDraft(project, request);
const { pullRequest } = request;
void (async () => {
const thread = pullRequest && (await findServerPullRequestThread(projects, pullRequest));
if (thread) openThread(thread, request.prompt);
else await openProjectDraft(project, request);
})();
return;
}
Dialog.alert(`You don't have ${promptLinkRepositoryName(repository)}`, undefined, [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { useSettingsEnvironmentFilter, type SettingsTarget } from "./settings-en
import { leaveSettingsPage } from "./settings-navigation";
import { workspaceHasColumns } from "../workspace/workspace-selection-store";
import { DEFAULT_SETTINGS_PAGE } from "../workspace/workspace-tab-navigation";
import { readUnloadedThreadCount } from "../../state/olderSettledShelf";

export function SettingsProjectOverviewRouteScreen() {
const insets = useSafeAreaInsets();
Expand Down Expand Up @@ -175,6 +176,7 @@ function ProjectOverviewContent(props: {
hasOtherMembers: props.hasOtherMembers,
displayName,
threads,
unloadedThreadCount: readUnloadedThreadCount(members, threads),
});
Dialog.confirm({
title: plan.targetKind === "checkout" ? "Remove checkout" : "Remove project",
Expand Down
58 changes: 53 additions & 5 deletions apps/mobile/src/features/threads/ThreadArrangementSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { SymbolView } from "../../components/ui/AppSymbol";
import { scopedThreadKey } from "../../lib/scopedEntities";
import { environmentServerConfigsAtom } from "../../state/server";
import { environmentThreadShells } from "../../state/threads";
import { sortSettledThreads } from "@t3tools/client-runtime/state/thread-sort";
import { useOlderSettledShelf, useOlderSettledShelfGrowth } from "../../state/olderSettledShelf";
import {
pendingThreadOrderAtom,
threadDropBusyAtom,
Expand All @@ -28,7 +30,12 @@ import {
threadDragAction,
type ThreadMoveDestination,
} from "./threadOrder";
import { getThreadListV2OrderedSection, threadListInboxReturns } from "./threadListV2";
import {
getThreadListV2OrderedSection,
THREAD_LIST_V2_SETTLED_INITIAL_COUNT,
THREAD_LIST_V2_SETTLED_PAGE_COUNT,
threadListInboxReturns,
} from "./threadListV2";
import { useThreadListV2ShelfPreferences } from "./use-thread-list-v2-shelf-preferences";

const ROW_HEIGHT = 56;
Expand All @@ -40,6 +47,8 @@ type Row = {
key: string;
section: Section;
thread?: EnvironmentThreadShell;
/** The Settled section's "Show more" row. */
more?: true;
offset: number;
height: number;
};
Expand Down Expand Up @@ -158,6 +167,15 @@ export function ThreadArrangementSheet(props: { onClose: () => void }) {
const { workingShelfEnabled } = useThreadListV2ShelfPreferences();
const [now, setNow] = useState(() => new Date().toISOString());
const [expanded, setExpanded] = useState({ snoozed: false, settled: false });
const [settledVisibleCount, setSettledVisibleCount] = useState(
THREAD_LIST_V2_SETTLED_INITIAL_COUNT,
);
// Older settled threads load in pages, as in the thread list.
const olderSettledShelf = useOlderSettledShelf({
scopes: [...configs.keys()].map((environmentId) => ({ environmentId, projectIds: null })),
expanded: expanded.settled,
visibleCount: settledVisibleCount,
});
useEffect(() => {
const wakeAt = Math.min(
...threads.flatMap((thread) => {
Expand Down Expand Up @@ -201,9 +219,13 @@ export function ThreadArrangementSheet(props: { onClose: () => void }) {
? sortInboxThreadsByReturn(active, threadListInboxReturns.returnedAt)
: active,
snoozed: parked.filter((thread) => effectiveSnoozed(thread, { now })),
settled: parked.filter((thread) => !effectiveSnoozed(thread, { now })),
settled: sortSettledThreads(parked.filter((thread) => !effectiveSnoozed(thread, { now }))),
};
}, [threads, configs, now, queuedThreadKeys, pendingOrder, workingShelfEnabled]);
const listedSettled = sections.settled.filter(olderSettledShelf.isListed);
useOlderSettledShelfGrowth(olderSettledShelf, listedSettled);
const visibleSettled = listedSettled.slice(0, settledVisibleCount);
const settledCount = sections.settled.length + olderSettledShelf.unloadedCount(threads);
const planners = useMemo(() => {
const planner = (section: "pinned" | "active") =>
createThreadMovePlanner({
Expand Down Expand Up @@ -232,13 +254,17 @@ export function ThreadArrangementSheet(props: { onClose: () => void }) {
result.push({ key: section, section, offset, height: HEADER_HEIGHT });
offset += HEADER_HEIGHT;
if ((section === "snoozed" || section === "settled") && !expanded[section]) continue;
for (const thread of sections[section]) {
for (const thread of section === "settled" ? visibleSettled : sections[section]) {
result.push({ key: keyOf(thread), section, thread, offset, height: ROW_HEIGHT });
offset += ROW_HEIGHT;
}
if (section === "settled" && settledCount > visibleSettled.length) {
result.push({ key: "settled-more", section, more: true, offset, height: ROW_HEIGHT });
offset += ROW_HEIGHT;
}
}
return result;
}, [sections, expanded]);
}, [sections, expanded, visibleSettled, settledCount]);
const list = useRef<FlatList<Row>>(null);
const geometry = useRef({ height: 0, offset: 0 });
const drag = useRef<Drag | null>(null);
Expand Down Expand Up @@ -507,6 +533,25 @@ export function ThreadArrangementSheet(props: { onClose: () => void }) {
}}
/>
</>
) : item.more ? (
<Pressable
disabled={olderSettledShelf.loading}
className="flex-1 justify-center self-stretch"
onPress={() => {
olderSettledShelf.retry();
setSettledVisibleCount(
(count) => count + THREAD_LIST_V2_SETTLED_PAGE_COUNT,
);
}}
>
<Text className="text-sm text-foreground-muted">
{olderSettledShelf.loading
? "Loading"
: olderSettledShelf.failed
? "Could not load older threads. Retry"
: `Show more (${settledCount - visibleSettled.length} settled hidden)`}
</Text>
</Pressable>
) : (
<Pressable
disabled={item.section === "pinned" || item.section === "active"}
Expand All @@ -519,7 +564,10 @@ export function ThreadArrangementSheet(props: { onClose: () => void }) {
>
<Text className="text-sm font-t3-semibold text-foreground-muted">
{item.section[0]!.toUpperCase() + item.section.slice(1)} (
{sections[item.section].length})
{item.section === "settled"
? settledCount
: sections[item.section].length}
)
</Text>
</Pressable>
)}
Expand Down
Loading
Loading