Skip to content

feat: complete Go/Wails → C++/WebView migration#26

Merged
KrisPowers merged 349 commits into
feat/cpp-migrationfrom
feat/webview-migration
Jun 22, 2026
Merged

feat: complete Go/Wails → C++/WebView migration#26
KrisPowers merged 349 commits into
feat/cpp-migrationfrom
feat/webview-migration

Conversation

@KrisPowers

@KrisPowers KrisPowers commented May 31, 2026

Copy link
Copy Markdown
Member

What this is

Step 2 of ripping Wails and Go out of cmdIDE and replacing them with a from-scratch C++ host wrapped around WebView2 (and webview/webview for cross-platform parity). Step 1 (feat/cpp-migration) got the C++ backend compiling and talking to the existing Go process over a named pipe. This PR finishes the job: it builds the actual native window, wires up IPC end-to-end, ports every backend feature the frontend depends on, rebuilds the installer in C++/WebView too, and then deletes the entire Go/Wails codebase (129 files) once nothing depends on it anymore.

Branch hierarchy: mainfeat/cpp-migration (Step 1) ← feat/webview-migration (Step 2, this PR)

How it went

Getting a window on screen. Scaffolded cpp/host/ around webview/webview via vcpkg. First milestone was a blank WebView2 window loading www/ next to the exe, which took some iteration to get CMake copying the frontend's dist/ output into the right place at build time. Once that worked, registered __cmdide_invoke as the IPC binding and got a ping/pong round trip going between JS and C++.

Swapping the IPC layer out from under the frontend. The frontend was built entirely around window.go.main.App.* Wails bindings. Instead of rewriting every call site, I wrote lib/ipc.ts (a typed invoke<T>()/on()/off() client) and lib/wails-shim.ts, which intercepts the old Wails calls at runtime and routes them through the new IPC. That let the backend migrate one feature at a time without the frontend noticing. Terminal (ConPTY), file ops, config, search, sysinfo, sessions, packing, the updater, window management, clipboard, and shell ops all now go through C++.

Making it feel native. A WebView in a default OS window looks cheap, so I added a frameless window with proper WM_NCHITTEST drag regions, ported the GDI+ splash screen and Windows jump list straight over from their Go implementations, and added a single-instance mutex so launching the app twice just refocuses the existing window.

Cross-platform. Up to that point everything was Windows-only by necessity. Added a forkpty-based terminal backend for macOS/Linux, made config paths resolve correctly per platform, guarded the _WIN32-only sysinfo code, and swapped WinHTTP for cpp-httplib plus OpenSSL so networking isn't Windows-locked either.

The installer. Took longer than expected. Plan was to port the installer to C++/WebView too with the same IPC pattern, but I hit a string of rendering issues: blank screens from file:// CORS restrictions, a missing window.runtime stub crashing React on mount, temp paths with backslashes breaking file:// URL construction, and a frameless-window flash on launch. Fixed it by serving the frontend from a local cpp-httplib server with virtual host mapping instead of file://, inlining SVG logos as data URIs, pre-creating the frameless HWND before showing it, and wrapping the shim import in a try/catch so React always mounts even if IPC isn't ready. End result: clean install flow with a GitHub-releases version picker, real progress events, no CMD-window flash, and everything statically linked.

