diff --git a/package-lock.json b/package-lock.json index 17f981b23..f4732e5da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-opener": "^2", + "lucide-react": "^0.562.0", "prismjs": "^1.30.0", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -2050,6 +2051,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", diff --git a/package.json b/package.json index 93bce31c0..67371d0b0 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-opener": "^2", + "lucide-react": "^0.562.0", "prismjs": "^1.30.0", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7f4d75615..e3427cf48 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -16,8 +16,8 @@ "title": "CodexMonitor", "width": 1200, "height": 700, - "minWidth": 1200, - "minHeight": 700, + "minWidth": 360, + "minHeight": 600, "titleBarStyle": "Overlay", "hiddenTitle": true, "transparent": true, diff --git a/src/App.tsx b/src/App.tsx index 796a7378b..256db6d5c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,10 @@ import "./styles/diff-viewer.css"; import "./styles/debug.css"; import "./styles/plan.css"; import "./styles/about.css"; +import "./styles/tabbar.css"; +import "./styles/compact-base.css"; +import "./styles/compact-phone.css"; +import "./styles/compact-tablet.css"; import { Sidebar } from "./components/Sidebar"; import { Home } from "./components/Home"; import { MainHeader } from "./components/MainHeader"; @@ -24,6 +28,9 @@ import { GitDiffViewer } from "./components/GitDiffViewer"; import { DebugPanel } from "./components/DebugPanel"; import { PlanPanel } from "./components/PlanPanel"; import { AboutView } from "./components/AboutView"; +import { TabBar } from "./components/TabBar"; +import { TabletNav } from "./components/TabletNav"; +import { ArrowLeft, TerminalSquare } from "lucide-react"; import { useWorkspaces } from "./hooks/useWorkspaces"; import { useThreads } from "./hooks/useThreads"; import { useWindowDrag } from "./hooks/useWindowDrag"; @@ -37,6 +44,7 @@ import { useDebugLog } from "./hooks/useDebugLog"; import { useWorkspaceRefreshOnFocus } from "./hooks/useWorkspaceRefreshOnFocus"; import { useWorkspaceRestore } from "./hooks/useWorkspaceRestore"; import { useResizablePanels } from "./hooks/useResizablePanels"; +import { useLayoutMode } from "./hooks/useLayoutMode"; import type { AccessMode, QueuedMessage } from "./types"; function useWindowLabel() { @@ -61,10 +69,18 @@ function MainApp() { planPanelHeight, onPlanPanelResizeStart, } = useResizablePanels(); + const layoutMode = useLayoutMode(); + const isCompact = layoutMode !== "desktop"; + const isTablet = layoutMode === "tablet"; + const isPhone = layoutMode === "phone"; const [centerMode, setCenterMode] = useState<"chat" | "diff">("chat"); const [selectedDiffPath, setSelectedDiffPath] = useState(null); const [gitPanelMode, setGitPanelMode] = useState<"diff" | "log">("diff"); const [accessMode, setAccessMode] = useState("current"); + const [activeTab, setActiveTab] = useState< + "projects" | "codex" | "git" | "log" + >("codex"); + const tabletTab = activeTab === "projects" ? "codex" : activeTab; const [queuedByThread, setQueuedByThread] = useState< Record >({}); @@ -98,11 +114,14 @@ function MainApp() { const { status: gitStatus, refresh: refreshGitStatus } = useGitStatus(activeWorkspace); + const compactTab = isTablet ? tabletTab : activeTab; + const shouldLoadDiffs = + centerMode === "diff" || (isCompact && compactTab === "git"); const { diffs: gitDiffs, isLoading: isDiffLoading, error: diffError, - } = useGitDiffs(activeWorkspace, gitStatus.files, centerMode === "diff"); + } = useGitDiffs(activeWorkspace, gitStatus.files, shouldLoadDiffs); const { entries: gitLogEntries, total: gitLogTotal, @@ -183,6 +202,24 @@ function MainApp() { ? queuedByThread[activeThreadId] ?? [] : []; + useEffect(() => { + if (!isPhone) { + return; + } + if (!activeWorkspace && activeTab !== "projects") { + setActiveTab("projects"); + } + }, [activeTab, activeWorkspace, isPhone]); + + useEffect(() => { + if (!isTablet) { + return; + } + if (activeTab === "projects") { + setActiveTab("codex"); + } + }, [activeTab, isTablet]); + useWindowDrag("titlebar"); useWorkspaceRestore({ workspaces, @@ -197,9 +234,24 @@ function MainApp() { }); async function handleAddWorkspace() { - const workspace = await addWorkspace(); - if (workspace) { - setActiveThreadId(null, workspace.id); + try { + const workspace = await addWorkspace(); + if (workspace) { + setActiveThreadId(null, workspace.id); + if (isCompact) { + setActiveTab("codex"); + } + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + addDebugEntry({ + id: `${Date.now()}-client-add-workspace-error`, + timestamp: Date.now(), + source: "error", + label: "workspace/add error", + payload: message, + }); + alert(`Failed to add workspace.\n\n${message}`); } } @@ -212,6 +264,9 @@ function MainApp() { }); } setActiveWorkspaceId(workspaceId); + if (isCompact) { + setActiveTab("codex"); + } } function exitDiffView() { @@ -226,12 +281,18 @@ function MainApp() { await connectWorkspace(workspace); } await startThreadForWorkspace(workspace.id); + if (isCompact) { + setActiveTab("codex"); + } } function handleSelectDiff(path: string) { setSelectedDiffPath(path); setCenterMode("diff"); setGitPanelMode("diff"); + if (isCompact) { + setActiveTab("git"); + } } async function handleSend(text: string) { @@ -307,59 +368,144 @@ function MainApp() { sendUserMessage, ]); - return ( -
{ + if (isCompact) { + setActiveTab("log"); + return; + } + setDebugOpen((prev) => !prev); + }; + + const showComposer = !isCompact + ? centerMode === "chat" + : (isTablet ? tabletTab : activeTab) === "codex"; + const showGitDetail = Boolean(selectedDiffPath) && isPhone; + const appClassName = `app ${isCompact ? "layout-compact" : "layout-desktop"}${ + isPhone ? " layout-phone" : "" + }${isTablet ? " layout-tablet" : ""}`; + + const sidebarNode = ( + { + exitDiffView(); + setActiveWorkspaceId(null); + if (isCompact) { + setActiveTab("projects"); + } + }} + onSelectWorkspace={(workspaceId) => { + exitDiffView(); + selectWorkspace(workspaceId); + }} + onConnectWorkspace={async (workspace) => { + await connectWorkspace(workspace); + if (isCompact) { + setActiveTab("codex"); + } + }} + onAddAgent={handleAddAgent} + onToggleWorkspaceCollapse={(workspaceId, collapsed) => { + const target = workspaces.find((entry) => entry.id === workspaceId); + if (!target) { + return; + } + void updateWorkspaceSettings(workspaceId, { + ...target.settings, + sidebarCollapsed: collapsed, + }); + }} + onSelectThread={(workspaceId, threadId) => { + exitDiffView(); + selectWorkspace(workspaceId); + setActiveThreadId(threadId, workspaceId); + }} + onDeleteThread={(workspaceId, threadId) => { + removeThread(workspaceId, threadId); + }} + onDeleteWorkspace={(workspaceId) => { + void removeWorkspace(workspaceId); + }} + /> + ); + + const messagesNode = ( + -
- { - exitDiffView(); - setActiveWorkspaceId(null); - }} - onSelectWorkspace={(workspaceId) => { - exitDiffView(); - selectWorkspace(workspaceId); - }} - onConnectWorkspace={connectWorkspace} - onAddAgent={handleAddAgent} - onToggleWorkspaceCollapse={(workspaceId, collapsed) => { - const target = workspaces.find((entry) => entry.id === workspaceId); - if (!target) { - return; - } - void updateWorkspaceSettings(workspaceId, { - ...target.settings, - sidebarCollapsed: collapsed, - }); - }} - onSelectThread={(workspaceId, threadId) => { - exitDiffView(); - selectWorkspace(workspaceId); - setActiveThreadId(threadId, workspaceId); - }} - onDeleteThread={(workspaceId, threadId) => { - removeThread(workspaceId, threadId); - }} - onDeleteWorkspace={(workspaceId) => { - void removeWorkspace(workspaceId); - }} - /> + /> + ); + + const composerNode = showComposer ? ( + { + if (prefillDraft?.id === id) { + setPrefillDraft(null); + } + }} + onEditQueued={(item) => { + if (!activeThreadId) { + return; + } + setQueuedByThread((prev) => ({ + ...prev, + [activeThreadId]: (prev[activeThreadId] ?? []).filter( + (entry) => entry.id !== item.id, + ), + })); + setPrefillDraft(item); + }} + onDeleteQueued={(id) => { + if (!activeThreadId) { + return; + } + setQueuedByThread((prev) => ({ + ...prev, + [activeThreadId]: (prev[activeThreadId] ?? []).filter( + (entry) => entry.id !== id, + ), + })); + }} + models={models} + selectedModelId={selectedModelId} + onSelectModel={setSelectedModelId} + reasoningOptions={reasoningOptions} + selectedEffort={selectedEffort} + onSelectEffort={setSelectedEffort} + accessMode={accessMode} + onSelectAccessMode={setAccessMode} + skills={skills} + /> + ) : null; + + const debugButton = hasDebugAlerts ? ( + + ) : null; + + const desktopLayout = ( + <> + {sidebarNode}
{centerMode === "diff" && ( )}
- {hasDebugAlerts && ( - - )} + {debugButton}
) : ( - + messagesNode )}
@@ -499,59 +606,7 @@ function MainApp() { - {centerMode === "chat" && ( - { - if (prefillDraft?.id === id) { - setPrefillDraft(null); - } - }} - onEditQueued={(item) => { - if (!activeThreadId) { - return; - } - setQueuedByThread((prev) => ({ - ...prev, - [activeThreadId]: (prev[activeThreadId] ?? []).filter( - (entry) => entry.id !== item.id, - ), - })); - setPrefillDraft(item); - }} - onDeleteQueued={(id) => { - if (!activeThreadId) { - return; - } - setQueuedByThread((prev) => ({ - ...prev, - [activeThreadId]: (prev[activeThreadId] ?? []).filter( - (entry) => entry.id !== id, - ), - })); - }} - models={models} - selectedModelId={selectedModelId} - onSelectModel={setSelectedModelId} - reasoningOptions={reasoningOptions} - selectedEffort={selectedEffort} - onSelectEffort={setSelectedEffort} - accessMode={accessMode} - onSelectAccessMode={setAccessMode} - skills={skills} - /> - )} + {composerNode} )} + + ); + + const tabletLayout = ( + <> + +
{sidebarNode}
+
+
+ + {showHome && ( + {}} + /> + )} + {activeWorkspace && !showHome && ( + <> +
+
+ +
+
+ {debugButton} +
+
+ {tabletTab === "codex" && ( + <> +
+ {messagesNode} +
+ {composerNode} + + )} + {tabletTab === "git" && ( +
+ +
+ +
+
+ )} + {tabletTab === "log" && ( + + )} + + )} +
+ + ); + + const phoneLayout = ( +
+ + {activeTab === "projects" &&
{sidebarNode}
} + {activeTab === "codex" && ( +
+ {activeWorkspace ? ( + <> +
+
+ +
+
+ {debugButton} +
+
+
+ {messagesNode} +
+ {composerNode} + + ) : ( +
+

No workspace selected

+

Choose a project to start chatting.

+ +
+ )} +
+ )} + {activeTab === "git" && ( +
+ {!activeWorkspace && ( +
+

No workspace selected

+

Select a project to inspect diffs.

+ +
+ )} + {activeWorkspace && showGitDetail && ( + <> +
+ + Diff +
+
+ +
+ + )} + {activeWorkspace && !showGitDetail && ( + <> +
+
+ +
+
+
+
+ +
+ {!isPhone && ( +
+ +
+ )} +
+ + )} +
+ )} + {activeTab === "log" && ( +
+ +
+ )} + +
+ ); + + return ( +
+
+ {isPhone ? phoneLayout : isTablet ? tabletLayout : desktopLayout}
); } diff --git a/src/components/DebugPanel.tsx b/src/components/DebugPanel.tsx index c037246ba..57b0cea4b 100644 --- a/src/components/DebugPanel.tsx +++ b/src/components/DebugPanel.tsx @@ -5,6 +5,7 @@ type DebugPanelProps = { isOpen: boolean; onClear: () => void; onCopy: () => void; + variant?: "dock" | "full"; }; function formatPayload(payload: unknown) { @@ -26,13 +27,17 @@ export function DebugPanel({ isOpen, onClear, onCopy, + variant = "dock", }: DebugPanelProps) { - if (!isOpen) { + const isVisible = variant === "full" || isOpen; + if (!isVisible) { return null; } return ( -
+
Debug
diff --git a/src/components/GitDiffPanel.tsx b/src/components/GitDiffPanel.tsx index 231413e5f..cdd89f6d7 100644 --- a/src/components/GitDiffPanel.tsx +++ b/src/components/GitDiffPanel.tsx @@ -4,6 +4,7 @@ import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { LogicalPosition } from "@tauri-apps/api/dpi"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { openUrl } from "@tauri-apps/plugin-opener"; +import { GitBranch } from "lucide-react"; type GitDiffPanelProps = { mode: "diff" | "log"; @@ -151,7 +152,10 @@ export function GitDiffPanel({ return (