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
26 changes: 14 additions & 12 deletions apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ type ArchivedThreadListItem =
readonly key: string;
readonly environmentLabel: string | null;
readonly environmentMachine: EnvironmentMachineKind;
readonly project: EnvironmentProject;
readonly project: EnvironmentProject | null;
}
| {
readonly kind: "thread";
Expand Down Expand Up @@ -368,22 +368,24 @@ function ArchivedThreadsHeader(props: {
function ProjectGroupLabel(props: {
readonly environmentLabel: string | null;
readonly environmentMachine: EnvironmentMachineKind;
readonly project: EnvironmentProject;
readonly project: EnvironmentProject | null;
}) {
return (
<View className="flex-row items-center gap-2.5 px-1 pb-2">
<ProjectFavicon
environmentId={props.project.environmentId}
faviconPath={props.project.faviconPath}
projectTitle={props.project.title}
size={18}
workspaceRoot={props.project.workspaceRoot}
/>
{props.project && (
<ProjectFavicon
environmentId={props.project.environmentId}
faviconPath={props.project.faviconPath}
projectTitle={props.project.title}
size={18}
workspaceRoot={props.project.workspaceRoot}
/>
)}
<Text
className="flex-1 text-xs font-t3-medium tracking-[0.5px] uppercase text-foreground-muted"
numberOfLines={1}
>
{props.project.title}
{props.project?.title ?? "Quick chats"}
</Text>
{props.environmentLabel ? (
<View className="max-w-[42%] flex-row items-center gap-1">
Expand Down Expand Up @@ -538,13 +540,13 @@ export function ArchivedThreadsScreen(props: {
const listItems = useMemo<ReadonlyArray<ArchivedThreadListItem>>(() => {
const items: ArchivedThreadListItem[] = [];
for (const group of props.groups) {
const environmentLabel = environmentLabelsById.get(group.project.environmentId) ?? null;
const environmentLabel = environmentLabelsById.get(group.threads[0]!.environmentId) ?? null;
items.push({
kind: "project",
key: `${group.key}:project`,
environmentLabel,
environmentMachine: resolveEnvironmentMachineKind(
serverConfigs.get(group.project.environmentId) ?? null,
serverConfigs.get(group.threads[0]!.environmentId) ?? null,
),
project: group.project,
});
Expand Down
28 changes: 27 additions & 1 deletion apps/mobile/src/features/archive/archivedThreadList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ describe("buildArchivedThreadGroups", () => {
});

expect(result).toHaveLength(1);
expect(result[0]?.project.environmentId).toBe(environmentId);
expect(result[0]?.project?.environmentId).toBe(environmentId);
expect(result[0]?.threads.map((thread) => thread.id)).toEqual(["thread-1"]);
});

Expand All @@ -144,3 +144,29 @@ describe("buildArchivedThreadGroups", () => {
expect(result).toEqual([]);
});
});

it("shows archived quick chats in an environment without projects", () => {
const quick = makeThread({ id: ThreadId.make("quick"), projectId: null, title: "Passkeys" });
const groups = buildArchivedThreadGroups({
snapshots: [makeSnapshot([], [quick])],
environmentLabels: {},
environmentId,
searchQuery: "passkeys",
sortOrder: "newest",
});
expect(groups).toHaveLength(1);
expect(groups[0]?.project).toBeNull();
expect(groups[0]?.threads[0]).toMatchObject({ id: quick.id, environmentId, projectId: null });
});

it("does not match every archived quick chat through a section-label substring", () => {
const quick = makeThread({ id: ThreadId.make("quick"), projectId: null, title: "Passkeys" });
const input = {
snapshots: [makeSnapshot([], [quick])],
environmentLabels: {},
environmentId,
sortOrder: "newest" as const,
};
expect(buildArchivedThreadGroups({ ...input, searchQuery: "ui" })).toEqual([]);
expect(buildArchivedThreadGroups({ ...input, searchQuery: "quick chats" })).toHaveLength(1);
});
26 changes: 23 additions & 3 deletions apps/mobile/src/features/archive/archivedThreadList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type ArchivedThreadSortOrder = "newest" | "oldest";

export interface ArchivedThreadGroup {
readonly key: string;
readonly project: EnvironmentProject;
readonly project: EnvironmentProject | null;
readonly threads: ReadonlyArray<EnvironmentThreadShell>;
}

Expand Down Expand Up @@ -44,7 +44,7 @@ export function buildArchivedThreadGroups(input: {
}

const environmentLabel = input.environmentLabels[entry.environmentId] ?? null;
const threadsByProjectId = new Map<string, EnvironmentThreadShell[]>();
const threadsByProjectId = new Map<string | null, EnvironmentThreadShell[]>();
for (const thread of entry.snapshot.threads) {
if (thread.archivedAt === null) {
continue;
Expand All @@ -54,6 +54,26 @@ export function buildArchivedThreadGroups(input: {
threadsByProjectId.set(thread.projectId, threads);
}

const quickChats = (threadsByProjectId.get(null) ?? [])
.filter(
(thread) =>
query.length === 0 ||
query === "quick chats" ||
matchesQuery(thread.title, query) ||
matchesQuery(environmentLabel, query),
)
Comment thread
StiensWout marked this conversation as resolved.
.sort(
(left, right) =>
(input.sortOrder === "newest" ? -1 : 1) *
(archiveTimestamp(left) - archiveTimestamp(right)),
);
if (quickChats.length > 0)
groups.push({
key: `${entry.environmentId}:quick-chats`,
project: null,
threads: quickChats,
});

for (const rawProject of entry.snapshot.projects) {
const project = scopeProject(entry.environmentId, rawProject);
const projectThreads = threadsByProjectId.get(project.id) ?? [];
Expand Down Expand Up @@ -98,7 +118,7 @@ export function buildArchivedThreadGroups(input: {
Order.Struct({ timestamp: timestampOrder, title: Order.String, key: Order.String }),
(group: ArchivedThreadGroup) => ({
timestamp: group.threads[0] ? archiveTimestamp(group.threads[0]) : 0,
title: group.project.title,
title: group.project?.title ?? "Quick chats",
key: group.key,
}),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { orchestrationEnvironment } from "../../state/orchestration";
function archivedSnapshotAtom(environmentId: EnvironmentId) {
return orchestrationEnvironment.archivedShellSnapshot({
environmentId,
input: {},
input: { includeQuickChats: true },
});
}

Expand Down
91 changes: 65 additions & 26 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -676,21 +676,12 @@ export function HomeScreen(props: HomeScreenProps) {
snoozeWakeTick,
]);
const threadListV2Layout = useMemo(() => {
if (!threadListV2Enabled)
return {
items: [],
hiddenSettledCount: 0,
snoozedCount: 0,
snoozedShelfHeaderIndex: null,
settledCount: 0,
settledShelfHeaderIndex: null,
nextSnoozeWakeAt: null,
};
// Settled threads are live shells; archived threads keep their original
// "hidden from lists" meaning.
return buildThreadListV2Items({
pendingOrder,
threads: props.threads.filter((thread) => thread.archivedAt === null),
threads: props.threads.filter(
(thread) =>
thread.archivedAt === null && (threadListV2Enabled || thread.projectId === null),
),
environmentId: props.selectedEnvironmentId,
projectRefs: v2ScopedProjectGroup === null ? null : v2ScopedProjectGroup.projectRefs,
searchQuery: props.searchQuery,
Expand Down Expand Up @@ -759,7 +750,7 @@ export function HomeScreen(props: HomeScreenProps) {
() =>
buildThreadListV2ListItems({
items: threadListV2Layout.items,
pendingTasks: v2PendingTasks,
pendingTasks: threadListV2Enabled ? v2PendingTasks : [],
snoozedCount: threadListV2Layout.snoozedCount,
snoozedShelfExpanded,
snoozedShelfHeaderIndex: threadListV2Layout.snoozedShelfHeaderIndex,
Expand All @@ -768,7 +759,13 @@ export function HomeScreen(props: HomeScreenProps) {
settledShelfHeaderIndex: threadListV2Layout.settledShelfHeaderIndex,
snoozeLabelNow: `${nowMinute}:00.000Z`,
}),
[settledShelfExpanded, snoozedShelfExpanded, threadListV2Layout, v2PendingTasks],
[
settledShelfExpanded,
snoozedShelfExpanded,
threadListV2Layout,
v2PendingTasks,
threadListV2Enabled,
],
);

const renderV2Item = useCallback(
Expand All @@ -777,6 +774,10 @@ export function HomeScreen(props: HomeScreenProps) {
const showTrailingDivider =
nextItem?.type === "v2-thread" ||
(nextItem?.type === "v2-pending" && !nextItem.showPendingDivider);
if (item.type === "v2-quick-chats-header")
return (
<Text className="px-4 pt-4 pb-2 text-sm font-t3-bold text-foreground">Quick chats</Text>
);
if (item.type === "v2-pending") {
const pendingScopeKey = scopedProjectKey(
item.pendingTask.environmentId,
Expand Down Expand Up @@ -860,14 +861,21 @@ export function HomeScreen(props: HomeScreenProps) {
onArchiveThread={props.onArchiveThread}
onRegenerateThreadTitle={handleRegenerateThreadTitle}
titleRegenerationSupported={titleRegenerationEnvironmentIds.has(thread.environmentId)}
settlementSupported={settlementEnvironmentIds.has(thread.environmentId)}
settlementSupported={
thread.projectId !== null && settlementEnvironmentIds.has(thread.environmentId)
}
onSettleThread={handleSettleThread}
snoozeSupported={snoozeEnvironmentIds.has(thread.environmentId)}
pinningSupported={pinningEnvironmentIds.has(thread.environmentId)}
snoozeSupported={
thread.projectId !== null && snoozeEnvironmentIds.has(thread.environmentId)
}
pinningSupported={
thread.projectId !== null && pinningEnvironmentIds.has(thread.environmentId)
}
reorderSupported={
item.item.pinned
thread.projectId !== null &&
(item.item.pinned
? pinReorderEnvironmentIds.has(thread.environmentId)
: activeReorderEnvironmentIds.has(thread.environmentId)
: activeReorderEnvironmentIds.has(thread.environmentId))
}
canMoveUp={pendingOrder === null && movePlanner(movedId, "up") !== null}
canMoveDown={pendingOrder === null && movePlanner(movedId, "down") !== null}
Expand Down Expand Up @@ -958,7 +966,7 @@ export function HomeScreen(props: HomeScreenProps) {
);

const renderItem = useCallback(
({ item }: LegendListRenderItemProps<HomeListItem>) => {
({ item }: { readonly item: HomeListItem }) => {
switch (item.type) {
case "header":
return (
Expand Down Expand Up @@ -1056,7 +1064,25 @@ export function HomeScreen(props: HomeScreenProps) {
],
);

const keyExtractor = useCallback((item: HomeListItem) => item.key, []);
const legacyListItems = useMemo(
() => [...listLayout.items, ...threadListV2Items],
[listLayout.items, threadListV2Items],
);
const renderLegacyListItem = useCallback(
(props: LegendListRenderItemProps<HomeListItem | ThreadListV2ListItem>) => {
const item = props.item;
if (
item.type === "header" ||
item.type === "thread" ||
item.type === "pending-task" ||
item.type === "show-more"
)
return renderItem({ item });
return renderV2Item({ item, index: props.index - listLayout.items.length });
},
[renderItem, renderV2Item, listLayout.items.length],
);
const keyExtractor = useCallback((item: HomeListItem | ThreadListV2ListItem) => item.key, []);

/* Empty states */
// The signal must ignore the search/environment filters: an active query
Expand Down Expand Up @@ -1200,15 +1226,28 @@ export function HomeScreen(props: HomeScreenProps) {
<SwipeableScrollGateProvider enabled={swipeEnabled}>
<LegendList
ref={listRef}
data={listLayout.items}
renderItem={renderItem}
data={legacyListItems}
renderItem={renderLegacyListItem}
keyExtractor={keyExtractor}
itemsAreEqual={homeListItemsAreEqual}
itemsAreEqual={(previous, item) => {
if (
(previous.type === "header" ||
previous.type === "thread" ||
previous.type === "pending-task" ||
previous.type === "show-more") &&
(item.type === "header" ||
item.type === "thread" ||
item.type === "pending-task" ||
item.type === "show-more")
)
return homeListItemsAreEqual(previous, item);
return previous === item;
}}
drawDistance={500}
estimatedItemSize={ESTIMATED_THREAD_ROW_HEIGHT}
extraData={extraData}
ListHeaderComponent={listHeader}
ListEmptyComponent={listEmpty}
ListEmptyComponent={threadListV2Items.length === 0 ? listEmpty : null}
style={{ flex: 1 }}
automaticallyAdjustsScrollIndicatorInsets={NATIVE_LIQUID_GLASS_SUPPORTED}
contentInsetAdjustmentBehavior={NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider";
import { checkoutNewTaskBranch } from "./checkout-new-task-branch";

function SelectionRow(props: {
export function SelectionRow(props: {
readonly icon?: "arrow.triangle.branch" | ReactNode;
readonly onPress: () => void;
readonly disabled?: boolean;
Expand Down Expand Up @@ -111,7 +111,7 @@ function ToggleRow(props: {
);
}

function BranchSelectionRow(props: {
export function BranchSelectionRow(props: {
readonly badge: string | null;
readonly branch: VcsRef;
readonly disabled: boolean;
Expand Down Expand Up @@ -142,7 +142,7 @@ function BranchSelectionRow(props: {
);
}

function PickerSurface(props: { readonly children: ReactNode }) {
export function PickerSurface(props: { readonly children: ReactNode }) {
return <View className="overflow-hidden rounded-2xl bg-card">{props.children}</View>;
}

Expand Down
6 changes: 5 additions & 1 deletion apps/mobile/src/features/threads/NewTaskRouteScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { QuickChatCreationActions } from "./QuickChatCreationActions";
import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
import {
StackActions,
Expand Down Expand Up @@ -102,7 +103,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
? `Choose a project for the ${incomingShare.attachments[0]?.type === "image" ? "image" : "file"} you shared`
: `Choose a project for the ${incomingShare.attachments.length} ${incomingShare.attachments.every((attachment) => attachment.type === "image") ? "images" : "files"} you shared`
: null;
const screenTitle = incomingShare ? "Start a task" : "Choose project";
const screenTitle = incomingShare ? "Start a task" : "New thread";
const projectEmptyState = deriveProjectEmptyState(catalogState);
const resumedDestinationKeyRef = useRef<string | null>(null);
const reservedDestinationProject = incomingShare?.destination
Expand Down Expand Up @@ -316,6 +317,9 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
})}
</View>
)}
{!incomingShare && (
<QuickChatCreationActions preferredEnvironmentId={selectedEnvironmentId} />
)}
</ScrollView>
</View>
);
Expand Down
Loading
Loading