Tearing out the old build, CI, and Go itself. Rewrote build.ps1 from scratch as pure CMake with two variants (base and plugins, matching the old Wails artifact names so downstream tooling doesn't break). Then went through code-quality.yml, release.yml, and build-matrix.yml fixing CI one failure at a time: vcpkg caching, rollup lockfile mismatches on Linux/macOS, cmake 4.x quirks on Ubuntu, a YAML heredoc indentation bug silently breaking release.yml, and removing Go from CodeQL/coverage. Once CI was green on all three platforms with no Go in the loop, deleted the Go application backend (78 files), the named-pipe IPC transport, and the standalone cmdide-backend.exe target, then updated .gitignore and the migration docs.

129 Go/Wails files gone. No .go files remain outside .git/.

What else landed on this branch

  • CWE security scanner: new C++ dispatch handler that scans source for common vulnerability patterns, plus a redesigned two-panel Problems UI showing the snippet, impact, and remediation side by side (and a fix for crashes on files with non-UTF-8 paths/contents).
  • Database panel rewrite: moved to a SQLite backend and added a privacy mode that blurs query results until clicked.
  • Tab/search UX overhaul: flat seamless tab bar with an active-tab indicator, replaced the old tab drawer with a proper search palette, reorganized toolbar button groups.
  • Terminal quality-of-life: Ctrl+click now opens files and cds into directories (with a "quick paths" banner), fixed Windows absolute-path extraction, fixed CWD label truncation, guarded against zero-dimension resize events that were panicking ConPTY during panel-split layout changes.
  • Theme system simplification: went from 11 themes down to dark and light, with a follow-up contrast pass for dropdowns and the terminal that had hardcoded white text invisible on light backgrounds.
  • Settings consolidation: collapsed settings to 3 sections with instant auto-save instead of an explicit save step.
  • Lint/CI hygiene: floating promises, unused vars, duplicate imports, unsafe-any downgrades, a curly-quote parse error, and a CodeQL alert about shell-string escaping in the ctrl-click cd command. All clean now.

Test plan

  • cmake --build cpp/build --config Release --target cmdide-host succeeds (CI: windows/macos/ubuntu all green)
  • ./build.ps1 -AppOnly produces cmdIDE-windows-amd64.zip (base) and cmdIDE-plugins-windows-amd64.zip (plugins)
  • cmdide-host.exe opens a window with the cmdIDE frontend UI
  • Terminal opens, accepts input, and streams output
  • No .go files remain outside .git/
  • TypeScript compiles with no errors in app/frontend/
  • Manual smoke test of macOS and Linux builds on real hardware (they compile clean in CI; haven't run them outside the build matrix yet)

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Large PR Warning

This PR changes 20425 lines (+8845 / -11580).

Large PRs are harder to review and more likely to introduce bugs. Consider splitting it into smaller, focused changes if possible.

@github-advanced-security

Copy link
Copy Markdown
Contributor

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Comment thread app/frontend/src/components/Terminal.tsx Fixed
@KrisPowers KrisPowers changed the title feat: complete Go/Wails → C++/WebView migration (Step 2) feat: complete Go/Wails → C++/WebView migration Jun 7, 2026
KrisPowers added 21 commits June 8, 2026 18:29
saveLayoutToStorage() serializes the pane tree with tab INDICES (not IDs)
so it survives sessions where tabs get fresh IDs. restoreLayoutFromStorage()
maps indices back to the newly-restored tab IDs. Layout + focused pane are
restored inside the soft-close session-restore path.
… during sub-page navigation

- Remove standalone 36px header bar; window controls move to the focused
  pane's tab bar via a windowControls prop threaded through SplitPaneView
- PaneTabBar accepts windowControls slot rendered after the pane controls
- Remove split-menu dropdown (PaneTabBar already has H/V split buttons)
- renderContent now always mounts terminals (display:none when not visible)
  so sub-page navigation no longer unmounts/gray-screens the terminal layer
Add 'ports' to PageId, wire Sidebar button to onNavigate('ports'),
and render PortsTab in renderSidebarPage alongside other sidebar pages.
…n loss

Iterate tabs (not leaves) in the terminal overlay so orphaned terminals
stay mounted with display:none during the brief gap between pane removal
and tab reassignment, preventing CloseTerminal from destroying the session.
Implements git.status, git.diff, git.add, git.reset, git.discard,
git.stash, git.stash.list, git.stash.pop, git.stash.drop,
git.commit, git.push, git.pull, git.branches, git.checkout.

Uses CreateProcess (Windows) / popen (POSIX) with no console window.
Commit messages written to temp files to avoid command-line quoting
issues with embedded quotes and newlines.
Comment thread app/frontend/src/components/VersionControlPanel.tsx Fixed
KrisPowers and others added 6 commits June 18, 2026 21:17
Built fresh, not derived from the deleted implementation: jobs are
grouped into dependency-depth lanes and rendered as content-sized pill
elements in normal document flow (CSS flex, not a hand-rolled pixel
grid). Connector positions come from actual measured DOM rects via
ResizeObserver, and wires are only ever drawn between a lane and the
one immediately before it.

There's no separate outcome column at all — a terminal job carries its
own pass/fail marker directly — so the long cross-column connector that
used to cut through unrelated nodes can't occur structurally. A
dependency on a non-adjacent lane is called out as text under the job
instead of a drawn line.
Shape (999px pill radius) and orientation (left-to-right lanes) were
both rejected. Removing this version entirely before rebuilding
top-to-bottom with a different node shape, rather than tweaking the
existing pills/orientation in place.
Built from a blank file, not adapted from the deleted pill version:
flows top to bottom instead of left to right, cards are rectangles with
a sane 6px corner radius and a colored top accent border instead of
999px pill capsules, and rows are layered with Kahn's algorithm (peel
off the current frontier, repeat) instead of the memoized longest-path
recursion the previous two versions both used under the hood.

Same structural guarantee as before: a terminal job carries its own
pass/fail chips, so there's no distant outcome row for a wire to cross
behind another card to reach.
Jobs and steps now carry their source line numbers so each card's edit
pencil and per-job "Add step" button jump straight to the right spot in
the code editor. Terminal jobs show real pass/fail/running status from
the latest run instead of static placeholder chips, and every step lists
its own outcome dot.
Lets users create new jobs directly from the events map, link two jobs
by clicking connector dots on their cards (writing a needs: edge), and
set or cycle a pass/fail/custom run condition on each link. Edits are
spliced directly into the workflow YAML on disk via the generic
fs.writefile channel, preserving unrelated file content untouched.
@github-actions

Copy link
Copy Markdown
Contributor

🔧 Auto-fixed ESLint issues — app/frontend

The ESLint check failed, so eslint --fix was run and the result committed directly to this branch:

 app/frontend/src/components/EventsMap.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

KrisPowers and others added 4 commits June 19, 2026 16:38
window.prompt() doesn't match the app's UI at all. Adds a styled modal
(same look as the split-view dialog) for entering a custom run
condition expression when cycling a link to "Other".
setJobCondition only replaced the if: line itself, leaving a block
scalar's (if: >- / if: |) continuation lines behind as orphaned text
that broke the file's YAML. Parses and replaces the whole ifLine..
ifEndLine span instead.
Combine the two sub-tabs into one "Code" section with the code editor
on the left and the events map on the right, sharing one toolbar with
Add process and Edit Workflow side by side. The divider between panes
is drag-resizable. Also adds drag-to-pan inside the events map canvas
so large workflows can be navigated like a map instead of relying on
scrollbars; the old full-width toolbar there is replaced by a small
floating hint while linking two processes.
@github-actions

Copy link
Copy Markdown
Contributor

🔧 Auto-fixed ESLint issues — app/frontend

The ESLint check failed, so eslint --fix was run and the result committed directly to this branch:

 app/frontend/src/components/WorkflowsPanel.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

KrisPowers and others added 13 commits June 19, 2026 17:14
The code editor was getting squeezed at an even 50/50 split. Give it
more room by default and shrink the events map's starting share.
Drop the redundant path label from the code/events bar and put the
section tabs there instead, since the path is already visible in the
code pane. The Run button moves down next to Add process and Edit
Workflow so every action lives in a single bar above the content.
Clicking Run now switches to the Console tab so the live output is
immediately visible, and Add process switches to the Code tab so the
newly inserted job is visible right away.
The workflow code pane was always rendering with default font size and
no indent guides regardless of the user's editor settings. Wire
appConfig.indent_guides and the current zoom level through, same as
the main Editor component. Minimap is intentionally left out since
it's too heavy for a small code snippet pane.
…p dot alignment

Console tab now shows live output and the step checklist side by side,
checklist narrower on the right, mirroring the Code tab's split layout.

Step status dots were vertically centered against the row's full
stretched height, which includes the bottom spacer between rows. That
drifted the dot away from the step title whenever the title wrapped to
multiple lines. Anchor the title's first line to the dot's center
instead via a fixed line-height and padding-top.
The console output and background process list already surface the
run's result, so the pill next to the Run button was redundant.
Hoist the sortable column header component to module scope so it
keeps a stable identity across re-renders, memoize the filtered/sorted
derivations, and fix row keys to include pid + index instead of just
protocol:address:port. That composite alone collides heavily on real
systems (multiple connections sharing a local port), and duplicate
React keys were causing the table to accumulate stale rows on every
poll tick instead of replacing them, which made sorting look like it
stopped working after the first click.
…buttons

Add a shared SortableColumnHeader component: clicking a column header
now opens a dropdown with explicit ascending/descending sort options
and per-table column visibility toggles. Ports' Port and State headers
fold their protocol/state filters into the dropdown, replacing the
standalone select boxes that used to sit next to the search bar.
Endpoints gets the same sort+column-visibility menu on its headers.
Adds a machine-local relay (no router/UPnP/NAT changes) for forwarding
a listening port to another host:port reachable from this machine.

- cpp/src/port_forward.{hpp,cpp}: Winsock-based TCP/UDP relay manager,
  threaded per rule, persisted via Config under "portForwards", with
  start/stop lifecycle hooks wired into the Dispatcher.
- Frontend: new "Forwards" sub-tab on the Ports page listing configured
  forwards with enable/disable/remove actions, plus a "New Forward"
  modal for entering name, protocol, listen port, and target host:port.
Forwards now sits after Open Ports/Endpoints in the sub-nav. Replaced
the inline em-dash disclaimer with a hoverable info icon and dropped
em-dashes from the remaining copy.
Native title attribute had OS-controlled hover delay and forced a
help cursor. Replaced with a CSS-only hover tooltip that shows
immediately and uses the default cursor.
Rename all Command IDE / cmdIDE / terminal-IDE references to Binder across
source, docs, CI workflows, build tooling, and Windows resource files.
Publisher is now BinderTools, Task Manager identity comes from VERSIONINFO
in resources.rc, and logo/icon/splash assets are replaced with the new mark.
Submodules and remotes are repointed to the bindertools org.
@github-actions

Copy link
Copy Markdown
Contributor

🔧 Auto-fixed ESLint issues — app/frontend

The ESLint check failed, so eslint --fix was run and the result committed directly to this branch:

 app/frontend/src/App.tsx                 | 6 +++---
 app/frontend/src/components/Database.tsx | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

- type problems.scan response via invoke<T> instead of misapplied as-cast
- remove unused textToB64 import and dead prevVisibleLine helper
- mark unused destructured/parameter bindings with underscore prefix
- void the fire-and-forget window.close invoke call
…t/webview-migration

# Conflicts:
#	app/frontend/src/App.tsx
@KrisPowers KrisPowers merged commit 3158ae1 into feat/cpp-migration Jun 22, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants