Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
daf4e6a
feat(config): add codex tool mode
Waishnav Jun 21, 2026
ebc2967
feat(tools): add workspace-confined patch engine
Waishnav Jun 21, 2026
0cd25ea
feat(tools): expose apply_patch in codex mode
Waishnav Jun 21, 2026
d0aa115
feat(exec): add resumable process session manager
Waishnav Jun 21, 2026
a440d42
feat(exec): expose exec_command and write_stdin
Waishnav Jun 21, 2026
2889ae9
feat(exec): support optional PTY sessions
Waishnav Jun 21, 2026
eb17836
fix(exec): terminate spawned process groups
Waishnav Jun 21, 2026
c97f41e
docs: document codex mode QA and rollout
Waishnav Jun 21, 2026
e447f7b
fix(config): keep codex tool names stable
Waishnav Jun 21, 2026
d7cac9b
feat(ui): add codex tool card payloads
Waishnav Jun 22, 2026
fcc59a8
feat(ui): render codex tool cards
Waishnav Jun 22, 2026
8d591ee
fix(exec): wait for interrupted process exit
Waishnav Jun 22, 2026
76469ee
fix(exec): wait after process interactions
Waishnav Jun 22, 2026
59d08f3
fix(deps): normalize optional pty lock entry
Waishnav Jun 22, 2026
961abd9
refactor(exec): isolate platform shell selection
Waishnav Jun 22, 2026
7efd7e8
fix(exec): terminate process trees on Windows
Waishnav Jun 22, 2026
bfe41b7
fix(patch): replace existing files on Windows
Waishnav Jun 22, 2026
9437059
test(tools): account for cross-platform semantics
Waishnav Jun 22, 2026
a0193d6
fix(exec): quote Windows commands consistently
Waishnav Jun 22, 2026
b00718a
fix(deps): use portable node-pty prebuilds
Waishnav Jun 22, 2026
8b2c135
test(exec): remove timing-only output assertion
Waishnav Jun 22, 2026
9a3c616
test(exec): decouple interrupt from output timing
Waishnav Jun 22, 2026
fcdb03f
fix(exec): delegate pipe shell quoting to Node
Waishnav Jun 22, 2026
a0361cd
fix(exec): pass raw commands to Windows PTYs
Waishnav Jun 22, 2026
5950b71
test(exec): quote Windows executable paths natively
Waishnav Jun 22, 2026
03198bf
fix(exec): preserve Windows PTY command lines
Waishnav Jun 22, 2026
e76ef80
test(exec): clean up PTYs after assertions
Waishnav Jun 22, 2026
a2cefeb
test(exec): avoid PTY line discipline assumptions
Waishnav Jun 22, 2026
584e7f0
test(exec): use native Windows PTY smoke command
Waishnav Jun 22, 2026
ce9522c
fix(exec): pass raw Windows PTY commands
Waishnav Jun 22, 2026
8db6800
fix(deps): update Windows PTY handle fixes
Waishnav Jun 22, 2026
330e416
fix(exec): run Windows PTYs through temp scripts
Waishnav Jun 22, 2026
ed792a1
fix(exec): guard Windows PTY listener setup
Waishnav Jun 22, 2026
6d02dfc
fix(deps): repair stable node-pty on macOS
Waishnav Jun 22, 2026
1447588
fix(exec): delay Windows PTY command startup
Waishnav Jun 22, 2026
0435473
fix(exec): omit PTY signals on Windows
Waishnav Jun 22, 2026
fee90be
test(exec): allow hosted Windows PTY startup
Waishnav Jun 22, 2026
9057500
fix(exec): exit Windows PTY scripts explicitly
Waishnav Jun 22, 2026
19116da
fix(deps): combine PTY platform repairs
Waishnav Jun 22, 2026
6055a32
fix(exec): use native Windows PTY command lines
Waishnav Jun 22, 2026
20429b4
fix(exec): start Windows PTYs after listeners
Waishnav Jun 22, 2026
2324f2a
fix(exec): fall back from Windows native PTYs
Waishnav Jun 22, 2026
7fa956c
fix(deps): retain stable Unix PTY support
Waishnav Jun 22, 2026
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
14 changes: 14 additions & 0 deletions docs/chatgpt-coding-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ Legacy names are available with `DEVSPACE_TOOL_NAMING=legacy`:

Use `DEVSPACE_TOOL_MODE=full` to restore dedicated search and directory tools.

The experimental Codex-style surface is enabled with
`DEVSPACE_TOOL_MODE=codex`. It exposes:

- `open_workspace`
- `read`
- `apply_patch`
- `exec_command`
- `write_stdin`

