/** * Projectless thread workspaces. * * Codex keeps two kinds of conversation: one attached to a project directory and * one that starts from nothing. The desktop main process owns the second kind — * it picks a folder under the user's workspace root, creates it, and hands the * paths back to the renderer through `projectlessThreads.createPaths`. * * The browser cannot touch the filesystem, so the Node host does it here and * exposes the same three paths over HTTP: * * cwd the directory this conversation runs in * outputDirectory where generated files land (same folder) * workspaceRoot the shared root that contains every thread * * Layout mirrors the desktop app: // */ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; export const WORKSPACE_ROOT = // Linux server default: ~/Codex. The desktop default ~/Documents/Codex // does not exist on a server, so it is not used here. process.env.CODEX_WEB_WORKSPACE_ROOT || path.join(os.homedir(), "Codex"); export function ensureWorkspaceRoot() { fs.mkdirSync(WORKSPACE_ROOT, { recursive: true }); return WORKSPACE_ROOT; } /** Lowercase, collapse anything awkward, keep CJK, cap the length. */ function slugify(value) { const text = String(value ?? "").trim(); if (!text) return ""; return text .toLowerCase() .replace(/[^\p{Letter}\p{Number}]+/gu, "-") .replace(/^-+|-+$/g, "") .slice(0, 40); } export function createProjectlessPaths({ directoryName, prompt } = {}) { const root = ensureWorkspaceRoot(); const day = new Date().toISOString().slice(0, 10); const base = slugify(directoryName) || slugify(prompt) || "thread"; let dir = path.join(root, day, base); let suffix = 1; while (fs.existsSync(dir)) { suffix += 1; dir = path.join(root, day, `${base}-${suffix}`); } fs.mkdirSync(dir, { recursive: true }); return { cwd: dir, outputDirectory: dir, workspaceRoot: root }; } export function readWorkspaceRoot() { return { workspaceRoot: ensureWorkspaceRoot() }; }