Skip to content
Open
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
22 changes: 11 additions & 11 deletions docs/chatgpt-coding-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ The Claude surface exposes these tool names:
- `write`
- `edit`
- `bash`
- `show_changes`

DevSpace uses the Codex-style surface by default. It exposes:

Expand All @@ -165,6 +166,7 @@ DevSpace uses the Codex-style surface by default. It exposes:
- `apply_patch`
- `exec_command`
- `write_stdin`
- `show_changes`

In this mode, `write`, `edit`, and `bash` are not registered. `exec_command`
returns a process session ID when a command is still
Expand All @@ -179,18 +181,16 @@ the configured shell tool with command-line tools such as `rg`, `find`, and

## Show Changes

By default, `DEVSPACE_WIDGETS=full`.
DevSpace exposes `show_changes` in both tool modes and attaches widget UI only
to `open_workspace` and `show_changes`. Reads, edits, and commands return normal
MCP results without creating an iframe for each call. Set `ui.enabled` to
`false` in `~/.devspace/config.json` to disable UI metadata while keeping the
aggregate review tool available.
Comment thread
Waishnav marked this conversation as resolved.

In that mode, DevSpace attaches widget UI to the exposed workspace, file, edit,
and shell tools. The aggregate `show_changes` tool is not exposed by default.

Use `DEVSPACE_WIDGETS=off` to disable widget UI, or `DEVSPACE_WIDGETS=changes`
to expose the aggregate show-changes flow.

When `show_changes` is exposed, call it exactly once after the final file
modification in any turn that changes files. It shows the combined changes for
that turn and advances the review point automatically. Reusing a workspace does
not change this workflow.
Call `show_changes` exactly once after the final file modification in any turn
that changes files. It shows the combined changes for that turn and advances
the review point automatically. Reusing a workspace does not change this
workflow.

## Shell Use

Expand Down
22 changes: 14 additions & 8 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,22 @@ Codex-mode commands run without a PTY by default. Set `tty: true` on
`node-pty` dependency; `write_stdin` can send input, poll output, and resize PTY
sessions.

## Widgets
## UI

`DEVSPACE_WIDGETS` controls ChatGPT Apps iframe usage.
DevSpace attaches ChatGPT Apps UI metadata only to `open_workspace` and
`show_changes`. This avoids creating an iframe for every read, edit, or command
tool call. The aggregate `show_changes` tool remains available to every MCP
host, including hosts that ignore UI metadata.

| Value | Behavior |
| --- | --- |
| `full` | Default. Widget UI is attached to exposed workspace, file, edit, and shell tools. |
| `changes` | Enables the aggregate `show_changes` tool and attaches widget UI to `open_workspace` and `show_changes`. |
| `off` | Disables widget UI. |
UI is enabled by default. Disable it without removing `show_changes`:

```json
{
"ui": {
"enabled": false
}
}
```

## Skills

