Skip to content

Commit 8bde169

Browse files
committed
fix desktop controls, oauth listener loop, and docs dependency matrix
1 parent 1314ad1 commit 8bde169

5 files changed

Lines changed: 177 additions & 50 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Package Matrix
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: [main]
7+
8+
jobs:
9+
native-package:
10+
name: Zig-WebUI Package (${{ matrix.os }})
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
os: [ubuntu-latest, macos-latest, windows-latest]
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Setup Node
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: 20
24+
cache: npm
25+
cache-dependency-path: frontend/package-lock.json
26+
27+
- name: Setup Zig
28+
uses: mlugg/setup-zig@v2
29+
with:
30+
version: 0.15.2
31+
32+
- name: Build unified executable
33+
run: zig build install -Doptimize=ReleaseFast
34+
35+
- name: Upload unified executable
36+
uses: actions/upload-artifact@v4
37+
with:
38+
name: codex-manager-binary-${{ matrix.os }}
39+
path: |
40+
zig-out/bin/codex-manager
41+
zig-out/bin/codex-manager.exe

README.md

Lines changed: 105 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,150 @@
11
# Codex Account Manager
22

3-
Codex Account Manager is a SolidJS frontend with a Zig backend powered by [zig-webui](https://github.com/webui-dev/zig-webui).
3+
Codex Account Manager is a SolidJS + Zig app (via [zig-webui](https://github.com/webui-dev/zig-webui)) for managing multiple Codex accounts.
4+
5+
It can switch the active `~/.codex/auth.json` on the fly, organize accounts by status, and show remaining usage per account.
6+
7+
## Features
8+
9+
- Manage multiple Codex accounts in one place
10+
- Switch active account instantly
11+
- Credits/usage checks via ChatGPT token flow (`/backend-api/wham/usage`)
12+
- Account buckets:
13+
- active
14+
- depleted
15+
- frozen
16+
- Drag-and-drop reordering in desktop and web modes
17+
- Custom desktop title bar controls:
18+
- minimize
19+
- fullscreen toggle
20+
- close
21+
- double-click title bar to toggle fullscreen
22+
- Theme persistence (light/dark)
23+
24+
## Tech Stack
25+
26+
- Frontend: SolidJS (`frontend/src`)
27+
- Backend: Zig (`src`, `main.zig`)
28+
- Runtime bridge: `window.cm_rpc(...)` and `window.webui.call(...)`
29+
- Build pipeline: Zig orchestrates frontend typecheck + Vite builds (`build.zig`)
430

5-
It manages multiple Codex accounts, switches active `~/.codex/auth.json` on the fly, and checks per-account credits.
31+
## Requirements
32+
33+
- Zig `0.15+`
34+
- Node.js `20+`
35+
- `npm`
36+
- `curl`
37+
- `codex` CLI
38+
39+
Linux desktop mode also requires GTK/WebKitGTK runtime libraries for WebView.
40+
41+
## Dependency Matrix
42+
43+
### Shared (all OS)
44+
45+
| Type | Dependencies |
46+
|---|---|
47+
| Comptime (build-time) | Zig `0.15.2+`, Node.js `20+`, npm, frontend packages: `solid-js`, `vite`, `vite-plugin-solid`, `typescript` |
48+
| Runtime | `curl` in `PATH`, `codex` CLI in `PATH` |
49+
| Notes | `zig_webui` is vendored via `build.zig.zon` and built as a static Zig dependency (`enable_tls = false`) |
50+
51+
### Linux
652

7-
## Architecture
53+
| Type | Dependencies |
54+
|---|---|
55+
| Comptime (build-time) | Shared dependencies above; C compilation support via Zig toolchain |
56+
| Runtime (desktop mode `--desktop`) | `libgtk-3.so.0`, `libwebkit2gtk-4.1.so.0` (or `libwebkit2gtk-4.0.so.37`) and their distro-provided transitive libs |
57+
| Runtime (web mode `--web`) | A supported installed browser (default browser launch path) |
58+
| Notes | This project loads GTK/WebKit at runtime via `dlopen`, so missing desktop libs fail at runtime, not compile time |
859

9-
- Frontend: SolidJS (`/home/a/projects/js/codex_manager/frontend/src`)
10-
- Backend runtime: Zig + WebUI (`/home/a/projects/js/codex_manager/src`)
11-
- Function bridge:
12-
- `window.cm_rpc(...)` (provided by `/webui.js` in desktop and `--web` modes)
60+
### macOS
1361

14-
## Build System
62+
| Type | Dependencies |
63+
|---|---|
64+
| Comptime (build-time) | Shared dependencies above; Xcode Command Line Tools (clang + macOS SDK) for Objective-C/WebKit build path |
65+
| Runtime (desktop mode `--desktop`) | System frameworks: `Cocoa.framework`, `WebKit.framework` |
66+
| Runtime (web mode `--web`) | A default browser |
67+
| Notes | No GTK/WebKitGTK packages are required on macOS |
1568

16-
The Zig project is now root-level:
69+
### Windows
1770

18-
- `/home/a/projects/js/codex_manager/build.zig`
19-
- `/home/a/projects/js/codex_manager/build.zig.zon`
71+
| Type | Dependencies |
72+
|---|---|
73+
| Comptime (build-time) | Shared dependencies above; C/C++ build support and Windows SDK link libs (`ws2_32`, `ole32`, and ABI-specific system libs) |
74+
| Runtime (desktop mode `--desktop`) | Microsoft Edge WebView2 Runtime |
75+
| Runtime (web mode `--web`) | A default browser |
76+
| Notes | WebView2 headers are vendored by the `webui` dependency; no separate header package is required |
2077

21-
### Commands
78+
## Development
2279

23-
Run app in dev mode:
80+
Run in default mode (web mode):
2481

2582
```bash
2683
zig build dev
2784
```
2885

29-
Default dev mode runs browser-hosted WebUI (`--web` behavior).
30-
31-
Run app in web mode:
86+
Run explicitly in web mode:
3287

3388
```bash
3489
zig build dev -- --web
3590
```
3691

37-
Run desktop WebView mode explicitly:
92+
Run in desktop WebView mode:
3893

3994
```bash
4095
zig build dev -- --desktop
4196
```
4297

43-
Build/install release binary:
98+
## Build and Install
99+
100+
Build/install optimized binary:
44101

45102
```bash
46103
zig build install -Doptimize=ReleaseFast
47104
```
48105

49-
Output binary:
106+
Binary output:
50107

51-
- Linux/macOS: `/home/a/projects/js/codex_manager/zig-out/bin/codex-manager`
52-
- Windows: `/home/a/projects/js/codex_manager/zig-out/bin/codex-manager.exe`
108+
- Linux/macOS: `zig-out/bin/codex-manager`
109+
- Windows: `zig-out/bin/codex-manager.exe`
53110

54-
Run built binary in web mode:
111+
Run installed binary:
55112

56113
```bash
57114
./zig-out/bin/codex-manager --web
115+
./zig-out/bin/codex-manager --desktop
58116
```
59117

60-
Run built binary in desktop mode:
118+
## Testing
119+
120+
Run backend Zig tests:
61121

62122
```bash
63-
./zig-out/bin/codex-manager --desktop
123+
zig build test
64124
```
65125

66-
## Self-contained Binary
126+
## Storage
67127

68-
`zig build install` compiles a single binary with frontend assets embedded at compile time:
69-
- web bundle: `frontend/dist-web/index.html`
70-
- desktop bundle: `frontend/dist-desktop/index.html`
128+
- Managed accounts: app local data directory, file `accounts.json`
129+
- UI bootstrap cache: app local data directory, file `bootstrap-state.json`
130+
- Theme setting: app local data directory, file `ui-theme.txt`
131+
- Active Codex auth target: `~/.codex/auth.json`
71132

72-
The runtime does not depend on shipping any external `dist` folder.
133+
## Troubleshooting
73134

74-
## Requirements
135+
- `WebUI RPC unavailable in this session`:
136+
- Launch through the app (`zig build dev -- --web` or `zig build dev -- --desktop`)
137+
- Open the app URL from the printed `127.0.0.1` address
138+
- Desktop WebView crashes on Linux:
139+
- Ensure GTK/WebKitGTK packages are installed
140+
- Try web mode (`--web`) if your system WebView stack is unstable
141+
- Window buttons do nothing:
142+
- This can happen in plain browser mode
143+
- Use desktop mode for native window controls
75144

76-
- Node.js 20+
77-
- Zig 0.15+
78-
- `curl` in PATH
79-
- `codex` CLI
145+
## Notes
146+
147+
- Frontend bundles are built into two targets:
148+
- `dist-web`
149+
- `dist-desktop`
150+
- The executable is self-contained for app logic and embedded frontend assets.

frontend/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,7 @@ function App() {
10351035
};
10361036

10371037
const handleDragOverAccount = (event: DragEvent, bucket: AccountBucket, targetId: string) => {
1038+
event.stopPropagation();
10381039
const draggedId = resolveDragId(event);
10391040
if (!draggedId) {
10401041
return;

src/main.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub fn main() !void {
4343
const args = try std.process.argsAlloc(allocator);
4444
defer std.process.argsFree(allocator, args);
4545

46-
const web_mode = !hasArg(args, "--desktop") or hasArg(args, "--web");
46+
const web_mode = hasArg(args, "--web") and !hasArg(args, "--desktop");
4747

4848
if (builtin.os.tag == .linux and !web_mode) {
4949
// Work around flaky EGL/DMABUF paths on some Linux+WebKitGTK stacks.

src/rpc.zig

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ fn waitForOAuthCallback(
403403

404404
var remaining_ms: i64 = @intCast(timeout_seconds * 1000);
405405

406-
while (remaining_ms > 0) {
406+
accept_loop: while (remaining_ms > 0) {
407407
if (cancel_ptr.load(.seq_cst)) {
408408
return error.CallbackListenerStopped;
409409
}
@@ -421,25 +421,39 @@ fn waitForOAuthCallback(
421421
defer conn.stream.close();
422422

423423
var buffer: [8192]u8 = undefined;
424-
const read_size = conn.stream.read(&buffer) catch 0;
425-
if (read_size == 0) {
426-
continue;
427-
}
424+
while (remaining_ms > 0) {
425+
if (cancel_ptr.load(.seq_cst)) {
426+
return error.CallbackListenerStopped;
427+
}
428+
429+
const read_size = conn.stream.read(&buffer) catch |err| switch (err) {
430+
error.WouldBlock => {
431+
std.Thread.sleep(10 * std.time.ns_per_ms);
432+
remaining_ms -= 10;
433+
continue;
434+
},
435+
else => continue :accept_loop,
436+
};
437+
if (read_size == 0) {
438+
continue :accept_loop;
439+
}
440+
441+
const request = buffer[0..read_size];
442+
const maybe_target = extractRequestTarget(request);
428443

429-
const request = buffer[0..read_size];
430-
const maybe_target = extractRequestTarget(request);
444+
if (maybe_target) |target| {
445+
if (std.mem.startsWith(u8, target, "/auth/callback")) {
446+
writeHttpResponse(conn.stream, "200 OK", "<html><body><h3>Login completed. You can return to Codex Account Manager.</h3></body></html>");
447+
return std.fmt.allocPrint(allocator, "http://localhost:1455{s}", .{target});
448+
}
431449

432-
if (maybe_target) |target| {
433-
if (std.mem.startsWith(u8, target, "/auth/callback")) {
434-
writeHttpResponse(conn.stream, "200 OK", "<html><body><h3>Login completed. You can return to Codex Account Manager.</h3></body></html>");
435-
return std.fmt.allocPrint(allocator, "http://localhost:1455{s}", .{target});
450+
writeHttpResponse(conn.stream, "404 Not Found", "<html><body>Not Found</body></html>");
451+
continue :accept_loop;
436452
}
437453

438-
writeHttpResponse(conn.stream, "404 Not Found", "<html><body>Not Found</body></html>");
439-
continue;
454+
writeHttpResponse(conn.stream, "400 Bad Request", "<html><body>Invalid callback request.</body></html>");
455+
continue :accept_loop;
440456
}
441-
442-
writeHttpResponse(conn.stream, "400 Bad Request", "<html><body>Invalid callback request.</body></html>");
443457
}
444458

445459
return error.CallbackListenerTimeout;

0 commit comments

Comments
 (0)