Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 5.1k
Add open-in-editor feature with Cmd+O shortcut#2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
3303d9a6204eafc8f33b884b71e036314bbFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -3,9 +3,10 @@ fixPath(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { spawn } from "node:child_process"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import path from "node:path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { BrowserWindow, app, dialog, ipcMain, session } from "electron"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { BrowserWindow, app, dialog, ipcMain, session, shell } from "electron"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| EDITORS, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IPC_CHANNELS, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type TerminalCommandInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type TerminalCommandResult, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @@ -125,6 +126,29 @@ function registerIpcHandlers(): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Shell handlers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ipcMain.handle( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IPC_CHANNELS.shellOpenInEditor, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async (_event, cwd: string, editor: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!cwd) throw new Error("cwd is required"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const editorDef = EDITORS.find((e) => e.id === editor); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!editorDef) throw new Error(`Unknown editor: ${editor}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!editorDef.command) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const error = await shell.openPath(cwd); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error) throw new Error(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const child = spawn(editorDef.command, [cwd], { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| detached: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stdio: "ignore", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| child.on("error", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* ignore spawn failures for detached editors */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| child.unref(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+129
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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:
💡 Result:
So error handling is typically: import{shell}from"electron";asyncfunctionopenFile(p: string){try{consterr=awaitshell.openPath(p);if(err){// open failed; Electron returns an error message stringconsole.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 Citations:
🌐 Web query:
💡 Result: In Node.js,
A common safe pattern is to funnel both import{spawn}from'node:child_process';functionspawnChecked(cmd,args,opts){constchild=spawn(cmd,args,opts);letsettled=false;constdone=(err,result)=>{if(settled)return;settled=true;if(err)throwerr;// or reject a Promise / call cb(err)returnresult;};child.once('error',(err)=>done(err));// spawn/kill/IPC failurechild.once('close',(code,signal)=>{// stdio fully closedif(code===0)returndone(null,{ code, signal });done(Object.assign(newError(`exit ${code??'null'} (${signal??'no-signal'})`),{ code, signal }));});returnchild;}Sources: Node.js References Citations:
Add error handling for 🛠️ 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
Suggested change
🤖 Prompt for AI Agents | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Agent handlers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ipcMain.handle( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IPC_CHANNELS.agentSpawn, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -8,6 +8,7 @@ import { | ||
| useState, | ||
| } from "react"; | ||
| import { EDITORS, type EditorId } from "@acme/contracts"; | ||
| import { | ||
| DEFAULT_MODEL, | ||
| DEFAULT_REASONING, | ||
| @@ -32,6 +33,18 @@ function formatMessageMeta(createdAt: string, duration: string | null): string { | ||
| return `${formatTimestamp(createdAt)} • ${duration}`; | ||
| } | ||
| const FILE_MANAGER_LABEL = navigator.platform.includes("Mac") | ||
| ? "Finder" | ||
| : navigator.platform.includes("Win") | ||
| ? "Explorer" | ||
| : "Files"; | ||
| function editorLabel(editor: (typeof EDITORS)[number]): string { | ||
| return editor.command ? editor.label : FILE_MANAGER_LABEL; | ||
| } | ||
| const LAST_EDITOR_KEY = "codething:last-editor"; | ||
| function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string { | ||
| if (tone === "error") return "text-rose-300/50"; | ||
| if (tone === "tool") return "text-[#8a8a8a]"; | ||
| @@ -46,12 +59,20 @@ export default function ChatView() { | ||
| const [isSending, setIsSending] = useState(false); | ||
| const [isConnecting, setIsConnecting] = useState(false); | ||
| const [isModelMenuOpen, setIsModelMenuOpen] = useState(false); | ||
| const [isEditorMenuOpen, setIsEditorMenuOpen] = useState(false); | ||
| const [lastEditor, setLastEditor] = useState<EditorId>(() => { | ||
| const stored = localStorage.getItem(LAST_EDITOR_KEY); | ||
| return EDITORS.some((e) => e.id === stored) | ||
| ? (stored as EditorId) | ||
| : EDITORS[0].id; | ||
| }); | ||
| const [selectedEffort, setSelectedEffort] = | ||
| useState<string>(DEFAULT_REASONING); | ||
| const [nowTick, setNowTick] = useState(() => Date.now()); | ||
| const messagesEndRef = useRef<HTMLDivElement>(null); | ||
| const textareaRef = useRef<HTMLTextAreaElement>(null); | ||
| const modelMenuRef = useRef<HTMLDivElement>(null); | ||
| const editorMenuRef = useRef<HTMLDivElement>(null); | ||
| const activeThread = state.threads.find((t) => t.id === state.activeThreadId); | ||
| const activeProject = state.projects.find( | ||
| @@ -179,6 +200,47 @@ export default function ChatView() { | ||
| }; | ||
| }, [isModelMenuOpen]); | ||
| useEffect(() => { | ||
| if (!isEditorMenuOpen) return; | ||
| const handleClickOutside = (event: MouseEvent) => { | ||
| if (!editorMenuRef.current) return; | ||
| if ( | ||
| event.target instanceof Node && | ||
| !editorMenuRef.current.contains(event.target) | ||
| ) { | ||
| setIsEditorMenuOpen(false); | ||
| } | ||
| }; | ||
| window.addEventListener("mousedown", handleClickOutside); | ||
| return () => { | ||
| window.removeEventListener("mousedown", handleClickOutside); | ||
| }; | ||
| }, [isEditorMenuOpen]); | ||
| // Cmd+O / Ctrl+O to open in last-used editor | ||
| useEffect(() => { | ||
| const handler = (e: globalThis.KeyboardEvent) => { | ||
| if (e.key === "o" && (e.metaKey || e.ctrlKey) && !e.shiftKey) { | ||
| if (api && activeProject) { | ||
| e.preventDefault(); | ||
| void api.shell.openInEditor(activeProject.cwd, lastEditor); | ||
| } | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| }; | ||
| window.addEventListener("keydown", handler); | ||
| return () => window.removeEventListener("keydown", handler); | ||
| }, [api, activeProject, lastEditor]); | ||
| const openInEditor = (editorId: EditorId) => { | ||
| if (!api || !activeProject) return; | ||
| void api.shell.openInEditor(activeProject.cwd, editorId); | ||
| setLastEditor(editorId); | ||
| localStorage.setItem(LAST_EDITOR_KEY, editorId); | ||
| setIsEditorMenuOpen(false); | ||
| }; | ||
| const ensureSession = async (): Promise<string | null> => { | ||
| if (!api || !activeThread || !activeProject) return null; | ||
| if (activeThread.session && activeThread.session.status !== "closed") { | ||
| @@ -313,6 +375,39 @@ export default function ChatView() { | ||
| </h2> | ||
| </div> | ||
| <div className="flex items-center gap-3"> | ||
| {/* Open in editor */} | ||
| {activeProject && ( | ||
| <div className="relative" ref={editorMenuRef}> | ||
| <button | ||
| type="button" | ||
| className="rounded-md px-2 py-1 text-[10px] text-[#a0a0a0]/40 transition-colors duration-150 hover:text-[#a0a0a0]/60" | ||
| onClick={() => setIsEditorMenuOpen((v) => !v)} | ||
| > | ||
| Open in… | ||
| </button> | ||
| {isEditorMenuOpen && ( | ||
| <div className="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-md border border-white/[0.08] bg-[#1b1b1d] py-1 shadow-xl"> | ||
| {EDITORS.map((editor) => ( | ||
| <button | ||
| key={editor.id} | ||
| type="button" | ||
| className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] text-[#e0e0e0] hover:bg-white/[0.06]" | ||
| onClick={() => openInEditor(editor.id)} | ||
| > | ||
| {editorLabel(editor)} | ||
| {editor.id === lastEditor && ( | ||
| <kbd className="ml-auto text-[9px] text-[#a0a0a0]/40"> | ||
| {navigator.platform.includes("Mac") | ||
| ? "\u2318O" | ||
| : "Ctrl+O"} | ||
| </kbd> | ||
| )} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </div> | ||
| )} | ||
| {/* Diff toggle */} | ||
| <button | ||
| type="button" | ||
Uh oh!
There was an error while loading. Please reload this page.