@golba98/codexa 1.0.3 → 1.0.5

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 (125) hide show
  1. package/README.md +17 -18
  2. package/bin/codexa.js +62 -144
  3. package/package.json +4 -3
  4. package/src/app.tsx +642 -303
  5. package/src/commands/handler.ts +7 -18
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/runtimeConfig.ts +1 -1
  9. package/src/config/settings.ts +1 -1
  10. package/src/config/trustStore.ts +1 -1
  11. package/src/core/README.md +52 -0
  12. package/src/core/agent/loop.ts +282 -0
  13. package/src/core/agent/protocol.ts +211 -0
  14. package/src/core/agent/tools.ts +414 -0
  15. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  16. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  17. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  18. package/src/core/debug/modelStateDebug.ts +34 -0
  19. package/src/core/executables/antigravityExecutable.ts +48 -0
  20. package/src/core/executables/executableResolver.ts +5 -1
  21. package/src/core/models/codexModelCapabilities.ts +57 -4
  22. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  23. package/src/core/models/providerModelCache.ts +106 -0
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/CommandRunner.ts +12 -1
  26. package/src/core/providerLauncher/registry.ts +64 -18
  27. package/src/core/providerLauncher/types.ts +12 -9
  28. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  29. package/src/core/providerRuntime/anthropic.ts +10 -6
  30. package/src/core/providerRuntime/antigravity.ts +461 -0
  31. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -446
  32. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  33. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  34. package/src/core/providerRuntime/local.ts +129 -51
  35. package/src/core/providerRuntime/mistralVibe.ts +663 -0
  36. package/src/core/providerRuntime/models.ts +40 -15
  37. package/src/core/providerRuntime/reasoning.ts +9 -6
  38. package/src/core/providerRuntime/registry.ts +76 -4
  39. package/src/core/providerRuntime/types.ts +20 -14
  40. package/src/core/providers/codexSubprocess.ts +2 -2
  41. package/src/core/providers/types.ts +1 -1
  42. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  43. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  44. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  45. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  46. package/src/core/terminal/frameLock.ts +109 -0
  47. package/src/core/terminal/inkRenderReset.ts +123 -0
  48. package/src/core/terminal/terminalControl.ts +22 -0
  49. package/src/core/terminal/terminalTitle.ts +16 -102
  50. package/src/core/{channel.ts → version/channel.ts} +1 -1
  51. package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
  52. package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
  53. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  54. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  55. package/src/headless/execRunner.ts +2 -2
  56. package/src/index.tsx +66 -98
  57. package/src/session/appSession.ts +10 -7
  58. package/src/session/chatLifecycle.ts +1 -1
  59. package/src/session/liveRenderScheduler.ts +1 -1
  60. package/src/session/types.ts +1 -1
  61. package/src/test/runtimeTestUtils.ts +90 -0
  62. package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
  63. package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
  64. package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
  65. package/src/ui/chrome/AppShell.tsx +672 -0
  66. package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +159 -158
  67. package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
  68. package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
  69. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  70. package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
  71. package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +54 -38
  72. package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +5 -5
  73. package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
  74. package/src/ui/{focus.ts → input/focus.ts} +1 -1
  75. package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
  76. package/src/ui/layout.ts +298 -24
  77. package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +3 -3
  78. package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
  79. package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
  80. package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
  81. package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
  82. package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +224 -42
  83. package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
  84. package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
  85. package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
  86. package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
  87. package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
  88. package/src/ui/panels/ProviderPicker.tsx +737 -0
  89. package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
  90. package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +5 -1
  91. package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +4 -4
  92. package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +4 -4
  93. package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
  94. package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +17 -23
  95. package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
  96. package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
  97. package/src/ui/{logoVariants.ts → render/logoVariants.ts} +4 -8
  98. package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
  99. package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
  100. package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +23 -10
  101. package/src/ui/{textLayout.ts → render/textLayout.ts} +15 -4
  102. package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
  103. package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
  104. package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
  105. package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
  106. package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +1620 -1471
  107. package/src/ui/timeline/TranscriptShell.tsx +322 -0
  108. package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
  109. package/src/ui/timeline/layoutListWindow.ts +145 -0
  110. package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
  111. package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
  112. package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +206 -134
  113. package/src/core/codex.ts +0 -124
  114. package/src/ui/AppShell.tsx +0 -706
  115. package/src/ui/ProviderPicker.tsx +0 -321
  116. package/src/ui/StaticTranscriptItem.tsx +0 -56
  117. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  118. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  119. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  120. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  121. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  122. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
  123. /package/src/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
  124. /package/src/ui/{slashCommands.ts → input/slashCommands.ts} +0 -0
  125. /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
