actoviq-agent-sdk 0.3.0 → 0.4.0

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 (204) hide show
  1. package/README-zh.md +37 -2
  2. package/README.md +37 -2
  3. package/bin/actoviq-tui.js +5 -0
  4. package/dist/src/cli/actoviq-react.js +93 -13
  5. package/dist/src/cli/actoviq-react.js.map +1 -1
  6. package/dist/src/cli/actoviq-tui.d.ts +3 -0
  7. package/dist/src/cli/actoviq-tui.d.ts.map +1 -0
  8. package/dist/src/cli/actoviq-tui.js +76 -0
  9. package/dist/src/cli/actoviq-tui.js.map +1 -0
  10. package/dist/src/config/actoviqSettingsStore.d.ts.map +1 -1
  11. package/dist/src/config/actoviqSettingsStore.js +8 -2
  12. package/dist/src/config/actoviqSettingsStore.js.map +1 -1
  13. package/dist/src/config/anthropicEnvMapping.d.ts +18 -0
  14. package/dist/src/config/anthropicEnvMapping.d.ts.map +1 -0
  15. package/dist/src/config/anthropicEnvMapping.js +49 -0
  16. package/dist/src/config/anthropicEnvMapping.js.map +1 -0
  17. package/dist/src/config/modelTiers.d.ts +12 -0
  18. package/dist/src/config/modelTiers.d.ts.map +1 -0
  19. package/dist/src/config/modelTiers.js +31 -0
  20. package/dist/src/config/modelTiers.js.map +1 -0
  21. package/dist/src/config/projectSessionDirectory.d.ts +8 -0
  22. package/dist/src/config/projectSessionDirectory.d.ts.map +1 -0
  23. package/dist/src/config/projectSessionDirectory.js +83 -0
  24. package/dist/src/config/projectSessionDirectory.js.map +1 -0
  25. package/dist/src/config/resolveRuntimeConfig.d.ts.map +1 -1
  26. package/dist/src/config/resolveRuntimeConfig.js +73 -21
  27. package/dist/src/config/resolveRuntimeConfig.js.map +1 -1
  28. package/dist/src/index.d.ts +7 -0
  29. package/dist/src/index.d.ts.map +1 -1
  30. package/dist/src/index.js +7 -0
  31. package/dist/src/index.js.map +1 -1
  32. package/dist/src/mcp/connectionManager.d.ts.map +1 -1
  33. package/dist/src/mcp/connectionManager.js +10 -1
  34. package/dist/src/mcp/connectionManager.js.map +1 -1
  35. package/dist/src/memory/actoviqMemory.d.ts.map +1 -1
  36. package/dist/src/memory/actoviqMemory.js +10 -6
  37. package/dist/src/memory/actoviqMemory.js.map +1 -1
  38. package/dist/src/memory/actoviqTranscriptLogger.d.ts.map +1 -1
  39. package/dist/src/memory/actoviqTranscriptLogger.js +2 -2
  40. package/dist/src/memory/actoviqTranscriptLogger.js.map +1 -1
  41. package/dist/src/parity/actoviqBridgeEvents.d.ts.map +1 -1
  42. package/dist/src/parity/actoviqBridgeEvents.js +4 -2
  43. package/dist/src/parity/actoviqBridgeEvents.js.map +1 -1
  44. package/dist/src/parity/actoviqBridgeSdk.d.ts.map +1 -1
  45. package/dist/src/parity/actoviqBridgeSdk.js +9 -1
  46. package/dist/src/parity/actoviqBridgeSdk.js.map +1 -1
  47. package/dist/src/parity/actoviqCleanBridgeCompatSdk.d.ts +277 -0
  48. package/dist/src/parity/actoviqCleanBridgeCompatSdk.d.ts.map +1 -0
  49. package/dist/src/parity/actoviqCleanBridgeCompatSdk.js +1395 -0
  50. package/dist/src/parity/actoviqCleanBridgeCompatSdk.js.map +1 -0
  51. package/dist/src/parity/portableSessions.d.ts.map +1 -1
  52. package/dist/src/parity/portableSessions.js +6 -2
  53. package/dist/src/parity/portableSessions.js.map +1 -1
  54. package/dist/src/provider/client.d.ts +5 -0
  55. package/dist/src/provider/client.d.ts.map +1 -1
  56. package/dist/src/provider/client.js +82 -8
  57. package/dist/src/provider/client.js.map +1 -1
  58. package/dist/src/provider/openai-client.d.ts.map +1 -1
  59. package/dist/src/provider/openai-client.js +43 -4
  60. package/dist/src/provider/openai-client.js.map +1 -1
  61. package/dist/src/provider/openai-model-api.d.ts.map +1 -1
  62. package/dist/src/provider/openai-model-api.js +3 -0
  63. package/dist/src/provider/openai-model-api.js.map +1 -1
  64. package/dist/src/provider/openai-types.d.ts +1 -0
  65. package/dist/src/provider/openai-types.d.ts.map +1 -1
  66. package/dist/src/runtime/actoviqAgentDefinitions.d.ts +8 -0
  67. package/dist/src/runtime/actoviqAgentDefinitions.d.ts.map +1 -0
  68. package/dist/src/runtime/actoviqAgentDefinitions.js +165 -0
  69. package/dist/src/runtime/actoviqAgentDefinitions.js.map +1 -0
  70. package/dist/src/runtime/actoviqAgents.d.ts +26 -2
  71. package/dist/src/runtime/actoviqAgents.d.ts.map +1 -1
  72. package/dist/src/runtime/actoviqAgents.js +237 -26
  73. package/dist/src/runtime/actoviqAgents.js.map +1 -1
  74. package/dist/src/runtime/actoviqApiMicrocompact.d.ts +14 -0
  75. package/dist/src/runtime/actoviqApiMicrocompact.d.ts.map +1 -1
  76. package/dist/src/runtime/actoviqApiMicrocompact.js +102 -0
  77. package/dist/src/runtime/actoviqApiMicrocompact.js.map +1 -1
  78. package/dist/src/runtime/actoviqBackgroundTasks.d.ts +15 -1
  79. package/dist/src/runtime/actoviqBackgroundTasks.d.ts.map +1 -1
  80. package/dist/src/runtime/actoviqBackgroundTasks.js +72 -10
  81. package/dist/src/runtime/actoviqBackgroundTasks.js.map +1 -1
  82. package/dist/src/runtime/actoviqCompact.d.ts +59 -0
  83. package/dist/src/runtime/actoviqCompact.d.ts.map +1 -1
  84. package/dist/src/runtime/actoviqCompact.js +344 -15
  85. package/dist/src/runtime/actoviqCompact.js.map +1 -1
  86. package/dist/src/runtime/actoviqModelApi.d.ts.map +1 -1
  87. package/dist/src/runtime/actoviqModelApi.js +20 -4
  88. package/dist/src/runtime/actoviqModelApi.js.map +1 -1
  89. package/dist/src/runtime/actoviqPermissions.d.ts +5 -2
  90. package/dist/src/runtime/actoviqPermissions.d.ts.map +1 -1
  91. package/dist/src/runtime/actoviqPermissions.js +91 -202
  92. package/dist/src/runtime/actoviqPermissions.js.map +1 -1
  93. package/dist/src/runtime/actoviqSessionPermissions.d.ts +5 -0
  94. package/dist/src/runtime/actoviqSessionPermissions.d.ts.map +1 -0
  95. package/dist/src/runtime/actoviqSessionPermissions.js +44 -0
  96. package/dist/src/runtime/actoviqSessionPermissions.js.map +1 -0
  97. package/dist/src/runtime/agentClient.d.ts +37 -3
  98. package/dist/src/runtime/agentClient.d.ts.map +1 -1
  99. package/dist/src/runtime/agentClient.js +1112 -100
  100. package/dist/src/runtime/agentClient.js.map +1 -1
  101. package/dist/src/runtime/agentSession.d.ts +8 -5
  102. package/dist/src/runtime/agentSession.d.ts.map +1 -1
  103. package/dist/src/runtime/agentSession.js +11 -4
  104. package/dist/src/runtime/agentSession.js.map +1 -1
  105. package/dist/src/runtime/conversationEngine.d.ts +2 -0
  106. package/dist/src/runtime/conversationEngine.d.ts.map +1 -1
  107. package/dist/src/runtime/conversationEngine.js +575 -36
  108. package/dist/src/runtime/conversationEngine.js.map +1 -1
  109. package/dist/src/runtime/defaultActoviqAgents.d.ts +3 -0
  110. package/dist/src/runtime/defaultActoviqAgents.d.ts.map +1 -0
  111. package/dist/src/runtime/defaultActoviqAgents.js +85 -0
  112. package/dist/src/runtime/defaultActoviqAgents.js.map +1 -0
  113. package/dist/src/runtime/safetyChecks.d.ts.map +1 -1
  114. package/dist/src/runtime/safetyChecks.js +15 -9
  115. package/dist/src/runtime/safetyChecks.js.map +1 -1
  116. package/dist/src/runtime/sessionManager.d.ts.map +1 -1
  117. package/dist/src/runtime/sessionManager.js +10 -5
  118. package/dist/src/runtime/sessionManager.js.map +1 -1
  119. package/dist/src/runtime/tools.d.ts.map +1 -1
  120. package/dist/src/runtime/tools.js +1 -0
  121. package/dist/src/runtime/tools.js.map +1 -1
  122. package/dist/src/scheduling/cron.d.ts.map +1 -1
  123. package/dist/src/scheduling/cron.js +3 -1
  124. package/dist/src/scheduling/cron.js.map +1 -1
  125. package/dist/src/storage/backgroundTaskStore.d.ts.map +1 -1
  126. package/dist/src/storage/backgroundTaskStore.js +3 -2
  127. package/dist/src/storage/backgroundTaskStore.js.map +1 -1
  128. package/dist/src/storage/mailboxStore.d.ts.map +1 -1
  129. package/dist/src/storage/mailboxStore.js +3 -3
  130. package/dist/src/storage/mailboxStore.js.map +1 -1
  131. package/dist/src/storage/pathSafety.d.ts +4 -0
  132. package/dist/src/storage/pathSafety.d.ts.map +1 -0
  133. package/dist/src/storage/pathSafety.js +28 -0
  134. package/dist/src/storage/pathSafety.js.map +1 -0
  135. package/dist/src/storage/sessionStore.d.ts +2 -2
  136. package/dist/src/storage/sessionStore.d.ts.map +1 -1
  137. package/dist/src/storage/sessionStore.js +23 -8
  138. package/dist/src/storage/sessionStore.js.map +1 -1
  139. package/dist/src/storage/teammateStore.d.ts.map +1 -1
  140. package/dist/src/storage/teammateStore.js +3 -2
  141. package/dist/src/storage/teammateStore.js.map +1 -1
  142. package/dist/src/swarm/actoviqSwarm.d.ts.map +1 -1
  143. package/dist/src/swarm/actoviqSwarm.js +6 -6
  144. package/dist/src/swarm/actoviqSwarm.js.map +1 -1
  145. package/dist/src/tools/actoviqCoreTools.d.ts +7 -2
  146. package/dist/src/tools/actoviqCoreTools.d.ts.map +1 -1
  147. package/dist/src/tools/actoviqCoreTools.js +2 -2
  148. package/dist/src/tools/actoviqCoreTools.js.map +1 -1
  149. package/dist/src/tools/actoviqFileTools.d.ts.map +1 -1
  150. package/dist/src/tools/actoviqFileTools.js +16 -5
  151. package/dist/src/tools/actoviqFileTools.js.map +1 -1
  152. package/dist/src/tools/actoviqNotebookEdit.d.ts.map +1 -1
  153. package/dist/src/tools/actoviqNotebookEdit.js +121 -4
  154. package/dist/src/tools/actoviqNotebookEdit.js.map +1 -1
  155. package/dist/src/tools/actoviqShellTools.d.ts.map +1 -1
  156. package/dist/src/tools/actoviqShellTools.js +38 -9
  157. package/dist/src/tools/actoviqShellTools.js.map +1 -1
  158. package/dist/src/tools/actoviqWebTools.d.ts.map +1 -1
  159. package/dist/src/tools/actoviqWebTools.js +50 -18
  160. package/dist/src/tools/actoviqWebTools.js.map +1 -1
  161. package/dist/src/tools/bash/BashTool.d.ts.map +1 -1
  162. package/dist/src/tools/bash/BashTool.js +47 -14
  163. package/dist/src/tools/bash/BashTool.js.map +1 -1
  164. package/dist/src/tools/todo/TodoWriteTool.d.ts +14 -0
  165. package/dist/src/tools/todo/TodoWriteTool.d.ts.map +1 -1
  166. package/dist/src/tools/todo/TodoWriteTool.js +45 -2
  167. package/dist/src/tools/todo/TodoWriteTool.js.map +1 -1
  168. package/dist/src/tui/actoviqTui.d.ts +13 -0
  169. package/dist/src/tui/actoviqTui.d.ts.map +1 -0
  170. package/dist/src/tui/actoviqTui.js +1594 -0
  171. package/dist/src/tui/actoviqTui.js.map +1 -0
  172. package/dist/src/tui/ansi.d.ts +33 -0
  173. package/dist/src/tui/ansi.d.ts.map +1 -0
  174. package/dist/src/tui/ansi.js +127 -0
  175. package/dist/src/tui/ansi.js.map +1 -0
  176. package/dist/src/tui/editor.d.ts +56 -0
  177. package/dist/src/tui/editor.d.ts.map +1 -0
  178. package/dist/src/tui/editor.js +253 -0
  179. package/dist/src/tui/editor.js.map +1 -0
  180. package/dist/src/tui/pluginCatalog.d.ts +13 -0
  181. package/dist/src/tui/pluginCatalog.d.ts.map +1 -0
  182. package/dist/src/tui/pluginCatalog.js +78 -0
  183. package/dist/src/tui/pluginCatalog.js.map +1 -0
  184. package/dist/src/tui/screen.d.ts +28 -0
  185. package/dist/src/tui/screen.d.ts.map +1 -0
  186. package/dist/src/tui/screen.js +119 -0
  187. package/dist/src/tui/screen.js.map +1 -0
  188. package/dist/src/tui/selection.d.ts +9 -0
  189. package/dist/src/tui/selection.d.ts.map +1 -0
  190. package/dist/src/tui/selection.js +16 -0
  191. package/dist/src/tui/selection.js.map +1 -0
  192. package/dist/src/tui/transcript.d.ts +41 -0
  193. package/dist/src/tui/transcript.d.ts.map +1 -0
  194. package/dist/src/tui/transcript.js +147 -0
  195. package/dist/src/tui/transcript.js.map +1 -0
  196. package/dist/src/types.d.ts +233 -2
  197. package/dist/src/types.d.ts.map +1 -1
  198. package/dist/src/types.js.map +1 -1
  199. package/dist/src/workflow/workflowEngine.js +26 -3
  200. package/dist/src/workflow/workflowEngine.js.map +1 -1
  201. package/dist/src/workspace/actoviqWorkspace.d.ts.map +1 -1
  202. package/dist/src/workspace/actoviqWorkspace.js +25 -2
  203. package/dist/src/workspace/actoviqWorkspace.js.map +1 -1
  204. package/package.json +26 -1
