1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
<script lang="ts">
import { untrack } from "svelte";
import type { MarkdownImageOptions } from "../logic/markdown";
import { textFilePreview, type TextFileReference } from "../logic/text-file";
import type { TextFileClient, TextFileState } from "../types";
import MarkdownContent from "./MarkdownContent.svelte";
let {
reference,
client,
image = undefined,
}: {
reference: TextFileReference;
client: TextFileClient;
image?: MarkdownImageOptions | undefined;
} = $props();
let loadState = $state<TextFileState>({ kind: "loading" });
let expanded = $state(false);
const preview = $derived(
loadState.kind === "ready"
? textFilePreview(loadState.file.text, loadState.file.format === "markdown")
: null,
);
const path = $derived(reference.path);
const currentClient = $derived(client);
$effect(() => {
const source = currentClient;
const destination = path;
expanded = false;
// The callback can publish synchronously. Only routing changes, not reads
// inside a subscriber or unrelated parent prop updates, own this effect.
return untrack(() =>
source.subscribe(destination, (next) => {
loadState = next;
}),
);
});
// Use the resolved file's directory for local images, not the chat's cwd.
const documentImage = $derived(
loadState.kind === "ready" && image
? {
apiBase: image.apiBase,
cwd: loadState.file.path.slice(0, loadState.file.path.lastIndexOf("/")) || "/",
}
: image,
);
</script>
<section
class="inset-well my-2 w-full min-w-0 rounded-box text-sm"
aria-label={`Text file: ${reference.label}`}
>
<header class="flex min-w-0 flex-wrap items-center gap-2 px-4 py-2 font-medium">
<span aria-hidden="true">▤</span>
<span>{reference.label}</span>
<span class="min-w-0 flex-1 break-all font-mono text-xs font-normal text-base-content/60"
>{reference.path}</span
>
<button
type="button"
class="btn btn-ghost btn-xs"
onclick={() => client.refresh(reference.path)}
disabled={loadState.kind === "loading"}
>
{loadState.kind === "error" ? "Retry" : "Refresh"}
</button>
</header>
<div class="min-w-0 px-4 pb-4">
{#if loadState.kind === "loading"}
<p role="status">
<span class="loading loading-dots loading-sm" aria-hidden="true"></span> Loading text file…
</p>
{:else if loadState.kind === "error"}
<p role="alert" class="text-error">{loadState.message}</p>
{:else if loadState.file.text === ""}
<p class="italic text-base-content/60">Empty file</p>
{:else if preview}
{#if expanded}
{@render content(loadState.file.text, loadState.file.format)}
{:else}
{@render content(preview.head, loadState.file.format)}
{#if preview.omitted > 0}
<p class="my-2 border-y border-base-content/20 py-2 text-xs text-base-content/60">
⋯ {preview.omitted} source lines omitted ⋯
</p>
{@render content(preview.tail, loadState.file.format)}
{/if}
{/if}
<button
type="button"
class="btn btn-soft btn-xs mt-2"
aria-expanded={expanded}
onclick={() => {
expanded = !expanded;
}}
>
{expanded ? "Collapse document" : "Expand document"}
</button>
<p class="mt-2 text-xs text-base-content/50">
Live file · Refresh rereads disk · maximum 256 KiB
</p>
{/if}
</div>
</section>
{#snippet content(text: string, format: "markdown" | "text")}
{#if format === "markdown"}
<!-- No textFiles port: nested references are inert, even in expanded content. -->
<MarkdownContent {text} image={documentImage} />
{:else}
<pre class="overflow-x-auto whitespace-pre font-mono text-xs"><code>{text}</code></pre>
{/if}
{/snippet}
|