Skip to content

Commit d504d70

Browse files
committed
ui fixes [skip ci]
1 parent d78e8ed commit d504d70

11 files changed

Lines changed: 129 additions & 97 deletions

File tree

apps/desktop/src/main/services/git/git.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,61 @@
11
import fs from "node:fs";
22
import path from "node:path";
3-
import { spawn } from "node:child_process";
3+
import { execFileSync, spawn } from "node:child_process";
44
import type { ConflictFileType } from "../../../shared/types";
55
import { terminateProcessTree } from "../shared/processExecution";
6+
import { resolveExecutableFromKnownLocations } from "../ai/cliExecutableResolver";
7+
8+
// Electron apps launched from Finder/Dock can have a stripped PATH that misses
9+
// where the user actually installed git (e.g. /opt/homebrew/bin on Apple
10+
// Silicon when shell PATH probe times out). Resolve git's absolute path once
11+
// and reuse it so spawn never throws ENOENT.
12+
let cachedGitExecutable: string | null = null;
13+
function resolveGitExecutable(): string {
14+
if (cachedGitExecutable) return cachedGitExecutable;
15+
if (process.env.ADE_GIT_EXECUTABLE && fs.existsSync(process.env.ADE_GIT_EXECUTABLE)) {
16+
cachedGitExecutable = process.env.ADE_GIT_EXECUTABLE;
17+
return cachedGitExecutable;
18+
}
19+
const fromKnown = resolveExecutableFromKnownLocations(process.platform === "win32" ? "git.exe" : "git");
20+
if (fromKnown?.path) {
21+
cachedGitExecutable = fromKnown.path;
22+
return cachedGitExecutable;
23+
}
24+
// Last resort: ask the user's login shell. Slower, but only runs if the
25+
// direct probe missed (e.g. git lives in an unusual nvm/asdf shim dir).
26+
if (process.platform !== "win32") {
27+
try {
28+
const shell = process.env.SHELL?.trim() || "/bin/sh";
29+
const out = execFileSync(shell, ["-lc", "command -v git"], {
30+
encoding: "utf8",
31+
timeout: 3_000,
32+
}).trim();
33+
if (out && fs.existsSync(out)) {
34+
cachedGitExecutable = out;
35+
return cachedGitExecutable;
36+
}
37+
} catch {
38+
// fall through
39+
}
40+
}
41+
// Fall back to bare "git" — spawn will surface the original ENOENT with a
42+
// clearer pre-check error message wrapped around it.
43+
cachedGitExecutable = process.platform === "win32" ? "git.exe" : "git";
44+
return cachedGitExecutable;
45+
}
46+
47+
function gitExecutableNotFoundMessage(executable: string): string {
48+
if (process.platform === "darwin") {
49+
return `git executable not found (tried ${executable}). Install Xcode Command Line Tools or set ADE_GIT_EXECUTABLE to git's absolute path.`;
50+
}
51+
if (process.platform === "win32") {
52+
return `git executable not found (tried ${executable}). Install Git for Windows or set ADE_GIT_EXECUTABLE to git's absolute path.`;
53+
}
54+
if (process.platform === "linux") {
55+
return `git executable not found (tried ${executable}). Install git with your package manager or set ADE_GIT_EXECUTABLE to git's absolute path.`;
56+
}
57+
return `git executable not found (tried ${executable}). Install git or set ADE_GIT_EXECUTABLE to git's absolute path.`;
58+
}
659

