From 916f55b4c7cd4551dfc98804b2d28ee4205f9400 Mon Sep 17 00:00:00 2001 From: WtXeLdd <807234943@qq.com> Date: Tue, 17 Mar 2026 11:05:55 +0800 Subject: [PATCH 1/7] feat: add scroll&change rounded --- src/renderer/src/components/context-chip.tsx | 2 +- .../src/components/conversation-view.tsx | 137 ++++++++++++------ .../src/components/inspector-panel.tsx | 2 +- src/renderer/src/components/main-content.tsx | 17 ++- src/renderer/src/components/message-block.tsx | 6 +- src/renderer/src/components/other-view.tsx | 4 +- .../src/components/profile-switcher.tsx | 49 ++++--- src/renderer/src/components/request-item.tsx | 2 +- .../src/components/session-sidebar.tsx | 2 +- .../src/components/settings-dialog.tsx | 8 +- src/renderer/src/components/system-view.tsx | 2 +- src/renderer/src/components/tools-view.tsx | 4 +- src/renderer/src/components/ui/badge.tsx | 2 +- src/renderer/src/components/ui/button.tsx | 10 +- src/renderer/src/components/ui/command.tsx | 8 +- src/renderer/src/components/ui/dialog.tsx | 2 +- src/renderer/src/components/ui/input.tsx | 2 +- src/renderer/src/components/ui/tabs.tsx | 4 +- src/renderer/src/components/ui/tooltip.tsx | 4 +- .../profiles/components/data-flow-diagram.tsx | 6 +- .../profiles/components/provider-badge.tsx | 2 +- .../profiles/components/shell-block.tsx | 4 +- .../src/features/profiles/configure-step.tsx | 2 +- .../profiles/provider-select-step.tsx | 4 +- 24 files changed, 164 insertions(+), 121 deletions(-) diff --git a/src/renderer/src/components/context-chip.tsx b/src/renderer/src/components/context-chip.tsx index 8d82f0b..a9f3f81 100644 --- a/src/renderer/src/components/context-chip.tsx +++ b/src/renderer/src/components/context-chip.tsx @@ -93,7 +93,7 @@ export function ContextChip({ return (
setExpanded(!expanded)} >
diff --git a/src/renderer/src/components/conversation-view.tsx b/src/renderer/src/components/conversation-view.tsx index 6c85754..de5a63b 100644 --- a/src/renderer/src/components/conversation-view.tsx +++ b/src/renderer/src/components/conversation-view.tsx @@ -4,6 +4,7 @@ import { ArrowUp, ArrowDown } from "lucide-react"; import { cn } from "../lib/utils"; import type { SessionTimeline } from "../../../shared/contracts"; import { useTraceStore } from "../stores/trace-store"; +import { useSessionStore } from "../stores/session-store"; const SCROLL_THRESHOLD = 120; @@ -15,6 +16,7 @@ interface ConversationViewProps { export function ConversationView({ timeline, rawMode }: ConversationViewProps) { const storeTrace = useTraceStore((state) => state.trace); const storeRawMode = useTraceStore((state) => state.rawMode); + const selectedSessionId = useSessionStore((s) => s.selectedSessionId); const activeTimeline = timeline ?? storeTrace?.timeline ?? { messages: [] }; const activeRawMode = rawMode ?? storeRawMode; const messages = activeTimeline.messages; @@ -24,16 +26,68 @@ export function ConversationView({ timeline, rawMode }: ConversationViewProps) { const [showBottom, setShowBottom] = useState(false); const [hasNew, setHasNew] = useState(false); const prevCountRef = useRef(messages.length); + const scrollCache = useRef>(new Map()); + const prevSessionRef = useRef(null); + const scrollListenerAttached = useRef(false); const updateButtons = useCallback(() => { const el = viewportRef.current; if (!el) return; const { scrollTop, scrollHeight, clientHeight } = el; - setShowTop(scrollTop > SCROLL_THRESHOLD); + const isScrollable = scrollHeight > clientHeight + 1; + setShowTop(isScrollable && scrollTop > SCROLL_THRESHOLD); const distanceFromBottom = scrollHeight - scrollTop - clientHeight; - setShowBottom(distanceFromBottom > SCROLL_THRESHOLD); + setShowBottom(isScrollable && distanceFromBottom > SCROLL_THRESHOLD); }, []); + // Ref callback to setup scroll listener once + const setViewportRef = useCallback((el: HTMLDivElement | null) => { + // Cleanup old listener if ref changes + if (viewportRef.current && scrollListenerAttached.current) { + viewportRef.current.removeEventListener("scroll", handleScroll); + scrollListenerAttached.current = false; + } + + viewportRef.current = el; + + if (el && !scrollListenerAttached.current) { + el.addEventListener("scroll", handleScroll, { passive: true }); + scrollListenerAttached.current = true; + requestAnimationFrame(updateButtons); + } + }, [updateButtons]); + + const handleScroll = useCallback(() => { + updateButtons(); + const el = viewportRef.current; + if (!el) return; + const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; + if (distanceFromBottom <= SCROLL_THRESHOLD) { + setHasNew(false); + } + }, [updateButtons]); + + // Save/restore scroll position on session switch + useEffect(() => { + const el = viewportRef.current; + if (prevSessionRef.current && el) { + scrollCache.current.set(prevSessionRef.current, el.scrollTop); + } + if (selectedSessionId && el) { + const saved = scrollCache.current.get(selectedSessionId); + requestAnimationFrame(() => { + el.scrollTo({ top: saved ?? 0 }); + updateButtons(); + }); + } + prevSessionRef.current = selectedSessionId ?? null; + }, [selectedSessionId, updateButtons]); + + // Recalculate buttons when content changes + useEffect(() => { + requestAnimationFrame(updateButtons); + }, [messages.length, updateButtons]); + // Detect new messages while not at bottom useEffect(() => { if (messages.length > prevCountRef.current) { @@ -48,41 +102,28 @@ export function ConversationView({ timeline, rawMode }: ConversationViewProps) { prevCountRef.current = messages.length; }, [messages.length]); - // Attach scroll listener to the viewport + // Watch for element size changes (visibility, content loading, etc.) useEffect(() => { const el = viewportRef.current; if (!el) return; - const onScroll = () => { + const resizeObserver = new ResizeObserver(() => { updateButtons(); - // Clear new indicator when scrolled to bottom - const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; - if (distanceFromBottom <= SCROLL_THRESHOLD) { - setHasNew(false); - } - }; - el.addEventListener("scroll", onScroll, { passive: true }); - updateButtons(); - return () => el.removeEventListener("scroll", onScroll); - }, [updateButtons]); - - // Grab the viewport element from radix ScrollArea - const containerRef = useCallback((node: HTMLDivElement | null) => { - if (node) { - const viewport = node.querySelector("[data-slot='scroll-area-viewport']"); - viewportRef.current = viewport as HTMLDivElement | null; - updateButtons(); - } + }); + resizeObserver.observe(el); + return () => resizeObserver.disconnect(); }, [updateButtons]); const scrollToTop = () => { - viewportRef.current?.scrollTo({ top: 0, behavior: "smooth" }); + viewportRef.current?.scrollTo({ top: 0 }); + requestAnimationFrame(updateButtons); }; const scrollToBottom = () => { const el = viewportRef.current; if (el) { - el.scrollTo({ top: el.scrollHeight, behavior: "smooth" }); + el.scrollTo({ top: el.scrollHeight }); setHasNew(false); + requestAnimationFrame(updateButtons); } }; @@ -95,8 +136,8 @@ export function ConversationView({ timeline, rawMode }: ConversationViewProps) { } return ( -
-
{ viewportRef.current = el; }}> +
+
{messages.map((msg, i) => (
- {/* Scroll to top */} - {showTop && ( - - )} - - {/* Scroll to latest */} - {showBottom && ( - + )} + {showBottom && ( + )} - +
)}
); diff --git a/src/renderer/src/components/inspector-panel.tsx b/src/renderer/src/components/inspector-panel.tsx index 2f23202..883a5de 100644 --- a/src/renderer/src/components/inspector-panel.tsx +++ b/src/renderer/src/components/inspector-panel.tsx @@ -67,7 +67,7 @@ export function InspectorPanel({ - + + + :{port} + + + +
+ +
+
+ +
+ +
+
+ + + + + { + await upsertProfile(profile); + setAddOpen(false); + toast.success("Profile created"); + }} + /> + + + + ); +} diff --git a/src/renderer/src/components/profile-switcher.tsx b/src/renderer/src/components/profile-switcher.tsx index 7147ece..7ab91ea 100644 --- a/src/renderer/src/components/profile-switcher.tsx +++ b/src/renderer/src/components/profile-switcher.tsx @@ -148,35 +148,33 @@ function ProfileRow({ profile, port, isRunning, onToggle, onEdit, onDelete }: Pr /> - {profile.name} - {/* Edit / Delete — overlay on hover */} - - - + + {profile.name} + {rowHovered && ( + + + + + )} :{port} -
- - -
- +
{/* Search & Filter */} -
+
- - {showAddForm ? "Add Profile" : "Settings"} - + {!showAddForm && ( + + Settings + + )} {showAddForm ? ( void; @@ -7,10 +8,17 @@ interface StatusBarProps { export function StatusBar({ onSettingsClick }: StatusBarProps) { return ( -
- Agent Trace +
+ {/* Left spacer — balances the right buttons so the center content stays centered */} +
-
+ {/* Center: Profiles */} + + + {/* Right: Action Buttons */} +
+ ); +} + export function SystemView() { const instructions = useTraceStore( (state) => state.trace?.instructions ?? EMPTY_INSTRUCTIONS, @@ -24,13 +47,20 @@ export function SystemView() { ); } + const copyText = rawMode ? JSON.stringify(instructions, null, 2) : text; + if (rawMode) { return (
-
-            {JSON.stringify(instructions, null, 2)}
-          
+
+
+ +
+
+              {JSON.stringify(instructions, null, 2)}
+            
+
); @@ -39,14 +69,11 @@ export function SystemView() { return (
-
- {rawMode ? ( -
-              {text}
-            
- ) : ( - - )} +
+
+ +
+
diff --git a/src/renderer/src/components/tools-view.tsx b/src/renderer/src/components/tools-view.tsx index fde8e7f..198ad45 100644 --- a/src/renderer/src/components/tools-view.tsx +++ b/src/renderer/src/components/tools-view.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { ScrollArea } from "./ui/scroll-area"; -import { ChevronRight } from "lucide-react"; +import { ChevronRight, Copy, Check } from "lucide-react"; import { cn } from "../lib/utils"; import { SchemaFieldRenderer } from "./ui/schema-field-renderer"; import type { InspectorSection, NormalizedTool } from "../../../shared/contracts"; @@ -16,13 +16,39 @@ function getToolsFromInspector(): NormalizedTool[] { return section?.tools ?? []; } +function CopyButton({ text }: { text: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = (e: React.MouseEvent) => { + e.stopPropagation(); + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }; + + return ( + + ); +} + function ToolItem({ tool, rawMode }: { tool: NormalizedTool; rawMode: boolean }) { const [expanded, setExpanded] = useState(false); const [descExpanded, setDescExpanded] = useState(false); + const copyText = rawMode ? JSON.stringify(tool, null, 2) : JSON.stringify(tool, null, 2); + if (rawMode) { return ( -
+
+
+ +
           {JSON.stringify(tool, null, 2)}
         
@@ -31,26 +57,42 @@ function ToolItem({ tool, rawMode }: { tool: NormalizedTool; rawMode: boolean }) } return ( -
+
setExpanded(true) : undefined} + >
setExpanded(!expanded)} + className={cn( + "flex items-center justify-between", + expanded && "sticky top-0 z-10 bg-card cursor-pointer -mx-4 -mt-4 px-4 pt-4 pb-2 border-b border-border/50 transition-colors hover:brightness-95 dark:hover:brightness-110" + )} + onClick={expanded ? () => setExpanded(false) : undefined} > - + + {tool.name} + {!expanded && tool.description && ( + + {tool.description} + )} - /> - {tool.name} - {!expanded && tool.description && ( - - {tool.description} - - )} +
+
e.stopPropagation()}> + +
{expanded && ( -
+
{tool.description && (
Date: Tue, 17 Mar 2026 13:05:34 +0800 Subject: [PATCH 4/7] fix: status --- src/main/ipc/register-ipc.ts | 2 ++ src/preload/index.ts | 12 ++++++++++++ src/renderer/src/hooks/use-proxy-events.ts | 8 +++++++- src/renderer/src/stores/profile-store.ts | 5 +++++ src/shared/contracts/events.ts | 5 +++++ src/shared/electron-api.ts | 2 ++ src/shared/ipc-channels.ts | 1 + 7 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/ipc/register-ipc.ts b/src/main/ipc/register-ipc.ts index 48ae1ad..bbd8faa 100644 --- a/src/main/ipc/register-ipc.ts +++ b/src/main/ipc/register-ipc.ts @@ -92,6 +92,7 @@ export function registerIpcHandlers(deps: IpcDependencies): () => void { const updated = profiles.map((p) => p.id === profileId ? { ...p, autoStart: true } : p); deps.profileStore.saveProfiles(updated); + broadcast(deps.getMainWindow, IPC.PROFILES_CHANGED, { profiles: updated }); broadcast(deps.getMainWindow, IPC.PROFILE_STATUS_CHANGED, { statuses: deps.proxyManager.getStatuses(), }); @@ -108,6 +109,7 @@ export function registerIpcHandlers(deps: IpcDependencies): () => void { const updated = profiles.map((p) => p.id === profileId ? { ...p, autoStart: false } : p); deps.profileStore.saveProfiles(updated); + broadcast(deps.getMainWindow, IPC.PROFILES_CHANGED, { profiles: updated }); broadcast(deps.getMainWindow, IPC.PROFILE_STATUS_CHANGED, { statuses: deps.proxyManager.getStatuses(), }); diff --git a/src/preload/index.ts b/src/preload/index.ts index efce7ee..d4f4c2d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -4,6 +4,7 @@ import type { ConnectionProfile, ExchangeDetailVM, ProfileStatusChangedEvent, + ProfilesChangedEvent, SessionListFilter, SessionListItemVM, SessionTraceVM, @@ -94,6 +95,17 @@ export const electronAPI: ElectronAPI = { ipcRenderer.removeListener(IPC.PROFILE_STATUS_CHANGED, handler); }, + onProfilesChanged: ( + cb: (payload: ProfilesChangedEvent) => void, + ): (() => void) => { + const handler = ( + _e: Electron.IpcRendererEvent, + payload: ProfilesChangedEvent, + ) => cb(payload); + ipcRenderer.on(IPC.PROFILES_CHANGED, handler); + return () => ipcRenderer.removeListener(IPC.PROFILES_CHANGED, handler); + }, + onUpdateStateChanged: (cb: (state: UpdateState) => void): (() => void) => { const handler = (_e: Electron.IpcRendererEvent, state: UpdateState) => cb(state); diff --git a/src/renderer/src/hooks/use-proxy-events.ts b/src/renderer/src/hooks/use-proxy-events.ts index a5e27f2..d27e837 100644 --- a/src/renderer/src/hooks/use-proxy-events.ts +++ b/src/renderer/src/hooks/use-proxy-events.ts @@ -10,6 +10,7 @@ export function useProxyEvents() { const resetSessions = useSessionStore((s) => s.reset); const loadTrace = useTraceStore((state) => state.loadTrace); const setStatuses = useProfileStore((state) => state.setStatuses); + const setProfiles = useProfileStore((state) => state.setProfiles); useEffect(() => { const api = getElectronAPI(); @@ -29,6 +30,10 @@ export function useProxyEvents() { setStatuses(payload.statuses); }); + const unsubProfilesChanged = api.onProfilesChanged((payload) => { + setProfiles(payload.profiles); + }); + const unsubError = api.onProxyError((error) => { toast.error("Proxy Error", { description: error }); }); @@ -37,7 +42,8 @@ export function useProxyEvents() { unsubCapture(); unsubReset(); unsubProfileStatus(); + unsubProfilesChanged(); unsubError(); }; - }, [loadTrace, resetSessions, setStatuses, upsertSession]); + }, [loadTrace, resetSessions, setStatuses, setProfiles, upsertSession]); } diff --git a/src/renderer/src/stores/profile-store.ts b/src/renderer/src/stores/profile-store.ts index abb9b35..f8fd849 100644 --- a/src/renderer/src/stores/profile-store.ts +++ b/src/renderer/src/stores/profile-store.ts @@ -15,6 +15,7 @@ interface ProfileState { startProfile: (profileId: string) => Promise; stopProfile: (profileId: string) => Promise; setStatuses: (statuses: ProfileStatuses) => void; + setProfiles: (profiles: ConnectionProfile[]) => void; } export const useProfileStore = create((set, get) => ({ @@ -78,4 +79,8 @@ export const useProfileStore = create((set, get) => ({ setStatuses: (statuses) => { set({ statuses }); }, + + setProfiles: (profiles) => { + set({ profiles }); + }, })); diff --git a/src/shared/contracts/events.ts b/src/shared/contracts/events.ts index 23fb443..6c716cb 100644 --- a/src/shared/contracts/events.ts +++ b/src/shared/contracts/events.ts @@ -1,4 +1,5 @@ import type { SessionListItemVM } from "./view-models"; +import type { ConnectionProfile } from "./profile"; export interface TraceCapturedEvent { updatedSession: SessionListItemVM; @@ -9,6 +10,10 @@ export interface ProfileStatusChangedEvent { statuses: Record; } +export interface ProfilesChangedEvent { + profiles: ConnectionProfile[]; +} + export interface TraceResetEvent { clearedAt: string; } diff --git a/src/shared/electron-api.ts b/src/shared/electron-api.ts index e52d4ec..17637fe 100644 --- a/src/shared/electron-api.ts +++ b/src/shared/electron-api.ts @@ -2,6 +2,7 @@ import type { ConnectionProfile, ExchangeDetailVM, ProfileStatusChangedEvent, + ProfilesChangedEvent, SessionListFilter, SessionListItemVM, SessionTraceVM, @@ -33,5 +34,6 @@ export interface ElectronAPI { onProfileStatusChanged( cb: (payload: ProfileStatusChangedEvent) => void, ): () => void; + onProfilesChanged(cb: (payload: ProfilesChangedEvent) => void): () => void; onUpdateStateChanged(cb: (state: UpdateState) => void): () => void; } diff --git a/src/shared/ipc-channels.ts b/src/shared/ipc-channels.ts index 2c783dc..abf69e1 100644 --- a/src/shared/ipc-channels.ts +++ b/src/shared/ipc-channels.ts @@ -17,5 +17,6 @@ export const IPC = { TRACE_CAPTURED: "trace:captured", TRACE_RESET: "trace:reset", PROFILE_STATUS_CHANGED: "profiles:status-changed", + PROFILES_CHANGED: "profiles:changed", UPDATE_STATE_CHANGED: "app:update-state-changed", } as const; From c8b030ad09574671f65bcdef69c7bc2d1c1b253d Mon Sep 17 00:00:00 2001 From: WtXeLdd <807234943@qq.com> Date: Tue, 17 Mar 2026 13:05:49 +0800 Subject: [PATCH 5/7] fix: rounded coners --- src/renderer/src/components/message-block.tsx | 2 +- src/renderer/src/components/tools-view.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/message-block.tsx b/src/renderer/src/components/message-block.tsx index 3513da3..25172df 100644 --- a/src/renderer/src/components/message-block.tsx +++ b/src/renderer/src/components/message-block.tsx @@ -143,7 +143,7 @@ export function MessageBlock({ message, rawMode }: MessageBlockProps) {
setExpanded(false) : undefined} > diff --git a/src/renderer/src/components/tools-view.tsx b/src/renderer/src/components/tools-view.tsx index 198ad45..52d7be8 100644 --- a/src/renderer/src/components/tools-view.tsx +++ b/src/renderer/src/components/tools-view.tsx @@ -66,7 +66,7 @@ function ToolItem({ tool, rawMode }: { tool: NormalizedTool; rawMode: boolean })
setExpanded(false) : undefined} > From 971b0e6147a752ed07fd548a69e8717f44495259 Mon Sep 17 00:00:00 2001 From: WtXeLdd <807234943@qq.com> Date: Tue, 17 Mar 2026 13:13:35 +0800 Subject: [PATCH 6/7] fix: test mock --- src/renderer/src/lib/electron-api.ts | 1 + tests/renderer/live-updates.test.tsx | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/renderer/src/lib/electron-api.ts b/src/renderer/src/lib/electron-api.ts index 401ad59..2193ce4 100644 --- a/src/renderer/src/lib/electron-api.ts +++ b/src/renderer/src/lib/electron-api.ts @@ -24,6 +24,7 @@ export function getElectronAPI(): ElectronAPI { "onTraceCaptured", "onTraceReset", "onProfileStatusChanged", + "onProfilesChanged", "onUpdateStateChanged", ]; diff --git a/tests/renderer/live-updates.test.tsx b/tests/renderer/live-updates.test.tsx index bfcd075..df8cef9 100644 --- a/tests/renderer/live-updates.test.tsx +++ b/tests/renderer/live-updates.test.tsx @@ -7,6 +7,7 @@ import { useSessionStore } from "../../src/renderer/src/stores/session-store"; import { useTraceStore } from "../../src/renderer/src/stores/trace-store"; import type { ProfileStatusChangedEvent, + ProfilesChangedEvent, SessionListItemVM, SessionTraceVM, TraceResetEvent, @@ -29,6 +30,7 @@ const mockOnProxyError = vi.fn(() => () => {}); let traceCapturedHandler: ((payload: TraceCapturedEvent) => void) | null = null; let profileStatusHandler: ((payload: ProfileStatusChangedEvent) => void) | null = null; +let profilesChangedHandler: ((payload: ProfilesChangedEvent) => void) | null = null; let traceResetHandler: ((payload: TraceResetEvent) => void) | null = null; const mockOnTraceCaptured = vi.fn((cb: (payload: TraceCapturedEvent) => void) => { @@ -47,6 +49,15 @@ const mockOnProfileStatusChanged = vi.fn( }, ); +const mockOnProfilesChanged = vi.fn( + (cb: (payload: ProfilesChangedEvent) => void) => { + profilesChangedHandler = cb; + return () => { + profilesChangedHandler = null; + }; + }, +); + const mockOnTraceReset = vi.fn((cb: (payload: TraceResetEvent) => void) => { traceResetHandler = cb; return () => { @@ -70,6 +81,7 @@ vi.mock("../../src/renderer/src/lib/electron-api", () => ({ onTraceCaptured: mockOnTraceCaptured, onTraceReset: mockOnTraceReset, onProfileStatusChanged: mockOnProfileStatusChanged, + onProfilesChanged: mockOnProfilesChanged, onProxyError: mockOnProxyError, }), })); @@ -139,9 +151,11 @@ describe("Session Store", () => { mockClearHistory.mockReset().mockResolvedValue(undefined); mockOnTraceCaptured.mockClear(); mockOnProfileStatusChanged.mockClear(); + mockOnProfilesChanged.mockClear(); mockOnTraceReset.mockClear(); traceCapturedHandler = null; profileStatusHandler = null; + profilesChangedHandler = null; traceResetHandler = null; }); From 5b7b646085143852c029282607e50b3af8050495 Mon Sep 17 00:00:00 2001 From: WtXeLdd <807234943@qq.com> Date: Tue, 17 Mar 2026 13:18:55 +0800 Subject: [PATCH 7/7] fix: setting add delete/edit --- .../src/components/settings-dialog.tsx | 88 ++++++++++++++++++- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/components/settings-dialog.tsx b/src/renderer/src/components/settings-dialog.tsx index 898327d..e094280 100644 --- a/src/renderer/src/components/settings-dialog.tsx +++ b/src/renderer/src/components/settings-dialog.tsx @@ -4,6 +4,9 @@ import { DialogContent, DialogHeader, DialogTitle, + DialogDescription, + DialogFooter, + DialogClose, } from "./ui/dialog"; import { Button } from "./ui/button"; import { useAppStore } from "../stores/app-store"; @@ -12,7 +15,8 @@ import { useSessionStore } from "../stores/session-store"; import { ProfileForm } from "../features/profiles/profile-form"; import { cn } from "../lib/utils"; import { toast } from "sonner"; -import { Github, ExternalLink, Trash2, Sparkles } from "lucide-react"; +import { Github, ExternalLink, Trash2, Sparkles, Pencil } from "lucide-react"; +import type { ConnectionProfile } from "../../../shared/contracts"; interface SettingsDialogProps { open: boolean; @@ -26,10 +30,12 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) { downloadUpdate, quitAndInstallUpdate, } = useAppStore(); - const { profiles, statuses, initialized, initialize, startProfile, stopProfile, upsertProfile } = + const { profiles, statuses, initialized, initialize, startProfile, stopProfile, upsertProfile, deleteProfile } = useProfileStore(); const clearHistory = useSessionStore((s) => s.clearHistory); const [showAddForm, setShowAddForm] = useState(false); + const [editingProfile, setEditingProfile] = useState(null); + const [deletingProfile, setDeletingProfile] = useState(null); useEffect(() => { if (!open || initialized) return; @@ -111,10 +117,13 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
{profiles.map((profile) => { const isRunning = statuses[profile.id]?.isRunning ?? false; + const [rowHovered, setRowHovered] = useState(false); return (
setRowHovered(true)} + onMouseLeave={() => setRowHovered(false)} >
+ {rowHovered && ( +
+ + +
+ )} :{profile.localPort} @@ -235,6 +262,61 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
)} + + {/* Edit Profile Dialog */} + !open && setEditingProfile(null)}> + + {editingProfile && ( + { + await upsertProfile(updated); + setEditingProfile(null); + toast.success("Profile updated"); + }} + /> + )} + + + + {/* Delete Confirmation Dialog */} + !open && setDeletingProfile(null)}> + + + Delete {deletingProfile?.name}? + + This will stop the proxy and remove this profile permanently. + + + + + + + + + + );