Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ pub(crate) struct WorkspaceSettings {
pub(crate) group_id: Option<String>,
#[serde(default, rename = "gitRoot")]
pub(crate) git_root: Option<String>,
#[serde(default, rename = "launchScript")]
pub(crate) launch_script: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand Down
4 changes: 4 additions & 0 deletions src-tauri/src/workspaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,7 @@ mod tests {
sort_order,
group_id: None,
git_root: None,
launch_script: None,
},
}
}
Expand Down Expand Up @@ -1948,13 +1949,15 @@ mod tests {
settings.group_id = Some("group-1".to_string());
settings.sidebar_collapsed = true;
settings.git_root = Some("/tmp".to_string());
settings.launch_script = Some("npm run dev".to_string());

let updated =
apply_workspace_settings_update(&mut workspaces, &id, settings.clone()).expect("update");
assert_eq!(updated.settings.sort_order, Some(3));
assert_eq!(updated.settings.group_id.as_deref(), Some("group-1"));
assert!(updated.settings.sidebar_collapsed);
assert_eq!(updated.settings.git_root.as_deref(), Some("/tmp"));
assert_eq!(updated.settings.launch_script.as_deref(), Some("npm run dev"));

let temp_dir = std::env::temp_dir()
.join(format!("codex-monitor-test-{}", Uuid::new_v4()));
Expand All @@ -1969,5 +1972,6 @@ mod tests {
assert_eq!(stored.settings.group_id.as_deref(), Some("group-1"));
assert!(stored.settings.sidebar_collapsed);
assert_eq!(stored.settings.git_root.as_deref(), Some("/tmp"));
assert_eq!(stored.settings.launch_script.as_deref(), Some("npm run dev"));
}
}
29 changes: 29 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import { useThreadRows } from "./features/app/hooks/useThreadRows";
import { useLiquidGlassEffect } from "./features/app/hooks/useLiquidGlassEffect";
import { useCopyThread } from "./features/threads/hooks/useCopyThread";
import { useTerminalController } from "./features/terminal/hooks/useTerminalController";
import { useWorkspaceLaunchScript } from "./features/app/hooks/useWorkspaceLaunchScript";
import { useGitCommitController } from "./features/app/hooks/useGitCommitController";
import { WorkspaceHome } from "./features/workspaces/components/WorkspaceHome";
import { useWorkspaceHome } from "./features/workspaces/hooks/useWorkspaceHome";
Expand Down Expand Up @@ -218,6 +219,7 @@ function MainApp() {
terminalOpen,
handleDebugClick,
handleToggleTerminal,
openTerminal,
} = useLayoutController({
uiScale,
activeWorkspaceId,
Expand Down Expand Up @@ -1310,13 +1312,30 @@ function MainApp() {
onNewTerminal,
onCloseTerminal,
terminalState,
ensureTerminalWithTitle,
restartTerminalSession,
} = useTerminalController({
activeWorkspaceId,
activeWorkspace,
terminalOpen,
onDebug: addDebugEntry,
});

const ensureLaunchTerminal = useCallback(
(workspaceId: string) => ensureTerminalWithTitle(workspaceId, "launch", "Launch"),
[ensureTerminalWithTitle],
);

const launchScriptState = useWorkspaceLaunchScript({
activeWorkspace,
updateWorkspaceSettings,
openTerminal,
ensureLaunchTerminal,
restartLaunchSession: restartTerminalSession,
terminalState,
activeTerminalId,
});

const { handleCycleAgent, handleCycleWorkspace } = useWorkspaceCycling({
workspaces,
groupedWorkspaces,
Expand Down Expand Up @@ -1527,6 +1546,16 @@ function MainApp() {
onCopyThread: handleCopyThread,
onToggleTerminal: handleToggleTerminal,
showTerminalButton: !isCompact,
launchScript: launchScriptState.launchScript,
launchScriptEditorOpen: launchScriptState.editorOpen,
launchScriptDraft: launchScriptState.draftScript,
launchScriptSaving: launchScriptState.isSaving,
launchScriptError: launchScriptState.error,
onRunLaunchScript: launchScriptState.onRunLaunchScript,
onOpenLaunchScriptEditor: launchScriptState.onOpenEditor,
onCloseLaunchScriptEditor: launchScriptState.onCloseEditor,
onLaunchScriptDraftChange: launchScriptState.onDraftScriptChange,
onSaveLaunchScript: launchScriptState.onSaveLaunchScript,
mainHeaderActionsNode: (
<MainHeaderActions
centerMode={centerMode}
Expand Down
102 changes: 102 additions & 0 deletions src/features/app/components/LaunchScriptButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useEffect, useRef } from "react";
import Play from "lucide-react/dist/esm/icons/play";

type LaunchScriptButtonProps = {
launchScript: string | null;
editorOpen: boolean;
draftScript: string;
isSaving: boolean;
error: string | null;
onRun: () => void;
onOpenEditor: () => void;
onCloseEditor: () => void;
onDraftChange: (value: string) => void;
onSave: () => void;
};

export function LaunchScriptButton({
launchScript,
editorOpen,
draftScript,
isSaving,
error,
onRun,
onOpenEditor,
onCloseEditor,
onDraftChange,
onSave,
}: LaunchScriptButtonProps) {
const popoverRef = useRef<HTMLDivElement | null>(null);
const hasLaunchScript = Boolean(launchScript?.trim());

useEffect(() => {
if (!editorOpen) {
return;
}
const handleClick = (event: MouseEvent) => {
const target = event.target as Node;
if (popoverRef.current?.contains(target)) {
return;
}
onCloseEditor();
};
window.addEventListener("mousedown", handleClick);
return () => {
window.removeEventListener("mousedown", handleClick);
};
}, [editorOpen, onCloseEditor]);

return (
<div className="launch-script-menu" ref={popoverRef}>
<div className="launch-script-buttons">
<button
type="button"
className="ghost main-header-action launch-script-run"
onClick={onRun}
onContextMenu={(event) => {
event.preventDefault();
onOpenEditor();
}}
data-tauri-drag-region="false"
aria-label={hasLaunchScript ? "Run launch script" : "Set launch script"}
title={hasLaunchScript ? "Run launch script" : "Set launch script"}
>
<Play size={14} aria-hidden />
</button>
</div>
{editorOpen && (
<div className="launch-script-popover popover-surface" role="dialog">
<div className="launch-script-title">Launch script</div>
<textarea
className="launch-script-textarea"
placeholder="e.g. npm run dev"
value={draftScript}
onChange={(event) => onDraftChange(event.target.value)}
rows={6}
data-tauri-drag-region="false"
/>
{error && <div className="launch-script-error">{error}</div>}
<div className="launch-script-actions">
<button
type="button"
className="ghost"
onClick={onCloseEditor}
data-tauri-drag-region="false"
>
Cancel
</button>
<button
type="button"
className="primary"
onClick={onSave}
disabled={isSaving}
data-tauri-drag-region="false"
>
{isSaving ? "Saving..." : "Save"}
</button>
</div>
</div>
)}
</div>
);
}
39 changes: 39 additions & 0 deletions src/features/app/components/MainHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { revealItemInDir } from "@tauri-apps/plugin-opener";
import type { BranchInfo, OpenAppTarget, WorkspaceInfo } from "../../../types";
import type { ReactNode } from "react";
import { OpenAppMenu } from "./OpenAppMenu";
import { LaunchScriptButton } from "./LaunchScriptButton";

type MainHeaderProps = {
workspace: WorkspaceInfo;
Expand All @@ -28,6 +29,16 @@ type MainHeaderProps = {
isTerminalOpen: boolean;
showTerminalButton?: boolean;
extraActionsNode?: ReactNode;
launchScript?: string | null;
launchScriptEditorOpen?: boolean;
launchScriptDraft?: string;
launchScriptSaving?: boolean;
launchScriptError?: string | null;
onRunLaunchScript?: () => void;
onOpenLaunchScriptEditor?: () => void;
onCloseLaunchScriptEditor?: () => void;
onLaunchScriptDraftChange?: (value: string) => void;
onSaveLaunchScript?: () => void;
worktreeRename?: {
name: string;
error: string | null;
Expand Down Expand Up @@ -69,6 +80,16 @@ export function MainHeader({
isTerminalOpen,
showTerminalButton = true,
extraActionsNode,
launchScript = null,
launchScriptEditorOpen = false,
launchScriptDraft = "",
launchScriptSaving = false,
launchScriptError = null,
onRunLaunchScript,
onOpenLaunchScriptEditor,
onCloseLaunchScriptEditor,
onLaunchScriptDraftChange,
onSaveLaunchScript,
worktreeRename,
}: MainHeaderProps) {
const [menuOpen, setMenuOpen] = useState(false);
Expand Down Expand Up @@ -488,6 +509,24 @@ export function MainHeader({
</div>
</div>
<div className="main-header-actions">
{onRunLaunchScript &&
onOpenLaunchScriptEditor &&
onCloseLaunchScriptEditor &&
onLaunchScriptDraftChange &&
onSaveLaunchScript && (
<LaunchScriptButton
launchScript={launchScript}
editorOpen={launchScriptEditorOpen}
draftScript={launchScriptDraft}
isSaving={launchScriptSaving}
error={launchScriptError}
onRun={onRunLaunchScript}
onOpenEditor={onOpenLaunchScriptEditor}
onCloseEditor={onCloseLaunchScriptEditor}
onDraftChange={onLaunchScriptDraftChange}
onSave={onSaveLaunchScript}
/>
)}
<OpenAppMenu
path={resolvedWorktreePath}
openTargets={openTargets}
Expand Down
2 changes: 2 additions & 0 deletions src/features/app/hooks/useLayoutController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export function useLayoutController({
terminalOpen,
onToggleDebug: handleDebugClick,
onToggleTerminal: handleToggleTerminal,
openTerminal,
} = usePanelVisibility({
isCompact,
activeWorkspaceId,
Expand Down Expand Up @@ -88,5 +89,6 @@ export function useLayoutController({
terminalOpen,
handleDebugClick,
handleToggleTerminal,
openTerminal,
};
}
Loading