stop showing opencode placeholder titles as if they were names
OpenCode titles a session from the conversation, but asynchronously — a finished turn of ours ended up called "Single color in oc-red2.png", better than anything we would generate. Until then the session is literally named "New session - 2026-08-10T15:44:17.178Z". That window is exactly when a session is most visible: /chat/live shows turns that are RUNNING, so the placeholder is what the panel catches, and a live row was being labelled with a timestamp string. Recognise it and treat it as untitled, so the good name arrives on its own. Passing --title on the run was the other option and is worse: it fixes the transient case by permanently replacing opencode own title with a truncated prompt, degrading it where it lasts longest. A pattern match rather than startsWith, because a genuine title is allowed to begin with those words. Two defects the tests caught while writing them: a whitespace-only title was not treated as unnamed, and the mapping let undefined through where a string was required. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -150,9 +150,11 @@ chatRouter.get('/live', async (ctx) => {
|
|||||||
isGenerating: true,
|
isGenerating: true,
|
||||||
pendingTasks: 0,
|
pendingTasks: 0,
|
||||||
harness: 'opencode' as const,
|
harness: 'opencode' as const,
|
||||||
// Still null for the first seconds of a turn: the id is only known once the subprocess prints it.
|
// Null for the first seconds of a turn (the id is only known once the subprocess prints it), and
|
||||||
// That window is real and short, and showing nothing beats showing a key the user has never seen.
|
// null again while OpenCode still calls the session `New session - <ISO>` — `listOpenCodeSessions`
|
||||||
title: meta?.title ?? null,
|
// normalises that placeholder to `(untitled)`, and a live row is better blank than named after a
|
||||||
|
// timestamp. The real title arrives on a later poll, because OpenCode writes it asynchronously.
|
||||||
|
title: meta?.title && meta.title !== '(untitled)' ? meta.title : null,
|
||||||
cwd: meta?.cwd || null,
|
cwd: meta?.cwd || null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { isPlaceholderOpenCodeTitle } from './opencode-sessions';
|
||||||
|
|
||||||
|
// OpenCode names a session `New session - <ISO>` until it has titled it from the conversation, which it
|
||||||
|
// does asynchronously. The strings below are real ones observed on 1.18.16 — both the placeholder and
|
||||||
|
// the good title it was replaced by.
|
||||||
|
|
||||||
|
describe('isPlaceholderOpenCodeTitle', () => {
|
||||||
|
it('recognises the placeholder OpenCode uses before it has titled a session', () => {
|
||||||
|
expect(isPlaceholderOpenCodeTitle('New session - 2026-08-10T15:44:17.178Z')).toBe(true);
|
||||||
|
expect(isPlaceholderOpenCodeTitle('New session - 2026-08-10T12:41:02.757Z')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats an absent or empty title as unnamed too', () => {
|
||||||
|
expect(isPlaceholderOpenCodeTitle(undefined)).toBe(true);
|
||||||
|
expect(isPlaceholderOpenCodeTitle('')).toBe(true);
|
||||||
|
expect(isPlaceholderOpenCodeTitle(' ')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a real title, including one that merely mentions a session', () => {
|
||||||
|
// The title OpenCode actually gave a turn of ours, and the reason this is a pattern match rather
|
||||||
|
// than a `startsWith('New session')`: a genuine title is allowed to begin with those words.
|
||||||
|
expect(isPlaceholderOpenCodeTitle('Single color in oc-red2.png')).toBe(false);
|
||||||
|
expect(isPlaceholderOpenCodeTitle('New session handling in the runner')).toBe(false);
|
||||||
|
expect(isPlaceholderOpenCodeTitle('New session - what should it be called?')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,6 +24,23 @@ import { logger } from './logger';
|
|||||||
* the `!cwd` escape never fired either and there was no configuration in which an OpenCode session
|
* the `!cwd` escape never fired either and there was no configuration in which an OpenCode session
|
||||||
* appeared in /chat. Verified against the live server: 7 sessions present, 0 returned.
|
* appeared in /chat. Verified against the live server: 7 sessions present, 0 returned.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* OpenCode's own placeholder name for a session it has not titled yet.
|
||||||
|
*
|
||||||
|
* It titles a session from the conversation, but ASYNCHRONOUSLY and after there is something to read —
|
||||||
|
* a finished turn ends up called "Single color in oc-red2.png", which is better than anything we would
|
||||||
|
* generate. Until then the session is literally named `New session - 2026-08-10T15:44:17.178Z`.
|
||||||
|
*
|
||||||
|
* That window is exactly when a session is most visible: `/chat/live` shows turns that are RUNNING, so
|
||||||
|
* the placeholder is what the panel catches. Treating it as "not yet titled" lets the good name arrive
|
||||||
|
* on its own. Forcing `--title` on the run instead would fix the transient case by permanently
|
||||||
|
* replacing opencode's title with a truncated prompt — worse where it lasts longest.
|
||||||
|
*/
|
||||||
|
export const isPlaceholderOpenCodeTitle = (title: string | undefined): boolean => {
|
||||||
|
const trimmed = title?.trim();
|
||||||
|
return !trimmed || /^New session - \d{4}-\d{2}-\d{2}T[\d:.]+Z?$/.test(trimmed);
|
||||||
|
};
|
||||||
|
|
||||||
export async function listOpenCodeSessions(cwd?: string): Promise<ClaudeSessionSummary[]> {
|
export async function listOpenCodeSessions(cwd?: string): Promise<ClaudeSessionSummary[]> {
|
||||||
try {
|
try {
|
||||||
const { baseUrl } = await ensureServer();
|
const { baseUrl } = await ensureServer();
|
||||||
@@ -35,7 +52,7 @@ export async function listOpenCodeSessions(cwd?: string): Promise<ClaudeSessionS
|
|||||||
const updated = s.time?.updated ?? created;
|
const updated = s.time?.updated ?? created;
|
||||||
return {
|
return {
|
||||||
id: s.id,
|
id: s.id,
|
||||||
title: s.title || '(untitled)',
|
title: isPlaceholderOpenCodeTitle(s.title) ? '(untitled)' : (s.title ?? '(untitled)'),
|
||||||
cwd: s.directory ?? '',
|
cwd: s.directory ?? '',
|
||||||
createdAt: new Date(created).toISOString(),
|
createdAt: new Date(created).toISOString(),
|
||||||
updatedAt: new Date(updated).toISOString(),
|
updatedAt: new Date(updated).toISOString(),
|
||||||
|
|||||||
Reference in New Issue
Block a user