mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-08 22:55:41 +08:00
App-created sessions (started by sending a message from cloudcli) were titled with placeholder names — "Untitled Codex Session" from the disk indexer and, briefly, "New session" from the empty canonical upsert — before a later sync finally settled on the right text, causing the sidebar title to flicker. - Codex/OpenCode synchronizers now title app-created sessions (distinct app id mapped to a provider id) from the first user message, while sessions found purely by indexing keep their existing setup. Claude keeps its AI-generated titles; Cursor already used the first message. - Decode OpenCode's JSON-string-literal prompts so titles no longer surface wrapped in quotes; hoist unwrapJsonStringLiteral into shared/utils since it's now used by both the reader and synchronizer. - Guard the sidebar upsert merge so an empty summary can never blank out a title that is already set. - Add codex/opencode synchronizer tests for the app-created vs indexed naming paths.
112 lines
4.2 KiB
TypeScript
112 lines
4.2 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
|
|
import { CodexSessionSynchronizer } from '@/modules/providers/list/codex/codex-session-synchronizer.provider.js';
|
|
|
|
const patchHomeDir = (nextHomeDir: string) => {
|
|
const original = os.homedir;
|
|
(os as any).homedir = () => nextHomeDir;
|
|
return () => {
|
|
(os as any).homedir = original;
|
|
};
|
|
};
|
|
|
|
async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
|
|
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'codex-provider-db-'));
|
|
const databasePath = path.join(tempDirectory, 'auth.db');
|
|
|
|
closeConnection();
|
|
process.env.DATABASE_PATH = databasePath;
|
|
await initializeDatabase();
|
|
|
|
try {
|
|
await runTest();
|
|
} finally {
|
|
closeConnection();
|
|
if (previousDatabasePath === undefined) {
|
|
delete process.env.DATABASE_PATH;
|
|
} else {
|
|
process.env.DATABASE_PATH = previousDatabasePath;
|
|
}
|
|
await rm(tempDirectory, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes one Codex rollout transcript. `firstUserMessage` mirrors the
|
|
* `event_msg`/`user_message` payload the runtime records for the prompt the
|
|
* user typed; omitting it produces a transcript with no user turn.
|
|
*/
|
|
const writeCodexTranscript = async (
|
|
homeDir: string,
|
|
codexSessionId: string,
|
|
workspacePath: string,
|
|
firstUserMessage?: string,
|
|
): Promise<string> => {
|
|
const sessionsDir = path.join(homeDir, '.codex', 'sessions', '2026', '07', '07');
|
|
await mkdir(sessionsDir, { recursive: true });
|
|
|
|
const lines: string[] = [
|
|
JSON.stringify({ type: 'session_meta', payload: { id: codexSessionId, cwd: workspacePath } }),
|
|
];
|
|
if (firstUserMessage !== undefined) {
|
|
lines.push(JSON.stringify({ type: 'event_msg', payload: { type: 'user_message', message: firstUserMessage } }));
|
|
}
|
|
|
|
const filePath = path.join(sessionsDir, `rollout-${codexSessionId}.jsonl`);
|
|
await writeFile(filePath, `${lines.join('\n')}\n`, 'utf8');
|
|
return filePath;
|
|
};
|
|
|
|
test('Codex synchronizer titles app-created sessions from the first user message', { concurrency: false }, async () => {
|
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'codex-session-sync-app-'));
|
|
const workspacePath = path.join(tempRoot, 'workspace');
|
|
await mkdir(workspacePath, { recursive: true });
|
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
|
|
try {
|
|
await writeCodexTranscript(tempRoot, 'codex-app-1', workspacePath, 'Fix the login redirect bug');
|
|
await withIsolatedDatabase(async () => {
|
|
// The app allocates its own id and later maps the provider id onto it,
|
|
// exactly as a message sent from cloudcli does.
|
|
sessionsDb.createAppSession('app-1', 'codex', workspacePath);
|
|
sessionsDb.assignProviderSessionId('app-1', 'codex-app-1');
|
|
|
|
const synchronizer = new CodexSessionSynchronizer();
|
|
await synchronizer.synchronize();
|
|
|
|
assert.equal(sessionsDb.getSessionById('app-1')?.custom_name, 'Fix the login redirect bug');
|
|
});
|
|
} finally {
|
|
restoreHomeDir();
|
|
await rm(tempRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('Codex synchronizer leaves indexed sessions untitled when no name is available', { concurrency: false }, async () => {
|
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'codex-session-sync-indexed-'));
|
|
const workspacePath = path.join(tempRoot, 'workspace');
|
|
await mkdir(workspacePath, { recursive: true });
|
|
const restoreHomeDir = patchHomeDir(tempRoot);
|
|
|
|
try {
|
|
// A CLI-created session has no app row; its first user message must NOT be
|
|
// used as the title, preserving the existing indexing behavior.
|
|
await writeCodexTranscript(tempRoot, 'codex-indexed-1', workspacePath, 'This prompt should be ignored');
|
|
await withIsolatedDatabase(async () => {
|
|
const synchronizer = new CodexSessionSynchronizer();
|
|
await synchronizer.synchronize();
|
|
|
|
assert.equal(sessionsDb.getSessionById('codex-indexed-1')?.custom_name, 'Untitled Codex Session');
|
|
});
|
|
} finally {
|
|
restoreHomeDir();
|
|
await rm(tempRoot, { recursive: true, force: true });
|
|
}
|
|
});
|