@@ -0,0 +1,1594 @@
1
+ /**
2
+ * Actoviq TUI — a full-screen-feel terminal UI for the Clean SDK, modeled on
3
+ * Claude Code's REPL: permanent transcript in native scrollback, a redrawable
4
+ * bottom region with a Claude-style prompt bar, slash-command menu, streaming
5
+ * output, permission dialogs, and mid-run steering. Dependency-free ANSI
6
+ * rendering (no React/Ink).
7
+ */
8
+ import { execSync } from 'node:child_process';
9
+ import path from 'node:path';
10
+ import * as readline from 'node:readline';
11
+ import { createActoviqCoreTools, createAgentSdk, loadDefaultActoviqSettings, loadJsonConfigFile, } from '../index.js';
12
+ import { persistActoviqSettingsStore, resolveActoviqSettingsStore, } from '../config/actoviqSettingsStore.js';
13
+ import { isRecord } from '../runtime/helpers.js';
14
+ import { A, stringWidth, truncateToWidth } from './ansi.js';
15
+ import { InputEditor } from './editor.js';
16
+ import { discoverActoviqPlugins } from './pluginCatalog.js';
17
+ import { TuiScreen } from './screen.js';
18
+ import { filterTuiSelectionItems, moveTuiSelection, } from './selection.js';
19
+ import { StreamFlusher, formatBanner, formatCompactNotice, formatErrorLine, formatInfoLine, formatQueuedPrompt, formatThinking, formatToolCall, formatToolResult, formatUserPrompt, } from './transcript.js';
20
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
21
+ const CTRL_C_EXIT_WINDOW_MS = 600;
22
+ const DYNAMIC_FRAME_MS = 33;
23
+ const MENU_MAX_ROWS = 12;
24
+ const PROMPT_GLYPH = '❯';
25
+ const SESSION_EFFORT_KEY = '__actoviqEffort';
26
+ const EFFORT_LEVELS = ['low', 'medium', 'high', 'max'];
27
+ /** Core tools that mutate state and require approval in 'default' mode. */
28
+ const MUTATING_TOOLS = new Set(['Bash', 'Write', 'Edit', 'NotebookEdit']);
29
+ const PERMISSION_MODES = new Set([
30
+ 'default',
31
+ 'acceptEdits',
32
+ 'plan',
33
+ 'bypassPermissions',
34
+ 'auto',
35
+ ]);
36
+ export const TUI_SLASH_COMMANDS = {
37
+ help: 'Show available commands',
38
+ clear: 'Clear the screen',
39
+ compact: 'Compact the current session',
40
+ memory: 'Show memory/compact state',
41
+ model: 'Select a model or configure its provider',
42
+ effort: 'Select the reasoning effort',
43
+ permissions: 'Show or set the permission mode',
44
+ sessions: 'List stored sessions',
45
+ resume: 'Resume a stored session',
46
+ tools: 'List available tools',
47
+ skills: 'Browse available skills',
48
+ agents: 'Browse available subagents',
49
+ mcp: 'Inspect MCP servers and tools',
50
+ plugins: 'Browse discovered Clean plugins',
51
+ dream: 'Inspect or run memory consolidation',
52
+ exit: 'Quit',
53
+ };
54
+ export function filterSlashCommands(input) {
55
+ if (!input.startsWith('/'))
56
+ return [];
57
+ const head = input.slice(1).split(/\s/, 1)[0] ?? '';
58
+ if (input.includes(' ') && head.length > 0)
59
+ return [];
60
+ const partial = head.toLowerCase();
61
+ return Object.keys(TUI_SLASH_COMMANDS).filter((name) => name.startsWith(partial));
62
+ }
63
+ function buildSystemPrompt(workDir) {
64
+ let isGit = false;
65
+ try {
66
+ execSync('git rev-parse --is-inside-work-tree', { cwd: workDir, stdio: 'ignore' });
67
+ isGit = true;
68
+ }
69
+ catch {
70
+ // not a git repo
71
+ }
72
+ return (`You are Hadamard Agent, an interactive CLI agent. Working directory: ${workDir}\n\n` +
73
+ `<env>\nWorking directory: ${workDir}\nIs git repo: ${isGit ? 'Yes' : 'No'}\nPlatform: ${process.platform}\nDate: ${new Date().toISOString().slice(0, 10)}\n</env>\n\n` +
74
+ `# Tone and style\n` +
75
+ `- Only use emojis if the user explicitly requests it.\n` +
76
+ `- Your responses should be short and concise.\n` +
77
+ `- When referencing code include the pattern file_path:line_number.\n\n` +
78
+ `# Doing tasks\n` +
79
+ `- Prefer editing existing files to creating new ones.\n` +
80
+ `- Do not add features, refactor, or introduce abstractions beyond what the task requires.\n` +
81
+ `- Default to writing no comments.\n\n` +
82
+ `# Git Safety Protocol\n` +
83
+ `- NEVER update the git config\n` +
84
+ `- NEVER run destructive git commands unless the user explicitly requests\n` +
85
+ `- NEVER skip hooks unless the user explicitly requests it\n` +
86
+ `- NEVER commit changes unless the user explicitly asks you to\n\n` +
87
+ `# Other\n` +
88
+ `- NEVER create documentation files (*.md) unless explicitly requested.\n` +
89
+ `- When in doubt, use TodoWrite to track progress.`);
90
+ }
91
+ export async function runActoviqTui(options = {}) {
92
+ const workDir = path.resolve(options.workDir ?? process.cwd());
93
+ const permissionMode = options.permissionMode ?? 'bypassPermissions';
94
+ const systemPrompt = buildSystemPrompt(workDir);
95
+ try {
96
+ if (options.configPath) {
97
+ await loadJsonConfigFile(options.configPath);
98
+ }
99
+ else {
100
+ await loadDefaultActoviqSettings();
101
+ }
102
+ }
103
+ catch {
104
+ // Missing local config is fine; env vars may carry credentials.
105
+ }
106
+ const tools = createActoviqCoreTools({ cwd: workDir });
107
+ const createCleanSdk = () => createAgentSdk({
108
+ workDir,
109
+ tools,
110
+ permissionMode,
111
+ ...(options.model ? { model: options.model } : {}),
112
+ });
113
+ let sdk = await createCleanSdk();
114
+ let toolMetadata = await sdk.listToolMetadata();
115
+ let session = options.resumeSessionId
116
+ ? await sdk.resumeSession(options.resumeSessionId, {
117
+ model: options.model,
118
+ permissionMode: options.permissionMode,
119
+ })
120
+ : options.continueMostRecent
121
+ ? await sdk.sessions.continueMostRecent({
122
+ model: options.model,
123
+ permissionMode: options.permissionMode,
124
+ })
125
+ : await sdk.createSession({
126
+ title: path.basename(workDir),
127
+ model: options.model,
128
+ permissionMode,
129
+ });
130
+ const screen = new TuiScreen(process.stdout);
131
+ const editor = new InputEditor();
132
+ const flusher = new StreamFlusher(() => screen.width);
133
+ let running = false;
134
+ let commandBusy = false;
135
+ let shuttingDown = false;
136
+ let abortCtrl = null;
137
+ let dialog = null;
138
+ let selectionDialog = null;
139
+ let textInputDialog = null;
140
+ let menuSelected = 0;
141
+ let spinnerFrame = 0;
142
+ let spinnerTimer = null;
143
+ let dynamicRenderTimer = null;
144
+ let runStartedAt = 0;
145
+ let runToolCount = 0;
146
+ let lastTokenEstimate;
147
+ let statusNote = '';
148
+ let ctrlCCount = 0;
149
+ let ctrlCTimer = null;
150
+ let streamedTextSeen = false;
151
+ const queuedInputs = [];
152
+ const currentPermissionMode = () => session.permissionContext.mode ?? permissionMode;
153
+ const currentEffort = () => {
154
+ const stored = session.metadata[SESSION_EFFORT_KEY];
155
+ if (stored === 'auto')
156
+ return 'auto';
157
+ return isActoviqEffort(stored) ? stored : sdk.config.effort;
158
+ };
159
+ function isActoviqEffort(value) {
160
+ return typeof value === 'string' && EFFORT_LEVELS.includes(value);
161
+ }
162
+ function selectItem(options) {
163
+ if (options.items.length === 0) {
164
+ return Promise.resolve(undefined);
165
+ }
166
+ return new Promise(resolve => {
167
+ selectionDialog = {
168
+ title: options.title,
169
+ subtitle: options.subtitle,
170
+ items: options.items,
171
+ selected: 0,
172
+ query: '',
173
+ searchable: options.searchable !== false,
174
+ resolve,
175
+ };
176
+ renderDynamic();
177
+ });
178
+ }
179
+ function promptText(options) {
180
+ return new Promise(resolve => {
181
+ const inputEditor = new InputEditor();
182
+ if (options.initial)
183
+ inputEditor.setText(options.initial);
184
+ textInputDialog = {
185
+ title: options.title,
186
+ label: options.label,
187
+ description: options.description,
188
+ editor: inputEditor,
189
+ secret: options.secret === true,
190
+ resolve,
191
+ };
192
+ renderDynamic();
193
+ });
194
+ }
195
+ async function reloadCleanSdk() {
196
+ const previousSdk = sdk;
197
+ const nextSdk = await createCleanSdk();
198
+ try {
199
+ const nextSession = await nextSdk.resumeSession(session.id);
200
+ const nextToolMetadata = await nextSdk.listToolMetadata();
201
+ sdk = nextSdk;
202
+ session = nextSession;
203
+ toolMetadata = nextToolMetadata;
204
+ }
205
+ catch (error) {
206
+ await nextSdk.close().catch(() => undefined);
207
+ throw error;
208
+ }
209
+ await previousSdk.close().catch(() => undefined);
210
+ }
211
+ const approver = async (context) => {
212
+ const outcome = await new Promise((resolve) => {
213
+ dialog = {
214
+ toolName: context.publicName,
215
+ summary: summarizeForDialog(context.input),
216
+ selected: 0,
217
+ resolve,
218
+ };
219
+ renderDynamic();
220
+ });
221
+ dialog = null;
222
+ renderDynamic();
223
+ if (outcome === 'always') {
224
+ const state = session.permissionContext;
225
+ const permissions = state.permissions.filter(rule => !(rule.toolName === context.publicName && rule.behavior === 'allow'));
226
+ permissions.push({
227
+ toolName: context.publicName,
228
+ behavior: 'allow',
229
+ source: 'tui-session',
230
+ });
231
+ await session.setPermissionContext({
232
+ mode: state.mode ?? permissionMode,
233
+ permissions,
234
+ approver,
235
+ });
236
+ return { behavior: 'allow', reason: 'Approved (always) in TUI.' };
237
+ }
238
+ return outcome === 'allow'
239
+ ? { behavior: 'allow', reason: 'Approved in TUI.' }
240
+ : { behavior: 'deny', reason: 'Denied in TUI permission dialog.' };
241
+ };
242
+ const canUseTool = permissionMode === 'default'
243
+ ? (context) => {
244
+ if (MUTATING_TOOLS.has(context.publicName)) {
245
+ return { behavior: 'ask', reason: `${context.publicName} mutates the workspace.` };
246
+ }
247
+ return undefined;
248
+ }
249
+ : undefined;
250
+ function summarizeForDialog(input) {
251
+ if (typeof input !== 'object' || input === null)
252
+ return '';
253
+ const record = input;
254
+ for (const key of ['command', 'file_path', 'notebook_path', 'url', 'path']) {
255
+ if (typeof record[key] === 'string')
256
+ return record[key];
257
+ }
258
+ try {
259
+ return JSON.stringify(record);
260
+ }
261
+ catch {
262
+ return '';
263
+ }
264
+ }
265
+ // ── Dynamic region rendering ───────────────────────────────────────
266
+ /** Wrap content into `│ {content padded to width-4} │` with a border color. */
267
+ function boxRow(content, borderColor) {
268
+ const inner = Math.max(screen.width - 4, 8);
269
+ const contentWidth = stringWidth(content);
270
+ const padded = contentWidth > inner
271
+ ? truncateToWidth(content, inner)
272
+ : content + ' '.repeat(inner - contentWidth);
273
+ return `${borderColor}│${A.reset} ${padded} ${borderColor}│${A.reset}`;
274
+ }
275
+ function boxTop(borderColor) {
276
+ return `${borderColor}╭${'─'.repeat(Math.max(screen.width - 2, 2))}╮${A.reset}`;
277
+ }
278
+ function boxBottom(borderColor) {
279
+ return `${borderColor}╰${'─'.repeat(Math.max(screen.width - 2, 2))}╯${A.reset}`;
280
+ }
281
+ function promptDivider() {
282
+ return `${A.gray}${'─'.repeat(Math.max(screen.width, 8))}${A.reset}`;
283
+ }
284
+ /** Insert an inverse-video caret at a display column of a plain line. */
285
+ function withCaret(line, caretCol) {
286
+ let width = 0;
287
+ let index = 0;
288
+ for (const char of line) {
289
+ if (width >= caretCol)
290
+ break;
291
+ width += stringWidth(char);
292
+ index += char.length;
293
+ }
294
+ const before = line.slice(0, index);
295
+ const rest = line.slice(index);
296
+ const caretChar = rest.length > 0 ? [...rest][0] : ' ';
297
+ const after = rest.length > 0 ? rest.slice(caretChar.length) : '';
298
+ return `${before}${A.inverse}${caretChar}${A.reset}${after}`;
299
+ }
300
+ function buildPromptBar() {
301
+ const editorWidth = Math.max(screen.width - 4, 8); // '> ' prefix on the first row
302
+ const lines = [];
303
+ lines.push(promptDivider());
304
+ if (editor.isEmpty()) {
305
+ const placeholder = truncateToWidth('Try "write a test for <filepath>"', editorWidth - 2);
306
+ lines.push(`${A.magenta}${PROMPT_GLYPH}${A.reset} ${A.dim}${placeholder}${A.reset}`);
307
+ }
308
+ else {
309
+ const visual = editor.visualLines(editorWidth - 1);
310
+ visual.lines.forEach((line, row) => {
311
+ const prefix = row === 0 ? `${A.magenta}${PROMPT_GLYPH}${A.reset} ` : ' ';
312
+ const body = row === visual.cursorRow ? withCaret(line, visual.cursorCol) : line;
313
+ lines.push(`${prefix}${body}`);
314
+ });
315
+ }
316
+ lines.push(promptDivider());
317
+ return lines;
318
+ }
319
+ function buildMenu() {
320
+ const matches = filterSlashCommands(editor.text);
321
+ if (matches.length === 0)
322
+ return [];
323
+ if (menuSelected >= matches.length)
324
+ menuSelected = matches.length - 1;
325
+ const commandWidth = Math.min(28, Math.max(14, Math.floor(screen.width * 0.28)));
326
+ const descriptionWidth = Math.max(screen.width - commandWidth - 4, 12);
327
+ return matches.slice(0, MENU_MAX_ROWS).map((name, index) => {
328
+ const selected = index === menuSelected;
329
+ const command = `/${name}`.padEnd(commandWidth);
330
+ const label = selected ? `${A.inverse}${command}${A.reset}` : command;
331
+ const description = truncateToWidth(TUI_SLASH_COMMANDS[name] ?? '', descriptionWidth);
332
+ return `${label} ${A.dim}${description}${A.reset}`;
333
+ });
334
+ }
335
+ function buildDialog() {
336
+ if (!dialog)
337
+ return [];
338
+ const inner = Math.max(screen.width - 4, 8);
339
+ const options = ['Yes', `Yes, always allow ${dialog.toolName}`, 'No (esc)'];
340
+ const lines = [];
341
+ lines.push(boxTop(A.yellow));
342
+ lines.push(boxRow(`${A.bold}Permission required · ${dialog.toolName}${A.reset}`, A.yellow));
343
+ lines.push(boxRow(`${A.dim}${truncateToWidth(dialog.summary || '(no arguments)', inner)}${A.reset}`, A.yellow));
344
+ options.forEach((option, index) => {
345
+ const selected = index === dialog.selected;
346
+ lines.push(boxRow(selected ? `${A.inverse} ${option} ${A.reset}` : ` ${option}`, A.yellow));
347
+ });
348
+ lines.push(boxBottom(A.yellow));
349
+ lines.push(`${A.dim} y/enter approve · a always · n/esc deny · ↑↓ select${A.reset}`);
350
+ return lines;
351
+ }
352
+ function buildSelectionDialog() {
353
+ if (!selectionDialog)
354
+ return [];
355
+ const filtered = filterTuiSelectionItems(selectionDialog.items, selectionDialog.query);
356
+ if (selectionDialog.selected >= filtered.length) {
357
+ selectionDialog.selected = Math.max(filtered.length - 1, 0);
358
+ }
359
+ const lines = [
360
+ boxTop(A.cyan),
361
+ boxRow(`${A.bold}${selectionDialog.title}${A.reset}`, A.cyan),
362
+ ];
363
+ if (selectionDialog.subtitle) {
364
+ lines.push(boxRow(`${A.dim}${selectionDialog.subtitle}${A.reset}`, A.cyan));
365
+ }
366
+ if (selectionDialog.searchable) {
367
+ const query = selectionDialog.query || 'type to filter';
368
+ lines.push(boxRow(`${A.magenta}›${A.reset} ${selectionDialog.query ? query : `${A.dim}${query}${A.reset}`}`, A.cyan));
369
+ }
370
+ if (filtered.length === 0) {
371
+ lines.push(boxRow(`${A.dim}No matching items${A.reset}`, A.cyan));
372
+ }
373
+ else {
374
+ const visibleRows = Math.min(10, Math.max((process.stdout.rows ?? 24) - 10, 4));
375
+ const start = Math.max(0, Math.min(selectionDialog.selected - Math.floor(visibleRows / 2), filtered.length - visibleRows));
376
+ for (let index = start; index < Math.min(start + visibleRows, filtered.length); index += 1) {
377
+ const item = filtered[index];
378
+ const description = item.description ? ` · ${item.description}` : '';
379
+ const label = truncateToWidth(`${item.label}${description}`, Math.max(screen.width - 8, 8));
380
+ lines.push(boxRow(index === selectionDialog.selected
381
+ ? `${A.inverse} ${label} ${A.reset}`
382
+ : ` ${label}`, A.cyan));
383
+ }
384
+ }
385
+ lines.push(boxBottom(A.cyan));
386
+ lines.push(`${A.dim} ↑↓ select · enter confirm · esc cancel${selectionDialog.searchable ? ' · type to filter' : ''}${A.reset}`);
387
+ return lines;
388
+ }
389
+ function buildTextInputDialog() {
390
+ if (!textInputDialog)
391
+ return [];
392
+ const value = textInputDialog.secret
393
+ ? '•'.repeat(textInputDialog.editor.text.length)
394
+ : textInputDialog.editor.text;
395
+ const displayed = withCaret(value, textInputDialog.editor.cursor);
396
+ const lines = [
397
+ boxTop(A.cyan),
398
+ boxRow(`${A.bold}${textInputDialog.title}${A.reset}`, A.cyan),
399
+ ];
400
+ if (textInputDialog.description) {
401
+ lines.push(boxRow(`${A.dim}${textInputDialog.description}${A.reset}`, A.cyan));
402
+ }
403
+ lines.push(boxRow(`${textInputDialog.label}: ${displayed}`, A.cyan));
404
+ lines.push(boxBottom(A.cyan));
405
+ lines.push(`${A.dim} enter confirm · esc cancel${textInputDialog.secret ? ' · value hidden' : ''}${A.reset}`);
406
+ return lines;
407
+ }
408
+ function buildStatusLine() {
409
+ if (!running)
410
+ return [];
411
+ const elapsed = Math.max(Math.round((Date.now() - runStartedAt) / 1000), 0);
412
+ const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
413
+ const context = lastTokenEstimate && lastTokenEstimate >= 1000
414
+ ? ` · ~${Math.round(lastTokenEstimate / 1000)}k ctx`
415
+ : '';
416
+ const note = statusNote ? ` · ${statusNote}` : '';
417
+ const queued = queuedInputs.length > 0 ? ` · ${queuedInputs.length} queued` : '';
418
+ return [
419
+ `${A.cyan}${frame}${A.reset} ${A.bold}Working…${A.reset}${A.dim} (${elapsed}s · ${runToolCount} tool${runToolCount === 1 ? '' : 's'}${context}${note}${queued} · esc to interrupt)${A.reset}`,
420
+ ];
421
+ }
422
+ function buildHintLine() {
423
+ if (running) {
424
+ return [`${A.dim} enter to queue a steering message · esc interrupt · ctrl+c twice to exit${A.reset}`];
425
+ }
426
+ return [`${A.dim} ? for shortcuts · / for commands · \\↵ newline · ↑↓ history · ctrl+c clear/exit${A.reset}`];
427
+ }
428
+ function renderDynamic() {
429
+ const lines = [];
430
+ lines.push(...buildStatusLine());
431
+ const tail = flusher.tail();
432
+ if (running && tail) {
433
+ lines.push(tail);
434
+ }
435
+ if (dialog) {
436
+ lines.push(...buildDialog());
437
+ }
438
+ else if (selectionDialog) {
439
+ lines.push(...buildSelectionDialog());
440
+ }
441
+ else if (textInputDialog) {
442
+ lines.push(...buildTextInputDialog());
443
+ }
444
+ else {
445
+ lines.push(...buildPromptBar());
446
+ const menu = buildMenu();
447
+ lines.push(...(menu.length > 0 ? menu : buildHintLine()));
448
+ }
449
+ screen.setDynamic(lines);
450
+ }
451
+ function scheduleDynamicRender() {
452
+ if (dynamicRenderTimer)
453
+ return;
454
+ dynamicRenderTimer = setTimeout(() => {
455
+ dynamicRenderTimer = null;
456
+ renderDynamic();
457
+ }, DYNAMIC_FRAME_MS);
458
+ }
459
+ function cancelScheduledDynamicRender() {
460
+ if (!dynamicRenderTimer)
461
+ return;
462
+ clearTimeout(dynamicRenderTimer);
463
+ dynamicRenderTimer = null;
464
+ }
465
+ function appendStatic(lines) {
466
+ screen.appendStatic(lines);
467
+ }
468
+ // ── Agent run ──────────────────────────────────────────────────────
469
+ async function startRun(text) {
470
+ running = true;
471
+ runStartedAt = Date.now();
472
+ runToolCount = 0;
473
+ statusNote = '';
474
+ streamedTextSeen = false;
475
+ lastTokenEstimate = undefined;
476
+ abortCtrl = new AbortController();
477
+ spinnerTimer = setInterval(() => {
478
+ spinnerFrame += 1;
479
+ renderDynamic();
480
+ }, 120);
481
+ appendStatic(formatUserPrompt(text));
482
+ renderDynamic();
483
+ try {
484
+ const stream = session.stream(text, {
485
+ systemPrompt,
486
+ signal: abortCtrl.signal,
487
+ permissionMode: currentPermissionMode(),
488
+ effort: currentEffort(),
489
+ approver,
490
+ ...(canUseTool ? { canUseTool } : {}),
491
+ drainQueuedInputs: () => {
492
+ const drained = queuedInputs.splice(0);
493
+ return drained;
494
+ },
495
+ });
496
+ for await (const event of stream) {
497
+ handleAgentEvent(event);
498
+ }
499
+ const result = await stream.result;
500
+ const rest = flusher.drain();
501
+ if (rest.length > 0)
502
+ appendStatic(rest);
503
+ if (!flusher.hasContent && result.text && runHadNoStreamedText()) {
504
+ appendStatic([result.text]);
505
+ }
506
+ if (result.incompleteReason) {
507
+ appendStatic(formatInfoLine(`run incomplete: ${result.incompleteReason}`));
508
+ }
509
+ appendStatic(['']);
510
+ }
511
+ catch (error) {
512
+ const rest = flusher.drain();
513
+ if (rest.length > 0)
514
+ appendStatic(rest);
515
+ const err = error;
516
+ if (err.name === 'RunAbortedError' || err.name === 'AbortError' || abortCtrl?.signal.aborted) {
517
+ appendStatic([`${A.yellow}⏹ interrupted${A.reset}`, '']);
518
+ }
519
+ else {
520
+ appendStatic([...formatErrorLine(err.message), '']);
521
+ }
522
+ }
523
+ finally {
524
+ running = false;
525
+ abortCtrl = null;
526
+ if (spinnerTimer) {
527
+ clearInterval(spinnerTimer);
528
+ spinnerTimer = null;
529
+ }
530
+ cancelScheduledDynamicRender();
531
+ renderDynamic();
532
+ }
533
+ // Steering messages typed too late to drain mid-run become the next turn.
534
+ if (queuedInputs.length > 0 && !shuttingDown) {
535
+ const next = queuedInputs.splice(0).join('\n');
536
+ await startRun(next);
537
+ }
538
+ }
539
+ function runHadNoStreamedText() {
540
+ return !streamedTextSeen;
541
+ }
542
+ function handleAgentEvent(event) {
543
+ switch (event.type) {
544
+ case 'run.started':
545
+ streamedTextSeen = false;
546
+ return;
547
+ case 'request.started':
548
+ lastTokenEstimate = event.requestTokenEstimate;
549
+ renderDynamic();
550
+ return;
551
+ case 'response.text.delta': {
552
+ const delta = typeof event.delta === 'string' ? event.delta : '';
553
+ if (!delta)
554
+ return;
555
+ streamedTextSeen = true;
556
+ const flushed = flusher.push(delta);
557
+ if (flushed.length > 0)
558
+ appendStatic(flushed);
559
+ scheduleDynamicRender();
560
+ return;
561
+ }
562
+ case 'response.content':
563
+ if (event.content.type === 'thinking') {
564
+ const thinking = event.content.thinking ?? '';
565
+ const lines = formatThinking(thinking, screen.width);
566
+ if (lines.length > 0)
567
+ appendStatic(lines);
568
+ }
569
+ return;
570
+ case 'tool.call': {
571
+ const pending = flusher.drain();
572
+ if (pending.length > 0)
573
+ appendStatic(pending);
574
+ runToolCount += 1;
575
+ statusNote = event.call.publicName;
576
+ appendStatic(formatToolCall(event.call.publicName, event.call.input, screen.width));
577
+ renderDynamic();
578
+ return;
579
+ }
580
+ case 'tool.progress': {
581
+ const data = event.data;
582
+ if (data?.message) {
583
+ statusNote = data.message;
584
+ scheduleDynamicRender();
585
+ }
586
+ return;
587
+ }
588
+ case 'tool.result':
589
+ statusNote = '';
590
+ appendStatic(formatToolResult({
591
+ isError: event.result.isError,
592
+ durationMs: event.result.durationMs,
593
+ outputText: event.result.outputText,
594
+ }, screen.width));
595
+ renderDynamic();
596
+ return;
597
+ case 'conversation.compacted':
598
+ appendStatic(formatCompactNotice(event.trigger ?? 'auto', event.tokenEstimateBefore, event.tokenEstimateAfter));
599
+ return;
600
+ case 'session.compacted':
601
+ appendStatic(formatCompactNotice(event.trigger));
602
+ return;
603
+ case 'tool.permission':
604
+ if (event.decision.behavior === 'deny') {
605
+ appendStatic(formatInfoLine(`permission denied: ${event.decision.publicName} — ${event.decision.reason}`));
606
+ }
607
+ return;
608
+ case 'error':
609
+ appendStatic(formatErrorLine(event.error.message));
610
+ return;
611
+ default:
612
+ return;
613
+ }
614
+ }
615
+ // ── Slash commands ─────────────────────────────────────────────────
616
+ function commandUsage(name) {
617
+ const usage = {
618
+ help: '/help',
619
+ clear: '/clear',
620
+ compact: '/compact [summary instructions]',
621
+ memory: '/memory',
622
+ model: '/model [model|min|medium|max|default|config]',
623
+ effort: '/effort [low|medium|high|max|auto]',
624
+ permissions: '/permissions [default|acceptEdits|plan|bypassPermissions|auto]',
625
+ sessions: '/sessions',
626
+ resume: '/resume [session-id]',
627
+ tools: '/tools',
628
+ skills: '/skills',
629
+ agents: '/agents',
630
+ mcp: '/mcp',
631
+ plugins: '/plugins',
632
+ dream: '/dream [run|status]',
633
+ exit: '/exit',
634
+ };
635
+ return usage[name] ?? `/${name}`;
636
+ }
637
+ async function resumeSession(sessionId) {
638
+ session = await sdk.resumeSession(sessionId);
639
+ appendStatic([
640
+ ...formatInfoLine(`resumed: ${session.id} · ${session.title} · ${session.model}`),
641
+ '',
642
+ ]);
643
+ }
644
+ async function chooseSessionToResume() {
645
+ const sessions = (await sdk.sessions.list()).filter(item => item.id !== session.id);
646
+ if (sessions.length === 0) {
647
+ appendStatic([...formatInfoLine('no other project sessions to resume'), '']);
648
+ return;
649
+ }
650
+ const selected = await selectItem({
651
+ title: 'Resume a project session',
652
+ subtitle: sdk.config.sessionDirectory,
653
+ items: sessions.map(item => ({
654
+ id: item.id,
655
+ label: item.title,
656
+ description: [
657
+ item.model,
658
+ item.status,
659
+ new Date(item.lastRunAt ?? item.updatedAt).toLocaleString(),
660
+ ].join(' · '),
661
+ detail: item.preview,
662
+ })),
663
+ });
664
+ if (selected)
665
+ await resumeSession(selected);
666
+ }
667
+ async function chooseModel() {
668
+ const items = [
669
+ {
670
+ id: 'default',
671
+ label: 'Configured default',
672
+ description: sdk.config.model,
673
+ },
674
+ ...['min', 'medium', 'max']
675
+ .filter(tier => Boolean(sdk.config.modelTiers[tier]))
676
+ .map(tier => ({
677
+ id: `tier:${tier}`,
678
+ label: tier,
679
+ description: sdk.config.modelTiers[tier],
680
+ })),
681
+ {
682
+ id: 'custom',
683
+ label: 'Enter a model ID',
684
+ description: 'Session override',
685
+ },
686
+ {
687
+ id: 'configure',
688
+ label: 'Configure provider, API key, and models',
689
+ description: 'Updates the active Hadamard Agent settings file',
690
+ },
691
+ ];
692
+ const selected = await selectItem({
693
+ title: 'Select model',
694
+ subtitle: `Current: ${session.model}`,
695
+ items,
696
+ searchable: false,
697
+ });
698
+ if (!selected)
699
+ return;
700
+ if (selected === 'configure') {
701
+ await configureModelSettings();
702
+ return;
703
+ }
704
+ if (selected === 'custom') {
705
+ const model = await promptText({
706
+ title: 'Custom model',
707
+ label: 'Model ID',
708
+ initial: session.model,
709
+ });
710
+ if (!model?.trim())
711
+ return;
712
+ await session.setModel(model.trim());
713
+ }
714
+ else if (selected === 'default') {
715
+ await session.setModel(sdk.config.model);
716
+ }
717
+ else {
718
+ await session.setModel(selected.slice('tier:'.length));
719
+ }
720
+ appendStatic([...formatInfoLine(`model set to: ${session.model}`), '']);
721
+ }
722
+ async function configureModelSettings() {
723
+ const store = await resolveActoviqSettingsStore({ configPath: options.configPath });
724
+ const raw = structuredClone(store.raw);
725
+ const env = {};
726
+ for (const [key, value] of Object.entries(raw)) {
727
+ if (/^[A-Z0-9_]+$/.test(key) && typeof value === 'string')
728
+ env[key] = value;
729
+ }
730
+ if (isRecord(raw.env)) {
731
+ for (const [key, value] of Object.entries(raw.env)) {
732
+ if (typeof value === 'string')
733
+ env[key] = value;
734
+ }
735
+ }
736
+ raw.env = env;
737
+ let dirty = false;
738
+ while (true) {
739
+ const selected = await selectItem({
740
+ title: 'Model and provider settings',
741
+ subtitle: store.configPath,
742
+ searchable: false,
743
+ items: [
744
+ {
745
+ id: 'provider',
746
+ label: 'Provider',
747
+ description: env.ACTOVIQ_PROVIDER ?? sdk.config.provider,
748
+ },
749
+ {
750
+ id: 'api-key',
751
+ label: 'API key',
752
+ description: env.ACTOVIQ_API_KEY || env.ACTOVIQ_AUTH_TOKEN ? 'configured' : 'not configured',
753
+ },
754
+ {
755
+ id: 'base-url',
756
+ label: 'Base URL',
757
+ description: env.ACTOVIQ_BASE_URL || 'provider default',
758
+ },
759
+ ...['min', 'medium', 'max'].map(tier => ({
760
+ id: `tier:${tier}`,
761
+ label: `${tier} model`,
762
+ description: env[`ACTOVIQ_DEFAULT_${tier.toUpperCase()}_MODEL`] || 'not configured',
763
+ })),
764
+ {
765
+ id: 'save',
766
+ label: 'Save and apply',
767
+ description: dirty ? 'Unsaved changes' : 'No changes',
768
+ },
769
+ { id: 'cancel', label: 'Cancel' },
770
+ ],
771
+ });
772
+ if (!selected || selected === 'cancel')
773
+ return;
774
+ if (selected === 'save') {
775
+ if (!dirty) {
776
+ appendStatic([...formatInfoLine('model settings unchanged'), '']);
777
+ return;
778
+ }
779
+ await persistActoviqSettingsStore(store.configPath, raw);
780
+ await loadJsonConfigFile(store.configPath);
781
+ await reloadCleanSdk();
782
+ appendStatic([
783
+ ...formatInfoLine(`model settings saved: ${store.configPath}`),
784
+ '',
785
+ ]);
786
+ return;
787
+ }
788
+ if (selected === 'provider') {
789
+ const provider = await selectItem({
790
+ title: 'Select provider protocol',
791
+ searchable: false,
792
+ items: [
793
+ { id: 'anthropic', label: 'Anthropic-compatible' },
794
+ { id: 'openai', label: 'OpenAI-compatible' },
795
+ ],
796
+ });
797
+ if (provider) {
798
+ env.ACTOVIQ_PROVIDER = provider;
799
+ dirty = true;
800
+ }
801
+ continue;
802
+ }
803
+ if (selected === 'api-key') {
804
+ const apiKey = await promptText({
805
+ title: 'Configure API key',
806
+ label: 'API key',
807
+ description: 'The value is masked and will not be written to the transcript.',
808
+ secret: true,
809
+ });
810
+ if (apiKey?.trim()) {
811
+ env.ACTOVIQ_API_KEY = apiKey.trim();
812
+ delete env.ACTOVIQ_AUTH_TOKEN;
813
+ dirty = true;
814
+ }
815
+ continue;
816
+ }
817
+ if (selected === 'base-url') {
818
+ const baseUrl = await promptText({
819
+ title: 'Configure base URL',
820
+ label: 'Base URL',
821
+ description: 'Leave empty to use the provider default.',
822
+ initial: env.ACTOVIQ_BASE_URL ?? '',
823
+ });
824
+ if (baseUrl !== undefined) {
825
+ if (baseUrl.trim())
826
+ env.ACTOVIQ_BASE_URL = baseUrl.trim();
827
+ else
828
+ delete env.ACTOVIQ_BASE_URL;
829
+ dirty = true;
830
+ }
831
+ continue;
832
+ }
833
+ if (selected.startsWith('tier:')) {
834
+ const tier = selected.slice('tier:'.length).toUpperCase();
835
+ const key = `ACTOVIQ_DEFAULT_${tier}_MODEL`;
836
+ const model = await promptText({
837
+ title: `Configure ${tier.toLowerCase()} model`,
838
+ label: 'Model ID',
839
+ initial: env[key] ?? '',
840
+ });
841
+ if (model !== undefined) {
842
+ if (model.trim())
843
+ env[key] = model.trim();
844
+ else
845
+ delete env[key];
846
+ dirty = true;
847
+ }
848
+ }
849
+ }
850
+ }
851
+ async function chooseEffort() {
852
+ const selected = await selectItem({
853
+ title: 'Select reasoning effort',
854
+ subtitle: `Current: ${currentEffort() ?? 'auto'}`,
855
+ searchable: false,
856
+ items: [
857
+ { id: 'auto', label: 'auto', description: 'Use the runtime default' },
858
+ { id: 'low', label: 'low', description: 'Fast, direct reasoning' },
859
+ { id: 'medium', label: 'medium', description: 'Balanced reasoning' },
860
+ { id: 'high', label: 'high', description: 'Deeper reasoning and verification' },
861
+ { id: 'max', label: 'max', description: 'Maximum supported reasoning effort' },
862
+ ],
863
+ });
864
+ if (selected)
865
+ await setEffort(selected);
866
+ }
867
+ async function setEffort(value) {
868
+ if (value !== 'auto' && !isActoviqEffort(value)) {
869
+ appendStatic([...formatErrorLine(`unknown effort: ${value}`), '']);
870
+ return;
871
+ }
872
+ await session.mergeMetadata({
873
+ [SESSION_EFFORT_KEY]: value,
874
+ });
875
+ appendStatic([...formatInfoLine(`effort set to: ${currentEffort() ?? 'auto'}`), '']);
876
+ }
877
+ async function showSkills() {
878
+ const skills = sdk.skills.listMetadata();
879
+ if (skills.length === 0) {
880
+ appendStatic([...formatInfoLine('no skills are registered'), '']);
881
+ return;
882
+ }
883
+ const selected = await selectItem({
884
+ title: 'Skills',
885
+ items: skills.map(skill => ({
886
+ id: skill.name,
887
+ label: skill.name,
888
+ description: `${skill.source} · ${skill.context}`,
889
+ detail: `${skill.description} ${skill.whenToUse ?? ''}`,
890
+ })),
891
+ });
892
+ const skill = skills.find(item => item.name === selected);
893
+ if (skill) {
894
+ appendStatic([
895
+ `${A.cyan}/${skill.name}${A.reset} ${skill.description}`,
896
+ `${A.dim}${skill.whenToUse ?? `source: ${skill.source} · context: ${skill.context}`}${A.reset}`,
897
+ '',
898
+ ]);
899
+ }
900
+ }
901
+ async function showAgents() {
902
+ const agents = sdk.agents.list();
903
+ if (agents.length === 0) {
904
+ appendStatic([...formatInfoLine('no subagents are registered'), '']);
905
+ return;
906
+ }
907
+ const selected = await selectItem({
908
+ title: 'Subagents',
909
+ items: agents.map(agent => ({
910
+ id: agent.name,
911
+ label: agent.name,
912
+ description: agent.model ?? 'inherits model',
913
+ detail: agent.description,
914
+ })),
915
+ });
916
+ const agent = agents.find(item => item.name === selected);
917
+ if (agent) {
918
+ appendStatic([
919
+ `${A.cyan}${agent.name}${A.reset} ${agent.description}`,
920
+ `${A.dim}model: ${agent.model ?? 'inherit'} · tools: ${agent.inheritDefaultTools ? 'inherit' : agent.toolNames.join(', ') || 'none'}${A.reset}`,
921
+ '',
922
+ ]);
923
+ }
924
+ }
925
+ async function showMcp() {
926
+ const byServer = new Map();
927
+ for (const tool of toolMetadata.filter(item => item.provider === 'mcp')) {
928
+ const server = tool.server ?? 'mcp';
929
+ const tools = byServer.get(server) ?? [];
930
+ tools.push(tool);
931
+ byServer.set(server, tools);
932
+ }
933
+ if (byServer.size === 0) {
934
+ appendStatic([...formatInfoLine('no MCP servers are active'), '']);
935
+ return;
936
+ }
937
+ const selected = await selectItem({
938
+ title: 'MCP servers',
939
+ items: [...byServer.entries()].map(([server, tools]) => ({
940
+ id: server,
941
+ label: server,
942
+ description: `${tools.length} tool${tools.length === 1 ? '' : 's'}`,
943
+ detail: tools.map(tool => tool.name).join(', '),
944
+ })),
945
+ });
946
+ if (selected) {
947
+ appendStatic([
948
+ `${A.cyan}${selected}${A.reset}`,
949
+ `${A.dim}${(byServer.get(selected) ?? []).map(tool => tool.name).join(', ')}${A.reset}`,
950
+ '',
951
+ ]);
952
+ }
953
+ }
954
+ async function showPlugins() {
955
+ const store = await resolveActoviqSettingsStore({ configPath: options.configPath });
956
+ const configuredDirs = Array.isArray(store.raw.pluginDirs)
957
+ ? store.raw.pluginDirs.filter((value) => typeof value === 'string')
958
+ : [];
959
+ const plugins = await discoverActoviqPlugins({
960
+ workDir,
961
+ homeDir: store.homeDir,
962
+ configuredDirs,
963
+ });
964
+ if (plugins.length === 0) {
965
+ appendStatic([
966
+ ...formatInfoLine('no Clean plugins discovered in user, project, or configured plugin directories'),
967
+ '',
968
+ ]);
969
+ return;
970
+ }
971
+ const selected = await selectItem({
972
+ title: 'Clean plugins',
973
+ items: plugins.map(plugin => ({
974
+ id: plugin.path,
975
+ label: plugin.name,
976
+ description: [plugin.version, plugin.capabilities.join(', ')].filter(Boolean).join(' · '),
977
+ detail: `${plugin.description ?? ''} ${plugin.path}`,
978
+ })),
979
+ });
980
+ const plugin = plugins.find(item => item.path === selected);
981
+ if (plugin) {
982
+ appendStatic([
983
+ `${A.cyan}${plugin.name}${A.reset}${plugin.version ? ` ${plugin.version}` : ''}`,
984
+ `${A.dim}${plugin.path} · ${plugin.capabilities.join(', ') || 'manifest only'}${A.reset}`,
985
+ '',
986
+ ]);
987
+ }
988
+ }
989
+ async function showDreamMenu() {
990
+ const selected = await selectItem({
991
+ title: 'Dream memory consolidation',
992
+ searchable: false,
993
+ items: [
994
+ { id: 'status', label: 'Show dream state' },
995
+ { id: 'run', label: 'Run consolidation now' },
996
+ ],
997
+ });
998
+ if (selected)
999
+ await runDreamCommand(selected);
1000
+ }
1001
+ async function runDreamCommand(action) {
1002
+ if (action === 'status') {
1003
+ const state = await session.dreamState();
1004
+ appendStatic([`${A.dim}${JSON.stringify(state, null, 2)}${A.reset}`, '']);
1005
+ return;
1006
+ }
1007
+ if (action !== 'run') {
1008
+ appendStatic([...formatErrorLine('usage: /dream [run|status]'), '']);
1009
+ return;
1010
+ }
1011
+ const result = await session.dream({ force: true });
1012
+ appendStatic([
1013
+ ...formatInfoLine(result.reason ?? (result.skipped ? 'dream skipped' : result.success ? 'dream completed' : 'dream failed')),
1014
+ '',
1015
+ ]);
1016
+ }
1017
+ async function runSlashCommand(raw) {
1018
+ const spaceIndex = raw.indexOf(' ');
1019
+ const name = (spaceIndex === -1 ? raw.slice(1) : raw.slice(1, spaceIndex)).toLowerCase();
1020
+ const args = spaceIndex === -1 ? '' : raw.slice(spaceIndex + 1).trim();
1021
+ appendStatic(formatUserPrompt(raw));
1022
+ commandBusy = true;
1023
+ renderDynamic();
1024
+ try {
1025
+ switch (name) {
1026
+ case 'help': {
1027
+ const selected = await selectItem({
1028
+ title: 'Help',
1029
+ items: Object.entries(TUI_SLASH_COMMANDS).map(([command, description]) => ({
1030
+ id: command,
1031
+ label: `/${command}`,
1032
+ description,
1033
+ detail: commandUsage(command),
1034
+ })),
1035
+ });
1036
+ if (selected) {
1037
+ appendStatic([
1038
+ `${A.cyan}${commandUsage(selected)}${A.reset}`,
1039
+ `${A.dim}${TUI_SLASH_COMMANDS[selected]}${A.reset}`,
1040
+ '',
1041
+ ]);
1042
+ }
1043
+ return;
1044
+ }
1045
+ case 'clear':
1046
+ process.stdout.write('\x1b[2J\x1b[H');
1047
+ renderDynamic();
1048
+ return;
1049
+ case 'exit':
1050
+ case 'quit':
1051
+ await shutdown(0);
1052
+ return;
1053
+ case 'model': {
1054
+ if (!args) {
1055
+ await chooseModel();
1056
+ return;
1057
+ }
1058
+ if (args === 'config') {
1059
+ await configureModelSettings();
1060
+ return;
1061
+ }
1062
+ await session.setModel(args === 'default' ? sdk.config.model : args);
1063
+ appendStatic([...formatInfoLine(`model set to: ${session.model}`), '']);
1064
+ return;
1065
+ }
1066
+ case 'effort':
1067
+ if (!args)
1068
+ await chooseEffort();
1069
+ else
1070
+ await setEffort(args.toLowerCase());
1071
+ return;
1072
+ case 'permissions': {
1073
+ const requested = args;
1074
+ if (!requested) {
1075
+ const state = session.permissionContext;
1076
+ appendStatic([
1077
+ ...formatInfoLine(`permissions: ${state.mode ?? permissionMode} · ${state.permissions.length} session rules`),
1078
+ '',
1079
+ ]);
1080
+ return;
1081
+ }
1082
+ if (!PERMISSION_MODES.has(requested)) {
1083
+ appendStatic([...formatErrorLine(`unknown permission mode: ${requested}`), '']);
1084
+ return;
1085
+ }
1086
+ await session.setPermissionContext({
1087
+ mode: requested,
1088
+ permissions: session.permissionContext.permissions,
1089
+ approver,
1090
+ });
1091
+ appendStatic([...formatInfoLine(`permissions set to: ${requested}`), '']);
1092
+ return;
1093
+ }
1094
+ case 'sessions': {
1095
+ const sessions = await sdk.sessions.list();
1096
+ appendStatic([
1097
+ ...(sessions.length > 0
1098
+ ? sessions.map(item => `${item.id === session.id ? A.green : A.dim}${item.id}${A.reset} ${item.title} · ${item.model} · ${item.status}`)
1099
+ : formatInfoLine('no stored sessions')),
1100
+ '',
1101
+ ]);
1102
+ return;
1103
+ }
1104
+ case 'resume': {
1105
+ if (!args)
1106
+ await chooseSessionToResume();
1107
+ else
1108
+ await resumeSession(args);
1109
+ return;
1110
+ }
1111
+ case 'tools':
1112
+ appendStatic([
1113
+ ...formatInfoLine(toolMetadata.map((tool) => tool.name).join(', ')),
1114
+ '',
1115
+ ]);
1116
+ return;
1117
+ case 'memory': {
1118
+ try {
1119
+ const state = await session.compactState();
1120
+ appendStatic([`${A.dim}${JSON.stringify(state, null, 2)}${A.reset}`, '']);
1121
+ }
1122
+ catch (error) {
1123
+ appendStatic([...formatErrorLine(error.message), '']);
1124
+ }
1125
+ return;
1126
+ }
1127
+ case 'compact': {
1128
+ try {
1129
+ const summaryInstructions = args || undefined;
1130
+ const result = await session.compact({ force: true, summaryInstructions });
1131
+ if (!result.compacted) {
1132
+ appendStatic([
1133
+ ...formatErrorLine(result.error ?? `compact skipped: ${result.reason}`),
1134
+ '',
1135
+ ]);
1136
+ return;
1137
+ }
1138
+ appendStatic([
1139
+ `${A.green}✓ compacted${A.reset}${A.dim} · ${result.messagesRemoved ?? '?'} messages summarized${A.reset}`,
1140
+ '',
1141
+ ]);
1142
+ }
1143
+ catch (error) {
1144
+ appendStatic([...formatErrorLine(error.message), '']);
1145
+ }
1146
+ return;
1147
+ }
1148
+ case 'dream': {
1149
+ try {
1150
+ if (!args)
1151
+ await showDreamMenu();
1152
+ else
1153
+ await runDreamCommand(args.toLowerCase());
1154
+ }
1155
+ catch (error) {
1156
+ appendStatic([...formatErrorLine(error.message), '']);
1157
+ }
1158
+ return;
1159
+ }
1160
+ case 'skills':
1161
+ await showSkills();
1162
+ return;
1163
+ case 'agents':
1164
+ await showAgents();
1165
+ return;
1166
+ case 'mcp':
1167
+ await showMcp();
1168
+ return;
1169
+ case 'plugins':
1170
+ await showPlugins();
1171
+ return;
1172
+ default:
1173
+ appendStatic([...formatErrorLine(`unknown command: /${name} — type /help`), '']);
1174
+ return;
1175
+ }
1176
+ }
1177
+ finally {
1178
+ commandBusy = false;
1179
+ renderDynamic();
1180
+ }
1181
+ }
1182
+ // ── Submit / key handling ──────────────────────────────────────────
1183
+ async function submit() {
1184
+ const matches = filterSlashCommands(editor.text);
1185
+ if (matches.length > 0 && !running) {
1186
+ const selected = matches[Math.min(menuSelected, matches.length - 1)];
1187
+ const args = editor.text.includes(' ') ? editor.text.slice(editor.text.indexOf(' ')) : '';
1188
+ editor.clear();
1189
+ menuSelected = 0;
1190
+ await runSlashCommand(`/${selected}${args}`);
1191
+ return;
1192
+ }
1193
+ const value = editor.submit();
1194
+ if (value === null) {
1195
+ renderDynamic();
1196
+ return;
1197
+ }
1198
+ const text = value.trim();
1199
+ menuSelected = 0;
1200
+ if (!text) {
1201
+ renderDynamic();
1202
+ return;
1203
+ }
1204
+ if (running) {
1205
+ if (text.startsWith('/')) {
1206
+ appendStatic(formatInfoLine('slash commands are unavailable while the agent is working'));
1207
+ renderDynamic();
1208
+ return;
1209
+ }
1210
+ queuedInputs.push(text);
1211
+ appendStatic(formatQueuedPrompt(text));
1212
+ renderDynamic();
1213
+ return;
1214
+ }
1215
+ if (commandBusy) {
1216
+ renderDynamic();
1217
+ return;
1218
+ }
1219
+ if (text.startsWith('/')) {
1220
+ await runSlashCommand(text);
1221
+ return;
1222
+ }
1223
+ void startRun(text);
1224
+ }
1225
+ function handleDialogKey(key) {
1226
+ if (!dialog)
1227
+ return;
1228
+ const name = key.name ?? '';
1229
+ if (name === 'up') {
1230
+ dialog.selected = (dialog.selected + 2) % 3;
1231
+ }
1232
+ else if (name === 'down' || name === 'tab') {
1233
+ dialog.selected = (dialog.selected + 1) % 3;
1234
+ }
1235
+ else if (name === 'return' || name === 'enter') {
1236
+ dialog.resolve(dialog.selected === 0 ? 'allow' : dialog.selected === 1 ? 'always' : 'deny');
1237
+ return;
1238
+ }
1239
+ else if (name === 'y') {
1240
+ dialog.resolve('allow');
1241
+ return;
1242
+ }
1243
+ else if (name === 'a') {
1244
+ dialog.resolve('always');
1245
+ return;
1246
+ }
1247
+ else if (name === 'n' || name === 'escape') {
1248
+ dialog.resolve('deny');
1249
+ return;
1250
+ }
1251
+ else if (name === 'c' && key.ctrl) {
1252
+ dialog.resolve('deny');
1253
+ return;
1254
+ }
1255
+ renderDynamic();
1256
+ }
1257
+ function finishSelection(value) {
1258
+ const active = selectionDialog;
1259
+ if (!active)
1260
+ return;
1261
+ selectionDialog = null;
1262
+ renderDynamic();
1263
+ active.resolve(value);
1264
+ }
1265
+ function handleSelectionKey(char, key) {
1266
+ if (!selectionDialog)
1267
+ return;
1268
+ const name = key.name ?? '';
1269
+ const filtered = filterTuiSelectionItems(selectionDialog.items, selectionDialog.query);
1270
+ if (name === 'up') {
1271
+ selectionDialog.selected = moveTuiSelection(selectionDialog.selected, filtered.length, -1);
1272
+ }
1273
+ else if (name === 'down' || name === 'tab') {
1274
+ selectionDialog.selected = moveTuiSelection(selectionDialog.selected, filtered.length, 1);
1275
+ }
1276
+ else if (name === 'pageup') {
1277
+ selectionDialog.selected = Math.max(selectionDialog.selected - 8, 0);
1278
+ }
1279
+ else if (name === 'pagedown') {
1280
+ selectionDialog.selected = Math.max(Math.min(selectionDialog.selected + 8, filtered.length - 1), 0);
1281
+ }
1282
+ else if (name === 'return' || name === 'enter') {
1283
+ finishSelection(filtered[selectionDialog.selected]?.id);
1284
+ return;
1285
+ }
1286
+ else if (name === 'escape' || (name === 'c' && key.ctrl)) {
1287
+ finishSelection(undefined);
1288
+ return;
1289
+ }
1290
+ else if (selectionDialog.searchable && name === 'backspace') {
1291
+ selectionDialog.query = selectionDialog.query.slice(0, -1);
1292
+ selectionDialog.selected = 0;
1293
+ }
1294
+ else if (selectionDialog.searchable && name === 'u' && key.ctrl) {
1295
+ selectionDialog.query = '';
1296
+ selectionDialog.selected = 0;
1297
+ }
1298
+ else if (selectionDialog.searchable && !key.ctrl && !key.meta) {
1299
+ const sequence = key.sequence ?? char ?? '';
1300
+ const cleaned = sequence.replace(/[\x00-\x1f\x7f]/g, '');
1301
+ if (cleaned) {
1302
+ selectionDialog.query += cleaned;
1303
+ selectionDialog.selected = 0;
1304
+ }
1305
+ }
1306
+ renderDynamic();
1307
+ }
1308
+ function finishTextInput(value) {
1309
+ const active = textInputDialog;
1310
+ if (!active)
1311
+ return;
1312
+ textInputDialog = null;
1313
+ renderDynamic();
1314
+ active.resolve(value);
1315
+ }
1316
+ function handleTextInputKey(char, key) {
1317
+ if (!textInputDialog)
1318
+ return;
1319
+ const name = key.name ?? '';
1320
+ const inputEditor = textInputDialog.editor;
1321
+ if (name === 'return' || name === 'enter') {
1322
+ finishTextInput(inputEditor.text);
1323
+ return;
1324
+ }
1325
+ if (name === 'escape' || (name === 'c' && key.ctrl)) {
1326
+ finishTextInput(undefined);
1327
+ return;
1328
+ }
1329
+ if (key.ctrl) {
1330
+ if (name === 'a')
1331
+ inputEditor.moveHome();
1332
+ else if (name === 'e')
1333
+ inputEditor.moveEnd();
1334
+ else if (name === 'u')
1335
+ inputEditor.clear();
1336
+ else if (name === 'w')
1337
+ inputEditor.deleteWordLeft();
1338
+ }
1339
+ else if (name === 'backspace') {
1340
+ inputEditor.backspace();
1341
+ }
1342
+ else if (name === 'delete') {
1343
+ inputEditor.deleteForward();
1344
+ }
1345
+ else if (name === 'left') {
1346
+ inputEditor.moveLeft();
1347
+ }
1348
+ else if (name === 'right') {
1349
+ inputEditor.moveRight();
1350
+ }
1351
+ else if (name === 'home') {
1352
+ inputEditor.moveHome();
1353
+ }
1354
+ else if (name === 'end') {
1355
+ inputEditor.moveEnd();
1356
+ }
1357
+ else {
1358
+ const sequence = key.sequence ?? char ?? '';
1359
+ const cleaned = sequence.replace(/[\x00-\x1f\x7f]/g, '');
1360
+ if (cleaned)
1361
+ inputEditor.insert(cleaned);
1362
+ }
1363
+ renderDynamic();
1364
+ }
1365
+ function handleKey(char, key) {
1366
+ if (shuttingDown)
1367
+ return;
1368
+ const name = key.name ?? '';
1369
+ if (name !== 'c' || !key.ctrl) {
1370
+ ctrlCCount = 0;
1371
+ }
1372
+ if (dialog) {
1373
+ handleDialogKey(key);
1374
+ return;
1375
+ }
1376
+ if (selectionDialog) {
1377
+ handleSelectionKey(char, key);
1378
+ return;
1379
+ }
1380
+ if (textInputDialog) {
1381
+ handleTextInputKey(char, key);
1382
+ return;
1383
+ }
1384
+ if (key.ctrl) {
1385
+ switch (name) {
1386
+ case 'c': {
1387
+ ctrlCCount += 1;
1388
+ if (ctrlCTimer)
1389
+ clearTimeout(ctrlCTimer);
1390
+ ctrlCTimer = setTimeout(() => {
1391
+ ctrlCCount = 0;
1392
+ }, CTRL_C_EXIT_WINDOW_MS);
1393
+ if (ctrlCCount >= 2) {
1394
+ void shutdown(0);
1395
+ return;
1396
+ }
1397
+ if (running && abortCtrl) {
1398
+ abortCtrl.abort();
1399
+ }
1400
+ else if (!editor.isEmpty()) {
1401
+ editor.clear();
1402
+ menuSelected = 0;
1403
+ }
1404
+ renderDynamic();
1405
+ return;
1406
+ }
1407
+ case 'd':
1408
+ if (editor.isEmpty()) {
1409
+ void shutdown(0);
1410
+ return;
1411
+ }
1412
+ editor.deleteForward();
1413
+ break;
1414
+ case 'a':
1415
+ editor.moveHome();
1416
+ break;
1417
+ case 'e':
1418
+ editor.moveEnd();
1419
+ break;
1420
+ case 'k':
1421
+ editor.killToEnd();
1422
+ break;
1423
+ case 'u':
1424
+ editor.killToStart();
1425
+ break;
1426
+ case 'w':
1427
+ editor.deleteWordLeft();
1428
+ break;
1429
+ case 'left':
1430
+ editor.moveWordLeft();
1431
+ break;
1432
+ case 'right':
1433
+ editor.moveWordRight();
1434
+ break;
1435
+ case 'l':
1436
+ process.stdout.write('\x1b[2J\x1b[H');
1437
+ break;
1438
+ case 'j':
1439
+ editor.insert('\n');
1440
+ break;
1441
+ default:
1442
+ break;
1443
+ }
1444
+ renderDynamic();
1445
+ return;
1446
+ }
1447
+ switch (name) {
1448
+ case 'return': {
1449
+ if (key.meta) {
1450
+ editor.insert('\n');
1451
+ break;
1452
+ }
1453
+ void submit();
1454
+ return;
1455
+ }
1456
+ case 'enter':
1457
+ editor.insert('\n');
1458
+ break;
1459
+ case 'escape': {
1460
+ if (running && abortCtrl) {
1461
+ abortCtrl.abort();
1462
+ }
1463
+ else if (!editor.isEmpty()) {
1464
+ editor.clear();
1465
+ menuSelected = 0;
1466
+ }
1467
+ break;
1468
+ }
1469
+ case 'backspace':
1470
+ editor.backspace();
1471
+ menuSelected = 0;
1472
+ break;
1473
+ case 'delete':
1474
+ editor.deleteForward();
1475
+ break;
1476
+ case 'left':
1477
+ if (key.meta)
1478
+ editor.moveWordLeft();
1479
+ else
1480
+ editor.moveLeft();
1481
+ break;
1482
+ case 'right':
1483
+ if (key.meta)
1484
+ editor.moveWordRight();
1485
+ else
1486
+ editor.moveRight();
1487
+ break;
1488
+ case 'home':
1489
+ editor.moveHome();
1490
+ break;
1491
+ case 'end':
1492
+ editor.moveEnd();
1493
+ break;
1494
+ case 'up': {
1495
+ const menu = filterSlashCommands(editor.text);
1496
+ if (menu.length > 0) {
1497
+ menuSelected = (menuSelected + menu.length - 1) % menu.length;
1498
+ }
1499
+ else if (!editor.onFirstLine()) {
1500
+ editor.moveUp();
1501
+ }
1502
+ else {
1503
+ editor.historyPrev();
1504
+ }
1505
+ break;
1506
+ }
1507
+ case 'down': {
1508
+ const menu = filterSlashCommands(editor.text);
1509
+ if (menu.length > 0) {
1510
+ menuSelected = (menuSelected + 1) % menu.length;
1511
+ }
1512
+ else if (!editor.onLastLine()) {
1513
+ editor.moveDown();
1514
+ }
1515
+ else {
1516
+ editor.historyNext();
1517
+ }
1518
+ break;
1519
+ }
1520
+ case 'tab': {
1521
+ const menu = filterSlashCommands(editor.text);
1522
+ if (menu.length > 0) {
1523
+ const selected = menu[Math.min(menuSelected, menu.length - 1)];
1524
+ editor.setText(`/${selected} `);
1525
+ }
1526
+ break;
1527
+ }
1528
+ default: {
1529
+ const sequence = key.sequence ?? char ?? '';
1530
+ if (sequence) {
1531
+ const cleaned = sequence
1532
+ .replace(/\x1b\[20[01]~/g, '')
1533
+ .replace(/\r\n?/g, '\n')
1534
+ .replace(/[\x00-\x08\x0b-\x1f\x7f]/g, (match) => (match === '\n' ? '\n' : ''));
1535
+ if (cleaned) {
1536
+ editor.insert(cleaned);
1537
+ menuSelected = 0;
1538
+ }
1539
+ }
1540
+ break;
1541
+ }
1542
+ }
1543
+ renderDynamic();
1544
+ }
1545
+ // ── Lifecycle ──────────────────────────────────────────────────────
1546
+ async function shutdown(code) {
1547
+ if (shuttingDown)
1548
+ return;
1549
+ shuttingDown = true;
1550
+ if (spinnerTimer)
1551
+ clearInterval(spinnerTimer);
1552
+ cancelScheduledDynamicRender();
1553
+ abortCtrl?.abort();
1554
+ screen.stop();
1555
+ process.stdout.write(`${A.dim}Goodbye.${A.reset}\n`);
1556
+ if (process.stdin.isTTY)
1557
+ process.stdin.setRawMode(false);
1558
+ process.stdin.pause();
1559
+ try {
1560
+ await sdk.close();
1561
+ }
1562
+ catch {
1563
+ // best-effort close
1564
+ }
1565
+ process.exit(code);
1566
+ }
1567
+ screen.start();
1568
+ appendStatic(formatBanner({
1569
+ workDir,
1570
+ model: session.model,
1571
+ toolCount: toolMetadata.length,
1572
+ permissionMode: currentPermissionMode(),
1573
+ width: screen.width,
1574
+ }));
1575
+ renderDynamic();
1576
+ readline.emitKeypressEvents(process.stdin);
1577
+ if (process.stdin.isTTY)
1578
+ process.stdin.setRawMode(true);
1579
+ process.stdin.resume();
1580
+ process.stdin.on('keypress', (char, key) => {
1581
+ try {
1582
+ handleKey(char, key ?? {});
1583
+ }
1584
+ catch (error) {
1585
+ appendStatic(formatErrorLine(`input error: ${error.message}`));
1586
+ renderDynamic();
1587
+ }
1588
+ });
1589
+ process.on('SIGTERM', () => void shutdown(0));
1590
+ process.stdout.on('resize', () => renderDynamic());
1591
+ // Keep the process alive until shutdown() exits it.
1592
+ await new Promise(() => { });
1593
+ }
1594
+ //# sourceMappingURL=actoviqTui.js.map