say which harness owns a chat row, on both kinds

The list is merged from two stores and only OpenCode rows were badged, so Claude was marked by
the ABSENCE of a badge — legible only if you already knew the list mixes two harnesses. Both
carry one now, and since `harness` is absent on older Claude rows, anything not OpenCode reads
as Claude, matching the server's own default.

The badge no longer replaces the message count, it sits before it: the count is real on Claude
rows and a hardcoded 0 on OpenCode ones (the session list has no count field and a real one
costs an HTTP call per row), so those rows show the badge and no count rather than a zero that
means "never asked".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 02:43:14 +01:00
co-authored by Claude Opus 5
parent adaaba658c
commit 801eba9284
@@ -10,6 +10,24 @@ import type { SelectedSession } from './ChatDetailPanel';
import { chatListPath, chatNewPath, chatSessionPath, cwdFromSplat } from './chat-routes';
import { PwdSelector } from './PwdSelector';
/**
* Which agent owns a conversation, said on EVERY row.
*
* Only OpenCode rows carried a badge before, which marks one harness and leaves the other inferred from
* its absence — fine when you already know the list mixes two, useless when you don't. The list is
* merged from two stores (Claude transcripts, OpenCode's session db) and the harness decides where a
* reply is dispatched, so it is a property of the row, not a footnote on the odd one out.
*
* `harness` is absent on older Claude rows, so anything that is not OpenCode reads as Claude — the same
* default the server applies.
*/
const HarnessBadge = ({ harness }: { harness?: 'claude' | 'opencode' }) =>
harness === 'opencode' ? (
<span className="rounded bg-info/10 px-1.5 py-0.5 font-medium text-info">OpenCode</span>
) : (
<span className="rounded bg-primary/10 px-1.5 py-0.5 font-medium text-primary">Claude</span>
);
// Reads the /chat conversation list from Claude's own transcript store (source of truth).
// Clicking a session loads its transcript and continues the real Claude session via --resume.
export const SessionList = () => {
@@ -194,13 +212,14 @@ export const SessionList = () => {
className="min-w-0 flex-1"
meta={[
<RelativeTime key="t" value={session.updatedAt} />,
session.harness === 'opencode' ? (
<span key="h" className="rounded bg-info/10 px-1.5 py-0.5 font-medium text-info">
OpenCode
</span>
) : (
`${session.messageCount} msg${session.messageCount === 1 ? '' : 's'}`
),
<HarnessBadge key="h" harness={session.harness} />,
// Claude's transcripts give a real turn count. OpenCode's session list has no
// count field, so the number on those rows is a hardcoded 0 and costs an HTTP
// call per row to make real — the badge stands in its place rather than beside a
// zero that means "not asked".
session.harness === 'opencode'
? null
: `${session.messageCount} msg${session.messageCount === 1 ? '' : 's'}`,
// One row, several transcripts: this conversation was `/clear`ed and carried on,
// and the server has spliced the parts back together. Worth saying out loud —
// the message count above covers all of them, and opening the row shows all of