march-cli 0.1.36 → 0.1.38

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 (58) hide show
  1. package/package.json +1 -1
  2. package/src/agent/code-search/tool.mjs +1 -1
  3. package/src/agent/runner/runner-utils.mjs +20 -0
  4. package/src/agent/runner.mjs +16 -18
  5. package/src/agent/runtime/remote-ui-client.mjs +0 -1
  6. package/src/agent/runtime/runner-process-client.mjs +7 -0
  7. package/src/agent/runtime/runner-process-factory.mjs +7 -3
  8. package/src/agent/runtime/runner-runtime-host.mjs +2 -2
  9. package/src/agent/runtime/ui-event-bridge.mjs +0 -2
  10. package/src/agent/session/session-options.mjs +2 -2
  11. package/src/agent/tools.mjs +5 -23
  12. package/src/agent/turn/turn-events.mjs +41 -0
  13. package/src/agent/turn/turn-runner.mjs +5 -2
  14. package/src/cli/args.mjs +0 -3
  15. package/src/cli/commands/registry/slash-command-registry.mjs +2 -0
  16. package/src/cli/fallback-ui.mjs +0 -2
  17. package/src/cli/input/history-store.mjs +65 -3
  18. package/src/cli/input/mode-state.mjs +1 -1
  19. package/src/cli/repl-loop.mjs +75 -25
  20. package/src/cli/startup/app-runtime.mjs +72 -31
  21. package/src/cli/startup/create-runtime-runner.mjs +5 -46
  22. package/src/cli/startup/startup-session.mjs +3 -13
  23. package/src/cli/tui/input/history-navigation-controller.mjs +56 -0
  24. package/src/cli/turn/turn-input-preparer.mjs +0 -1
  25. package/src/cli/ui.mjs +9 -6
  26. package/src/cli/workspace/command.mjs +147 -0
  27. package/src/cli/workspace/output-router.mjs +108 -0
  28. package/src/cli/workspace/project-runtime.mjs +92 -0
  29. package/src/config/features.mjs +0 -1
  30. package/src/context/engine.mjs +4 -2
  31. package/src/context/system-core/base.md +4 -1
  32. package/src/extensions/lifecycle-adapter.mjs +1 -1
  33. package/src/history/runner.mjs +11 -0
  34. package/src/history/store.mjs +129 -0
  35. package/src/history/tool.mjs +39 -0
  36. package/src/lsp/client.mjs +12 -5
  37. package/src/lsp/service.mjs +15 -3
  38. package/src/main.mjs +5 -2
  39. package/src/notification/desktop-notifier.mjs +16 -8
  40. package/src/web-ui/command.mjs +2 -2
  41. package/src/web-ui/dist/assets/index-BQtl1uQs.css +1 -0
  42. package/src/web-ui/dist/assets/index-DrlJis_D.js +1845 -0
  43. package/src/web-ui/dist/index.html +13 -0
  44. package/src/web-ui/runtime-host.mjs +5 -25
  45. package/src/web-ui/session-manager.mjs +2 -2
  46. package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +2 -10
  47. package/src/web-ui/src/mockData.ts +1 -8
  48. package/src/web-ui/src/model.ts +0 -2
  49. package/src/web-ui/src/runtime/client.ts +0 -1
  50. package/src/web-ui/src/runtime/runtimeTimeline.ts +1 -3
  51. package/src/web-ui/src/styles/shell.css +1 -2
  52. package/src/web-ui/src/timelineAdapter.ts +1 -2
  53. package/src/workspace/project-id.mjs +14 -0
  54. package/src/workspace/project-registry.mjs +74 -0
  55. package/src/workspace/session-index.mjs +75 -0
  56. package/src/workspace/supervisor.mjs +172 -0
  57. package/src/cli/permissions.mjs +0 -103
  58. package/src/cli/tui/permission-request-ui.mjs +0 -18
@@ -14,12 +14,13 @@ export async function runSingleShotPrompt({
14
14
  refreshStatusBar,
15
15
  modeState = null,
16
16
  }) {
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 });
21
- refreshStatusBar.startWorking?.();
17
+ memoryStore.beginTurn();
22
18
  try {
19
+ const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
20
+ ui.writeln(turnInput.displayMessage);
21
+ ui.recall?.({ source: "user", hints: turnInput.userRecallHints });
22
+ if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ source: "assistant", hints: turnInput.carryoverRecallHints });
23
+ refreshStatusBar.startWorking?.();
23
24
  const result = await runner.runTurn(turnInput.fullPrompt, turnInput.userMessage, turnInput.runOptions);
