t3code-cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -8,7 +8,7 @@ import { C as make$12, S as Crypto, x as T3Auth } from "./layer-CfC5qZol.js";
8
8
  import { n as T3Config, t as Environment } from "./service-CLmRO2Dp.js";
9
9
  import { t as T3Application } from "./service-ybOWV9pL.js";
10
10
  import { i as resolveWorktreePath, r as resolveThreadId, t as resolveCommandProjectRef } from "./scope-GycYiJ54.js";
11
- import { d as latestAssistantMessage, f as threadStatus, m as ThreadSessionError, p as ProjectLookupError, t as AppLayer } from "./layer-DIg0RxSO.js";
11
+ import { d as latestAssistantMessage, f as threadStatus, m as ThreadSessionError, p as ProjectLookupError, t as AppLayer } from "./layer-MqCKkIGD.js";
12
12
  import { t as NodeEnvironmentLive } from "./layer-DHhKS5jd.js";
13
13
  import * as NodeChildProcess from "node:child_process";
14
14
  import * as NodeCrypto from "node:crypto";
@@ -4859,7 +4859,7 @@ var T3Version = class extends Service()("t3cli/T3Version") {};
4859
4859
  //#endregion
4860
4860
  //#region src/version/layer.ts
4861
4861
  const PackageJsonSchema = fromJsonString(Struct({ version: String$1 }));
