History

fix(web): tolerate servers that predate git identity in project import (#10547)

Julius Marminge committed 2 months ago

Commit 62fbbe01 parent 1d1bf505 files · +73 −14Browse files

Unified · Split

5 files
  1. apps/server/src/project/AgentSessionScanner.ts +4 −3
  2. apps/web/src/onboarding/projectImport.logic.test.ts +17 −0
  3. apps/web/src/onboarding/projectImport.logic.ts +10 −9
  4. packages/contracts/src/agentSessions.test.ts +36 −0
  5. packages/contracts/src/agentSessions.ts +6 −2
apps/server/src/project/AgentSessionScanner.ts+4−3
@@ -24,6 +24,7 @@ import { · 23 unmodified lines
2424 resolveProviderInstanceEnabled,
2525 type AgentSessionImportSource,
2626 type AgentSessionProjectCandidate,
27+ type AgentSessionProjectGit,
2728 type AgentSessionScanResult,
2829 type ProviderInstanceConfig,
2930 } from "@t3tools/contracts";
@@ -695,7 +696,7 @@ export const make = Effect.gen(function* () { · 665 unmodified lines
695696 const readGitIdentity = Effect.fn("AgentSessionScanner.readGitIdentity")(function* (
696697 directory: string,
697698 ): Effect.fn.Return<
698− | { readonly _tag: "Repository"; readonly git: AgentSessionProjectCandidate["git"] }
699+ | { readonly _tag: "Repository"; readonly git: AgentSessionProjectGit | null }
699700 | { readonly _tag: "Worktree" }
700701 | { readonly _tag: "NotGit" }
701702 > {
@@ -1209,11 +1210,11 @@ export const make = Effect.gen(function* () { · 507 unmodified lines
12091210 sources: Array<AgentSessionSource>;
12101211 threadCount: number;
12111212 lastActiveAtMs: number | null;
1212− git: AgentSessionProjectCandidate["git"];
1213+ git: AgentSessionProjectGit | null;
12131214 }
12141215 >();
12151216 const directoryKeys = new Map<string, string>();
1216− const gitIdentities = new Map<string, AgentSessionProjectCandidate["git"]>();
1217+ const gitIdentities = new Map<string, AgentSessionProjectGit | null>();
12171218
12181219 for (const candidate of raw) {
12191220 const expanded = expandHomePath(candidate.cwd.trim());
apps/web/src/onboarding/projectImport.logic.test.ts+17−0
@@ -74,6 +74,12 @@ describe("partitionOnboardingProjects", () => { · 73 unmodified lines
7474
7575 expect(partitionOnboardingProjects([repo, folder, thin], now).recent).toEqual([repo]);
7676 });
77+
78+ it("selects candidates from servers that do not report git identity", () => {
79+ const { git: _git, ...legacy } = candidate("/projects/legacy");
80+
81+ expect(partitionOnboardingProjects([legacy], now).recent).toEqual([legacy]);
82+ });
7783 });
7884
7985 describe("groupOnboardingProjects", () => {
@@ -127,6 +133,17 @@ describe("groupOnboardingProjects", () => { · 47 unmodified lines
127133 },
128134 ]);
129135 });
136+
137+ it("lists candidates without git identity as standalone repositories", () => {
138+ const { git: _git, ...legacy } = candidate("/code/legacy", { title: "legacy" });
139+
140+ const grouped = groupOnboardingProjects([legacy]);
141+
142+ expect(grouped.other).toEqual([]);
143+ expect(grouped.repositories.map((group) => [group.label, group.repository])).toEqual([
144+ ["legacy", null],
145+ ]);
146+ });
130147 });
131148
132149 describe("resolveOnboardingProjectId", () => {
apps/web/src/onboarding/projectImport.logic.ts+10−9
@@ -9,6 +9,8 @@ const DEFAULT_SELECTION_MIN_THREADS = 3; · 8 unmodified lines
99 * Existing projects still need their agent history imported, so every scan
1010 * candidate is offered. The default selection is narrower: git repositories
1111 * active in the last 30 days with enough threads to look like real work.
12+ * Servers that predate the git scan omit `git`; their candidates are treated
13+ * as repositories so old computers still get a useful default selection.
1214 */
1315 export function partitionOnboardingProjects<T extends AgentSessionProjectCandidate>(
1416 candidates: ReadonlyArray<T>,
@@ -48,9 +50,10 @@ function latestActivity(left: string | null, right: string | null): string | nul · 33 unmodified lines
4850 /**
4951 * Group scan candidates for the onboarding picker. Clones of one repository
5052 * share a group keyed by their normalized origin URL. Repositories without an
51− * origin get a group each. Directories that are not git repositories are
52− * returned separately so the UI can fold them away by default. Groups sort by
53− * most recent activity, newest first.
53+ * origin get a group each, as do candidates from servers that do not report
54+ * git identity. Directories that are not git repositories are returned
55+ * separately so the UI can fold them away by default. Groups sort by most
56+ * recent activity, newest first.
5457 */
5558 export function groupOnboardingProjects<
5659 T extends Pick<
@@ -66,16 +69,14 @@ export function groupOnboardingProjects< · 9 unmodified lines
6669 other.push(candidate);
6770 continue;
6871 }
69− const key =
70− candidate.git.remoteKey === null
71− ? `path:${candidate.path}`
72− : `remote:${candidate.git.remoteKey}`;
72+ const git = candidate.git ?? { remoteKey: null, repository: null };
73+ const key = git.remoteKey === null ? `path:${candidate.path}` : `remote:${git.remoteKey}`;
7374 const existing = groups.get(key);
7475 if (existing === undefined) {
7576 groups.set(key, {
7677 key,
77− label: candidate.git.repository ?? candidate.title,
78− repository: candidate.git.repository,
78+ label: git.repository ?? candidate.title,
79+ repository: git.repository,
7980 candidates: [candidate],
8081 threadCount: candidate.threadCount,
8182 lastActiveAt: candidate.lastActiveAt,
packages/contracts/src/agentSessions.test.ts+36−0
@@ -0,0 +1,36 @@
1+import * as Schema from "effect/Schema";
2+import { describe, expect, it } from "vite-plus/test";
3+
4+import { AgentSessionScanResult } from "./agentSessions.ts";
5+
6+const decodeScanResult = Schema.decodeUnknownSync(AgentSessionScanResult);
7+
8+const candidate = {
9+ path: "/projects/repo",
10+ title: "repo",
11+ sources: ["codex"],
12+ threadCount: 3,
13+ lastActiveAt: "2026-08-20T12:00:00.000Z",
14+ alreadyImported: false,
15+} as const;
16+
17+describe("AgentSessionScanResult", () => {
18+ it("decodes candidates from servers that predate the git scan", () => {
19+ const result = decodeScanResult({
20+ candidates: [candidate],
21+ scannedAt: "2026-08-22T12:00:00.000Z",
22+ });
23+
24+ expect(result.candidates[0]?.git).toBeUndefined();
25+ });
26+
27+ it("preserves reported git identity", () => {
28+ const git = { remoteKey: "github.com/pingdotgg/t3code", repository: "pingdotgg/t3code" };
29+ const result = decodeScanResult({
30+ candidates: [{ ...candidate, git }],
31+ scannedAt: "2026-08-22T12:00:00.000Z",
32+ });
33+
34+ expect(result.candidates[0]?.git).toEqual(git);
35+ });
36+});
packages/contracts/src/agentSessions.ts+6−2
@@ -57,8 +57,12 @@ export const AgentSessionProjectCandidate = Schema.Struct({ · 56 unmodified lines
5757 threadCount: NonNegativeInt,
5858 lastActiveAt: Schema.NullOr(IsoDateTime),
5959 alreadyImported: Schema.Boolean,
60− /** `null` when the directory is not the root of a git repository. */
61− git: Schema.NullOr(AgentSessionProjectGit),
60+ /**
61+ * `null` when the directory is not the root of a git repository. Missing on
62+ * servers that predate the git scan, where the client cannot tell repositories
63+ * from plain folders and should treat every candidate as a standalone project.
64+ */
65+ git: Schema.optionalKey(Schema.NullOr(AgentSessionProjectGit)),
6266 });
6367 export type AgentSessionProjectCandidate = typeof AgentSessionProjectCandidate.Type;
6468