feat: snip a region into chat, and make links clickable (#14)
This commit was merged in pull request #14.
This commit is contained in:
@@ -15,6 +15,7 @@ function message(overrides: Partial<ThreadMessage> & Pick<ThreadMessage, "role">
|
||||
isStreaming: false,
|
||||
thinking: "",
|
||||
toolCalls: [],
|
||||
attachments: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -74,9 +75,9 @@ describe("buildThreadMarkdown", () => {
|
||||
expect(markdown.indexOf("second question")).toBeLessThan(markdown.indexOf("first answer"));
|
||||
});
|
||||
|
||||
it("renders no images, because markdown cannot resolve them", () => {
|
||||
// Images render block-level and centred at up to 200px (`MdImage.qml`), and
|
||||
// every candidate icon source fails. Keep the transcript text-only.
|
||||
it("renders no images for a message that has no attachment", () => {
|
||||
// Images only ever come from an attachment this extension took itself. A URL
|
||||
// the model invented is not resolvable and renders as an empty block.
|
||||
const markdown = buildThreadMarkdown([
|
||||
message({ role: "user", text: "q" }),
|
||||
message({ role: "assistant", text: "a" }),
|
||||
@@ -146,6 +147,34 @@ describe("buildThreadMarkdown", () => {
|
||||
expect(markdown).not.toContain("```bash");
|
||||
});
|
||||
|
||||
it("turns a fenced list of URLs into links, because a fence is not clickable", () => {
|
||||
// The reported case: asked for alternatives, the model answered with a code
|
||||
// block of bare GitHub addresses. `MdCodeBlock.qml` draws that as a dark
|
||||
// panel with a Copy button and no link handling, so the address could not be
|
||||
// opened. Only `CMARK_NODE_LINK` becomes an `<a href>` the selection
|
||||
// controller can activate.
|
||||
const markdown = buildThreadMarkdown([
|
||||
message({
|
||||
role: "assistant",
|
||||
text: "GitHub links:\n\n```\nhttps://github.com/jasonkneen/openclicky\nhttps://github.com/unn-Known1/clickyX\n```",
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(markdown).not.toContain("```");
|
||||
expect(markdown).toContain(
|
||||
"[https://github.com/jasonkneen/openclicky](https://github.com/jasonkneen/openclicky)",
|
||||
);
|
||||
});
|
||||
|
||||
it("links a lone URL the model put in inline code", () => {
|
||||
const markdown = buildThreadMarkdown([
|
||||
message({ role: "assistant", text: "`https://example.com`" }),
|
||||
]);
|
||||
|
||||
expect(markdown).not.toContain("```");
|
||||
expect(markdown).toContain("[https://example.com](https://example.com)");
|
||||
});
|
||||
|
||||
it("lists tools on one quiet line, flagging failures", () => {
|
||||
const markdown = buildThreadMarkdown([
|
||||
message({
|
||||
@@ -212,7 +241,17 @@ describe("buildThreadMarkdown", () => {
|
||||
|
||||
describe("sessionToPlainText", () => {
|
||||
function turn(prompt: string, response: string, error?: string): ChatTurn {
|
||||
return { id: prompt, prompt, response, thinking: "", model: "m", createdAt: 0, toolCalls: [], error };
|
||||
return {
|
||||
id: prompt,
|
||||
prompt,
|
||||
response,
|
||||
thinking: "",
|
||||
model: "m",
|
||||
createdAt: 0,
|
||||
toolCalls: [],
|
||||
attachments: [],
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
it("returns an empty string for an empty conversation", () => {
|
||||
@@ -247,3 +286,69 @@ describe("sessionToPlainText", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("attachments in the transcript", () => {
|
||||
const screenshot = { path: "/tmp/snip.png", mime: "image/png" };
|
||||
|
||||
it("shows the attachment instead of just naming it", () => {
|
||||
// Vicinae resolves a local file through `ImageURL::local`, so the capture is
|
||||
// rendered rather than described. `MdImage.qml` caps the height at 200px, so
|
||||
// it is a thumbnail, and the alt text is what is left if the file is gone.
|
||||
const markdown = buildThreadMarkdown([
|
||||
message({ role: "user", text: "what is this?", attachments: [screenshot] }),
|
||||
]);
|
||||
|
||||
expect(markdown).toContain("");
|
||||
});
|
||||
|
||||
it("keeps the question above its image", () => {
|
||||
const markdown = buildThreadMarkdown([
|
||||
message({ role: "user", text: "what is this?", attachments: [screenshot] }),
|
||||
]);
|
||||
|
||||
expect(markdown.indexOf("> what is this?")).toBeLessThan(markdown.indexOf("!["));
|
||||
});
|
||||
|
||||
it("renders every attachment on a turn", () => {
|
||||
const markdown = buildThreadMarkdown([
|
||||
message({
|
||||
role: "user",
|
||||
text: "compare",
|
||||
attachments: [screenshot, { path: "/tmp/second.png", mime: "image/png" }],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(markdown).toContain("file:///tmp/snip.png");
|
||||
expect(markdown).toContain("file:///tmp/second.png");
|
||||
});
|
||||
|
||||
it("never puts an image on an answer, which has no attachments", () => {
|
||||
const markdown = buildThreadMarkdown([
|
||||
message({ role: "assistant", text: "a cat", attachments: [screenshot] }),
|
||||
]);
|
||||
|
||||
expect(markdown).not.toContain("![");
|
||||
});
|
||||
|
||||
it("says nothing about attachments when there are none", () => {
|
||||
const markdown = buildThreadMarkdown([message({ role: "user", text: "q" })]);
|
||||
|
||||
expect(markdown).not.toContain("Screenshot");
|
||||
});
|
||||
|
||||
it("notes the screenshot when a conversation is copied as plain text", () => {
|
||||
const question = {
|
||||
id: "t1",
|
||||
prompt: "what is this?",
|
||||
response: "a cat",
|
||||
thinking: "",
|
||||
model: "m",
|
||||
createdAt: 0,
|
||||
toolCalls: [],
|
||||
attachments: [screenshot],
|
||||
};
|
||||
const text = sessionToPlainText([question]);
|
||||
|
||||
expect(text).toContain("_(screenshot attached)_");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user