@@ -0,0 +1,109 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { isTerminalResizing } from "./terminalControl.js";
4
+
5
+ export interface FrameLockOptions {
6
+ stdout: any;
7
+ env: Record<string, string | undefined>;
8
+ }
9
+
10
+ const resizeFrameCacheResetters = new WeakMap<object, () => void>();
11
+
12
+ export function resetFrameLockForResize(stdout: object): void {
13
+ resizeFrameCacheResetters.get(stdout)?.();
14
+ }
15
+
16
+ /**
17
+ * Wraps the stdout stream to enforce frame-level deduplication, a flush lock,
18
+ * and width-safe row padding via ANSI clear-to-EOL (\x1b[K) injection.
19
+ */
20
+ export function wrapStdoutWithFrameLock({
21
+ stdout,
22
+ env,
23
+ }: FrameLockOptions) {
24
+ let lastFrame = "";
25
+ let isFlushing = false;
26
+ let debugLogStream: fs.WriteStream | null = null;
27
+
28
+ if (env.CODEXA_RENDER_DEBUG === "1") {
29
+ try {
30
+ const logPath = env.CODEXA_RENDER_DEBUG_FILE?.trim()
31
+ || path.join(process.cwd(), ".codexa", "debug", "render-status.log");
32
+ const logDir = path.dirname(logPath);
33
+ if (!fs.existsSync(logDir)) {
34
+ fs.mkdirSync(logDir, { recursive: true });
35
+ }
36
+ debugLogStream = fs.createWriteStream(logPath, { flags: "a" });
37
+ } catch (e) {
38
+ // Gracefully fall back if logging fails (e.g. read-only filesystem)
39
+ void e;
40
+ }
41
+ }
42
+
43
+ const logDebug = (msg: string) => {
44
+ if (debugLogStream) {
45
+ debugLogStream.write(`${new Date().toISOString()} ${msg}\n`);
46
+ }
47
+ };
48
+
49
+ const originalWrite = stdout.write.bind(stdout);
50
+
51
+ // A terminal resize can make an otherwise identical Ink frame meaningful:
52
+ // rows reflow at a new width and height-driven spacer/layout changes must be
53
+ // written even when their source text did not change. src/index.tsx owns the
54
+ // single resize listener and invokes this reset through the exported helper.
55
+ resizeFrameCacheResetters.set(stdout, () => {
56
+ lastFrame = "";
57
+ logDebug("Frame cache reset: terminal resize");
58
+ });
59
+
60
+ stdout.write = (chunk: string | Uint8Array) => {
61
+ if (typeof chunk !== "string") {
62
+ return originalWrite(chunk);
63
+ }
64
+
65
+ // Task 7: Frame Lock
66
+ // Ensure only one render flush can write to stdout at a time.
67
+ if (isFlushing) {
68
+ logDebug("Frame dropped: lock active (concurrent flush)");
69
+ return true;
70
+ }
71
+
72
+ // Task 8: Full-frame Deduplication
73
+ // Skip if identical to last frame.
74
+ if (chunk === lastFrame) {
75
+ logDebug("Frame dropped: identical to last frame");
76
+ return true;
77
+ }
78
+
79
+ // Task 4: Width-Safe Injection
80
+ // Ensure every row clears to the end of the line. This fixes artifacts
81
+ // left behind when a shorter row replaces a longer row or when terminal
82
+ // wrapping occurs during resize.
83
+ //
84
+ // We inject \x1b[K before every newline and at the end of the frame.
85
+ // padding via \x1b[K injection.
86
+ // Only apply if the chunk is non-empty and doesn't look like a control-only
87
+ // sequence (e.g. terminal title OSC).
88
+ let processed = chunk;
89
+ const isControlSequence = chunk.includes("\x1b]");
90
+ if (chunk.length > 0 && !isControlSequence) {
91
+ processed = chunk.replace(/\n/g, "\x1b[K\n");
92
+ if (!processed.endsWith("\x1b[K") && !processed.endsWith("\x1b[K\n")) {
93
+ processed += "\x1b[K";
94
+ }
95
+ }
96
+
97
+ isFlushing = true;
98
+ try {
99
+ lastFrame = chunk;
100
+ const resizing = isTerminalResizing();
101
+ logDebug(`Frame written: length=${processed.length} original=${chunk.length} resizing=${resizing}`);
102
+ return originalWrite(processed);
103
+ } finally {
104
+ isFlushing = false;
105
+ }
106
+ };
107
+
108
+ return stdout;
109
+ }
@@ -0,0 +1,123 @@
1
+ import * as renderDebug from "../perf/renderDebug.js";
2
+
3
+ /**
4
+ * Typed subset of the internal Ink class instance we reach into.
5
+ *
6
+ * `unsubscribeResize` is used at startup to disable Ink's competing resize
7
+ * handler (see src/index.tsx). The remaining fields are Ink's per-frame render
8
+ * caches; we reset them on the explicit /clear boundary so the next frame is
9
+ * written authoritatively from a clean baseline — exactly like the first frame
10
+ * after a cold startup — rather than diffed against pre-clear output.
11
+ *
12
+ * All fields are optional so the type also describes the minimal capability
13
+ * needed by callers that only disable resize, and so resolution degrades
14
+ * gracefully across Ink versions / test mocks.
15
+ */
16
+ export interface InkRenderInstance {
17
+ unsubscribeResize?: () => void;
18
+ renderInteractiveFrame?: (output: string, outputHeight: number, staticOutput: string) => void;
19
+ lastOutput?: string;
20
+ lastOutputToRender?: string;
21
+ lastOutputHeight?: number;
22
+ lastTerminalWidth?: number;
23
+ fullStaticOutput?: string;
24
+ log?: { reset?: () => void };
25
+ throttledOnRender?: { cancel?: () => void };
26
+ throttledLog?: { cancel?: () => void };
27
+ }
28
+
29
+ export interface AppStdoutLike {
30
+ columns?: number;
31
+ rows?: number;
32
+ }
33
+
34
+ /**
35
+ * Resolve the real Ink class instance via Ink's internal WeakMap<stdout, Ink>.
36
+ * Returns null if resolution fails (e.g. different Ink version, test mocks).
37
+ *
38
+ * Bun doesn't resolve bare subpath imports like "ink/build/instances.js" so we
39
+ * resolve ink's main entry first, then derive the sibling path.
40
+ */
41
+ export function resolveInkRenderInstance(stdout: object): InkRenderInstance | null {
42
+ try {
43
+ const { createRequire } = require("node:module");
44
+ const req = createRequire(import.meta.url);
45
+ const inkMain = req.resolve("ink");
46
+ const instancesPath = inkMain.replace(/index\.js$/, "instances.js");
47
+ const instances = req(instancesPath);
48
+ const weakMap: WeakMap<object, InkRenderInstance> =
49
+ instances.default ?? instances;
50
+ return weakMap.get(stdout) ?? null;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ export interface ResetInkOutputOptions {
57
+ instance: InkRenderInstance | null;
58
+ /** Current terminal column count, used to reseat lastTerminalWidth. */
59
+ columns?: number;
60
+ }
61
+
62
+ /**
63
+ * Reset Ink's render-state caches to the fresh-startup baseline.
64
+ *
65
+ * Call this immediately AFTER the terminal has been physically cleared (e.g. by
66
+ * terminalControl.clearTranscript on /clear) and BEFORE the post-clear React
67
+ * render commits. It mirrors what a brand-new Ink instance looks like at cold
68
+ * startup: empty frame caches and zero previous height, so the next frame is
69
+ * authoritative and the subsequent resize math (Ink's renderInteractiveFrame /
70
+ * shouldClearTerminalForFrame) is evaluated against truthful state.
71
+ *
72
+ * This does NOT emit any clear/erase escape sequences itself — the terminal is
73
+ * already physically cleared, and Ink's log.reset() zeroes log-update's
74
+ * accounting without writing. Returns false (graceful no-op) if no live Ink
75
+ * instance is available.
76
+ */
77
+ export function resetInkOutputForFreshFrame({ instance, columns }: ResetInkOutputOptions): boolean {
78
+ if (!instance) {
79
+ renderDebug.traceEvent("terminal", "clearRenderReset", { instanceResolved: false });
80
+ return false;
81
+ }
82
+
83
+ const before = {
84
+ lastOutputLength: instance.lastOutput?.length ?? 0,
85
+ lastOutputToRenderLength: instance.lastOutputToRender?.length ?? 0,
86
+ lastOutputHeight: instance.lastOutputHeight ?? 0,
87
+ fullStaticOutputLength: instance.fullStaticOutput?.length ?? 0,
88
+ lastTerminalWidth: instance.lastTerminalWidth,
89
+ };
90
+
91
+ // Drop any pending throttled render/log so a stale pre-clear frame can't be
92
+ // flushed after we reset the caches.
93
+ instance.throttledOnRender?.cancel?.();
94
+ instance.throttledLog?.cancel?.();
95
+
96
+ // Zero log-update's previousOutput/previousLineCount WITHOUT emitting escape
97
+ // sequences (the terminal was already physically cleared).
98
+ instance.log?.reset?.();
99
+
100
+ // Reset Ink's frame caches to the constructor/startup state.
101
+ instance.lastOutput = "";
102
+ instance.lastOutputToRender = "";
103
+ instance.lastOutputHeight = 0;
104
+ instance.fullStaticOutput = "";
105
+ if (typeof columns === "number" && Number.isFinite(columns)) {
106
+ instance.lastTerminalWidth = columns;
107
+ }
108
+
109
+ renderDebug.traceEvent("terminal", "clearRenderReset", {
110
+ instanceResolved: true,
111
+ columns,
112
+ before,
113
+ after: {
114
+ lastOutputLength: instance.lastOutput.length,
115
+ lastOutputToRenderLength: instance.lastOutputToRender.length,
116
+ lastOutputHeight: instance.lastOutputHeight,
117
+ fullStaticOutputLength: instance.fullStaticOutput.length,
118
+ lastTerminalWidth: instance.lastTerminalWidth,
119
+ },
120
+ });
121
+
122
+ return true;
123
+ }
@@ -14,11 +14,24 @@ export const TERMINAL_SEQUENCES = {
14
14
  mouseEnable: "\x1b[?1000h\x1b[?1006h",
15
15
  mouseDisable: "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l",
16
16
  title: `\x1b]0;${TERMINAL_TITLE}\x07\x1b]2;${TERMINAL_TITLE}\x07`,
17
+ alternateScreenEnable: "\x1b[?1049h",
18
+ alternateScreenDisable: "\x1b[?1049l",
17
19
  } as const;
18
20
 
19
21
  export type TerminalWrite = (chunk: string) => boolean | void;
20
22
  export type TerminalChannel = "stdout" | "stderr";
21
23
 
24
+ // Tracks whether the terminal is currently in a live resize debounce.
25
+ let terminalResizing = false;
26
+
27
+ export function setTerminalResizing(resizing: boolean): void {
28
+ terminalResizing = resizing;
29
+ }
30
+
31
+ export function isTerminalResizing(): boolean {
32
+ return terminalResizing;
33
+ }
34
+
22
35
  // Tracks current UI state kind for diagnostic assertions.
23
36
  let currentUIStateKind: string = "IDLE";
24
37
 
@@ -107,12 +120,14 @@ export interface TerminalModeController {
107
120
  clearViewport(source: string): void;
108
121
  setMouseReporting(enabled: boolean, source: string): void;
109
122
  setBracketedPaste(enabled: boolean, source: string): void;
123
+ setAlternateScreen(enabled: boolean, source: string): void;
110
124
  resetModes(): void;
111
125
  }
112
126
 
113
127
  export function createTerminalModeController(write: TerminalWrite): TerminalModeController {
114
128
  let mouseReporting: boolean | null = null;
115
129
  let bracketedPaste: boolean | null = null;
130
+ let alternateScreen: boolean | null = null;
116
131
 
117
132
  const writeStdout = (sequence: string, source: string) =>
118
133
  writeTerminalControl(write, "stdout", source, sequence);
@@ -135,13 +150,20 @@ export function createTerminalModeController(write: TerminalWrite): TerminalMode
135
150
  bracketedPaste = enabled;
136
151
  writeStdout(enabled ? TERMINAL_SEQUENCES.bracketedPasteEnable : TERMINAL_SEQUENCES.bracketedPasteDisable, source);
137
152
  },
153
+ setAlternateScreen(enabled, source) {
154
+ if (alternateScreen === enabled) return;
155
+ alternateScreen = enabled;
156
+ writeStdout(enabled ? TERMINAL_SEQUENCES.alternateScreenEnable : TERMINAL_SEQUENCES.alternateScreenDisable, source);
157
+ },
138
158
  resetModes() {
139
159
  renderDebug.traceEvent("terminal", "resetModes", {
140
160
  mouseReporting,
141
161
  bracketedPaste,
162
+ alternateScreen,
142
163
  });
143
164
  mouseReporting = null;
144
165
  bracketedPaste = null;
166
+ alternateScreen = null;
145
167
  },
146
168
  };
147
169
  }
