@golba98/codexa 1.0.6 → 1.0.7

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.
package/README.md CHANGED
@@ -26,8 +26,10 @@ codexa --version
26
26
  | Local dev / running from source | [Bun](https://bun.sh) |
27
27
  | Codex/OpenAI route | Codex CLI installed and authenticated |
28
28
  | Claude/Anthropic route | Claude Code CLI installed and authenticated |
29
- | Gemini/Google route | Gemini CLI installed and authenticated |
29
+ | Mistral Vibe route | Mistral Vibe CLI (`vibe`) installed and authenticated |
30
+ | Antigravity route | Antigravity CLI (`agy`) installed and authenticated |
30
31
  | Local route | LM Studio (or any OpenAI-compatible server) |
32
+ | Gemini/Google route | *(Legacy)* Gemini CLI installed and authenticated (falls back to OpenAI route) |
31
33
 
32
34
  ## Installing Codexa
33
35
 
@@ -73,6 +75,8 @@ Codexa and provider CLIs are independent tools with separate version numbers. Up
73
75
  |------|---------|---------------|--------|
74
76
  | Codex CLI | OpenAI coding agent | `codex --version` | `codex update` or reinstall |
75
77
  | Claude Code CLI | Anthropic coding agent | `claude --version` | `npm install -g @anthropic-ai/claude-code@latest` |
78
+ | Mistral Vibe CLI | Mistral coding agent | `vibe --version` | `vibe --setup` or reinstall |
79
+ | Antigravity CLI | Antigravity coding agent | `agy --version` | Reinstall via package manager |
76
80
  | Codexa | TUI wrapper / workspace experience | `codexa --version` | `npm install -g @golba98/codexa@latest` |
77
81
 
78
82
  ## Provider CLI Setup
@@ -146,9 +150,12 @@ claude
146
150
  npm install -g @anthropic-ai/claude-code@latest
147
151
  ```
148
152
 
149
- ### Installing Gemini CLI
153
+ ### Installing Gemini CLI (Legacy / Deprecated)
150
154
 
151
- Codexa routes to the `gemini` executable. Install the Gemini CLI separately and authenticate per Google's instructions. Once installed and authenticated, Codexa can use it without additional configuration.
155
+ > [!NOTE]
156
+ > Google/Gemini is no longer supported as an active provider route inside Codexa and falls back automatically to the OpenAI/Codex route.
157
+
158
+ If you are using legacy features that route to the `gemini` executable, install the Gemini CLI separately and authenticate per Google's instructions.
152
159
 
153
160
  If the Gemini executable is not on PATH or has a non-standard name, specify it explicitly:
154
161
 
@@ -160,6 +167,30 @@ export GEMINI_EXECUTABLE="/path/to/gemini"
160
167
  gemini_command_path = "/path/to/gemini"
161
168
  ```
162
169
 
170
+ ### Installing Mistral Vibe CLI
171
+
172
+ Codexa supports routing through the Mistral Vibe CLI (`vibe`).
173
+
174
+ 1. **Install Vibe CLI** — Install the Mistral Vibe CLI tool.
175
+ 2. **Authenticate Vibe** — Run `vibe` or `vibe --setup` in a terminal and sign in when prompted.
176
+ 3. **Environment & Config (Optional)**:
177
+ - Codexa automatically detects the active model from your environment (`VIBE_ACTIVE_MODEL`) or configuration files (`.vibe/config.toml` in your project/workspace, or `~/.vibe/config.toml`).
178
+ - You can customize the active session location using the `VIBE_HOME` environment variable.
179
+
180
+ ### Installing Antigravity CLI
181
+
182
+ Codexa supports routing through the Antigravity CLI (`agy`).
183
+
184
+ 1. **Install Antigravity CLI** — Install the Antigravity CLI tool (`agy`).
185
+ 2. **Authenticate Antigravity** — Authenticate the CLI according to your provider setup.
186
+ 3. **Environment & Config (Optional)**:
187
+ - Specifying the `agy` command path:
188
+ ```toml
189
+ # in .codex/config.toml
190
+ antigravity_command_path = "/path/to/agy"
191
+ ```
192
+ - Alternatively, set the `AGY_EXECUTABLE` environment variable.
193
+
163
194
  ### Using Local Models (LM Studio)
164
195
 
165
196
  Codexa can route the Local provider through any OpenAI-compatible server.
@@ -178,7 +209,15 @@ export CODEXA_LOCAL_API_KEY="lm-studio"
178
209
  export CODEXA_LOCAL_MODEL="google/gemma-4-26b-a4b"
179
210
  ```
180
211
 
181
- Workspace provider config in `.codexa/providers.json`:
212
+ Provider configuration is stored in Codexa user data and remains isolated per workspace:
213
+
214
+ - Windows: `%LOCALAPPDATA%\\Codexa\\workspaces\\<workspace-hash>\\providers.json`
215
+ - macOS: `~/Library/Application Support/Codexa/workspaces/<workspace-hash>/providers.json`
216
+ - Linux: `$XDG_DATA_HOME/codexa/workspaces/<workspace-hash>/providers.json` (or `~/.local/share/codexa/...`)
217
+
218
+ Existing `.codexa/providers.json` files are read as a legacy fallback and are never deleted. New saves go to user data, so opening or configuring Codexa no longer adds `.codexa` files to projects.
219
+
220
+ The provider config format is:
182
221
 
183
222
  ```json
184
223
  {
@@ -305,6 +344,7 @@ Type `/` in the composer to access in-app commands. Key commands:
305
344
  | `/verbose` | Toggle verbose output |
306
345
  | `/copy` | Copy last response to clipboard |
307
346
  | `/diagnose github` | Run GitHub connectivity diagnostics |
347
+ | `/diagnose providers` | Collect diagnostics for all configured providers |
308
348
  | `!<command>` | Run a shell command inline |
309
349
 
310
350
  ## Layered Config
@@ -425,6 +465,12 @@ UI-only preferences are stored in `~/.codexa-settings.json` (separate from runti
425
465
 
426
466
  > **Gotcha:** When testing local changes, confirm you are running the local build and not the global one. Use `Get-Command codexa` (Windows) or `which -a codexa` (Linux/macOS) to check.
427
467
 
468
+ ## Maintainer Documentation
469
+
470
+ - [Architecture](docs/ARCHITECTURE.md) explains Codexa's entry points, subsystem boundaries, prompt and rendering flows, provider layers, state model, persistence, and maintenance invariants with diagrams.
471
+ - [Source Guide](docs/SOURCE_GUIDE.md) catalogs the purpose of every file under `src/` and records the maintenance rules for each source area.
472
+ - [Developer Scripts](scripts/README.md) lists every package command and explains launcher, shim, audit, build-metadata, smoke, and test behavior.
473
+
428
474
  ## Local Development
429
475
 
430
476
  **Requirement:** [Bun](https://bun.sh) installed.
@@ -486,6 +532,63 @@ This replaces the global `codexa` with a symlink to the repo. Undo with `npm unl
486
532
  npm install -g .
487
533
  ```
488
534
 
535
+ ## Maintainer release and NPM publishing
536
+
537
+ The package name is `@golba98/codexa`. Publish from the repository root, and
538
+ increment `version` in `package.json` before each release. NPM versions are
539
+ immutable, so never reuse a version that has already been published.
540
+
541
+ Check the current local and registry versions:
542
+
543
+ ```bash
544
+ npm pkg get name version
545
+ npm whoami
546
+ npm view @golba98/codexa version
547
+ npm view @golba98/codexa dist-tags --json
548
+ ```
549
+
550
+ Run the release checks:
551
+
552
+ ```bash
553
+ bun install
554
+ bun run typecheck
555
+ bun test
556
+ git diff --check
557
+ ```
558
+
559
+ Inspect exactly what will be included in the package:
560
+
561
+ ```bash
562
+ npm pack --dry-run
563
+ ```
564
+
565
+ Publish the next version publicly:
566
+
567
+ ```bash
568
+ npm publish --access public
569
+ ```
570
+
571
+ `npm publish` automatically runs `prepublishOnly`, which regenerates build
572
+ metadata, typechecks, and runs the full test suite. Verify the registry and
573
+ the installed command after publishing:
574
+
575
+ ```bash
576
+ npm view @golba98/codexa version
577
+ npm view @golba98/codexa dist-tags --json
578
+ npm install -g @golba98/codexa@latest
579
+ codexa --version
580
+ ```
581
+
582
+ If the release is tracked in Git, commit the version and generated build-info
583
+ change, then tag and push it:
584
+
585
+ ```bash
586
+ git add package.json src/config/buildInfo.ts README.md scripts/README.md
587
+ git commit -m "release: publish Codexa vX.Y.Z"
588
+ git tag vX.Y.Z
589
+ git push origin main --follow-tags
590
+ ```
591
+
489
592
  ## Testing
490
593
 
491
594
  ```bash
@@ -605,8 +708,14 @@ If you expect a notice but don't see one:
605
708
 
606
709
  See [CHANGELOG.md](CHANGELOG.md) for the full release history.
607
710
 
608
- **Current release: v1.0.6**
711
+ **Current release: v1.0.7**
609
712
 
610
- v1.0.6 checks npm on every interactive startup and shows an update prompt as soon as Codexa is idle. The prompt offers the correct update command for the package manager that installed Codexa.
713
+ v1.0.7 keeps workspaces clean: provider choices, imported attachments, and default diagnostic logs now live in Codexa user data instead of a project-local `.codexa` directory.
611
714
 
612
- > Versions before v1.0.6 can wait for a cached result before discovering a new npm release. Update directly with `npm install -g @golba98/codexa@latest` if needed.
715
+ Other recent releases introduce major additions, including:
716
+ - **Mistral Vibe CLI Routing**: Connect through the `vibe` CLI tool (`vibe --setup`).
717
+ - **Antigravity CLI Routing**: Connect through the `agy` CLI tool (`agy`).
718
+ - **Dynamic Model Discovery & Caching**: Models are discovered dynamically and cached locally at `~/.codexa-model-cache.json` for instant launch, with instant provider sync for the model picker.
719
+ - **Native Terminal Scrollback**: History follows native scrollback with composer anchored at the bottom.
720
+ - **Kitty Keyboard Protocol Fix**: Corrected probe output leak.
721
+ - **Google/Gemini Route Deprecation**: In-Codexa routing for Google/Gemini now falls back to OpenAI/Codex.
package/bin/codexa.js CHANGED
@@ -1,11 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from "child_process";
4
- import { appendFileSync, mkdirSync, readFileSync } from "fs";
4
+ import { appendFileSync, mkdirSync, readFileSync } from "fs";
5
+ import { homedir } from "os";
5
6
  import { dirname, join } from "path";
6
7
  import { fileURLToPath } from "url";
7
8
 
8
- process.title = "CODEXA";
9
+ process.title = "CODEXA";
10
+
11
+ function resolveLauncherDebugDir() {
12
+ const dataDir = process.env.CODEXA_DATA_DIR?.trim()
13
+ || (process.platform === "win32"
14
+ ? join(process.env.LOCALAPPDATA?.trim() || process.env.APPDATA?.trim() || join(homedir(), "AppData", "Local"), "Codexa")
15
+ : process.platform === "darwin"
16
+ ? join(homedir(), "Library", "Application Support", "Codexa")
17
+ : join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "codexa"));
18
+ return join(dataDir, "debug");
19
+ }
9
20
 
10
21
  const currentFile = fileURLToPath(import.meta.url);
11
22
  const packageRoot = dirname(dirname(currentFile));
@@ -21,7 +32,7 @@ function writeRenderDebugRecord(kind, fields) {
21
32
  }
22
33
 
23
34
  try {
24
- const debugDir = join(workspaceRoot, ".codexa", "debug");
35
+ const debugDir = resolveLauncherDebugDir();
25
36
  mkdirSync(debugDir, { recursive: true });
26
37
  appendFileSync(
27
38
  process.env.CODEXA_RENDER_DEBUG_FILE?.trim() || join(debugDir, "render-status.log"),
@@ -56,7 +67,7 @@ function writeTerminalTitleDebugRecord(fields) {
56
67
  }
57
68
 
58
69
  try {
59
- const debugDir = join(workspaceRoot, ".codexa", "debug");
70
+ const debugDir = resolveLauncherDebugDir();
60
71
  mkdirSync(debugDir, { recursive: true });
61
72
  appendFileSync(
62
73
  process.env.CODEXA_TERMINAL_TITLE_DEBUG_FILE?.trim()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golba98/codexa",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Terminal UI for Codexa — TypeScript + Bun + Ink",
5
5
  "type": "module",
6
6
  "bin": {
package/src/app.tsx CHANGED
@@ -154,7 +154,8 @@ import {
154
154
  type ModelContextMetadata,
155
155
  } from "./core/providerRuntime/contextMetadata.js";
156
156
  import { captureWorkspaceSnapshot, createWorkspaceActivityTracker, diffWorkspaceSnapshots } from "./core/workspace/workspaceActivity.js";
157
- import { resolveWorkspaceRoot } from "./core/workspace/workspaceRoot.js";
157
+ import { resolveWorkspaceRoot } from "./core/workspace/workspaceRoot.js";
158
+ import { resolveCodexaAttachmentDir } from "./core/workspace/appData.js";
158
159
  import {
159
160
  importExternalFile,
160
161
  isImageFile,
@@ -3830,13 +3831,12 @@ export function App({ launchArgs }: AppProps) {
3830
3831
 
3831
3832
  const handleImportConfirm = useCallback(async () => {
3832
3833
  if (!pendingImport) return;
3833
- const replacements: Array<{ rawPath: string; workspaceRelativePath: string }> = [];
3834
+ const replacements: Array<{ rawPath: string; replacementPath: string }> = [];
3834
3835
  for (const file of pendingImport.files) {
3835
3836
  try {
3836
3837
  const destPath = await importExternalFile(file.srcPath, pendingImport.attachmentsDir);
3837
3838
  if (destPath) {
3838
- const relPath = path.relative(workspaceRoot, destPath).replace(/\\/g, "/");
3839
- replacements.push({ rawPath: file.rawPath, workspaceRelativePath: relPath });
3839
+ replacements.push({ rawPath: file.rawPath, replacementPath: destPath });
3840
3840
  }
3841
3841
  } catch (err: any) {
3842
3842
  appendErrorEvent("Import failed", `Could not import ${path.basename(file.srcPath)}: ${err.message}`);
@@ -4470,9 +4470,7 @@ export function App({ launchArgs }: AppProps) {
4470
4470
 
4471
4471
  if (outsideViolations.length > 0) {
4472
4472
  if (runtimeConfig.policy.allowExternalFileImport) {
4473
- const attachmentsDir = path.isAbsolute(runtimeConfig.policy.attachmentDir)
4474
- ? runtimeConfig.policy.attachmentDir
4475
- : path.join(workspaceRoot, runtimeConfig.policy.attachmentDir);
4473
+ const attachmentsDir = resolveCodexaAttachmentDir(workspaceRoot, runtimeConfig.policy.attachmentDir);
4476
4474
  const importFiles: PendingImportFile[] = outsideViolations.map((v) => ({
4477
4475
  srcPath: v.normalizedPath,
4478
4476
  rawPath: v.rawPath,
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/gen-build-info.mjs — do not edit manually.
2
- export const BUILD_COMMIT = "8a275f0a19434d0cf59ff59cebe1aae734559bde" as const;
3
- export const APP_VERSION = "1.0.6" as const;
2
+ export const BUILD_COMMIT = "c04f84f315a4b2c4b8bea27d41ac370a27467572" as const;
3
+ export const APP_VERSION = "1.0.7" as const;
@@ -130,7 +130,7 @@ export const DEFAULT_RUNTIME_POLICY: RuntimePolicyConfig = {
130
130
  serviceTier: "flex",
131
131
  personality: "none",
132
132
  allowExternalFileImport: true,
133
- attachmentDir: ".codexa/attachments",
133
+ attachmentDir: "attachments",
134
134
  };
135
135
 
136
136
  export const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
@@ -1,5 +1,6 @@
1
1
  import { appendFileSync, mkdirSync } from "fs";
2
- import { dirname, join } from "path";
2
+ import { dirname } from "path";
3
+ import { resolveCodexaDebugLogPath } from "../workspace/appData.js";
3
4
 
4
5
  type ModelStateDebugDetails = Record<string, unknown>;
5
6
 
@@ -12,7 +13,7 @@ export function isModelStateDebugEnabled(): boolean {
12
13
  export function getModelStateDebugLogPath(): string {
13
14
  return process.env.CODEXA_RENDER_DEBUG_FILE?.trim()
14
15
  || process.env.CODEXA_DEBUG_MODEL_STATE_LOG?.trim()
15
- || join(process.cwd(), ".codexa", "debug", "render-status.log");
16
+ || resolveCodexaDebugLogPath();
16
17
  }
17
18
 
18
19
  export function traceModelStateDebug(event: string, details: ModelStateDebugDetails = {}): void {
@@ -1,6 +1,7 @@
1
1
  import { appendFileSync, mkdirSync } from "fs";
2
- import { dirname, join } from "path";
3
- import { useEffect, useRef } from "react";
2
+ import { dirname } from "path";
3
+ import { useEffect, useRef } from "react";
4
+ import { resolveCodexaDebugLogPath } from "../workspace/appData.js";
4
5
 
5
6
  type DebugEnv = Record<string, string | undefined>;
6
7
 
@@ -11,7 +12,7 @@ let renderTraceEnabled = false;
11
12
  let lifecycleEnabled = false;
12
13
  let flickerEnabled = false;
13
14
  let plainActionsEnabled = false;
14
- let logPath = join(process.cwd(), ".codexa", "debug", "render-status.log");
15
+ let logPath = resolveCodexaDebugLogPath();
15
16
  let sessionId = `${Date.now()}-${process.pid}`;
16
17
  const counters = new Map<string, number>();
17
18
 
@@ -29,7 +30,7 @@ function configureFromEnv(env: DebugEnv = process.env): void {
29
30
  lifecycleEnabled = env["CODEXA_DEBUG_LIFECYCLE"] === "1";
30
31
  flickerEnabled = env["CODEXA_DEBUG_FLICKER"] === "1";
31
32
  plainActionsEnabled = env["CODEXA_DEBUG_PLAIN_ACTIONS"] === "1";
32
- logPath = env["CODEXA_RENDER_DEBUG_FILE"]?.trim() || join(process.cwd(), ".codexa", "debug", "render-status.log");
33
+ logPath = env["CODEXA_RENDER_DEBUG_FILE"]?.trim() || resolveCodexaDebugLogPath(env);
33
34
  sessionId = `${Date.now()}-${process.pid}`;
34
35
  configured = true;
35
36
  }
@@ -39,7 +39,7 @@ export function buildProviderLaunchSpec(provider: ProviderConfig, cwd: string):
39
39
  if (!provider.enabled) {
40
40
  return {
41
41
  status: "disabled",
42
- message: `${provider.displayName} is disabled. Configure a command in .codexa/providers.json before launching it.`,
42
+ message: `${provider.displayName} is disabled. Configure a command in Codexa's provider settings before launching it.`,
43
43
  };
44
44
  }
45
45
 
@@ -1,7 +1,8 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
2
2
  import { dirname, join } from "path";
3
- import { DEFAULT_MODEL } from "../../config/settings.js";
4
- import { normalizeWorkspaceRoot } from "../workspace/workspaceRoot.js";
3
+ import { DEFAULT_MODEL } from "../../config/settings.js";
4
+ import { resolveCodexaWorkspaceDataDir } from "../workspace/appData.js";
5
+ import { normalizeWorkspaceRoot } from "../workspace/workspaceRoot.js";
5
6
  import { isKnownProviderId } from "./registry.js";
6
7
  import { getDefaultRouteModel, getProviderRuntime, isProviderRouteConfigured, isProviderRoutableInCodexa } from "../providerRuntime/registry.js";
7
8
  import { normalizeGeminiModelId } from "../providerRuntime/models.js";
@@ -17,8 +18,12 @@ const DEPRECATED_ANTIGRAVITY_PROVIDER_ID = "antigravity";
17
18
  const DEPRECATED_ANTIGRAVITY_BACKENDS = new Set(["antigravity-cli-auth", "agy"]);
18
19
  const DEPRECATED_GOOGLE_PROVIDER_ID = "google";
19
20
 
20
- export function getProviderWorkspaceConfigFile(workspaceRoot: string): string {
21
- return join(normalizeWorkspaceRoot(workspaceRoot), ".codexa", "providers.json");
21
+ export function getProviderWorkspaceConfigFile(workspaceRoot: string): string {
22
+ return join(resolveCodexaWorkspaceDataDir(normalizeWorkspaceRoot(workspaceRoot)), "providers.json");
23
+ }
24
+
25
+ export function getLegacyProviderWorkspaceConfigFile(workspaceRoot: string): string {
26
+ return join(normalizeWorkspaceRoot(workspaceRoot), ".codexa", "providers.json");
22
27
  }
23
28
 
24
29
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -345,15 +350,24 @@ export function serializeProviderWorkspaceConfig(config: ProviderWorkspaceConfig
345
350
  };
346
351
  }
347
352
 
348
- export function loadProviderWorkspaceConfig(workspaceRoot: string): ProviderWorkspaceConfig {
349
- const filePath = getProviderWorkspaceConfigFile(workspaceRoot);
350
- if (!existsSync(filePath)) return {};
351
- try {
352
- return parseProviderWorkspaceConfig(JSON.parse(readFileSync(filePath, "utf-8")));
353
- } catch {
354
- return {};
355
- }
356
- }
353
+ export function loadProviderWorkspaceConfig(workspaceRoot: string): ProviderWorkspaceConfig {
354
+ const filePath = getProviderWorkspaceConfigFile(workspaceRoot);
355
+ if (existsSync(filePath)) {
356
+ try {
357
+ return parseProviderWorkspaceConfig(JSON.parse(readFileSync(filePath, "utf-8")));
358
+ } catch {
359
+ return {};
360
+ }
361
+ }
362
+
363
+ const legacyFilePath = getLegacyProviderWorkspaceConfigFile(workspaceRoot);
364
+ if (!existsSync(legacyFilePath)) return {};
365
+ try {
366
+ return parseProviderWorkspaceConfig(JSON.parse(readFileSync(legacyFilePath, "utf-8")));
367
+ } catch {
368
+ return {};
369
+ }
370
+ }
357
371
 
358
372
  export function saveProviderWorkspaceConfig(workspaceRoot: string, config: ProviderWorkspaceConfig): void {
359
373
  const filePath = getProviderWorkspaceConfigFile(workspaceRoot);
@@ -74,16 +74,16 @@ export async function importExternalFile(
74
74
  return destPath;
75
75
  }
76
76
 
77
- export function rewritePromptWithImportedPaths(
78
- prompt: string,
79
- replacements: Array<{ rawPath: string; workspaceRelativePath: string }>,
80
- ): string {
81
- let result = prompt;
82
- for (const { rawPath, workspaceRelativePath } of replacements) {
83
- const needsQuotes = /\s/.test(workspaceRelativePath);
84
- const quotedReplacement = needsQuotes
85
- ? `"${workspaceRelativePath}"`
86
- : workspaceRelativePath;
77
+ export function rewritePromptWithImportedPaths(
78
+ prompt: string,
79
+ replacements: Array<{ rawPath: string; replacementPath: string }>,
80
+ ): string {
81
+ let result = prompt;
82
+ for (const { rawPath, replacementPath } of replacements) {
83
+ const needsQuotes = /\s/.test(replacementPath);
84
+ const quotedReplacement = needsQuotes
85
+ ? `"${replacementPath}"`
86
+ : replacementPath;
87
87
  // Replace quoted forms first so the unquoted pass doesn't double-replace
88
88
  result = result.split(`"${rawPath}"`).join(quotedReplacement);
89
89
  result = result.split(`'${rawPath}'`).join(quotedReplacement);
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { resolveCodexaDebugLogPath } from "../workspace/appData.js";
3
4
  import { isTerminalResizing } from "./terminalControl.js";
4
5
 
5
6
  export interface FrameLockOptions {
@@ -28,7 +29,7 @@ export function wrapStdoutWithFrameLock({
28
29
  if (env.CODEXA_RENDER_DEBUG === "1") {
29
30
  try {
30
31
  const logPath = env.CODEXA_RENDER_DEBUG_FILE?.trim()
31
- || path.join(process.cwd(), ".codexa", "debug", "render-status.log");
32
+ || resolveCodexaDebugLogPath(env);
32
33
  const logDir = path.dirname(logPath);
33
34
  if (!fs.existsSync(logDir)) {
34
35
  fs.mkdirSync(logDir, { recursive: true });
@@ -1,7 +1,8 @@
1
1
  import { APP_NAME, type TerminalTitleMode, formatTerminalTitlePath } from "../../config/settings.js";
2
2
  import { appendFileSync, mkdirSync } from "fs";
3
- import { dirname, join } from "path";
3
+ import { dirname } from "path";
4
4
  import * as renderDebug from "../perf/renderDebug.js";
5
+ import { resolveCodexaDebugLogPath } from "../workspace/appData.js";
5
6
 
6
7
  export const DEFAULT_TERMINAL_TITLE = APP_NAME;
7
8
 
@@ -52,7 +53,7 @@ function findIncompleteOscTitleStart(text: string): number {
52
53
 
53
54
  const TERMINAL_TITLE_DEBUG_LOG_PATH = process.env["CODEXA_TERMINAL_TITLE_DEBUG_FILE"]?.trim()
54
55
  || process.env["CODEXA_RENDER_DEBUG_FILE"]?.trim()
55
- || join(process.cwd(), ".codexa", "debug", "render-status.log");
56
+ || resolveCodexaDebugLogPath();
56
57
 
57
58
  let terminalTitleLifecycleState = "unknown";
58
59
 
@@ -0,0 +1,45 @@
1
+ import { createHash } from "node:crypto";
2
+ import { homedir } from "node:os";
3
+ import { isAbsolute, join, normalize } from "node:path";
4
+
5
+ type Platform = "win32" | "darwin" | "linux" | string;
6
+ type Environment = Record<string, string | undefined>;
7
+
8
+ export function resolveCodexaDataDir(
9
+ platformOverride?: Platform,
10
+ env: Environment = process.env,
11
+ home = homedir(),
12
+ ): string {
13
+ const configuredDir = env["CODEXA_DATA_DIR"]?.trim();
14
+ if (configuredDir) return configuredDir;
15
+
16
+ const platform = platformOverride ?? process.platform;
17
+ if (platform === "win32") {
18
+ return join(env["LOCALAPPDATA"]?.trim() || env["APPDATA"]?.trim() || join(home, "AppData", "Local"), "Codexa");
19
+ }
20
+ if (platform === "darwin") {
21
+ return join(home, "Library", "Application Support", "Codexa");
22
+ }
23
+
24
+ return join(env["XDG_DATA_HOME"]?.trim() || join(home, ".local", "share"), "codexa");
25
+ }
26
+
27
+ export function workspaceStorageKey(workspaceRoot: string): string {
28
+ return createHash("sha256").update(workspaceRoot).digest("hex").slice(0, 16);
29
+ }
30
+
31
+ export function resolveCodexaWorkspaceDataDir(workspaceRoot: string): string {
32
+ return join(resolveCodexaDataDir(), "workspaces", workspaceStorageKey(workspaceRoot));
33
+ }
34
+
35
+ export function resolveCodexaAttachmentDir(workspaceRoot: string, configuredDir: string): string {
36
+ if (isAbsolute(configuredDir)) return configuredDir;
37
+
38
+ const normalized = configuredDir.trim().replace(/\\/g, "/").replace(/^\.codexa\/?/, "") || "attachments";
39
+ const safeRelativeDir = normalize(normalized).replace(/^(\.\.([/\\]|$))+/, "") || "attachments";
40
+ return join(resolveCodexaWorkspaceDataDir(workspaceRoot), safeRelativeDir);
41
+ }
42
+
43
+ export function resolveCodexaDebugLogPath(env: Environment = process.env): string {
44
+ return join(resolveCodexaDataDir(undefined, env), "debug", "render-status.log");
45
+ }
@@ -1,9 +1,10 @@
1
1
  import { createHash } from "crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
- import { homedir } from "os";
4
- import { join } from "path";
5
- import { isNoiseLine } from "../providers/codexTranscript.js";
6
- import { sanitizeTerminalOutput } from "../terminal/terminalSanitize.js";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ import { isNoiseLine } from "../providers/codexTranscript.js";
6
+ import { sanitizeTerminalOutput } from "../terminal/terminalSanitize.js";
7
+ import { resolveCodexaDataDir } from "./appData.js";
7
8
 
8
9
  type Platform = "win32" | "darwin" | "linux" | string;
9
10
 
@@ -91,15 +92,8 @@ export function resolvePlanDir(platformOverride?: Platform): string {
91
92
  return join(homedir(), "AppData", "Local", "Codexa", "plans");
92
93
  }
93
94
 
94
- if (platform === "darwin") {
95
- return join(homedir(), "Library", "Application Support", "Codexa", "plans");
96
- }
97
-
98
- // Linux and other Unix-like
99
- const xdgDataHome = process.env["XDG_DATA_HOME"];
100
- if (xdgDataHome) return join(xdgDataHome, "codexa", "plans");
101
- return join(homedir(), ".local", "share", "codexa", "plans");
102
- }
95
+ return join(resolveCodexaDataDir(platform), "plans");
96
+ }
103
97
 
104
98
  // SHA-256 of the workspace path ensures filename uniqueness across projects with the same name.
105
99
  function workspaceHash(workspaceRoot: string): string {
@@ -43,7 +43,8 @@ export function AttachmentImportPanel({
43
43
  }
44
44
  }, { isActive: isFocused });
45
45
 
46
- const relativeAttachmentsDir = path.relative(workspaceRoot, attachmentsDir).replace(/\\/g, "/");
46
+ const relativeAttachmentsDir = path.relative(workspaceRoot, attachmentsDir).replace(/\\/g, "/");
47
+ const displayedAttachmentsDir = relativeAttachmentsDir.startsWith("..") ? attachmentsDir : relativeAttachmentsDir;
47
48
  const hasImages = files.some((f) => f.isImage);
48
49
  const showVisionWarning = hasImages && modelSupportsVision === false;
49
50
  const fileLabel = files.length === 1 ? "file" : "files";
@@ -59,7 +60,7 @@ export function AttachmentImportPanel({
59
60
  >
60
61
  <Text color={theme.accent} bold>IMPORT FILE </Text>
61
62
  <Text color={theme.textMuted}>
62
- Copy {files.length} outside-workspace {fileLabel} into .codexa/attachments?
63
+ Copy {files.length} outside-workspace {fileLabel} into {displayedAttachmentsDir}?
63
64
  </Text>
64
65
  </Box>
65
66
 
@@ -76,7 +77,7 @@ export function AttachmentImportPanel({
76
77
  <Box key={i} flexDirection="column" marginBottom={i < files.length - 1 ? 1 : 0}>
77
78
  <Text color={theme.text}>{path.basename(file.srcPath)}</Text>
78
79
  <Text color={theme.textDim}>
79
- {"→ "}{relativeAttachmentsDir}/{file.destFilename}
80
+ {"→ "}{displayedAttachmentsDir}/{file.destFilename}
80
81
  </Text>
81
82
  </Box>
82
83
  ))}