-
-
Notifications
You must be signed in to change notification settings - Fork 240
feat: add Codex-style tool mode #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Waishnav
wants to merge
43
commits into
main
Choose a base branch
from
feat/codex-tool-mode
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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 ebc2967
feat(tools): add workspace-confined patch engine
Waishnav 0cd25ea
feat(tools): expose apply_patch in codex mode
Waishnav d0aa115
feat(exec): add resumable process session manager
Waishnav a440d42
feat(exec): expose exec_command and write_stdin
Waishnav 2889ae9
feat(exec): support optional PTY sessions
Waishnav eb17836
fix(exec): terminate spawned process groups
Waishnav c97f41e
docs: document codex mode QA and rollout
Waishnav e447f7b
fix(config): keep codex tool names stable
Waishnav d7cac9b
feat(ui): add codex tool card payloads
Waishnav fcc59a8
feat(ui): render codex tool cards
Waishnav 8d591ee
fix(exec): wait for interrupted process exit
Waishnav 76469ee
fix(exec): wait after process interactions
Waishnav 59d08f3
fix(deps): normalize optional pty lock entry
Waishnav 961abd9
refactor(exec): isolate platform shell selection
Waishnav 7efd7e8
fix(exec): terminate process trees on Windows
Waishnav bfe41b7
fix(patch): replace existing files on Windows
Waishnav 9437059
test(tools): account for cross-platform semantics
Waishnav a0193d6
fix(exec): quote Windows commands consistently
Waishnav b00718a
fix(deps): use portable node-pty prebuilds
Waishnav 8b2c135
test(exec): remove timing-only output assertion
Waishnav 9a3c616
test(exec): decouple interrupt from output timing
Waishnav fcdb03f
fix(exec): delegate pipe shell quoting to Node
Waishnav a0361cd
fix(exec): pass raw commands to Windows PTYs
Waishnav 5950b71
test(exec): quote Windows executable paths natively
Waishnav 03198bf
fix(exec): preserve Windows PTY command lines
Waishnav e76ef80
test(exec): clean up PTYs after assertions
Waishnav a2cefeb
test(exec): avoid PTY line discipline assumptions
Waishnav 584e7f0
test(exec): use native Windows PTY smoke command
Waishnav ce9522c
fix(exec): pass raw Windows PTY commands
Waishnav 8db6800
fix(deps): update Windows PTY handle fixes
Waishnav 330e416
fix(exec): run Windows PTYs through temp scripts
Waishnav ed792a1
fix(exec): guard Windows PTY listener setup
Waishnav 6d02dfc
fix(deps): repair stable node-pty on macOS
Waishnav 1447588
fix(exec): delay Windows PTY command startup
Waishnav 0435473
fix(exec): omit PTY signals on Windows
Waishnav fee90be
test(exec): allow hosted Windows PTY startup
Waishnav 9057500
fix(exec): exit Windows PTY scripts explicitly
Waishnav 19116da
fix(deps): combine PTY platform repairs
Waishnav 6055a32
fix(exec): use native Windows PTY command lines
Waishnav 20429b4
fix(exec): start Windows PTYs after listeners
Waishnav 2324f2a
fix(exec): fall back from Windows native PTYs
Waishnav 7fa956c
fix(deps): retain stable Unix PTY support
Waishnav File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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:
Repository: Waishnav/devspace
Length of output: 155
🏁 Script executed:
Repository: Waishnav/devspace
Length of output: 233
🏁 Script executed:
Repository: Waishnav/devspace
Length of output: 397
🏁 Script executed:
Repository: Waishnav/devspace
Length of output: 242
🏁 Script executed:
Repository: Waishnav/devspace
Length of output: 6066
🏁 Script executed:
Repository: Waishnav/devspace
Length of output: 179
🏁 Script executed:
Repository: Waishnav/devspace
Length of output: 472
🏁 Script executed:
Repository: Waishnav/devspace
Length of output: 1165
🏁 Script executed:
Repository: Waishnav/devspace
Length of output: 1421
🏁 Script executed:
Repository: Waishnav/devspace
Length of output: 1029
The test will fail on Unix platforms when
node-ptyis omitted from optional dependencies.npm testrunssrc/process-sessions.test.ts, which includes unconditional PTY tests for Unix systems (lines 137-158). These tests callmanager.start({tty: true}), which internally callsstartPty(). Whennode-ptyis missing,startPty()throws an error that is not caught by the test. The test must either skip these tests conditionally or mocknode-ptywhen the optional dependency is unavailable, sincenode-ptyis marked as optional inpackage.json.🤖 Prompt for AI Agents