diff --git a/apps/web/src/components/settings/ProjectSettingsPanel.tsx b/apps/web/src/components/settings/ProjectSettingsPanel.tsx index 8e5edec0f485..9907ea832f0a 100644 --- a/apps/web/src/components/settings/ProjectSettingsPanel.tsx +++ b/apps/web/src/components/settings/ProjectSettingsPanel.tsx @@ -10,7 +10,7 @@ import { AsyncResult } from "effect/unstable/reactivity"; import { type EnvironmentId, type ProjectIconOverride } from "@t3tools/contracts"; import { useLocation, useNavigate } from "@tanstack/react-router"; import * as Cause from "effect/Cause"; -import { InfoIcon, Trash2Icon } from "lucide-react"; +import { DownloadIcon, InfoIcon, Trash2Icon } from "lucide-react"; import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useComposerDraftStore } from "../../composerDraftStore"; @@ -20,6 +20,7 @@ import { type SidebarProjectGroupMember, type SidebarProjectSnapshot, } from "../../sidebarProjectGrouping"; +import { agentSessionImport } from "../../state/agentSessions"; import { useEnvironments, usePrimaryEnvironmentId } from "../../state/environments"; import { useThreadShells } from "../../state/entities"; import { projectEnvironment } from "../../state/projects"; @@ -178,6 +179,7 @@ function ProjectDetail({ const threads = useThreadShells(); const updateProject = useAtomCommand(projectEnvironment.update, { reportFailure: false }); const deleteProject = useAtomCommand(projectEnvironment.delete, { reportFailure: false }); + const importSessions = useAtomCommand(agentSessionImport, { reportFailure: false }); const projectNameEditedRef = useRef(false); const faviconPath = representative.faviconPath ?? null; @@ -273,6 +275,52 @@ function ProjectDetail({ [group.memberProjects, updateAllMembers], ); + // Onboarding imports in bulk; this reruns the same idempotent import for a + // project that already exists, e.g. to pick up sessions started in a terminal. + const [isImportingSessions, setIsImportingSessions] = useState(false); + const importMemberSessions = async () => { + setIsImportingSessions(true); + let importedCount = 0; + let skippedCount = 0; + let succeededCount = 0; + try { + // One offline checkout must not block the others. + for (const member of group.memberProjects) { + const result = await importSessions({ + environmentId: member.environmentId, + input: { projectId: member.id, expectedWorkspaceRoot: member.workspaceRoot }, + }); + if (result._tag === "Failure") { + reportFailure( + group.memberProjects.length > 1 + ? `Failed to import sessions on ${member.environmentLabel ?? "the current environment"}` + : "Failed to import sessions", + mapAtomCommandResult(result, () => undefined), + ); + continue; + } + succeededCount += 1; + importedCount += result.value.importedCount; + skippedCount += result.value.skippedCount; + } + } finally { + setIsImportingSessions(false); + } + if (succeededCount === 0) return; + toastManager.add({ + type: skippedCount > 0 ? "warning" : "success", + title: skippedCount > 0 ? "Some sessions could not be imported" : "Sessions imported", + description: [ + `${importedCount} recent ${importedCount === 1 ? "session is" : "sessions are"} in this project.`, + ...(skippedCount > 0 + ? [ + `${skippedCount} ${skippedCount === 1 ? "session" : "sessions"} could not be imported.`, + ] + : []), + ].join(" "), + }); + }; + // ----- project icon ----- const [faviconPickerOpen, setFaviconPickerOpen] = useState(false); const [iconPickerOpen, setIconPickerOpen] = useState(false); @@ -488,6 +536,22 @@ function ProjectDetail({ } /> + void importMemberSessions()} + > + + {isImportingSessions ? "Importing…" : "Import"} + + } + /> diff --git a/docs/user/project-settings.md b/docs/user/project-settings.md index fe3d96ba472c..b89c6f0b0c1a 100644 --- a/docs/user/project-settings.md +++ b/docs/user/project-settings.md @@ -45,11 +45,15 @@ access. Source Control contains automatic pull, the default pull request merge m generation. The same rows edit environment defaults or project overrides depending on the project crumb. -The Project category, shown while a project is selected, holds the project's name, icon, actions, +The Project category, shown while a project is selected, holds the project's name, icon, session import, actions, checkouts and removal. Actions belong to a project: editing them creates the project's own list on each selected environment, and reset returns to the environment's shared list. A project's `t3.json` actions can be imported there. +To bring in Claude Code and Codex conversations that ran in the project's folders during the last +30 days, select **Import** in the **Import sessions** row. Run it again to pick up newer +conversations; ones already imported are skipped. + Settings a repository can also declare in `t3.json`, such as the workspace for new threads, resolve in one order: a project override, then the environment setting, then `t3.json`, then the built-in default. Leave a setting on **Inherit** to let the next tier decide. diff --git a/docs/user/welcome-wizard.md b/docs/user/welcome-wizard.md index 816a24e7d844..89f4bfd8adfa 100644 --- a/docs/user/welcome-wizard.md +++ b/docs/user/welcome-wizard.md @@ -78,5 +78,8 @@ with up to 100,000 input records. Run import again to continue a large batch. Completed conversations are not imported again. You can continue without the remaining history. +To import conversations after setup, such as ones you started in a terminal, pick the +project in Settings and select **Import** in the **Import sessions** row on its **Project** page. + You can continue without configuring agents or importing projects, or return to an earlier step using the setup progress bar. Navigation pauses while an import is running.