march-cli 0.1.33 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/package.json +12 -1
  2. package/src/agent/lifecycle/runner-lifecycle.mjs +16 -0
  3. package/src/agent/lifecycle/runtime-restart-tool.mjs +22 -0
  4. package/src/agent/runner.mjs +9 -14
  5. package/src/agent/runtime/remote-runner-client.mjs +7 -15
  6. package/src/agent/runtime/runner-ipc-target.mjs +3 -22
  7. package/src/agent/runtime/runner-process-client.mjs +101 -24
  8. package/src/agent/runtime/runner-runtime-host.mjs +2 -0
  9. package/src/agent/runtime/state/runner-state.mjs +80 -0
  10. package/src/agent/session/session-options.mjs +2 -1
  11. package/src/agent/tools.mjs +3 -1
  12. package/src/cli/args.mjs +14 -3
  13. package/src/cli/commands/catalog/visible-commands.mjs +5 -0
  14. package/src/cli/commands/help-command.mjs +1 -7
  15. package/src/cli/commands/registry/slash-command-registry.mjs +293 -0
  16. package/src/cli/input/autocomplete.mjs +2 -25
  17. package/src/cli/repl-loop.mjs +24 -41
  18. package/src/cli/slash-commands.mjs +19 -185
  19. package/src/cli/startup/app-runtime.mjs +201 -0
  20. package/src/cli/startup/configured-command.mjs +9 -0
  21. package/src/cli/startup/early-command.mjs +29 -0
  22. package/src/cli/turn/turn-input-preparer.mjs +41 -0
  23. package/src/main.mjs +47 -242
  24. package/src/memory/markdown/memory-id.mjs +36 -0
  25. package/src/memory/markdown-store.mjs +17 -6
  26. package/src/memory/markdown-tools.mjs +3 -2
  27. package/src/web-ui/command.mjs +112 -0
  28. package/src/web-ui/dist/assets/index-BUmhnID4.css +1 -0
  29. package/src/web-ui/dist/assets/index-CtuqTjcB.js +1845 -0
  30. package/src/web-ui/dist/index.html +13 -0
  31. package/src/web-ui/index.html +12 -0
  32. package/src/web-ui/runtime-host.mjs +185 -0
  33. package/src/web-ui/server.mjs +139 -0
  34. package/src/web-ui/session-manager.mjs +109 -0
  35. package/src/web-ui/src/App.tsx +7 -0
  36. package/src/web-ui/src/components/AppShell.tsx +47 -0
  37. package/src/web-ui/src/components/Composer.tsx +47 -0
  38. package/src/web-ui/src/components/FileExplorer.tsx +46 -0
  39. package/src/web-ui/src/components/RightSidebar.tsx +70 -0
  40. package/src/web-ui/src/components/SessionTimeline.tsx +31 -0
  41. package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +109 -0
  42. package/src/web-ui/src/components/timeline/TimelineList.tsx +14 -0
  43. package/src/web-ui/src/fileTreeAdapter.ts +51 -0
  44. package/src/web-ui/src/main.tsx +11 -0
  45. package/src/web-ui/src/mockData.ts +87 -0
  46. package/src/web-ui/src/model.ts +62 -0
  47. package/src/web-ui/src/runtime/client.ts +74 -0
  48. package/src/web-ui/src/runtime/runtimeTimeline.ts +88 -0
  49. package/src/web-ui/src/runtime/useWebRuntime.ts +132 -0
  50. package/src/web-ui/src/styles/shell.css +156 -0
  51. package/src/web-ui/src/styles/tokens.css +116 -0
  52. package/src/web-ui/src/timelineAdapter.ts +43 -0
  53. package/src/web-ui/src/vite-env.d.ts +1 -0
  54. package/src/web-ui/tsconfig.json +20 -0
  55. package/src/web-ui/vite.config.mjs +11 -0
