|
|
@@ -1,14 +1,14 @@
|
|
|
import { createResource, Show, createMemo, createSignal, onMount, type Accessor, type JSX } from "solid-js"
|
|
|
-import { TextAttributes, type RGBA } from "@opentui/core"
|
|
|
+import { TextAttributes } from "@opentui/core"
|
|
|
import { useTerminalDimensions } from "@opentui/solid"
|
|
|
import { debounce, leadingAndTrailing } from "@solid-primitives/scheduled"
|
|
|
-import type { Message, Part, Session as SdkSession, SnapshotFileDiff } from "@opencode-ai/sdk/v2"
|
|
|
+import type { Message, Part, Session as SdkSession } from "@opencode-ai/sdk/v2"
|
|
|
import { useTheme } from "@tui/context/theme"
|
|
|
import { useSDK } from "@tui/context/sdk"
|
|
|
import { useSync } from "@tui/context/sync"
|
|
|
import { Locale } from "@/util/locale"
|
|
|
import { Spinner } from "@tui/component/spinner"
|
|
|
-import { extractMessageMarkdown, extractMessageText, formatDiffSummary, relativeTime, shortModelLabel } from "./util"
|
|
|
+import { extractMessageMarkdown, extractMessageText, relativeTime } from "./util"
|
|
|
|
|
|
type WithParts = { info: Message; parts: Part[] }
|
|
|
|
|
|
@@ -16,7 +16,6 @@ type Sdk = ReturnType<typeof useSDK>
|
|
|
type Sync = ReturnType<typeof useSync>
|
|
|
|
|
|
const messageCache = new Map<string, Promise<WithParts[]>>()
|
|
|
-const diffCache = new Map<string, Promise<SnapshotFileDiff[]>>()
|
|
|
|
|
|
function cacheKey(sessionID: string, version: number) {
|
|
|
return `${sessionID}:${version}`
|
|
|
@@ -36,41 +35,21 @@ function loadMessages(sdk: Sdk, sessionID: string, version: number): Promise<Wit
|
|
|
const promise = sdk.client.session
|
|
|
.messages({ sessionID, limit: 50 })
|
|
|
.then((res) => {
|
|
|
- if (res.error) messageCache.delete(key)
|
|
|
+ if (res.error) throw res.error
|
|
|
return (res.data as WithParts[] | undefined) ?? []
|
|
|
})
|
|
|
- .catch(() => {
|
|
|
+ .catch((error) => {
|
|
|
messageCache.delete(key)
|
|
|
- return [] as WithParts[]
|
|
|
+ throw error
|
|
|
})
|
|
|
messageCache.set(key, promise)
|
|
|
return promise
|
|
|
}
|
|
|
|
|
|
-function loadDiff(sdk: Sdk, sessionID: string, version: number): Promise<SnapshotFileDiff[]> {
|
|
|
- const key = cacheKey(sessionID, version)
|
|
|
- const cached = diffCache.get(key)
|
|
|
- if (cached) return cached
|
|
|
-
|
|
|
- const promise = sdk.client.session
|
|
|
- .diff({ sessionID })
|
|
|
- .then((res) => {
|
|
|
- if (res.error) diffCache.delete(key)
|
|
|
- return (res.data as SnapshotFileDiff[] | undefined) ?? []
|
|
|
- })
|
|
|
- .catch(() => {
|
|
|
- diffCache.delete(key)
|
|
|
- return [] as SnapshotFileDiff[]
|
|
|
- })
|
|
|
- diffCache.set(key, promise)
|
|
|
- return promise
|
|
|
-}
|
|
|
-
|
|
|
export function prefetchPreviews(sdk: Sdk, sync: Sync, sessionIDs: readonly string[]) {
|
|
|
for (const id of sessionIDs) {
|
|
|
const version = sync.data.session.find((session) => session.id === id)?.time.updated ?? 0
|
|
|
if (!hydrateFromSync(sync, id)) loadMessages(sdk, id, version).catch(() => {})
|
|
|
- if (!sync.data.session_diff[id]?.length) loadDiff(sdk, id, version).catch(() => {})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -121,13 +100,6 @@ export function SessionPreviewPane(props: {
|
|
|
return hydrateFromSync(sync, id)
|
|
|
})
|
|
|
|
|
|
- const syncedDiff = createMemo(() => {
|
|
|
- const id = props.sessionID()
|
|
|
- if (!id) return undefined
|
|
|
- const diff = sync.data.session_diff[id]
|
|
|
- return diff && diff.length > 0 ? (diff as SnapshotFileDiff[]) : undefined
|
|
|
- })
|
|
|
-
|
|
|
const [fetchedMessages] = createResource(
|
|
|
() => {
|
|
|
const id = props.sessionID()
|
|
|
@@ -137,31 +109,7 @@ export function SessionPreviewPane(props: {
|
|
|
async (input) => loadMessages(sdk, input.sessionID, input.version),
|
|
|
)
|
|
|
|
|
|
- const [fetchedDiff] = createResource(
|
|
|
- () => {
|
|
|
- const id = props.sessionID()
|
|
|
- if (!id || syncedDiff()) return undefined
|
|
|
- return { sessionID: id, version: session()?.time.updated ?? 0 }
|
|
|
- },
|
|
|
- async (input) => loadDiff(sdk, input.sessionID, input.version),
|
|
|
- )
|
|
|
-
|
|
|
const messages = createMemo(() => syncedMessages() ?? fetchedMessages() ?? [])
|
|
|
- const diff = createMemo(() => syncedDiff() ?? fetchedDiff() ?? [])
|
|
|
-
|
|
|
- const diffSummary = createMemo(() => {
|
|
|
- const live = diff()
|
|
|
- if (live && live.length > 0) {
|
|
|
- let additions = 0
|
|
|
- let deletions = 0
|
|
|
- for (const file of live) {
|
|
|
- additions += file.additions ?? 0
|
|
|
- deletions += file.deletions ?? 0
|
|
|
- }
|
|
|
- return formatDiffSummary({ additions, deletions, files: live.length })
|
|
|
- }
|
|
|
- return formatDiffSummary(session()?.summary)
|
|
|
- })
|
|
|
|
|
|
const exchange = createMemo(() => {
|
|
|
const items = messages()
|
|
|
@@ -174,13 +122,13 @@ export function SessionPreviewPane(props: {
|
|
|
return { user, assistant }
|
|
|
})
|
|
|
|
|
|
- const loading = createMemo(() => (fetchedMessages.loading || fetchedDiff.loading) && !exchange())
|
|
|
+ const loading = createMemo(() => fetchedMessages.loading && !exchange())
|
|
|
|
|
|
const statusLabel = createMemo(() => {
|
|
|
const s = status()
|
|
|
- if (s === "busy") return { text: "working", color: theme.warning }
|
|
|
- if (s === "retry") return { text: "retrying", color: theme.warning }
|
|
|
- return { text: "idle", color: theme.textMuted }
|
|
|
+ if (s === "busy") return "working"
|
|
|
+ if (s === "retry") return "retrying"
|
|
|
+ return "idle"
|
|
|
})
|
|
|
|
|
|
return (
|
|
|
@@ -191,7 +139,7 @@ export function SessionPreviewPane(props: {
|
|
|
paddingTop={1}
|
|
|
paddingBottom={1}
|
|
|
gap={1}
|
|
|
- maxHeight={maxHeight()}
|
|
|
+ height={maxHeight()}
|
|
|
overflow="hidden"
|
|
|
>
|
|
|
<Show
|
|
|
@@ -204,7 +152,7 @@ export function SessionPreviewPane(props: {
|
|
|
>
|
|
|
{(s) => (
|
|
|
<>
|
|
|
- <Header session={s()} statusLabel={statusLabel()} diff={diffSummary()} />
|
|
|
+ <Header session={s()} statusLabel={statusLabel()} />
|
|
|
<Show when={loading()}>
|
|
|
<Spinner>loading preview...</Spinner>
|
|
|
</Show>
|
|
|
@@ -213,7 +161,7 @@ export function SessionPreviewPane(props: {
|
|
|
fallback={
|
|
|
<Show when={!loading()}>
|
|
|
<text fg={theme.textMuted} wrapMode="word">
|
|
|
- No messages yet
|
|
|
+ {fetchedMessages.error ? "Preview unavailable" : "No messages yet"}
|
|
|
</text>
|
|
|
</Show>
|
|
|
}
|
|
|
@@ -241,24 +189,12 @@ function messageParentID(item: WithParts) {
|
|
|
|
|
|
const ROW_WIDTH = 40
|
|
|
|
|
|
-function Header(props: {
|
|
|
- session: SdkSession
|
|
|
- statusLabel: { text: string; color: RGBA }
|
|
|
- diff: { additions: number; deletions: number; files: number } | undefined
|
|
|
-}) {
|
|
|
+function Header(props: { session: SdkSession; statusLabel: string }) {
|
|
|
const { theme } = useTheme()
|
|
|
const title = createMemo(() => Locale.truncate(props.session.title, ROW_WIDTH))
|
|
|
- const modelAgent = createMemo(() => {
|
|
|
- const m = shortModelLabel(props.session.model)
|
|
|
- const a = props.session.agent ?? ""
|
|
|
- if (m && a) return Locale.truncate(`${m} · ${a}`, ROW_WIDTH)
|
|
|
- if (m) return Locale.truncate(m, ROW_WIDTH)
|
|
|
- if (a) return Locale.truncate(a, ROW_WIDTH)
|
|
|
- return ""
|
|
|
- })
|
|
|
const statusRest = createMemo(() => {
|
|
|
const joined = ` · ${relativeTime(props.session.time.updated)}`
|
|
|
- return Locale.truncate(joined, Math.max(0, ROW_WIDTH - props.statusLabel.text.length))
|
|
|
+ return Locale.truncate(joined, Math.max(0, ROW_WIDTH - props.statusLabel.length))
|
|
|
})
|
|
|
|
|
|
return (
|
|
|
@@ -268,20 +204,12 @@ function Header(props: {
|
|
|
{title()}
|
|
|
</text>
|
|
|
</Row>
|
|
|
- <Show when={modelAgent()}>
|
|
|
- <Row height={1}>
|
|
|
- <text fg={theme.text} wrapMode="none" overflow="hidden">
|
|
|
- {modelAgent()}
|
|
|
- </text>
|
|
|
- </Row>
|
|
|
- </Show>
|
|
|
<Row height={1}>
|
|
|
<text fg={theme.textMuted} wrapMode="none" overflow="hidden">
|
|
|
- <span style={{ fg: props.statusLabel.color }}>{props.statusLabel.text}</span>
|
|
|
+ <span>{props.statusLabel}</span>
|
|
|
<span>{statusRest()}</span>
|
|
|
</text>
|
|
|
</Row>
|
|
|
- <Show when={props.diff}>{(d) => <DiffRow diff={d()} />}</Show>
|
|
|
</box>
|
|
|
)
|
|
|
}
|
|
|
@@ -294,28 +222,6 @@ function Row(props: { height: number; children: JSX.Element }) {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function DiffRow(props: { diff: { additions: number; deletions: number; files: number } }) {
|
|
|
- const { theme } = useTheme()
|
|
|
- const showAdds = () => props.diff.additions > 0
|
|
|
- const showDels = () => props.diff.deletions > 0
|
|
|
- if (!showAdds() && !showDels()) return null
|
|
|
- return (
|
|
|
- <Row height={1}>
|
|
|
- <text wrapMode="none" overflow="hidden">
|
|
|
- <Show when={showAdds()}>
|
|
|
- <span style={{ fg: theme.diffAdded }}>+{props.diff.additions}</span>
|
|
|
- </Show>
|
|
|
- <Show when={showAdds() && showDels()}>
|
|
|
- <span> </span>
|
|
|
- </Show>
|
|
|
- <Show when={showDels()}>
|
|
|
- <span style={{ fg: theme.diffRemoved }}>−{props.diff.deletions}</span>
|
|
|
- </Show>
|
|
|
- </text>
|
|
|
- </Row>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
const PROMPT_MAX_CHARS = 240
|
|
|
const REPLY_MAX_LINES = 12
|
|
|
const REPLY_MAX_CHARS = 800
|