760
export type GitRunOptions = {
861
cwd: string;
@@ -109,7 +162,7 @@ async function runGitOnce(args: string[], opts: GitRunOptions): Promise<GitRunRe
109162
: DEFAULT_MAX_OUTPUT_BYTES;
110163

111164
return await new Promise<GitRunResult>((resolve) => {
112-
const child = spawn("git", args, {
165+
const child = spawn(resolveGitExecutable(), args, {
113166
cwd: opts.cwd,
114167
env: { ...process.env, ...(opts.env ?? {}) },
115168
stdio: ["ignore", "pipe", "pipe"]
@@ -176,10 +229,15 @@ async function runGitOnce(args: string[], opts: GitRunOptions): Promise<GitRunRe
176229
});
177230

178231
child.on("error", (error) => {
232+
const code = (error as NodeJS.ErrnoException)?.code;
233+
const friendlyMessage =
234+
code === "ENOENT"
235+
? gitExecutableNotFoundMessage(resolveGitExecutable())
236+
: error.message;
179237
finish({
180238
exitCode: 1,
181239
stdout,
182-
stderr: stderr.length ? stderr : error.message,
240+
stderr: stderr.length ? stderr : friendlyMessage,
183241
stdoutTruncated,
184242
stderrTruncated
185243
});

apps/desktop/src/main/services/lanes/laneService.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2404,15 +2404,15 @@ describe("laneService delete teardown + cancellation + streaming", () => {
24042404
vi.mocked(runGit).mockImplementation(async (args: string[]) => {
24052405
if (args[0] === "status") return { exitCode: 0, stdout: "", stderr: "" } as any;
24062406
if (args[0] === "show-ref") return { exitCode: 1, stdout: "", stderr: "" } as any;
2407-
return { exitCode: 0, stdout: "", stderr: "" } as any;
2408-
});
2409-
vi.mocked(runGitOrThrow).mockImplementation(async (args: string[]) => {
24102407
if (args[0] === "worktree" && args[1] === "remove") {
24112408
fake.calls.push("git_worktree_remove");
24122409
return { exitCode: 0, stdout: "", stderr: "" } as any;
24132410
}
24142411
return { exitCode: 0, stdout: "", stderr: "" } as any;
24152412
});
2413+
vi.mocked(runGitOrThrow).mockImplementation(async () => {
2414+
return { exitCode: 0, stdout: "", stderr: "" } as any;
2415+
});
24162416

24172417
await service.delete({ laneId: "lane-child", deleteBranch: false });
24182418

apps/desktop/src/main/services/lanes/laneService.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3011,7 +3011,14 @@ export function createLaneService({
30113011
throw new Error("Cannot delete a lane with active child lanes. Delete or rebase/archive children first.");
30123012
}
30133013

3014-
const hasWorktree = row.lane_type === "worktree" && Boolean(row.worktree_path) && fs.existsSync(row.worktree_path);
3014+
const worktreeMetadataPath = row.worktree_path
3015+
? path.join(projectRoot, ".git", "worktrees", path.basename(row.worktree_path))
3016+
: "";
3017+
const worktreeRegistered = Boolean(worktreeMetadataPath) && fs.existsSync(worktreeMetadataPath);
3018+
const hasWorktree =
3019+
row.lane_type === "worktree" &&
3020+
Boolean(row.worktree_path) &&
3021+
(fs.existsSync(row.worktree_path) || worktreeRegistered);
30153022
const stepNames: LaneDeleteStepName[] = [];
30163023
if (hasWorktree) stepNames.push("git_status");
30173024
stepNames.push("cancel_auto_rebase", "stop_processes", "stop_ptys", "stop_watchers", "cleanup_env");
@@ -3166,8 +3173,29 @@ export function createLaneService({
31663173
const removeArgs = ["worktree", "remove"];
31673174
if (force) removeArgs.push("--force");
31683175
removeArgs.push(row.worktree_path);
3169-
await runGitOrThrow(removeArgs, { cwd: projectRoot, timeoutMs: 15_000 });
3170-
return { detail: row.worktree_path };
3176+
// 60s — large worktrees (e.g. with node_modules) can take longer than 15s
3177+
// to walk; a timeout here mid-remove leaves the worktree in a half-deleted
3178+
// state that blocks future deletes.
3179+
const removeRes = await runGit(removeArgs, { cwd: projectRoot, timeoutMs: 60_000 });
3180+
if (removeRes.exitCode === 0) {
3181+
return { detail: row.worktree_path };
3182+
}
3183+
// Recovery path: a previous failed delete (or this one's first attempt)
3184+
// can leave the worktree dir present without its `.git` pointer file, or
3185+
// the dir gone with stale metadata still registered. Either way: rm the
3186+
// dir if any, then prune git's metadata.
3187+
try {
3188+
fs.rmSync(row.worktree_path, { recursive: true, force: true });
3189+
} catch (rmError) {
3190+
const original = (removeRes.stderr || removeRes.stdout || "").trim();
3191+
throw new Error(
3192+
`git worktree remove failed (${original}); manual cleanup also failed: ${
3193+
rmError instanceof Error ? rmError.message : String(rmError)
3194+
}`
3195+
);
3196+
}
3197+
await runGitOrThrow(["worktree", "prune"], { cwd: projectRoot, timeoutMs: 30_000 });
3198+
return { detail: `${row.worktree_path} (recovered from stale state)` };
31713199
});
31723200
}
31733201

@@ -3192,7 +3220,7 @@ export function createLaneService({
31923220
}
31933221
const remoteRefCheck = await runGit(["ls-remote", "--heads", remote, row.branch_ref], {
31943222
cwd: projectRoot,
3195-
timeoutMs: 12_000,
3223+
timeoutMs: 30_000,
31963224
});
31973225
if (remoteRefCheck.exitCode !== 0 || remoteRefCheck.stdout.trim().length === 0) {
31983226
return { detail: "remote branch not found" };

apps/desktop/src/renderer/components/app/App.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,13 @@ export function RequireProject({ children }: { children: React.ReactElement }):
165165
}
166166

167167
const hasActiveProject = Boolean(project?.rootPath);
168-
if ((!hasActiveProject || showWelcome) && location.pathname !== "/project" && location.pathname !== "/onboarding") {
169-
return <Navigate to="/project" replace />;
168+
if (
169+
(!hasActiveProject || showWelcome) &&
170+
location.pathname !== "/work" &&
171+
location.pathname !== "/project" &&
172+
location.pathname !== "/onboarding"
173+
) {
174+
return <Navigate to="/work" replace />;
170175
}
171176

172177
return children;
@@ -241,7 +246,11 @@ function PersistentWorkSurface({ active }: { active: boolean }) {
241246
}
242247

243248
if (!hasActiveProject || showWelcome) {
244-
return active ? <Navigate to="/project" replace /> : null;
249+
return active ? (
250+
<PageErrorBoundary>
251+
<RunPage />
252+
</PageErrorBoundary>
253+
) : null;
245254
}
246255

247256
return (

apps/desktop/src/renderer/components/app/TabNav.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,9 @@ export function TabNav({ githubStatus }: { githubStatus?: GitHubStatus | null })
9090
const renderItem = (
9191
it: { to: string; label: string; icon: React.ElementType },
9292
) => {
93-
const isActive = primaryTabPath(location.pathname) === it.to;
94-
const isActiveAllowed = (!showWelcome && hasActiveProject) || it.to === "/project";
93+
const onWelcomeLanding = showWelcome || !hasActiveProject;
94+
const isActive = !onWelcomeLanding && primaryTabPath(location.pathname) === it.to;
95+
const isActiveAllowed = !showWelcome && hasActiveProject;
9596
const navTarget = it.to === "/prs" ? readStoredPrsRoute(project?.rootPath) ?? it.to : it.to;
9697

9798
if (!isActiveAllowed) {
@@ -136,7 +137,7 @@ export function TabNav({ githubStatus }: { githubStatus?: GitHubStatus | null })
136137
{/* Active indicator bar */}
137138
{isActive && (
138139
<div
139-
className="absolute inset-0 rounded-lg bg-white/[0.06]"
140+
className="absolute inset-0 bg-white/[0.08]"
140141
/>
141142
)}
142143

apps/desktop/src/renderer/components/chat/ChatSurfaceShell.tsx

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,18 +114,6 @@ export function ChatSurfaceShell({
114114
background: "var(--color-bg)",
115115
}}
116116
>
117-
<div
118-
aria-hidden
119-
className="pointer-events-none absolute inset-y-0 left-0 z-[5]"
120-
style={{
121-
width: "var(--chat-lane-rail-width, 0px)",
122-
opacity: "var(--chat-lane-rail-opacity, 0)",
123-
borderTopLeftRadius: "var(--chat-radius-shell)",
124-
borderBottomLeftRadius: "var(--chat-radius-shell)",
125-
background:
126-
"linear-gradient(180deg, color-mix(in srgb, var(--chat-accent) 72%, transparent) 0%, color-mix(in srgb, var(--chat-accent) 32%, transparent) 100%)",
127-
}}
128-
/>
129117
{scaled ? (
130118
<div className="flex min-h-0 flex-1 flex-col overflow-hidden" style={scaleWrapperStyle}>
131119
{inner}

apps/desktop/src/renderer/components/chat/chatSurfaceTheme.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,21 +114,17 @@ function coloredChatSurfaceVars(mode: ChatSurfaceMode, accentColor?: string | nu
114114
const accent = resolveChatSurfaceAccent(mode, accentColor);
115115
const m = 1;
116116
return {
117-
["--chat-lane-rail-width" as string]: "3px",
118-
["--chat-lane-rail-opacity" as string]: "0.52",
119117
["--chat-user-border-accent-mix" as string]: "28%",
120118
["--chat-user-shadow-accent-mix" as string]: "34%",
121119
...sharedSurfaceTokens(accent, m),
122120
};
123121
}
124122

125-
/** Monochrome chat — no side rail, gray accent token, reduced glow (Slack-style base). */
123+
/** Monochrome chat — gray accent token, reduced glow (Slack-style base). */
126124
function neutralChatSurfaceVars(): CSSProperties {
127125
const accent = NEUTRAL_CHROME_ACCENT;
128126
const m = 0.85;
129127
return {
130-
["--chat-lane-rail-width" as string]: "0px",
131-
["--chat-lane-rail-opacity" as string]: "0",
132128
["--chat-user-border-accent-mix" as string]: "20%",
133129
["--chat-user-shadow-accent-mix" as string]: "24%",
134130
...sharedSurfaceTokens(accent, m),

apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ function SectionCard({
276276
dataTestId,
277277
showDescription = false,
278278
sectionStyle,
279+
headerStyle,
279280
bodyStyle,
280281
}: {
281282
title: string;
@@ -285,6 +286,7 @@ function SectionCard({
285286
dataTestId?: string;
286287
showDescription?: boolean;
287288
sectionStyle?: React.CSSProperties;
289+
headerStyle?: React.CSSProperties;
288290
bodyStyle?: React.CSSProperties;
289291
}) {
290292
return (
@@ -310,6 +312,7 @@ function SectionCard({
310312
padding: "8px 10px",
311313
borderBottom: `1px solid ${COLORS.border}`,
312314
background: COLORS.recessedBg,
315+
...headerStyle,
313316
}}
314317
>
315318
<div style={{ minWidth: 0 }}>
@@ -2166,11 +2169,12 @@ export function LaneGitActionsPane({
21662169
: "Changed files. Stashes are saved snapshots below."
21672170
}
21682171
dataTestId="files-section"
2169-
sectionStyle={{ minHeight: 0, height: "100%" }}
2172+
sectionStyle={{ minHeight: 0, height: "100%", background: "rgba(255,255,255,0.03)" }}
2173+
headerStyle={{ background: "rgba(255,255,255,0.03)" }}
21702174
bodyStyle={
21712175
diffViewActive
2172-
? { flex: 1, minHeight: 0, padding: 0, gap: 0, display: "flex", flexDirection: "column", overflow: "hidden" }
2173-
: { flex: 1, minHeight: 0 }
2176+
? { flex: 1, minHeight: 0, padding: 0, gap: 0, display: "flex", flexDirection: "column", overflow: "hidden", background: "rgba(255,255,255,0.03)" }
2177+
: { flex: 1, minHeight: 0, background: "rgba(255,255,255,0.03)" }
21742178
}
21752179
aside={
21762180
diffViewActive ? (

apps/desktop/src/renderer/components/lanes/laneDesignTokens.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import type { ProcessRuntimeStatus } from "../../../shared/types";
44
/** Semantic palette — resolves against `[data-theme]` / `:root` in `index.css`. */
55
export const COLORS = {
66
pageBg: "var(--color-bg)",
7-
cardBg: "color-mix(in srgb, var(--color-card) 94%, var(--color-bg) 6%)",
8-
cardBgSolid: "var(--color-card)",
9-
recessedBg: "var(--color-muted)",
7+
cardBg: "rgba(255,255,255,0.03)",
8+
cardBgSolid: "#181423",
9+
recessedBg: "rgba(255,255,255,0.02)",
1010
hoverBg: "color-mix(in srgb, var(--color-fg) 6%, transparent)",
1111
border: "var(--color-border)",
1212
outlineBorder: "color-mix(in srgb, var(--color-border) 88%, var(--color-fg) 12%)",

apps/desktop/src/renderer/components/settings/ChatAppearancePreview.test.tsx

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ describe("ChatAppearancePreview", () => {
7373
expect(first.style.getPropertyValue("--chat-bubble-assistant-py").trim()).toBe("22px");
7474
});
7575

76-
it("renders three chat surface shells with colored chrome rail and standard border mix", () => {
76+
it("renders three chat surface shells with colored chrome and standard border mix", () => {
7777
const { container } = render(
7878
<ChatAppearancePreview
7979
theme="dark"
@@ -90,27 +90,9 @@ describe("ChatAppearancePreview", () => {
9090
for (const el of shells) {
9191
const style = (el as HTMLElement).style;
9292
expect(style.getPropertyValue("--chat-user-border-accent-mix").trim()).toBe("28%");
93-
expect(style.getPropertyValue("--chat-lane-rail-width").trim()).toBe("3px");
9493
}
9594
});
9695

97-
it("hides the colored lane rail in no-tint (neutral) mode", () => {
98-
const { container } = render(
99-
<ChatAppearancePreview
100-
theme="dark"
101-
chatFontSizePx={14}
102-
transcriptDensity="comfortable"
103-
chromeTint="neutral"
104-
shellGeometry="default"
105-
/>,
106-
);
107-
const first = container.querySelector("section[data-chat-chrome-tint='neutral']") as HTMLElement;
108-
expect(first).toBeTruthy();
109-
const style = first.style;
110-
expect(style.getPropertyValue("--chat-lane-rail-width").trim()).toBe("0px");
111-
expect(style.getPropertyValue("--chat-lane-rail-opacity").trim()).toBe("0");
112-
});
113-
11496
it("scopes theme to the preview subtree", () => {
11597
const { container } = render(
11698
<ChatAppearancePreview

0 commit comments

Comments
 (0)