Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 2026-10-27

- [cli] `wraithgrid send` marks the pane as having a conversation, as Enter typed in it does, so a restart resumes it (H6 follow-up): a new gui relay action `sent` (no host protocol change; no window open, nothing to mark); docs/TODO.md cleared of six items that had already shipped (#39–#41, file attach, delete chat confirm, sign-in dialog) (files: src/shared/ipc-contract.ts, src/main/{cli,ipc}.ts, src/renderer/app/store.ts, tests/unit/cli.test.ts, tests/e2e/cli.spec.ts, docs/TODO.md)
- [host] Old Rust hosts no longer pile up on ssh machines (#76): each connect touches this version's `~/.wraithgrid/bin/wraithgrid-host-<version>` (its date says when it was last used) and deletes other versions unused for 30 days, so two Wraithgrid versions sharing a machine don't copy theirs over again on every connect; a host still running a deleted one keeps running. `command find`, since a fish config can shadow `find` (this machine's does); tested for real in sh and fish (files: src/main/remote-bin.ts, tests/unit/remote-host.test.ts)
- [host] Rust host phase 4a (#76, ADR 0004 Accepted): terminal panes run in the Rust host by default on Linux, macOS and Windows. `settings.nodeHost` (Settings → Advanced "Use the Node host", off; `WRAITHGRID_NODE_HOST=1` in tests) replaces `rustHost`: every config saved that as false, so it is dropped on load and everyone moves to Rust; a build without the binary runs the Node host. `bun run test:e2e` builds the Rust host first, so e2e runs the default; `rust-host.spec` checks both ways (files: src/shared/{types,schema}.ts, src/main/app.ts, src/renderer/components/SettingsView.tsx, tests/e2e/{rust-host,machines}.spec.ts, package.json, docs/adr/0004-rust-pty-host.md, docs/CONTEXT.md)
- [host] Rust host phase 3b (#76, ADR 0004): Windows builds carry `wraithgrid-host.exe` (`dist:win` builds it). Panes run in a ConPTY of our own (portable-pty quotes every argument, and an npm `.cmd` CLI comes as a ready `cmd.exe /d /s /c "…"` line): file and args quoted the way CreateProcess reads them, a ready line kept as is, stdio cut off from the host's log, the exit code from the process handle (no signals), kill = close the console then `taskkill /T /F` after the grace, as the Node host does. The host serves a named pipe (one instance per client, remote clients refused), checks the lock owner with OpenProcess, gets its token from BCryptGenRandom, and a Ctrl-C hook that can't be set (no console) never stops it; the ssh bridge stays Unix. `host-rs/tests/smoke.rs` starts the built host on every OS, connects over its socket or pipe, runs a pane that prints and one typed into, then shuts it down; CI's Windows job runs it with clippy. Settings → Advanced "Use the Rust host" shows on every OS (files: host-rs/src/{pty,main,server}.rs, host-rs/Cargo.{toml,lock}, host-rs/tests/smoke.rs, src/main/app.ts, src/renderer/components/SettingsView.tsx, package.json, electron-builder.yml, .github/workflows/build.yml, docs/CONTEXT.md)
Expand Down
13 changes: 6 additions & 7 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,10 @@
- CI flakes fixed 2026-10-27 (PR #152): chat.spec.ts:299 (sideways-wheel noise paused follow), chat-history.spec.ts (caret placed a frame late collapsed a select-all). Still seen once locally: responsive.spec.ts:131 (Settings nav heading not hidden in a small window), passed 3/3 on rerun
- Audit (2026-10-26) bugs: all fixed (#135–#138 PR #148, #139 PR #150, #140 PR #151). Not seen live: #138 cancelled pkexec prompt, #140 renderer-crash reattach, codex cancel/stop paths, agy first-start effort
- Audit (2026-10-26) perf: #141 done (PR #149); not measured live yet (a DevTools profile of a long streaming chat before/after would confirm)
- #41 (2026-10-22): agy in the chat UI (`src/main/chat/agy.ts`, `chat: true`); agy 1.4.4 has `-p --input-format/--output-format stream-json`, `--model/--effort/--mode`, `--continue/--conversation`; also set its `resumeArgs`
- #40 (2026-10-22): Codex CLI in the chat UI (`src/main/chat/codex.ts` adapter, `chat: true`; `codex exec --json` vs `app-server` to decide) + codex sign-in dialog (`codex login --device-auth`/`status`; login.ts per-CLI)
- #39 (2026-10-22): chat looks stopped while claude runs background work (release, push, `run_in_background` Bash/Agent); `result` in chat/claude.ts sets busy false. Track background tool_use ids → a separate "Running in background" state in chat + pane/sidebar; capture real stream-json fixture first
- Plan (2026-10-22): attach files from the local machine in chat panes. Today the message box only takes pasted images (Composer `onPaste`; PNG/JPEG/GIF/WebP, max 4, ~5 MB each, `CHAT_IMAGE_*` in shared/chat.ts). To decide: an attach button with a native file picker (main `dialog.showOpenDialog` behind a new IPC channel + zod contract; main reads the file, never trusts a renderer path), drag and drop, which file types (images as image parts; text/code inlined or passed as a path for claude to read; PDFs?), size limits
- Issue (2026-10-22): improve desktop app performance. Needs specifics from the user first: what feels slow (startup, switching projects/panes, typing, long chat threads, many panes, memory use) and on which OS. Then measure before changing (DevTools Performance profile, `process.getProcessMemoryInfo`); overlaps with the NFR-1 8-pane check above
- Issue (2026-10-22): no visible way to delete a chat on the project screen (ProjectView). Open chats have no delete at all; History rows have a trash button (`.pj-del`) but it is invisible until hover (opacity 0, polish.css), and it deletes at once with no confirm. Want: a visible delete for chats (open and History), and clicking it opens a "Delete this chat?" confirm dialog (build on `Dialog.tsx`, like `RemoveWorktreeDialog`) before `deleteHistory` / stopping an open chat. To decide: show the button always or on hover + in a right-click menu; what deleting an open chat does (stop + forget the session?)
- Plan (2026-10-22): sign in from a UI dialog, not a terminal. Today the account Login action opens a terminal pane running claude's own `/login` (store.ts `loginAccount`, `loginFlow` ~640). Want: a Wraithgrid dialog (steps: open browser → paste code if asked → "Signed in as …"), with claude still doing the OAuth itself in the account's config dir (never handle tokens ourselves). To check with the real CLI: is there a non-interactive login command (e.g. `claude auth login`) whose URL/code prompt main can drive over pipes; else drive `/login` in a hidden pty and parse the URL. Then the same for other CLIs (codex/gemini…) or keep the terminal for them
- Herdr parity (released in v1.10.0): verify on Windows (named pipe host, ConPTY in a detached ELECTRON_RUN_AS_NODE process, NSIS update closing the host) and macOS (host from the .app, tray, ⌘Q asks); a real ssh machine for H5; NFR-1: memory/CPU of the host with 8 busy panes (headless xterm per pane)
- Windows: `wraithgrid` CLI prints nothing in a console (GUI-subsystem exe); needs AttachConsole or a `wraithgrid.cmd` shim
- H5 follow-ups: a machine's own `wraithgrid` path (AppImage/macOS remote), remote sign-in state per machine (login banner uses the local flag), remote Mods/Changes panel (local only today)
- H6 follow-up: a prompt sent with `wraithgrid send` does not set `hasConversation` (resume on restore skips it)
- H8 publish (needs your accounts): Homebrew tap repo, winget-pkgs PR, AUR `wraithgrid-bin` push; see packaging/README.md. Run scripts/update-packaging.sh after each release
- Mods as one list (2026-10-13): try Install / Add to all accounts with the real accounts (only the fake CLI and a throwaway config dir so far); confirm the repo link target for in-marketplace mods
- Mods (2026-10-13): try with a real mod in a pane (pane/band drawing, `$.ui.copy`, `$.ui.notify` with a channel set, GPU rendering with many panes)
Expand Down Expand Up @@ -43,6 +36,12 @@ H1–H7 released in v1.10.0; H8 publishing needs the accounts (Now).
## Later
- `agy`/opencode per-account isolation if they gain a config-dir variable

## Done (2026-10-27)
- Stale items cleared: agy and Codex chat (#41, #40), "Running in background" (#39), attach files (chat attach.ts), delete chat with a confirm (DeleteChatDialog), sign in from a dialog (Login dialog) were already shipped
- H6 follow-up: `wraithgrid send` marks the pane as having a conversation (gui `sent` action), so a restart resumes it
- #76: Rust host on macOS (#153), Windows (#154), the default (#155), remote cleanup of unused versions (#156)
- Audit #135–#141 and CI flakes (#148–#152)

## Done (2026-10-24)
- #87 sidebar tree: projects vs chats clearly apart, plus steadier chat follow (PR #90, merged)
- #86 account picker search: flat focus (accent divider, no square ring) (PR #91, merged)
Expand Down
4 changes: 4 additions & 0 deletions src/main/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@ export async function runCli(argv: string[], deps: CliDeps): Promise<number> {
}
// Make sure the writes left before the connection closes.
await c.request({ t: 'list' })
// The window marks the pane as having a conversation, as when Enter is typed in it, so
// a restart resumes it (H6). No window open: nothing to mark, and nothing fails.
if (!p.flags.has('--no-enter'))
await c.request({ t: 'gui', action: { kind: 'sent', paneId: pane.id } }).catch(() => null)
return 0
}
case 'wait': {
Expand Down
2 changes: 1 addition & 1 deletion src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1748,7 +1748,7 @@ export function registerIpc(deps: IpcDeps): IpcHandle {
const parsed = cliActionSchema.safeParse(raw)
if (!parsed.success) return fail('Invalid command')
const action = parsed.data
if (action.cwd) {
if (action.kind !== 'sent' && action.cwd) {
try {
if (!(await stat(resolveUserPath(action.cwd, homeDir))).isDirectory())
return fail(`Not a folder: ${action.cwd}`)
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,12 @@ export function createAppStore({

/** `wraithgrid new|split` (H6): picks the account, workspace and folder, then adds the pane. */
const runCliAction = (action: CliAction): CliActionResult => {
// As if typed: Enter in the pane means it has a conversation to resume after a restart.
if (action.kind === 'sent') {
if (!findPane(get().config, action.paneId)) return { ok: false, error: 'No such pane.' }
get().markConversation(action.paneId)
return { ok: true, paneId: action.paneId }
}
const c = get().config
const byName = <T extends { id: string; name: string }>(
list: T[],
Expand Down
4 changes: 3 additions & 1 deletion src/shared/ipc-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,9 @@ export const cliActionSchema = z.discriminatedUnion('kind', [
cwd: cliPath.optional(),
shell: z.boolean().optional(),
args: cliArgs.optional()
})
}),
/** `wraithgrid send` pressed Enter in this pane: it has a conversation to resume (H6). */
z.object({ kind: z.literal('sent'), paneId: idSchema })
])
export type CliAction = z.infer<typeof cliActionSchema>
export type CliActionResult = { ok: true; paneId: string } | { ok: false; error: string }
Expand Down
8 changes: 8 additions & 0 deletions tests/e2e/cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ test('the wraithgrid command lists, reads, types into, waits on and opens panes'
// Type a prompt; the pane answers; read shows it; wait sees it settle.
await cli('send', 'alpha', 'from the cli')
await expect.poll(() => paneText(win, 'alpha')).toContain('got:fromthecli')
// The pane now has a conversation, as if typed: a restart resumes it (H6).
await expect
.poll(() => {
const cfg = JSON.parse(fs.readFileSync(path.join(userData, 'config.json'), 'utf8'))
return cfg.workspaces[0].panes.find((p: { id: string }) => p.id === 'p-alpha')
?.hasConversation
})
.toBe(true)
expect((await cli('read', 'alpha')).stdout).toContain('got: from the cli')
expect((await cli('wait', 'p-alpha', '--quiet', '300', '--timeout', '20')).stdout.trim()).toBe(
'idle'
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ describe('wraithgrid command', () => {
onMismatch: async () => 'keep',
onGuiAction: async (action) => {
actions.push(action)
if (action.kind === 'sent') return { ok: true, paneId: String(action.paneId) }
lastId = 'p-new'
await gui.create(req('p-new'))
return { ok: true, paneId: 'p-new' }
Expand Down Expand Up @@ -179,8 +180,11 @@ describe('wraithgrid command', () => {
it('types text, then Enter; a line break is the CLI new-line key', async () => {
expect(await run(['send', 'alpha', 'fix', 'the', 'bug'])).toBe(0)
expect(ptys.get('p1')!.written).toEqual(['fix the bug', '\r'])
// The window hears of the Enter, so the pane resumes its conversation after a restart (H6).
expect(actions).toEqual([{ kind: 'sent', paneId: 'p1' }])
expect(await run(['send', 'p1', 'one\ntwo', '--no-enter'])).toBe(0)
expect(ptys.get('p1')!.written.slice(2)).toEqual(['one\x1b\rtwo'])
expect(actions).toHaveLength(1)
// gemini has no new-line key: the text goes as typed.
expect(await run(['send', 'beta', 'a\nb', '--no-enter'])).toBe(0)
expect(ptys.get('p2')!.written).toEqual(['a\nb'])
Expand Down
Loading