fix(sessions): title app-created sessions from the first user message

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.
This commit is contained in:
Haileyesus
2026-07-07 15:07:50 +03:00
parent 6336d94fd6
commit 472c7a8055
7 changed files with 331 additions and 24 deletions

View File

@@ -1066,6 +1066,30 @@ export function getOpenCodeDatabasePath(): string {
return path.join(os.homedir(), '.local', 'share', 'opencode', 'opencode.db');
}
/**
* Decodes an OpenCode text payload that was persisted as a JSON string literal.
*
* OpenCode can store the first user prompt (and other text parts) as `"hello"`
* instead of `hello`. Used by both the OpenCode session reader (transcript
* history) and the OpenCode synchronizer (session titling) so a session name or
* message body never surfaces with surrounding quote characters. Only fully
* quoted, valid JSON string literals are unwrapped; ordinary prose that merely
* happens to start/end with a quote is returned untouched.
*/
export function unwrapJsonStringLiteral(value: string): string {
const trimmed = value.trim();
if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) {
return value;
}
try {
const parsed = JSON.parse(trimmed);
return typeof parsed === 'string' ? parsed : value;
} catch {
return value;
}
}
// ---------------------------
//----------------- SAFE DIRECTORY NAME UTILITIES ------------
/**