Skip to content
Draft
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
26 changes: 26 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ npx @waishnav/devspace config set publicBaseUrl https://devspace.example.com
| `DEVSPACE_OAUTH_OWNER_TOKEN` | Owner password for OAuth approval. Must be at least 16 characters. |
| `DEVSPACE_WORKTREE_ROOT` | Directory for managed Git worktrees. Defaults to `~/.devspace/worktrees`. |
| `DEVSPACE_STATE_DIR` | Directory for SQLite state. Defaults to `~/.local/share/devspace`. |
| `DEVSPACE_INLINE_OUTPUT_CHARACTERS` | Maximum characters returned inline for text-heavy tool output. Defaults to `12000`; larger results use a bounded head/tail preview. |

## OAuth

Expand Down Expand Up @@ -77,6 +78,31 @@ 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.

## Inline Output Limits

Text-heavy tools such as `read`, `grep`, `glob`, `ls`, `bash`, `exec_command`, and
`write_stdin` return one bounded model-readable preview instead of copying the complete
output into every MCP response channel.

Configure the ceiling through the environment:

```bash
DEVSPACE_INLINE_OUTPUT_CHARACTERS=12000 npx @waishnav/devspace serve
```

or persist it in `~/.devspace/config.json`:

```json
{
"inlineOutputCharacters": 12000
}
```

The result metadata reports the original character and line counts, the inline preview
size, and whether characters were omitted. Narrow the command or request a smaller file
range when more detail is needed. When widgets are set to `changes` or `off`, text-heavy
tools also omit their card payload instead of sending an unused second preview to the UI.

## Widgets

`DEVSPACE_WIDGETS` controls ChatGPT Apps iframe usage.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"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/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.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 && tsx src/cli.test.ts",
"test": "tsx src/config.test.ts && tsx src/tool-output.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.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 && tsx src/cli.test.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [],
Expand Down
3 changes: 3 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ async function runInit({ force }: { force: boolean }): Promise<void> {
port,
allowedRoots,
publicBaseUrl,
inlineOutputCharacters: files.config.inlineOutputCharacters,
subagents: resolveSubagentsFlag(files.config),
};
const auth = {
Expand Down Expand Up @@ -219,6 +220,7 @@ async function serve(): Promise<void> {
console.log(`public base url: ${config.publicBaseUrl}`);
console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
console.log(`allowed hosts: ${config.allowedHosts.join(", ")}`);
console.log(`inline output characters: ${config.inlineOutputCharacters}`);
if (config.allowedHosts.includes("*")) {
console.warn("warning: Host header allowlist is disabled because DEVSPACE_ALLOWED_HOSTS=*");
}
Expand Down Expand Up @@ -264,6 +266,7 @@ async function runDoctor(): Promise<void> {
console.log(`Public MCP URL: ${new URL("/mcp", config.publicBaseUrl).toString()}`);
console.log(`Allowed roots: ${config.allowedRoots.join(", ")}`);
console.log(`Allowed hosts: ${config.allowedHosts.join(", ")}`);
console.log(`Inline output characters: ${config.inlineOutputCharacters}`);
} catch (error) {
console.log(`Config status: ${error instanceof Error ? error.message : String(error)}`);
}
Expand Down
11 changes: 11 additions & 0 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ 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).inlineOutputCharacters, 12_000);
assert.equal(
loadConfig({ ...baseEnv, DEVSPACE_INLINE_OUTPUT_CHARACTERS: "4096" }).inlineOutputCharacters,
4096,
);
assert.equal(loadConfig(baseEnv).toolMode, "minimal");
assert.equal(loadConfig({ ...baseEnv, DEVSPACE_TOOL_MODE: "minimal" }).toolMode, "minimal");
assert.equal(loadConfig({ ...baseEnv, DEVSPACE_TOOL_MODE: "full" }).toolMode, "full");
Expand Down Expand Up @@ -60,6 +65,10 @@ assert.throws(
() => loadConfig({ ...baseEnv, DEVSPACE_TOOL_MODE: "invalid" }),
/Invalid DEVSPACE_TOOL_MODE: invalid/,
);
assert.throws(
() => loadConfig({ ...baseEnv, DEVSPACE_INLINE_OUTPUT_CHARACTERS: "0" }),
/Invalid DEVSPACE_INLINE_OUTPUT_CHARACTERS: 0/,
);

assert.deepEqual(loadConfig(baseEnv).logging, {
level: "info",
Expand Down Expand Up @@ -162,6 +171,7 @@ writeFileSync(
port: 8787,
allowedRoots: [process.cwd()],
publicBaseUrl: "https://devspace.example.com",
inlineOutputCharacters: 8192,
subagents: true,
}),
);
Expand All @@ -176,6 +186,7 @@ const fileConfig = loadConfig({ DEVSPACE_CONFIG_DIR: configDir });
assert.equal(fileConfig.port, 8787);
assert.equal(fileConfig.oauth.ownerToken, "persisted-owner-token-long-enough");
assert.equal(fileConfig.publicBaseUrl, "https://devspace.example.com");
assert.equal(fileConfig.inlineOutputCharacters, 8192);
assert.equal(fileConfig.subagents, true);
assert.deepEqual(fileConfig.allowedHosts, [
"localhost",
Expand Down
11 changes: 9 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { join, resolve } from "node:path";
import { expandHomePath } from "./roots.js";
import type { LoggingConfig, LogFormat, LogLevel } from "./logger.js";
import type { OAuthConfig } from "./oauth-provider.js";
import { DEFAULT_INLINE_OUTPUT_CHARACTERS } from "./tool-output.js";
import { devspaceAgentsDir, devspaceSkillsDir, loadDevspaceFiles } from "./user-config.js";

export type ToolMode = "minimal" | "full" | "codex";
Expand All @@ -19,6 +20,7 @@ export interface ServerConfig {
publicBaseUrl: string;
toolMode: ToolMode;
widgets: WidgetMode;
inlineOutputCharacters: number;
stateDir: string;
worktreeRoot: string;
skillsEnabled: boolean;
Expand Down Expand Up @@ -124,8 +126,8 @@ function parseStringList(value: string | undefined, fallback: string[]): string[
return entries && entries.length > 0 ? entries : fallback;
}

function parsePositiveInteger(value: string | undefined, fallback: number, name: string): number {
if (!value) return fallback;
function parsePositiveInteger(value: string | number | undefined, fallback: number, name: string): number {
if (value === undefined || value === "") return fallback;

const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 1) {
Expand Down Expand Up @@ -224,6 +226,11 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {
publicBaseUrl,
toolMode: parseToolMode(env),
widgets: parseWidgetMode(env.DEVSPACE_WIDGETS),
inlineOutputCharacters: parsePositiveInteger(
env.DEVSPACE_INLINE_OUTPUT_CHARACTERS ?? files.config.inlineOutputCharacters,
DEFAULT_INLINE_OUTPUT_CHARACTERS,
"DEVSPACE_INLINE_OUTPUT_CHARACTERS",
),
stateDir: resolve(expandHomePath(env.DEVSPACE_STATE_DIR ?? files.config.stateDir ?? defaultStateDir())),
worktreeRoot: resolve(expandHomePath(env.DEVSPACE_WORKTREE_ROOT ?? files.config.worktreeRoot ?? defaultWorktreeRoot())),
skillsEnabled: env.DEVSPACE_SKILLS === undefined ? true : parseBoolean(env.DEVSPACE_SKILLS),
Expand Down
Loading