Skip to content

Commit c8bb17c

Browse files
mariusvniekerkcodexwesm
authored
feat: add configurable worktree project mappings (#481)
Closes #416. Adds user-configurable rules that map worktree paths back to a canonical project, so worktree sessions group with the parent repo instead of becoming a separate project derived from the branch name. The built-in parser infers a session's project from its `cwd`, which works for standard layouts but not custom worktree conventions like `~/code/{project}.worktrees/feat/my-feature/` — those sessions end up under `my-feature` rather than `{project}`. This branch adds a manual override that lives in SQLite, is scoped to the current machine, and is applied both as new sessions stream in and to existing sessions on demand. ## How it works - New `worktree_project_mappings` table keyed by `(machine, path_prefix)` with an `enabled` flag. Path prefixes are cleaned and have trailing separators stripped so matches are stable regardless of how a user types the prefix. - The sync engine loads the active mappings for the host machine on each pass and rewrites `project` for any session whose `cwd` falls under an enabled prefix. The resolver is wired into all ingestion paths: single-session writes, incremental sync, bulk resync, and the rebuild-and-swap full resync. Excluded, trashed, and skipped session files are left alone so the rewriter does not resurrect intentionally removed data. - REST API under `/api/v1/settings/worktree-mappings` (list / create / update / delete / apply). The server derives the machine identifier itself instead of accepting it from the client, so a mapping created on one machine never bleeds into another machine's view of synced sessions. - Settings panel in the SPA for adding, editing, toggling, and deleting mappings, plus an Apply action that runs the rewrite over already-imported sessions and reports how many were matched and updated. ## Notes - Mappings are explicit and manual — there is no auto-discovery. The intent is to let users formalize their personal layout once and have it stick. - Mappings only mutate `project`; the rest of the session record, including data carried through the bulk resync's rebuild-and-copy path, is preserved. --------- Co-authored-by: OpenAI Codex <noreply@openai.com> Co-authored-by: Wes McKinney <wesmckinn+git@gmail.com>
1 parent 9beed32 commit c8bb17c

14 files changed

Lines changed: 2956 additions & 19 deletions

frontend/src/lib/api/client.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,75 @@ export function updateSettings(
813813
});
814814
}
815815

816+
export interface WorktreeProjectMapping {
817+
id: number;
818+
machine: string;
819+
path_prefix: string;
820+
project: string;
821+
enabled: boolean;
822+
created_at: string;
823+
updated_at: string;
824+
}
825+
826+
export interface WorktreeMappingsResponse {
827+
machine: string;
828+
mappings: WorktreeProjectMapping[];
829+
}
830+
831+
export interface WorktreeMappingInput {
832+
path_prefix: string;
833+
project: string;
834+
enabled: boolean;
835+
}
836+
837+
export interface ApplyWorktreeMappingsResponse {
838+
machine: string;
839+
matched_sessions: number;
840+
updated_sessions: number;
841+
}
842+
843+
export function getWorktreeMappings(): Promise<WorktreeMappingsResponse> {
844+
return fetchJSON("/settings/worktree-mappings");
845+
}
846+
847+
export function createWorktreeMapping(
848+
input: WorktreeMappingInput,
849+
): Promise<WorktreeProjectMapping> {
850+
return fetchJSON("/settings/worktree-mappings", {
851+
method: "POST",
852+
headers: { "Content-Type": "application/json" },
853+
body: JSON.stringify(input),
854+
});
855+
}
856+
857+
export function updateWorktreeMapping(
858+
id: number,
859+
input: WorktreeMappingInput,
860+
): Promise<WorktreeProjectMapping> {
861+
return fetchJSON(`/settings/worktree-mappings/${id}`, {
862+
method: "PUT",
863+
headers: { "Content-Type": "application/json" },
864+
body: JSON.stringify(input),
865+
});
866+
}
867+
868+
export async function deleteWorktreeMapping(id: number): Promise<void> {
869+
const res = await fetch(
870+
`${getBase()}/settings/worktree-mappings/${id}`,
871+
authHeaders({ method: "DELETE" }),
872+
);
873+
if (!res.ok) {
874+
const body = await res.text();
875+
throw new ApiError(res.status, apiErrorMessage(res.status, body));
876+
}
877+
}
878+
879+
export function applyWorktreeMappings(): Promise<ApplyWorktreeMappingsResponse> {
880+
return fetchJSON("/settings/worktree-mappings/apply", {
881+
method: "POST",
882+
});
883+
}
884+
816885
/* Analytics */
817886

818887
export interface AnalyticsParams {

frontend/src/lib/components/settings/SettingsPage.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import TerminalSettings from "./TerminalSettings.svelte";
99
import GithubSettings from "./GithubSettings.svelte";
1010
import RemoteSettings from "./RemoteSettings.svelte";
11+
import WorktreeMappingSettings from "./WorktreeMappingSettings.svelte";
1112
1213
let authTokenInput: string = $state("");
1314
@@ -87,6 +88,7 @@
8788
<AppearanceSettings />
8889
<AgentDirSettings />
8990
<TerminalSettings />
91+
<WorktreeMappingSettings />
9092
<GithubSettings />
9193
<RemoteSettings />
9294

0 commit comments

Comments
 (0)