feat: snip a region into chat, and make links clickable (#14)

This commit was merged in pull request #14.
This commit is contained in:
2026-10-05 08:38:50 +00:00
parent f5d1ced4d6
commit 576c5946f7
44 changed files with 3890 additions and 120 deletions
+28 -3
View File
@@ -1,5 +1,6 @@
import { imageSource } from "./attachments";
import { repairCodeBlocks } from "./code";
import type { ChatTurn, ThreadMessage } from "./types";
import type { Attachment, ChatTurn, ThreadMessage } from "./types";
/**
* Markdown rendering for the detail panel.
@@ -22,6 +23,24 @@ import type { ChatTurn, ThreadMessage } from "./types";
export const STREAMING_CURSOR = "▍";
/**
* Renders attachments as markdown images.
*
* Vicinae does resolve a local file: `MdImage.qml` draws it block-level and
* centred, uses the alt text as an italic caption underneath, and caps the
* height at 200px no matter what is asked for. So this is a thumbnail rather
* than something readable, which is why the action panel also offers Open
* Screenshot for the full image.
*
* The alt text is short on purpose. Whatever is here is drawn centred under the
* image, so a sentence reads as a stray line of prose floating in the middle of
* the pane. It is also the only thing left if the file has since been deleted,
* which is why it is not empty.
*/
export function renderAttachments(attachments: Attachment[]): string {
return attachments.map((attachment) => `![Screenshot](${imageSource(attachment)})`).join("\n\n");
}
/** Which optional sections the transcript includes. Driven by preferences. */
export interface TranscriptOptions {
showToolCalls: boolean;
@@ -49,7 +68,12 @@ function renderTools(message: ThreadMessage): string {
function renderMessage(message: ThreadMessage, options: TranscriptOptions): string {
// The question is identified by its styling alone.
if (message.role === "user") return quote(message.text);
if (message.role === "user") {
const images = renderAttachments(message.attachments);
const asked = quote(message.text);
return images ? `${asked}\n\n${images}` : asked;
}
// While a tool is running there is no answer text yet, so the activity line
// stands in and the user can see what Lam is actually doing.
@@ -138,9 +162,10 @@ export function sessionToPlainText(turns: ChatTurn[]): string {
return turns
.map((turn) => {
const question = `**You**\n\n${turn.prompt}`;
const attached = turn.attachments.length > 0 ? "\n\n_(screenshot attached)_" : "";
const answer = turn.response.trim() || (turn.error ? "" : "_(no response)_");
const failure = turn.error ? `\n\n**[error]** ${turn.error}` : "";
return `${question}\n\n**Lam**\n\n${answer}${failure}`;
return `${question}${attached}\n\n**Lam**\n\n${answer}${failure}`;
})
.join("\n\n---\n\n");
}