Download packages/app/src/pages/session/timeline/rows.ts from SaylorTwift/opencode: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/app/src/pages/session/timeline/rows.ts
- Command line
-
hf download hf://SaylorTwift/opencode/packages/app/src/pages/session/timeline/rows.ts
-
curl -L -o rows.ts https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/app/src/pages/session/timeline/rows.ts
11 kB
| import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note" | |
| import type { SessionMessageInfo } from "@opencode-ai/client/promise" | |
| import { AssistantMessage, Part, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2" | |
| import { groupParts, renderable, type PartGroup } from "@opencode-ai/session-ui/message-part" | |
| import { TimelineRow, type SummaryDiff } from "./timeline-row" | |
| import { uniqueSummaryDiffs } from "./summary-diffs" | |
| import { compareMessages } from "@/utils/session-message" | |
| export { TimelineRow, type SummaryDiff } from "./timeline-row" | |
| export type TimelineRowMap = { | |
| TurnGap: { userMessageID: string } | |
| CommentStrip: { | |
| userMessageID: string | |
| } | |
| UserMessage: { | |
| userMessageID: string | |
| anchor: boolean | |
| } | |
| TurnDivider: { | |
| userMessageID: string | |
| label: "compaction" | "interrupted" | |
| } | |
| AssistantPart: { | |
| userMessageID: string | |
| group: PartGroup | |
| previousAssistantPart: boolean | |
| } | |
| Thinking: { userMessageID: string; reasoningHeading?: string } | |
| Retry: { userMessageID: string } | |
| DiffSummary: { userMessageID: string; diffs: SummaryDiff[] } | |
| Error: { userMessageID: string; text: string } | |
| } | |
| export namespace Timeline { | |
| export function constructSessionMessageRows( | |
| messages: SessionMessageInfo[], | |
| getMessage: (messageID: string) => UserMessage | AssistantMessage | undefined, | |
| getMessageParts: (messageID: string) => Part[], | |
| showReasoning: boolean, | |
| status: SessionStatus["type"], | |
| inlineComments: boolean, | |
| projectedUserMessages: UserMessage[], | |
| ) { | |
| const turns: { user: UserMessage; assistants: AssistantMessage[] }[] = [] | |
| const turnByUserID = new Map<string, (typeof turns)[number]>() | |
| messages.forEach((message) => { | |
| const projected = getMessage(message.id) | |
| if (message.type === "shell" && projected?.role === "user") { | |
| const assistant = getMessage(`${message.id}:assistant`) | |
| const turn = { user: projected, assistants: assistant?.role === "assistant" ? [assistant] : [] } | |
| turns.push(turn) | |
| turnByUserID.set(projected.id, turn) | |
| return | |
| } | |
| if (projected?.role === "user") { | |
| if (turnByUserID.has(projected.id)) return | |
| const turn = { user: projected, assistants: [] } | |
| turns.push(turn) | |
| turnByUserID.set(projected.id, turn) | |
| return | |
| } | |
| if (projected?.role !== "assistant") return | |
| const existing = turnByUserID.get(projected.parentID) | |
| if (existing) { | |
| existing.assistants.push(projected) | |
| return | |
| } | |
| const user = getMessage(projected.parentID) | |
| if (user?.role !== "user") return | |
| const turn = { user, assistants: [projected] } | |
| turns.push(turn) | |
| turnByUserID.set(user.id, turn) | |
| }) | |
| projectedUserMessages.forEach((user) => { | |
| if (turnByUserID.has(user.id)) return | |
| const turn = { user, assistants: [] } | |
| const index = turns.findIndex((item) => compareMessages(user, item.user) < 0) | |
| if (index < 0) turns.push(turn) | |
| if (index >= 0) turns.splice(index, 0, turn) | |
| turnByUserID.set(user.id, turn) | |
| }) | |
| const activeMessageID = turns.at(-1)?.user.id | |
| return { | |
| activeMessageID, | |
| rows: turns.flatMap((turn, index) => | |
| constructMessageRows( | |
| turn.user, | |
| getMessageParts, | |
| turn.assistants, | |
| index, | |
| showReasoning, | |
| status, | |
| turn.user.id === activeMessageID, | |
| inlineComments, | |
| ), | |
| ), | |
| } | |
| } | |
| export function constructMessageRows( | |
| userMessage: UserMessage, | |
| getMessageParts: (messageID: string) => Part[], | |
| assistantMessages: AssistantMessage[], | |
| index: number, | |
| showReasoning: boolean, | |
| status: SessionStatus["type"], | |
| isActive: boolean, | |
| // v2 renders comments inside the user message attachments row instead of a strip row | |
| inlineComments: boolean, | |
| ) { | |
| const rows: TimelineRow.TimelineRow[] = [] | |
| const previousUserMessage = index > 0 | |
| const userParts = getMessageParts(userMessage.id) | |
| const comments = userParts.flatMap((p) => MessageComment.fromPart(p) ?? []) | |
| const compaction = userParts.some((p) => p.type === "compaction") | |
| const interruptedMessageIndex = assistantMessages.findIndex((m) => m.error?.name === "MessageAbortedError") | |
| const interrupted = interruptedMessageIndex !== -1 | |
| const latestError = assistantMessages.at(-1)?.error | |
| const error = latestError?.name === "MessageAbortedError" ? undefined : latestError | |
| const assistantPartRefs = assistantMessages.flatMap((message, messageIndex) => | |
| getMessageParts(message.id) | |
| .filter((part) => renderable(part, showReasoning)) | |
| .map((part) => ({ messageID: message.id, messageIndex, part })), | |
| ) | |
| const assistantItems = | |
| interrupted && !compaction | |
| ? [ | |
| ...groupParts(assistantPartRefs.filter((ref) => ref.messageIndex <= interruptedMessageIndex)).map( | |
| (group) => ({ | |
| type: "part" as const, | |
| group, | |
| }), | |
| ), | |
| { type: "interrupted" as const }, | |
| ...groupParts(assistantPartRefs.filter((ref) => ref.messageIndex > interruptedMessageIndex)).map( | |
| (group) => ({ | |
| type: "part" as const, | |
| group, | |
| }), | |
| ), | |
| ] | |
| : groupParts(assistantPartRefs).map((group) => ({ type: "part" as const, group })) | |
| if (previousUserMessage) rows.push(new TimelineRow.TurnGap({ userMessageID: userMessage.id })) | |
| if (comments.length > 0 && !inlineComments) | |
| rows.push( | |
| new TimelineRow.CommentStrip({ | |
| userMessageID: userMessage.id, | |
| }), | |
| ) | |
| rows.push( | |
| new TimelineRow.UserMessage({ | |
| userMessageID: userMessage.id, | |
| anchor: inlineComments || comments.length === 0, | |
| }), | |
| ) | |
| if (compaction) { | |
| rows.push( | |
| new TimelineRow.TurnDivider({ | |
| userMessageID: userMessage.id, | |
| label: "compaction", | |
| }), | |
| ) | |
| } | |
| let assistantGroupIndex = 0 | |
| assistantItems.forEach((item) => { | |
| if (item.type === "interrupted") { | |
| rows.push( | |
| new TimelineRow.TurnDivider({ | |
| userMessageID: userMessage.id, | |
| label: "interrupted", | |
| }), | |
| ) | |
| return | |
| } | |
| rows.push( | |
| new TimelineRow.AssistantPart({ | |
| userMessageID: userMessage.id, | |
| group: item.group, | |
| previousAssistantPart: assistantGroupIndex > 0, | |
| }), | |
| ) | |
| assistantGroupIndex += 1 | |
| }) | |
| if (isActive && status === "busy" && !error && (showReasoning ? assistantPartRefs.length === 0 : true)) { | |
| const heading = assistantMessages | |
| .flatMap((message) => getMessageParts(message.id)) | |
| .map((part) => (part.type === "reasoning" && part.text ? reasoningHeading(part.text) : undefined)) | |
| .find((value): value is string => !!value) | |
| rows.push( | |
| new TimelineRow.Thinking({ | |
| userMessageID: userMessage.id, | |
| reasoningHeading: heading, | |
| }), | |
| ) | |
| } | |
| if (isActive && status === "retry") rows.push(new TimelineRow.Retry({ userMessageID: userMessage.id })) | |
| const diffs = uniqueSummaryDiffs(userMessage.summary?.diffs) | |
| if (diffs.length > 0 && (status === "idle" || !isActive)) { | |
| rows.push( | |
| new TimelineRow.DiffSummary({ | |
| userMessageID: userMessage.id, | |
| diffs, | |
| }), | |
| ) | |
| } | |
| if (error) { | |
| const data = error.data?.message | |
| rows.push( | |
| new TimelineRow.Error({ | |
| userMessageID: userMessage.id, | |
| text: unwrapErrorMessage( | |
| typeof data === "string" ? data : data === undefined || data === null ? "" : String(data), | |
| ), | |
| }), | |
| ) | |
| } | |
| return rows | |
| } | |
| function reasoningHeading(text: string) { | |
| const markdown = text.replace(/\r\n?/g, "\n") | |
| const html = markdown.match(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/i) | |
| if (html?.[1]) { | |
| const value = cleanHeading(html[1].replace(/<[^>]+>/g, " ")) | |
| if (value) return value | |
| } | |
| const atx = markdown.match(/^\s{0,3}#{1,6}[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/m) | |
| if (atx?.[1]) { | |
| const value = cleanHeading(atx[1]) | |
| if (value) return value | |
| } | |
| const setext = markdown.match(/^([^\n]+)\n(?:=+|-+)\s*$/m) | |
| if (setext?.[1]) { | |
| const value = cleanHeading(setext[1]) | |
| if (value) return value | |
| } | |
| const strong = markdown.match(/^\s*(?:\*\*|__)(.+?)(?:\*\*|__)\s*$/m) | |
| if (strong?.[1]) { | |
| const value = cleanHeading(strong[1]) | |
| if (value) return value | |
| } | |
| } | |
| function cleanHeading(value: string) { | |
| return value | |
| .replace(/`([^`]+)`/g, "$1") | |
| .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") | |
| .replace(/[*_~]+/g, "") | |
| .trim() | |
| } | |
| function unwrapErrorMessage(message: string) { | |
| const text = message.replace(/^Error:\s*/, "").trim() | |
| const parse = (value: string) => { | |
| try { | |
| return JSON.parse(value) as unknown | |
| } catch { | |
| return undefined | |
| } | |
| } | |
| const read = (value: string) => { | |
| const first = parse(value) | |
| if (typeof first !== "string") return first | |
| return parse(first.trim()) | |
| } | |
| let json = read(text) | |
| if (json === undefined) { | |
| const start = text.indexOf("{") | |
| const end = text.lastIndexOf("}") | |
| if (start !== -1 && end > start) json = read(text.slice(start, end + 1)) | |
| } | |
| if (!record(json)) return message | |
| const err = record(json.error) ? json.error : undefined | |
| if (err) { | |
| const type = typeof err.type === "string" ? err.type : undefined | |
| const msg = typeof err.message === "string" ? err.message : undefined | |
| if (type && msg) return `${type}: ${msg}` | |
| if (msg) return msg | |
| if (type) return type | |
| const code = typeof err.code === "string" ? err.code : undefined | |
| if (code) return code | |
| } | |
| const msg = typeof json.message === "string" ? json.message : undefined | |
| if (msg) return msg | |
| const reason = typeof json.error === "string" ? json.error : undefined | |
| if (reason) return reason | |
| return message | |
| } | |
| function record(value: unknown): value is Record<string, unknown> { | |
| return !!value && typeof value === "object" && !Array.isArray(value) | |
| } | |
| } | |
| export namespace MessageComment { | |
| export type MessageComment = { | |
| path: string | |
| comment: string | |
| selection?: { | |
| startLine: number | |
| endLine: number | |
| } | |
| } | |
| export const fromPart = (part: Part): MessageComment | undefined => { | |
| if (part.type !== "text" || !part.synthetic) return | |
| const next = readCommentMetadata(part.metadata) ?? parseCommentNote(part.text) | |
| if (!next) return | |
| return { | |
| path: next.path, | |
| comment: next.comment, | |
| selection: next.selection | |
| ? { | |
| startLine: next.selection.startLine, | |
| endLine: next.selection.endLine, | |
| } | |
| : undefined, | |
| } | |
| } | |
| } | |