Skip to content

Commit cf6f737

Browse files
committed
fix(app): stabilize runtime, polish chat UI, and harden pty packaging
- restore spawn-helper +x via electron-builder afterPack hook - preserve goal elapsed timing across dock remounts - ignore benign promise rejections in renderer global error handler - refine PixelLoader, StatusIcon, PlanProposal, and composer glow styling - guard stale thread sweep and shell start with toast feedback - tighten thread session manager pty resolution and permissions
1 parent c37cff3 commit cf6f737

30 files changed

Lines changed: 1043 additions & 325 deletions

build/after-pack.cjs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// electron-builder afterPack hook.
2+
//
3+
// node-pty ships a `spawn-helper` binary that posix_spawn invokes to set up
4+
// the pty. electron-builder's asar-unpack copy can strip the execute bit,
5+
// which makes posix_spawnp fail at runtime with the opaque
6+
// "posix_spawnp failed." error. Restore +x on every prebuild we ship.
7+
8+
const { existsSync, statSync, chmodSync, readdirSync } = require("node:fs");
9+
const { join } = require("node:path");
10+
11+
function ensureExecutable(path) {
12+
if (!existsSync(path)) return false;
13+
const stat = statSync(path);
14+
if (!stat.isFile()) return false;
15+
if ((stat.mode & 0o111) === 0o111) return false;
16+
chmodSync(path, stat.mode | 0o111);
17+
return true;
18+
}
19+
20+
function findResourcesDir(appOutDir, electronPlatformName) {
21+
if (electronPlatformName === "darwin" || electronPlatformName === "mas") {
22+
const entries = readdirSync(appOutDir).filter((name) => name.endsWith(".app"));
23+
if (entries.length === 0) return null;
24+
return join(appOutDir, entries[0], "Contents", "Resources");
25+
}
26+
if (electronPlatformName === "linux") {
27+
return join(appOutDir, "resources");
28+
}
29+
if (electronPlatformName === "win32") {
30+
return join(appOutDir, "resources");
31+
}
32+
return null;
33+
}
34+
35+
function chmodNodePtyHelpers(resourcesDir) {
36+
const prebuildsRoot = join(
37+
resourcesDir,
38+
"app.asar.unpacked",
39+
"node_modules",
40+
"node-pty",
41+
"prebuilds",
42+
);
43+
if (!existsSync(prebuildsRoot)) return [];
44+
const fixed = [];
45+
for (const platformDir of readdirSync(prebuildsRoot)) {
46+
const helper = join(prebuildsRoot, platformDir, "spawn-helper");
47+
if (ensureExecutable(helper)) fixed.push(helper);
48+
}
49+
return fixed;
50+
}
51+
52+
module.exports = async function afterPack(context) {
53+
const resourcesDir = findResourcesDir(context.appOutDir, context.electronPlatformName);
54+
if (!resourcesDir) return;
55+
const fixed = chmodNodePtyHelpers(resourcesDir);
56+
for (const path of fixed) {
57+
console.log(`[afterPack] chmod +x ${path}`);
58+
}
59+
};

scripts/build-desktop-artifact.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,8 @@ asarUnpack:
407407
- dist/main/claudeSdkProbeWorker.mjs
408408
- node_modules/@anthropic-ai/claude-agent-sdk/**/*
409409
410+
afterPack: build/after-pack.cjs
411+
410412
publish:
411413
provider: github
412414
owner: SDSLeon

src/renderer/actions/terminalActions.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { buildWorktreeLocation } from "@/shared/worktree";
2-
import { readBridge } from "@/renderer/bridge";
32
import { useAppStore } from "@/renderer/state/appStore";
43
import { useDevTerminalStore } from "@/renderer/state/devTerminalStore";
54
import { usePanelStore } from "@/renderer/state/panelStore";
65
import { useSharedSettings } from "@/renderer/state/sharedSettingsStore";
7-
import { writeScriptToShell } from "@/renderer/utils/shellUtils";
6+
import { startShellWithToast, writeScriptToShell } from "@/renderer/utils/shellUtils";
87
import { closeAllPanels } from "./panelActions";
98

