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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
the entries exact. A session restored from its log has only the text the
model saw, so replayed cards degrade to today's plain-text body.

- **`/plan`** enters plan mode directly (`/plan off` leaves it) β€” the same
switch `/mode plan` already threw, one word shorter and matching what Claude
Code users type. A trailing prompt (`/plan do X`) is refused with a pointer
rather than silently dropped, because half-obeying a command teaches the
wrong lesson about what it did.

- **Persistent shells now last a whole CLI session, and `/shells` shows them.**
The registry landed in #273 owned by a single `runAgent` call, which meant a
shell opened in one turn was gone by the next β€” a slower `Bash` with extra
Expand Down
19 changes: 19 additions & 0 deletions apps/cli/src/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,25 @@ describe('built-in command behavior', () => {
expect(ctx.mode).toBe('plan');
});

it('/plan enters plan mode and /plan off leaves it', async () => {
const reg = new CommandRegistry();
const ctx = makeContext();
await reg.match('/plan')!.cmd.run([], ctx);
expect(ctx.mode).toBe('plan');
await reg.match('/plan off')!.cmd.run(['off'], ctx);
expect(ctx.mode).toBe('default');
});

it('/plan refuses a trailing prompt instead of silently dropping it', async () => {
const reg = new CommandRegistry();
const ctx = makeContext();
const out = await reg
.match('/plan refactor the auth flow')!
.cmd.run(['refactor', 'the', 'auth', 'flow'], ctx);
expect(out.join('\n')).toMatch(/takes no prompt/);
expect(ctx.mode).toBe('default'); // unchanged β€” nothing half-happened
});

it('/effort switches when valid', async () => {
const reg = new CommandRegistry();
const ctx = makeContext();
Expand Down
18 changes: 18 additions & 0 deletions apps/cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,23 @@ export const ModeCommand: SlashCommand = {
},
};

export const PlanCommand: SlashCommand = {
name: '/plan',
description: 'Enter plan mode (read-only exploring; same as /mode plan).',
run(args, ctx) {
// `/plan off` reads as the obvious way back out, so honor it rather than
// teaching one more incantation. Anything else after /plan is a mistake β€”
// the prompt itself is typed as a normal message once the mode is set.
if (args.length > 0 && args[0] !== 'off') {
return ['/plan takes no prompt. Enter plan mode first, then type your message.'];
}
const next = args[0] === 'off' ? 'default' : 'plan';
if (ctx.mode === next) return [`Already in ${next} mode.`];
ctx.mode = next;
return [`Mode switched to ${next}.`];
},
};

// Effort tier UI metadata surfaced by `/effort` with no args.
// why: the maxTokens/temperature numbers are NOT defined here β€” they are read
// from EFFORT_PARAMS in @deepcode/core, the single source of truth the REPL and
Expand Down Expand Up @@ -1440,6 +1457,7 @@ export const BUILTIN_COMMANDS: SlashCommand[] = [
StatusCommand,
ModelCommand,
ModeCommand,
PlanCommand,
EffortCommand,
CostCommand,
ContextCommand,
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/lib/slash-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ describe('parseSlash', () => {
expect(parseSlash('/effort max')).toEqual({ kind: 'set-effort', value: 'max' });
});

it('/plan enters plan mode, /plan off leaves, a trailing prompt is refused', () => {
expect(parseSlash('/plan')).toEqual({ kind: 'set-mode', value: 'plan' });
expect(parseSlash('/plan off')).toEqual({ kind: 'set-mode', value: 'default' });
const r = parseSlash('/plan refactor auth');
expect(r?.kind).toBe('error');
expect(r && 'message' in r && r.message).toContain('takes no prompt');
});

it('rejects an invalid argument with the usage line, not silently', () => {
const r = parseSlash('/effort ludicrous');
expect(r?.kind).toBe('error');
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/lib/slash-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const DESKTOP_COMMANDS: SlashCommand[] = [
args: '<name>',
summary: 'Switch approval mode (default, plan, acceptEdits, …)',
},
{ name: '/plan', args: '[off]', summary: 'Enter plan mode (read-only); /plan off leaves it' },
{ name: '/effort', args: '<tier>', summary: 'Switch effort tier (low … max)' },
{ name: '/cost', summary: 'Spend and token usage this conversation' },
{ name: '/context', summary: 'How much of the context window is used' },
Expand Down Expand Up @@ -122,6 +123,16 @@ export function parseSlash(input: string): SlashAction | null {
return (MODES as string[]).includes(arg)
? { kind: 'set-mode', value: arg as AgentMode }
: { kind: 'error', message: `Usage: /mode ${MODES.join(' | ')}` };
case '/plan':
// Same switch /mode plan throws, one word shorter. A trailing prompt is
// refused rather than silently dropped β€” type it as a normal message.
if (arg === '' || arg === 'off') {
return { kind: 'set-mode', value: arg === 'off' ? 'default' : 'plan' };
}
return {
kind: 'error',
message: '/plan takes no prompt β€” enter plan mode, then type your message.',
};
case '/effort':
return (EFFORTS as string[]).includes(arg)
? { kind: 'set-effort', value: arg as Effort }
Expand Down
2 changes: 1 addition & 1 deletion docs/BEHAVIOR_PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Legend: `βœ…` matches Β· `🟑` matches with caveats Β· `πŸ”„` deferred Β· `⚠
| `/batch` | βœ“ | βœ— | πŸ”„ β€” batch-of-prompts not yet wired (use `/background` per prompt) |
| `/tasks` | βœ“ | βœ“ | βœ… β€” lists this session's background tasks; `/tasks <id>` shows one's status + output |
| `/shells` | βœ— | βœ“ | πŸ†• DeepCode-only β€” lists this session's persistent shells; `/shells close <id>` closes one |
| `/plan` | βœ“ | βœ— | πŸ”„ β€” set via `/mode plan` in DeepCode |
| `/plan` | βœ“ | βœ“ | βœ… β€” `/plan` enters plan mode, `/plan off` leaves; `/mode plan` still works |
| `/login` / `/logout` | βœ“ | βœ“ | βœ… β€” /logout clears creds + exits; /login <key> stores a new key (next launch) |
| `/export` | βœ“ | βœ“ | βœ… β€” writes the conversation to a markdown file |
| `/bug` (alias `/feedback`) | βœ“ | βœ“ | βœ… β€” prints a prefilled GitHub issue link (model/mode/effort in the body) |
Expand Down
Loading