@@ -0,0 +1,293 @@
1
+ import { listExtensionPathsCommand } from "../extensions-command.mjs";
2
+ import { handleExportCommand, parseExportCommand } from "../export-command.mjs";
3
+ import { handleModelCommand, parseModelCommand } from "../model-command.mjs";
4
+ import { formatHotkeysPanel } from "../../repl-commands.mjs";
5
+ import { copyLastAssistantMessage } from "../copy-command.mjs";
6
+ import { handleSessionSourceCommand } from "../../session/session-source-command.mjs";
7
+ import { statusCommand } from "../status-command.mjs";
8
+ import { handleThinkingCommand, parseThinkingCommand } from "../thinking-command.mjs";
9
+ import { formatPromptTemplateLines } from "../../input/prompt-templates.mjs";
10
+ import { handleSettingsCommand, parseSettingsCommand } from "../../../config/settings-command.mjs";
11
+ import { handleSessionNameCommand, parseSessionNameCommand } from "../../session/session-name-command.mjs";
12
+ import { handleShellCommand, parseShellCommand } from "../../shell/shell-command.mjs";
13
+ import { handleProviderCommand, parseProviderCommand } from "../provider-command.mjs";
14
+ import { handleModeCommand, parseModeCommand } from "../mode-command.mjs";
15
+
16
+ export const SLASH_COMMANDS = [
17
+ exactCommand({
18
+ name: "exit",
19
+ aliases: ["quit"],
20
+ description: "Exit March",
21
+ run: async (ctx) => {
22
+ await handleSessionSourceCommand("/save", ctx);
23
+ return { handled: true, exit: true };
24
+ },
25
+ }),
26
+ exactCommand({
27
+ name: "new",
28
+ description: "Start a new pi session",
29
+ run: handleNewCommand,
30
+ }),
31
+ exactCommand({
32
+ name: "help",
33
+ description: "Show available commands",
34
+ run: async ({ ui }) => writeLines(ui, formatHelpLines()),
35
+ }),
36
+ exactCommand({
37
+ name: "reload",
38
+ aliases: ["reload-runtime"],
39
+ description: "Restart the March runtime",
40
+ run: handleReloadCommand,
41
+ }),
42
+ parsedCommand({
43
+ names: ["do", "discuss", "mode"],
44
+ metadata: [
45
+ { name: "do", description: "Switch to Do mode" },
46
+ { name: "discuss", description: "Switch to Discuss mode" },
47
+ { name: "mode", description: "Show current mode" },
48
+ ],
49
+ parse: parseModeCommand,
50
+ run: async (ctx, command) => writeLines(ctx.ui, handleModeCommand(command, { modeState: ctx.modeState })),
51
+ }),
52
+ exactCommand({
53
+ name: "hotkeys",
54
+ description: "Show keyboard shortcuts and input prefixes",
55
+ run: async ({ ui, keybindings, keybindingDiagnostics }) => writeLines(ui, formatHotkeysPanel(keybindings, keybindingDiagnostics)),
56
+ }),
57
+ exactCommand({
58
+ name: "templates",
59
+ description: "List project prompt templates",
60
+ run: async ({ ui, promptTemplates, promptTemplateDiagnostics }) => writeLines(ui, formatPromptTemplateLines(promptTemplates, promptTemplateDiagnostics)),
61
+ }),
62
+ parsedCommand({
63
+ names: ["export"],
64
+ metadata: [
65
+ { name: "export jsonl", description: "Export current session turns as JSONL" },
66
+ { name: "export html", description: "Export current session turns as HTML" },
67
+ { name: "export gist jsonl", helpSyntax: "export gist <jsonl|html>", description: "Share current session JSONL as a private GitHub Gist" },
68
+ { name: "export gist html", help: false, description: "Share current session HTML as a private GitHub Gist" },
69
+ ],
70
+ parse: parseExportCommand,
71
+ run: async (ctx, command) => writeLines(ctx.ui, await handleExportCommand(command, ctx)),
72
+ }),
73
+ parsedCommand({
74
+ names: ["settings"],
75
+ metadata: [{ name: "settings", description: "Show or edit global/project settings" }],
76
+ parse: parseSettingsCommand,
77
+ run: async (ctx, command) => writeLines(ctx.ui, handleSettingsCommand(command, { cwd: ctx.runner.engine.cwd, homeDir: ctx.settingsHomeDir })),
78
+ }),
79
+ exactCommand({
80
+ name: "extensions",
81
+ description: "List extension paths",
82
+ run: async ({ ui, runner, extensionPaths }) => writeLines(ui, listExtensionPathsCommand(
83
+ extensionPaths,
84
+ runner.getExtensionDiagnostics?.(),
85
+ runner.getExtensionLifecycleState?.(),
86
+ )),
87
+ }),
88
+ parsedCommand({
89
+ names: ["thinking"],
90
+ metadata: [
91
+ { name: "thinking", description: "Open thinking selector" },
92
+ { name: "thinking list", description: "List available thinking levels" },
93
+ ],
94
+ parse: parseThinkingCommand,
95
+ run: async (ctx, command) => writeLines(ctx.ui, await handleThinkingCommand(command, { runner: ctx.runner, ui: ctx.ui })),
96
+ }),
97
+ exactCommand({
98
+ name: "status",
99
+ description: "Show runtime status",
100
+ run: async ({ ui, runner, sessionState, sessionSource }) => writeLines(ui, statusCommand({
101
+ runner,
102
+ sessionState,
103
+ sessionSource,
104
+ extensionDiagnostics: runner.getExtensionDiagnostics?.() ?? [],
105
+ lifecycleState: runner.getExtensionLifecycleState?.() ?? null,
106
+ })),
107
+ }),
108
+ exactCommand({
109
+ name: "notify",
110
+ visible: false,
111
+ help: false,
112
+ autocomplete: false,
113
+ description: "Test desktop notifications",
114
+ run: async ({ ui, runner }) => writeLines(ui, [formatNotificationResult(await runner.notifyTest?.())]),
115
+ }),
116
+ parsedCommand({
117
+ names: ["shell"],
118
+ metadata: [
119
+ { name: "shell", description: "List shells or inspect shell output" },
120
+ { name: "shell spawn", helpSyntax: "shell spawn [name]", description: "Start a default PTY shell" },
121
+ ],
122
+ parse: parseShellCommand,
123
+ run: async (ctx, command) => writeLines(ctx.ui, handleShellCommand(command, { shellRuntime: ctx.runner.shellRuntime })),
124
+ }),
125
+ parsedCommand({
126
+ names: ["name"],
127
+ metadata: [{ name: "name", description: "Show or set session name" }],
128
+ parse: parseSessionNameCommand,
129
+ run: async (ctx, command) => writeLines(ctx.ui, handleSessionNameCommand(command, ctx)),
130
+ }),
131
+ exactCommand({
132
+ name: "copy",
133
+ description: "Copy last assistant response to clipboard",
134
+ run: async ({ ui, runner, writeClipboard }) => writeLines(ui, copyLastAssistantMessage({ engine: runner.engine, writeClipboard })),
135
+ }),
136
+ exactCommand({
137
+ name: "mouse",
138
+ visible: false,
139
+ help: false,
140
+ autocomplete: false,
141
+ description: "Show mouse selection status",
142
+ run: async ({ ui }) => writeLines(ui, ["Mouse selection is always enabled."]),
143
+ }),
144
+ sessionSourceCommand(),
145
+ parsedCommand({
146
+ names: ["providers"],
147
+ metadata: [{ name: "providers", description: "List configured providers" }],
148
+ parse: parseProviderCommand,
149
+ run: async (ctx, command) => {
150
+ try {
151
+ writeLines(ctx.ui, [await handleProviderCommand(command, { ui: ctx.ui, runner: ctx.runner })]);
152
+ } catch (err) {
153
+ writeLines(ctx.ui, [`Error: ${err.message}`]);
154
+ }
155
+ return { handled: true };
156
+ },
157
+ }),
158
+ parsedCommand({
159
+ names: ["model"],
160
+ metadata: [{ name: "model", description: "Open model selector" }],
161
+ parse: parseModelCommand,
162
+ run: async (ctx, command) => {
163
+ try {
164
+ writeLines(ctx.ui, [await handleModelCommand(command, { runner: ctx.runner, ui: ctx.ui, configHomeDir: ctx.configHomeDir })]);
165
+ } catch (err) {
166
+ writeLines(ctx.ui, [`Error: ${err.message}`]);
167
+ }
168
+ return { handled: true };
169
+ },
170
+ }),
171
+ ];
172
+
173
+ export async function runSlashCommand(trimmed, context) {
174
+ for (const command of SLASH_COMMANDS) {
175
+ const match = command.match(trimmed);
176
+ if (!match) continue;
177
+ return await command.run(context, match.parsed);
178
+ }
179
+ return { handled: false };
180
+ }
181
+
182
+ export function getVisibleCommandEntries() {
183
+ return SLASH_COMMANDS.flatMap((command) => command.metadata ?? [])
184
+ .filter((command) => command.visible !== false)
185
+ .map((command) => ({ ...command }));
186
+ }
187
+
188
+ export function getAutocompleteCommands() {
189
+ return getVisibleCommandEntries()
190
+ .filter((command) => command.autocomplete !== false)
191
+ .flatMap((command) => [command.name, ...(command.aliases ?? [])]
192
+ .map((name) => ({ name, description: command.description })));
193
+ }
194
+
195
+ export function getHelpCommandSyntaxes() {
196
+ return getVisibleCommandEntries()
197
+ .filter((command) => command.help !== false)
198
+ .map((command) => `/${command.helpSyntax ?? command.name}`);
199
+ }
200
+
201
+ function exactCommand({ name, aliases = [], description, visible = true, help = true, autocomplete = true, run }) {
202
+ const names = [name, ...aliases];
203
+ return {
204
+ metadata: [{ name, aliases, description, visible, help, autocomplete }],
205
+ match: (trimmed) => names.some((candidate) => trimmed === `/${candidate}`) ? { parsed: { type: name } } : null,
206
+ run,
207
+ };
208
+ }
209
+
210
+ function parsedCommand({ names, metadata, parse, run }) {
211
+ return {
212
+ metadata,
213
+ match: (trimmed) => {
214
+ if (!names.some((name) => trimmed === `/${name}` || trimmed.startsWith(`/${name} `))) return null;
215
+ const parsed = parse(trimmed);
216
+ return parsed?.type === "none" ? null : { parsed };
217
+ },
218
+ run,
219
+ };
220
+ }
221
+
222
+ function sessionSourceCommand() {
223
+ return {
224
+ metadata: [
225
+ { name: "session", description: "Open previous session selector" },
226
+ { name: "save", description: "Show auto-save status" },
227
+ ],
228
+ match: (trimmed) => (trimmed === "/session" || trimmed === "/save") ? { parsed: { trimmed } } : null,
229
+ run: async (ctx, { trimmed }) => handleSessionSourceCommand(trimmed, ctx),
230
+ };
231
+ }
232
+
233
+ async function handleNewCommand({ ui, runner, renderStartupBanner }) {
234
+ if (!runner.canSwitchPiSession?.()) {
235
+ ui.writeln("Error: pi runtime host is not enabled");
236
+ return { handled: true };
237
+ }
238
+ let refreshContextTokens = false;
239
+ try {
240
+ const result = await runner.startNewSession();
241
+ if (result?.cancelled) {
242
+ ui.writeln("New session cancelled");
243
+ } else {
244
+ refreshContextTokens = true;
245
+ ui.clearOutput?.();
246
+ const bannerLines = typeof renderStartupBanner === "function" ? renderStartupBanner() : [];
247
+ if (bannerLines.length > 0) {
248
+ writeLines(ui, bannerLines);
249
+ } else {
250
+ ui.writeln(`Started new session: ${result.sessionId}`);
251
+ }
252
+ }
253
+ } catch (err) {
254
+ ui.writeln(`Error: ${err.message}`);
255
+ }
256
+ return { handled: true, refreshContextTokens };
257
+ }
258
+
259
+ async function handleReloadCommand({ ui, runner }) {
260
+ if (typeof runner.restartRuntime !== "function") {
261
+ ui.writeln("Runtime reload is unavailable in in-process mode. Restart March to load code changes.");
262
+ return { handled: true };
263
+ }
264
+ try {
265
+ await runner.restartRuntime();
266
+ ui.writeln("● March runtime 已重启,下一轮将使用磁盘上的最新代码");
267
+ return { handled: true, refreshContextTokens: true };
268
+ } catch (err) {
269
+ ui.writeln(`Error: ${err.message}`);
270
+ return { handled: true };
271
+ }
272
+ }
273
+
274
+ function writeLines(ui, lines) {
275
+ for (const line of lines) ui.writeln(line);
276
+ return { handled: true };
277
+ }
278
+
279
+ export function formatHelpLines() {
280
+ return [
281
+ `Commands: ${getHelpCommandSyntaxes().join(", ")}`,
282
+ "Sessions: /session opens previous sessions and restores the selected one.",
283
+ "Shortcuts: Tab = toggle Do/Discuss, Esc = abort turn, Ctrl+C = abort turn / press twice to exit when idle, Ctrl+O = toggle tool output, Alt+S = shell pane, Alt+N = next shell, Alt+K/J = shell scroll, PageUp/PageDown = output scroll, Ctrl+G = external editor, Shift+Tab = thinking selector, Ctrl+T = thinking selector, Ctrl+L = model selector",
284
+ ];
285
+ }
286
+
287
+ function formatNotificationResult(result) {
288
+ if (!result) return "notification: unavailable";
289
+ const channels = (result.results ?? [])
290
+ .map((entry) => `${entry.channel}:${entry.ok ? "ok" : entry.reason ?? "failed"}`)
291
+ .join(", ");
292
+ return `notification: ${result.ok ? "ok" : result.reason ?? "failed"}${channels ? ` (${channels})` : ""}`;
293
+ }
@@ -1,30 +1,7 @@
1
1
  import { CombinedAutocompleteProvider } from "@earendil-works/pi-tui";
