@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,15 +1,15 @@
1
1
  import React, { memo } from "react";
2
2
  import { Box, Text } from "ink";
3
- import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../config/settings.js";
4
- import { formatCodexaBrandLabel } from "../core/version/channel.js";
5
- import { formatVersionLabel } from "../core/version/updateCheck.js";
6
- import type { RuntimeSummary } from "../config/runtimeConfig.js";
7
- import type { CodexAuthState } from "../core/auth/codexAuth.js";
8
- import { getAuthStateLabel } from "../core/auth/codexAuth.js";
9
- import * as renderDebug from "../core/perf/renderDebug.js";
10
- import { useTheme } from "./theme.js";
11
- import { clampVisualText, isDecorativeLayoutMode, type Layout, useAppLayoutBudget } from "./layout.js";
12
- import { getTextWidth } from "./textLayout.js";
3
+ import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../../config/settings.js";
4
+ import { formatCodexaBrandLabel } from "../../core/version/channel.js";
5
+ import { CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../../core/version/updateCheck.js";
6
+ import type { RuntimeSummary } from "../../config/runtimeConfig.js";
7
+ import type { CodexAuthState } from "../../core/auth/codexAuth.js";
8
+ import { getAuthStateLabel } from "../../core/auth/codexAuth.js";
9
+ import * as renderDebug from "../../core/perf/renderDebug.js";
10
+ import { useTheme } from "../theme.js";
11
+ import { clampVisualText, isDecorativeLayoutMode, type Layout, useAppLayoutBudget } from "../layout.js";
12
+ import { getTextWidth } from "../render/textLayout.js";
13
13
  import { UPDATE_CARD_ROWS, UpdateAvailableCard } from "./UpdateAvailableCard.js";
14
14
  import {
15
15
  LOGO_LARGE,
@@ -18,7 +18,7 @@ import {
18
18
  LOGO_LARGE_MIN_COLS,
19
19
  LOGO_LARGE_MIN_ROWS,
20
20
  getLogoWidth,
21
- } from "./logoVariants.js";
21
+ } from "../render/logoVariants.js";
22
22
 
23
23
  // Re-exported for backward compatibility with existing tests.
24
24
  export const HEADER_WORDMARK_LINES = LOGO_LARGE;
@@ -58,6 +58,7 @@ type HeaderMetadataLine = { key: string; text: string; color: string; bold: bool
58
58
  export interface UpdateAvailableInfo {
59
59
  latestVersion: string;
60
60
  currentVersion: string;
61
+ updateCommand?: string;
61
62
  }
62
63
 
63
64
  interface TopHeaderProps {
@@ -343,6 +344,7 @@ export function TopHeader({
343
344
  <UpdateAvailableCard
344
345
  latestVersion={updateAvailable.latestVersion}
345
346
  currentVersion={updateAvailable.currentVersion}
347
+ updateCommand={updateAvailable.updateCommand}
346
348
  width={metadataWidth}
347
349
  />
348
350
  <Box height={UPDATE_CARD_GAP_ROWS} />
@@ -359,7 +361,7 @@ export function TopHeader({
359
361
  )}
360
362
  {metadataColumn}
361
363
  {updateAvailable && (
362
- <Text color={theme.warning} wrap="truncate">{`Update available: Codexa ${formatVersionLabel(updateAvailable.latestVersion)} — Run: npm install -g @golba98/codexa@latest`}</Text>
364
+ <Text color={theme.warning} wrap="truncate">{`Update available: Codexa ${formatVersionLabel(updateAvailable.latestVersion)} — Run: ${updateAvailable.updateCommand ?? CODEXA_UPDATE_COMMAND}`}</Text>
363
365
  )}
364
366
  </Box>
365
367
  )}
@@ -434,4 +436,4 @@ export const MemoizedTopHeader = memo(TopHeader, (prev, next) => {
434
436
  export const MIN_LOGO_TERMINAL_WIDTH = LOGO_COMPACT_MIN_COLS;
435
437
 
436
438
  // Re-export for consumers that reference these constants directly.
437
- export { LOGO_LARGE_MIN_COLS, LOGO_MEDIUM_MIN_COLS, LOGO_COMPACT_MIN_COLS } from "./logoVariants.js";
439
+ export { LOGO_LARGE_MIN_COLS, LOGO_MEDIUM_MIN_COLS, LOGO_COMPACT_MIN_COLS } from "../render/logoVariants.js";
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
- import { CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../core/version/updateCheck.js";
4
- import { clampVisualText } from "./layout.js";
5
- import { useTheme } from "./theme.js";
3
+ import { CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../../core/version/updateCheck.js";
4
+ import { clampVisualText } from "../layout.js";
5
+ import { useTheme } from "../theme.js";
6
6
 
7
7
  export const UPDATE_CARD_CONTENT_ROWS = 4; // title + available + using + command
8
8
  export const UPDATE_CARD_ROWS = UPDATE_CARD_CONTENT_ROWS + 2; // +2 for top/bottom border rows
@@ -10,13 +10,14 @@ export const UPDATE_CARD_ROWS = UPDATE_CARD_CONTENT_ROWS + 2; // +2 for top/bott
10
10
  export interface UpdateAvailableCardProps {
11
11
  latestVersion: string;
12
12
  currentVersion: string;
13
+ updateCommand?: string;
13
14
  /** Total box width including borders. Long lines are truncated to fit. */
14
15
  width?: number;
15
16
  }
16
17
 
17
- export function UpdateAvailableCard({ latestVersion, currentVersion, width }: UpdateAvailableCardProps) {
18
+ export function UpdateAvailableCard({ latestVersion, currentVersion, updateCommand = CODEXA_UPDATE_COMMAND, width }: UpdateAvailableCardProps) {
18
19
  const theme = useTheme();
19
- const command = `Run: ${CODEXA_UPDATE_COMMAND}`;
20
+ const command = `Run: ${updateCommand}`;
20
21
  // Inner content width = boxWidth - 2 (left/right border cols)
21
22
  const innerWidth = width !== undefined ? Math.max(8, width - 2) : undefined;
22
23
 
@@ -1,4 +1,4 @@
1
- import { formatTerminalAnswerInline } from "./terminalAnswerFormat.js";
1
+ import { formatTerminalAnswerInline } from "../render/terminalAnswerFormat.js";
2
2
 
3
3
  function stripOuterQuotes(s: string): string {
4
4
  if ((s.startsWith("'") && s.endsWith("'")) || (s.startsWith('"') && s.endsWith('"'))) {
@@ -1,4 +1,4 @@
1
- import type { Screen } from "../session/types.js";
1
+ import type { Screen } from "../../session/types.js";
2
2
 
3
3
  export const FOCUS_IDS = {
4
4
  composer: "composer",
@@ -1,6 +1,6 @@
1
- import type { WrappedTextRow } from "./textLayout.js";
2
- import { getTextWidth, normalizeLineBreaks, wrapTextRows } from "./textLayout.js";
3
- import { sanitizeTerminalInput } from "../core/terminal/terminalSanitize.js";
1
+ import type { WrappedTextRow } from "../render/textLayout.js";
2
+ import { getTextWidth, normalizeLineBreaks, wrapTextRows } from "../render/textLayout.js";
3
+ import { sanitizeTerminalInput } from "../../core/terminal/terminalSanitize.js";
4
4
 
5
5
  export type WrappedInputRow = WrappedTextRow;
6
6
 
@@ -24,6 +24,7 @@ export const SLASH_COMMANDS = [
24
24
  { cmd: "/auth", desc: "Manage authentication" },
25
25
  { cmd: "/workspace", desc: "Show the locked workspace" },
26
26
  { cmd: "/copy", desc: "Copy the full conversation transcript to clipboard" },
27
+ { cmd: "/update", desc: "Check for updates and install the latest Codexa" },
27
28
  { cmd: "/mouse", desc: "Toggle mouse capture for wheel scrolling (off by default)" },
28
29
  { cmd: "/exit", desc: "Quit the application" },
29
30
  ] as const satisfies readonly SlashCommandSuggestion[];
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { Box, Text, useFocus, useInput } from "ink";
3
3
  import path from "node:path";
4
- import { useTheme } from "./theme.js";
4
+ import { useTheme } from "../theme.js";
5
5
 
6
6
  export interface PendingImportFile {
7
7
  srcPath: string;
@@ -1,10 +1,10 @@
1
1
  import React from "react";
2
2
  import { Box, Text, useFocus, useInput } from "ink";
3
- import { AUTH_PREFERENCES, formatAuthPreferenceLabel } from "../config/settings.js";
4
- import type { CodexAuthProbeResult } from "../core/auth/codexAuth.js";
5
- import { getAuthStateLabel } from "../core/auth/codexAuth.js";
6
- import type { BackendProvider } from "../core/providers/types.js";
7
- import { useTheme } from "./theme.js";
3
+ import { AUTH_PREFERENCES, formatAuthPreferenceLabel } from "../../config/settings.js";
4
+ import type { CodexAuthProbeResult } from "../../core/auth/codexAuth.js";
5
+ import { getAuthStateLabel } from "../../core/auth/codexAuth.js";
6
+ import type { BackendProvider } from "../../core/providers/types.js";
7
+ import { useTheme } from "../theme.js";
8
8
 
9
9
  interface AuthPanelProps {
10
10
  focusId: string;
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
- import { AVAILABLE_BACKENDS } from "../config/settings.js";
3
- import { FOCUS_IDS } from "./focus.js";
2
+ import { AVAILABLE_BACKENDS } from "../../config/settings.js";
3
+ import { FOCUS_IDS } from "../input/focus.js";
4
4
  import { SelectionPanel } from "./SelectionPanel.js";
5
5
 
6
6
  interface BackendPickerProps {
@@ -1,5 +1,5 @@
1
- import { AVAILABLE_MODES } from "../config/settings.js";
2
- import { FOCUS_IDS } from "./focus.js";
1
+ import { AVAILABLE_MODES } from "../../config/settings.js";
2
+ import { FOCUS_IDS } from "../input/focus.js";
3
3
  import { SelectionPanel } from "./SelectionPanel.js";
4
4
 
5
5
  interface ModePickerProps {
@@ -1,5 +1,5 @@
1
- import { type CodexModelCapability } from "../core/models/codexModelCapabilities.js";
2
- import { FOCUS_IDS } from "./focus.js";
1
+ import { type CodexModelCapability } from "../../core/models/codexModelCapabilities.js";
2
+ import { FOCUS_IDS } from "../input/focus.js";
3
3
  import { SelectionPanel } from "./SelectionPanel.js";
4
4
 
5
5
  interface ModelPickerProps {
@@ -4,10 +4,10 @@ import {
4
4
  type CodexModelCapability,
5
5
  type ReasoningEffortCapability,
6
6
  normalizeReasoningForModelCapabilities,
7
- } from "../core/models/codexModelCapabilities.js";
8
- import { formatReasoningLabel } from "../config/settings.js";
9
- import { traceInputDebug } from "../core/debug/inputDebug.js";
10
- import { FOCUS_IDS } from "./focus.js";
7
+ } from "../../core/models/codexModelCapabilities.js";
8
+ import { formatReasoningLabel } from "../../config/settings.js";
9
+ import { traceInputDebug } from "../../core/debug/inputDebug.js";
10
+ import { FOCUS_IDS } from "../input/focus.js";
11
11
  import {
12
12
  clampVisualText,
13
13
  getShellWidth,
@@ -19,10 +19,10 @@ import {
19
19
  type ActivePanelLayout,
20
20
  type PanelLayout,
21
21
  usePanelLayout,
22
- } from "./layout.js";
23
- import { calculateListWindow } from "./layoutListWindow.js";
24
- import { useTheme } from "./theme.js";
25
- import type { GeminiModelSelection } from "../core/providerRuntime/types.js";
22
+ } from "../layout.js";
23
+ import { calculateListWindow } from "../timeline/layoutListWindow.js";
24
+ import { useTheme } from "../theme.js";
25
+ import type { GeminiModelSelection } from "../../core/providerRuntime/types.js";
26
26
 
27
27
  // ─── Types & helpers ─────────────────────────────────────────────────────────
28
28
 
@@ -498,7 +498,9 @@ export function ModelPickerScreen({
498
498
  >
499
499
  {models.length === 0 ? (
500
500
  <Text color={theme.textMuted}>
501
- {isLoading ? "Discovering models from the Codex runtime..." : (emptyMessage ?? "No models available.")}
501
+ {isLoading
502
+ ? `Discovering models from ${activeProviderLabel === "OpenAI" ? "the Codex runtime" : activeProviderLabel}...`
503
+ : (emptyMessage ?? "No models available.")}
502
504
  </Text>
503
505
  ) : (
504
506
  visibleModels.map((model, index) => {
@@ -4,11 +4,11 @@ import {
4
4
  type CodexModelCapability,
5
5
  type ReasoningEffortCapability,
6
6
  normalizeReasoningForModelCapabilities,
7
- } from "../core/models/codexModelCapabilities.js";
8
- import { formatReasoningLabel } from "../config/settings.js";
9
- import { traceInputDebug } from "../core/debug/inputDebug.js";
10
- import { FOCUS_IDS } from "./focus.js";
11
- import { useTheme } from "./theme.js";
7
+ } from "../../core/models/codexModelCapabilities.js";
8
+ import { formatReasoningLabel } from "../../config/settings.js";
9
+ import { traceInputDebug } from "../../core/debug/inputDebug.js";
10
+ import { FOCUS_IDS } from "../input/focus.js";
11
+ import { useTheme } from "../theme.js";
12
12
 
13
13
  // ─── Types & helpers ─────────────────────────────────────────────────────────
14
14
 
@@ -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
  interface PanelProps {
6
6
  cols: number;
@@ -1,10 +1,10 @@
1
- import type { ResolvedRuntimeConfig, RuntimeConfig } from "../config/runtimeConfig.js";
1
+ import type { ResolvedRuntimeConfig, RuntimeConfig } from "../../config/runtimeConfig.js";
2
2
  import {
3
3
  formatApprovalPolicyLabel,
4
4
  formatNetworkAccessLabel,
5
5
  formatSandboxModeLabel,
6
- } from "../config/runtimeConfig.js";
7
- import { FOCUS_IDS } from "./focus.js";
6
+ } from "../../config/runtimeConfig.js";
7
+ import { FOCUS_IDS } from "../input/focus.js";
8
8
  import { SelectionPanel } from "./SelectionPanel.js";
9
9
 
10
10
  export type PermissionsPanelAction =
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useRef, useState } from "react";
2
2
  import { Box, Text, useFocus, useInput, useStdin } from "ink";
3
- import { FOCUS_IDS } from "./focus.js";
4
- import { useTheme } from "./theme.js";
3
+ import { FOCUS_IDS } from "../input/focus.js";
4
+ import { useTheme } from "../theme.js";
5
5
 
6
6
  export type PlanActionValue = "implement" | "revise" | "cancel";
7
7
 
@@ -1,10 +1,10 @@
1
1
  import { useMemo } from "react";
2
2
  import { Box, Text } from "ink";
3
- import { normalizePlanReviewMarkdown } from "../core/workspace/planStorage.js";
4
- import { getUsableShellWidth } from "./layout.js";
5
- import { parseMarkdown } from "./Markdown.js";
6
- import { getTextWidth } from "./textLayout.js";
7
- import { useTheme } from "./theme.js";
3
+ import { normalizePlanReviewMarkdown } from "../../core/workspace/planStorage.js";
4
+ import { getUsableShellWidth } from "../layout.js";
5
+ import { parseMarkdown } from "../render/Markdown.js";
6
+ import { getTextWidth } from "../render/textLayout.js";
7
+ import { useTheme } from "../theme.js";
8
8
 
9
9
  // ─── Types ────────────────────────────────────────────────────────────────────
10
10
 
@@ -1,8 +1,8 @@
1
1
  import React, { useMemo, useState } from "react";
2
2
  import { Box, Text, useFocus, useInput } from "ink";
3
- import type { ProviderConfig, ProviderId, ProviderPickerAction } from "../core/providerLauncher/types.js";
4
- import { traceInputDebug } from "../core/debug/inputDebug.js";
5
- import { FOCUS_IDS } from "./focus.js";
3
+ import type { ProviderConfig, ProviderId, ProviderPickerAction } from "../../core/providerLauncher/types.js";
4
+ import { traceInputDebug } from "../../core/debug/inputDebug.js";
5
+ import { FOCUS_IDS } from "../input/focus.js";
6
6
  import {
7
7
  clampVisualText,
8
8
  getShellWidth,
@@ -14,9 +14,9 @@ import {
14
14
  type ActivePanelLayout,
15
15
  type PanelLayout,
16
16
  usePanelLayout,
17
- } from "./layout.js";
18
- import { calculateListWindow } from "./layoutListWindow.js";
19
- import { useTheme } from "./theme.js";
17
+ } from "../layout.js";
18
+ import { calculateListWindow } from "../timeline/layoutListWindow.js";
19
+ import { useTheme } from "../theme.js";
20
20
 
21
21
  // ─── Types & helpers ─────────────────────────────────────────────────────────
22
22
 
@@ -247,7 +247,9 @@ export function ProviderPicker({
247
247
  return;
248
248
  }
249
249
  if (key.return && selectedProvider) {
250
- onAction(selectedProvider.id, actions[actionIndex]?.value ?? "cancel");
250
+ const action = actions[actionIndex];
251
+ if (action?.disabledReason) return;
252
+ onAction(selectedProvider.id, action?.value ?? "cancel");
251
253
  }
252
254
  }, { isActive: isFocused });
253
255
 
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
- import { type ReasoningEffortCapability } from "../core/models/codexModelCapabilities.js";
3
- import { formatReasoningLabel } from "../config/settings.js";
4
- import { FOCUS_IDS } from "./focus.js";
2
+ import { type ReasoningEffortCapability } from "../../core/models/codexModelCapabilities.js";
3
+ import { formatReasoningLabel } from "../../config/settings.js";
4
+ import { FOCUS_IDS } from "../input/focus.js";
5
5
  import { SelectionPanel } from "./SelectionPanel.js";
6
6
 
7
7
  interface ReasoningPickerProps {
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { Box, Text, useFocus, useInput } from "ink";
3
3
  import SelectInput from "ink-select-input";
4
- import { useTheme } from "./theme.js";
4
+ import { useTheme } from "../theme.js";
5
5
 
6
6
  interface SelectionPanelProps {
7
7
  focusId: string;
@@ -1,8 +1,8 @@
1
1
  import React, { useMemo, useState } from "react";
2
2
  import { Box, Text, useFocus, useInput } from "ink";
3
- import type { SettingDefinition } from "../config/settings.js";
4
- import type { FocusTargetId } from "./focus.js";
5
- import { useTheme } from "./theme.js";
3
+ import type { SettingDefinition } from "../../config/settings.js";
4
+ import type { FocusTargetId } from "../input/focus.js";
5
+ import { useTheme } from "../theme.js";
6
6
 
7
7
  export interface SettingsPanelProps<TKey extends string> {
8
8
  focusId: FocusTargetId;
@@ -1,8 +1,8 @@
1
1
  import React, { useMemo, useState } from "react";
2
2
  import { Box, Text, useFocus, useInput } from "ink";
3
- import type { FocusTargetId } from "./focus.js";
4
- import { useTheme } from "./theme.js";
5
- import { stripMouseEscapes } from "./inputBuffer.js";
3
+ import type { FocusTargetId } from "../input/focus.js";
4
+ import { useTheme } from "../theme.js";
5
+ import { stripMouseEscapes } from "../input/inputBuffer.js";
6
6
 
7
7
  interface TextEntryPanelProps {
8
8
  focusId: FocusTargetId;
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
- import { AVAILABLE_THEMES, formatThemeLabel } from "../config/settings.js";
3
- import { FOCUS_IDS } from "./focus.js";
2
+ import { AVAILABLE_THEMES, formatThemeLabel } from "../../config/settings.js";
3
+ import { FOCUS_IDS } from "../input/focus.js";
4
4
  import { SelectionPanel } from "./SelectionPanel.js";
5
5
 
6
6
  interface ThemePickerProps {
@@ -1,31 +1,45 @@
1
1
  import React, { useEffect, useRef, useState } from "react";
2
2
  import { Box, Text, useFocus, useInput } from "ink";
3
- import { spawn } from "child_process";
4
- import { useTheme } from "./theme.js";
5
- import { CODEXA_NPM_PACKAGE, CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../core/version/updateCheck.js";
3
+ import { useTheme } from "../theme.js";
4
+ import { CODEXA_NPM_PACKAGE, formatVersionLabel } from "../../core/version/updateCheck.js";
5
+ import {
6
+ formatPermissionGuidance,
7
+ getUpdateCommand,
8
+ isPermissionError,
9
+ runUpdateCommand,
10
+ type GlobalPackageManager,
11
+ } from "../../core/version/packageManager.js";
12
+ import type { CommandResult, CommandStreamHandlers } from "../../core/process/CommandRunner.js";
6
13
 
7
14
  type Phase = "menu" | "running" | "done" | "error";
8
15
 
16
+ export type RunUpdateFn = (
17
+ pm: GlobalPackageManager,
18
+ handlers?: CommandStreamHandlers,
19
+ ) => { result: Promise<CommandResult>; cancel: () => void };
20
+
9
21
  const MENU_ITEMS = [
10
22
  { label: "Update now" },
11
- { label: "Skip" },
12
- { label: "Skip until next version" },
23
+ { label: "Later" },
13
24
  ] as const;
14
25
 
15
26
  interface UpdatePromptPanelProps {
16
27
  focusId: string;
17
28
  currentVersion: string;
18
29
  latestVersion: string;
30
+ packageManager: GlobalPackageManager;
31
+ /** Test seam — defaults to the real cross-platform runner. */
32
+ runUpdate?: RunUpdateFn;
19
33
  onSkip: () => void;
20
- onSkipUntilNextVersion: (version: string) => void;
21
34
  }
22
35
 
23
36
  export function UpdatePromptPanel({
24
37
  focusId,
25
38
  currentVersion,
26
39
  latestVersion,
40
+ packageManager,
41
+ runUpdate,
27
42
  onSkip,
28
- onSkipUntilNextVersion,
29
43
  }: UpdatePromptPanelProps) {
30
44
  const theme = useTheme();
31
45
  const { isFocused } = useFocus({ id: focusId, autoFocus: true });
@@ -35,7 +49,7 @@ export function UpdatePromptPanel({
35
49
  const [outputLines, setOutputLines] = useState<string[]>([]);
36
50
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
37
51
 
38
- const spawnStartedRef = useRef(false);
52
+ const runStartedRef = useRef(false);
39
53
 
40
54
  useInput((input, key) => {
41
55
  if (key.escape) {
@@ -54,10 +68,8 @@ export function UpdatePromptPanel({
54
68
  if (key.return) {
55
69
  if (selectedIndex === 0) {
56
70
  setPhase("running");
57
- } else if (selectedIndex === 1) {
58
- onSkip();
59
71
  } else {
60
- onSkipUntilNextVersion(latestVersion);
72
+ onSkip();
61
73
  }
62
74
  return;
63
75
  }
@@ -70,42 +82,42 @@ export function UpdatePromptPanel({
70
82
 
71
83
  useEffect(() => {
72
84
  if (phase !== "running") return;
73
- if (spawnStartedRef.current) return;
74
- spawnStartedRef.current = true;
85
+ if (runStartedRef.current) return;
86
+ runStartedRef.current = true;
75
87
 
76
- const child = spawn("npm", ["install", "-g", `${CODEXA_NPM_PACKAGE}@latest`], {
77
- shell: false,
78
- stdio: ["ignore", "pipe", "pipe"],
79
- });
80
-
81
- const appendLines = (buf: Buffer) => {
82
- const lines = buf.toString("utf8").split(/\r?\n/).filter(Boolean);
88
+ const appendLines = (text: string) => {
89
+ const lines = text.split(/\r?\n/).filter(Boolean);
83
90
  if (lines.length > 0) {
84
91
  setOutputLines((prev) => [...prev, ...lines]);
85
92
  }
86
93
  };
87
94
 
88
- child.stdout?.on("data", appendLines);
89
- child.stderr?.on("data", appendLines);
90
-
91
- child.once("error", (err: Error) => {
92
- setErrorMessage(err.message);
93
- setPhase("error");
95
+ const runner = runUpdate ?? runUpdateCommand;
96
+ const { result, cancel } = runner(packageManager, {
97
+ onStdout: appendLines,
98
+ onStderr: appendLines,
94
99
  });
95
100
 
96
- child.once("close", (code: number | null) => {
97
- if (code === 0) {
101
+ let disposed = false;
102
+ void result.then((res) => {
103
+ if (disposed) return;
104
+ if (res.status === "completed" && res.exitCode === 0) {
98
105
  setPhase("done");
106
+ return;
107
+ }
108
+ if (isPermissionError(res)) {
109
+ setErrorMessage(formatPermissionGuidance(packageManager));
99
110
  } else {
100
- setErrorMessage(`npm exited with code ${code ?? "unknown"}.`);
101
- setPhase("error");
111
+ setErrorMessage(res.userMessage);
102
112
  }
113
+ setPhase("error");
103
114
  });
104
115
 
105
116
  return () => {
106
- try { child.kill(); } catch { /* ignore */ }
117
+ disposed = true;
118
+ cancel();
107
119
  };
108
- }, [phase]);
120
+ }, [phase, packageManager, runUpdate]);
109
121
 
110
122
  const footerText = phase === "menu"
111
123
  ? "Esc to close · Enter to confirm"
@@ -122,16 +134,16 @@ export function UpdatePromptPanel({
122
134
  flexDirection="column"
123
135
  >
124
136
  <Box>
125
- <Text color={theme.accent} bold>{`Update available: Codexa ${formatVersionLabel(latestVersion)}`}</Text>
137
+ <Text color={theme.accent} bold>{`Update available: Codexa ${latestVersion}`}</Text>
126
138
  </Box>
127
139
  <Box marginTop={1}>
128
- <Text color={theme.text}>{`${formatVersionLabel(currentVersion)} -> ${formatVersionLabel(latestVersion)}`}</Text>
140
+ <Text color={theme.text}>{`Current version: ${currentVersion}`}</Text>
129
141
  </Box>
130
142
  <Box>
131
143
  <Text color={theme.textMuted}>{`Package: ${CODEXA_NPM_PACKAGE}`}</Text>
132
144
  </Box>
133
145
  <Box>
134
- <Text color={theme.textMuted}>{`Run: ${CODEXA_UPDATE_COMMAND}`}</Text>
146
+ <Text color={theme.textMuted}>{`Run: ${getUpdateCommand(packageManager).displayCommand}`}</Text>
135
147
  </Box>
136
148
  </Box>
137
149
 
@@ -146,19 +158,17 @@ export function UpdatePromptPanel({
146
158
  >
147
159
  {phase === "menu" && (
148
160
  <>
149
- {MENU_ITEMS.map((item, index) => (
150
- <Box key={item.label}>
151
- <Text color={index === selectedIndex ? theme.accent : theme.textMuted}>
152
- {index === selectedIndex ? "› " : " "}
153
- </Text>
161
+ <Box>
162
+ {MENU_ITEMS.map((item, index) => (
154
163
  <Text
164
+ key={item.label}
155
165
  color={index === selectedIndex ? theme.text : theme.textMuted}
156
166
  bold={index === selectedIndex}
157
167
  >
158
- {`${index + 1}. ${item.label}`}
168
+ {`[ ${item.label} ]${index === 0 ? " " : ""}`}
159
169
  </Text>
160
- </Box>
161
- ))}
170
+ ))}
171
+ </Box>
162
172
  </>
163
173
  )}
164
174
 
@@ -173,7 +183,7 @@ export function UpdatePromptPanel({
173
183
 
174
184
  {phase === "done" && (
175
185
  <>
176
- <Text color={theme.success}>{"Codexa was updated successfully."}</Text>
186
+ <Text color={theme.success}>{`Codexa ${formatVersionLabel(latestVersion)} installed successfully.`}</Text>
177
187
  <Text color={theme.textMuted}>{"Restart Codexa to use the new version."}</Text>
178
188
  </>
179
189
  )}
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
- import { useTheme } from "./theme.js";
4
- import { Panel } from "./Panel.js";
3
+ import { useTheme } from "../theme.js";
4
+ import { Panel } from "../panels/Panel.js";
5
5
  import { maybeRenderDiff, type DiffRenderLineType } from "./diffRenderer.js";
6
6
  import { formatLocalPathForTerminal, formatTerminalAnswerInline } from "./terminalAnswerFormat.js";
7
7
 
@@ -1,4 +1,4 @@
1
- import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
1
+ import { sanitizeTerminalOutput } from "../../core/terminal/terminalSanitize.js";
2
2
 
3
3
  export type DiffRenderLineType = "file" | "hunk" | "add" | "remove" | "context" | "meta";
4
4
 
@@ -1,5 +1,5 @@
1
- import { formatModeLabel } from "../config/settings.js";
2
- import type { Theme } from "./theme.js";
1
+ import { formatModeLabel } from "../../config/settings.js";
2
+ import type { Theme } from "../theme.js";
3
3
 
4
4
  export interface ModeDisplaySpec {
5
5
  label: string;
@@ -1,5 +1,5 @@
1
- import { isNoiseLine } from "../core/providers/codexTranscript.js";
2
- import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
1
+ import { isNoiseLine } from "../../core/providers/codexTranscript.js";
2
+ import { sanitizeTerminalOutput } from "../../core/terminal/terminalSanitize.js";
3
3
  import { parseMarkdown, type Segment } from "./Markdown.js";
4
4
 
5
5
  /**