New project flow: native directory picker, project name (auto-filled
from folder name), project type selector (Auto-detect plus common
CMS/framework types), optional docroot, and a "start after creating"
checkbox. Runs ddev config with cwd = the chosen directory (streamed
through the existing commandRunner), then reuses the existing
useStartProject() mutation for the start step rather than inventing
multi-phase operation semantics — ddev config registers the project
by name, so a plain `ddev start <name>` works identically to starting
any other project afterward.
Scoped down from the original app's wizard: skipped scaffolding a
fresh CMS codebase (composer create-project / wp core download /
etc.) since each framework needs different install commands — this
covers project registration + config + start, with ddev's own
--auto/--project-type detection handling "auto-detect for existing
folders" for free (no custom detection heuristics needed).
Verified end-to-end against the real scratch environment: called the
create.configure IPC directly (the picker itself opens a native OS
dialog CDP can't drive, same constraint as import/export in step 4)
with a real target directory, confirmed the generated .ddev/config.yaml
has the correct name/type/docroot, confirmed the new project appears
in the sidebar via the existing polling within one refresh cycle, and
verified the modal's form guards (Create disabled until a directory
is chosen) and all type-selector options render correctly. Cleaned up
via `ddev delete`.
ddev logs -f runs indefinitely rather than completing, so it doesn't
fit runStreamed's resolve/reject-on-exit model or the terminal panel's
operation semantics — added a parallel startLogStream/logs:data path
in commandRunner.ts that shares the same process-tracking map (so
stopping a log stream reuses the existing terminal:cancel IPC) but
pushes chunks over a dedicated channel decoupled from the status
bar/toast system. Kill all tracked processes on app quit so a
forgotten open log viewer doesn't leave an orphaned `ddev logs -f`.
Two real bugs found and fixed via live testing:
- react-hooks/set-state-in-effect flagged synchronous setState calls
used only to reset state on service change. Fixed by keying the
streaming component by service (LogPane key={service}) so switching
services remounts it and state resets via useState initializers
instead — the React-recommended pattern for this.
- Filtering operated on raw stream chunks, not lines: a single data
chunk can bundle many log lines or split one across chunk
boundaries, so filtering by chunk let unrelated lines through
whenever a match happened to share a chunk. Fixed by buffering
partial lines per stream and only filtering/rendering once complete
lines are assembled.
Verified end-to-end against the real scratch project via CDP-driven
clicks: live streaming from real containers (db and web service logs
both confirmed with distinct real content), service switching,
filtering (confirmed both the false-positive case is fixed and real
matches still work), and confirmed closing the viewer actually kills
the underlying `ddev logs -f` process rather than leaving it orphaned.
Browse the ~270-entry DDEV add-on registry with search, install via
`ddev addon get`, remove via `ddev addon remove` — both routed through
the streaming commandRunner. `addon get`/`remove` support `--project
<name>` directly (unlike snapshot restore), so no cwd trick needed
here. Verified working with the project stopped, per the plan's
requirement.
Found and fixed a real bug via live UI testing: `ddev addon list
--installed --json-output` omits the `raw` key entirely when nothing
is installed (unlike `ddev list`/`ddev snapshot --list`, which include
`raw: null`). The shared runDdevRead helper treated that as an error,
so after removing the last add-on the query errored on refetch and
React Query kept showing the stale cached row instead of clearing it.
Split the helper into runDdevRead (strict, for describeProject where
missing data is a real error) and runDdevReadList (treats missing raw
as empty, for every list-style command) and fixed all four list call
sites to use it.
Verified end-to-end via CDP-driven clicks against the real scratch
project: search filtering, install (streamed output, confirmed via
`ddev addon list --installed` on the CLI), and remove — including
re-confirming after the fix that the installed-add-ons table actually
clears instead of showing stale data.
Snapshot create/list/restore/delete and DB import/export via native
file dialogs, all routed through the streaming commandRunner from
step 3. `ddev snapshot restore` has no project-name flag (unlike every
other ddev command used so far) — it resolves the project from cwd,
so commandRunner/execDdev now accept an optional cwd and every
database command runs with cwd = project approot instead of passing
the name positionally.
Fixed a real bug found via live testing: the snapshot-naming UI used
window.prompt(), which Electron's renderer does not support (it
returns null with no dialog, silently). window.confirm() does work
(verified: real native dialog, respects accept/dismiss) so the delete
confirmation was left as-is. Snapshot naming now uses an inline text
input instead.
Verified end-to-end against the real scratch DDEV project via CDP-
driven clicks: named snapshot creation, deletion, and restore
(streamed output ending in "Database snapshot restoretest was
restored in 7s") all confirmed working through the actual UI.
import-db/export-db verified via direct CLI round-trip using the
exact --file= flag syntax the app invokes (skipped UI-driving these
since they open native OS file dialogs that CDP cannot dismiss).
Step 3 of the build plan. Long-running ddev commands (start/stop/
restart) now spawn via commandRunner.ts and stream stdout/stderr to
the renderer over IPC (terminal:data/terminal:exit), instead of the
previous fire-and-forget JSON exec. A single useTerminalEvents hook
owns turning those events into terminal panel lines, status bar
progress, and success/error toasts, decoupled from whichever mutation
triggered the command so later features (snapshots, addons) can reuse
the same pipeline. Cancel is wired end-to-end: the status bar's Cancel
button kills the underlying child process via a tracked operation id.
Verified against the real scratch DDEV project via a CDP driver
script (Electron launched with --remoteDebuggingPort, driven by
clicking real DOM buttons): confirmed live streaming output, the
status bar's spinner/cancel affordance, and that cancelling mid-
restart genuinely interrupts the process rather than just hiding the
UI (only the first output line appears, vs. full output on a normal
run).
Wire up the full stack for step 2 of the build plan: a ddev.ts CLI
wrapper in the main process (execFile + JSON envelope parsing, with a
PATH fallback since GUI apps on macOS don't inherit Homebrew's PATH),
IPC handlers exposed through a typed preload window.api surface,
TanStack Query hooks for polling/mutations, and a two-pane UI (project
list sidebar + detail panel with URLs, services, and DB credentials).
Verified end-to-end against a real scratch DDEV project: typecheck,
lint, and tests pass, and the running app correctly displays live
project data and reflects state changes (start/stop) via polling.