Share: load server data on page load

This commit is contained in:
Frank 2025-06-08 01:17:54 -04:00
parent 879d02f86c
commit 1d782dc19a
3 changed files with 329 additions and 132 deletions

View file

@ -69,6 +69,15 @@ export class SyncServer extends DurableObject<Env> {
return secret
}
public async messages() {
const data = await this.ctx.storage.list()
const messages = []
for (const [key, content] of data.entries()) {
messages.push({ key, content })
}
return messages
}
private async getSecret() {
return this.ctx.storage.get<string>("secret")
}
@ -163,5 +172,17 @@ export default {
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
return stub.fetch(request)
}
if (request.method === "GET" && method === "share_messages") {
const id = url.searchParams.get("id")
console.log("share_messages", id)
if (!id)
return new Response("Error: Share ID is required", { status: 400 })
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
const messages = await stub.messages()
return new Response(JSON.stringify({ messages }), {
headers: { "Content-Type": "application/json" },
})
}
},
}

View file

@ -139,12 +139,10 @@ function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> {
entries.push([arrayPath, item])
}
})
}
else {
} else {
entries.push(...flattenToolArgs(value, path))
}
}
else {
} else {
entries.push([path, value])
}
}
@ -360,7 +358,9 @@ function TerminalPart(props: TerminalPartProps) {
{...rest}
>
<div data-section="body">
<div data-section="header"><span>{local.desc}</span></div>
<div data-section="header">
<span>{local.desc}</span>
</div>
<div data-section="content">
<CodeBlock
lang="ansi"
@ -384,25 +384,26 @@ function TerminalPart(props: TerminalPartProps) {
}
function ToolFooter(props: { time: number }) {
return (
props.time > MIN_DURATION
? <span data-part-footer title={`${props.time}ms`}>
{formatDuration(props.time)}
</span>
: <div data-part-footer="spacer"></div>
return props.time > MIN_DURATION ? (
<span data-part-footer title={`${props.time}ms`}>
{formatDuration(props.time)}
</span>
) : (
<div data-part-footer="spacer"></div>
)
}
export default function Share(props: { api: string }) {
export default function Share(props: {
api: string
data: { key: string; content: SessionMessage | SessionInfo }[]
}) {
let params = new URLSearchParams(document.location.search)
const id = params.get("id")
const [store, setStore] = createStore<{
info?: SessionInfo
messages: Record<string, SessionMessage>
}>({
messages: {},
})
}>({ messages: {} })
const messages = createMemo(() =>
Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)),
)
@ -410,6 +411,19 @@ export default function Share(props: { api: string }) {
[Status, string?]
>(["disconnected", "Disconnected"])
const processDatum = (d: any) => {
const [root, type, ...splits] = d.key.split("/")
if (root !== "session") return
if (type === "info") {
setStore("info", reconcile(d.content))
return
}
if (type === "message") {
const [, messageID] = splits
setStore("messages", messageID, reconcile(d.content))
}
}
onMount(() => {
const apiUrl = props.api
@ -424,6 +438,10 @@ export default function Share(props: { api: string }) {
return
}
for (const datum of props.data) {
processDatum(datum)
}
let reconnectTimer: number | undefined
let socket: WebSocket | null = null
@ -454,17 +472,7 @@ export default function Share(props: { api: string }) {
socket.onmessage = (event) => {
console.log("WebSocket message received")
try {
const data = JSON.parse(event.data)
const [root, type, ...splits] = data.key.split("/")
if (root !== "session") return
if (type === "info") {
setStore("info", reconcile(data.content))
return
}
if (type === "message") {
const [, messageID] = splits
setStore("messages", messageID, reconcile(data.content))
}
processDatum(JSON.parse(event.data))
} catch (error) {
console.error("Error parsing WebSocket message:", error)
}
@ -540,16 +548,15 @@ export default function Share(props: { api: string }) {
result.tokens.output += assistant.tokens.output
result.tokens.reasoning += assistant.tokens.reasoning
result.models.push([
assistant.providerID,
assistant.modelID,
])
result.models.push([assistant.providerID, assistant.modelID])
}
}
return result
})
const [showingSystemPrompt, showSystemPrompt] = createSignal(false)
console.log(data())
return (
<main class={`${styles.root} not-content`}>
<div class={styles.header}>
@ -563,9 +570,9 @@ export default function Share(props: { api: string }) {
data().created || 0,
).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
>
{DateTime.fromMillis(
data().created || 0,
).toLocaleString(DateTime.DATE_MED)}
{DateTime.fromMillis(data().created || 0).toLocaleString(
DateTime.DATE_MED,
)}
</span>
) : (
<span data-element-label data-placeholder>
@ -575,7 +582,9 @@ export default function Share(props: { api: string }) {
</div>
<p data-section="status">
<span data-status={connectionStatus()[0]}>&#9679;</span>
<span data-element-label>{getStatusText(connectionStatus())}</span>
<span data-element-label>
{getStatusText(connectionStatus())}
</span>
</p>
</div>
</div>
@ -645,11 +654,9 @@ export default function Share(props: { api: string }) {
onClick={() => showSystemPrompt((e) => !e)}
>
<span>
{
showingSystemPrompt()
? "Hide system prompt"
: "Show system prompt"
}
{showingSystemPrompt()
? "Hide system prompt"
: "Show system prompt"}
</span>
<span data-button-icon>
<Show
@ -825,27 +832,40 @@ export default function Share(props: { api: string }) {
}
>
{(part) => {
const metadata = createMemo(() =>
msg.metadata?.tool[part().toolInvocation.toolCallId]
const metadata = createMemo(
() =>
msg.metadata?.tool[
part().toolInvocation.toolCallId
],
)
const args = part().toolInvocation.args
const result = part().toolInvocation.state === "result" && part().toolInvocation.result
const result =
part().toolInvocation.state === "result" &&
part().toolInvocation.result
const matches = metadata()?.matches
const { pattern, ...rest } = args
const duration = createMemo(() =>
DateTime.fromMillis(metadata()?.time.end || 0).diff(
DateTime.fromMillis(metadata()?.time.start || 0),
).toMillis(),
DateTime.fromMillis(metadata()?.time.end || 0)
.diff(
DateTime.fromMillis(
metadata()?.time.start || 0,
),
)
.toMillis(),
)
return (
<div data-section="part" data-part-type="tool-grep">
<div
data-section="part"
data-part-type="tool-grep"
>
<div data-section="decoration">
<div title="Grep files">
<IconDocumentMagnifyingGlass
width={18} height={18}
width={18}
height={18}
/>
</div>
<div></div>
@ -873,13 +893,16 @@ export default function Share(props: { api: string }) {
<Match when={matches > 0}>
<div data-part-tool-result>
<ResultsButton
showCopy={matches === 1
? "1 match"
: `${matches} matches`
showCopy={
matches === 1
? "1 match"
: `${matches} matches`
}
hideCopy="Hide matches"
results={results()}
onClick={() => showResults((e) => !e)}
onClick={() =>
showResults((e) => !e)
}
/>
<Show when={results()}>
<TextPart
@ -919,25 +942,40 @@ export default function Share(props: { api: string }) {
}
>
{(part) => {
const metadata = createMemo(() =>
msg.metadata?.tool[part().toolInvocation.toolCallId]
const metadata = createMemo(
() =>
msg.metadata?.tool[
part().toolInvocation.toolCallId
],
)
const args = part().toolInvocation.args
const result = part().toolInvocation.state === "result" && part().toolInvocation.result
const result =
part().toolInvocation.state === "result" &&
part().toolInvocation.result
const count = metadata()?.count
const pattern = args.pattern
const duration = createMemo(() =>
DateTime.fromMillis(metadata()?.time.end || 0).diff(
DateTime.fromMillis(metadata()?.time.start || 0),
).toMillis(),
DateTime.fromMillis(metadata()?.time.end || 0)
.diff(
DateTime.fromMillis(
metadata()?.time.start || 0,
),
)
.toMillis(),
)
return (
<div data-section="part" data-part-type="tool-glob">
<div
data-section="part"
data-part-type="tool-glob"
>
<div data-section="decoration">
<div title="Glob files">
<IconMagnifyingGlass width={18} height={18} />
<IconMagnifyingGlass
width={18}
height={18}
/>
</div>
<div></div>
</div>
@ -951,12 +989,15 @@ export default function Share(props: { api: string }) {
<Match when={count > 0}>
<div data-part-tool-result>
<ResultsButton
showCopy={count === 1
? "1 result"
: `${count} results`
showCopy={
count === 1
? "1 result"
: `${count} results`
}
results={results()}
onClick={() => showResults((e) => !e)}
onClick={() =>
showResults((e) => !e)
}
/>
<Show when={results()}>
<TextPart
@ -996,23 +1037,36 @@ export default function Share(props: { api: string }) {
}
>
{(part) => {
const metadata = createMemo(() =>
msg.metadata?.tool[part().toolInvocation.toolCallId]
const metadata = createMemo(
() =>
msg.metadata?.tool[
part().toolInvocation.toolCallId
],
)
const args = part().toolInvocation.args
const path = args.path
const duration = createMemo(() =>
DateTime.fromMillis(metadata()?.time.end || 0).diff(
DateTime.fromMillis(metadata()?.time.start || 0),
).toMillis(),
DateTime.fromMillis(metadata()?.time.end || 0)
.diff(
DateTime.fromMillis(
metadata()?.time.start || 0,
),
)
.toMillis(),
)
return (
<div data-section="part" data-part-type="tool-list">
<div
data-section="part"
data-part-type="tool-list"
>
<div data-section="decoration">
<div title="List files">
<IconRectangleStack width={18} height={18} />
<IconRectangleStack
width={18}
height={18}
/>
</div>
<div></div>
</div>
@ -1026,21 +1080,25 @@ export default function Share(props: { api: string }) {
<Match
when={
part().toolInvocation.state ===
"result" &&
"result" &&
part().toolInvocation.result
}
>
<div data-part-tool-result>
<ResultsButton
results={results()}
onClick={() => showResults((e) => !e)}
onClick={() =>
showResults((e) => !e)
}
/>
<Show when={results()}>
<TextPart
expand
data-size="sm"
data-color="dimmed"
text={part().toolInvocation.result}
text={
part().toolInvocation.result
}
/>
</Show>
</div>
@ -1063,21 +1121,35 @@ export default function Share(props: { api: string }) {
}
>
{(part) => {
const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId])
const metadata = createMemo(
() =>
msg.metadata?.tool[
part().toolInvocation.toolCallId
],
)
const args = part().toolInvocation.args
const filePath = args.filePath
const hasError = metadata()?.error
const preview = metadata()?.preview
const result = part().toolInvocation.state === "result" && part().toolInvocation.result
const result =
part().toolInvocation.state === "result" &&
part().toolInvocation.result
const duration = createMemo(() =>
DateTime.fromMillis(metadata()?.time.end || 0).diff(
DateTime.fromMillis(metadata()?.time.start || 0),
).toMillis(),
DateTime.fromMillis(metadata()?.time.end || 0)
.diff(
DateTime.fromMillis(
metadata()?.time.start || 0,
),
)
.toMillis(),
)
return (
<div data-section="part" data-part-type="tool-read">
<div
data-section="part"
data-part-type="tool-read"
>
<div data-section="decoration">
<div title="Read file">
<IconDocument width={18} height={18} />
@ -1107,7 +1179,9 @@ export default function Share(props: { api: string }) {
showCopy="Show preview"
hideCopy="Hide preview"
results={results()}
onClick={() => showResults((e) => !e)}
onClick={() =>
showResults((e) => !e)
}
/>
<Show when={results()}>
<div data-part-tool-code>
@ -1123,7 +1197,9 @@ export default function Share(props: { api: string }) {
<div data-part-tool-result>
<ResultsButton
results={results()}
onClick={() => showResults((e) => !e)}
onClick={() =>
showResults((e) => !e)
}
/>
<Show when={results()}>
<TextPart
@ -1153,21 +1229,35 @@ export default function Share(props: { api: string }) {
}
>
{(part) => {
const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId])
const metadata = createMemo(
() =>
msg.metadata?.tool[
part().toolInvocation.toolCallId
],
)
const args = part().toolInvocation.args
const filePath = args.filePath
const content = args.content
const hasError = metadata()?.error
const result = part().toolInvocation.state === "result" && part().toolInvocation.result
const result =
part().toolInvocation.state === "result" &&
part().toolInvocation.result
const duration = createMemo(() =>
DateTime.fromMillis(metadata()?.time.end || 0).diff(
DateTime.fromMillis(metadata()?.time.start || 0),
).toMillis(),
DateTime.fromMillis(metadata()?.time.end || 0)
.diff(
DateTime.fromMillis(
metadata()?.time.start || 0,
),
)
.toMillis(),
)
return (
<div data-section="part" data-part-type="tool-write">
<div
data-section="part"
data-part-type="tool-write"
>
<div data-section="decoration">
<div title="Write file">
<IconDocumentPlus width={18} height={18} />
@ -1197,7 +1287,9 @@ export default function Share(props: { api: string }) {
showCopy="Show contents"
hideCopy="Hide contents"
results={results()}
onClick={() => showResults((e) => !e)}
onClick={() =>
showResults((e) => !e)
}
/>
<Show when={results()}>
<div data-part-tool-code>
@ -1227,14 +1319,23 @@ export default function Share(props: { api: string }) {
}
>
{(part) => {
const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId])
const metadata = createMemo(
() =>
msg.metadata?.tool[
part().toolInvocation.toolCallId
],
)
const args = part().toolInvocation.args
const filePath = args.filePath
const duration = createMemo(() =>
DateTime.fromMillis(metadata()?.time.end || 0).diff(
DateTime.fromMillis(metadata()?.time.start || 0),
).toMillis(),
DateTime.fromMillis(metadata()?.time.end || 0)
.diff(
DateTime.fromMillis(
metadata()?.time.start || 0,
),
)
.toMillis(),
)
return (
@ -1278,17 +1379,29 @@ export default function Share(props: { api: string }) {
}
>
{(part) => {
const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId])
const metadata = createMemo(
() =>
msg.metadata?.tool[
part().toolInvocation.toolCallId
],
)
const command = part().toolInvocation.args.command
const desc = part().toolInvocation.args.description
const stdout = metadata()?.stdout
const result = stdout || (part().toolInvocation.state === "result" && part().toolInvocation.result)
const result =
stdout ||
(part().toolInvocation.state === "result" &&
part().toolInvocation.result)
const duration = createMemo(() =>
DateTime.fromMillis(metadata()?.time.end || 0).diff(
DateTime.fromMillis(metadata()?.time.start || 0),
).toMillis(),
DateTime.fromMillis(metadata()?.time.end || 0)
.diff(
DateTime.fromMillis(
metadata()?.time.start || 0,
),
)
.toMillis(),
)
return (
@ -1307,7 +1420,9 @@ export default function Share(props: { api: string }) {
<TerminalPart
desc={desc}
data-size="sm"
text={command + (result ? `\n${result}` : "")}
text={
command + (result ? `\n${result}` : "")
}
/>
</div>
<ToolFooter time={duration()} />
@ -1321,17 +1436,27 @@ export default function Share(props: { api: string }) {
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "opencode_todoread" &&
part.toolInvocation.toolName ===
"opencode_todoread" &&
part
}
>
{(part) => {
const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId])
const metadata = createMemo(
() =>
msg.metadata?.tool[
part().toolInvocation.toolCallId
],
)
const duration = createMemo(() =>
DateTime.fromMillis(metadata()?.time.end || 0).diff(
DateTime.fromMillis(metadata()?.time.start || 0),
).toMillis(),
DateTime.fromMillis(metadata()?.time.end || 0)
.diff(
DateTime.fromMillis(
metadata()?.time.start || 0,
),
)
.toMillis(),
)
return (
@ -1362,24 +1487,39 @@ export default function Share(props: { api: string }) {
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "opencode_todowrite" &&
part.toolInvocation.toolName ===
"opencode_todowrite" &&
part
}
>
{(part) => {
const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId])
const todos = createMemo(() => sortTodosByStatus(
part().toolInvocation.args.todos
))
const starting = todos().every(t => t.status === "pending")
const finished = todos().every(t => t.status === "completed")
const metadata = createMemo(
() =>
msg.metadata?.tool[
part().toolInvocation.toolCallId
],
)
const todos = createMemo(() =>
sortTodosByStatus(
part().toolInvocation.args.todos,
),
)
const starting = todos().every(
(t) => t.status === "pending",
)
const finished = todos().every(
(t) => t.status === "completed",
)
const duration = createMemo(() =>
DateTime.fromMillis(metadata()?.time.end || 0).diff(
DateTime.fromMillis(metadata()?.time.start || 0),
).toMillis(),
DateTime.fromMillis(metadata()?.time.end || 0)
.diff(
DateTime.fromMillis(
metadata()?.time.start || 0,
),
)
.toMillis(),
)
return (
@ -1408,12 +1548,12 @@ export default function Share(props: { api: string }) {
<Show when={todos().length > 0}>
<ul class={styles.todos}>
<For each={todos()}>
{({ status, content }) =>
{({ status, content }) => (
<li data-status={status}>
<span></span>
{content}
</li>
}
)}
</For>
</ul>
</Show>
@ -1429,26 +1569,41 @@ export default function Share(props: { api: string }) {
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "opencode_webfetch" &&
part.toolInvocation.toolName ===
"opencode_webfetch" &&
part
}
>
{(part) => {
const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId])
const metadata = createMemo(
() =>
msg.metadata?.tool[
part().toolInvocation.toolCallId
],
)
const args = part().toolInvocation.args
const url = args.url
const format = args.format
const hasError = metadata()?.error
const result = part().toolInvocation.state === "result" && part().toolInvocation.result
const result =
part().toolInvocation.state === "result" &&
part().toolInvocation.result
const duration = createMemo(() =>
DateTime.fromMillis(metadata()?.time.end || 0).diff(
DateTime.fromMillis(metadata()?.time.start || 0),
).toMillis(),
DateTime.fromMillis(metadata()?.time.end || 0)
.diff(
DateTime.fromMillis(
metadata()?.time.start || 0,
),
)
.toMillis(),
)
return (
<div data-section="part" data-part-type="tool-fetch">
<div
data-section="part"
data-part-type="tool-fetch"
>
<div data-section="decoration">
<div title="Web fetch">
<IconGlobeAlt width={18} height={18} />
@ -1476,7 +1631,9 @@ export default function Share(props: { api: string }) {
<div data-part-tool-result>
<ResultsButton
results={results()}
onClick={() => showResults((e) => !e)}
onClick={() =>
showResults((e) => !e)
}
/>
<Show when={results()}>
<div data-part-tool-code>
@ -1505,12 +1662,21 @@ export default function Share(props: { api: string }) {
}
>
{(part) => {
const metadata = createMemo(() => msg.metadata?.tool[part().toolInvocation.toolCallId])
const metadata = createMemo(
() =>
msg.metadata?.tool[
part().toolInvocation.toolCallId
],
)
const duration = createMemo(() =>
DateTime.fromMillis(metadata()?.time.end || 0).diff(
DateTime.fromMillis(metadata()?.time.start || 0),
).toMillis(),
DateTime.fromMillis(metadata()?.time.end || 0)
.diff(
DateTime.fromMillis(
metadata()?.time.start || 0,
),
)
.toMillis(),
)
return (
@ -1551,21 +1717,25 @@ export default function Share(props: { api: string }) {
<Match
when={
part().toolInvocation.state ===
"result" &&
"result" &&
part().toolInvocation.result
}
>
<div data-part-tool-result>
<ResultsButton
results={results()}
onClick={() => showResults((e) => !e)}
onClick={() =>
showResults((e) => !e)
}
/>
<Show when={results()}>
<TextPart
expand
data-size="sm"
data-color="dimmed"
text={part().toolInvocation.result}
text={
part().toolInvocation.result
}
/>
</Show>
</div>

View file

@ -4,6 +4,12 @@ import config from "virtual:starlight/user-config";
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
import Share from "../../components/Share.tsx";
const apiUrl = import.meta.env.VITE_API_URL;
const id = Astro.url.searchParams.get('id')
const res = await fetch(`${apiUrl}/share_messages?id=${id}`);
const data = await res.json();
---
<StarlightPage
@ -15,7 +21,7 @@ import Share from "../../components/Share.tsx";
tableOfContents: false,
}}
>
<Share api={import.meta.env.VITE_API_URL} client:only="solid" />
<Share api={apiUrl} data={data.messages} client:only="solid" />
</StarlightPage>
<style is:global>