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
20 changes: 20 additions & 0 deletions apps/mobile/src/features/archive/archivedThreadList.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ArchivedSnapshotEntry } from "@t3tools/client-runtime/state/threads";
import { GENERAL_CHATS_PROJECT_ID } from "@t3tools/client-runtime/general-chats";
import type { OrchestrationProjectShell, OrchestrationThreadShell } from "@t3tools/contracts";
import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";
Expand Down Expand Up @@ -61,6 +62,25 @@ function makeSnapshot(
}

describe("buildArchivedThreadGroups", () => {
it("keeps archived standalone chats out of the mobile archive", () => {
const chatsProject = makeProject({ id: GENERAL_CHATS_PROJECT_ID, title: "Chats" });
const chatsThread = makeThread({
id: ThreadId.make("chat-thread"),
projectId: GENERAL_CHATS_PROJECT_ID,
title: "Archived chat",
});

expect(
buildArchivedThreadGroups({
snapshots: [makeSnapshot([chatsProject], [chatsThread])],
environmentLabels: {},
environmentId: null,
searchQuery: "",
sortOrder: "newest",
}),
).toEqual([]);
});

it("groups archived threads by project and sorts newest first", () => {
const project = makeProject({ id: ProjectId.make("project-1"), title: "T3 Code" });
const older = makeThread({
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/src/features/archive/archivedThreadList.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ArchivedSnapshotEntry } from "@t3tools/client-runtime/state/threads";
import { isGeneralChatsProjectId } from "@t3tools/client-runtime/general-chats";
import {
scopeProject,
scopeThreadShell,
Expand Down Expand Up @@ -55,6 +56,9 @@ export function buildArchivedThreadGroups(input: {
}

for (const rawProject of entry.snapshot.projects) {
if (isGeneralChatsProjectId(rawProject.id)) {
continue;
}
const project = scopeProject(entry.environmentId, rawProject);
const projectThreads = threadsByProjectId.get(project.id) ?? [];
const groupMatches =
Expand Down
18 changes: 18 additions & 0 deletions apps/mobile/src/features/home/homeThreadList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
EnvironmentProject,
EnvironmentThreadShell,
} from "@t3tools/client-runtime/state/shell";
import { GENERAL_CHATS_PROJECT_ID } from "@t3tools/client-runtime/general-chats";
import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

Expand Down Expand Up @@ -65,6 +66,23 @@ function buildGroups(
}

describe("buildHomeThreadGroups", () => {
it("keeps standalone chats out of the normal mobile thread groups", () => {
const environmentId = EnvironmentId.make("environment-1");
const chatsProject = makeProject({
environmentId,
id: GENERAL_CHATS_PROJECT_ID,
title: "Chats",
});
const chatsThread = makeThread({
environmentId,
id: ThreadId.make("chat-thread"),
projectId: GENERAL_CHATS_PROJECT_ID,
title: "Ad hoc chat",
});

expect(buildGroups([chatsProject], [chatsThread])).toEqual([]);
});

it("sorts the newest thread first regardless of snapshot order", () => {
const environmentId = EnvironmentId.make("environment-1");
const project = makeProject({
Expand Down
14 changes: 12 additions & 2 deletions apps/mobile/src/features/home/homeThreadList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import {
deriveLogicalProjectKey,
deriveProjectGroupLabel,
} from "@t3tools/client-runtime/state/project-grouping";
import {
excludeGeneralChatsProject,
excludeGeneralChatsThreads,
isGeneralChatsProjectId,
} from "@t3tools/client-runtime/general-chats";
import type {
EnvironmentProject,
EnvironmentThreadShell,
Expand Down Expand Up @@ -100,10 +105,12 @@ export function buildHomeThreadGroups(input: {
readonly now?: number;
}): ReadonlyArray<HomeThreadGroup> {
const now = input.now ?? Date.now();
const projects = excludeGeneralChatsProject(input.projects);
const threads = excludeGeneralChatsThreads(input.threads);
const groups = new Map<string, MutableHomeThreadGroup>();
const groupKeyByProjectKey = new Map<string, string>();

for (const project of input.projects) {
for (const project of projects) {
if (input.environmentId !== null && project.environmentId !== input.environmentId) {
continue;
}
Expand All @@ -123,6 +130,9 @@ export function buildHomeThreadGroups(input: {
}

for (const pendingTask of input.pendingTasks ?? []) {
if (isGeneralChatsProjectId(pendingTask.creation.projectId)) {
continue;
}
if (input.environmentId !== null && pendingTask.message.environmentId !== input.environmentId) {
continue;
}
Expand Down Expand Up @@ -161,7 +171,7 @@ export function buildHomeThreadGroups(input: {
groups.get(groupKey)?.pendingTasks.push(pendingTask);
}

for (const thread of input.threads) {
for (const thread of threads) {
if (thread.archivedAt !== null) {
continue;
}
Expand Down
5 changes: 4 additions & 1 deletion apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isAtomCommandInterrupted,
squashAtomCommandFailure,
} from "@t3tools/client-runtime/state/runtime";
import { isGeneralChatsProjectId } from "@t3tools/client-runtime/general-chats";

import { ComposerEditor, type ComposerEditorHandle } from "../../components/ComposerEditor";
import {
Expand Down Expand Up @@ -243,7 +244,9 @@ export function NewTaskDraftScreen(props: {
const directProject =
projects.find(
(project) =>
project.environmentId === initialEnvironmentId && project.id === initialProjectId,
project.environmentId === initialEnvironmentId &&
project.id === initialProjectId &&
!isGeneralChatsProjectId(project.id),
) ?? null;

if (directProject) {
Expand Down
9 changes: 7 additions & 2 deletions apps/mobile/src/features/threads/new-task-flow-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import type { DraftComposerImageAttachment } from "../../lib/composerImages";
import type { ModelOption, ProviderGroup } from "../../lib/modelOptions";
import { buildModelOptions, groupByProvider } from "../../lib/modelOptions";
import { groupProjectsByRepository } from "../../lib/repositoryGroups";
import {
excludeGeneralChatsProject,
isGeneralChatsProjectId,
} from "@t3tools/client-runtime/general-chats";
import { scopedProjectKey } from "../../lib/scopedEntities";
import { appAtomRegistry } from "../../state/atom-registry";
import {
Expand Down Expand Up @@ -166,7 +170,8 @@ type NewTaskFlowContextValue = {
const NewTaskFlowContext = React.createContext<NewTaskFlowContextValue | null>(null);

export function NewTaskFlowProvider(props: React.PropsWithChildren) {
const projects = useProjects();
const allProjects = useProjects();
const projects = useMemo(() => excludeGeneralChatsProject(allProjects), [allProjects]);
const threads = useThreadShells();
const { savedConnectionsById } = useSavedRemoteConnections();

Expand Down Expand Up @@ -625,7 +630,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {

const beginEditingPendingTask = useCallback((messageId: string): boolean => {
const message = findQueuedPendingTask(messageId);
if (!message?.creation) {
if (!message?.creation || isGeneralChatsProjectId(message.creation.projectId)) {
return false;
}
const draftKey = pendingTaskDraftKey(message.messageId);
Expand Down
21 changes: 21 additions & 0 deletions apps/mobile/src/lib/repositoryGroups.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools

import { groupProjectsByRepository } from "./repositoryGroups";
import { EnvironmentProject, EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
import { GENERAL_CHATS_PROJECT_ID } from "@t3tools/client-runtime/general-chats";

function makeProject(
input: Partial<EnvironmentProject> & Pick<EnvironmentProject, "environmentId" | "id" | "title">,
Expand Down Expand Up @@ -187,4 +188,24 @@ describe("groupProjectsByRepository", () => {
expect(groups[0]?.title).toBe("Scratchpad");
expect(groups[0]?.subtitle).toBeNull();
});

it("keeps the app-managed Chats project out of mobile project pickers", () => {
const environmentId = EnvironmentId.make("env-local");
const chatsProject = makeProject({
environmentId,
id: GENERAL_CHATS_PROJECT_ID,
title: "Chats",
});
const chatsThread = makeThread({
environmentId,
id: ThreadId.make("chat-thread"),
projectId: GENERAL_CHATS_PROJECT_ID,
title: "Ad hoc chat",
modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },
});

expect(groupProjectsByRepository({ projects: [chatsProject], threads: [chatsThread] })).toEqual(
[],
);
});
});
10 changes: 8 additions & 2 deletions apps/mobile/src/lib/repositoryGroups.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import * as Order from "effect/Order";
import * as Arr from "effect/Array";
import {
excludeGeneralChatsProject,
excludeGeneralChatsThreads,
} from "@t3tools/client-runtime/general-chats";
import type { RepositoryIdentity } from "@t3tools/contracts";

import { scopedProjectKey } from "./scopedEntities";
Expand Down Expand Up @@ -62,9 +66,11 @@ export function groupProjectsByRepository(input: {
readonly projects: ReadonlyArray<EnvironmentProject>;
readonly threads: ReadonlyArray<EnvironmentThreadShell>;
}): ReadonlyArray<RepositoryGroup> {
const projects = excludeGeneralChatsProject(input.projects);
const threads = excludeGeneralChatsThreads(input.threads);
const threadsByProjectKey = new Map<string, EnvironmentThreadShell[]>();

for (const thread of input.threads) {
for (const thread of threads) {
const key = scopedProjectKey(thread.environmentId, thread.projectId);
const existing = threadsByProjectKey.get(key);
if (existing) {
Expand All @@ -76,7 +82,7 @@ export function groupProjectsByRepository(input: {

const grouped = new Map<string, RepositoryGroup>();

for (const project of input.projects) {
for (const project of projects) {
const key = deriveRepositoryGroupKey(project);
const projectKey = scopedProjectKey(project.environmentId, project.id);
const threads = Arr.sortWith(
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5230,6 +5230,7 @@ function ChatViewContent(props: ChatViewProps) {
activeThreadId={activeThread.id}
{...(routeKind === "draft" && draftId ? { draftId } : {})}
activeThreadTitle={activeThread.title}
activeProjectId={activeProject?.id}
activeProjectName={activeProject?.title}
openInCwd={gitCwd}
activeProjectScripts={activeProject?.scripts}
Expand Down
62 changes: 32 additions & 30 deletions apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "./Icons"
import { ProjectFavicon } from "./ProjectFavicon";
import { ThreadRowLeadingStatus, ThreadRowTrailingStatus } from "./ThreadStatusIndicators";
import { primaryServerKeybindingsAtom } from "../state/server";
import { excludeGeneralChatsProject } from "../generalChats";
import { resolveShortcutCommand } from "../keybindings";
import {
Command,
Expand Down Expand Up @@ -474,6 +475,7 @@ function OpenCommandPaletteDialog(props: {
const { activeDraftThread, activeThread, defaultProjectRef, handleNewThread } =
useHandleNewThread();
const projects = useProjects();
const regularProjects = useMemo(() => excludeGeneralChatsProject(projects), [projects]);
const threads = useThreadShells();
const keybindings = useAtomValue(primaryServerKeybindingsAtom);
const [viewStack, setViewStack] = useState<CommandPaletteView[]>([]);
Expand Down Expand Up @@ -653,7 +655,7 @@ function OpenCommandPaletteDialog(props: {
const projectSearchItems = useMemo(
() =>
buildProjectActionItems({
projects,
projects: regularProjects,
valuePrefix: "project",
icon: (project) => (
<ProjectFavicon
Expand All @@ -664,13 +666,13 @@ function OpenCommandPaletteDialog(props: {
),
runProject: openProjectFromSearch,
}),
[openProjectFromSearch, projects],
[openProjectFromSearch, regularProjects],
);

const projectThreadItems = useMemo(
() =>
buildProjectActionItems({
projects,
projects: regularProjects,
valuePrefix: "new-thread-in",
shortcutCommand: "chat.new",
icon: (project) => (
Expand All @@ -692,7 +694,7 @@ function OpenCommandPaletteDialog(props: {
);
},
}),
[activeDraftThread, activeThread, defaultProjectRef, handleNewThread, projects],
[activeDraftThread, activeThread, defaultProjectRef, handleNewThread, regularProjects],
);

const allThreadItems = useMemo(
Expand Down Expand Up @@ -962,34 +964,34 @@ function OpenCommandPaletteDialog(props: {

const actionItems: Array<CommandPaletteActionItem | CommandPaletteSubmenuItem> = [];

if (projects.length > 0) {
const activeProjectTitle = currentProjectId
? (projectTitleById.get(currentProjectId) ?? null)
: null;
const activeProjectTitle = currentProjectId
? (projectTitleById.get(currentProjectId) ?? null)
: null;

if (activeProjectTitle) {
actionItems.push({
kind: "action",
value: "action:new-thread",
searchTerms: ["new thread", "chat", "create", "draft"],
title: (
<>
New thread in <span className="font-semibold">{activeProjectTitle}</span>
</>
),
icon: <SquarePenIcon className={ITEM_ICON_CLASS} />,
shortcutCommand: "chat.new",
run: async () => {
await startNewThreadFromContext({
activeDraftThread,
activeThread: activeThread ?? undefined,
defaultProjectRef,
handleNewThread,
});
},
});
}
if (activeProjectTitle) {
actionItems.push({
kind: "action",
value: "action:new-thread",
searchTerms: ["new thread", "chat", "create", "draft"],
title: (
<>
New thread in <span className="font-semibold">{activeProjectTitle}</span>
</>
),
icon: <SquarePenIcon className={ITEM_ICON_CLASS} />,
shortcutCommand: "chat.new",
run: async () => {
await startNewThreadFromContext({
activeDraftThread,
activeThread: activeThread ?? undefined,
defaultProjectRef,
handleNewThread,
});
},
});
}

if (regularProjects.length > 0) {
actionItems.push({
kind: "submenu",
value: "action:new-thread-in",
Expand Down
Loading
Loading