Add open-in-editor feature with Cmd+O shortcut#2
Conversation
Add dropdown in header to open workspace in Cursor or system file manager (Finder/Explorer/Files). Include Cmd+O/Ctrl+O global keyboard shortcut to open in last-used editor, persisted to localStorage. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
WalkthroughAdds an "open in editor" feature: new Changes
Sequence DiagramsequenceDiagram
actor User
participant ChatView as Renderer (ChatView.tsx)
participant Preload as Preload (preload.ts)
participant Main as Main Process (main.ts)
participant OS as Operating System Shell
User->>ChatView: Click "Open in…" or press Cmd/Ctrl+O
ChatView->>ChatView: openInEditor(editorId) and persist lastEditor
ChatView->>Preload: invoke 'shell:open-in-editor' (cwd, editorId)
Preload->>Main: Forward IPC invoke
Main->>Main: Lookup editor in EDITORS
alt editor == 'file-manager'
Main->>OS: shell.openPath(cwd)
else editor has command
Main->>OS: spawn(editorCommand, [cwd], {detached, stdio:'ignore'}) and unref()
end
OS-->>Main: process/path opened
Main-->>Preload: resolve Promise<void>
Preload-->>ChatView: invoke resolves
ChatView->>ChatView: close editor menu
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
Comment |
Add Cmd+O/Ctrl+O open-in-editor action in chat view and handle
|
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@apps/desktop/src/main.ts`:
- Around line 143-145: The CI formatter flagged the EDITOR_COMMANDS block — run
the project formatter (e.g., run the formatter or execute `bun run lint`) to
reformat the object literal; specifically ensure the EDITOR_COMMANDS constant
and its entry for "cursor" follow the project's formatting rules (spacing,
trailing commas, and indentation) so the object shape Record<string, { command:
string; args: (cwd: string) => string[] }> and the cursor entry { command:
"cursor", args: (p) => [p] } are formatted exactly as the linter expects.
- Around line 135-153: The IPC handler for ipcMain.handle should validate cwd is
non-empty, await shell.openPath(cwd) and check its returned string for an error
(log/throw when non-empty) instead of assuming success, and protect the spawn
path by wrapping the spawn call in try/catch and attaching an 'error' listener
to the returned child (e.g., child.on('error', ...) to log/throw when the CLI
cannot be spawned); reference EDITOR_COMMANDS and entry (from editor) to locate
the spawn logic, and ensure you handle and surface errors for both
shell.openPath and spawn so the handler doesn't crash or fail silently.
In `@apps/renderer/src/components/ChatView.tsx`:
- Around line 66-69: Validate the value read from localStorage under
LAST_EDITOR_KEY against the known EDITORS list before using it as the initial
state for lastEditor; replace the current initializer (() =>
localStorage.getItem(LAST_EDITOR_KEY) ?? EDITORS[0].id) with logic that reads
localStorage, checks EDITORS.some(e => e.id === stored), and falls back to
EDITORS[0].id when the stored id is missing or not found; also ensure any places
that write to lastEditor (setLastEditor) persist only validated ids to
localStorage.
- Around line 183-191: The keydown handler currently calls e.preventDefault()
unconditionally for Cmd/Ctrl+O even when the action won't run; update the
handler in the useEffect (function handler used for the KeyboardEvent) to only
call e.preventDefault() when api and activeProject are truthy and you are about
to call api.shell.openInEditor(activeProject.cwd, lastEditor), i.e. move or
guard preventDefault() behind the same condition that checks api and
activeProject so the key event is not swallowed when no activeProject exists.
| // Shell handlers | ||
| ipcMain.handle( | ||
| IPC_CHANNELS.shellOpenInEditor, | ||
| async (_event, cwd: string, editor: string) => { | ||
| if (editor === "file-manager") { | ||
| await shell.openPath(cwd); | ||
| return; | ||
| } | ||
| const EDITOR_COMMANDS: Record<string, { command: string; args: (cwd: string) => string[] }> = { | ||
| cursor: { command: "cursor", args: (p) => [p] }, | ||
| }; | ||
| const entry = EDITOR_COMMANDS[editor]; | ||
| if (!entry) throw new Error(`Unknown editor: ${editor}`); | ||
| const child = spawn(entry.command, entry.args(cwd), { | ||
| detached: true, | ||
| stdio: "ignore", | ||
| }); | ||
| child.unref(); | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/desktop/src/main.ts | sed -n '135,153p'Repository: pingdotgg/codething-mvp
Length of output: 848
🌐 Web query:
Electron shell.openPath() return value error handling
💡 Result:
shell.openPath(path) returns a Promise<string> that resolves (not rejects) to:
""(empty string) on success- a non-empty error message string on failure (electronjs.org)
So error handling is typically:
import { shell } from "electron";
async function openFile(p: string) {
try {
const err = await shell.openPath(p);
if (err) {
// open failed; Electron returns an error message string
console.error("openPath failed:", err);
// show UI / log / fallback here
}
} catch (e) {
// for unexpected exceptions (e.g., bad argument types, other runtime issues)
console.error("openPath threw:", e);
}
}Key point: check the resolved string—don’t rely only on catch. (electronjs.org)
Citations:
- 1: https://www.electronjs.org/docs/latest/api/shell?utm_source=openai
- 2: https://www.electronjs.org/docs/latest/api/shell?utm_source=openai
🌐 Web query:
Node.js child_process spawn error event handling
💡 Result:
In Node.js, child_process.spawn() can fail asynchronously, so you should always attach an 'error' handler to the returned ChildProcess:
'error'event is emitted when the process could not be spawned (e.g.,ENOENT,EACCES), could not be killed, or IPC send failed. The'exit'event may or may not fire after an'error', so don’t rely on'exit'alone. [1]'close'event fires after the process ends and all stdio streams are closed; it will emit after'exit', or after'error'if the child failed to spawn. [2]
A common safe pattern is to funnel both 'error' and 'close' into a single “done once” handler:
import { spawn } from 'node:child_process';
function spawnChecked(cmd, args, opts) {
const child = spawn(cmd, args, opts);
let settled = false;
const done = (err, result) => {
if (settled) return;
settled = true;
if (err) throw err; // or reject a Promise / call cb(err)
return result;
};
child.once('error', (err) => done(err)); // spawn/kill/IPC failure
child.once('close', (code, signal) => { // stdio fully closed
if (code === 0) return done(null, { code, signal });
done(Object.assign(new Error(`exit ${code ?? 'null'} (${signal ?? 'no-signal'})`), { code, signal }));
});
return child;
}Sources: Node.js ChildProcess event docs for 'error' and 'close'. [1] [2]
References
[1] Node.js docs — ChildProcess event: 'error' (nodejs.org)
[2] Node.js v22 docs — ChildProcess event: 'close' (nodejs.org)
Citations:
- 1: https://nodejs.org/download/release/v7.7.0/docs/api/child_process.html?utm_source=openai
- 2: https://nodejs.org/download/release/v22.19.0/docs/api/child_process.html?utm_source=openai
Add error handling for shell.openPath() and spawn() to prevent crashes and silent failures.
shell.openPath() returns a Promise that resolves to an error string (not thrown), and spawn() emits an 'error' event asynchronously when the process cannot be spawned (e.g., missing CLI). Without handling these, the IPC handler silently fails or crashes. Also guard against empty cwd.
🛠️ Suggested fix
- async (_event, cwd: string, editor: string) => {
- if (editor === "file-manager") {
- await shell.openPath(cwd);
- return;
- }
+ async (_event, cwd: string, editor: string) => {
+ if (!cwd) {
+ throw new Error("cwd is required");
+ }
+ if (editor === "file-manager") {
+ const error = await shell.openPath(cwd);
+ if (error) {
+ throw new Error(error);
+ }
+ return;
+ }
const EDITOR_COMMANDS: Record<string, { command: string; args: (cwd: string) => string[] }> = {
cursor: { command: "cursor", args: (p) => [p] },
};
const entry = EDITOR_COMMANDS[editor];
if (!entry) throw new Error(`Unknown editor: ${editor}`);
- const child = spawn(entry.command, entry.args(cwd), {
- detached: true,
- stdio: "ignore",
- });
- child.unref();
+ await new Promise<void>((resolve, reject) => {
+ const child = spawn(entry.command, entry.args(cwd), {
+ detached: true,
+ stdio: "ignore",
+ });
+ child.once("error", reject);
+ child.once("spawn", () => {
+ child.unref();
+ resolve();
+ });
+ });
},
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Shell handlers | |
| ipcMain.handle( | |
| IPC_CHANNELS.shellOpenInEditor, | |
| async (_event, cwd: string, editor: string) => { | |
| if (editor === "file-manager") { | |
| await shell.openPath(cwd); | |
| return; | |
| } | |
| const EDITOR_COMMANDS: Record<string, { command: string; args: (cwd: string) => string[] }> = { | |
| cursor: { command: "cursor", args: (p) => [p] }, | |
| }; | |
| const entry = EDITOR_COMMANDS[editor]; | |
| if (!entry) throw new Error(`Unknown editor: ${editor}`); | |
| const child = spawn(entry.command, entry.args(cwd), { | |
| detached: true, | |
| stdio: "ignore", | |
| }); | |
| child.unref(); | |
| }, | |
| // Shell handlers | |
| ipcMain.handle( | |
| IPC_CHANNELS.shellOpenInEditor, | |
| async (_event, cwd: string, editor: string) => { | |
| if (!cwd) { | |
| throw new Error("cwd is required"); | |
| } | |
| if (editor === "file-manager") { | |
| const error = await shell.openPath(cwd); | |
| if (error) { | |
| throw new Error(error); | |
| } | |
| return; | |
| } | |
| const EDITOR_COMMANDS: Record<string, { command: string; args: (cwd: string) => string[] }> = { | |
| cursor: { command: "cursor", args: (p) => [p] }, | |
| }; | |
| const entry = EDITOR_COMMANDS[editor]; | |
| if (!entry) throw new Error(`Unknown editor: ${editor}`); | |
| await new Promise<void>((resolve, reject) => { | |
| const child = spawn(entry.command, entry.args(cwd), { | |
| detached: true, | |
| stdio: "ignore", | |
| }); | |
| child.once("error", reject); | |
| child.once("spawn", () => { | |
| child.unref(); | |
| resolve(); | |
| }); | |
| }); | |
| }, | |
| ); |
🤖 Prompt for AI Agents
In `@apps/desktop/src/main.ts` around lines 135 - 153, The IPC handler for
ipcMain.handle should validate cwd is non-empty, await shell.openPath(cwd) and
check its returned string for an error (log/throw when non-empty) instead of
assuming success, and protect the spawn path by wrapping the spawn call in
try/catch and attaching an 'error' listener to the returned child (e.g.,
child.on('error', ...) to log/throw when the CLI cannot be spawned); reference
EDITOR_COMMANDS and entry (from editor) to locate the spawn logic, and ensure
you handle and surface errors for both shell.openPath and spawn so the handler
doesn't crash or fail silently.
| const [isEditorMenuOpen, setIsEditorMenuOpen] = useState(false); | ||
| const [lastEditor, setLastEditor] = useState( | ||
| () => localStorage.getItem(LAST_EDITOR_KEY) ?? EDITORS[0].id, | ||
| ); |
There was a problem hiding this comment.
Guard against stale lastEditor values in localStorage.
If localStorage contains an unknown editor id (older versions, manual edits), the IPC handler will throw. Prefer validating against EDITORS and falling back.
🧩 Suggested fix
- const [lastEditor, setLastEditor] = useState(
- () => localStorage.getItem(LAST_EDITOR_KEY) ?? EDITORS[0].id,
- );
+ const [lastEditor, setLastEditor] = useState(() => {
+ const stored = localStorage.getItem(LAST_EDITOR_KEY);
+ return EDITORS.some((editor) => editor.id === stored)
+ ? stored
+ : EDITORS[0].id;
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [isEditorMenuOpen, setIsEditorMenuOpen] = useState(false); | |
| const [lastEditor, setLastEditor] = useState( | |
| () => localStorage.getItem(LAST_EDITOR_KEY) ?? EDITORS[0].id, | |
| ); | |
| const [isEditorMenuOpen, setIsEditorMenuOpen] = useState(false); | |
| const [lastEditor, setLastEditor] = useState(() => { | |
| const stored = localStorage.getItem(LAST_EDITOR_KEY); | |
| return EDITORS.some((editor) => editor.id === stored) | |
| ? stored | |
| : EDITORS[0].id; | |
| }); |
🤖 Prompt for AI Agents
In `@apps/renderer/src/components/ChatView.tsx` around lines 66 - 69, Validate the
value read from localStorage under LAST_EDITOR_KEY against the known EDITORS
list before using it as the initial state for lastEditor; replace the current
initializer (() => localStorage.getItem(LAST_EDITOR_KEY) ?? EDITORS[0].id) with
logic that reads localStorage, checks EDITORS.some(e => e.id === stored), and
falls back to EDITORS[0].id when the stored id is missing or not found; also
ensure any places that write to lastEditor (setLastEditor) persist only
validated ids to localStorage.
|
@cursor fix ci pls |
|
All done. Here's what I fixed:
Lint passes cleanly across all three packages. Changes pushed to |
EDITORS config and EditorId type now live in @acme/contracts so the shell.openInEditor IPC channel is strictly typed end-to-end. The main process uses the contract's command field (null = file manager via shell.openPath). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Validate cwd is non-empty in shell:open-in-editor IPC handler - Check shell.openPath() return value and throw on error - Attach spawn error listener to prevent crashes from missing CLI - Validate localStorage lastEditor against known EDITORS list - Only call preventDefault() for Cmd+O when action will run Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>
Keep open-in-editor dropdown, accept main's removal of status indicator from header. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add open-in-editor feature with Cmd+O shortcut Add dropdown in header to open workspace in Cursor or system file manager (Finder/Explorer/Files). Include Cmd+O/Ctrl+O global keyboard shortcut to open in last-used editor, persisted to localStorage. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * Move editor definitions to contracts for strict IPC typing EDITORS config and EditorId type now live in @acme/contracts so the shell.openInEditor IPC channel is strictly typed end-to-end. The main process uses the contract's command field (null = file manager via shell.openPath). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix biome formatting issues Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add error handling and address review feedback - Validate cwd is non-empty in shell:open-in-editor IPC handler - Check shell.openPath() return value and throw on error - Attach spawn error listener to prevent crashes from missing CLI - Validate localStorage lastEditor against known EDITORS list - Only call preventDefault() for Cmd+O when action will run Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com> --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>
Add file attachments and inline projector
- Store: index files by cwd+relativePath to avoid cross-worktree collisions; never overwrite dirty buffer on refetch/reopen - EditorPanel: reorder render branches so error state is tested before empty state (was unreachable) - A11y: separate tab activation (div[role=tab]) from close button (proper button element) — no nested interactive elements - Extract shared RightPanelInlineSidebar, RightPanelSheet, and shouldAcceptPanelWidth into RightPanelPrimitives.tsx so PR pingdotgg#2 (Browser panel) only needs to add a config block
Add thread header tabs and persistent thread notes
- Add CODEX_INTEGRATION_ANALYSIS.md with detailed protocol flow analysis * Protocol flow architecture and high-level overview * Request-response JSON-RPC sequence diagrams * Event streaming and transformation pipeline * Session state machine and lifecycle events * Provider dispatch routing analysis * 5 critical improvements ranked by priority * Failure scenarios and recovery analysis * Testing and deployment recommendations - Add CODEX_IMPROVEMENTS_GUIDE.md with step-by-step implementation guide * Improvement pingdotgg#1: Method-specific timeout configuration * Improvement pingdotgg#2: Partial stream recovery buffer * Improvement pingdotgg#3: Circuit breaker pattern implementation * Improvement pingdotgg#4: Graceful shutdown with timeout * Improvement pingdotgg#5: Structured observability logging * Integration checklist and testing procedures * 5-week rollout strategy Architecture grade: B+ (strong foundation, operational concerns remain) Expected impact: 10x reduction in false timeouts, 99.9% → 99.99% availability These guides provide actionable recommendations for improving connection robustness, timeout strategies, error recovery, and observability in the Codex App Server integration.


Summary
Add a header dropdown to open the workspace in Cursor or the system file manager (Finder on macOS, Explorer on Windows, Files on Linux). Includes a global
Cmd+O/Ctrl+Okeyboard shortcut that opens in the last-used editor, with persistence via localStorage.Changes
shell:open-in-editorin contractsshell.openPath()for cross-platform file manager supportCmd+O/Ctrl+O🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Platform Integration