Expand Down Expand Up @@ -259,7 +266,6 @@ DEVSPACE_ALLOWED_ROOTS="$HOME/personal,$HOME/work" \
DEVSPACE_PUBLIC_BASE_URL="https://devspace.example.com" \
DEVSPACE_WORKTREE_ROOT="$HOME/.devspace/worktrees" \
DEVSPACE_ARTIFACTS="1" \
DEVSPACE_WIDGETS="full" \
npx @waishnav/devspace serve
```

Expand Down
14 changes: 6 additions & 8 deletions docs/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,10 @@ If a skill appears in `open_workspace`, the model must read that skill's

## Review Card Does Not Appear

Per-tool widget cards are enabled by default with:
DevSpace attaches widget UI only to `open_workspace` and `show_changes`.
Ordinary reads, edits, and commands intentionally render as normal tool results
to avoid one iframe per call. Plain MCP clients may ignore ChatGPT Apps widget
metadata and only show text results; `show_changes` remains available there.

```bash
DEVSPACE_WIDGETS=full
```

The aggregate `show_changes` tool is only exposed with
`DEVSPACE_WIDGETS=changes`. Plain MCP clients may ignore ChatGPT Apps widget
metadata and only show text results.
If both cards are missing in ChatGPT, confirm that `ui.enabled` is not `false`
in `~/.devspace/config.json` and reconnect the MCP server.
19 changes: 3 additions & 16 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ const baseEnv = {
DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough",
};

assert.equal(loadConfig(baseEnv).widgets, "full");
assert.equal(loadConfig({ ...baseEnv, DEVSPACE_WIDGETS: "changes" }).widgets, "changes");
assert.equal(loadConfig({ ...baseEnv, DEVSPACE_WIDGETS: "full" }).widgets, "full");
assert.equal(loadConfig({ ...baseEnv, DEVSPACE_WIDGETS: "off" }).widgets, "off");
assert.equal(loadConfig(baseEnv).uiEnabled, true);
assert.equal(loadConfig(baseEnv).toolMode, "codex");
assert.equal(loadConfig(baseEnv).skillsEnabled, true);
assert.equal(loadConfig(baseEnv).devspaceSkillsDir, join(emptyConfigDir, "skills"));
Expand All @@ -33,18 +30,6 @@ assert.deepEqual(loadConfig({ ...baseEnv, DEVSPACE_SUBAGENTS: "1" }).subagents,
enabled: true,
providers: [],
});
assert.throws(
() => loadConfig({ ...baseEnv, DEVSPACE_WIDGETS: "invalid" }),
/Invalid DEVSPACE_WIDGETS: invalid/,
);
assert.throws(
() => loadConfig({ ...baseEnv, DEVSPACE_WIDGETS: "minimal" }),
/Invalid DEVSPACE_WIDGETS: minimal/,
);
assert.throws(
() => loadConfig({ ...baseEnv, DEVSPACE_WIDGETS: "write-only" }),
/Invalid DEVSPACE_WIDGETS: write-only/,
);
assert.deepEqual(loadConfig(baseEnv).logging, {
level: "info",
format: "json",
Expand Down Expand Up @@ -154,6 +139,7 @@ writeFileSync(
artifactsEnabled: true,
artifactMaxFileBytes: 321,
tools: { mode: "claude" },
ui: { enabled: false },
}),
);
writeFileSync(
Expand All @@ -172,6 +158,7 @@ assert.equal(fileConfig.subagents.providers.length, 7);
assert.equal(fileConfig.artifactsEnabled, true);
assert.equal(fileConfig.artifactMaxFileBytes, 321);
assert.equal(fileConfig.toolMode, "claude");
assert.equal(fileConfig.uiEnabled, false);
assert.deepEqual(fileConfig.allowedHosts, [
"localhost",
"127.0.0.1",
Expand Down
12 changes: 2 additions & 10 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { devspaceAgentsDir, devspaceSkillsDir, loadDevspaceFiles } from "./user-
import { resolveSubagentsConfig, type SubagentsConfig } from "./local-agent-config.js";

export type ToolMode = "claude" | "codex";
export type WidgetMode = "off" | "changes" | "full";
const DEFAULT_OAUTH_ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
const DEFAULT_OAUTH_REFRESH_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
const DEFAULT_ARTIFACT_MAX_FILE_BYTES = 100 * 1024 * 1024;
Expand All @@ -20,7 +19,7 @@ export interface ServerConfig {
allowedHosts: string[];
publicBaseUrl: string;
toolMode: ToolMode;
widgets: WidgetMode;
uiEnabled: boolean;
stateDir: string;
worktreeRoot: string;
artifactsEnabled: boolean;
Expand Down Expand Up @@ -145,13 +144,6 @@ function parseLoggingConfig(env: NodeJS.ProcessEnv): LoggingConfig {
};
}

function parseWidgetMode(value: string | undefined): WidgetMode {
if (!value || value === "full") return "full";
if (value === "off" || value === "changes") return value;

throw new Error(`Invalid DEVSPACE_WIDGETS: ${value}`);
}

function parseRequiredSecret(value: string | undefined, name: string): string {
const secret = value?.trim();
if (!secret) {
Expand Down Expand Up @@ -221,7 +213,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {
allowedHosts: parseAllowedHosts(env.DEVSPACE_ALLOWED_HOSTS, derivedAllowedHosts),
publicBaseUrl,
toolMode: files.config.tools?.mode ?? "codex",
widgets: parseWidgetMode(env.DEVSPACE_WIDGETS),
uiEnabled: files.config.ui?.enabled ?? true,
Comment thread
Waishnav marked this conversation as resolved.
stateDir: resolve(expandHomePath(env.DEVSPACE_STATE_DIR ?? files.config.stateDir ?? defaultStateDir())),
worktreeRoot: resolve(expandHomePath(env.DEVSPACE_WORKTREE_ROOT ?? files.config.worktreeRoot ?? defaultWorktreeRoot())),
artifactsEnabled:
Expand Down
18 changes: 18 additions & 0 deletions src/review-checkpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ test("a clean workspace reports no changes from the last-shown checkpoint", asyn
assert.match(clean.result, /No changes since last shown changes/);
});

test("initialization reports whether aggregate review is available", async (t) => {
const gitRoot = await committedRepository(t);
const plainRoot = await mkdtemp(join(tmpdir(), "devspace-review-plain-test-"));
t.after(() => rm(plainRoot, { recursive: true, force: true }));
const manager = createReviewCheckpointManager();

assert.deepEqual(
await manager.initializeWorkspace({ workspaceId: "ws_git", root: gitRoot }),
{ available: true },
);
const unavailable = await manager.initializeWorkspace({
workspaceId: "ws_plain",
root: plainRoot,
});
assert.equal(unavailable.available, false);
if (!unavailable.available) assert.match(unavailable.reason, /git repository/i);
});

test("show_changes reports and advances the last-shown checkpoint", async (t) => {
const root = await committedRepository(t);
const manager = createReviewCheckpointManager();
Expand Down
23 changes: 19 additions & 4 deletions src/review-checkpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export interface ReviewChangesResult {
patch: string;
}

export type ReviewAvailability =
| { available: true }
| { available: false; reason: string };

interface WorkspaceReviewState {
root: string;
gitRoot?: string;
Expand All @@ -37,7 +41,7 @@ interface WorkspaceReviewState {
}

export interface ReviewCheckpointManager {
initializeWorkspace(input: { workspaceId: string; root: string }): Promise<void>;
initializeWorkspace(input: { workspaceId: string; root: string }): Promise<ReviewAvailability>;
reviewChanges(input: {
workspaceId: string;
root: string;
Expand All @@ -57,14 +61,15 @@ export function createReviewCheckpointManager(): ReviewCheckpointManager {
const existingState = states.get(workspaceId);
assertWorkspaceRoot(existingState, workspaceId, root);
if (existingState?.root === root && existingState.gitRoot !== undefined) {
return;
return reviewAvailability(existingState);
}

const pending = initializations.get(workspaceId);
if (pending) {
await pending;
assertWorkspaceRoot(states.get(workspaceId), workspaceId, root);
return;
const initializedState = states.get(workspaceId);
assertWorkspaceRoot(initializedState, workspaceId, root);
return reviewAvailability(initializedState);
}

const initialize = initializeWorkspaceState(states, workspaceId, root);
Expand All @@ -76,6 +81,7 @@ export function createReviewCheckpointManager(): ReviewCheckpointManager {
initializations.delete(workspaceId);
}
}
return reviewAvailability(states.get(workspaceId));
},

async reviewChanges({ workspaceId, root, since = "last_shown", markReviewed = true }) {
Expand Down Expand Up @@ -193,6 +199,15 @@ async function initializeWorkspaceState(
}
}

function reviewAvailability(state: WorkspaceReviewState | undefined): ReviewAvailability {
return state?.gitRoot
? { available: true }
: {
available: false,
reason: state?.diagnostic ?? "show_changes is unavailable for this workspace.",
};
}

function isReadyState(state: WorkspaceReviewState | undefined): boolean {
return state?.gitRoot !== undefined;
}
Expand Down
Loading
Loading