@golba98/codexa 1.0.4 → 1.0.6

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 (78) hide show
  1. package/README.md +6 -10
  2. package/package.json +1 -1
  3. package/src/app.tsx +188 -121
  4. package/src/commands/handler.ts +11 -20
  5. package/src/config/appVersion.ts +65 -0
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/settings.ts +5 -1
  8. package/src/config/updateCheckCache.ts +18 -8
  9. package/src/core/models/codexModelCapabilities.ts +57 -4
  10. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  11. package/src/core/models/providerModelCache.ts +90 -0
  12. package/src/core/process/CommandRunner.ts +12 -1
  13. package/src/core/providerLauncher/registry.ts +37 -7
  14. package/src/core/providerLauncher/types.ts +2 -1
  15. package/src/core/providerRuntime/anthropic.ts +3 -5
  16. package/src/core/providerRuntime/antigravity.ts +257 -90
  17. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -692
  18. package/src/core/providerRuntime/mistralVibe.ts +663 -0
  19. package/src/core/providerRuntime/models.ts +18 -4
  20. package/src/core/providerRuntime/reasoning.ts +9 -6
  21. package/src/core/providerRuntime/registry.ts +93 -44
  22. package/src/core/providerRuntime/types.ts +1 -0
  23. package/src/core/version/packageManager.ts +119 -0
  24. package/src/core/version/updateCheck.ts +15 -9
  25. package/src/index.tsx +23 -9
  26. package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
  27. package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
  28. package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
  29. package/src/ui/{AppShell.tsx → chrome/AppShell.tsx} +11 -11
  30. package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +15 -15
  31. package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
  32. package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
  33. package/src/ui/{RuntimeStatusBar.tsx → chrome/RuntimeStatusBar.tsx} +3 -3
  34. package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
  35. package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +15 -13
  36. package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +6 -5
  37. package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
  38. package/src/ui/{focus.ts → input/focus.ts} +1 -1
  39. package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
  40. package/src/ui/{slashCommands.ts → input/slashCommands.ts} +1 -0
  41. package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +1 -1
  42. package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
  43. package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
  44. package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
  45. package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
  46. package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +11 -9
  47. package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
  48. package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
  49. package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
  50. package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
  51. package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
  52. package/src/ui/{ProviderPicker.tsx → panels/ProviderPicker.tsx} +9 -7
  53. package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
  54. package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +1 -1
  55. package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +3 -3
  56. package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +3 -3
  57. package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
  58. package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +54 -44
  59. package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
  60. package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
  61. package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
  62. package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
  63. package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +9 -8
  64. package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
  65. package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
  66. package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
  67. package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
  68. package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +8 -8
  69. package/src/ui/{TranscriptShell.tsx → timeline/TranscriptShell.tsx} +6 -6
  70. package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
  71. package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
  72. package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
  73. package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +14 -14
  74. /package/src/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
  75. /package/src/ui/{logoVariants.ts → render/logoVariants.ts} +0 -0
  76. /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
  77. /package/src/ui/{textLayout.ts → render/textLayout.ts} +0 -0
  78. /package/src/ui/{layoutListWindow.ts → timeline/layoutListWindow.ts} +0 -0
@@ -1,4 +1,5 @@
1
- import type { ReasoningEffortCapability } from "../models/codexModelCapabilities.js";
1
+ import type { ReasoningEffortCapability } from "../models/codexModelCapabilities.js";
2
+ import { formatReasoningLabel } from "../../config/settings.js";
2
3
 