@@ -1,6 +1,7 @@
1
- import { APP_NAME, type TerminalTitleMode, formatTerminalTitlePath } from "../../config/settings.js";
2
- import { appendFileSync, mkdirSync } from "fs";
3
- import { dirname, join } from "path";
1
+ import { APP_NAME, type TerminalTitleMode, formatTerminalTitlePath } from "../../config/settings.js";
2
+ import { appendFileSync, mkdirSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import * as renderDebug from "../perf/renderDebug.js";
4
5
 
5
6
  export const DEFAULT_TERMINAL_TITLE = APP_NAME;
6
7
 
@@ -49,16 +50,17 @@ function findIncompleteOscTitleStart(text: string): number {
49
50
  return -1;
50
51
  }
51
52
 
52
- const TERMINAL_TITLE_DEBUG_LOG_PATH = process.env["CODEXA_TERMINAL_TITLE_DEBUG_FILE"]?.trim()
53
- || join(process.cwd(), ".codexa-debug", "terminal-title-debug.log");
53
+ const TERMINAL_TITLE_DEBUG_LOG_PATH = process.env["CODEXA_TERMINAL_TITLE_DEBUG_FILE"]?.trim()
54
+ || process.env["CODEXA_RENDER_DEBUG_FILE"]?.trim()
55
+ || join(process.cwd(), ".codexa", "debug", "render-status.log");
54
56
 
55
57
  let terminalTitleLifecycleState = "unknown";
56
58
 
57
- function debugLog(msg: string): void {
58
- if (DEBUG_TERMINAL_TITLE) {
59
- process.stderr.write(`[codexa:terminal-title] ${msg}\n`);
60
- }
61
- }
59
+ function debugLog(msg: string): void {
60
+ if (DEBUG_TERMINAL_TITLE) {
61
+ writeTerminalTitleDebugRecord({ event: "debug", message: msg });
62
+ }
63
+ }
62
64
 
63
65
  function writeTerminalTitleDebugRecord(fields: Record<string, unknown>): void {
64
66
  if (!DEBUG_TERMINAL_TITLE) return;
@@ -144,8 +146,9 @@ export function writeCodexaTerminalTitle(title: string, options?: TerminalTitleO
144
146
 
145
147
  lastWrittenTerminalTitle = cleanTitle;
146
148
 
147
- const sequence = buildTerminalTitleSequence(cleanTitle);
148
- writeTerminalTitleDebugRecord({
149
+ const sequence = buildTerminalTitleSequence(cleanTitle);
150
+ renderDebug.traceTerminalWrite("stdout", `terminalTitle:${options?.reason ?? "unknown"}`, sequence);
151
+ writeTerminalTitleDebugRecord({
149
152
  event: "codexaTitleWrite",
150
153
  title: cleanTitle,
151
154
  reason: options?.reason ?? "unknown",
@@ -353,76 +356,7 @@ export function writeGuardedTerminalOutput(
353
356
  return write(safeText) !== false;
354
357
  }
355
358
 
356
- // ─── Startup guard ────────────────────────────────────────────────────────────
357
-
358
- /**
359
- * Schedules a sequence of title assertions to outlast Windows Terminal's
360
- * shell integration, which overwrites the process title at unpredictable
361
- * points during startup (shell prompt init, profile scripts, etc.).
362
- * The retries at 50/250/500/1000ms are intentional: no single delay is
363
- * reliable across all terminal configurations, so we assert multiple times.
364
- */
365
- export function beginColdStartSequence(title: string, options?: { write?: (chunk: string) => void }) {
366
- setIntendedTerminalTitle(title, { force: true, write: options?.write, reason: "cold-start-immediate" });
367
-
368
- const timers: ReturnType<typeof setTimeout>[] = [];
369
- let cancelled = false;
370
-
371
- const scheduleRetry = (delayMs: number) => {
372
- const id = setTimeout(() => {
373
- if (!cancelled) {
374
- debugLog(`cold-start retry t=${delayMs}ms fired`);
375
- reassertIntendedTerminalTitle({ write: options?.write, reason: `cold-start-retry-${delayMs}ms` });
376
- }
377
- }, delayMs);
378
- timers.push(id);
379
- };
380
-
381
- scheduleRetry(50);
382
- scheduleRetry(250);
383
- scheduleRetry(500);
384
- scheduleRetry(1000);
385
-
386
- return () => {
387
- cancelled = true;
388
- timers.forEach(clearTimeout);
389
- debugLog("cold-start sequence cancelled");
390
- };
391
- }
392
-
393
- export function startTerminalTitleStartupGuard(options?: {
394
- write?: (chunk: string) => void;
395
- intervalMs?: number;
396
- durationMs?: number;
397
- reason?: string;
398
- }): () => void {
399
- const intervalMs = options?.intervalMs ?? 150;
400
- const durationMs = options?.durationMs ?? 3500;
401
- const reason = options?.reason ?? "startup-guard";
402
- let stopped = false;
403
- const startedAt = Date.now();
404
-
405
- reassertIntendedTerminalTitle({ write: options?.write, reason: `${reason}-start` });
406
- const interval = setInterval(() => {
407
- if (stopped) return;
408
- if (Date.now() - startedAt >= durationMs) {
409
- stopped = true;
410
- clearInterval(interval);
411
- reassertIntendedTerminalTitle({ write: options?.write, reason: `${reason}-end` });
412
- return;
413
- }
414
- reassertIntendedTerminalTitle({ write: options?.write, reason });
415
- }, intervalMs);
416
- interval.unref?.();
417
-
418
- return () => {
419
- if (stopped) return;
420
- stopped = true;
421
- clearInterval(interval);
422
- };
423
- }
424
-
425
- export function sanitizeTerminalTitle(title: string): string {
359
+ export function sanitizeTerminalTitle(title: string): string {
426
360
  return title
427
361
  .replace(/[\u0000-\u001f\u007f]/g, " ")
428
362
  .replace(/\u001b/g, "")
@@ -461,23 +395,3 @@ export function reassertTerminalTitle(
461
395
  write(buildTerminalTitleSequence(title));
462
396
  }
463
397
 
464
- /**
465
- * Acquire a run-scoped title guard that immediately asserts the title,
466
- * reasserts it periodically while work is active, and emits one final
467
- * assertion when released.
468
- */
469
- export function acquireTerminalTitleGuard(
470
- intervalMs = 250,
471
- reassert: () => void = () => reassertTerminalTitle(),
472
- ): () => void {
473
- let released = false;
474
- reassert();
475
- const timer = setInterval(reassert, intervalMs);
476
- timer.unref?.();
477
- return () => {
478
- if (released) return;
479
- released = true;
480
- clearInterval(timer);
481
- reassert();
482
- };
483
- }
@@ -1,4 +1,4 @@
1
- import { APP_VERSION } from "../config/settings.js";
1
+ import { APP_VERSION } from "../../config/settings.js";
2
2
 
3
3
  export const CODEXA_CHANNEL_ENV = "CODEXA_CHANNEL";
4
4
  export const LOCAL_DEV_CHANNEL = "local-dev";
@@ -1,4 +1,4 @@
1
- import { APP_VERSION } from "../config/settings.js";
1
+ import { APP_VERSION } from "../../config/settings.js";
2
2
  import { isLocalDevChannel } from "./channel.js";
3
3
 
4
4
  export const CODEXA_NPM_PACKAGE = "@golba98/codexa";
@@ -8,7 +8,7 @@ export const CODEXA_UPDATE_COMMAND = `npm install -g ${CODEXA_NPM_PACKAGE}@lates
8
8
  export type UpdateStatus = "up-to-date" | "update-available" | "unknown" | "error";
9
9
 
10
10
  export interface NpmRegistryMetadata {
11
- "dist-tags": { latest?: string };
11
+ "dist-tags"?: { latest?: unknown };
12
12
  }
13
13
 
14
14
  export interface UpdateCheckResult {
@@ -27,6 +27,11 @@ export function normalizeVersion(v: string): string {
27
27
  return v.startsWith("v") ? v.slice(1) : v;
28
28
  }
29
29
 
30
+ export function formatVersionLabel(version: string): string {
31
+ const normalized = normalizeVersion(version.trim());
32
+ return normalized ? `v${normalized}` : version;
33
+ }
34
+
30
35
  const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
31
36
 
32
37
  /** Returns true for valid semver strings with or without a leading "v". */
@@ -111,7 +116,7 @@ export async function checkForUpdates(
111
116
  const metadata = await fetchFn(CODEXA_NPM_REGISTRY_URL);
112
117
  const rawLatest = metadata["dist-tags"]?.latest;
113
118
 
114
- if (!rawLatest) {
119
+ if (typeof rawLatest !== "string" || !rawLatest.trim()) {
115
120
  return {
116
121
  status: "error",
117
122
  currentVersion,
@@ -122,7 +127,7 @@ export async function checkForUpdates(
122
127
  };
123
128
  }
124
129
 
125
- const latestVersion = normalizeVersion(rawLatest);
130
+ const latestVersion = normalizeVersion(rawLatest.trim());
126
131
 
127
132
  if (!isValidSemver(latestVersion) || !isValidSemver(currentVersion)) {
128
133
  return {
@@ -163,7 +168,7 @@ export function formatUpdateInstructions(result: UpdateCheckResult | null): stri
163
168
 
164
169
  let statusLine: string;
165
170
  if (result?.status === "update-available" && result.latestVersion) {
166
- statusLine = `Update available: Codexa ${result.latestVersion} is available. You are using ${current}.`;
171
+ statusLine = `Update available: Codexa ${formatVersionLabel(result.latestVersion)}`;
167
172
  } else if (result?.status === "up-to-date") {
168
173
  statusLine = "Already up to date.";
169
174
  } else {
@@ -3,7 +3,7 @@ import { basename, join, resolve } from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  import { resolveWorkspacePath } from "./workspaceGuard.js";
5
5
  import { normalizeWorkspaceRoot } from "./workspaceRoot.js";
6
- import { CODEXA_CHANNEL_ENV, LOCAL_DEV_CHANNEL, isLocalDevChannel } from "./channel.js";
6
+ import { CODEXA_CHANNEL_ENV, LOCAL_DEV_CHANNEL, isLocalDevChannel } from "../version/channel.js";
7
7
 
8
8
  export type LaunchKind = "installed-bin" | "dev-run";
9
9
 
@@ -177,22 +177,15 @@ export function buildWorkspaceStatusMessage(launchContext: LaunchContext): strin
177
177
  }
178
178
 
179
179
  export function buildDevLaunchNotice(launchContext: LaunchContext): string | null {
180
- if (launchContext.launchKind !== "dev-run") {
181
- return null;
182
- }
183
-
180
+ if (launchContext.launchKind !== "dev-run") {
181
+ return null;
182
+ }
183
+
184
184
  return [
185
- "This session was started from local-dev Codexa.",
186
- "Normal flow:",
187
- " npm run install:dev-bin",
188
- " which codexa-dev",
189
- " cd <target-folder>",
190
- " codexa-dev",
191
- "",
192
- "Recovery from this session:",
193
- " /workspace relaunch <path>",
194
- ].join("\n");
195
- }
185
+ "Ready. Type a prompt, run !shell, or use /command.",
186
+ "Tip: /workspace relaunch <path>",
187
+ ].join("\n");
188
+ }
196
189
 
197
190
  export function guardWorkspaceRelaunch(busy: boolean): { allowed: boolean; message?: string } {
198
191
  if (!busy) {
@@ -2,8 +2,8 @@ import { createHash } from "crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
3
  import { homedir } from "os";
4
4
  import { join } from "path";
5
- import { isNoiseLine } from "./providers/codexTranscript.js";
6
- import { sanitizeTerminalOutput } from "./terminal/terminalSanitize.js";
5
+ import { isNoiseLine } from "../providers/codexTranscript.js";
6
+ import { sanitizeTerminalOutput } from "../terminal/terminalSanitize.js";
7
7
 
8
8
  type Platform = "win32" | "darwin" | "linux" | string;
9
9