Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
dd8c649
Mobile: enlarge new-task composer text to 17pt
t3dotgg Jun 29, 2026
e899698
Pull uncommitted work: fix mobile thread scroll jump (worktree 20990773)
t3dotgg Jul 4, 2026
6f5dd37
Pull uncommitted work: quick project thread button (worktree 27a5341b)
t3dotgg Jul 4, 2026
fab05c3
Pull uncommitted work: relative time formatting tweaks (worktree 2d30…
t3dotgg Jul 4, 2026
ad6c6c1
Pull uncommitted work: thread swipe area / route screens (worktree 42…
t3dotgg Jul 4, 2026
9531885
Pull uncommitted work: new-task draft flow changes (worktree 4673f128)
t3dotgg Jul 4, 2026
efee0c9
Pull uncommitted work: home thread list changes (worktree c4c62ae9), …
t3dotgg Jul 4, 2026
aa38fd2
Merge origin/main into integration/test-all, re-porting mobile change…
t3dotgg Jul 4, 2026
9ac1d7f
Upgrade react-native-keyboard-controller 1.21.6 → 1.21.13 for content…
t3dotgg Jul 4, 2026
90ea2d3
Fail the header drawer pan at touch-down outside the header strip
t3dotgg Jul 4, 2026
f726113
Remove the legacy thread navigation drawer and its edge-pan gesture
t3dotgg Jul 4, 2026
3edb72e
Address macroscope review findings
t3dotgg Jul 4, 2026
3fd7f73
Group header polish + second-round review fixes
t3dotgg Jul 4, 2026
cf9a564
Treat empty workspaceRoot as null when matching hosts by basename
t3dotgg Jul 4, 2026
14dcf30
Fix group header plus-button vertical alignment
t3dotgg Jul 4, 2026
8fb1785
Extend collapse toggle hitSlop over the left gutter
t3dotgg Jul 4, 2026
21eb3bd
Add the quick new-thread button to the split-view sidebar
t3dotgg Jul 4, 2026
542ce60
Show the quick new-thread button on multi-machine project groups
t3dotgg Jul 4, 2026
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
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
"react-native": "0.85.3",
"react-native-gesture-handler": "~2.31.1",
"react-native-image-viewing": "^0.2.2",
"react-native-keyboard-controller": "1.21.6",
"react-native-keyboard-controller": "1.21.13",
"react-native-nitro-markdown": "^0.5.0",
"react-native-nitro-modules": "0.35.9",
"react-native-reanimated": "4.3.1",
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ function ArchivedThreadRow(props: {
</Text>
<Text
className="text-xs text-foreground-tertiary"
style={{ fontVariant: ["tabular-nums"] }}
style={{ fontVariant: ["tabular-nums"], minWidth: 30, textAlign: "right" }}
>
{timestamp}
</Text>
Expand Down
10 changes: 10 additions & 0 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ export function HomeRouteScreen() {
}}
onSelectPendingTask={openPendingTask}
onDeletePendingTask={confirmDeletePendingTask}
onNewThreadInProject={(project) => {
navigation.navigate("NewTaskSheet", {
screen: "NewTaskDraft",
params: {
environmentId: String(project.environmentId),
projectId: String(project.id),
title: project.title,
},
});
}}
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
onThreadSortOrderChange={setThreadSortOrder}
pendingTasks={pendingTasks}
Expand Down
8 changes: 8 additions & 0 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ interface HomeScreenProps {
readonly onDeleteThread: (thread: EnvironmentThreadShell) => void;
readonly onSelectPendingTask: (pendingTask: PendingNewTask) => void;
readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void;
readonly onNewThreadInProject: (project: EnvironmentProject) => void;
}

/* ─── Layout constants ───────────────────────────────────────────────── */
Expand Down Expand Up @@ -248,6 +249,12 @@ export function HomeScreen(props: HomeScreenProps) {
isFirst={item.isFirst}
groupKey={item.group.key}
onGroupAction={updateGroupDisplay}
// Aggregated groups (same repo across machines) have no single
// target project, and `pending-project:` groups hold a placeholder
// built from queued-task metadata rather than a real project shell,
// so the quick new-thread button is single-real-project only.
newThreadTarget={item.group.newThreadTarget}
onNewThread={props.onNewThreadInProject}
project={item.group.representative}
threadCount={item.group.threads.length + item.group.pendingTasks.length}
title={item.group.title}
Expand Down Expand Up @@ -308,6 +315,7 @@ export function HomeScreen(props: HomeScreenProps) {
props.onArchiveThread,
props.onDeletePendingTask,
props.onDeleteThread,
props.onNewThreadInProject,
props.onSelectPendingTask,
props.onSelectThread,
props.savedConnectionsById,
Expand Down
53 changes: 50 additions & 3 deletions apps/mobile/src/features/home/homeListItems.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,20 @@ function makeThread(id: string, projectId: ProjectId): EnvironmentThreadShell {

function makeGroup(key: string, threadCount: number): HomeThreadGroup {
const project = makeProject(key, key);
const threads = Array.from({ length: threadCount }, (_, index) =>
makeThread(`${key}-thread-${index}`, project.id),
);
return {
key,
title: key,
representative: project,
projects: [project],
pendingTasks: [],
threads: Array.from({ length: threadCount }, (_, index) =>
makeThread(`${key}-thread-${index}`, project.id),
),
threads,
// All threads inside the recency window, so the baseline stays at the
// initial page size and the pagination expectations below hold.
recentThreads: threads,
newThreadTarget: project,
};
}

Expand Down Expand Up @@ -153,6 +158,48 @@ describe("buildHomeListLayout", () => {
expect(reset.visibleCount).toBe(HOME_INITIAL_VISIBLE_THREADS);
});

it("offers show-less after expanding a stale group whose baseline is below the page size", () => {
// Stale project: 10 threads total but only 3 within the recency window.
const project = makeProject("stale", "stale");
const threads = Array.from({ length: 10 }, (_, index) =>
makeThread(`stale-thread-${index}`, project.id),
);
const group: HomeThreadGroup = {
key: "stale",
title: "stale",
representative: project,
projects: [project],
pendingTasks: [],
threads,
recentThreads: threads.slice(0, 3),
newThreadTarget: project,
};

const collapsedToRecent = buildHomeListLayout({
groups: [group],
displayStates: displayStates({}),
});
expect(collapsedToRecent.items.filter((item) => item.type === "thread")).toHaveLength(3);
expect(collapsedToRecent.items.at(-1)).toMatchObject({
type: "show-more",
hiddenCount: 7,
canShowLess: false,
});

const expanded = buildHomeListLayout({
groups: [group],
displayStates: displayStates({
stale: nextGroupDisplayState(DEFAULT_GROUP_DISPLAY_STATE, "show-more"),
}),
});
expect(expanded.items.filter((item) => item.type === "thread")).toHaveLength(10);
expect(expanded.items.at(-1)).toMatchObject({
type: "show-more",
hiddenCount: 0,
canShowLess: true,
});
});

it("hides threads and the show-more row for collapsed groups", () => {
const layout = buildHomeListLayout({
groups: [makeGroup("alpha", 12), makeGroup("beta", 2)],
Expand Down
24 changes: 21 additions & 3 deletions apps/mobile/src/features/home/homeListItems.ts
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,27 @@ export function buildHomeListLayout(input: {
}

const totalCount = group.threads.length;
// Default to the group's recent-activity window (last few days, or a small
// fallback for stale projects), capped at the initial page size. Until the
// user taps "Show more", older threads stay hidden to save vertical space;
// "Show less" resets visibleCount to the initial constant, which lands back
// here at the recency baseline.
const baselineCount = Math.min(
group.recentThreads.length,
HOME_INITIAL_VISIBLE_THREADS,
totalCount,
);
const visibleCount = input.showAllThreads
? totalCount
: Math.min(Math.max(display.visibleCount, HOME_INITIAL_VISIBLE_THREADS), totalCount);
: Math.min(
display.visibleCount > HOME_INITIAL_VISIBLE_THREADS
? display.visibleCount
: baselineCount,
totalCount,
);
const visibleThreads = group.threads.slice(0, visibleCount);
const hiddenCount = totalCount - visibleCount;
const hasShowMoreRow = !input.showAllThreads && totalCount > HOME_INITIAL_VISIBLE_THREADS;
const hasShowMoreRow = !input.showAllThreads && totalCount > baselineCount;

// Pending (unsent) tasks lead the group and are never paginated away.
for (const [pendingIndex, pendingTask] of group.pendingTasks.entries()) {
Expand Down Expand Up @@ -180,7 +195,10 @@ export function buildHomeListLayout(input: {
key: `show-more:${group.key}`,
groupKey: group.key,
hiddenCount,
canShowLess: visibleCount > HOME_INITIAL_VISIBLE_THREADS,
// Compare against the group's own baseline, not the global page size:
// stale projects start below HOME_INITIAL_VISIBLE_THREADS, and "Show
// less" must be offered as soon as anything beyond the baseline shows.
canShowLess: visibleCount > baselineCount,
});
}
}
Expand Down
143 changes: 143 additions & 0 deletions apps/mobile/src/features/home/homeThreadList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ function makeThread(
};
}

const NOW = Date.parse("2026-06-29T00:00:00.000Z");

function buildGroups(
projects: ReadonlyArray<EnvironmentProject>,
threads: ReadonlyArray<EnvironmentThreadShell>,
Expand All @@ -57,6 +59,7 @@ function buildGroups(
projectSortOrder: "updated_at",
threadSortOrder: "updated_at",
projectGroupingMode: "repository",
now: NOW,
...overrides,
});
}
Expand Down Expand Up @@ -220,4 +223,144 @@ describe("buildHomeThreadGroups", () => {
);
expect(buildGroups(projects, threads, { projectGroupingMode: "separate" })).toHaveLength(2);
});

it("default view shows only threads from the last 5 days", () => {
const environmentId = EnvironmentId.make("environment-1");
const project = makeProject({
environmentId,
id: ProjectId.make("project-1"),
title: "T3 Code",
});
const threads = [
makeThread({
environmentId,
id: ThreadId.make("recent-1"),
projectId: project.id,
title: "Today",
updatedAt: "2026-06-28T00:00:00.000Z",
}),
makeThread({
environmentId,
id: ThreadId.make("recent-2"),
projectId: project.id,
title: "Within window",
updatedAt: "2026-06-25T00:00:00.000Z",
}),
makeThread({
environmentId,
id: ThreadId.make("old"),
projectId: project.id,
title: "Two weeks ago",
updatedAt: "2026-06-14T00:00:00.000Z",
}),
];

const group = buildGroups([project], threads)[0];
// Default view trims to recent threads...
expect(group?.recentThreads.map((thread) => thread.id)).toEqual(["recent-1", "recent-2"]);
// ...while full history stays available for the expanded view.
expect(group?.threads.map((thread) => thread.id)).toEqual(["recent-1", "recent-2", "old"]);
});

it("falls back to the most recent 3 threads when none are within 5 days", () => {
const environmentId = EnvironmentId.make("environment-1");
const project = makeProject({
environmentId,
id: ProjectId.make("project-1"),
title: "T3 Code",
});
const threads = ["2026-06-01", "2026-06-02", "2026-06-03", "2026-06-04", "2026-06-05"].map(
(day, index) =>
makeThread({
environmentId,
id: ThreadId.make(`thread-${index}`),
projectId: project.id,
title: `Thread ${index}`,
updatedAt: `${day}T00:00:00.000Z`,
}),
);

const group = buildGroups([project], threads)[0];
expect(group?.recentThreads.map((thread) => thread.id)).toEqual([
"thread-4",
"thread-3",
"thread-2",
]);
expect(group?.threads).toHaveLength(5);
});

it("does not apply the recency window while searching", () => {
const environmentId = EnvironmentId.make("environment-1");
const project = makeProject({
environmentId,
id: ProjectId.make("project-1"),
title: "T3 Code",
});
const threads = ["2026-06-01", "2026-06-02", "2026-06-03", "2026-06-04", "2026-06-05"].map(
(day, index) =>
makeThread({
environmentId,
id: ThreadId.make(`thread-${index}`),
projectId: project.id,
title: `Thread ${index}`,
updatedAt: `${day}T00:00:00.000Z`,
}),
);

const group = buildGroups([project], threads, { searchQuery: "T3 Code" })[0];
// Search reaches the full history rather than the 3-thread fallback.
expect(group?.recentThreads).toHaveLength(5);
expect(group?.recentThreads.map((thread) => thread.id)).toEqual(
group?.threads.map((thread) => thread.id),
);
});

it("targets quick new threads at the group member with the newest thread", () => {
const laptopEnv = EnvironmentId.make("environment-laptop");
const desktopEnv = EnvironmentId.make("environment-desktop");
const repositoryIdentity = {
canonicalKey: "github.com/pingdotgg/t3code",
locator: {
source: "git-remote" as const,
remoteName: "origin",
remoteUrl: "git@github.com:pingdotgg/t3code.git",
},
};
const laptopProject = makeProject({
environmentId: laptopEnv,
id: ProjectId.make("project-laptop"),
title: "t3code",
repositoryIdentity,
});
const desktopProject = makeProject({
environmentId: desktopEnv,
id: ProjectId.make("project-desktop"),
title: "t3code",
repositoryIdentity,
});
const threads = [
makeThread({
environmentId: laptopEnv,
id: ThreadId.make("thread-laptop"),
projectId: laptopProject.id,
title: "Older laptop thread",
updatedAt: "2026-06-27T00:00:00.000Z",
}),
makeThread({
environmentId: desktopEnv,
id: ThreadId.make("thread-desktop"),
projectId: desktopProject.id,
title: "Newest desktop thread",
updatedAt: "2026-06-28T00:00:00.000Z",
}),
];

// Aggregated into one group by repository; the quick new-thread target
// must follow the newest thread (desktop), not the arbitrary first member.
const groups = buildGroups([laptopProject, desktopProject], threads);
expect(groups).toHaveLength(1);
expect(groups[0]?.projects).toHaveLength(2);
expect(groups[0]?.newThreadTarget?.environmentId).toBe(desktopEnv);
expect(groups[0]?.newThreadTarget?.id).toBe(desktopProject.id);
});
});
Loading
Loading