24
25
  renderPendingAssistantRecallPreview({ runner, ui });
25
26
  await handleTurnLifecycleAction(result?.lifecycleAction, { runner, ui });
@@ -36,6 +37,10 @@ export async function runInteractiveRepl({
36
37
  runner,
37
38
  memoryStore,
38
39
  currentProject,
40
+ currentProjectInfo = null,
41
+ workspaceSupervisor = null,
42
+ workspaceOutputRouter = null,
43
+ stateRoot = null,
39
44
  sessionState,
40
45
  sessionsRoot,
41
46
  projectMarchDir,
@@ -57,7 +62,8 @@ export async function runInteractiveRepl({
57
62
  let trimmed = line.trim();
58
63
  if (!trimmed) continue;
59
64
 
60
- const handledInline = handleInlineCommand(trimmed, { cwd, ui, lastInlineShellCommand });
65
+ const active = getActiveRuntime({ workspaceSupervisor, cwd, runner, memoryStore, currentProject, currentProjectInfo, sessionState, sessionsRoot, projectMarchDir, extensionPaths, keybindingConfig, promptTemplateConfig });
66
+ const handledInline = handleInlineCommand(trimmed, { cwd: active.cwd, ui, lastInlineShellCommand });
61
67
  if (handledInline.type === "handled") {
62
68
  lastInlineShellCommand = handledInline.lastInlineShellCommand;
63
69
  continue;
@@ -66,45 +72,70 @@ export async function runInteractiveRepl({
66
72
 
67
73
  const slashResult = await handleSlashCommand(trimmed, {
68
74
  ui,
69
- runner,
70
- sessionState,
71
- sessionsRoot,
72
- projectMarchDir,
75
+ runner: active.runner,
76
+ workspaceSupervisor,
77
+ workspaceOutputRouter,
78
+ sessionState: active.sessionState,
79
+ sessionsRoot: active.sessionsRoot,
80
+ projectMarchDir: active.projectMarchDir,
73
81
  sessionSource,
74
- extensionPaths,
75
- keybindings: keybindingConfig.keybindings,
76
- keybindingDiagnostics: keybindingConfig.diagnostics,
77
- promptTemplates: promptTemplateConfig.templates,
78
- promptTemplateDiagnostics: promptTemplateConfig.diagnostics,
82
+ extensionPaths: active.extensionPaths,
83
+ keybindings: active.keybindingConfig.keybindings,
84
+ keybindingDiagnostics: active.keybindingConfig.diagnostics,
85
+ promptTemplates: active.promptTemplateConfig.templates,
86
+ promptTemplateDiagnostics: active.promptTemplateConfig.diagnostics,
79
87
  modeState,
80
88
  renderStartupBanner,
81
89
  configHomeDir,
90
+ stateRoot,
91
+ currentProjectId: active.project?.projectId ?? null,
82
92
  });
83
93
  if (slashResult.exit) break;
84
94
  if (slashResult.handled) {
85
- refreshStatusBar(contextTokenRefreshOptions(slashResult, runner));
95
+ const refreshedActive = getActiveRuntime({ workspaceSupervisor, cwd, runner, memoryStore, currentProject, currentProjectInfo, sessionState, sessionsRoot, projectMarchDir, extensionPaths, keybindingConfig, promptTemplateConfig });
96
+ refreshStatusBar(contextTokenRefreshOptions(slashResult, refreshedActive.runner));
86
97
  continue;
87
98
  }
88
99
 
89
- const templateResult = expandPromptTemplate(trimmed, promptTemplateConfig.templates);
100
+ const turnActive = getActiveRuntime({ workspaceSupervisor, cwd, runner, memoryStore, currentProject, currentProjectInfo, sessionState, sessionsRoot, projectMarchDir, extensionPaths, keybindingConfig, promptTemplateConfig });
101
+ const templateResult = expandPromptTemplate(trimmed, turnActive.promptTemplateConfig.templates);
90
102
  if (templateResult.type === "template") {
91
103
  ui.writeln(brightBlack(`● template: ${templateResult.name}`));
92
104
  trimmed = templateResult.prompt;
93
105
  }
94
106
 
95
- await runReplTurn({
107
+ if (turnActive.turnTask) {
108
+ ui.writeln("This session is still running. Use /switch to start or inspect another session.");
109
+ continue;
110
+ }
111
+
112
+ startReplTurn({
113
+ runtime: turnActive,
96
114
  prompt: trimmed,
97
- runner,
98
- memoryStore,
99
- currentProject,
100
115
  ui,
101
116
  refreshStatusBar,
102
117
  setTurnRunning,
118
+ workspaceSupervisor,
103
119
  modeState,
104
120
  });
105
121
  }
106
122
  }
107
123
 
124
+ export function getActiveRuntime({ workspaceSupervisor, cwd, runner, memoryStore, currentProject, currentProjectInfo, sessionState, sessionsRoot, projectMarchDir, extensionPaths, keybindingConfig, promptTemplateConfig }) {
125
+ return workspaceSupervisor?.getActive?.() ?? {
126
+ project: currentProjectInfo,
127
+ cwd,
128
+ runner,
129
+ memoryStore,
130
+ currentProject,
131
+ sessionState,
132
+ sessionsRoot,
133
+ projectMarchDir,
134
+ extensionPaths,
135
+ keybindingConfig,
136
+ promptTemplateConfig,
137
+ };
138
+ }
108
139
  export function contextTokenRefreshOptions(slashResult, runner) {
109
140
  if (!slashResult?.refreshContextTokens) return undefined;
110
141
  if (typeof runner.estimateContextTokens !== "function") return undefined;
@@ -126,9 +157,31 @@ function handleInlineCommand(trimmed, { cwd, ui, lastInlineShellCommand }) {
126
157
  return { type: "none" };
127
158
  }
128
159
 
160
+ function startReplTurn({ runtime, prompt, ui, refreshStatusBar, setTurnRunning, workspaceSupervisor = null, modeState = null }) {
161
+ const turnUi = runtime.ui ?? ui;
162
+ const task = runReplTurn({
163
+ prompt,
164
+ runner: runtime.runner,
165
+ memoryStore: runtime.memoryStore,
166
+ currentProject: runtime.currentProject,
167
+ ui: turnUi,
168
+ refreshStatusBar,
169
+ setTurnRunning,
170
+ modeState,
171
+ }).finally(() => {
172
+ if (runtime.turnTask === task) runtime.turnTask = null;
173
+ const hasRunningTurn = Boolean(workspaceSupervisor?.hasRunningTurn?.());
174
+ setTurnRunning(hasRunningTurn);
175
+ if (!hasRunningTurn) refreshStatusBar.stopWorking?.();
176
+ refreshStatusBar();
177
+ });
178
+ runtime.turnTask = task;
179
+ }
180
+
129
181
  async function runReplTurn({ prompt, runner, memoryStore, currentProject, ui, refreshStatusBar, setTurnRunning, modeState = null }) {
130
- const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
182
+ memoryStore.beginTurn();
131
183
  try {
184
+ const turnInput = prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState });
132
185
  ui.writeln(turnInput.displayMessage);
133
186
  ui.recall?.({ source: "user", hints: turnInput.userRecallHints });
134
187
  if (turnInput.shouldRenderCarryoverRecall) ui.recall?.({ source: "assistant", hints: turnInput.carryoverRecallHints });
@@ -141,10 +194,7 @@ async function runReplTurn({ prompt, runner, memoryStore, currentProject, ui, re
141
194
  } catch (err) {
142
195
  ui.writeln(`Error: ${err.message}`);
143
196
  } finally {
144
- setTurnRunning(false);
145
- refreshStatusBar.stopWorking?.();
146
197
  memoryStore.endTurn();
147
- refreshStatusBar();
148
198
  }
149
199
  }
150
200
 
@@ -1,7 +1,6 @@
1
1
  import { basename, join, resolve } from "node:path";
2
2
  import { existsSync, mkdirSync } from "node:fs";
3
3
  import { createUI } from "../ui.mjs";
4
- import { createPermissionController, MODE } from "../permissions.mjs";
5
4
  import { loadKeybindings } from "../input/keybindings.mjs";
6
5
  import { createInputHistoryStore } from "../input/history-store.mjs";
7
6
  import { createModeState } from "../input/mode-state.mjs";
@@ -12,21 +11,21 @@ import { createMarchAuthStorage } from "../../auth/storage.mjs";
12
11
  import { createRuntimeRunner } from "./create-runtime-runner.mjs";
13
12
  import { createCliShellRuntime } from "../../shell/cli-runtime.mjs";
14
13
  import { MarkdownMemoryStore } from "../../memory/markdown-store.mjs";
15
- import { createMarkdownMemoryTools } from "../../memory/markdown-tools.mjs";
16
14
  import { discoverProjectExtensionPaths } from "../../extensions/discovery.mjs";
17
15
  import { loadProjectLifecycleHookManifests } from "../../extensions/lifecycle-manifest.mjs";
18
16
  import { loadOrCreateProjectId, resumeStartupSession } from "./startup-session.mjs";
19
- import { initializeMcp } from "../../mcp/index.mjs";
20
- import { createWebToolsFromConfig } from "../../web/tools.mjs";
21
- import { createModelContextDumper } from "../../debug/model-context-dumper.mjs";
22
17
  import { createLogger, installProcessLogHandlers } from "../../debug/logger.mjs";
23
18
  import { defaultProfilePaths, ensureProfileFiles } from "../../context/profiles.mjs";
24
- import { createDesktopTurnNotifier } from "../../notification/desktop-notifier.mjs";
25
19
  import { normalizeRemoteMemorySources } from "../../memory/remote/config.mjs";
26
20
  import { resolveMemoryRoot } from "../../memory/root.mjs";
27
21
  import { ensureBrowserDaemon } from "../../browser/client/lifecycle.mjs";
22
+ import { registerProject } from "../../workspace/project-registry.mjs";
23
+ import { listWorkspaceSessions } from "../../workspace/session-index.mjs";
24
+ import { createWorkspaceSessionSupervisor } from "../../workspace/supervisor.mjs";
25
+ import { createWorkspaceProjectRuntime } from "../workspace/project-runtime.mjs";
26
+ import { createWorkspaceOutputRouter } from "../workspace/output-router.mjs";
28
27
 
29
- export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot, useRuntimeProcess } = {}) {
28
+ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot } = {}) {
30
29
  if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
31
30
  await ensureBrowserDaemon({ stateRoot }).catch(() => {});
32
31
 
@@ -62,28 +61,15 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
62
61
  const inputHistoryStore = createInputHistoryStore({ path: join(projectMarchDir, "input-history.json") });
63
62
  const modeState = createModeState();
64
63
  const namespace = loadOrCreateProjectId(projectMarchDir);
64
+ const currentProjectInfo = registerProject({ stateRoot, rootPath: cwd });
65
65
  const memoryRoot = resolveMemoryRoot(config.memoryRoot, stateRoot);
66
66
  const profilePaths = defaultProfilePaths();
67
67
  ensureProfileFiles(profilePaths);
68
68
  const memoryStore = new MarkdownMemoryStore({ root: memoryRoot });
69
69
  const remoteMemorySources = normalizeRemoteMemorySources(config);
70
- const memoryTools = createMarkdownMemoryTools(memoryStore, { remoteSources: remoteMemorySources });
71
70
  const currentProject = basename(cwd);
72
71
  const shellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
73
72
 
74
- const mcpInit = useRuntimeProcess
75
- ? { clientManager: null, mcpTools: [], mcpInjections: [], errors: [] }
76
- : await initializeMcp({ projectDir: cwd });
77
- for (const { server, error } of mcpInit.errors) {
78
- if (!args.json) process.stderr.write(`[mcp] ${server}: ${error}\n`);
79
- }
80
-
81
- const webTools = createWebToolsFromConfig(config);
82
- const turnNotifier = createDesktopTurnNotifier({ enabled: Boolean(config.notifications?.turnEnd), config: config.notifications });
83
- const permissionMode = args.permissionMode ?? MODE.BYPASS;
84
- const permissionController = createPermissionController({ mode: permissionMode });
85
- const usePiSessions = true;
86
- const usePiRuntimeHost = true;
87
73
  const sessionSource = "pi";
88
74
  const sessionsRoot = join(projectMarchDir, "sessions");
89
75
  const sessionState = {
@@ -92,10 +78,6 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
92
78
  };
93
79
  sessionState.sessionDir = join(sessionsRoot, sessionState.sessionId);
94
80
  const contextDumpRoot = resolve(projectMarchDir, "context-dumps", sessionState.sessionId);
95
- const modelContextDumper = createModelContextDumper({
96
- enabled: args.dumpContext,
97
- rootDir: contextDumpRoot,
98
- });
99
81
 
100
82
  const ui = createUI({
101
83
  json: args.json,
@@ -105,7 +87,8 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
105
87
  shellRuntime,
106
88
  historyStore: inputHistoryStore,
107
89
  });
108
-
90
+ const outputRouter = createWorkspaceOutputRouter({ ui, activeProjectId: currentProjectInfo.projectId, activeSessionId: sessionState.sessionId });
91
+ const runtimeUi = outputRouter.createProjectUi(currentProjectInfo.projectId, () => sessionState.sessionId);
109
92
  let turnRunning = false;
110
93
  let refreshStatusBar = null;
111
94
  const runnerOptions = {
@@ -121,22 +104,26 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
121
104
  namespace,
122
105
  projectMarchDir,
123
106
  extensionPaths,
124
- permissionMode,
125
107
  shellRuntime: Boolean(shellRuntime),
126
108
  lifecycleHooks: lifecycleManifests.hooks,
127
109
  lifecycleDiagnostics: lifecycleManifests.diagnostics,
128
110
  modelContextDumper: { enabled: args.dumpContext, rootDir: contextDumpRoot },
129
111
  remoteMemorySources,
112
+ notificationContext: { projectId: currentProjectInfo.projectId },
130
113
  };
131
114
 
132
115
  let runner;
116
+ let workspaceSupervisor = null;
117
+ const onNotificationActivation = (activation) => {
118
+ handleNotificationActivation({ activation, stateRoot, workspaceSupervisor, outputRouter, ui }).catch((err) => ui.writeln(`Notification activation failed: ${err.message}`));
119
+ };
133
120
  try {
134
121
  runner = await createRuntimeRunner({
135
- useRuntimeProcess, runnerOptions, ui, memoryStore, memoryTools, shellRuntime,
136
- mcpTools: mcpInit.mcpTools, mcpInjections: mcpInit.mcpInjections, mcpClientManager: mcpInit.clientManager, webTools,
137
- usePiSessions, usePiRuntimeHost, authStorage: authConfig.authStorage,
138
- permissionController, modelContextDumper, turnNotifier, logger,
122
+ runnerOptions,
123
+ ui: runtimeUi,
124
+ shellRuntime,
139
125
  refreshStatusBar: (...args) => refreshStatusBar?.(...args),
126
+ onNotificationActivation,
140
127
  });
141
128
  } catch (err) {
142
129
  process.stderr.write(`Error: ${err.message}\n`);
@@ -145,6 +132,42 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
145
132
  return { ok: false, code: 1, logger };
146
133
  }
147
134
 
135
+ const initialRuntime = {
136
+ project: currentProjectInfo,
137
+ cwd,
138
+ currentProject,
139
+ runner,
140
+ ui: runtimeUi,
141
+ memoryStore,
142
+ sessionState,
143
+ sessionsRoot,
144
+ projectMarchDir,
145
+ extensionPaths,
146
+ keybindingConfig,
147
+ promptTemplateConfig,
148
+ };
149
+ workspaceSupervisor = createWorkspaceSessionSupervisor({
150
+ initialRuntime,
151
+ createProjectRuntime: (project) => createWorkspaceProjectRuntime({
152
+ project,
153
+ args,
154
+ config,
155
+ stateRoot,
156
+ memoryRoot,
157
+ profilePaths,
158
+ createMemoryStore: () => new MarkdownMemoryStore({ root: memoryRoot }),
159
+ provider,
160
+ serviceTier,
161
+ model,
162
+ remoteMemorySources,
163
+ createUi: (runtimeSessionState) => outputRouter.createProjectUi(project.projectId, () => runtimeSessionState.sessionId),
164
+ refreshStatusBar: (...args) => refreshStatusBar?.(...args),
165
+ onNotificationActivation,
166
+ }),
167
+ onActivate: ({ projectId, sessionId }) => outputRouter.setActiveSession(projectId, sessionId),
168
+ });
169
+ runner = workspaceSupervisor.runner;
170
+
148
171
  refreshStatusBar = createStatusLineUpdater({
149
172
  ui,
150
173
  runner,
@@ -174,6 +197,8 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
174
197
  projectMarchDir,
175
198
  ui,
176
199
  });
200
+ workspaceSupervisor.refreshActiveRuntime();
201
+ outputRouter.setActiveSession(currentProjectInfo.projectId, sessionState.sessionId);
177
202
  refreshStatusBar();
178
203
 
179
204
  return {
@@ -182,8 +207,11 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
182
207
  cwd,
183
208
  ui,
184
209
  runner,
210
+ workspaceSupervisor,
211
+ workspaceOutputRouter: outputRouter,
185
212
  memoryStore,
186
213
  currentProject,
214
+ currentProjectInfo,
187
215
  sessionState,
188
216
  sessionsRoot,
189
217
  projectMarchDir,
@@ -199,3 +227,16 @@ export async function createCliAppRuntime({ args, config, cwd, argv, stateRoot,
199
227
  setTurnRunning(value) { turnRunning = value; },
200
228
  };
201
229
  }
230
+
231
+ async function handleNotificationActivation({ activation, stateRoot, workspaceSupervisor, outputRouter, ui }) {
232
+ if (activation?.type !== "workspace-session" || !activation.projectId) return;
233
+ if (!workspaceSupervisor) throw new Error("workspace supervisor is not ready");
234
+ const projects = await listWorkspaceSessions({ stateRoot, currentProjectId: workspaceSupervisor.getActive?.()?.project?.projectId ?? null });
235
+ const runtime = await workspaceSupervisor.activateWorkspaceSessionById({
236
+ projects,
237
+ projectId: activation.projectId,
238
+ sessionId: activation.sessionId,
239
+ });
240
+ outputRouter?.replayBufferedCalls?.(activation.projectId, activation.sessionId);
241
+ ui.writeln(`Activated session from notification: ${runtime.project.displayName}`);
242
+ }
@@ -1,61 +1,20 @@
1
- import { createRunner } from "../../agent/runner.mjs";
2
1
  import { createRunnerProcessClient } from "../../agent/runtime/runner-process-client.mjs";
3
- import { resolvePiSessionManager } from "../../session/pi-manager.mjs";
4
2
 
5
3
  export async function createRuntimeRunner({
6
- useRuntimeProcess = false,
7
4
  runnerOptions,
8
5
  ui,
9
- memoryStore,
10
- memoryTools,
11
6
  shellRuntime,
12
- mcpTools,
13
- mcpInjections,
14
- mcpClientManager,
15
- webTools,
16
- usePiSessions,
17
- usePiRuntimeHost,
18
- authStorage,
19
- permissionController,
20
- modelContextDumper,
21
- turnNotifier,
22
- logger,
23
7
  refreshStatusBar,
8
+ onNotificationActivation = null,
24
9
  } = {}) {
25
10
  const onModelPayload = ({ estimatedTokens }) => {
26
11
  refreshStatusBar?.({ contextTokens: estimatedTokens });
27
12
  };
13
+ const onLspStatusChange = () => {
14
+ refreshStatusBar?.();
15
+ };
28
16
 
29
- const runner = useRuntimeProcess
30
- ? (await createRunnerProcessClient({ runnerOptions, ui, onModelPayload })).runner
31
- : await createRunner({
32
- ...runnerOptions,
33
- ui,
34
- memoryStore,
35
- memoryTools,
36
- shellRuntime,
37
- mcpTools,
38
- mcpInjections,
39
- mcpClientManager,
40
- webTools,
41
- sessionManager: resolvePiSessionManager({
42
- cwd: runnerOptions.cwd,
43
- projectMarchDir: runnerOptions.projectMarchDir,
44
- enabled: usePiSessions,
45
- }),
46
- useRuntimeHost: usePiRuntimeHost,
47
- syncPiSidecar: usePiSessions || usePiRuntimeHost,
48
- authStorage,
49
- maxTurns: runnerOptions.config?.maxTurns ?? undefined,
50
- trimBatch: runnerOptions.config?.trimBatch ?? undefined,
51
- hostedTools: runnerOptions.config?.hostedTools,
52
- permissionController,
53
- modelContextDumper,
54
- turnNotifier,
55
- logger,
56
- onModelPayload,
57
- });
58
-
17
+ const { runner } = await createRunnerProcessClient({ runnerOptions, ui, onModelPayload, onLspStatusChange, onNotificationActivation });
59
18
  runner.shellRuntime ??= shellRuntime;
60
19
  return runner;
61
20
  }
@@ -1,20 +1,10 @@
1
- import { randomUUID } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { resolve } from "node:path";
1
+ import { loadOrCreateProjectId } from "../../workspace/project-id.mjs";
4
2
  import { listPiSessionInfos } from "../../session/pi-manager.mjs";
3
+
5
4
  import { loadPiSessionTranscriptTurns } from "../../session/transcript.mjs";
6
5
  import { resumePiSessionById } from "../session/pi-session-switch-command.mjs";
7
6
 
8
- export function loadOrCreateProjectId(projectMarchDir) {
9
- if (!existsSync(projectMarchDir)) mkdirSync(projectMarchDir, { recursive: true });
10
- const idFile = resolve(projectMarchDir, "project-id");
11
- if (existsSync(idFile)) {
12
- return readFileSync(idFile, "utf8").trim();
13
- }
14
- const id = randomUUID();
15
- writeFileSync(idFile, id, "utf8");
16
- return id;
17
- }
7
+ export { loadOrCreateProjectId };
18
8
 
19
9
  export async function resumeStartupSession({
20
10
  resumeId,
@@ -0,0 +1,56 @@
1
+ import { matchesKey } from "@earendil-works/pi-tui";
2
+
3
+ export function createHistoryNavigationController({ editor, requestRender, isAutocompleteOpen = () => false, hasOverlay = () => false } = {}) {
4
+ let draftText = null;
5
+
6
+ return {
7
+ handleInput(data) {
8
+ if (isAutocompleteOpen() || hasOverlay()) return undefined;
9
+
10
+ if (matchesKey(data, "alt+up")) return moveWithinInput(-1);
11
+ if (matchesKey(data, "alt+down")) return moveWithinInput(1);
12
+ if (matchesKey(data, "up")) return navigateHistory(-1);
13
+ if (matchesKey(data, "down")) return navigateHistory(1);
14
+
15
+ return undefined;
16
+ },
17
+ };
18
+
19
+ function navigateHistory(direction) {
20
+ const history = Array.isArray(editor?.history) ? editor.history : [];
21
+ const currentIndex = Number.isInteger(editor?.historyIndex) ? editor.historyIndex : -1;
22
+ const nextIndex = currentIndex - direction;
23
+ if (nextIndex < -1 || nextIndex >= history.length) return undefined;
24
+
25
+ if (currentIndex === -1) draftText = editor.getText?.() ?? "";
26
+ editor.lastAction = null;
27
+ editor.historyIndex = nextIndex;
28
+ setEditorTextPreservingHistory(nextIndex === -1 ? draftText ?? "" : history[nextIndex] ?? "");
29
+ if (nextIndex === -1) draftText = null;
30
+ requestRender?.();
31
+ return { consume: true };
32
+ }
33
+
34
+ function moveWithinInput(direction) {
35
+ editor.lastAction = null;
36
+ if (direction < 0) {
37
+ if (editor.isOnFirstVisualLine?.()) editor.moveToLineStart?.();
38
+ else editor.moveCursor?.(-1, 0);
39
+ } else {
40
+ if (editor.isOnLastVisualLine?.()) editor.moveToLineEnd?.();
41
+ else editor.moveCursor?.(1, 0);
42
+ }
43
+ requestRender?.();
44
+ return { consume: true };
45
+ }
46
+
47
+ function setEditorTextPreservingHistory(text) {
48
+ if (typeof editor.setTextInternal === "function") {
49
+ editor.setTextInternal(text);
50
+ return;
51
+ }
52
+ const historyIndex = editor.historyIndex;
53
+ editor.setText?.(text);
54
+ editor.historyIndex = historyIndex;
55
+ }
56
+ }
@@ -5,7 +5,6 @@ import { formatMessageAttachmentsForDisplay } from "../../session/attachment-dis
5
5
  import { formatShellHints } from "../../shell/hints.mjs";
6
6
 
7
7
  export function prepareTurnInput({ prompt, runner, memoryStore, currentProject, modeState = null }) {
8
- memoryStore.beginTurn();
9
8
  const engine = runner.engine ?? {};
10
9
  const carryoverAlreadyRendered = engine.hasRenderedPendingAssistantRecallHints?.() ?? false;
11
10
  const carryoverRecallHints = engine.takePendingAssistantRecallHints?.() ?? [];
package/src/cli/ui.mjs CHANGED
@@ -5,7 +5,6 @@ import { buildMarchCommands, MarchAutocompleteProvider } from "./input/autocompl
5
5
  import { createJsonUI, createPlainUI } from "./fallback-ui.mjs";
6
6
  import { createKeybindingDispatcher } from "./input/keybinding-dispatch.mjs";
7
7
  import { OutputBuffer } from "./tui/output-buffer.mjs";
8
- import { requestToolPermission } from "./tui/permission-request-ui.mjs";
9
8
  import { runTuiExternalEditor } from "./tui/editor/external-editor-runner.mjs";
10
9
  import { createRetryStatusController } from "./tui/status/retry-status.mjs";
11
10
  import { createShellDrawerControls } from "./shell/shell-drawer-controls.mjs";
@@ -16,6 +15,7 @@ import { showEditorSelectList } from "./tui/select/editor-select-list.mjs";
16
15
  import { StatusBar } from "./tui/status/status-bar.mjs";
17
16
  import { MainPaneLayout } from "./tui/layout/main-pane-layout.mjs";
18
17
  import { SafeRenderBoundary } from "./tui/layout/safe-render-boundary.mjs";
18
+ import { createHistoryNavigationController } from "./tui/input/history-navigation-controller.mjs";
19
19
  import { createMouseSelectionController } from "./tui/input/mouse-selection-controller.mjs";
20
20
  import { ScreenSelection } from "./tui/selection-screen.mjs";
21
21
  import { writeEditDiff } from "./tui/tui-diff-rendering.mjs";
@@ -68,6 +68,12 @@ export function createTuiUI({
68
68
  const retryStatus = createRetryStatusController({ output, requestRender, stopSpinner: spinnerStatus.stop });
69
69
  const shellDrawerControls = createShellDrawerControls({ shellDrawer, output, requestRender });
70
70
  const mouseSelectionController = createMouseSelectionController({ terminal, output, shellDrawer, shellDrawerControls, selection, writeClipboard, requestRender });
71
+ const historyNavigationController = createHistoryNavigationController({
72
+ editor,
73
+ requestRender,
74
+ isAutocompleteOpen: () => editor.isShowingAutocomplete(),
75
+ hasOverlay: () => tui.hasOverlay(),
76
+ });
71
77
 
72
78
  let onEscapeHandler = null, onCtrlCHandler = null, onShiftTabHandler = null;
73
79
  let onCtrlTHandler = null, onCtrlLHandler = null, onPasteImageHandler = null, onToggleModeHandler = null;
@@ -108,6 +114,8 @@ export function createTuiUI({
108
114
  requestRender();
109
115
  return { consume: true };
110
116
  }
117
+ const historyNavigationResult = historyNavigationController.handleInput(data);
118
+ if (historyNavigationResult) return historyNavigationResult;
111
119
  });
112
120
  terminal.write("\x1b[?1049h");
113
121
  terminal.write("\x1b[?1002h\x1b[?1006h");
@@ -239,11 +247,6 @@ export function createTuiUI({
239
247
  requestRender();
240
248
  },
241
249
 
242
- requestPermission: async ({ toolName, params, category }) => {
243
- ensureStarted();
244
- spinnerStatus.stop();
245
- return requestToolPermission({ toolName, params, category, output, selectList, requestRender });
246
- },
247
250
 
248
251
  setEscapeHandler: (fn) => { onEscapeHandler = fn; },
249
252
  setCtrlCHandler: (fn) => { onCtrlCHandler = fn; },