@golba98/codexa 1.0.1

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 (226) hide show
  1. package/README.md +320 -0
  2. package/bin/codexa.js +445 -0
  3. package/package.json +45 -0
  4. package/scripts/audit-codexa-capabilities.mjs +466 -0
  5. package/scripts/smoke-terminal-bench.mjs +35 -0
  6. package/src/app.tsx +4561 -0
  7. package/src/appRenderStability.test.ts +131 -0
  8. package/src/commands/handler.test.ts +643 -0
  9. package/src/commands/handler.ts +875 -0
  10. package/src/config/launchArgs.test.ts +158 -0
  11. package/src/config/launchArgs.ts +186 -0
  12. package/src/config/layeredConfig.test.ts +143 -0
  13. package/src/config/layeredConfig.ts +836 -0
  14. package/src/config/persistence.test.ts +110 -0
  15. package/src/config/persistence.ts +311 -0
  16. package/src/config/runtimeConfig.test.ts +218 -0
  17. package/src/config/runtimeConfig.ts +554 -0
  18. package/src/config/settings.test.ts +155 -0
  19. package/src/config/settings.ts +401 -0
  20. package/src/config/toml-serialize.ts +98 -0
  21. package/src/config/trustStore.test.ts +29 -0
  22. package/src/config/trustStore.ts +68 -0
  23. package/src/core/attachments.test.ts +155 -0
  24. package/src/core/attachments.ts +71 -0
  25. package/src/core/auth/codexAuth.test.ts +68 -0
  26. package/src/core/auth/codexAuth.ts +359 -0
  27. package/src/core/cleanupFastFail.test.ts +76 -0
  28. package/src/core/cleanupFastFail.ts +67 -0
  29. package/src/core/clipboard.ts +24 -0
  30. package/src/core/codex.ts +124 -0
  31. package/src/core/codexExecArgs.test.ts +195 -0
  32. package/src/core/codexExecArgs.ts +152 -0
  33. package/src/core/codexLaunch.test.ts +205 -0
  34. package/src/core/codexLaunch.ts +162 -0
  35. package/src/core/codexPrompt.test.ts +252 -0
  36. package/src/core/codexPrompt.ts +428 -0
  37. package/src/core/executables/claudeExecutable.ts +63 -0
  38. package/src/core/executables/codexExecutable.test.ts +212 -0
  39. package/src/core/executables/codexExecutable.ts +159 -0
  40. package/src/core/executables/executableResolver.test.ts +129 -0
  41. package/src/core/executables/executableResolver.ts +138 -0
  42. package/src/core/executables/geminiExecutable.test.ts +116 -0
  43. package/src/core/executables/geminiExecutable.ts +78 -0
  44. package/src/core/executables/pathSanityScan.test.ts +47 -0
  45. package/src/core/githubDiagnostics.test.ts +92 -0
  46. package/src/core/githubDiagnostics.ts +222 -0
  47. package/src/core/hollowResponseFormat.test.ts +58 -0
  48. package/src/core/hollowResponseFormat.ts +39 -0
  49. package/src/core/inputDebug.ts +51 -0
  50. package/src/core/launchContext.test.ts +157 -0
  51. package/src/core/launchContext.ts +266 -0
  52. package/src/core/models/codexCapabilities.test.ts +45 -0
  53. package/src/core/models/codexCapabilities.ts +95 -0
  54. package/src/core/models/codexModelCapabilities.test.ts +246 -0
  55. package/src/core/models/codexModelCapabilities.ts +571 -0
  56. package/src/core/models/modelSpecs.test.ts +283 -0
  57. package/src/core/models/modelSpecs.ts +300 -0
  58. package/src/core/perf/profiler.ts +125 -0
  59. package/src/core/perf/renderDebug.test.ts +230 -0
  60. package/src/core/perf/renderDebug.ts +373 -0
  61. package/src/core/planStorage.test.ts +143 -0
  62. package/src/core/planStorage.ts +141 -0
  63. package/src/core/process/CommandRunner.test.ts +105 -0
  64. package/src/core/process/CommandRunner.ts +269 -0
  65. package/src/core/process/processValidation.ts +101 -0
  66. package/src/core/projectInstructions.test.ts +50 -0
  67. package/src/core/projectInstructions.ts +54 -0
  68. package/src/core/providerLauncher/launcher.test.ts +238 -0
  69. package/src/core/providerLauncher/launcher.ts +203 -0
  70. package/src/core/providerLauncher/registry.test.ts +324 -0
  71. package/src/core/providerLauncher/registry.ts +253 -0
  72. package/src/core/providerLauncher/types.ts +84 -0
  73. package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
  74. package/src/core/providerLauncher/workspaceConfig.ts +407 -0
  75. package/src/core/providerRuntime/anthropic.test.ts +1120 -0
  76. package/src/core/providerRuntime/anthropic.ts +576 -0
  77. package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
  78. package/src/core/providerRuntime/capabilityProfile.ts +288 -0
  79. package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
  80. package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
  81. package/src/core/providerRuntime/contextMetadata.ts +409 -0
  82. package/src/core/providerRuntime/gemini.test.ts +437 -0
  83. package/src/core/providerRuntime/gemini.ts +784 -0
  84. package/src/core/providerRuntime/lmstudio.test.ts +168 -0
  85. package/src/core/providerRuntime/lmstudio.ts +118 -0
  86. package/src/core/providerRuntime/local.test.ts +787 -0
  87. package/src/core/providerRuntime/local.ts +754 -0
  88. package/src/core/providerRuntime/models.ts +150 -0
  89. package/src/core/providerRuntime/reasoning.ts +17 -0
  90. package/src/core/providerRuntime/registry.test.ts +233 -0
  91. package/src/core/providerRuntime/registry.ts +203 -0
  92. package/src/core/providerRuntime/types.ts +103 -0
  93. package/src/core/providers/codexJsonStream.test.ts +148 -0
  94. package/src/core/providers/codexJsonStream.ts +305 -0
  95. package/src/core/providers/codexSubprocess.test.ts +68 -0
  96. package/src/core/providers/codexSubprocess.ts +372 -0
  97. package/src/core/providers/codexTranscript.test.ts +284 -0
  98. package/src/core/providers/codexTranscript.ts +695 -0
  99. package/src/core/providers/openaiNative.ts +13 -0
  100. package/src/core/providers/registry.ts +21 -0
  101. package/src/core/providers/types.ts +59 -0
  102. package/src/core/terminal/terminalCapabilities.test.ts +93 -0
  103. package/src/core/terminal/terminalCapabilities.ts +100 -0
  104. package/src/core/terminal/terminalControl.test.ts +75 -0
  105. package/src/core/terminal/terminalControl.ts +147 -0
  106. package/src/core/terminal/terminalSanitize.test.ts +22 -0
  107. package/src/core/terminal/terminalSanitize.ts +147 -0
  108. package/src/core/terminal/terminalSelection.test.ts +42 -0
  109. package/src/core/terminal/terminalSelection.ts +66 -0
  110. package/src/core/terminal/terminalTitle.test.ts +328 -0
  111. package/src/core/terminal/terminalTitle.ts +483 -0
  112. package/src/core/workspaceActivity.test.ts +163 -0
  113. package/src/core/workspaceActivity.ts +380 -0
  114. package/src/core/workspaceGuard.test.ts +151 -0
  115. package/src/core/workspaceGuard.ts +288 -0
  116. package/src/core/workspaceRoot.test.ts +23 -0
  117. package/src/core/workspaceRoot.ts +47 -0
  118. package/src/exec.test.ts +13 -0
  119. package/src/exec.ts +72 -0
  120. package/src/headless/execArgs.test.ts +147 -0
  121. package/src/headless/execArgs.ts +294 -0
  122. package/src/headless/execRunner.test.ts +434 -0
  123. package/src/headless/execRunner.ts +304 -0
  124. package/src/index.test.tsx +618 -0
  125. package/src/index.tsx +296 -0
  126. package/src/session/appSession.test.ts +897 -0
  127. package/src/session/appSession.ts +761 -0
  128. package/src/session/chatLifecycle.test.ts +64 -0
  129. package/src/session/chatLifecycle.ts +951 -0
  130. package/src/session/liveRenderScheduler.test.ts +201 -0
  131. package/src/session/liveRenderScheduler.ts +214 -0
  132. package/src/session/planFlow.test.ts +103 -0
  133. package/src/session/planFlow.ts +149 -0
  134. package/src/session/planTranscript.test.ts +65 -0
  135. package/src/session/planTranscript.ts +15 -0
  136. package/src/session/promptRunSchedule.test.ts +36 -0
  137. package/src/session/promptRunSchedule.ts +26 -0
  138. package/src/session/types.ts +228 -0
  139. package/src/test/runtimeTestUtils.ts +14 -0
  140. package/src/types/react-dom.d.ts +3 -0
  141. package/src/ui/ActionRequiredBlock.tsx +38 -0
  142. package/src/ui/ActivityBars.tsx +68 -0
  143. package/src/ui/ActivityIndicator.test.tsx +58 -0
  144. package/src/ui/ActivityIndicator.tsx +58 -0
  145. package/src/ui/AgentBlock.test.ts +6 -0
  146. package/src/ui/AgentBlock.tsx +130 -0
  147. package/src/ui/AnimatedStatusText.test.ts +16 -0
  148. package/src/ui/AnimatedStatusText.tsx +69 -0
  149. package/src/ui/AppShell.test.tsx +1739 -0
  150. package/src/ui/AppShell.tsx +698 -0
  151. package/src/ui/AttachmentImportPanel.test.tsx +204 -0
  152. package/src/ui/AttachmentImportPanel.tsx +98 -0
  153. package/src/ui/AuthPanel.tsx +113 -0
  154. package/src/ui/BackendPicker.tsx +28 -0
  155. package/src/ui/BottomComposer.test.ts +674 -0
  156. package/src/ui/BottomComposer.tsx +1028 -0
  157. package/src/ui/CodexLogo.tsx +55 -0
  158. package/src/ui/DashCard.tsx +82 -0
  159. package/src/ui/Markdown.test.ts +157 -0
  160. package/src/ui/Markdown.tsx +310 -0
  161. package/src/ui/ModePicker.tsx +27 -0
  162. package/src/ui/ModelPicker.tsx +31 -0
  163. package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
  164. package/src/ui/ModelPickerScreen.test.tsx +99 -0
  165. package/src/ui/ModelPickerScreen.tsx +416 -0
  166. package/src/ui/ModelPickerState.test.tsx +151 -0
  167. package/src/ui/ModelReasoningPicker.test.tsx +447 -0
  168. package/src/ui/ModelReasoningPicker.tsx +458 -0
  169. package/src/ui/Panel.tsx +51 -0
  170. package/src/ui/PermissionsPanel.tsx +78 -0
  171. package/src/ui/PlanActionPicker.tsx +119 -0
  172. package/src/ui/PlanReviewPanel.test.tsx +267 -0
  173. package/src/ui/PlanReviewPanel.tsx +212 -0
  174. package/src/ui/PromptCardBorder.test.tsx +161 -0
  175. package/src/ui/ProviderPicker.test.tsx +289 -0
  176. package/src/ui/ProviderPicker.tsx +321 -0
  177. package/src/ui/ProviderShortcut.test.tsx +143 -0
  178. package/src/ui/ReasoningPicker.tsx +46 -0
  179. package/src/ui/RunFooter.tsx +65 -0
  180. package/src/ui/SelectionPanel.tsx +67 -0
  181. package/src/ui/SettingsPanel.test.tsx +233 -0
  182. package/src/ui/SettingsPanel.tsx +156 -0
  183. package/src/ui/Spinner.tsx +25 -0
  184. package/src/ui/StaticIntroItem.tsx +54 -0
  185. package/src/ui/StaticTranscriptItem.tsx +56 -0
  186. package/src/ui/TextEntryPanel.tsx +139 -0
  187. package/src/ui/ThemePicker.tsx +31 -0
  188. package/src/ui/ThinkingBlock.tsx +100 -0
  189. package/src/ui/Timeline.test.ts +2067 -0
  190. package/src/ui/Timeline.tsx +1472 -0
  191. package/src/ui/TimelineNavigation.test.tsx +201 -0
  192. package/src/ui/TopHeader.test.tsx +239 -0
  193. package/src/ui/TopHeader.tsx +257 -0
  194. package/src/ui/TurnGroup.test.tsx +365 -0
  195. package/src/ui/TurnGroup.tsx +657 -0
  196. package/src/ui/busyStatusAnimation.test.ts +30 -0
  197. package/src/ui/busyStatusAnimation.ts +11 -0
  198. package/src/ui/commandNormalize.test.ts +142 -0
  199. package/src/ui/commandNormalize.ts +66 -0
  200. package/src/ui/diffRenderer.test.ts +102 -0
  201. package/src/ui/diffRenderer.ts +116 -0
  202. package/src/ui/focus.ts +61 -0
  203. package/src/ui/focusFlow.test.tsx +1098 -0
  204. package/src/ui/inputBuffer.test.ts +151 -0
  205. package/src/ui/inputBuffer.ts +203 -0
  206. package/src/ui/layout.test.ts +145 -0
  207. package/src/ui/layout.ts +287 -0
  208. package/src/ui/modeDisplay.test.ts +42 -0
  209. package/src/ui/modeDisplay.ts +52 -0
  210. package/src/ui/outputPipeline.ts +64 -0
  211. package/src/ui/progressEntries.ts +156 -0
  212. package/src/ui/runActivityView.test.ts +89 -0
  213. package/src/ui/runActivityView.ts +37 -0
  214. package/src/ui/runLifecycleView.test.tsx +237 -0
  215. package/src/ui/slashCommands.ts +41 -0
  216. package/src/ui/statusRenderIsolation.test.tsx +654 -0
  217. package/src/ui/terminalAnswerFormat.test.ts +19 -0
  218. package/src/ui/terminalAnswerFormat.ts +128 -0
  219. package/src/ui/textLayout.test.ts +18 -0
  220. package/src/ui/textLayout.ts +338 -0
  221. package/src/ui/theme.tsx +395 -0
  222. package/src/ui/themeFlow.test.ts +53 -0
  223. package/src/ui/themeFlow.ts +41 -0
  224. package/src/ui/timelineMeasure.ts +3088 -0
  225. package/src/ui/timelineMeasureCache.test.ts +986 -0
  226. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,875 @@
