From 91699a382ecddf7cddf41f171a1c7fe81236d876 Mon Sep 17 00:00:00 2001 From: h Date: Tue, 13 Oct 2026 01:23:49 +0200 Subject: [PATCH] feat(view,tests): query blocks stay code in text the agent did not write --- preview/fake-client.ts | 18 +++++++++++++++++- src/ui/host.ts | 18 +++++++++++++++--- tests/inert.test.ts | 27 +++++++++++++++++++++++++++ tests/obsidian-stub.ts | 6 ++++++ vitest.config.ts | 1 + 5 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 tests/inert.test.ts diff --git a/preview/fake-client.ts b/preview/fake-client.ts index 3d8ea13..df38db1 100644 --- a/preview/fake-client.ts +++ b/preview/fake-client.ts @@ -223,7 +223,12 @@ function stamped(messages: HistoryMessage[]): HistoryMessage[] { const HISTORY: Record = { [MASTER]: [ - { content: "с утра: дневник приехал? что в хендауте", role: "user" }, + { + content: "с утра: дневник приехал? что в хендауте", + role: "user", + service: + "[envelope - background signal, not a message; react only if it bears on the question]\ntime: 2026-08-29 06:04 (Warsaw)\nvault, changed since 05:09: 📅 дни/2026-08-29.md (+4)\n--- 📅 дни/2026-08-29.md, added lines only ---\n+ # планы\n+ просрочены:\n+ ```tasks\n+ not done\n+ 06:04 - пожалуй пойду спать", + }, { content: "Приехал. В [[2026-08-28|хендауте]] два хвоста: **бластер** ждёт выбора модели, и **ufw на dell** - порт 62990 снаружи молчит. Ветка по ufw открыта, см. [[ufw на dell]].", @@ -232,6 +237,11 @@ const HISTORY: Record = { { content: "ок, гони ufw", role: "user" }, { content: [ + { + thinking: + "The firewall rule and the proxy config can disagree in two ways: the port is closed, or nothing listens behind it. Reading the Caddy site first tells me which port the outside is supposed to reach at all.", + type: "thinking", + }, { text: "Смотрю правила через `ssh dell` и сверяю с `site.caddy`.", type: "text", @@ -242,6 +252,12 @@ const HISTORY: Record = { name: "Bash", type: "tool_use", }, + { + id: "tu_say", + input: { text: "Нашёл: снаружи должен отвечать 443, не 62990." }, + name: "mcp__gateway__say", + type: "tool_use", + }, ], role: "assistant", }, diff --git a/src/ui/host.ts b/src/ui/host.ts index 96b9592..138f560 100644 --- a/src/ui/host.ts +++ b/src/ui/host.ts @@ -1,5 +1,6 @@ import type { App, Component } from "obsidian"; -import { MarkdownRenderer, Platform } from "obsidian"; +import { MarkdownPreviewRenderer, MarkdownRenderer, Platform } from "obsidian"; +import { inertFences } from "$lib/panel/fences"; import type { PanelHost } from "$lib/panel/host"; export interface HostOptions { @@ -12,17 +13,28 @@ export interface HostOptions { sourcePath: () => string; } +const QUERY_LANGUAGES = new Set(["tasks", "dataview", "dataviewjs", "query"]); + +function runs(language: string): boolean { + const registered = ( + MarkdownPreviewRenderer as unknown as { + codeBlockPostProcessors?: Record; + } + ).codeBlockPostProcessors; + return QUERY_LANGUAGES.has(language) || language in (registered ?? {}); +} + // The Obsidian side of the panel: markdown through the app's own renderer, // [[links]] opened in place, a conversation's note opened by its binding. export function obsidianHost(options: HostOptions): PanelHost { const { app, component } = options; return { - markdown(node, text) { + markdown(node, text, { inert = false } = {}) { let alive = true; const staging = document.createElement("div"); MarkdownRenderer.render( app, - text, + inert ? inertFences(text, runs) : text, staging, options.sourcePath(), component diff --git a/tests/inert.test.ts b/tests/inert.test.ts new file mode 100644 index 0000000..1116bba --- /dev/null +++ b/tests/inert.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { inertFences } from "$lib/panel/fences"; + +const runs = (language: string) => ["tasks", "dataview"].includes(language); + +describe("inertFences", () => { + it("turns a query block into code", () => { + expect(inertFences("```tasks\nnot done\n```", runs)).toBe( + "```text\nnot done\n```" + ); + expect(inertFences("~~~Dataview\nlist\n~~~", runs)).toBe( + "~~~text\nlist\n~~~" + ); + }); + + it("reaches a block opened inside a list item or a quote", () => { + expect(inertFences("+ ```tasks\n+ not done", runs)).toBe( + "+ ```text\n+ not done" + ); + expect(inertFences("> 1. ````tasks {x}", runs)).toBe("> 1. ````text {x}"); + }); + + it("leaves ordinary code and closing fences alone", () => { + const text = "```python\nprint(1)\n```\ninline `tasks` stays"; + expect(inertFences(text, runs)).toBe(text); + }); +}); diff --git a/tests/obsidian-stub.ts b/tests/obsidian-stub.ts index 8375b67..9508d6c 100644 --- a/tests/obsidian-stub.ts +++ b/tests/obsidian-stub.ts @@ -34,6 +34,12 @@ export const moment = () => undefined; export const requestUrl = () => Promise.reject(new Error("no network in the stub")); export const setIcon = () => undefined; +export const MarkdownPreviewRenderer = { + codeBlockPostProcessors: { tasks: () => undefined } as Record< + string, + unknown + >, +}; function escapeHtml(value: string): string { return value diff --git a/vitest.config.ts b/vitest.config.ts index 2cf5494..65018c9 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ resolve: { alias: { + $lib: new URL("../beaver-gateway/ui/src/lib", import.meta.url).pathname, obsidian: new URL("./tests/obsidian-stub.ts", import.meta.url).pathname, }, },