In this mode, `write`, `edit`, `bash`, `grep`, `glob`, and `ls` are not
registered. `exec_command` returns a process session ID when a command is still
running after its yield window. Use `write_stdin` to poll it, send input, resize
a PTY, or send Ctrl-C. Set `tty: true` only for commands that need a terminal.

## Show Changes

By default, `DEVSPACE_WIDGETS=full`.
Expand Down
73 changes: 73 additions & 0 deletions docs/codex-tool-mode-qa.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Codex Tool Mode Manual QA

Run these checks against a disposable Git repository inside an allowed DevSpace
root. Keep the DevSpace server logs visible during the test.

## Setup

1. Build the current branch with `npm ci && npm run build`.
2. Start DevSpace with `DEVSPACE_TOOL_MODE=codex devspace serve`.
3. Connect or refresh the DevSpace connector in ChatGPT.
4. Open the disposable repository with `open_workspace`.
5. Confirm the core tools are `open_workspace`, `read`, `apply_patch`,
`exec_command`, and `write_stdin`.
6. Confirm `write`, `edit`, `bash`, `grep`, `glob`, and `ls` are absent.
7. If `DEVSPACE_WIDGETS=changes`, also expect `show_changes`.

## Apply Patch

1. Add a text file containing multiple lines and a blank line.
2. Update two separate regions of that file in one patch.
3. Create a nested file, rename it, and then delete it.
4. Patch an existing CRLF file and verify it remains CRLF.
5. Verify executable permissions survive an update and a move.
6. Try to add `../outside.txt`; confirm the tool rejects the path.
7. Patch through a symlink targeting an external directory; confirm rejection.
8. Submit a hunk whose context is absent; confirm no file from that patch changes.
9. With changes widgets enabled, inspect the aggregate diff.

## Foreground Commands

1. Run `pwd` and confirm it reports the opened workspace.
2. Run a command in a relative `workingDirectory` and confirm the directory.
3. Write to stdout and stderr; confirm both appear.
4. Exit nonzero; confirm `running=false` and the exit code.
5. Use a small output budget on a noisy command; confirm truncation is reported.

## Background Sessions

1. Start a delayed command with a short yield time.
2. Confirm `exec_command` returns `running=true` and a `sessionId`.
3. Poll with empty `chars`; confirm output is not duplicated.
4. Poll until completion; confirm the final exit code and no `sessionId`.
5. Poll the completed session again; confirm it is unknown.
6. Reconnect MCP without restarting DevSpace and confirm polling still works.
7. Restart DevSpace and confirm old process session IDs are invalid.

## Input, Interrupt, And PTY

1. Start a program that reads stdin without a PTY and send it a line.
2. Start a long-running process and send `\u0003`; confirm it stops.
3. Start an interactive program with `tty=true`; confirm it detects a TTY.
4. Resize a PTY from 80x24 to 120x30 and verify the observed dimensions.
5. Omit optional dependencies; normal commands must work and `tty=true` must
return the explicit `node-pty` error.

## Cleanup

1. Start a non-PTY command that creates a long-running child process.
2. Stop DevSpace with SIGINT and verify both shell and child exit.
3. Repeat with a PTY command.
4. Confirm no process remains after server exit.
5. Repeat session cycles and check that memory use does not steadily increase.

## Existing Mode Regression

1. Start without `DEVSPACE_TOOL_MODE`; confirm `minimal` remains the default.
2. Minimal must expose `read`, `write`, `edit`, and `bash`, but not Codex tools
or dedicated search tools.
3. `DEVSPACE_TOOL_MODE=full` must add `grep`, `glob`, and `ls`.
4. With no explicit mode, `DEVSPACE_MINIMAL_TOOLS=1` maps to minimal and `0`
maps to full.
5. Set `DEVSPACE_TOOL_MODE=codex` with `DEVSPACE_MINIMAL_TOOLS=0`; confirm the
explicit Codex mode wins.
15 changes: 13 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,19 @@ MCP clients discover metadata from:

| Value | Behavior |
| --- | --- |
| `minimal` | Default. Disables dedicated search and list tools. Clients use the shell tool with `rg`, `grep`, `find`, `ls`, or `tree` for inspection. |
| `full` | Enables dedicated `grep`, `glob`, and `ls` tools. |
| `minimal` | Default. Exposes `open_workspace`, `read`, `write`, `edit`, and `bash`. Clients use `bash` with tools such as `rg`, `find`, and `ls` for inspection. |
| `full` | Exposes the minimal tools plus dedicated `grep`, `glob`, and `ls` tools. |
| `codex` | Experimental. Exposes `open_workspace`, `read`, `apply_patch`, `exec_command`, and `write_stdin`. Existing mutation and shell tools are hidden. |