1
+ import {
2
+ formatLayeredConfigStatus,
3
+ type LayeredConfigResult,
4
+ } from "../config/layeredConfig.js";
5
+ import {
6
+ formatApprovalPolicyLabel,
7
+ formatNetworkAccessLabel,
8
+ formatPermissionsStatus,
9
+ formatPersonalityLabel,
10
+ formatRuntimeStatus,
11
+ formatSandboxModeLabel,
12
+ formatServiceTierLabel,
13
+ type ResolvedRuntimeConfig,
14
+ type RuntimeConfig,
15
+ type RuntimeNetworkAccess,
16
+ } from "../config/runtimeConfig.js";
17
+ import {
18
+ AUTH_PREFERENCES,
19
+ AVAILABLE_BACKENDS,
20
+ AVAILABLE_MODELS,
21
+ AVAILABLE_REASONING_LEVELS,
22
+ AVAILABLE_THEMES,
23
+ BUSY_LOADER_SETTING_VALUES,
24
+ WORKSPACE_DISPLAY_MODES,
25
+ formatAuthPreferenceLabel,
26
+ formatBackendLabel,
27
+ formatModeCommandHelp,
28
+ formatModeLabel,
29
+ formatReasoningLabel,
30
+ formatThemeLabel,
31
+ formatWorkspaceDisplayModeLabel,
32
+ normalizeLegacyDirectoryDisplayMode,
33
+ resolveModeCommand,
34
+ type BusyLoaderSettingValue,
35
+ type TerminalTitleMode,
36
+ type WorkspaceDisplayMode,
37
+ } from "../config/settings.js";
38
+
39
+ import type { WorkspaceCommandContext } from "../core/launchContext.js";
40
+ import {
41
+ findModelCapability,
42
+ formatModelCapabilitiesList,
43
+ getSelectableModelCapabilities,
44
+ type CodexModelCapabilities,
45
+ } from "../core/models/codexModelCapabilities.js";
46
+ import { dumpRenderCounts } from "../core/perf/renderDebug.js";
47
+
48
+ export type CommandAction =
49
+ | "exit"
50
+ | "clear"
51
+ | "login"
52
+ | "logout"
53
+ | "auth_status"
54
+ | "backend"
55
+ | "open_provider_picker"
56
+ | "route_status"
57
+ | "model"
58
+ | "mode"
59
+ | "auth"
60
+ | "open_backend_picker"
61
+ | "open_model_picker"
62
+ | "open_mode_picker"
63
+ | "reasoning"
64
+ | "open_reasoning_picker"
65
+ | "plan_mode"
66
+ | "open_settings_panel"
67
+ | "setting_status"
68
+ | "setting_workspace_display"
69
+ | "setting_terminal_title"
70
+ | "setting_busy_loader"
71
+ | "theme"
72
+ | "help"
73
+ | "copy"
74
+ | "backends"
75
+ | "models"
76
+ | "workspace"
77
+ | "workspace_relaunch"
78
+ | "config_status"
79
+ | "config_trust_status"
80
+ | "config_trust_set"
81
+ | "open_auth_panel"
82
+ | "open_theme_picker"
83
+ | "open_permissions_panel"
84
+ | "themes"
85
+ | "mouse_toggle"
86
+ | "verbose_toggle"
87
+ | "status"
88
+ | "permissions_status"
89
+ | "runtime_approval_policy"
90
+ | "runtime_sandbox_mode"
91
+ | "runtime_network_access"
92
+ | "runtime_writable_roots_add"
93
+ | "runtime_writable_roots_remove"
94
+ | "runtime_writable_roots_clear"
95
+ | "runtime_writable_roots_list"
96
+ | "runtime_service_tier"
97
+ | "runtime_personality"
98
+ | "diagnose_github"
99
+ | "diagnose_providers"
100
+ | "unknown";
101
+
102
+ export interface CommandResult {
103
+ action: CommandAction;
104
+ message?: string;
105
+ value?: string;
106
+ }
107
+
108
+ export interface CommandContext {
109
+ config: LayeredConfigResult;
110
+ runtime: RuntimeConfig;
111
+ resolvedRuntime: ResolvedRuntimeConfig;
112
+ settings: {
113
+ workspaceDisplayMode: WorkspaceDisplayMode;
114
+ terminalTitleMode: TerminalTitleMode;
115
+ showBusyLoader: boolean;
116
+ };
117
+ workspace: WorkspaceCommandContext;
118
+ tokensUsed?: number;
119
+ modelCapabilities?: CodexModelCapabilities | null;
120
+ routeStatusMessage?: string;
121
+ activeRouteProviderLabel?: string;
122
+ projectInstructions?: import("../core/projectInstructions.js").ProjectInstructionsLoadResult | null;
123
+ }
124
+
125
+ // Mirrors AVAILABLE_APPROVAL_POLICIES[].id from runtimeConfig.ts
126
+ const APPROVAL_POLICY_VALUES = ["inherit", "untrusted", "on-request", "never"] as const;
127
+ // Mirrors AVAILABLE_SANDBOX_MODES[].id from runtimeConfig.ts
128
+ const SANDBOX_MODE_VALUES = ["inherit", "read-only", "workspace-write", "danger-full-access"] as const;
129
+ // Input aliases — "on"/"off" are mapped to "enabled"/"disabled" in the network case below
130
+ const NETWORK_ACCESS_VALUES = ["inherit", "on", "off"] as const;
131
+ // Mirrors AVAILABLE_SERVICE_TIERS[].id from runtimeConfig.ts
132
+ const SERVICE_TIER_VALUES = ["flex", "fast"] as const;
133
+ // Mirrors AVAILABLE_PERSONALITIES[].id from runtimeConfig.ts
134
+ const PERSONALITY_VALUES = ["none", "friendly", "pragmatic"] as const;
135
+
136
+ function isOneOf<T extends string>(value: string, list: readonly T[]): value is T {
137
+ return (list as readonly string[]).includes(value);
138
+ }
139
+
140
+ function formatWritableRoots(roots: readonly string[]): string {
141
+ return roots.length > 0
142
+ ? roots.map((root) => ` - ${root}`).join("\n")
143
+ : " - none";
144
+ }
145
+
146
+ function expandReasoningAliases(arg: string): string {
147
+ const normalized = arg.toLowerCase();
148
+ // "extra high" is a user-facing alias for "xhigh"
149
+ return normalized === "extra high" ? "xhigh" : normalized;
150
+ }
151
+
152
+ function isKnownFallbackReasoning(value: string): boolean {
153
+ return AVAILABLE_REASONING_LEVELS.some((item) => item.id === value);
154
+ }
155
+
156
+ function simplePolicySetter<T extends string>(
157
+ rest: string,
158
+ normalizedRest: string,
159
+ action: CommandAction,
160
+ values: readonly T[],
161
+ statusMessage: string,
162
+ setMessage: (value: T) => string,
163
+ usageMessage: string,
164
+ ): CommandResult {
165
+ if (!rest || normalizedRest === "status") {
166
+ return { action, message: statusMessage };
167
+ }
168
+ if (isOneOf(normalizedRest, values)) {
169
+ return { action, value: normalizedRest, message: setMessage(normalizedRest) };
170
+ }
171
+ return { action: "unknown", message: usageMessage };
172
+ }
173
+
174
+ function handlePolicyCommand(
175
+ commandPrefix: "/runtime" | "/permissions",
176
+ arg: string,
177
+ context: CommandContext,
178
+ includeExtendedControls = true,
179
+ ): CommandResult {
180
+ const [subcommandRaw, ...restTokens] = arg.split(/\s+/);
181
+ const subcommand = subcommandRaw?.toLowerCase() ?? "";
182
+ const rest = restTokens.join(" ").trim();
183
+ const normalizedRest = rest.toLowerCase();
184
+
185
+ switch (subcommand) {
186
+ case "approval-policy": {
187
+ return simplePolicySetter(
188
+ rest,
189
+ normalizedRest,
190
+ "runtime_approval_policy",
191
+ APPROVAL_POLICY_VALUES,
192
+ `Approval policy: configured ${formatApprovalPolicyLabel(context.runtime.policy.approvalPolicy)}; effective ${formatApprovalPolicyLabel(context.resolvedRuntime.policy.approvalPolicy)}.`,
193
+ (v) => `Approval policy set to ${formatApprovalPolicyLabel(v)}.`,
194
+ `Usage: ${commandPrefix} approval-policy [status|inherit|untrusted|on-request|never]`,
195
+ );
196
+ }
197
+
198
+ case "sandbox": {
199
+ return simplePolicySetter(
200
+ rest,
201
+ normalizedRest,
202
+ "runtime_sandbox_mode",
203
+ SANDBOX_MODE_VALUES,
204
+ `Sandbox mode: configured ${formatSandboxModeLabel(context.runtime.policy.sandboxMode)}; effective ${formatSandboxModeLabel(context.resolvedRuntime.policy.sandboxMode)}.`,
205
+ (v) => `Sandbox mode set to ${formatSandboxModeLabel(v)}.`,
206
+ `Usage: ${commandPrefix} sandbox [status|inherit|read-only|workspace-write|danger-full-access]`,
207
+ );
208
+ }
209
+
210
+ case "network": {
211
+ if (!rest || normalizedRest === "status") {
212
+ return {
213
+ action: "runtime_network_access",
214
+ message: `Network access: configured ${formatNetworkAccessLabel(context.runtime.policy.networkAccess)}; effective ${formatNetworkAccessLabel(context.resolvedRuntime.policy.networkAccess)}.`,
215
+ };
216
+ }
217
+ if (isOneOf(normalizedRest, NETWORK_ACCESS_VALUES)) {
218
+ // "on"/"off" are accepted aliases for "enabled"/"disabled"
219
+ const value: RuntimeNetworkAccess = normalizedRest === "on"
220
+ ? "enabled"
221
+ : normalizedRest === "off"
222
+ ? "disabled"
223
+ : "inherit";
224
+ return {
225
+ action: "runtime_network_access",
226
+ value,
227
+ message: `Network access set to ${formatNetworkAccessLabel(value)}.`,
228
+ };
229
+ }
230
+ return {
231
+ action: "unknown",
232
+ message: `Usage: ${commandPrefix} network [status|inherit|on|off]`,
233
+ };
234
+ }
235
+
236
+ case "writable-roots": {
237
+ if (!rest || normalizedRest === "list" || normalizedRest === "status") {
238
+ return {
239
+ action: "runtime_writable_roots_list",
240
+ message: `Writable roots:\n${formatWritableRoots(context.runtime.policy.writableRoots)}`,
241
+ };
242
+ }
243
+
244
+ if (normalizedRest === "clear") {
245
+ return {
246
+ action: "runtime_writable_roots_clear",
247
+ message: "Writable roots cleared.",
248
+ };
249
+ }
250
+
251
+ if (normalizedRest.startsWith("add ")) {
252
+ const pathValue = rest.slice("add".length).trim();
253
+ if (!pathValue) {
254
+ return {
255
+ action: "unknown",
256
+ message: `Usage: ${commandPrefix} writable-roots add <path>`,
257
+ };
258
+ }
259
+ return {
260
+ action: "runtime_writable_roots_add",
261
+ value: pathValue,
262
+ message: `Writable root added: ${pathValue}`,
263
+ };
264
+ }
265
+
266
+ if (normalizedRest.startsWith("remove ")) {
267
+ const pathValue = rest.slice("remove".length).trim();
268
+ if (!pathValue) {
269
+ return {
270
+ action: "unknown",
271
+ message: `Usage: ${commandPrefix} writable-roots remove <path>`,
272
+ };
273
+ }
274
+ return {
275
+ action: "runtime_writable_roots_remove",
276
+ value: pathValue,
277
+ message: `Writable root removed: ${pathValue}`,
278
+ };
279
+ }
280
+
281
+ return {
282
+ action: "unknown",
283
+ message: `Usage: ${commandPrefix} writable-roots [list|add <path>|remove <path>|clear]`,
284
+ };
285
+ }
286
+
287
+ case "service-tier": {
288
+ if (!includeExtendedControls) {
289
+ return {
290
+ action: "unknown",
291
+ message: `Unknown ${commandPrefix.slice(1)} command. Use ${commandPrefix} <approval-policy|sandbox|network|writable-roots>.`,
292
+ };
293
+ }
294
+ return simplePolicySetter(
295
+ rest,
296
+ normalizedRest,
297
+ "runtime_service_tier",
298
+ SERVICE_TIER_VALUES,
299
+ `Service tier: ${formatServiceTierLabel(context.runtime.policy.serviceTier)}.`,
300
+ (v) => `Service tier set to ${formatServiceTierLabel(v)}.`,
301
+ `Usage: ${commandPrefix} service-tier [status|flex|fast]`,
302
+ );
303
+ }
304
+
305
+ case "personality": {
306
+ if (!includeExtendedControls) {
307
+ return {
308
+ action: "unknown",
309
+ message: `Unknown ${commandPrefix.slice(1)} command. Use ${commandPrefix} <approval-policy|sandbox|network|writable-roots>.`,
310
+ };
311
+ }
312
+ return simplePolicySetter(
313
+ rest,
314
+ normalizedRest,
315
+ "runtime_personality",
316
+ PERSONALITY_VALUES,
317
+ `Personality: ${formatPersonalityLabel(context.runtime.policy.personality)}.`,
318
+ (v) => `Personality set to ${formatPersonalityLabel(v)}.`,
319
+ `Usage: ${commandPrefix} personality [status|none|friendly|pragmatic]`,
320
+ );
321
+ }
322
+
323
+ default:
324
+ return {
325
+ action: "unknown",
326
+ message: includeExtendedControls
327
+ ? `Unknown ${commandPrefix.slice(1)} command. Use /status or ${commandPrefix} <approval-policy|sandbox|network|writable-roots|service-tier|personality>.`
328
+ : `Unknown ${commandPrefix.slice(1)} command. Use /permissions <approval-policy|sandbox|network|writable-roots>.`,
329
+ };
330
+ }
331
+ }
332
+
333
+ function formatRenderCounts(): string {
334
+ const counts = dumpRenderCounts();
335
+ const lines = Object.entries(counts)
336
+ .sort(([, a], [, b]) => b - a)
337
+ .map(([name, count]) => ` ${name}: ${count}`);
338
+ return lines.length > 0
339
+ ? `Render counts:\n${lines.join("\n")}`
340
+ : "No render counts recorded. Set CODEXA_RENDER_DEBUG=1 to enable.";
341
+ }
342
+
343
+ function buildHelpMessage(context: CommandContext): string {
344
+ return [
345
+ "Shell execution:",
346
+ " !<command> Run any shell command directly e.g. !ls -la",
347
+ " Output streams live in a terminal block",
348
+ " Esc cancels a running command",
349
+ "",
350
+ "Commands:",
351
+ " /exit, /quit Quit the application and cancel active run",
352
+ " /clear Clear the chat window and cancel the active run",
353
+ " /diagnose github|providers Run diagnostics",
354
+ " /backend [name] Switch backend (no arg opens picker)",
355
+ " /providers Open provider picker (/provider alias)",
356
+ " /route Show workspace default and active chat route",
357
+ " /model [name] Switch model (no arg opens picker)",
358
+ ` /mode [name] Switch execution mode (${formatModeCommandHelp()})`,
359
+ " suggest = read-only-style prompting, auto-edit = file edits, full-auto = strongest autonomy",
360
+ " /reasoning [level] Set reasoning level (no arg opens picker)",
361
+ " /plan [on|off] Show or toggle session plan mode",
362
+ " /setting, /settings Open the settings picker",
363
+ " /setting workspace [dir|name|simple] Control the header workspace label",
364
+ " /setting terminal-title [dir|name|simple] Control the terminal tab title",
365
+ " /setting busy-loader [true|false] Control the busy footer animation",
366
+ " /status Show the effective runtime configuration",
367
+ " /config Show layered config sources and winning values",
368
+ " /config trust [status|on|off] Manage whether project config is allowed to load",
369
+ " /permissions Open or update permissions and sandbox controls",
370
+ " /permissions status",
371
+ " /permissions approval-policy [status|inherit|untrusted|on-request|never]",
372
+ " /permissions sandbox [status|inherit|read-only|workspace-write|danger-full-access]",
373
+ " /permissions network [status|inherit|on|off]",
374
+ " /permissions writable-roots [list|add <path>|remove <path>|clear]",
375
+ " /runtime ... Inspect or update runtime policy controls",
376
+ " Compatibility surface; /permissions is the primary entry point",
377
+ " /runtime approval-policy [status|inherit|untrusted|on-request|never]",
378
+ " /runtime sandbox [status|inherit|read-only|workspace-write|danger-full-access]",
379
+ " /runtime network [status|inherit|on|off]",
380
+ " /runtime writable-roots [list|add <path>|remove <path>|clear]",
381
+ " /runtime service-tier [status|flex|fast]",
382
+ " /runtime personality [status|none|friendly|pragmatic]",
383
+ " /theme [name] Switch theme directly (no arg opens picker)",
384
+ " /themes Open visual theme picker (Up/Down + Enter)",
385
+ " /verbose Toggle verbose mode (shows detailed processing info)",
386
+ " /mouse Toggle SGR mouse capture for in-app wheel scroll (off by default). On: wheel scrolls the Codexa timeline; drag-select requires Shift. Off: native drag-select and native wheel scroll work without modifiers.",
387
+ " /auth [option] Open auth panel or set auth preference",
388
+ " /auth status Probe Codexa auth status",
389
+ " /login Show guided ChatGPT subscription login steps",
390
+ " /logout Show guided logout steps",
391
+ " /backends List all available backends",
392
+ " /models Open model picker",
393
+ " /workspace Show the locked workspace for this session",
394
+ " /workspace relaunch <path> Restart the app in another workspace folder",
395
+ ` Current reasoning: ${formatReasoningLabel(context.runtime.reasoningLevel)}`,
396
+ ` Current plan mode: ${context.runtime.planMode ? "Enabled" : "Disabled"}`,
397
+ " /copy Copy last response to clipboard",
398
+ " /help Show this help",
399
+ "",
400
+ "Install on Windows:",
401
+ " npm link Make the codexa command available",
402
+ " where codexa Verify the command resolves",
403
+ "",
404
+ "Shortcuts:",
405
+ " Ctrl+B Open backend picker",
406
+ " Ctrl+O Open model picker",
407
+ " Shift+Tab Toggle plan mode",
408
+ " Ctrl+P Open mode picker",
409
+ " Ctrl+Alt+P Open provider picker",
410
+ " Ctrl+A Open auth panel",
411
+ " Ctrl+L Clear chat and cancel active run",
412
+ " Esc Cancel active run or shell command",
413
+ " Ctrl+Y Cycle execution mode",
414
+ " Ctrl+C / Ctrl+Q Quit",
415
+ " ↑ / ↓ Navigate input history",
416
+ ].join("\n");
417
+ }
418
+
419
+ export function handleCommand(text: string, context: CommandContext): CommandResult | null {
420
+ if (text.startsWith("/")) {
421
+ const [rawCmd, ...argTokens] = text.slice(1).trim().split(/\s+/);
422
+ const cmd = rawCmd?.toLowerCase() ?? "";
423
+ const arg = argTokens.join(" ").trim();
424
+ const normalizedArg = arg.toLowerCase();
425
+
426
+ switch (cmd) {
427
+ case "exit":
428
+ case "quit":
429
+ return { action: "exit" };
430
+
431
+ case "clear":
432
+ return { action: "clear" };
433
+
434
+ case "backend": {
435
+ if (!arg) return { action: "open_backend_picker" };
436
+ if (AVAILABLE_BACKENDS.some((item) => item.id === arg)) {
437
+ return {
438
+ action: "backend",
439
+ value: arg,
440
+ message: `Backend switched to ${formatBackendLabel(arg)}`,
441
+ };
442
+ }
443
+ return {
444
+ action: "unknown",
445
+ message: `Unknown backend: ${arg}. Use /backends to list available backends.`,
446
+ };
447
+ }
448
+
449
+ case "providers":
450
+ case "provider":
451
+ return { action: "open_provider_picker" };
452
+
453
+ case "route":
454
+ return {
455
+ action: "route_status",
456
+ message: context.routeStatusMessage ?? [
457
+ "Route status:",
458
+ " Workspace default: OpenAI",
459
+ ` Active chat route: OpenAI / ${context.runtime.model}`,
460
+ ` Active model: ${context.runtime.model}`,
461
+ " Active provider mode: Usable inside Codexa",
462
+ ].join("\n"),
463
+ };
464
+
465
+ case "model": {
466
+ if (!arg) return { action: "open_model_picker" };
467
+ const detectedModels = context.modelCapabilities
468
+ ? getSelectableModelCapabilities(context.modelCapabilities)
469
+ : [];
470
+ const detectedModel = detectedModels.find((model) => model.model === arg || model.id === arg);
471
+ if (detectedModel) {
472
+ return { action: "model", value: detectedModel.model, message: `Model switched to ${detectedModel.model}` };
473
+ }
474
+ if (!context.modelCapabilities && (AVAILABLE_MODELS as readonly string[]).includes(arg)) {
475
+ return { action: "model", value: arg, message: `Model switched to ${arg}` };
476
+ }
477
+ return {
478
+ action: "unknown",
479
+ message: `Unknown model: ${arg}. Use /models to list available models.`,
480
+ };
481
+ }
482
+
483
+ case "models":
484
+ return { action: "open_model_picker" };
485
+
486
+ case "mode": {
487
+ if (!arg) return { action: "open_mode_picker" };
488
+ const resolvedMode = resolveModeCommand(arg);
489
+ if (resolvedMode) {
490
+ return {
491
+ action: "mode",
492
+ value: resolvedMode,
493
+ message: `Mode switched to ${formatModeLabel(resolvedMode)}`,
494
+ };
495
+ }
496
+ return {
497
+ action: "unknown",
498
+ message: `Unknown mode: ${arg}. Valid: ${formatModeCommandHelp()}`,
499
+ };
500
+ }
501
+
502
+ case "reasoning": {
503
+ if (!arg) return { action: "open_reasoning_picker" };
504
+ const normalized = expandReasoningAliases(arg);
505
+ const modelCapability = findModelCapability(context.modelCapabilities, context.runtime.model);
506
+ const detectedLevels = modelCapability?.supportedReasoningLevels;
507
+ if (detectedLevels && detectedLevels.some((item) => item.id === normalized)) {
508
+ return {
509
+ action: "reasoning",
510
+ value: normalized,
511
+ message: `Reasoning level switched to ${formatReasoningLabel(normalized)}`,
512
+ };
513
+ }
514
+ if (detectedLevels) {
515
+ const valid = detectedLevels.map((item) => item.id).join(", ");
516
+ return {
517
+ action: "unknown",
518
+ message: `Unknown reasoning level for ${context.runtime.model}: ${arg}. Valid: ${valid}`,
519
+ };
520
+ }
521
+ if (isKnownFallbackReasoning(normalized)) {
522
+ return {
523
+ action: "reasoning",
524
+ value: normalized,
525
+ message: `Reasoning level switched to ${formatReasoningLabel(normalized)} (runtime metadata unavailable)`,
526
+ };
527
+ }
528
+ return {
529
+ action: "unknown",
530
+ message: `Unknown reasoning level: ${arg}. Runtime reasoning metadata is unavailable for ${context.runtime.model}.`,
531
+ };
532
+ }
533
+
534
+ case "plan": {
535
+ if (!arg || normalizedArg === "status") {
536
+ return {
537
+ action: "plan_mode",
538
+ message: `Plan mode: ${context.runtime.planMode ? "Enabled" : "Disabled"}.`,
539
+ };
540
+ }
541
+
542
+ if (normalizedArg === "on" || normalizedArg === "off") {
543
+ return {
544
+ action: "plan_mode",
545
+ value: normalizedArg,
546
+ message: `Plan mode ${normalizedArg === "on" ? "enabled" : "disabled"}.`,
547
+ };
548
+ }
549
+
550
+ return {
551
+ action: "unknown",
552
+ message: "Usage: /plan [on|off]",
553
+ };
554
+ }
555
+
556
+ case "setting":
557
+ case "settings": {
558
+ if (!arg) {
559
+ return { action: "open_settings_panel" };
560
+ }
561
+
562
+ if (normalizedArg === "workspace" || normalizedArg === "workspace-display" || normalizedArg === "directory") {
563
+ return {
564
+ action: "setting_workspace_display",
565
+ message: [
566
+ `Workspace display: ${formatWorkspaceDisplayModeLabel(context.settings.workspaceDisplayMode)} (${context.settings.workspaceDisplayMode})`,
567
+ "Allowed values: dir, name, simple",
568
+ "dir = show the current workspace folder name",
569
+ "name = show Codexa",
570
+ "simple = show only the final folder name",
571
+ ].join("\n"),
572
+ };
573
+ }
574
+
575
+ const workspaceSettingPrefix = ["workspace ", "workspace-display ", "directory "].find((prefix) => normalizedArg.startsWith(prefix));
576
+ if (workspaceSettingPrefix) {
577
+ const nextValue = normalizedArg.slice(workspaceSettingPrefix.length).trim();
578
+ // "normal" was the legacy default label before "dir" was introduced
579
+ const legacyMap: Record<string, WorkspaceDisplayMode> = {
580
+ normal: normalizeLegacyDirectoryDisplayMode("normal"),
581
+ };
582
+ const mappedValue = legacyMap[nextValue] ?? nextValue;
583
+ if (WORKSPACE_DISPLAY_MODES.includes(mappedValue as WorkspaceDisplayMode)) {
584
+ const value = mappedValue as WorkspaceDisplayMode;
585
+ return {
586
+ action: "setting_workspace_display",
587
+ value,
588
+ message: `Workspace display set to ${formatWorkspaceDisplayModeLabel(value)} (${value}).`,
589
+ };
590
+ }
591
+
592
+ return {
593
+ action: "unknown",
594
+ message: "Usage: /setting workspace [dir|name|simple]",
595
+ };
596
+ }
597
+
598
+ if (normalizedArg === "terminal-title" || normalizedArg === "terminal") {
599
+ return {
600
+ action: "setting_terminal_title",
601
+ message: [
602
+ `Terminal title: ${formatWorkspaceDisplayModeLabel(context.settings.terminalTitleMode)} (${context.settings.terminalTitleMode})`,
603
+ "Allowed values: dir, name, simple",
604
+ "dir = show the current workspace folder name",
605
+ "name = show Codexa",
606
+ "simple = show only the final folder name",
607
+ ].join("\n"),
608
+ };
609
+ }
610
+
611
+ const terminalTitleSettingPrefix = ["terminal-title ", "terminal "].find((prefix) => normalizedArg.startsWith(prefix));
612
+ if (terminalTitleSettingPrefix) {
613
+ const nextValue = normalizedArg.slice(terminalTitleSettingPrefix.length).trim();
614
+ if (WORKSPACE_DISPLAY_MODES.includes(nextValue as WorkspaceDisplayMode)) {
615
+ const value = nextValue as TerminalTitleMode;
616
+ return {
617
+ action: "setting_terminal_title",
618
+ value,
619
+ message: `Terminal title set to ${formatWorkspaceDisplayModeLabel(value)} (${value}).`,
620
+ };
621
+ }
622
+
623
+ return {
624
+ action: "unknown",
625
+ message: "Usage: /setting terminal-title [dir|name|simple]",
626
+ };
627
+ }
628
+
629
+ if (normalizedArg === "busy-loader") {
630
+ return {
631
+ action: "setting_busy_loader",
632
+ message: `Busy loader: ${context.settings.showBusyLoader ? "true" : "false"}`,
633
+ };
634
+ }
635
+
636
+ if (normalizedArg.startsWith("busy-loader ")) {
637
+ const nextValue = normalizedArg.slice("busy-loader ".length).trim();
638
+ if (BUSY_LOADER_SETTING_VALUES.includes(nextValue as BusyLoaderSettingValue)) {
639
+ return {
640
+ action: "setting_busy_loader",
641
+ value: nextValue,
642
+ message: `Busy loader ${nextValue === "true" ? "enabled" : "disabled"}.`,
643
+ };
644
+ }
645
+
646
+ return {
647
+ action: "unknown",
648
+ message: "Usage: /setting busy-loader [true|false]",
649
+ };
650
+ }
651
+
652
+ return {
653
+ action: "unknown",
654
+ message: "Usage: /setting, /setting workspace [dir|name|simple], /setting terminal-title [dir|name|simple], or /setting busy-loader [true|false]",
655
+ };
656
+ }
657
+
658
+ case "theme": {
659
+ if (!arg) return { action: "open_theme_picker" };
660
+ if (AVAILABLE_THEMES.some((item) => item.id === arg)) {
661
+ return {
662
+ action: "theme",
663
+ value: arg,
664
+ message: `Theme switched to ${formatThemeLabel(arg)}`,
665
+ };
666
+ }
667
+ return {
668
+ action: "unknown",
669
+ message: `Unknown theme: ${arg}. Use /themes to list available themes.`,
670
+ };
671
+ }
672
+
673
+ case "backends": {
674
+ const list = AVAILABLE_BACKENDS
675
+ .map((item, index) => ` ${index + 1}. ${item.label} (${item.id})`)
676
+ .join("\n");
677
+ return {
678
+ action: "backends",
679
+ message: `Available backends:\n${list}\n\nCurrent: ${formatBackendLabel(context.runtime.provider)}`,
680
+ };
681
+ }
682
+
683
+ case "workspace": {
684
+ if (!arg) {
685
+ return {
686
+ action: "workspace",
687
+ message: context.workspace.summaryMessage,
688
+ };
689
+ }
690
+
691
+ if (normalizedArg === "relaunch") {
692
+ return {
693
+ action: "unknown",
694
+ message: "Usage: /workspace relaunch <path>",
695
+ };
696
+ }
697
+
698
+ if (normalizedArg.startsWith("relaunch ")) {
699
+ return {
700
+ action: "workspace_relaunch",
701
+ value: arg.slice("relaunch".length).trim(),
702
+ };
703
+ }
704
+
705
+ return {
706
+ action: "unknown",
707
+ message: "Unknown workspace command. Use /workspace or /workspace relaunch <path>.",
708
+ };
709
+ }
710
+
711
+ case "config": {
712
+ if (!arg || normalizedArg === "status") {
713
+ return {
714
+ action: "config_status",
715
+ message: formatLayeredConfigStatus(context.config),
716
+ };
717
+ }
718
+
719
+ if (normalizedArg === "trust" || normalizedArg === "trust status") {
720
+ return {
721
+ action: "config_trust_status",
722
+ message: [
723
+ "Project trust:",
724
+ ` Root: ${context.config.diagnostics.projectRoot}`,
725
+ ` Status: ${context.config.diagnostics.projectTrusted ? "Trusted" : "Untrusted"}`,
726
+ ].join("\n"),
727
+ };
728
+ }
729
+
730
+ if (normalizedArg === "trust on" || normalizedArg === "trust off") {
731
+ return {
732
+ action: "config_trust_set",
733
+ value: normalizedArg.endsWith("on") ? "on" : "off",
734
+ message: `Project trust ${normalizedArg.endsWith("on") ? "enabled" : "disabled"}.`,
735
+ };
736
+ }
737
+
738
+ return {
739
+ action: "unknown",
740
+ message: "Unknown config command. Use /config, /config status, or /config trust [status|on|off].",
741
+ };
742
+ }
743
+
744
+ case "auth": {
745
+ if (!arg) return { action: "open_auth_panel" };
746
+ if (arg === "status") {
747
+ return { action: "auth_status" };
748
+ }
749
+ if (AUTH_PREFERENCES.some((item) => item.id === arg)) {
750
+ return {
751
+ action: "auth",
752
+ value: arg,
753
+ message: `Auth preference set to ${formatAuthPreferenceLabel(arg)}`,
754
+ };
755
+ }
756
+ return {
757
+ action: "unknown",
758
+ message: "Unknown auth option. Use /auth, /auth status, or one of the documented preference ids.",
759
+ };
760
+ }
761
+
762
+ case "status":
763
+ return {
764
+ action: "status",
765
+ message: [
766
+ formatRuntimeStatus(context.resolvedRuntime, {
767
+ workspaceRoot: context.workspace.root,
768
+ tokensUsed: context.tokensUsed,
769
+ projectInstructions: context.projectInstructions ?? null,
770
+ }),
771
+ context.routeStatusMessage,
772
+ ].filter((line): line is string => Boolean(line)).join("\n\n"),
773
+ };
774
+
775
+ case "permissions": {
776
+ if (!arg) {
777
+ return { action: "open_permissions_panel" };
778
+ }
779
+
780
+ if (normalizedArg === "status") {
781
+ return {
782
+ action: "permissions_status",
783
+ message: formatPermissionsStatus(
784
+ context.runtime,
785
+ context.resolvedRuntime,
786
+ context.workspace.root,
787
+ ),
788
+ };
789
+ }
790
+
791
+ return handlePolicyCommand("/permissions", arg, context, false);
792
+ }
793
+
794
+ case "runtime": {
795
+ if (!arg) {
796
+ return {
797
+ action: "status",
798
+ message: formatRuntimeStatus(context.resolvedRuntime, {
799
+ workspaceRoot: context.workspace.root,
800
+ tokensUsed: context.tokensUsed,
801
+ }),
802
+ };
803
+ }
804
+ return handlePolicyCommand("/runtime", arg, context, true);
805
+ }
806
+
807
+ case "login":
808
+ return { action: "login" };
809
+
810
+ case "logout":
811
+ return { action: "logout" };
812
+
813
+ case "copy":
814
+ return { action: "copy" };
815
+
816
+ case "themes":
817
+ return { action: "open_theme_picker" };
818
+
819
+ case "mouse":
820
+ return { action: "mouse_toggle" };
821
+
822
+ case "verbose":
823
+ return { action: "verbose_toggle" };
824
+
825
+ case "debug": {
826
+ if (normalizedArg === "renders") {
827
+ return {
828
+ action: "verbose_toggle",
829
+ message: formatRenderCounts(),
830
+ };
831
+ }
832
+ return { action: "verbose_toggle" };
833
+ }
834
+
835
+ case "diagnose": {
836
+ if (normalizedArg === "github") {
837
+ return {
838
+ action: "diagnose_github",
839
+ message: "Running GitHub connectivity diagnostics...",
840
+ };
841
+ }
842
+ if (normalizedArg === "providers") {
843
+ return {
844
+ action: "diagnose_providers",
845
+ message: "Collecting provider diagnostics...",
846
+ };
847
+ }
848
+ return {
849
+ action: "unknown",
850
+ message: "Usage: /diagnose github|providers",
851
+ };
852
+ }
853
+
854
+ case "help":
855
+ return { action: "help", message: buildHelpMessage(context) };
856
+
857
+ default:
858
+ return {
859
+ action: "unknown",
860
+ message: `Unknown command: /${cmd}. Type /help for available commands.`,
861
+ };
862
+ }
863
+ }
864
+
865
+ // "?cmd" is a common mistype of "/cmd" — suggest the corrected form
866
+ if (text.startsWith("?")) {
867
+ const potentialCmd = text.slice(1).trim().split(/\s+/)[0]?.toLowerCase() ?? "";
868
+ return {
869
+ action: "unknown",
870
+ message: `Invalid command syntax: ${text}. Use /help for available commands. Did you mean /${potentialCmd}?`,
871
+ };
872
+ }
873
+
874
+ return null;
875
+ }