109
function applyTerminalPanel(
@@ -86,10 +85,13 @@ export function runProjectAction(projectId: string, actionId: string, worktreePa
8685
}
8786
store.setActiveTab(tab.id);
8887

89-
void readBridge().startShell({
90-
shellId: tab.id,
91-
projectLocation: location,
92-
...(worktreePath ? { worktreePath } : {}),
93-
});
88+
startShellWithToast(
89+
{
90+
shellId: tab.id,
91+
projectLocation: location,
92+
...(worktreePath ? { worktreePath } : {}),
93+
},
94+
tabLabel,
95+
);
9496
writeScriptToShell(tab.id, action.command);
9597
}

src/renderer/actions/threadActions.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,16 @@ export function sweepStaleThreads(): void {
166166
if (
167167
visibleThreadIds.has(thread.id) ||
168168
(thread.status !== "idle" && thread.status !== "finished") ||
169-
!thread.sessionRef ||
170-
new Date(thread.updatedAt).getTime() > staleBefore
169+
!thread.sessionRef
171170
) {
172171
continue;
173172
}
173+
const updatedAtMs = new Date(thread.updatedAt).getTime();
174+
const lastViewedAtMs = store.lastViewedAtByThreadId[thread.id] ?? 0;
175+
const lastActiveMs = Math.max(updatedAtMs, lastViewedAtMs);
176+
if (lastActiveMs > staleBefore) {
177+
continue;
178+
}
174179

175180
void unloadStoredThread(thread.id).catch(() => undefined);
176181
}

src/renderer/commands/registry.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useDevTerminalStore } from "@/renderer/state/devTerminalStore";
1717
import { useFileEditorStore } from "@/renderer/state/fileEditorStore";
1818
import { usePanelStore } from "@/renderer/state/panelStore";
1919
import { useSharedSettings } from "@/renderer/state/sharedSettingsStore";
20-
import { writeScriptToShell } from "@/renderer/utils/shellUtils";
20+
import { startShellWithToast, writeScriptToShell } from "@/renderer/utils/shellUtils";
2121
import { useCommandPaletteStore } from "./commandPaletteStore";
2222
import type { CommandWhenContext } from "./when";
2323
import { evaluateWhenClause } from "./when";
@@ -321,11 +321,14 @@ function runTerminalCommand(args: unknown): void {
321321
}
322322
terminal.setActiveTab(tab.id);
323323

324-
void readBridge().startShell({
325-
shellId: tab.id,
326-
projectLocation: location,
327-
...(worktreePath ? { worktreePath } : {}),
328-
});
324+
startShellWithToast(
325+
{
326+
shellId: tab.id,
327+
projectLocation: location,
328+
...(worktreePath ? { worktreePath } : {}),
329+
},
330+
title,
331+
);
329332
writeScriptToShell(tab.id, command);
330333
}
331334

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { SVGProps } from "react";
2+
3+
export function AnimatingPlanIcon({ className, ...props }: SVGProps<SVGSVGElement>) {
4+
return (
5+
<svg
6+
xmlns="http://www.w3.org/2000/svg"
7+
viewBox="0 0 24 24"
8+
fill="none"
9+
stroke="currentColor"
10+
strokeWidth="2"
11+
strokeLinecap="round"
12+
strokeLinejoin="round"
13+
className={className}
14+
{...props}
15+
>
16+
<path d="m3 7 2 2 4-4" className="lightcode-plan-check-1" />
17+
<path d="m3 17 2 2 4-4" className="lightcode-plan-check-2" />
18+
<path d="M13 6h8" />
19+
<path d="M13 12h8" />
20+
<path d="M13 18h8" />
21+
</svg>
22+
);
23+
}

0 commit comments

Comments
 (0)