`DEVSPACE_MINIMAL_TOOLS` remains a backward-compatible alias when
`DEVSPACE_TOOL_MODE` is unset: `1` selects `minimal` and `0` selects `full`.
The `codex` mode must be selected through `DEVSPACE_TOOL_MODE` and always uses
its fixed short tool names regardless of `DEVSPACE_TOOL_NAMING`.

Codex-mode commands run without a PTY by default. Set `tty: true` on
`exec_command` for interactive terminal programs. PTY support uses the optional
`node-pty` dependency; `write_stdin` can send input, poll output, and resize PTY
sessions.

## Widgets

Expand Down
25 changes: 24 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@
"build": "npm run clean && npm run build:app && tsc -p tsconfig.build.json",
"build:app": "vite build",
"dev": "node scripts/dev-server.mjs",
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
"start": "node dist/cli.js serve",
"test": "tsx src/config.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts",
"test": "tsx src/config.test.ts && tsx src/ui/card-types.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts",
Comment on lines +27 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'node-pty|describe\.skip|it\.skip|test\.skip|skip' src/process-sessions.test.ts

Repository: Waishnav/devspace

Length of output: 155


🏁 Script executed:

#!/bin/bash
# First, check if the test file exists
ls -la src/process-sessions.test.ts 2>&1 || echo "File not found"

Repository: Waishnav/devspace

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Check the structure of src directory to find test files
find src -name "*.test.ts" -type f | head -20

Repository: Waishnav/devspace

Length of output: 397


🏁 Script executed:

#!/bin/bash
# Check package.json for optionalDependencies
cat -n package.json | grep -A 5 "optionalDependencies"

Repository: Waishnav/devspace

Length of output: 242


🏁 Script executed:

#!/bin/bash
# Read the full test file
cat -n src/process-sessions.test.ts

Repository: Waishnav/devspace

Length of output: 6066


🏁 Script executed:

#!/bin/bash
# Find and read the ProcessSessionManager implementation
find src -name "process-sessions.ts" -o -name "process-sessions.js" | head -5

Repository: Waishnav/devspace

Length of output: 179


🏁 Script executed:

#!/bin/bash
# Check how node-pty is imported/required in the codebase
rg -n "require.*node-pty|import.*node-pty|from.*node-pty" src/ --type ts

Repository: Waishnav/devspace

Length of output: 472


🏁 Script executed:

#!/bin/bash
# Read process-sessions.ts around the node-pty handling
sed -n '235,260p' src/process-sessions.ts

Repository: Waishnav/devspace

Length of output: 1165


🏁 Script executed:

#!/bin/bash
# Check if there's try/catch or fallback logic after the error throw in startPty
sed -n '240,280p' src/process-sessions.ts

Repository: Waishnav/devspace

Length of output: 1421


🏁 Script executed:

#!/bin/bash
# Check the start() method to see if it handles PTY errors gracefully
rg -n "async start" src/process-sessions.ts -A 10

Repository: Waishnav/devspace

Length of output: 1029


The test will fail on Unix platforms when node-pty is omitted from optional dependencies.

npm test runs src/process-sessions.test.ts, which includes unconditional PTY tests for Unix systems (lines 137-158). These tests call manager.start({tty: true}), which internally calls startPty(). When node-pty is missing, startPty() throws an error that is not caught by the test. The test must either skip these tests conditionally or mock node-pty when the optional dependency is unavailable, since node-pty is marked as optional in package.json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 27 - 29, The PTY tests in
src/process-sessions.test.ts (lines 137-158) that call manager.start({tty:
true}) will fail when node-pty is not installed, even though it is marked as
optional in package.json. Modify the test file to conditionally handle the
absence of node-pty by either wrapping the affected test cases with a check to
skip them when node-pty is unavailable, or by providing a mock implementation of
node-pty for when the optional dependency is not present. Ensure that startPty()
either skips execution gracefully or has a mocked fallback so the test suite can
pass whether or not node-pty is installed.

"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [],
Expand Down Expand Up @@ -60,5 +61,8 @@
"overrides": {
"protobufjs": "7.6.4",
"ws": "8.21.0"
},
"optionalDependencies": {
"node-pty": "^1.1.0"
}
}
22 changes: 22 additions & 0 deletions scripts/fix-node-pty-permissions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { chmod } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

