Skip to content

Commit 3e7b642

Browse files
ralyodioclaude
andauthored
feat(desktop): a server manager you can browse and edit (#27)
The app could **create** a connection and nothing else. No list, no edit, no delete, no rename. "New server" was a one-way door: get a field wrong and your only recourse was the CLI. And every field beyond the six in that dialog was unreachable from the window entirely — **tags** most consequentially, since fleet selection is built on `--on tag:production` and there was nowhere in the app to set one. Same for the jump host, a non-standard remote rsync path, notes and agent forwarding. So: a manager, in the shape the job actually has. Every server on the left with its tags, every field of the selected one on the right, and New / Duplicate / Import / Save / Test / Delete. One list shared by both tabs, because servers are shared — the same list backs the transfer panes and Fleet. `~/.ssh/config` hosts are listed but not editable; that is somebody else's file. "Save a copy" turns one into a connection of your own, which is the same deliberate act the CLI's import performs, and leaves the file untouched. Editing upserts on the connection id, so changing a name updates the row rather than leaving a second copy behind. Also fixes the smaller half of the same complaint. Saving a fleet command under an existing name has always updated it — the store upserts — but the control read "Save these settings" either way, so there was no way to tell an edit from a new one. It now reads "Update <name>" when what is on screen is a saved command, and prefills the name so one keypress is enough. The renderer's `Connection` type carried only the eight fields the two-pane view happened to need, which is why the rest were uneditable: they were absent from the type, so nothing could render them. It now describes what a connection actually holds. 517 tests. Verified by driving the built renderer: the list, the tags, the full form, and selecting a different server loading it for editing. Claude-Session: https://claude.ai/code/session_01UeSWg1Czsb2Lwxj8vHUnA4 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6e62210 commit 3e7b642

5 files changed

Lines changed: 557 additions & 8 deletions

File tree

‎apps/desktop/src/app/page.tsx‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { ConnectionDialog } from '@/components/connection-dialog'
1919
import { FleetView } from '@/components/fleet-view'
2020
import { ProfileBar } from '@/components/profile-bar'
21+
import { ServerManager } from '@/components/server-manager'
2122
import { endpointLabel, loadPane, Pane, type PaneEndpoint, type PaneState } from '@/components/pane'
2223
import { TransferRail } from '@/components/transfer-rail'
2324
import { MirrorPreviewDialog, TransferBand, type ActiveJob } from '@/components/transfer-panel'
@@ -90,6 +91,7 @@ export default function Workspace() {
9091
const [job, setJob] = useState<ActiveJob | null>(null)
9192
const [error, setError] = useState<string | null>(null)
9293
const [showConnection, setShowConnection] = useState(false)
94+
const [showServers, setShowServers] = useState(false)
9395
const [tab, setTab] = useState<'transfer' | 'fleet'>('transfer')
9496
const [profiles, setProfiles] = useState<SyncProfile[]>([])
9597
const [outsideShell, setOutsideShell] = useState(false)
@@ -380,11 +382,11 @@ export default function Workspace() {
380382
<div className="ml-auto flex items-center gap-2">
381383
<Button
382384
variant="outline"
383-
onClick={() => setShowConnection(true)}
385+
onClick={() => setShowServers(true)}
384386
className="h-[var(--control)] gap-2 border-line-strong text-[12px]"
385387
>
386388
<Users className="size-3.5" />
387-
New server
389+
Servers
388390
</Button>
389391

390392
{/*
@@ -408,8 +410,8 @@ export default function Workspace() {
408410
<PopoverContent align="end" className="w-[236px] gap-0.5 p-1.5">
409411
<MenuItem
410412
icon={<Plus className="size-3.5" />}
411-
onClick={() => setShowConnection(true)}
412-
label="Add a server…"
413+
onClick={() => setShowServers(true)}
414+
label="Servers…"
413415
/>
414416
<MenuItem
415417
icon={<FileDown className="size-3.5" />}
@@ -543,9 +545,15 @@ export default function Workspace() {
543545
</div>
544546

545547
<div className={`flex min-h-0 flex-1 flex-col ${tab === 'fleet' ? '' : 'hidden'}`}>
546-
<FleetView onAddServer={() => setShowConnection(true)} />
548+
<FleetView onAddServer={() => setShowServers(true)} />
547549
</div>
548550

551+
<ServerManager
552+
open={showServers}
553+
onClose={() => setShowServers(false)}
554+
onChanged={() => void refreshConnections()}
555+
/>
556+
549557
<ConnectionDialog open={showConnection} onClose={() => setShowConnection(false)} onSaved={() => void refreshConnections()} />
550558

551559
<MirrorPreviewDialog

‎apps/desktop/src/components/fleet-view.tsx‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,15 @@ export function FleetView({ onAddServer }: { onAddServer: () => void }) {
360360

361361
const canRun = selected.size > 0 && script.trim().length > 0 && !running
362362

363+
/**
364+
* Is what is on screen a saved command you are changing?
365+
*
366+
* Saving under an existing name has always updated it — the store upserts —
367+
* but the control said "Save these settings" either way, so there was no way
368+
* to tell an edit from a new one.
369+
*/
370+
const editingSaved = commands.some((command) => !command.builtin && command.name === label)
371+
363372
return (
364373
<div className="flex min-h-0 flex-1 flex-col">
365374
{error ? (
@@ -563,12 +572,18 @@ export function FleetView({ onAddServer }: { onAddServer: () => void }) {
563572
{script.trim() && !savingCommand ? (
564573
<button
565574
type="button"
566-
onClick={() => setSavingCommand(true)}
575+
onClick={() => {
576+
// Prefill with the loaded command's name so saving over it
577+
// is one keypress. Updating was always possible — saving
578+
// under the same name upserts — but nothing said so.
579+
setNewCommandName(editingSaved ? label : '')
580+
setSavingCommand(true)
581+
}}
567582
disabled={running}
568583
className="focus-ring inline-flex items-center gap-1 rounded-md border border-dashed border-line-strong px-2 py-0.5 text-[11px] text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50"
569584
>
570585
<BookmarkPlus className="size-2.5" />
571-
Save these settings
586+
{editingSaved ? `Update ${label}` : 'Save these settings'}
572587
</button>
573588
) : null}
574589

0 commit comments

Comments
 (0)