From 48cdc3d19a319981a8a4128be98db3c283f81790 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sat, 24 Oct 2026 18:34:12 +0900 Subject: feat: render live text-file embeds in expandable Markdown blocks Show rendered first/last six-line previews with expand, refresh and retry; bound and deduplicate context-guarded fetches. Verify 2586 tests twice with Node Web Storage compatibility flag, 2 live integration tests, typecheck, Biome and production build. --- src/app/conversation-settings.svelte.ts | 16 ++++++++++++++-- src/app/store-types.ts | 4 ++++ src/app/store.svelte.ts | 7 +++++++ src/app/views/ChatPane.svelte | 11 +++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) (limited to 'src/app') diff --git a/src/app/conversation-settings.svelte.ts b/src/app/conversation-settings.svelte.ts index df259af..b853a8f 100644 --- a/src/app/conversation-settings.svelte.ts +++ b/src/app/conversation-settings.svelte.ts @@ -43,9 +43,11 @@ export function createConversationSettings(deps: { // The active conversation's persisted working directory (per-tab). Seeded from // the backend on focus change; null for a draft / when unset. let cwd = $state(null); + let textFileContextRevision = $state(0); /** Refetch the workspace conversation's cwd into reactive state (works for a draft too). */ async function refreshCwd(): Promise { + textFileContextRevision++; const id = deps.workspaceConversationId(); try { const res = await fetchImpl(`${httpBase}/conversations/${encodeURIComponent(id)}/cwd`); @@ -68,6 +70,7 @@ export function createConversationSettings(deps: { * so it is a silent no-op (mirrors `refreshCwd` for a draft). */ async function refreshComputer(): Promise { + textFileContextRevision++; const id = deps.workspaceConversationId(); // Clear immediately so a switch never shows the PREVIOUS conversation's // computer while the fetch is in flight (null renders as "Local"). @@ -177,6 +180,9 @@ export function createConversationSettings(deps: { refreshThinking, refreshCompactPercent, refreshVisionSettings, + get textFileContextRevision() { + return textFileContextRevision; + }, get cwd() { return cwd; }, @@ -213,7 +219,10 @@ export function createConversationSettings(deps: { } const data = (await res.json()) as CwdResponse; const next = data.cwd ?? null; - if (deps.workspaceConversationId() === id) cwd = next; + if (deps.workspaceConversationId() === id) { + cwd = next; + textFileContextRevision++; + } return { ok: true, cwd: next }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : "Set cwd request failed" }; @@ -241,7 +250,10 @@ export function createConversationSettings(deps: { } const data = (await res.json()) as ConversationComputerResponse; const next = data.computerId ?? null; - if (deps.workspaceConversationId() === id) computerId = next; + if (deps.workspaceConversationId() === id) { + computerId = next; + textFileContextRevision++; + } return { ok: true, computerId: next }; } catch (err) { return { diff --git a/src/app/store-types.ts b/src/app/store-types.ts index 774d041..6835166 100644 --- a/src/app/store-types.ts +++ b/src/app/store-types.ts @@ -35,6 +35,7 @@ import type { HeartbeatStopResult, } from "../features/heartbeat"; import type { LayoutStore } from "../features/layout"; +import type { TextFileClient } from "../features/markdown"; import type { NamedAgent, NamedAgentDeleteResult, @@ -215,6 +216,9 @@ export interface AppStore { warmNow(): Promise; /** The workspace conversation's persisted working directory, or null when unset. */ readonly cwd: string | null; + /** Invalidates live-file routing after known settings changes. */ + readonly textFileContextRevision: number; + createTextFileClient(conversationId: string): TextFileClient; /** The conversation workspace settings target: the active tab, or the pending draft's id. */ readonly currentConversationId: string; /** diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts index fa12036..8fcc4cc 100644 --- a/src/app/store.svelte.ts +++ b/src/app/store.svelte.ts @@ -21,6 +21,7 @@ import { createLayoutStore, listPanelViews, } from "../features/layout"; +import { createTextFileClient } from "../features/markdown"; import type { NamedAgent, NamedAgentSelectionSnapshot } from "../features/named-agents"; import type { PluginsStore } from "../features/plugins"; import { createPluginsHttp, createPluginsStore } from "../features/plugins"; @@ -472,6 +473,12 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { get storage() { return localStorageOpt; }, + get textFileContextRevision(): number { + return settings.textFileContextRevision; + }, + createTextFileClient(conversationId) { + return createTextFileClient(httpBase, conversationId, fetchImpl); + }, get cwd(): string | null { return settings.cwd; }, diff --git a/src/app/views/ChatPane.svelte b/src/app/views/ChatPane.svelte index d132fad..b5e05bb 100644 --- a/src/app/views/ChatPane.svelte +++ b/src/app/views/ChatPane.svelte @@ -13,6 +13,16 @@ let { ports }: { ports: AppViewPorts } = $props(); const store = $derived(ports.store); + const textFiles = $derived.by(() => { + void store.activeWorkspaceId; + void store.textFileContextRevision; + const id = store.activeConversationId; + return id ? store.createTextFileClient(id) : undefined; + }); + $effect(() => { + const client = textFiles; + return () => client?.dispose(); + }); // Cache timer: Claude Code subscription models only (5-minute prompt-cache expiry). const cacheTimer = $derived( @@ -88,6 +98,7 @@ providerRetry={store.activeChat.providerRetry} apiBaseUrl={store.httpBase} cwd={store.cwd} + {textFiles} {cacheTimer} /> {/key} -- cgit v1.2.3