feat: conversations on the left, the transcript on the right

The transcript was duplicated across both panes, which was the actual
complaint. The left column is now the list of conversations and the right
pane is the transcript of the selected one, so the panes show different
things.

- the open conversation is marked with a check icon and an "Open" tag
- each row carries its own transcript as its detail, so moving the
  selection previews that conversation
- conversations are ordered most-recently-updated first, which keeps the
  one being chatted in at row zero, where Vicinae's selection reset lands
- Return now adapts: with text in the search bar it sends, with an empty
  bar it opens the highlighted conversation (sending an empty prompt was a
  no-op anyway)
- the search bar accessory dropdown is gone. The left column is the
  conversation switcher now, so the dropdown only duplicated it.

Speaker icons in the transcript are not possible, so the labels are plain
text. MdImage renders images block-level, centred, and capped at 200px
tall, and all three candidate sources were tried in a live probe --
icon://person, a raw icon slug, and a file:// asset path -- and each one
fell back to rendering its alt text as a caption. markdown.ts records the
reason so nobody retries it blind.

Verified on screen: one conversation row with an Open tag and a check
icon, the full transcript on the right, and the action bar reading "Open
Conversation" while the search bar is empty. typecheck, eslint, 109 tests
and vici build all pass.
This commit is contained in:
2026-10-04 05:09:15 +08:00
parent d448a4377b
commit 05f77fcc7f
5 changed files with 223 additions and 158 deletions
+12 -8
View File
@@ -15,14 +15,18 @@ import type { ChatTurn, ThreadMessage } from "./types";
export const STREAMING_CURSOR = "▍";
/**
* Role markers for the transcript.
* Speaker labels.
*
* Emoji rather than icon images: Vicinae's markdown renderer is minimal and
* there is no reliable way to inline an `icon://` reference into markdown, but
* plain text always renders.
* Plain text, not icons. Vicinae's markdown renderer draws images as
* block-level, centred elements capped at 200px tall (`MdImage.qml`), and none
* of the candidate sources resolve inside markdown — `icon://person`, a raw
* icon slug, and a `file://` asset path were all tried and each one fell back
* to rendering the alt text as a caption. An icon therefore cannot sit inline
* beside a message the way a text label can. The dim/bright styling carries
* most of the distinction regardless.
*/
export const USER_MARKER = "👤";
export const ASSISTANT_MARKER = "✨";
export const USER_LABEL = "You";
export const ASSISTANT_LABEL = "Lam";
/** Prefixes every line so a multi-line message stays one continuous quote block. */
export function quote(text: string): string {
@@ -41,11 +45,11 @@ export function previewText(text: string, max: number): string {
function renderMessage(message: ThreadMessage): string {
if (message.role === "user") {
return `${USER_MARKER} **You**\n\n${quote(message.text)}`;
return `**${USER_LABEL}**\n\n${quote(message.text)}`;
}
const body = message.text.trim() || (message.isStreaming ? "_Thinking…_" : "_No response._");
const sections = [`${ASSISTANT_MARKER} **Lam**`, "", message.isStreaming ? `${body} ${STREAMING_CURSOR}` : body];
const sections = [`**${ASSISTANT_LABEL}**`, "", message.isStreaming ? `${body} ${STREAMING_CURSOR}` : body];
if (message.error) {
sections.push("", `**Error** — ${message.error}`);