if (process.platform === "darwin") {
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
for (const architecture of ["arm64", "x64"]) {
const helper = resolve(
projectRoot,
"node_modules",
"node-pty",
"prebuilds",
`darwin-${architecture}`,
"spawn-helper",
);
try {
await chmod(helper, 0o755);
} catch (error) {
if (error.code !== "ENOENT") throw error;
}
}
}
130 changes: 130 additions & 0 deletions src/apply-patch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import assert from "node:assert/strict";
import { chmod, mkdtemp, readFile, stat, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { applyPatch, parsePatch, replaceFile } from "./apply-patch.js";

const root = await mkdtemp(join(tmpdir(), "devspace-apply-patch-"));
const replacement = join(root, "replacement.txt");
const replacementTemporary = join(root, "replacement.tmp");
await writeFile(replacement, "old\n");
await writeFile(replacementTemporary, "new\n");
await replaceFile(replacementTemporary, replacement, true, "win32");
assert.equal(await readFile(replacement, "utf8"), "new\n");
await writeFile(join(root, "alpha.txt"), "one\ntwo\nthree\n");
await writeFile(join(root, "remove.txt"), "remove me\n");
await writeFile(join(root, "windows.txt"), "first\r\nsecond\r\n");

const result = await applyPatch(
root,
`*** Begin Patch
*** Add File: nested/added.txt
+new
+file
*** Update File: alpha.txt
@@
one
-two
+changed
three
*** Update File: windows.txt
@@
first
-second
+updated
*** Delete File: remove.txt
*** End Patch`,
);

assert.deepEqual(result.files, [
{ path: "nested/added.txt", operation: "add" },
{ path: "alpha.txt", operation: "update" },
{ path: "windows.txt", operation: "update" },
{ path: "remove.txt", operation: "delete" },
]);
assert.equal(result.additions, 4);
assert.equal(result.removals, 3);
assert.match(result.patch, /diff --git a\/alpha\.txt b\/alpha\.txt/);
assert.match(result.patch, /-two\n\+changed/);
assert.equal(await readFile(join(root, "nested/added.txt"), "utf8"), "new\nfile\n");
assert.equal(await readFile(join(root, "alpha.txt"), "utf8"), "one\nchanged\nthree\n");
assert.equal(await readFile(join(root, "windows.txt"), "utf8"), "first\r\nupdated\r\n");
await assert.rejects(readFile(join(root, "remove.txt"), "utf8"), /ENOENT/);

if (process.platform !== "win32") await chmod(join(root, "alpha.txt"), 0o755);
const moveResult = await applyPatch(
root,
`*** Begin Patch
*** Update File: alpha.txt
*** Move to: moved/alpha.txt
@@
-one
+ONE
changed
*** End Patch`,
);
assert.deepEqual(moveResult.files, [
{ path: "moved/alpha.txt", previousPath: "alpha.txt", operation: "move" },
]);
assert.equal(await readFile(join(root, "moved/alpha.txt"), "utf8"), "ONE\nchanged\nthree\n");
if (process.platform !== "win32") {
assert.notEqual((await stat(join(root, "moved/alpha.txt"))).mode & 0o111, 0);
}
await assert.rejects(readFile(join(root, "alpha.txt"), "utf8"), /ENOENT/);

await assert.rejects(
applyPatch(
root,
`*** Begin Patch
*** Add File: ../escape.txt
+no
*** End Patch`,
),
/path escapes the workspace/,
);

const outside = await mkdtemp(join(tmpdir(), "devspace-apply-patch-outside-"));
await symlink(outside, join(root, "outside-link"), process.platform === "win32" ? "junction" : "dir");
await assert.rejects(
applyPatch(
root,
`*** Begin Patch
*** Add File: outside-link/escape.txt
+no
*** End Patch`,
),
/path resolves outside the workspace/,
);

await assert.rejects(
applyPatch(
root,
`*** Begin Patch
*** Update File: moved/alpha.txt
@@
-not present
+replacement
*** End Patch`,
),
/could not find hunk context/,
);
assert.equal(await readFile(join(root, "moved/alpha.txt"), "utf8"), "ONE\nchanged\nthree\n");

await assert.rejects(
applyPatch(
root,
`*** Begin Patch
*** Add File: should-not-exist.txt
+staged
*** Update File: moved/alpha.txt
@@
-missing context
+replacement
*** End Patch`,
),
/could not find hunk context/,
);
await assert.rejects(readFile(join(root, "should-not-exist.txt"), "utf8"), /ENOENT/);

assert.throws(() => parsePatch("*** Begin Patch\n*** End Patch"), /contains no file actions/);
assert.throws(() => parsePatch("*** Add File: bad.txt\n+x"), /missing .* marker/);
Loading
Loading