Skip to content

Commit a3e44a0

Browse files
committed
Merge branch 'slash-command-in-vscode-extension'
2 parents 9c233dd + ae6bf40 commit a3e44a0

16 files changed

Lines changed: 1384 additions & 294 deletions

‎packages/cli/src/ui/core/prompt-buffer.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getSlashToken } from "@vegamo/deepcode-core/common/slash-commands";
2+
13
export type PromptBufferState = {
24
text: string;
35
cursor: number;
@@ -154,11 +156,7 @@ export function isEmpty(state: PromptBufferState): boolean {
154156
}
155157

156158
export function getCurrentSlashToken(state: PromptBufferState): string | null {
157-
const text = state.text;
158-
if (text.length === 0 || !text.startsWith("/")) {
159-
return null;
160-
}
161-
return text;
159+
return getSlashToken(state.text);
162160
}
163161

164162
/**
Lines changed: 1 addition & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -1,206 +1 @@
1-
import type { SkillInfo } from "@vegamo/deepcode-core";
2-
3-
export type SlashCommandKind =
4-
| "skill"
5-
| "skills"
6-
| "model"
7-
| "permissions"
8-
| "plan"
9-
| "new"
10-
| "init"
11-
| "resume"
12-
| "fork"
13-
| "continue"
14-
| "compact"
15-
| "undo"
16-
| "mcp"
17-
| "raw"
18-
| "exit";
19-
20-
export type SlashCommandItem = {
21-
kind: SlashCommandKind;
22-
name: string;
23-
label: string;
24-
description: string;
25-
skill?: SkillInfo;
26-
args?: string[];
27-
};
28-
29-
export const BUILTIN_SLASH_COMMANDS: SlashCommandItem[] = [
30-
{
31-
kind: "skills",
32-
name: "skills",
33-
label: "/skills",
34-
description: "List available skills",
35-
},
36-
{
37-
kind: "model",
38-
name: "model",
39-
label: "/model",
40-
description: "Select model, thinking mode and effort control",
41-
},
42-
{
43-
kind: "permissions",
44-
name: "permissions",
45-
label: "/permissions",
46-
description: "Configure tool permissions",
47-
},
48-
{
49-
kind: "plan",
50-
name: "plan",
51-
label: "/plan",
52-
description: "Switch the input to Plan Mode",
53-
},
54-
{
55-
kind: "new",
56-
name: "new",
57-
label: "/new",
58-
description: "Start a fresh conversation",
59-
},
60-
{
61-
kind: "init",
62-
name: "init",
63-
label: "/init",
64-
description: "Initialize an AGENTS.md file with instructions for LLM",
65-
},
66-
{
67-
kind: "resume",
68-
name: "resume",
69-
label: "/resume",
70-
description: "Pick a previous conversation to continue",
71-
},
72-
{
73-
kind: "fork",
74-
name: "fork",
75-
label: "/fork",
76-
description: "Fork the current conversation",
77-
},
78-
{
79-
kind: "continue",
80-
name: "continue",
81-
label: "/continue",
82-
description: "Continue the active conversation or pick one to resume",
83-
},
84-
{
85-
kind: "compact",
86-
name: "compact",
87-
label: "/compact",
88-
description: "Compact the current conversation",
89-
},
90-
{
91-
kind: "undo",
92-
name: "undo",
93-
label: "/undo",
94-
description: "Restore code and/or conversation to a previous point",
95-
},
96-
{
97-
kind: "mcp",
98-
name: "mcp",
99-
label: "/mcp",
100-
description: "Show MCP server status and available tools",
101-
},
102-
{
103-
kind: "raw",
104-
name: "raw",
105-
label: "/raw",
106-
args: ["lite", "normal", "raw-scrollback"],
107-
description: "Toggle display mode for viewing or collapsing reasoning content",
108-
},
109-
{
110-
kind: "exit",
111-
name: "exit",
112-
label: "/exit",
113-
description: "Quit Deep Code CLI",
114-
},
115-
];
116-
117-
export function buildSlashCommands(skills: SkillInfo[]): SlashCommandItem[] {
118-
const skillItems: SlashCommandItem[] = skills.map((skill) => ({
119-
kind: "skill",
120-
name: skill.name,
121-
label: `/${skill.name}`,
122-
description: skill.description || "(no description)",
123-
skill,
124-
}));
125-
return [...skillItems, ...BUILTIN_SLASH_COMMANDS];
126-
}
127-
128-
export function filterSlashCommands(items: SlashCommandItem[], token: string): SlashCommandItem[] {
129-
if (!token.startsWith("/")) {
130-
return [];
131-
}
132-
const query = token.slice(1).toLowerCase();
133-
if (!query) {
134-
return items;
135-
}
136-
const prefixMatches: SlashCommandItem[] = [];
137-
const otherMatches: SlashCommandItem[] = [];
138-
for (const item of items) {
139-
const name = item.name.toLowerCase();
140-
if (name.startsWith(query)) {
141-
prefixMatches.push(item);
142-
} else if (name.includes(query)) {
143-
otherMatches.push(item);
144-
}
145-
}
146-
return [...prefixMatches, ...otherMatches];
147-
}
148-
149-
export function findExactSlashCommand(items: SlashCommandItem[], token: string): SlashCommandItem | null {
150-
if (!token.startsWith("/")) {
151-
return null;
152-
}
153-
const query = token.slice(1);
154-
const matches = items.filter((item) => item.name === query);
155-
return matches.find((item) => item.kind !== "skill") ?? matches[0] ?? null;
156-
}
157-
158-
export type SlashSubmission =
159-
| { kind: "command"; item: SlashCommandItem }
160-
| { kind: "plan"; item: SlashCommandItem; text: string }
161-
| { kind: "raw"; item: SlashCommandItem; mode: string }
162-
| { kind: "argument-error"; item: SlashCommandItem; argument: string };
163-
164-
/**
165-
* Classify a submitted slash input without touching UI state. A command alone
166-
* runs as before; `/plan <text>` is a one-shot plan-mode prompt; `/raw <mode>`
167-
* selects that mode and reports an unknown mode. Other commands keep their
168-
* existing behavior and ignore trailing text.
169-
*/
170-
export function resolveSlashSubmission(items: SlashCommandItem[], text: string): SlashSubmission | null {
171-
const trimmed = text.trim();
172-
if (!trimmed.startsWith("/")) {
173-
return null;
174-
}
175-
const match = /^(\S+)([\s\S]*)$/.exec(trimmed);
176-
if (!match) {
177-
return null;
178-
}
179-
const item = findExactSlashCommand(items, match[1]);
180-
if (!item) {
181-
return null;
182-
}
183-
const rest = match[2].trim();
184-
if (!rest) {
185-
return { kind: "command", item };
186-
}
187-
if (item.kind === "plan") {
188-
return { kind: "plan", item, text: rest };
189-
}
190-
if (item.kind === "raw") {
191-
if (item.args?.includes(rest)) {
192-
return { kind: "raw", item, mode: rest };
193-
}
194-
return { kind: "argument-error", item, argument: rest };
195-
}
196-
// Other commands keep their existing behavior and ignore trailing text.
197-
return { kind: "command", item };
198-
}
199-
200-
export function formatSlashCommandDescription(description: string): string {
201-
return (description || "(no description)").trim().replace(/\s+/g, " ");
202-
}
203-
204-
export function formatSlashCommandLabel(item: SlashCommandItem): string {
205-
return item.kind === "skill" && item.skill?.isLoaded ? `${item.label} ✓` : item.label;
206-
}
1+
export * from "@vegamo/deepcode-core/common/slash-commands";

‎packages/cli/src/ui/views/App.tsx‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getCompletedProposedPlan } from "@vegamo/deepcode-core/common/plan-implementation";
12
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
23
import { Box, Static, Text, useApp, useStdout, useWindowSize } from "ink";
34
import chalk from "chalk";
@@ -24,7 +25,6 @@ import {
2425
import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPrompt";
2526
import {
2627
PlanImplementationPrompt,
27-
extractProposedPlan,
2828
getClearContextImplementationPrompt,
2929
getImplementationPrompt,
3030
type PlanImplementationChoice,
@@ -525,10 +525,11 @@ function App({
525525
await refreshSkills();
526526
refreshSessionsList();
527527
const completedSession = sessionManager.getSession(sessionManager.getActiveSessionId() ?? "");
528-
const proposedPlan =
529-
prompt.planMode && completedSession?.status === "completed"
530-
? extractProposedPlan(completedSession.assistantReply)
531-
: null;
528+
const proposedPlan = getCompletedProposedPlan(
529+
prompt.planMode === true,
530+
completedSession?.status,
531+
completedSession?.assistantReply ?? null
532+
);
532533
setPendingPlanImplementation(proposedPlan);
533534
} catch (error) {
534535
const message = error instanceof Error ? error.message : String(error);

‎packages/cli/src/ui/views/PlanImplementationPrompt.tsx‎

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,20 @@ import React, { useEffect, useState } from "react";
22
import { Box, Text } from "ink";
33
import { useTerminalInput } from "../hooks";
44
import type { InputKey } from "../hooks";
5+
import { PLAN_IMPLEMENTATION_CHOICES as CHOICES } from "@vegamo/deepcode-core/common/plan-implementation";
6+
import type { PlanImplementationChoice } from "@vegamo/deepcode-core/common/plan-implementation";
57

6-
export type PlanImplementationChoice = "implement" | "clear-context" | "stay" | "default";
8+
export {
9+
extractProposedPlan,
10+
getImplementationPrompt,
11+
getClearContextImplementationPrompt,
12+
} from "@vegamo/deepcode-core/common/plan-implementation";
13+
export type { PlanImplementationChoice } from "@vegamo/deepcode-core/common/plan-implementation";
714

815
type Props = {
916
onSelect: (choice: PlanImplementationChoice) => void;
1017
};
1118

12-
const CHOICES: Array<{ value: PlanImplementationChoice; label: string }> = [
13-
{ value: "implement", label: "implement this plan" },
14-
{ value: "clear-context", label: "clear context and implement" },
15-
{ value: "stay", label: "stay in Plan mode" },
16-
{ value: "default", label: "switch to Default mode" },
17-
];
18-
19-
const CLEAR_CONTEXT_IMPLEMENTATION_PREFIX =
20-
"A previous agent produced the plan below to accomplish the user's task. " +
21-
"Implement the plan in a fresh context. Treat the plan as the source of " +
22-
"user intent, re-read files as needed, and carry the work through " +
23-
"implementation and verification.";
24-
25-
/** Return only a complete proposed plan, so historical or partial tags cannot trigger the chooser. */
26-
export function extractProposedPlan(reply: string | null): string | null {
27-
if (!reply) {
28-
return null;
29-
}
30-
const match = reply.match(/<proposed_plan>\s*([\s\S]*?\S[\s\S]*?)\s*<\/proposed_plan>/);
31-
return match?.[1] ?? null;
32-
}
33-
34-
export function getImplementationPrompt(plan: string): string {
35-
const fullWidthPunctuationCount = (plan.match(/[,、;。]/g) ?? []).length;
36-
return fullWidthPunctuationCount > 5 ? "实现此方案。" : "Implement the plan.";
37-
}
38-
39-
export function getClearContextImplementationPrompt(plan: string): string {
40-
return `${CLEAR_CONTEXT_IMPLEMENTATION_PREFIX}\n\n${plan}`;
41-
}
42-
4319
export function getPlanImplementationChoice(
4420
input: string,
4521
key: Pick<InputKey, "escape" | "return">,

‎packages/core/package.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@
3535
"types": "./dist/common/npm-registry.d.ts",
3636
"import": "./dist/common/npm-registry.js"
3737
},
38+
"./common/plan-implementation": {
39+
"types": "./dist/common/plan-implementation.d.ts",
40+
"import": "./dist/common/plan-implementation.js"
41+
},
42+
"./common/slash-commands": {
43+
"types": "./dist/common/slash-commands.d.ts",
44+
"import": "./dist/common/slash-commands.js"
45+
},
3846
"./common/session-id": {
3947
"types": "./dist/common/session-id.d.ts",
4048
"import": "./dist/common/session-id.js"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export type PlanImplementationChoice = "implement" | "clear-context" | "stay" | "default";
2+
3+
export const PLAN_IMPLEMENTATION_CHOICES: Array<{ value: PlanImplementationChoice; label: string }> = [
4+
{ value: "implement", label: "implement this plan" },
5+
{ value: "clear-context", label: "clear context and implement" },
6+
{ value: "stay", label: "stay in Plan mode" },
7+
{ value: "default", label: "switch to Default mode" },
8+
];
9+
10+
const CLEAR_CONTEXT_IMPLEMENTATION_PREFIX =
11+
"A previous agent produced the plan below to accomplish the user's task. " +
12+
"Implement the plan in a fresh context. Treat the plan as the source of " +
13+
"user intent, re-read files as needed, and carry the work through " +
14+
"implementation and verification.";
15+
16+
/** Extract a complete, non-empty proposed plan from the final reply. */
17+
export function extractProposedPlan(reply: string | null): string | null {
18+
if (!reply) {
19+
return null;
20+
}
21+
const match = reply.match(/<proposed_plan>\s*([\s\S]*?\S[\s\S]*?)\s*<\/proposed_plan>/);
22+
return match?.[1] ?? null;
23+
}
24+
25+
export function getImplementationPrompt(plan: string): string {
26+
const fullWidthPunctuationCount = (plan.match(/[,、;。]/g) ?? []).length;
27+
return fullWidthPunctuationCount > 5 ? "实现此方案。" : "Implement the plan.";
28+
}
29+
30+
export function getClearContextImplementationPrompt(plan: string): string {
31+
return `${CLEAR_CONTEXT_IMPLEMENTATION_PREFIX}\n\n${plan}`;
32+
}
33+
34+
export function getCompletedProposedPlan(
35+
planMode: boolean,
36+
status: string | null | undefined,
37+
reply: string | null
38+
): string | null {
39+
return planMode && status === "completed" ? extractProposedPlan(reply) : null;
40+
}

0 commit comments

Comments
 (0)