4862
- const T3VersionBundledLive = sync$2(T3Version, () => ({ version: "0.6.0" }));
4862
+ const T3VersionBundledLive = sync$2(T3Version, () => ({ version: "0.7.0" }));
4863
4863
  effect(T3Version, gen(function* () {
4864
4864
  const packageJson = yield* (yield* FileSystem).readFileString(fileURLToPath(new URL("../../package.json", import.meta.url)));
4865
4865
  return { version: (yield* decodeUnknownEffect(PackageJsonSchema)(packageJson)).version };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as T3Application } from "./service-ybOWV9pL.js";
2
- import { n as AuthAppLayer, t as AppLayer } from "./layer-DIg0RxSO.js";
2
+ import { n as AuthAppLayer, t as AppLayer } from "./layer-MqCKkIGD.js";
3
3
  import "./application.js";
4
4
  import "./runtime.js";
5
5
  export { AppLayer, AuthAppLayer, T3Application };
@@ -120,73 +120,68 @@ const makeModelsApplication = fn("makeModelsApplication")(function* () {
120
120
  //#endregion
121
121
  //#region src/domain/helpers.ts
122
122
  const resolveProjectScope = fn("resolveProjectScope")(function* (snapshot, input) {
123
- const absoluteRef = yield* resolveAbsolutePath(input.ref, input.cwd);
123
+ const path = yield* Path;
124
124
  const byId = findProjectById(snapshot, input.ref);
125
125
  if (byId !== null) return { project: byId };
126
- const byExactPath = yield* findProjectByWorkspaceRoot(snapshot, absoluteRef, input.cwd);
127
- if (byExactPath !== void 0) return { project: byExactPath };
128
- const byAncestor = yield* findProjectByAncestorPath(snapshot, absoluteRef, input.cwd);
129
- if (byAncestor !== void 0) return byAncestor;
130
- return yield* findProjectByKnownWorktreePath(snapshot, absoluteRef, input.cwd);
126
+ if (!path.isAbsolute(input.ref)) return;
127
+ return yield* findProjectByPathPriority(snapshot, path.normalize(input.ref));
131
128
  });
132
129
  function findProjectById(snapshot, projectId) {
133
130
  return snapshot.projects.find((project) => project.id === projectId) ?? null;
134
131
  }
135
- const resolveAbsolutePath = fn("resolveAbsolutePath")(function* (ref, cwd) {
136
- const path = yield* Path;
137
- return path.isAbsolute(ref) ? path.normalize(ref) : path.normalize(path.resolve(cwd, ref));
138
- });
139
132
  const isDescendantPath = fn("isDescendantPath")(function* (parent, child) {
140
133
  const path = yield* Path;
141
134
  if (child === parent) return true;
142
135
  const relative = path.relative(parent, child);
143
136
  return relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative);
144
137
  });
145
- const findProjectByWorkspaceRoot = fn("findProjectByWorkspaceRoot")(function* (snapshot, absolutePath, cwd) {
146
- for (const project of snapshot.projects) if ((yield* resolveAbsolutePath(project.workspaceRoot, cwd)) === absolutePath) return project;
147
- });
148
- const findProjectByAncestorPath = fn("findProjectByAncestorPath")(function* (snapshot, absolutePath, cwd) {
149
- let bestProject;
150
- let bestWorkspaceRoot = "";
151
- for (const project of snapshot.projects) {
152
- const workspaceRoot = yield* resolveAbsolutePath(project.workspaceRoot, cwd);
153
- if (!(yield* isDescendantPath(workspaceRoot, absolutePath))) continue;
154
- if (workspaceRoot.length > bestWorkspaceRoot.length) {
155
- bestProject = project;
156
- bestWorkspaceRoot = workspaceRoot;
157
- }
158
- }
159
- if (bestProject === void 0) return;
160
- if (absolutePath === bestWorkspaceRoot) return { project: bestProject };
161
- return {
162
- project: bestProject,
163
- inferredWorktreePath: absolutePath
164
- };
165
- });
166
- const findProjectByKnownWorktreePath = fn("findProjectByKnownWorktreePath")(function* (snapshot, absolutePath, cwd) {
138
+ const findProjectByPathPriority = fn("findProjectByPathPriority")(function* (snapshot, absolutePath) {
139
+ const path = yield* Path;
167
140
  const projectsById = new Map(snapshot.projects.map((project) => [project.id, project]));
168
- let bestProject;
169
- let bestKnownPath = "";
141
+ const workspaceRoots = new Map(snapshot.projects.map((project) => [project.id, path.normalize(project.workspaceRoot)]));
142
+ const candidates = [];
170
143
  for (const thread of snapshot.threads) {
171
144
  if (thread.worktreePath === null) continue;
172
145
  const project = projectsById.get(thread.projectId);
173
146
  if (project === void 0) continue;
174
- const knownPath = yield* resolveAbsolutePath(thread.worktreePath, cwd);
175
- if (!(yield* isDescendantPath(knownPath, absolutePath))) continue;
176
- if (knownPath.length > bestKnownPath.length) {
177
- bestProject = project;
178
- bestKnownPath = knownPath;
179
- }
147
+ const workspaceRoot = workspaceRoots.get(thread.projectId);
148
+ if (workspaceRoot === void 0) continue;
149
+ const matchPath = path.normalize(thread.worktreePath);
150
+ if (!(yield* isDescendantPath(matchPath, absolutePath))) continue;
151
+ candidates.push({
152
+ project,
153
+ matchPath,
154
+ workspaceRoot,
155
+ source: "worktree"
156
+ });
157
+ }
158
+ for (const project of snapshot.projects) {
159
+ const workspaceRoot = workspaceRoots.get(project.id);
160
+ if (workspaceRoot === void 0) continue;
161
+ if (!(yield* isDescendantPath(workspaceRoot, absolutePath))) continue;
162
+ candidates.push({
163
+ project,
164
+ matchPath: workspaceRoot,
165
+ workspaceRoot,
166
+ source: "project"
167
+ });
168
+ }
169
+ if (candidates.length === 0) return;
170
+ candidates.sort((left, right) => {
171
+ if (left.matchPath.length !== right.matchPath.length) return right.matchPath.length - left.matchPath.length;
172
+ if (left.source === right.source) return 0;
173
+ return left.source === "worktree" ? -1 : 1;
174
+ });
175
+ const best = candidates[0];
176
+ if (absolutePath === best.matchPath) {
177
+ if (best.source === "worktree" && best.matchPath !== best.workspaceRoot) return {
178
+ project: best.project,
179
+ inferredWorktreePath: best.matchPath
180
+ };
181
+ return { project: best.project };
180
182
  }
181
- if (bestProject === void 0) return;
182
- const workspaceRoot = yield* resolveAbsolutePath(bestProject.workspaceRoot, cwd);
183
- if (absolutePath === workspaceRoot) return { project: bestProject };
184
- if (absolutePath === bestKnownPath) return bestKnownPath === workspaceRoot ? { project: bestProject } : {
185
- project: bestProject,
186
- inferredWorktreePath: bestKnownPath
187
- };
188
183
  return {
189
- project: bestProject,
184
+ project: best.project,
190
185
  inferredWorktreePath: absolutePath
191
186
  };
192
187
  });
@@ -444,10 +439,7 @@ const makeThreadApplication = fn("makeThreadApplication")(function* () {
444
439
  const environment = yield* Environment;
445
440
  const listThreads = fn("T3ApplicationLive.listThreads")(function* (projectRef) {
446
441
  const snapshot = yield* orchestration.getShellSnapshot();
447
- const scope = yield* resolveProjectScope(snapshot, {
448
- ref: projectRef,
449
- cwd: environment.cwd
450
- }).pipe(provideService(Path, path));
442
+ const scope = yield* resolveProjectScope(snapshot, { ref: projectRef }).pipe(provideService(Path, path));
451
443
  if (scope === void 0) return yield* fail(new ProjectLookupError({
452
444
  message: `project not found: ${projectRef}`,
453
445
  ref: projectRef
@@ -471,10 +463,7 @@ const makeThreadApplication = fn("makeThreadApplication")(function* () {
471
463
  message: "project is required",
472
464
  ref: environment.cwd
473
465
  }));
474
- const scope = yield* resolveProjectScope(snapshot, {
475
- ref: projectRef,
476
- cwd: environment.cwd
477
- }).pipe(provideService(Path, path));
466
+ const scope = yield* resolveProjectScope(snapshot, { ref: projectRef }).pipe(provideService(Path, path));
478
467
  if (scope === void 0) return yield* fail(new ProjectLookupError({
479
468
  message: `project not found: ${projectRef}`,
480
469
  ref: projectRef
@@ -1,2 +1,2 @@
1
- import { l as T3OrchestrationLayer, u as T3Orchestration } from "./layer-DIg0RxSO.js";
1
+ import { l as T3OrchestrationLayer, u as T3Orchestration } from "./layer-MqCKkIGD.js";
2
2
  export { T3Orchestration, T3OrchestrationLayer };
package/dist/runtime.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as T3AuthTransportLayer, c as T3LocalAuthTokenLayer, i as T3AuthPairingLayer, l as T3OrchestrationLayer, n as AuthAppLayer, o as T3LocalAuthLayer, r as T3AuthLayer, s as T3LocalAuthOriginLayer, t as AppLayer } from "./layer-DIg0RxSO.js";
1
+ import { a as T3AuthTransportLayer, c as T3LocalAuthTokenLayer, i as T3AuthPairingLayer, l as T3OrchestrationLayer, n as AuthAppLayer, o as T3LocalAuthLayer, r as T3AuthLayer, s as T3LocalAuthOriginLayer, t as AppLayer } from "./layer-MqCKkIGD.js";
2
2
  import { t as NodeEnvironmentLive } from "./layer-DHhKS5jd.js";
3
3
  export { AppLayer, AuthAppLayer, NodeEnvironmentLive, T3AuthLayer, T3AuthPairingLayer, T3AuthTransportLayer, T3LocalAuthLayer, T3LocalAuthOriginLayer, T3LocalAuthTokenLayer, T3OrchestrationLayer };
@@ -91,7 +91,6 @@ export declare const resolveProjectScope: (snapshot: {
91
91
  readonly updatedAt: string;
92
92
  }, input: {
93
93
  readonly ref: string;
94
- readonly cwd: string;
95
94
  }) => Effect.Effect<{
96
95
  project: {
97
96
  readonly id: string & import("effect/Brand").Brand<"ProjectId">;
@@ -128,7 +127,7 @@ export declare const resolveProjectScope: (snapshot: {
128
127
  readonly name?: string;
129
128
  } | null | undefined;
130
129
  };
131
- inferredWorktreePath?: never;
130
+ inferredWorktreePath: string;
132
131
  } | {
133
132
  project: {
134
133
  readonly id: string & import("effect/Brand").Brand<"ProjectId">;
@@ -165,6 +164,6 @@ export declare const resolveProjectScope: (snapshot: {
165
164
  readonly name?: string;
166
165
  } | null | undefined;
167
166
  };
168
- inferredWorktreePath: string;
167
+ inferredWorktreePath?: never;
169
168
  } | undefined, never, Path.Path>;
170
169
  export declare function findProjectById(snapshot: OrchestrationShellSnapshot, projectId: string): OrchestrationProjectShell | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "t3code-cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "CLI for t3code",
5
5
  "keywords": [
6
6
  "claude",
@@ -93,13 +93,15 @@
93
93
  },
94
94
  "devDependencies": {
95
95
  "@changesets/cli": "^2.31.0",
96
+ "@effect/vitest": "4.0.0-beta.78",
97
+ "@total-typescript/shoehorn": "^0.1.2",
96
98
  "@types/node": "^25.9.1",
97
99
  "typescript": "^6.0.3",
98
100
  "vite-plus": "^0.1.24",
99
101
  "@t3tools/contracts": "0.0.24"
100
102
  },
101
103
  "engines": {
102
- "node": "^24.0.0"
104
+ "node": ">=24.0.0"
103
105
  },
104
106
  "scripts": {
105
107
  "build": "vp pack && tsc -p tsconfig.dts.json",
@@ -108,6 +110,7 @@
108
110
  "format:check": "vp fmt --check",
109
111
  "lint": "vp lint",
110
112
  "lint:fix": "vp lint --fix",
113
+ "test": "vp test run",
111
114
  "changeset": "changeset",
112
115
  "release:version": "changeset version && pnpm format",
113
116
  "release:check": "pnpm check && pnpm typecheck && pnpm pack --dry-run",
@@ -29,7 +29,6 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
29
29
  const snapshot = yield* orchestration.getShellSnapshot();
30
30
  const scope = yield* resolveProjectScope(snapshot, {
31
31
  ref: projectRef,
32
- cwd: environment.cwd,
33
32
  }).pipe(Effect.provideService(Path.Path, path));
34
33
  if (scope === undefined) {
35
34
  return yield* Effect.fail(
@@ -70,7 +69,6 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
70
69
  }
71
70
  const scope = yield* resolveProjectScope(snapshot, {
72
71
  ref: projectRef,
73
- cwd: environment.cwd,
74
72
  }).pipe(Effect.provideService(Path.Path, path));
75
73
  if (scope === undefined) {
76
74
  return yield* Effect.fail(
@@ -0,0 +1,103 @@
1
+ import "vite-plus/test/config";
2
+
3
+ import * as Effect from "effect/Effect";
4
+ import * as NodeServices from "@effect/platform-node/NodeServices";
5
+ import { assert, describe, it } from "@effect/vitest";
6
+ import { fromPartial } from "@total-typescript/shoehorn";
7
+
8
+ import type { OrchestrationShellSnapshot } from "#t3tools/contracts";
9
+
10
+ import { resolveProjectScope } from "./helpers.ts";
11
+
12
+ describe("resolveProjectScope", () => {
13
+ it.layer(NodeServices.layer)("resolveProjectScope", (t) => {
14
+ t.effect("resolves by id first (even if ref not absolute)", () =>
15
+ Effect.gen(function* () {
16
+ const snapshot: OrchestrationShellSnapshot = fromPartial({
17
+ projects: [{ id: "proj-1", workspaceRoot: "/workspace" }],
18
+ threads: [],
19
+ });
20
+
21
+ const scope = yield* resolveProjectScope(snapshot, { ref: "proj-1" });
22
+ assert.equal(scope?.project.id, "proj-1");
23
+ assert.equal(scope?.inferredWorktreePath, undefined);
24
+ }),
25
+ );
26
+
27
+ t.effect("returns undefined for non-absolute path refs", () =>
28
+ Effect.gen(function* () {
29
+ const snapshot: OrchestrationShellSnapshot = fromPartial({
30
+ projects: [{ id: "proj-1", workspaceRoot: "/workspace" }],
31
+ threads: [],
32
+ });
33
+
34
+ const scope = yield* resolveProjectScope(snapshot, { ref: "workspace/subdir" });
35
+ assert.equal(scope, undefined);
36
+ }),
37
+ );
38
+
39
+ t.effect("prefers longest matching workspaceRoot", () =>
40
+ Effect.gen(function* () {
41
+ const snapshot: OrchestrationShellSnapshot = fromPartial({
42
+ projects: [
43
+ { id: "proj-a", workspaceRoot: "/workspace" },
44
+ { id: "proj-b", workspaceRoot: "/workspace/sub" },
45
+ ],
46
+ threads: [],
47
+ });
48
+
49
+ const scope = yield* resolveProjectScope(snapshot, { ref: "/workspace/sub/deep" });
50
+ assert.equal(scope?.project.id, "proj-b");
51
+ assert.equal(scope?.inferredWorktreePath, "/workspace/sub/deep");
52
+ }),
53
+ );
54
+
55
+ t.effect("does not infer worktree when ref equals workspaceRoot", () =>
56
+ Effect.gen(function* () {
57
+ const snapshot: OrchestrationShellSnapshot = fromPartial({
58
+ projects: [
59
+ { id: "proj-a", workspaceRoot: "/workspace" },
60
+ { id: "proj-b", workspaceRoot: "/workspace/sub" },
61
+ ],
62
+ threads: [],
63
+ });
64
+
65
+ const scope = yield* resolveProjectScope(snapshot, { ref: "/workspace/sub" });
66
+ assert.equal(scope?.project.id, "proj-b");
67
+ assert.equal(scope?.inferredWorktreePath, undefined);
68
+ }),
69
+ );
70
+
71
+ t.effect("prefers worktree candidate over project candidate for same match path", () =>
72
+ Effect.gen(function* () {
73
+ const snapshot: OrchestrationShellSnapshot = fromPartial({
74
+ projects: [
75
+ { id: "proj-a", workspaceRoot: "/workspace" },
76
+ { id: "proj-b", workspaceRoot: "/workspace/proj" },
77
+ ],
78
+ threads: [{ projectId: "proj-a", worktreePath: "/workspace/proj" }],
79
+ });
80
+
81
+ const scope = yield* resolveProjectScope(snapshot, { ref: "/workspace/proj" });
82
+ assert.equal(scope?.project.id, "proj-a");
83
+ assert.equal(scope?.inferredWorktreePath, "/workspace/proj");
84
+ }),
85
+ );
86
+
87
+ t.effect("prefers longest matching worktree path", () =>
88
+ Effect.gen(function* () {
89
+ const snapshot: OrchestrationShellSnapshot = fromPartial({
90
+ projects: [{ id: "proj-a", workspaceRoot: "/workspace" }],
91
+ threads: [
92
+ { projectId: "proj-a", worktreePath: "/workspace/proj" },
93
+ { projectId: "proj-a", worktreePath: "/workspace/proj/deep" },
94
+ ],
95
+ });
96
+
97
+ const scope = yield* resolveProjectScope(snapshot, { ref: "/workspace/proj/deep/child" });
98
+ assert.equal(scope?.project.id, "proj-a");
99
+ assert.equal(scope?.inferredWorktreePath, "/workspace/proj/deep/child");
100
+ }),
101
+ );
102
+ });
103
+ });
@@ -11,27 +11,20 @@ export const resolveProjectScope = Effect.fn("resolveProjectScope")(function* (
11
11
  snapshot: OrchestrationShellSnapshot,
12
12
  input: {
13
13
  readonly ref: string;
14
- readonly cwd: string;
15
14
  },
16
15
  ) {
17
- const absoluteRef = yield* resolveAbsolutePath(input.ref, input.cwd);
16
+ const path = yield* Path.Path;
18
17
 
19
18
  const byId = findProjectById(snapshot, input.ref);
20
19
  if (byId !== null) {
21
20
  return { project: byId };
22
21
  }
23
22
 
24
- const byExactPath = yield* findProjectByWorkspaceRoot(snapshot, absoluteRef, input.cwd);
25
- if (byExactPath !== undefined) {
26
- return { project: byExactPath };
27
- }
28
-
29
- const byAncestor = yield* findProjectByAncestorPath(snapshot, absoluteRef, input.cwd);
30
- if (byAncestor !== undefined) {
31
- return byAncestor;
23
+ if (!path.isAbsolute(input.ref)) {
24
+ return undefined;
32
25
  }
33
26
 
34
- return yield* findProjectByKnownWorktreePath(snapshot, absoluteRef, input.cwd);
27
+ return yield* findProjectByPathPriority(snapshot, path.normalize(input.ref));
35
28
  });
36
29
 
37
30
  export function findProjectById(
@@ -41,11 +34,6 @@ export function findProjectById(
41
34
  return snapshot.projects.find((project) => project.id === projectId) ?? null;
42
35
  }
43
36
 
44
- const resolveAbsolutePath = Effect.fn("resolveAbsolutePath")(function* (ref: string, cwd: string) {
45
- const path = yield* Path.Path;
46
- return path.isAbsolute(ref) ? path.normalize(ref) : path.normalize(path.resolve(cwd, ref));
47
- });
48
-
49
37
  const isDescendantPath = Effect.fn("isDescendantPath")(function* (parent: string, child: string) {
50
38
  const path = yield* Path.Path;
51
39
  if (child === parent) {
@@ -55,61 +43,23 @@ const isDescendantPath = Effect.fn("isDescendantPath")(function* (parent: string
55
43
  return relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative);
56
44
  });
57
45
 
58
- const findProjectByWorkspaceRoot = Effect.fn("findProjectByWorkspaceRoot")(function* (
59
- snapshot: OrchestrationShellSnapshot,
60
- absolutePath: string,
61
- cwd: string,
62
- ) {
63
- for (const project of snapshot.projects) {
64
- const workspaceRoot = yield* resolveAbsolutePath(project.workspaceRoot, cwd);
65
- if (workspaceRoot === absolutePath) {
66
- return project;
67
- }
68
- }
69
- return undefined;
70
- });
71
-
72
- const findProjectByAncestorPath = Effect.fn("findProjectByAncestorPath")(function* (
46
+ const findProjectByPathPriority = Effect.fn("findProjectByPathPriority")(function* (
73
47
  snapshot: OrchestrationShellSnapshot,
74
48
  absolutePath: string,
75
- cwd: string,
76
- ) {
77
- let bestProject: OrchestrationProjectShell | undefined;
78
- let bestWorkspaceRoot = "";
79
-
80
- for (const project of snapshot.projects) {
81
- const workspaceRoot = yield* resolveAbsolutePath(project.workspaceRoot, cwd);
82
- if (!(yield* isDescendantPath(workspaceRoot, absolutePath))) {
83
- continue;
84
- }
85
- if (workspaceRoot.length > bestWorkspaceRoot.length) {
86
- bestProject = project;
87
- bestWorkspaceRoot = workspaceRoot;
88
- }
89
- }
90
-
91
- if (bestProject === undefined) {
92
- return undefined;
93
- }
94
-
95
- if (absolutePath === bestWorkspaceRoot) {
96
- return { project: bestProject };
97
- }
98
-
99
- return {
100
- project: bestProject,
101
- inferredWorktreePath: absolutePath,
102
- };
103
- });
104
-
105
- const findProjectByKnownWorktreePath = Effect.fn("findProjectByKnownWorktreePath")(function* (
106
- snapshot: OrchestrationShellSnapshot,
107
- absolutePath: string,
108
- cwd: string,
109
49
  ) {
50
+ const path = yield* Path.Path;
110
51
  const projectsById = new Map(snapshot.projects.map((project) => [project.id, project]));
111
- let bestProject: OrchestrationProjectShell | undefined;
112
- let bestKnownPath = "";
52
+ const workspaceRoots = new Map(
53
+ snapshot.projects.map(
54
+ (project) => [project.id, path.normalize(project.workspaceRoot)] as const,
55
+ ),
56
+ );
57
+ const candidates: Array<{
58
+ readonly project: OrchestrationProjectShell;
59
+ readonly matchPath: string;
60
+ readonly workspaceRoot: string;
61
+ readonly source: "worktree" | "project";
62
+ }> = [];
113
63
 
114
64
  for (const thread of snapshot.threads) {
115
65
  if (thread.worktreePath === null) {
@@ -119,33 +69,59 @@ const findProjectByKnownWorktreePath = Effect.fn("findProjectByKnownWorktreePath
119
69
  if (project === undefined) {
120
70
  continue;
121
71
  }
122
- const knownPath = yield* resolveAbsolutePath(thread.worktreePath, cwd);
123
- if (!(yield* isDescendantPath(knownPath, absolutePath))) {
72
+ const workspaceRoot = workspaceRoots.get(thread.projectId);
73
+ if (workspaceRoot === undefined) {
124
74
  continue;
125
75
  }
126
- if (knownPath.length > bestKnownPath.length) {
127
- bestProject = project;
128
- bestKnownPath = knownPath;
76
+ const matchPath = path.normalize(thread.worktreePath);
77
+ if (!(yield* isDescendantPath(matchPath, absolutePath))) {
78
+ continue;
129
79
  }
80
+ candidates.push({
81
+ project,
82
+ matchPath,
83
+ workspaceRoot,
84
+ source: "worktree",
85
+ });
130
86
  }
131
87
 
132
- if (bestProject === undefined) {
133
- return undefined;
88
+ for (const project of snapshot.projects) {
89
+ const workspaceRoot = workspaceRoots.get(project.id);
90
+ if (workspaceRoot === undefined) {
91
+ continue;
92
+ }
93
+ if (!(yield* isDescendantPath(workspaceRoot, absolutePath))) {
94
+ continue;
95
+ }
96
+ candidates.push({
97
+ project,
98
+ matchPath: workspaceRoot,
99
+ workspaceRoot,
100
+ source: "project",
101
+ });
134
102
  }
135
103
 
136
- const workspaceRoot = yield* resolveAbsolutePath(bestProject.workspaceRoot, cwd);
137
- if (absolutePath === workspaceRoot) {
138
- return { project: bestProject };
104
+ if (candidates.length === 0) {
105
+ return undefined;
139
106
  }
140
107
 
141
- if (absolutePath === bestKnownPath) {
142
- return bestKnownPath === workspaceRoot
143
- ? { project: bestProject }
144
- : { project: bestProject, inferredWorktreePath: bestKnownPath };
108
+ candidates.sort((left, right) => {
109
+ if (left.matchPath.length !== right.matchPath.length) {
110
+ return right.matchPath.length - left.matchPath.length;
111
+ }
112
+ if (left.source === right.source) {
113
+ return 0;
114
+ }
115
+ return left.source === "worktree" ? -1 : 1;
116
+ });
117
+
118
+ const best = candidates[0]!;
119
+ if (absolutePath === best.matchPath) {
120
+ if (best.source === "worktree" && best.matchPath !== best.workspaceRoot) {
121
+ return { project: best.project, inferredWorktreePath: best.matchPath };
122
+ }
123
+ return { project: best.project };
145
124
  }
146
125
 
147
- return {
148
- project: bestProject,
149
- inferredWorktreePath: absolutePath,
150
- };
126
+ return { project: best.project, inferredWorktreePath: absolutePath };
151
127
  });