3
4
  export const CLAUDE_CODE_EFFORT_LEVELS: readonly ReasoningEffortCapability[] = [
4
5
  { id: "low", label: "Low", description: "Claude Code low effort." },
@@ -10,8 +11,10 @@ export const CLAUDE_CODE_EFFORT_LEVELS: readonly ReasoningEffortCapability[] = [
10
11
 
11
12
  export const CLAUDE_CODE_EFFORT_IDS = new Set(CLAUDE_CODE_EFFORT_LEVELS.map((level) => level.id));
12
13
 
13
- export function getClaudeCodeEffortLevels(ids: readonly string[]): readonly ReasoningEffortCapability[] {
14
- return ids
15
- .map((id) => CLAUDE_CODE_EFFORT_LEVELS.find((level) => level.id === id))
16
- .filter((level): level is ReasoningEffortCapability => Boolean(level));
17
- }
14
+ export function getClaudeCodeEffortLevels(ids: readonly string[]): readonly ReasoningEffortCapability[] {
15
+ return ids.map((id) => CLAUDE_CODE_EFFORT_LEVELS.find((level) => level.id === id) ?? {
16
+ id,
17
+ label: formatReasoningLabel(id),
18
+ description: null,
19
+ });
20
+ }
@@ -1,10 +1,13 @@
1
1
  import { codexSubprocessProvider } from "../providers/codexSubprocess.js";
2
+ import { loadSeededOpenAiModels } from "../models/codexModelsCacheSeed.js";
3
+ import { loadCachedProviderModels, saveCachedProviderModels } from "../models/providerModelCache.js";
2
4
  import type { BackendRunHandlers } from "../providers/types.js";
3
5
  import type { ProviderId, ProviderActiveRoute, ProviderWorkspaceOverride } from "../providerLauncher/types.js";
4
- import { anthropicRuntime } from "./anthropic.js";
5
- import { geminiRuntime } from "./gemini.js";
6
- import { localRuntime } from "./local.js";
7
- import { antigravityRuntime, ANTIGRAVITY_DEFAULT_MODEL_ID, migrateAntigravityLegacyModelId } from "./antigravity.js";
6
+ import { anthropicRuntime } from "./anthropic.js";
7
+ import { geminiRuntime } from "./gemini.js";
8
+ import { localRuntime } from "./local.js";
9
+ import { antigravityRuntime, ANTIGRAVITY_DEFAULT_MODEL_ID, migrateAntigravityLegacyModelId } from "./antigravity.js";
10
+ import { mistralVibeRuntime } from "./mistralVibe.js";
8
11
  import {
9
12
  ANTHROPIC_FALLBACK_MODELS,
10
13
  GEMINI_DEFAULT_MODEL_ID,
@@ -28,11 +31,14 @@ const openAiRuntime: ProviderRuntime = {
28
31
  routeAvailable: true,
29
32
  routeStatus: "Uses the configured Codex/OpenAI backend inside Codexa.",
30
33
  launchAvailable: true,
34
+ // Seeded from local caches (codex's own models_cache.json or Codexa's
35
+ // last-good discovery); live app-server discovery refreshes them via
36
+ // getCodexModelCapabilities in the app run loop.
31
37
  discoverModels: () => ({
32
38
  status: "ready",
33
39
  providerId: "openai",
34
40
  backendKind: "codex-cli-auth",
35
- models: [],
41
+ models: loadSeededOpenAiModels()?.models ?? [],
36
42
  }),
37
43
  run: (request: ProviderChatRequest, handlers: BackendRunHandlers) => {
38
44
  handlers.onProgress?.({
@@ -72,11 +78,12 @@ function unavailableRuntime(providerId: ProviderId, label: string): ProviderRunt
72
78
 
73
79
  const PROVIDER_RUNTIMES: Record<ProviderId, ProviderRuntime> = {
74
80
  openai: openAiRuntime,
75
- anthropic: anthropicRuntime,
76
- google: geminiRuntime,
77
- local: localRuntime,
78
- antigravity: antigravityRuntime,
79
- };
81
+ anthropic: anthropicRuntime,
82
+ google: geminiRuntime,
83
+ mistral: mistralVibeRuntime,
84
+ local: localRuntime,
85
+ antigravity: antigravityRuntime,
86
+ };
80
87
 
81
88
  export function getProviderRuntime(providerId: ProviderId): ProviderRuntime {
82
89
  return PROVIDER_RUNTIMES[providerId];
@@ -97,17 +104,39 @@ export function getProviderRouteSetupMessage(providerId: ProviderId): string {
97
104
  }
98
105
 
99
106
  export function discoverProviderModels(providerId: ProviderId): ProviderModelDiscoveryResult {
100
- return getProviderRuntime(providerId).discoverModels();
107
+ const result = getProviderRuntime(providerId).discoverModels();
108
+ // When the live/synchronous probe only produced fallback entries (or none),
109
+ // prefer the last-good discovery persisted from a previous session.
110
+ const hasRuntimeModels = result.models.some((model) => model.source && model.source !== "fallback");
111
+ if (result.status === "ready" && !hasRuntimeModels) {
112
+ const cached = loadCachedProviderModels(providerId);
113
+ if (cached) {
114
+ return { ...result, models: cached.models };
115
+ }
116
+ }
117
+ return result;
118
+ }
119
+
120
+ // Persist a ready discovery so the next session starts from last-good models.
121
+ export function persistProviderDiscovery(discovery: ProviderModelDiscoveryResult): void {
122
+ const runtimeModels = discovery.models.filter((model) => model.source && model.source !== "fallback");
123
+ if (discovery.status !== "ready" || runtimeModels.length === 0) {
124
+ return;
125
+ }
126
+ saveCachedProviderModels(discovery.providerId, {
127
+ discoveredAt: Date.now(),
128
+ models: runtimeModels,
129
+ });
101
130
  }
102
131
 
103
132
  export async function validateProviderRouteActivation(options: {
104
133
  route: ProviderRoute;
105
- workspaceRoot: string;
106
- geminiCommandPath?: string | null;
107
- claudeCommandPath?: string | null;
108
- antigravityCommandPath?: string | null;
109
- localConfig?: ProviderWorkspaceOverride | null;
110
- }): Promise<ProviderRouteValidationResult> {
134
+ workspaceRoot: string;
135
+ geminiCommandPath?: string | null;
136
+ claudeCommandPath?: string | null;
137
+ antigravityCommandPath?: string | null;
138
+ localConfig?: ProviderWorkspaceOverride | null;
139
+ }): Promise<ProviderRouteValidationResult> {
111
140
  const runtime = getProviderRuntime(options.route.providerId);
112
141
  if (!runtime.routeAvailable) {
113
142
  return {
@@ -157,7 +186,7 @@ export function resolveActiveProviderRoute(options: {
157
186
  currentReasoning: string;
158
187
  }): ActiveProviderRoute {
159
188
  const configuredRoute = options.workspaceConfigActiveRoute;
160
- if (configuredRoute && configuredRoute.providerId !== "google" && isProviderRoutableInCodexa(configuredRoute.providerId)) {
189
+ if (configuredRoute && configuredRoute.providerId !== "google" && isProviderRoutableInCodexa(configuredRoute.providerId)) {
161
190
  const route: ActiveProviderRoute = {
162
191
  providerId: configuredRoute.providerId,
163
192
  modelId: configuredRoute.modelId,
@@ -166,36 +195,52 @@ export function resolveActiveProviderRoute(options: {
166
195
  ...(configuredRoute.modelSelection ? { modelSelection: configuredRoute.modelSelection } : {}),
167
196
  };
168
197
 
169
- if (route.providerId === "google" && route.modelSelection) {
170
- route.modelId = resolveGeminiModelId(route.modelSelection);
171
- } else if (route.providerId === "google") {
172
- route.modelId = normalizeGeminiModelId(route.modelId);
198
+ if (route.providerId === "google" && route.modelSelection) {
199
+ route.modelId = resolveGeminiModelId(route.modelSelection);
200
+ } else if (route.providerId === "google") {
201
+ route.modelId = normalizeGeminiModelId(route.modelId);
173
202
  } else if (route.providerId === "anthropic") {
174
203
  const discovery = discoverProviderModels("anthropic");
175
- const stillAvailable = discovery.models.some((model) =>
176
- model.modelId === route.modelId ||
177
- model.id === route.modelId ||
178
- model.canonicalId === route.modelId
179
- );
204
+ const stillAvailable = discovery.models.some((model) =>
205
+ model.modelId === route.modelId ||
206
+ model.id === route.modelId ||
207
+ model.canonicalId === route.modelId
208
+ );
180
209
  const hasNonFallbackModels = discovery.models.some((model) => model.source !== "fallback");
181
- if (discovery.status === "ready" && hasNonFallbackModels && discovery.models.length > 0 && !stillAvailable) {
210
+ // Preserve explicit versioned Claude IDs even when the current discovery
211
+ // output does not advertise that historical model. Short aliases are the
212
+ // values that need migration to the first currently discovered model.
213
+ const isKnownShortAlias = ANTHROPIC_FALLBACK_MODELS.some((model) => model.modelId === route.modelId);
214
+ if (discovery.status === "ready" && hasNonFallbackModels && discovery.models.length > 0 && !stillAvailable && isKnownShortAlias) {
182
215
  route.modelId = discovery.models[0]!.modelId;
183
216
  }
184
- } else if (route.providerId === "local") {
185
- const discovery = discoverProviderModels("local");
186
- const selectedModel = typeof discovery.diagnostics?.selectedModel === "string"
217
+ } else if (route.providerId === "local") {
218
+ const discovery = discoverProviderModels("local");
219
+ const selectedModel = typeof discovery.diagnostics?.selectedModel === "string"
187
220
  ? discovery.diagnostics.selectedModel.trim()
188
221
  : "";
189
- if (discovery.status === "ready" && selectedModel) {
190
- route.modelId = selectedModel;
191
- }
222
+ if (discovery.status === "ready" && selectedModel) {
223
+ route.modelId = selectedModel;
224
+ }
192
225
  } else if (route.providerId === "antigravity") {
193
226
  const migrated = migrateAntigravityLegacyModelId(route.modelId);
194
227
  route.modelId = migrated.modelId;
195
228
  if (!route.reasoning && migrated.reasoning) {
196
229
  route.reasoning = migrated.reasoning;
197
230
  }
198
- }
231
+ const discovery = discoverProviderModels("antigravity");
232
+ if (discovery.status === "ready" && discovery.models.length > 0) {
233
+ let model = discovery.models.find((item) => item.modelId === route.modelId || item.id === route.modelId);
234
+ if (!model) {
235
+ model = discovery.models[0];
236
+ route.modelId = model.modelId;
237
+ }
238
+ const levels = model.supportedReasoningLevels;
239
+ if (levels?.length && (!route.reasoning || !levels.some((level) => level.id === route.reasoning))) {
240
+ route.reasoning = model.defaultReasoningLevel ?? levels[0]?.id;
241
+ }
242
+ }
243
+ }
199
244
 
200
245
  return route;
201
246
  }
@@ -214,17 +259,21 @@ export function getDefaultRouteModel(providerId: ProviderId, currentOpenAiModel:
214
259
  if (discovered.status === "ready" && discovered.models.length > 0) {
215
260
  return discovered.models[0].modelId;
216
261
  }
217
- return ANTHROPIC_FALLBACK_MODELS[0]?.modelId ?? "sonnet";
262
+ return ANTHROPIC_FALLBACK_MODELS[0]?.modelId ?? "sonnet";
218
263
  }
219
264
  if (providerId === "google") {
220
265
  return GEMINI_FALLBACK_MODELS[0]?.modelId ?? GEMINI_DEFAULT_MODEL_ID;
221
266
  }
222
- if (providerId === "local") {
223
- const discovery = discoverProviderModels("local");
224
- return discovery.models[0]?.modelId ?? "Local default";
225
- }
267
+ if (providerId === "local") {
268
+ const discovery = discoverProviderModels("local");
269
+ return discovery.models[0]?.modelId ?? "Local default";
270
+ }
271
+ if (providerId === "mistral") {
272
+ const discovery = discoverProviderModels("mistral");
273
+ return discovery.models[0]?.modelId ?? "Vibe default";
274
+ }
226
275
  if (providerId === "antigravity") {
227
- return ANTIGRAVITY_DEFAULT_MODEL_ID;
228
- }
229
- return currentOpenAiModel;
230
- }
276
+ return discoverProviderModels("antigravity").models[0]?.modelId ?? ANTIGRAVITY_DEFAULT_MODEL_ID;
277
+ }
278
+ return currentOpenAiModel;
279
+ }
@@ -10,6 +10,7 @@ export type ProviderBackendKind =
10
10
  | "codex-cli-auth"
11
11
  | "gemini-cli-auth"
12
12
  | "claude-code-auth"
13
+ | "mistral-vibe-cli-auth"
13
14
  | "antigravity-cli-auth"
14
15
  | "openai-api-key"
15
16
  | "gemini-api-key"
@@ -0,0 +1,119 @@
1
+ import type { ChildProcess } from "child_process";
2
+ import {
3
+ runCommand,
4
+ runShellCommand,
5
+ type CommandResult,
6
+ type CommandSpec,
7
+ type CommandStreamHandlers,
8
+ } from "../process/CommandRunner.js";
9
+ import { CODEXA_NPM_PACKAGE } from "./updateCheck.js";
10
+
11
+ export type GlobalPackageManager = "npm" | "pnpm" | "yarn" | "bun";
12
+
13
+ const UPDATE_TIMEOUT_MS = 300_000;
14
+
15
+ const PACKAGE_SPEC = `${CODEXA_NPM_PACKAGE}@latest`;
16
+
17
+ // Yarn Classic only — Yarn Berry (v2+) removed `yarn global`, but Berry installs
18
+ // don't produce the global launcher paths we detect, so Classic is the only case.
19
+ const UPDATE_ARGV: Record<GlobalPackageManager, readonly string[]> = {
20
+ npm: ["npm", "install", "-g", PACKAGE_SPEC],
21
+ pnpm: ["pnpm", "add", "-g", PACKAGE_SPEC],
22
+ yarn: ["yarn", "global", "add", PACKAGE_SPEC],
23
+ bun: ["bun", "add", "-g", PACKAGE_SPEC],
24
+ };
25
+
26
+ /**
27
+ * Infers which package manager owns the global Codexa install from the
28
+ * launcher script location (CODEXA_LAUNCHER_SCRIPT, set by bin/codexa.js).
29
+ */
30
+ export function detectGlobalPackageManager(
31
+ env: NodeJS.ProcessEnv = process.env,
32
+ launcherPathOverride?: string,
33
+ ): GlobalPackageManager {
34
+ const launcherPath = launcherPathOverride ?? env.CODEXA_LAUNCHER_SCRIPT ?? process.argv[1] ?? "";
35
+ const normalized = launcherPath.toLowerCase().replace(/\\/g, "/");
36
+ if (!normalized) return "npm";
37
+
38
+ if (normalized.includes("pnpm")) return "pnpm";
39
+ if (normalized.includes("/.bun/") || normalized.includes("/bun/install/global/")) return "bun";
40
+ if (normalized.includes("/.yarn/") || normalized.includes("/yarn/") || normalized.includes(".config/yarn")) {
41
+ return "yarn";
42
+ }
43
+ return "npm";
44
+ }
45
+
46
+ export function getUpdateCommand(pm: GlobalPackageManager): {
47
+ displayCommand: string;
48
+ argv: readonly string[];
49
+ } {
50
+ const argv = UPDATE_ARGV[pm];
51
+ return { displayCommand: argv.join(" "), argv };
52
+ }
53
+
54
+ export interface RunUpdateCommandDeps {
55
+ platform?: NodeJS.Platform;
56
+ cwd?: string;
57
+ runCommandFn?: (
58
+ spec: CommandSpec,
59
+ handlers?: CommandStreamHandlers,
60
+ ) => { child: ChildProcess; result: Promise<CommandResult>; cancel: () => void };
61
+ runShellCommandFn?: (
62
+ command: string,
63
+ options: Pick<CommandSpec, "cwd" | "env" | "timeoutMs">,
64
+ handlers?: CommandStreamHandlers,
65
+ ) => { child: ChildProcess; result: Promise<CommandResult>; cancel: () => void };
66
+ }
67
+
68
+ /**
69
+ * Runs the global update command for the detected package manager.
70
+ * POSIX spawns the argv directly; Windows routes the constant, whitelisted
71
+ * command string through cmd.exe so .cmd shims (npm.cmd, pnpm.cmd) resolve —
72
+ * spawning them without a shell throws EINVAL on Node >= 18.20.
73
+ */
74
+ export function runUpdateCommand(
75
+ pm: GlobalPackageManager,
76
+ handlers: CommandStreamHandlers = {},
77
+ deps: RunUpdateCommandDeps = {},
78
+ ): { result: Promise<CommandResult>; cancel: () => void } {
79
+ const platform = deps.platform ?? process.platform;
80
+ const cwd = deps.cwd ?? process.cwd();
81
+ const { displayCommand, argv } = getUpdateCommand(pm);
82
+
83
+ if (platform === "win32") {
84
+ const runShell = deps.runShellCommandFn ?? runShellCommand;
85
+ const { result, cancel } = runShell(displayCommand, { cwd, timeoutMs: UPDATE_TIMEOUT_MS }, handlers);
86
+ return { result, cancel };
87
+ }
88
+
89
+ const run = deps.runCommandFn ?? runCommand;
90
+ const { result, cancel } = run(
91
+ { executable: argv[0]!, args: [...argv.slice(1)], cwd, timeoutMs: UPDATE_TIMEOUT_MS },
92
+ handlers,
93
+ );
94
+ return { result, cancel };
95
+ }
96
+
97
+ const PERMISSION_STDERR_RE = /EACCES|EPERM|permission denied/i;
98
+
99
+ /** npm reports EACCES via stderr with a nonzero exit, not as a spawn error. */
100
+ export function isPermissionError(result: CommandResult): boolean {
101
+ if (result.errorCode === "EACCES" || result.errorCode === "EPERM") return true;
102
+ return result.exitCode !== 0 && PERMISSION_STDERR_RE.test(result.stderr);
103
+ }
104
+
105
+ export function formatPermissionGuidance(pm: GlobalPackageManager): string {
106
+ const { displayCommand } = getUpdateCommand(pm);
107
+ const lines = [
108
+ `${pm} could not write to its global package directory, so the update was not installed.`,
109
+ ];
110
+ if (pm === "npm") {
111
+ lines.push(
112
+ "Check where global packages are installed with `npm config get prefix` and make sure that directory is writable by your user, or switch to a user-writable prefix (for example `npm config set prefix ~/.npm-global`, then add its bin directory to PATH).",
113
+ );
114
+ } else {
115
+ lines.push(`Make sure the ${pm} global package directory is writable by your user.`);
116
+ }
117
+ lines.push(`You can also run the command manually in a terminal with the right permissions: ${displayCommand}`);
118
+ return lines.join("\n");
119
+ }
@@ -154,7 +154,10 @@ export async function checkForUpdates(
154
154
  }
155
155
  }
156
156
 
157
- export function formatUpdateInstructions(result: UpdateCheckResult | null): string {
157
+ export function formatUpdateInstructions(
158
+ result: UpdateCheckResult | null,
159
+ updateCommand: string = CODEXA_UPDATE_COMMAND,
160
+ ): string {
158
161
  const current = result?.currentVersion ?? APP_VERSION;
159
162
  const latest = result?.latestVersion ?? "unknown";
160
163
 
@@ -166,21 +169,24 @@ export function formatUpdateInstructions(result: UpdateCheckResult | null): stri
166
169
  ].join("\n");
167
170
  }
168
171
 
169
- let statusLine: string;
170
- if (result?.status === "update-available" && result.latestVersion) {
171
- statusLine = `Update available: Codexa ${formatVersionLabel(result.latestVersion)}`;
172
- } else if (result?.status === "up-to-date") {
173
- statusLine = "Already up to date.";
174
- } else {
175
- statusLine = "Status unknown — could not reach npm registry.";
172
+ if (result?.status === "up-to-date") {
173
+ return [
174
+ "Codexa is up to date.",
175
+ `Current installed version: ${current}`,
176
+ `npm latest version: ${latest}`,
177
+ ].join("\n");
176
178
  }
177
179
 
180
+ const statusLine = result?.status === "update-available" && result.latestVersion
181
+ ? `Update available: Codexa ${formatVersionLabel(result.latestVersion)}`
182
+ : "Status unknown — could not reach npm registry.";
183
+
178
184
  return [
179
185
  `Current installed version: ${current}`,
180
186
  `npm latest version: ${latest}`,
181
187
  `Status: ${statusLine}`,
182
188
  "",
183
- `Run: ${CODEXA_UPDATE_COMMAND}`,
189
+ `Run: ${updateCommand}`,
184
190
  ].join("\n");
185
191
  }
186
192
 
package/src/index.tsx CHANGED
@@ -18,11 +18,24 @@ import {
18
18
  import { resolveInkRenderInstance, type InkRenderInstance } from "./core/terminal/inkRenderReset.js";
19
19
  import { resetFrameLockForResize, wrapStdoutWithFrameLock } from "./core/terminal/frameLock.js";
20
20
 
21
- type RenderHandle = Pick<Instance, "clear" | "cleanup" | "waitUntilExit">;
22
- const KITTY_KEYBOARD_OPTIONS: RenderOptions["kittyKeyboard"] = {
23
- mode: "auto",
24
- flags: ["disambiguateEscapeCodes"],
25
- };
21
+ type RenderHandle = Pick<Instance, "clear" | "cleanup" | "waitUntilExit">;
22
+
23
+ export function resolveKittyKeyboardOptions(
24
+ env: Record<string, string | undefined>,
25
+ ): RenderOptions["kittyKeyboard"] | undefined {
26
+ // Ink's auto mode probes with CSI ? u. Kitty's response can race Ink's
27
+ // regular input listeners and leak into both the first frame and composer.
28
+ // A direct Kitty session is already known to support the protocol, so enable
29
+ // it without probing. Avoid protocol negotiation in all other terminals.
30
+ if ((env.TERM ?? "").toLowerCase() !== "xterm-kitty") {
31
+ return undefined;
32
+ }
33
+
34
+ return {
35
+ mode: "enabled",
36
+ flags: ["disambiguateEscapeCodes"],
37
+ };
38
+ }
26
39
 
27
40
  export interface AppStdout {
28
41
  isTTY: boolean;
@@ -222,10 +235,11 @@ export function startApp({
222
235
  process.on("uncaughtException", handleFatal);
223
236
  process.on("unhandledRejection", handleFatal);
224
237
 
225
- renderHandle = renderApp(<App launchArgs={launchArgs} />, {
226
- kittyKeyboard: KITTY_KEYBOARD_OPTIONS,
227
- stdout: wrappedStdout as any,
228
- });
238
+ const kittyKeyboard = resolveKittyKeyboardOptions(env);
239
+ renderHandle = renderApp(<App launchArgs={launchArgs} />, {
240
+ ...(kittyKeyboard ? { kittyKeyboard } : {}),
241
+ stdout: wrappedStdout as any,
242
+ });
229
243
 
230
244
  // Resolve the real Ink class instance to get access to lastOutput,
231
245
  // onRender, calculateLayout, etc. Gracefully degrades to null in tests.
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
- import { useTheme } from "./theme.js";
3
+ import { useTheme } from "../theme.js";
4
4
 
5
5
  // ─── WaveBar ─────────────────────────────────────────────────────────────────
6
6
  // Audio-visualizer style wave. Renders as a single <Text> node to minimise
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { Text } from "ink";
3
- import type { UIState, ExternalCliStatus } from "../session/types.js";
4
- import { useTheme } from "./theme.js";
3
+ import type { UIState, ExternalCliStatus } from "../../session/types.js";
4
+ import { useTheme } from "../theme.js";
5
5
 
6
6
  const SPINNER_FRAMES = ["?", "?", "?", "?", "?", "?", "?", "?", "?", "?"];
7
7
  const STREAMING_FRAMES = ["?", "?", "?", "?"];
@@ -36,7 +36,7 @@ export function ActivityIndicator({ uiState, externalCliStatus = "idle" }: Activ
36
36
  let bold = false;
37
37
 
38
38
  if (isError) {
39
- glyph = "�";
39
+ glyph = "�";
40
40
  color = theme.error;
41
41
  bold = true;
42
42
  } else if (isAction) {
@@ -1,8 +1,8 @@
1
1
  import React, { useEffect, useState } from "react";
2
2
  import { Text } from "ink";
3
- import * as renderDebug from "../core/perf/renderDebug.js";
4
- import { useTheme } from "./theme.js";
5
- import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
3
+ import * as renderDebug from "../../core/perf/renderDebug.js";
4
+ import { useTheme } from "../theme.js";
5
+ import { sanitizeTerminalOutput } from "../../core/terminal/terminalSanitize.js";
6
6
  import { BUSY_STATUS_FRAME_MS, BUSY_STATUS_FRAMES, getBusyStatusFrame } from "./busyStatusAnimation.js";
7
7
 
8
8
  interface AnimatedStatusTextProps {
@@ -1,12 +1,12 @@
1
1
  import React, { memo, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box, Text, useStdin } from "ink";
3
- import { useTheme } from "./theme.js";
4
- import type { RuntimeSummary } from "../config/runtimeConfig.js";
5
- import type { CodexAuthState } from "../core/auth/codexAuth.js";
6
- import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../config/settings.js";
7
- import * as renderDebug from "../core/perf/renderDebug.js";
8
- import type { Screen, TimelineEvent, UIState } from "../session/types.js";
9
- import { isBusy } from "../session/types.js";
3
+ import { useTheme } from "../theme.js";
4
+ import type { RuntimeSummary } from "../../config/runtimeConfig.js";
5
+ import type { CodexAuthState } from "../../core/auth/codexAuth.js";
6
+ import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../../config/settings.js";
7
+ import * as renderDebug from "../../core/perf/renderDebug.js";
8
+ import type { Screen, TimelineEvent, UIState } from "../../session/types.js";
9
+ import { isBusy } from "../../session/types.js";
10
10
  import {
11
11
  ActivePanelLayoutContext,
12
12
  type ActivePanelLayout,
@@ -24,7 +24,7 @@ import {
24
24
  MIN_TERMINAL_COLS,
25
25
  MIN_TERMINAL_ROWS,
26
26
  type TerminalViewport,
27
- } from "./layout.js";
27
+ } from "../layout.js";
28
28
  import {
29
29
  buildActiveRenderItems,
30
30
  buildStaticRenderItems,
@@ -33,9 +33,9 @@ import {
33
33
  Timeline,
34
34
  TimelineRowView,
35
35
  type TimelineItem,
36
- } from "./Timeline.js";
37
- import { buildNativeTranscriptParts, type NativeTranscriptRowItem, type TimelineRow } from "./timelineMeasure.js";
38
- import type { TerminalSelectionProfile } from "../core/terminal/terminalSelection.js";
36
+ } from "../timeline/Timeline.js";
37
+ import { buildNativeTranscriptParts, type NativeTranscriptRowItem, type TimelineRow } from "../timeline/timelineMeasure.js";
38
+ import type { TerminalSelectionProfile } from "../../core/terminal/terminalSelection.js";
39
39
  import { MemoizedTopHeader, measureTopHeaderRows, type UpdateAvailableInfo } from "./TopHeader.js";
40
40
 
41
41
  const COMPACT_HEADER_TO_COMPOSER_GAP_ROWS = 1;
@@ -1,9 +1,9 @@
1
1
  import React, { memo, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box, Text, useFocus, useInput, useStdin } from "ink";
3
- import { formatContextCompact } from "../core/providerRuntime/contextMetadata.js";
4
- import type { ModelSpec } from "../core/models/modelSpecs.js";
5
- import type { ExternalCliStatus, UIState } from "../session/types.js";
6
- import { FOCUS_IDS } from "./focus.js";
3
+ import { formatContextCompact } from "../../core/providerRuntime/contextMetadata.js";
4
+ import type { ModelSpec } from "../../core/models/modelSpecs.js";
5
+ import type { ExternalCliStatus, UIState } from "../../session/types.js";
6
+ import { FOCUS_IDS } from "../input/focus.js";
7
7
  import {
8
8
  createInputViewport,
9
9
  deleteInputBackward,
@@ -14,22 +14,22 @@ import {
14
14
  moveCursorRight,
15
15
  normalizeInputText,
16
16
  normalizeCursorOffset,
17
- } from "./inputBuffer.js";
18
- import { getModeDisplaySpec } from "./modeDisplay.js";
17
+ } from "../input/inputBuffer.js";
18
+ import { getModeDisplaySpec } from "../render/modeDisplay.js";
19
19
  import { ActivityIndicator } from "./ActivityIndicator.js";
20
20
  import { measureRunFooterRows, MemoizedRunFooter } from "./RunFooter.js";
21
- import { useTheme } from "./theme.js";
22
- import { clampVisualText, getShellWidth, type Layout } from "./layout.js";
23
- import { getTextWidth, splitTextAtColumn } from "./textLayout.js";
24
- import { useThrottledValue } from "./useThrottledValue.js";
25
- import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
26
- import { getStdinDebugState, traceInputDebug } from "../core/debug/inputDebug.js";
27
- import * as renderDebug from "../core/perf/renderDebug.js";
21
+ import { useTheme } from "../theme.js";
22
+ import { clampVisualText, getShellWidth, type Layout } from "../layout.js";
23
+ import { getTextWidth, splitTextAtColumn } from "../render/textLayout.js";
24
+ import { useThrottledValue } from "../useThrottledValue.js";
25
+ import { sanitizeTerminalOutput } from "../../core/terminal/terminalSanitize.js";
26
+ import { getStdinDebugState, traceInputDebug } from "../../core/debug/inputDebug.js";
27
+ import * as renderDebug from "../../core/perf/renderDebug.js";
28
28
  import { AnimatedStatusText } from "./AnimatedStatusText.js";
29
29
  import { isAnimatedBusyState } from "./busyStatusAnimation.js";
30
30
  import { Spinner } from "./Spinner.js";
31
- import type { TerminalSelectionProfile } from "../core/terminal/terminalSelection.js";
32
- import { getSlashCommandSuggestions, type CommandSuggestion } from "./slashCommands.js";
31
+ import type { TerminalSelectionProfile } from "../../core/terminal/terminalSelection.js";
32
+ import { getSlashCommandSuggestions, type CommandSuggestion } from "../input/slashCommands.js";
33
33
 
34
34
  // ─── Types & constants ────────────────────────────────────────────────────────
35
35
 
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
2
  import type { ReactNode } from "react";
3
3
  import { Box, Text } from "ink";
4
- import { useTheme } from "./theme.js";
5
- import { getVisualWidth } from "./layout.js";
4
+ import { useTheme } from "../theme.js";
5
+ import { getVisualWidth } from "../layout.js";
6
6
 
7
7
  interface DashCardProps {
8
8
  cols: number;
@@ -1,8 +1,8 @@
1
1
  import React, { memo } from "react";
2
2
  import { Box, Text } from "ink";
3
- import type { UIState } from "../session/types.js";
4
- import * as renderDebug from "../core/perf/renderDebug.js";
5
- import { useTheme } from "./theme.js";
3
+ import type { UIState } from "../../session/types.js";
4
+ import * as renderDebug from "../../core/perf/renderDebug.js";
5
+ import { useTheme } from "../theme.js";
6
6
  import { AnimatedStatusText } from "./AnimatedStatusText.js";
7
7
  import { isAnimatedBusyState } from "./busyStatusAnimation.js";
8
8
 
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
- import { useTheme } from "./theme.js";
4
- import type { Layout } from "./layout.js";
5
- import * as renderDebug from "../core/perf/renderDebug.js";
3
+ import { useTheme } from "../theme.js";
4
+ import type { Layout } from "../layout.js";
5
+ import * as renderDebug from "../../core/perf/renderDebug.js";
6
6
 
7
7
  export type RuntimeAvailability = "available" | "checking" | "reconnecting" | "unavailable" | "unknown";
8
8
 
@@ -1,6 +1,6 @@
1
1
  import React, { startTransition, useEffect, useState } from "react";
2
2
  import { Text } from "ink";
3
- import { useTheme } from "./theme.js";
3
+ import { useTheme } from "../theme.js";
4
4
 
5
5
  const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
6
6