Skip to content

Commit 3318d23

Browse files
centdixclaude
andcommitted
feat: add workspace labels
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0f19a04 commit 3318d23

36 files changed

Lines changed: 705 additions & 26 deletions

backend/src/__tests__/agents-ui-service.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ describe("buildAgentsUiWorktreeSummary", () => {
55
it("maps worktree snapshots into conversation-aware summary data", () => {
66
const summary = buildAgentsUiWorktreeSummary({
77
branch: "feature/search",
8+
label: "Search ranking",
89
baseBranch: "main",
910
path: "/repo/__worktrees/feature-search",
1011
dir: "/repo/__worktrees/feature-search",
@@ -87,6 +88,7 @@ describe("buildAgentsUiWorktreeSummary", () => {
8788
it("defaults missing conversation metadata to null", () => {
8889
const summary = buildAgentsUiWorktreeSummary({
8990
branch: "feature/idle",
91+
label: null,
9092
path: "/repo/__worktrees/feature-idle",
9193
dir: "/repo/__worktrees/feature-idle",
9294
archived: false,

backend/src/__tests__/claude-conversation-service.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ function makeMeta(): WorktreeMeta {
4646
function makeWorktree(): WorktreeSnapshot {
4747
return {
4848
branch: "claude-feature",
49+
label: null,
4950
path: "/tmp/worktrees/claude-feature",
5051
dir: "claude-feature",
5152
archived: false,

backend/src/__tests__/lifecycle-service.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,29 @@ describe("LifecycleService", () => {
10631063
expect(archiveState.entries[0]?.path).toBe(join(repoRoot, "__worktrees", "feature-archive"));
10641064
});
10651065

1066+
it("updates and clears a worktree label in metadata and runtime state", async () => {
1067+
const repoRoot = await initRepo();
1068+
const runtime = new ProjectRuntime();
1069+
const tmux = new FakeTmuxGateway();
1070+
const lifecycle = makeLifecycleService(repoRoot, tmux, runtime);
1071+
1072+
await lifecycle.createWorktree({ branch: "feature-label" });
1073+
const labeled = await lifecycle.setWorktreeLabel("feature-label", " Search ranking ");
1074+
1075+
const worktreePath = join(repoRoot, "__worktrees", "feature-label");
1076+
const gitDir = new BunGitGateway().resolveWorktreeGitDir(worktreePath);
1077+
1078+
expect(labeled).toEqual({ label: "Search ranking" });
1079+
expect((await readWorktreeMeta(gitDir))?.label).toBe("Search ranking");
1080+
expect(runtime.getWorktreeByBranch("feature-label")?.label).toBe("Search ranking");
1081+
1082+
const cleared = await lifecycle.setWorktreeLabel("feature-label", "");
1083+
1084+
expect(cleared).toEqual({ label: null });
1085+
expect((await readWorktreeMeta(gitDir))?.label).toBeUndefined();
1086+
expect(runtime.getWorktreeByBranch("feature-label")?.label).toBeNull();
1087+
});
1088+
10661089
it("creates a managed docker worktree through the container runtime path", async () => {
10671090
const repoRoot = await initRepo();
10681091
const runtime = new ProjectRuntime();

backend/src/__tests__/native-terminal-service.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ function makeState(overrides: Partial<ManagedWorktreeRuntimeState> = {}): Manage
66
return {
77
worktreeId: "wt_feature_search",
88
branch: "feature/search",
9+
label: null,
910
baseBranch: null,
1011
path: "/repo/__worktrees/feature-search",
1112
profile: "default",

backend/src/__tests__/project-runtime.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe("ProjectRuntime", () => {
1616

1717
expect(state.worktreeId).toBe("wt_search");
1818
expect(state.branch).toBe("feature/search");
19+
expect(state.label).toBeNull();
1920
expect(state.baseBranch).toBe("main");
2021
expect(state.profile).toBe("default");
2122
expect(state.agentName).toBe("claude");
@@ -109,4 +110,26 @@ describe("ProjectRuntime", () => {
109110
expect(runtime.getWorktreeByBranch("feature/search")).toBeNull();
110111
expect(runtime.getWorktreeByBranch("feature/search-v2")?.worktreeId).toBe("wt_search");
111112
});
113+
114+
it("updates label metadata without changing branch lookups", () => {
115+
const runtime = new ProjectRuntime();
116+
runtime.upsertWorktree({
117+
worktreeId: "wt_search",
118+
branch: "feature/search",
119+
label: "Search UI",
120+
path: "/repo/__worktrees/feature-search",
121+
runtime: "host",
122+
});
123+
124+
const state = runtime.upsertWorktree({
125+
worktreeId: "wt_search",
126+
branch: "feature/search",
127+
label: "Search ranking",
128+
path: "/repo/__worktrees/feature-search",
129+
runtime: "host",
130+
});
131+
132+
expect(state.label).toBe("Search ranking");
133+
expect(runtime.getWorktreeByBranch("feature/search")?.label).toBe("Search ranking");
134+
});
112135
});

backend/src/__tests__/snapshot-service.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe("buildProjectSnapshot", () => {
99
runtime.upsertWorktree({
1010
worktreeId: "wt_search",
1111
branch: "feature/search",
12+
label: "Search ranking",
1213
baseBranch: "main",
1314
path: "/repo/__worktrees/feature-search",
1415
profile: "default",
@@ -100,6 +101,7 @@ describe("buildProjectSnapshot", () => {
100101
expect(snapshot.worktrees).toEqual([
101102
{
102103
branch: "feature/search",
104+
label: "Search ranking",
103105
baseBranch: "main",
104106
path: "/repo/__worktrees/feature-search",
105107
dir: "/repo/__worktrees/feature-search",
@@ -219,6 +221,7 @@ describe("buildProjectSnapshot", () => {
219221
expect(snapshot.worktrees).toEqual([
220222
{
221223
branch: "feature/new-flow",
224+
label: null,
222225
baseBranch: "main",
223226
path: "/repo/__worktrees/feature/new-flow",
224227
dir: "/repo/__worktrees/feature/new-flow",

backend/src/__tests__/worktree-conversation-service.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ function makeCodexConversationMeta(threadId: string, cwd: string, lastSeenAt = "
162162
function makeWorktree(): WorktreeSnapshot {
163163
return {
164164
branch: "codex-feature",
165+
label: null,
165166
path: "/tmp/worktrees/codex-feature",
166167
dir: "codex-feature",
167168
archived: false,

backend/src/__tests__/worktree-storage.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
readWorktreeMeta,
1414
renderEnvFile,
1515
writeWorktreePrs,
16+
writeWorktreeMeta,
1617
} from "../adapters/fs";
1718
import type { WorktreeMeta } from "../domain/model";
1819
import { createManagedWorktree, initializeManagedWorktree } from "../services/worktree-service";
@@ -398,6 +399,17 @@ describe("initializeManagedWorktree", () => {
398399
});
399400
});
400401

402+
it("normalizes blank labels out of worktree metadata", async () => {
403+
gitDir = await mkdtemp(join(tmpdir(), "webmux-meta-label-"));
404+
405+
await writeWorktreeMeta(gitDir, {
406+
...makeMeta(),
407+
label: " ",
408+
});
409+
410+
expect((await readWorktreeMeta(gitDir))?.label).toBeUndefined();
411+
});
412+
401413
it("round-trips PR storage through the worktree webmux dir", async () => {
402414
gitDir = await mkdtemp(join(tmpdir(), "webmux-prs-gitdir-"));
403415

backend/src/adapters/fs.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -218,14 +218,19 @@ function normalizeConversationMeta(raw: WorktreeConversationMeta | null | undefi
218218
return normalized;
219219
}
220220

221+
function normalizeOptionalString(raw: unknown): string | undefined {
222+
return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
223+
}
224+
221225
function normalizeWorktreeMeta(meta: WorktreeMeta): WorktreeMeta {
222-
const conversation = normalizeConversationMeta(meta.conversation);
223-
return conversation === meta.conversation
224-
? meta
225-
: {
226-
...meta,
227-
conversation,
228-
};
226+
const { label, conversation: rawConversation, ...rest } = meta;
227+
const conversation = normalizeConversationMeta(rawConversation);
228+
const normalizedLabel = normalizeOptionalString(label);
229+
return {
230+
...rest,
231+
...(normalizedLabel ? { label: normalizedLabel } : {}),
232+
...(conversation !== undefined ? { conversation } : {}),
233+
};
229234
}
230235

231236
function isPrComment(raw: unknown): raw is PrComment {

backend/src/domain/model.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface WorktreeMeta {
3030
schemaVersion: number;
3131
worktreeId: string;
3232
branch: string;
33+
label?: string;
3334
baseBranch?: string;
3435
createdAt: string;
3536
profile: string;
@@ -162,6 +163,7 @@ export interface WorktreeCreationSnapshot {
162163
export interface ManagedWorktreeRuntimeState {
163164
worktreeId: string;
164165
branch: string;
166+
label: string | null;
165167
baseBranch: string | null;
166168
path: string;
167169
profile: string | null;
@@ -184,6 +186,7 @@ export interface NotificationView {
184186

185187
export interface WorktreeSnapshot {
186188
branch: string;
189+
label: string | null;
187190
baseBranch?: string;
188191
path: string;
189192
dir: string;

0 commit comments

Comments
 (0)