2
+ import { getAutocompleteCommands } from "../commands/catalog/visible-commands.mjs";
2
3
  import { FileSearchIndex } from "./file-search/index.mjs";
3
4
 
4
- const MARCH_COMMANDS = [
5
- { name: "new", description: "Start a new pi session" },
6
- { name: "exit", description: "Exit March" },
7
- { name: "quit", description: "Exit March" },
8
- { name: "help", description: "Show available commands" },
9
- { name: "model", description: "Open model selector" },
10
- { name: "models", description: "List available models" },
11
- { name: "session", description: "Open previous session selector" },
12
- { name: "save", description: "Show auto-save status" },
13
- { name: "name", description: "Show or set session name" },
14
- { name: "copy", description: "Copy last assistant response to clipboard" },
15
- { name: "thinking", description: "Open thinking selector" },
16
- { name: "thinking list", description: "List available thinking levels" },
17
- { name: "hotkeys", description: "Show keyboard shortcuts and input prefixes" },
18
- { name: "templates", description: "List project prompt templates" },
19
- { name: "export jsonl", description: "Export current session turns as JSONL" },
20
- { name: "export html", description: "Export current session turns as HTML" },
21
- { name: "export gist html", description: "Share current session HTML as a private GitHub Gist" },
22
- { name: "export gist jsonl", description: "Share current session JSONL as a private GitHub Gist" },
23
- { name: "settings", description: "Show or edit global/project settings" },
24
- { name: "shell", description: "List shells or inspect shell output" },
25
- { name: "shell spawn", description: "Start a default PTY shell" },
26
- ];
27
-
28
5
  export function buildMarchCommands(promptTemplates = []) {
29
6
  const templateCommands = promptTemplates
30
7
  .map((template) => ({
@@ -32,7 +9,7 @@ export function buildMarchCommands(promptTemplates = []) {
32
9
  description: "Expand prompt template",
33
10
  }))
34
11
  .filter((command) => command.name && !command.name.startsWith("/"));
35
- return [...MARCH_COMMANDS, ...templateCommands];
12
+ return [...getAutocompleteCommands(), ...templateCommands];
36
13
  }
37
14
 
38
15
  export class MarchAutocompleteProvider {
@@ -1,11 +1,9 @@
1
- import { brightBlack, inverse } from "./tui/ui-theme.mjs";
1
+ import { brightBlack } from "./tui/ui-theme.mjs";
2
2
  import { handleSlashCommand } from "./slash-commands.mjs";
3
- import { appendModeReminder } from "./input/mode-state.mjs";
4
3
  import { expandPromptTemplate } from "./input/prompt-templates.mjs";
5
4
  import { parseInlineShellInput, runInlineShellCommand } from "./repl-commands.mjs";
6
- import { formatRecallHints } from "../memory/markdown-store.mjs";
7
- import { formatMessageAttachmentsForDisplay } from "../session/attachment-display.mjs";
8
- import { formatShellHints } from "../shell/hints.mjs";
5
+ import { prepareTurnInput } from "./turn/turn-input-preparer.mjs";
6
+ export { formatUserDisplayMessage } from "./turn/turn-input-preparer.mjs";
9
7
 
10
8
  export async function runSingleShotPrompt({
11
9
  prompt,
@@ -13,26 +11,18 @@ export async function runSingleShotPrompt({
13
11
  memoryStore,
14
12
  currentProject,
15
13
  ui,
16
- sessionState,
17
14
  refreshStatusBar,
18
15
  modeState = null,
19
16
  }) {
20
- memoryStore.beginTurn();
21
- const carryoverAlreadyRendered = runner.engine.hasRenderedPendingAssistantRecallHints?.() ?? false;
22
- const carryoverRecallHints = runner.engine.takePendingAssistantRecallHints?.() ?? [];
23
- const userRecallHints = memoryStore.recallForUser(prompt, { currentProject, excludedIds: runner.engine.getRecentRecallMemoryIds?.() ?? [] });
24
- const recallBlock = formatRecallHints("user", userRecallHints);
25
- const carryoverRecallBlock = formatRecallHints("assistant", carryoverRecallHints);
26
- const shellHints = formatShellHints(runner.shellRuntime);
27
- const modePrompt = appendModeReminder(prompt, modeState?.get?.());
28
- const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, carryoverRecallBlock, shellHints);
29
- ui.writeln(formatUserDisplayMessage(prompt));
30
- ui.recall?.({ source: "user", hints: userRecallHints });
31
- if (carryoverRecallHints.length > 0 && !carryoverAlreadyRendered) ui.recall?.({ source: "assistant", hints: carryoverRecallHints });
17
+ const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
18
+ ui.writeln(turnInput.displayMessage);
19
+ ui.recall?.({ source: "user", hints: turnInput.userRecallHints });
20
+ if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ source: "assistant", hints: turnInput.carryoverRecallHints });
32
21
  refreshStatusBar.startWorking?.();
33
22
  try {
34
- await runner.runTurn(fullPrompt, prompt, { userRecallHints, currentProject });
23
+ const result = await runner.runTurn(turnInput.fullPrompt, turnInput.userMessage, turnInput.runOptions);
35
24
  renderPendingAssistantRecallPreview({ runner, ui });
25
+ await handleTurnLifecycleAction(result?.lifecycleAction, { runner, ui });
36
26
  } finally {
37
27
  refreshStatusBar.stopWorking?.();
38
28
  memoryStore.endTurn();
@@ -42,7 +32,6 @@ export async function runSingleShotPrompt({
42
32
 
43
33
  export async function runInteractiveRepl({
44
34
  cwd,
45
- args,
46
35
  ui,
47
36
  runner,
48
37
  memoryStore,
@@ -105,7 +94,6 @@ export async function runInteractiveRepl({
105
94
 
106
95
  await runReplTurn({
107
96
  prompt: trimmed,
108
- args,
109
97
  runner,
110
98
  memoryStore,
111
99
  currentProject,
@@ -138,24 +126,17 @@ function handleInlineCommand(trimmed, { cwd, ui, lastInlineShellCommand }) {
138
126
  return { type: "none" };
139
127
  }
140
128
 
141
- async function runReplTurn({ prompt, args, runner, memoryStore, currentProject, ui, refreshStatusBar, setTurnRunning, modeState = null }) {
142
- memoryStore.beginTurn();
143
- const carryoverAlreadyRendered = runner.engine.hasRenderedPendingAssistantRecallHints?.() ?? false;
144
- const carryoverRecallHints = runner.engine.takePendingAssistantRecallHints?.() ?? [];
145
- const userRecallHints = memoryStore.recallForUser(prompt, { currentProject, excludedIds: runner.engine.getRecentRecallMemoryIds?.() ?? [] });
146
- const recallBlock = formatRecallHints("user", userRecallHints);
147
- const carryoverRecallBlock = formatRecallHints("assistant", carryoverRecallHints);
148
- const shellHints = formatShellHints(runner.shellRuntime);
149
- const modePrompt = appendModeReminder(prompt, modeState?.get?.());
150
- const fullPrompt = appendPromptBlocks(modePrompt, recallBlock, carryoverRecallBlock, shellHints);
129
+ async function runReplTurn({ prompt, runner, memoryStore, currentProject, ui, refreshStatusBar, setTurnRunning, modeState = null }) {
130
+ const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
151
131
  try {
152
- ui.writeln(formatUserDisplayMessage(prompt));
153
- ui.recall?.({ source: "user", hints: userRecallHints });
154
- if (carryoverRecallHints.length > 0 && !carryoverAlreadyRendered) ui.recall?.({ source: "assistant", hints: carryoverRecallHints });
132
+ ui.writeln(turnInput.displayMessage);
133
+ ui.recall?.({ source: "user", hints: turnInput.userRecallHints });
134
+ if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ source: "assistant", hints: turnInput.carryoverRecallHints });
155
135
  setTurnRunning(true);
156
136
  refreshStatusBar.startWorking?.();
157
- await runner.runTurn(fullPrompt, prompt, { userRecallHints, currentProject });
137
+ const result = await runner.runTurn(turnInput.fullPrompt, turnInput.userMessage, turnInput.runOptions);
158
138
  renderPendingAssistantRecallPreview({ runner, ui });
139
+ await handleTurnLifecycleAction(result?.lifecycleAction, { runner, ui });
159
140
  ui.writeln("");
160
141
  } catch (err) {
161
142
  ui.writeln(`Error: ${err.message}`);
@@ -167,12 +148,14 @@ async function runReplTurn({ prompt, args, runner, memoryStore, currentProject,
167
148
  }
168
149
  }
169
150
 
170
- export function formatUserDisplayMessage(prompt) {
171
- return `${inverse(" USER ")} ${formatMessageAttachmentsForDisplay(prompt)}`;
172
- }
173
-
174
- function appendPromptBlocks(prompt, ...blocks) {
175
- return [prompt, ...blocks.filter(Boolean)].join("\n\n");
151
+ async function handleTurnLifecycleAction(action, { runner, ui }) {
152
+ if (action?.type !== "restart_runtime") return;
153
+ if (typeof runner.restartRuntime !== "function") {
154
+ ui.writeln("March runtime restart requested, but runtime reload is unavailable in in-process mode. Restart March to load code changes.");
155
+ return;
156
+ }
157
+ await runner.restartRuntime();
158
+ ui.writeln("● March runtime 已重启,下一轮将使用磁盘上的最新代码");
176
159
  }
177
160
 
178
161
  function renderPendingAssistantRecallPreview({ runner, ui }) {
@@ -1,25 +1,10 @@
1
- import { listExtensionPathsCommand } from "./commands/extensions-command.mjs";
2
- import { handleExportCommand, parseExportCommand } from "./commands/export-command.mjs";
3
- import { handleModelCommand, listModels, parseModelCommand } from "./commands/model-command.mjs";
4
- import { formatHotkeysPanel } from "./repl-commands.mjs";
5
- import { copyLastAssistantMessage } from "./commands/copy-command.mjs";
6
- import { handleSessionSourceCommand } from "./session/session-source-command.mjs";
7
- import { statusCommand } from "./commands/status-command.mjs";
8
- import { handleThinkingCommand, parseThinkingCommand } from "./commands/thinking-command.mjs";
9
- import { formatPromptTemplateLines } from "./input/prompt-templates.mjs";
10
- import { handleSettingsCommand, parseSettingsCommand } from "../config/settings-command.mjs";
11
- import { handleSessionNameCommand, parseSessionNameCommand } from "./session/session-name-command.mjs";
12
- import { handleShellCommand, parseShellCommand } from "./shell/shell-command.mjs";
13
- import { handleProviderCommand, parseProviderCommand } from "./commands/provider-command.mjs";
14
- import { handleModeCommand, parseModeCommand } from "./commands/mode-command.mjs";
15
- import { formatHelpLines } from "./commands/help-command.mjs";
1
+ import { runSlashCommand } from "./commands/registry/slash-command-registry.mjs";
16
2
 
17
- export async function handleSlashCommand(trimmed, {
18
- ui,
19
- runner,
20
- sessionState,
21
- sessionsRoot,
22
- projectMarchDir,
3
+ export async function handleSlashCommand(trimmed, options) {
4
+ return runSlashCommand(trimmed, normalizeSlashCommandOptions(options));
5
+ }
6
+
7
+ function normalizeSlashCommandOptions({
23
8
  sessionSource = "pi",
24
9
  extensionPaths = [],
25
10
  keybindings,
@@ -30,170 +15,19 @@ export async function handleSlashCommand(trimmed, {
30
15
  renderStartupBanner = null,
31
16
  settingsHomeDir,
32
17
  configHomeDir = settingsHomeDir,
33
- writeClipboard,
18
+ ...rest
34
19
  }) {
35
- if (trimmed === "/exit" || trimmed === "/quit") {
36
- await handleSessionSourceCommand("/save", { ui, runner, sessionState, sessionSource });
37
- return { handled: true, exit: true };
38
- }
39
-
40
- if (trimmed === "/new") {
41
- if (!runner.canSwitchPiSession?.()) {
42
- ui.writeln("Error: pi runtime host is not enabled");
43
- return { handled: true };
44
- }
45
- let refreshContextTokens = false;
46
- try {
47
- const result = await runner.startNewSession();
48
- if (result?.cancelled) {
49
- ui.writeln("New session cancelled");
50
- } else {
51
- refreshContextTokens = true;
52
- ui.clearOutput?.();
53
- const bannerLines = typeof renderStartupBanner === "function" ? renderStartupBanner() : [];
54
- if (bannerLines.length > 0) {
55
- for (const line of bannerLines) ui.writeln(line);
56
- } else {
57
- ui.writeln(`Started new session: ${result.sessionId}`);
58
- }
59
- }
60
- } catch (err) {
61
- ui.writeln(`Error: ${err.message}`);
62
- }
63
- return { handled: true, refreshContextTokens };
64
- }
65
-
66
- if (trimmed === "/help") {
67
- for (const line of formatHelpLines()) ui.writeln(line);
68
- return { handled: true };
69
- }
70
-
71
- const modeCommand = parseModeCommand(trimmed);
72
- if (modeCommand.type !== "none") {
73
- for (const line of handleModeCommand(modeCommand, { modeState })) ui.writeln(line);
74
- return { handled: true };
75
- }
76
-
77
- if (trimmed === "/hotkeys") {
78
- for (const line of formatHotkeysPanel(keybindings, keybindingDiagnostics)) ui.writeln(line);
79
- return { handled: true };
80
- }
81
-
82
- if (trimmed === "/templates") {
83
- for (const line of formatPromptTemplateLines(promptTemplates, promptTemplateDiagnostics)) ui.writeln(line);
84
- return { handled: true };
85
- }
86
-
87
- const exportCommand = parseExportCommand(trimmed);
88
- if (exportCommand.type !== "none") {
89
- for (const line of await handleExportCommand(exportCommand, { runner, sessionState, sessionSource, projectMarchDir })) ui.writeln(line);
90
- return { handled: true };
91
- }
92
-
93
- const settingsCommand = parseSettingsCommand(trimmed);
94
- if (settingsCommand.type !== "none") {
95
- for (const line of handleSettingsCommand(settingsCommand, { cwd: runner.engine.cwd, homeDir: settingsHomeDir })) {
96
- ui.writeln(line);
97
- }
98
- return { handled: true };
99
- }
100
-
101
- if (trimmed === "/extensions") {
102
- for (const line of listExtensionPathsCommand(
103
- extensionPaths,
104
- runner.getExtensionDiagnostics?.(),
105
- runner.getExtensionLifecycleState?.(),
106
- )) ui.writeln(line);
107
- return { handled: true };
108
- }
109
-
110
- const thinkingCommand = parseThinkingCommand(trimmed);
111
- if (thinkingCommand.type !== "none") {
112
- for (const line of await handleThinkingCommand(thinkingCommand, { runner, ui })) ui.writeln(line);
113
- return { handled: true };
114
- }
115
-
116
- if (trimmed === "/status") {
117
- for (const line of statusCommand({
118
- runner,
119
- sessionState,
120
- sessionSource,
121
- extensionDiagnostics: runner.getExtensionDiagnostics?.() ?? [],
122
- lifecycleState: runner.getExtensionLifecycleState?.() ?? null,
123
- })) ui.writeln(line);
124
- return { handled: true };
125
- }
126
-
127
- if (trimmed === "/notify") {
128
- const result = await runner.notifyTest?.();
129
- ui.writeln(formatNotificationResult(result));
130
- return { handled: true };
131
- }
132
-
133
- const shellCommand = parseShellCommand(trimmed);
134
- if (shellCommand.type !== "none") {
135
- for (const line of handleShellCommand(shellCommand, { shellRuntime: runner.shellRuntime })) ui.writeln(line);
136
- return { handled: true };
137
- }
138
-
139
- const nameCommand = parseSessionNameCommand(trimmed);
140
- if (nameCommand.type !== "none") {
141
- for (const line of handleSessionNameCommand(nameCommand, { runner, sessionState, sessionSource })) ui.writeln(line);
142
- return { handled: true };
143
- }
144
-
145
- if (trimmed === "/copy") {
146
- for (const line of copyLastAssistantMessage({ engine: runner.engine, writeClipboard })) ui.writeln(line);
147
- return { handled: true };
148
- }
149
-
150
- if (trimmed === "/mouse") {
151
- ui.writeln("Mouse selection is always enabled.");
152
- return { handled: true };
153
- }
154
-
155
- const sessionSourceCommand = await handleSessionSourceCommand(trimmed, {
156
- ui,
157
- runner,
158
- sessionState,
159
- sessionsRoot,
160
- projectMarchDir,
20
+ return {
21
+ ...rest,
161
22
  sessionSource,
162
- });
163
- if (sessionSourceCommand.handled) return sessionSourceCommand;
164
-
165
- const providerCommand = parseProviderCommand(trimmed);
166
- if (providerCommand.type !== "none") {
167
- try {
168
- ui.writeln(await handleProviderCommand(providerCommand, { ui, runner }));
169
- } catch (err) {
170
- ui.writeln(`Error: ${err.message}`);
171
- }
172
- return { handled: true };
173
- }
174
-
175
- const modelCommand = parseModelCommand(trimmed);
176
- if (modelCommand.type !== "none") {
177
- try {
178
- ui.writeln(await handleModelCommand(modelCommand, { runner, ui, configHomeDir }));
179
- } catch (err) {
180
- ui.writeln(`Error: ${err.message}`);
181
- }
182
- return { handled: true };
183
- }
184
-
185
- if (trimmed === "/models") {
186
- for (const line of listModels({ runner })) ui.writeln(line);
187
- return { handled: true };
188
- }
189
-
190
- return { handled: false };
191
- }
192
-
193
- function formatNotificationResult(result) {
194
- if (!result) return "notification: unavailable";
195
- const channels = (result.results ?? [])
196
- .map((entry) => `${entry.channel}:${entry.ok ? "ok" : entry.reason ?? "failed"}`)
197
- .join(", ");
198
- return `notification: ${result.ok ? "ok" : result.reason ?? "failed"}${channels ? ` (${channels})` : ""}`;
23
+ extensionPaths,
24
+ keybindings,
25
+ keybindingDiagnostics,
26
+ promptTemplates,
27
+ promptTemplateDiagnostics,
28
+ modeState,
29
+ renderStartupBanner,
30
+ settingsHomeDir,
31
+ configHomeDir,
32
+ };
199
33
  }