mirror of
https://github.com/sst/opencode.git
synced 2025-07-23 15:55:03 +00:00
message rendering performance improvements
This commit is contained in:
parent
6470243095
commit
c952e9ae3d
5 changed files with 334 additions and 264 deletions
|
@ -17,6 +17,9 @@ import { DebugCommand } from "./cli/cmd/debug"
|
||||||
import { StatsCommand } from "./cli/cmd/stats"
|
import { StatsCommand } from "./cli/cmd/stats"
|
||||||
import { McpCommand } from "./cli/cmd/mcp"
|
import { McpCommand } from "./cli/cmd/mcp"
|
||||||
import { InstallGithubCommand } from "./cli/cmd/install-github"
|
import { InstallGithubCommand } from "./cli/cmd/install-github"
|
||||||
|
import { Trace } from "./trace"
|
||||||
|
|
||||||
|
Trace.init()
|
||||||
|
|
||||||
const cancel = new AbortController()
|
const cancel = new AbortController()
|
||||||
|
|
||||||
|
@ -42,7 +45,7 @@ const cli = yargs(hideBin(process.argv))
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
})
|
})
|
||||||
.middleware(async () => {
|
.middleware(async () => {
|
||||||
await Log.init({ print: process.argv.includes("--print-logs") })
|
await Log.init({ print: process.argv.includes("--print-logs"), dev: Installation.isDev() })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { Config } = await import("./config/config")
|
const { Config } = await import("./config/config")
|
||||||
|
|
53
packages/opencode/src/trace/index.ts
Normal file
53
packages/opencode/src/trace/index.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { Global } from "../global"
|
||||||
|
import { Installation } from "../installation"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
export namespace Trace {
|
||||||
|
export function init() {
|
||||||
|
if (!Installation.isDev()) return
|
||||||
|
const writer = Bun.file(path.join(Global.Path.data, "log", "fetch.log")).writer()
|
||||||
|
|
||||||
|
const originalFetch = globalThis.fetch
|
||||||
|
// @ts-expect-error
|
||||||
|
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
|
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url
|
||||||
|
const method = init?.method || "GET"
|
||||||
|
|
||||||
|
const urlObj = new URL(url)
|
||||||
|
|
||||||
|
writer.write(`\n${method} ${urlObj.pathname}${urlObj.search} HTTP/1.1\n`)
|
||||||
|
writer.write(`Host: ${urlObj.host}\n`)
|
||||||
|
|
||||||
|
if (init?.headers) {
|
||||||
|
if (init.headers instanceof Headers) {
|
||||||
|
init.headers.forEach((value, key) => {
|
||||||
|
writer.write(`${key}: ${value}\n`)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
for (const [key, value] of Object.entries(init.headers)) {
|
||||||
|
writer.write(`${key}: ${value}\n`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (init?.body) {
|
||||||
|
writer.write(`\n${init.body}`)
|
||||||
|
}
|
||||||
|
writer.flush()
|
||||||
|
const response = await originalFetch(input, init)
|
||||||
|
const clonedResponse = response.clone()
|
||||||
|
writer.write(`\nHTTP/1.1 ${response.status} ${response.statusText}\n`)
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
writer.write(`${key}: ${value}\n`)
|
||||||
|
})
|
||||||
|
if (clonedResponse.body) {
|
||||||
|
clonedResponse.text().then(async (x) => {
|
||||||
|
writer.write(`\n${x}\n`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
writer.flush()
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,6 +50,7 @@ export namespace Log {
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
print: boolean
|
print: boolean
|
||||||
|
dev?: boolean
|
||||||
level?: Level
|
level?: Level
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +64,10 @@ export namespace Log {
|
||||||
await fs.mkdir(dir, { recursive: true })
|
await fs.mkdir(dir, { recursive: true })
|
||||||
cleanup(dir)
|
cleanup(dir)
|
||||||
if (options.print) return
|
if (options.print) return
|
||||||
logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log")
|
logpath = path.join(
|
||||||
|
dir,
|
||||||
|
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
|
||||||
|
)
|
||||||
const logfile = Bun.file(logpath)
|
const logfile = Bun.file(logpath)
|
||||||
await fs.truncate(logpath).catch(() => {})
|
await fs.truncate(logpath).catch(() => {})
|
||||||
const writer = logfile.writer()
|
const writer = logfile.writer()
|
||||||
|
@ -75,15 +79,16 @@ export namespace Log {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanup(dir: string) {
|
async function cleanup(dir: string) {
|
||||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
const glob = new Bun.Glob("????-??-??T??????.log")
|
||||||
const files = entries
|
const files = await Array.fromAsync(
|
||||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".log"))
|
glob.scan({
|
||||||
.map((entry) => path.join(dir, entry.name))
|
cwd: dir,
|
||||||
|
absolute: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
if (files.length <= 5) return
|
if (files.length <= 5) return
|
||||||
|
|
||||||
const filesToDelete = files.slice(0, -10)
|
const filesToDelete = files.slice(0, -10)
|
||||||
|
|
||||||
await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {})))
|
await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,9 +68,6 @@ type SendMsg struct {
|
||||||
type SetEditorContentMsg struct {
|
type SetEditorContentMsg struct {
|
||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
type OptimisticMessageAddedMsg struct {
|
|
||||||
Message opencode.MessageUnion
|
|
||||||
}
|
|
||||||
type FileRenderedMsg struct {
|
type FileRenderedMsg struct {
|
||||||
FilePath string
|
FilePath string
|
||||||
}
|
}
|
||||||
|
@ -508,7 +505,6 @@ func (a *App) SendChatMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
a.Messages = append(a.Messages, Message{Info: message, Parts: parts})
|
a.Messages = append(a.Messages, Message{Info: message, Parts: parts})
|
||||||
cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: message}))
|
|
||||||
|
|
||||||
cmds = append(cmds, func() tea.Msg {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
partsParam := []opencode.SessionChatParamsPartUnion{}
|
partsParam := []opencode.SessionChatParamsPartUnion{}
|
||||||
|
|
|
@ -36,13 +36,14 @@ type messagesComponent struct {
|
||||||
header string
|
header string
|
||||||
viewport viewport.Model
|
viewport viewport.Model
|
||||||
cache *PartCache
|
cache *PartCache
|
||||||
rendering bool
|
loading bool
|
||||||
showToolDetails bool
|
showToolDetails bool
|
||||||
|
rendering bool
|
||||||
|
dirty bool
|
||||||
tail bool
|
tail bool
|
||||||
partCount int
|
partCount int
|
||||||
lineCount int
|
lineCount int
|
||||||
}
|
}
|
||||||
type renderFinishedMsg struct{}
|
|
||||||
|
|
||||||
type ToggleToolDetailsMsg struct{}
|
type ToggleToolDetailsMsg struct{}
|
||||||
|
|
||||||
|
@ -62,34 +63,24 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.width = effectiveWidth
|
m.width = effectiveWidth
|
||||||
m.height = msg.Height - 7
|
m.height = msg.Height - 7
|
||||||
m.viewport.SetWidth(m.width)
|
m.viewport.SetWidth(m.width)
|
||||||
m.header = m.renderHeader()
|
m.loading = true
|
||||||
return m, m.Reload()
|
return m, m.Reload()
|
||||||
case app.SendMsg:
|
case app.SendMsg:
|
||||||
m.viewport.GotoBottom()
|
m.viewport.GotoBottom()
|
||||||
m.tail = true
|
m.tail = true
|
||||||
return m, nil
|
return m, nil
|
||||||
case app.OptimisticMessageAddedMsg:
|
|
||||||
m.tail = true
|
|
||||||
m.rendering = true
|
|
||||||
return m, m.Reload()
|
|
||||||
case dialog.ThemeSelectedMsg:
|
case dialog.ThemeSelectedMsg:
|
||||||
m.cache.Clear()
|
m.cache.Clear()
|
||||||
m.rendering = true
|
m.loading = true
|
||||||
return m, m.Reload()
|
return m, m.Reload()
|
||||||
case ToggleToolDetailsMsg:
|
case ToggleToolDetailsMsg:
|
||||||
m.showToolDetails = !m.showToolDetails
|
m.showToolDetails = !m.showToolDetails
|
||||||
m.rendering = true
|
|
||||||
return m, m.Reload()
|
return m, m.Reload()
|
||||||
case app.SessionLoadedMsg, app.SessionClearedMsg:
|
case app.SessionLoadedMsg, app.SessionClearedMsg:
|
||||||
m.cache.Clear()
|
m.cache.Clear()
|
||||||
m.tail = true
|
m.tail = true
|
||||||
m.rendering = true
|
m.loading = true
|
||||||
return m, m.Reload()
|
return m, m.Reload()
|
||||||
case renderFinishedMsg:
|
|
||||||
m.rendering = false
|
|
||||||
if m.tail {
|
|
||||||
m.viewport.GotoBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
case opencode.EventListResponseEventSessionUpdated:
|
case opencode.EventListResponseEventSessionUpdated:
|
||||||
if msg.Properties.Info.ID == m.app.Session.ID {
|
if msg.Properties.Info.ID == m.app.Session.ID {
|
||||||
|
@ -97,17 +88,24 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
case opencode.EventListResponseEventMessageUpdated:
|
case opencode.EventListResponseEventMessageUpdated:
|
||||||
if msg.Properties.Info.SessionID == m.app.Session.ID {
|
if msg.Properties.Info.SessionID == m.app.Session.ID {
|
||||||
m.renderView()
|
cmds = append(cmds, m.renderView())
|
||||||
if m.tail {
|
|
||||||
m.viewport.GotoBottom()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case opencode.EventListResponseEventMessagePartUpdated:
|
case opencode.EventListResponseEventMessagePartUpdated:
|
||||||
if msg.Properties.Part.SessionID == m.app.Session.ID {
|
if msg.Properties.Part.SessionID == m.app.Session.ID {
|
||||||
m.renderView()
|
cmds = append(cmds, m.renderView())
|
||||||
if m.tail {
|
}
|
||||||
m.viewport.GotoBottom()
|
case renderCompleteMsg:
|
||||||
}
|
m.partCount = msg.partCount
|
||||||
|
m.lineCount = msg.lineCount
|
||||||
|
m.rendering = false
|
||||||
|
m.loading = false
|
||||||
|
m.viewport.SetHeight(m.height - lipgloss.Height(m.header))
|
||||||
|
m.viewport.SetContent(msg.content)
|
||||||
|
if m.tail {
|
||||||
|
m.viewport.GotoBottom()
|
||||||
|
}
|
||||||
|
if m.dirty {
|
||||||
|
cmds = append(cmds, m.renderView())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,144 +117,179 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesComponent) renderView() {
|
type renderCompleteMsg struct {
|
||||||
measure := util.Measure("messages.renderView")
|
content string
|
||||||
defer measure("messageCount", len(m.app.Messages))
|
partCount int
|
||||||
|
lineCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *messagesComponent) renderView() tea.Cmd {
|
||||||
m.header = m.renderHeader()
|
m.header = m.renderHeader()
|
||||||
|
|
||||||
t := theme.CurrentTheme()
|
if m.rendering {
|
||||||
blocks := make([]string, 0)
|
m.dirty = true
|
||||||
m.partCount = 0
|
return func() tea.Msg {
|
||||||
m.lineCount = 0
|
return nil
|
||||||
|
}
|
||||||
orphanedToolCalls := make([]opencode.ToolPart, 0)
|
|
||||||
|
|
||||||
width := min(m.width, app.MAX_CONTAINER_WIDTH)
|
|
||||||
if m.app.Config.Layout == opencode.LayoutConfigStretch {
|
|
||||||
width = m.width
|
|
||||||
}
|
}
|
||||||
|
m.dirty = false
|
||||||
|
m.rendering = true
|
||||||
|
|
||||||
for _, message := range m.app.Messages {
|
return func() tea.Msg {
|
||||||
var content string
|
measure := util.Measure("messages.renderView")
|
||||||
var cached bool
|
defer measure()
|
||||||
|
|
||||||
switch casted := message.Info.(type) {
|
t := theme.CurrentTheme()
|
||||||
case opencode.UserMessage:
|
blocks := make([]string, 0)
|
||||||
for partIndex, part := range message.Parts {
|
partCount := 0
|
||||||
switch part := part.(type) {
|
lineCount := 0
|
||||||
case opencode.TextPart:
|
|
||||||
if part.Synthetic {
|
orphanedToolCalls := make([]opencode.ToolPart, 0)
|
||||||
continue
|
|
||||||
}
|
width := min(m.width, app.MAX_CONTAINER_WIDTH)
|
||||||
remainingParts := message.Parts[partIndex+1:]
|
if m.app.Config.Layout == opencode.LayoutConfigStretch {
|
||||||
fileParts := make([]opencode.FilePart, 0)
|
width = m.width
|
||||||
for _, part := range remainingParts {
|
}
|
||||||
switch part := part.(type) {
|
|
||||||
case opencode.FilePart:
|
for _, message := range m.app.Messages {
|
||||||
fileParts = append(fileParts, part)
|
var content string
|
||||||
|
var cached bool
|
||||||
|
|
||||||
|
switch casted := message.Info.(type) {
|
||||||
|
case opencode.UserMessage:
|
||||||
|
for partIndex, part := range message.Parts {
|
||||||
|
switch part := part.(type) {
|
||||||
|
case opencode.TextPart:
|
||||||
|
if part.Synthetic {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
remainingParts := message.Parts[partIndex+1:]
|
||||||
flexItems := []layout.FlexItem{}
|
fileParts := make([]opencode.FilePart, 0)
|
||||||
if len(fileParts) > 0 {
|
for _, part := range remainingParts {
|
||||||
fileStyle := styles.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Padding(0, 1)
|
switch part := part.(type) {
|
||||||
mediaTypeStyle := styles.NewStyle().Background(t.Secondary()).Foreground(t.BackgroundPanel()).Padding(0, 1)
|
case opencode.FilePart:
|
||||||
for _, filePart := range fileParts {
|
fileParts = append(fileParts, part)
|
||||||
mediaType := ""
|
|
||||||
switch filePart.Mime {
|
|
||||||
case "text/plain":
|
|
||||||
mediaType = "txt"
|
|
||||||
case "image/png", "image/jpeg", "image/gif", "image/webp":
|
|
||||||
mediaType = "img"
|
|
||||||
mediaTypeStyle = mediaTypeStyle.Background(t.Accent())
|
|
||||||
case "application/pdf":
|
|
||||||
mediaType = "pdf"
|
|
||||||
mediaTypeStyle = mediaTypeStyle.Background(t.Primary())
|
|
||||||
}
|
|
||||||
flexItems = append(flexItems, layout.FlexItem{
|
|
||||||
View: mediaTypeStyle.Render(mediaType) + fileStyle.Render(filePart.Filename),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bgColor := t.BackgroundPanel()
|
|
||||||
files := layout.Render(
|
|
||||||
layout.FlexOptions{
|
|
||||||
Background: &bgColor,
|
|
||||||
Width: width - 6,
|
|
||||||
Direction: layout.Column,
|
|
||||||
},
|
|
||||||
flexItems...,
|
|
||||||
)
|
|
||||||
|
|
||||||
key := m.cache.GenerateKey(casted.ID, part.Text, width, files)
|
|
||||||
content, cached = m.cache.Get(key)
|
|
||||||
if !cached {
|
|
||||||
content = renderText(
|
|
||||||
m.app,
|
|
||||||
message.Info,
|
|
||||||
part.Text,
|
|
||||||
m.app.Config.Username,
|
|
||||||
m.showToolDetails,
|
|
||||||
width,
|
|
||||||
files,
|
|
||||||
)
|
|
||||||
content = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
content,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
m.cache.Set(key, content)
|
|
||||||
}
|
|
||||||
if content != "" {
|
|
||||||
m.partCount++
|
|
||||||
m.lineCount += lipgloss.Height(content) + 1
|
|
||||||
blocks = append(blocks, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case opencode.AssistantMessage:
|
|
||||||
hasTextPart := false
|
|
||||||
for partIndex, p := range message.Parts {
|
|
||||||
switch part := p.(type) {
|
|
||||||
case opencode.TextPart:
|
|
||||||
hasTextPart = true
|
|
||||||
finished := casted.Time.Completed > 0
|
|
||||||
remainingParts := message.Parts[partIndex+1:]
|
|
||||||
toolCallParts := make([]opencode.ToolPart, 0)
|
|
||||||
|
|
||||||
// sometimes tool calls happen without an assistant message
|
|
||||||
// these should be included in this assistant message as well
|
|
||||||
if len(orphanedToolCalls) > 0 {
|
|
||||||
toolCallParts = append(toolCallParts, orphanedToolCalls...)
|
|
||||||
orphanedToolCalls = make([]opencode.ToolPart, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
remaining := true
|
|
||||||
for _, part := range remainingParts {
|
|
||||||
if !remaining {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
switch part := part.(type) {
|
|
||||||
case opencode.TextPart:
|
|
||||||
// we only want tool calls associated with the current text part.
|
|
||||||
// if we hit another text part, we're done.
|
|
||||||
remaining = false
|
|
||||||
case opencode.ToolPart:
|
|
||||||
toolCallParts = append(toolCallParts, part)
|
|
||||||
if part.State.Status != opencode.ToolPartStateStatusCompleted && part.State.Status != opencode.ToolPartStateStatusError {
|
|
||||||
// i don't think there's a case where a tool call isn't in result state
|
|
||||||
// and the message time is 0, but just in case
|
|
||||||
finished = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
flexItems := []layout.FlexItem{}
|
||||||
|
if len(fileParts) > 0 {
|
||||||
|
fileStyle := styles.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Padding(0, 1)
|
||||||
|
mediaTypeStyle := styles.NewStyle().Background(t.Secondary()).Foreground(t.BackgroundPanel()).Padding(0, 1)
|
||||||
|
for _, filePart := range fileParts {
|
||||||
|
mediaType := ""
|
||||||
|
switch filePart.Mime {
|
||||||
|
case "text/plain":
|
||||||
|
mediaType = "txt"
|
||||||
|
case "image/png", "image/jpeg", "image/gif", "image/webp":
|
||||||
|
mediaType = "img"
|
||||||
|
mediaTypeStyle = mediaTypeStyle.Background(t.Accent())
|
||||||
|
case "application/pdf":
|
||||||
|
mediaType = "pdf"
|
||||||
|
mediaTypeStyle = mediaTypeStyle.Background(t.Primary())
|
||||||
|
}
|
||||||
|
flexItems = append(flexItems, layout.FlexItem{
|
||||||
|
View: mediaTypeStyle.Render(mediaType) + fileStyle.Render(filePart.Filename),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bgColor := t.BackgroundPanel()
|
||||||
|
files := layout.Render(
|
||||||
|
layout.FlexOptions{
|
||||||
|
Background: &bgColor,
|
||||||
|
Width: width - 6,
|
||||||
|
Direction: layout.Column,
|
||||||
|
},
|
||||||
|
flexItems...,
|
||||||
|
)
|
||||||
|
|
||||||
if finished {
|
key := m.cache.GenerateKey(casted.ID, part.Text, width, files)
|
||||||
key := m.cache.GenerateKey(casted.ID, part.Text, width, m.showToolDetails)
|
|
||||||
content, cached = m.cache.Get(key)
|
content, cached = m.cache.Get(key)
|
||||||
if !cached {
|
if !cached {
|
||||||
|
content = renderText(
|
||||||
|
m.app,
|
||||||
|
message.Info,
|
||||||
|
part.Text,
|
||||||
|
m.app.Config.Username,
|
||||||
|
m.showToolDetails,
|
||||||
|
width,
|
||||||
|
files,
|
||||||
|
)
|
||||||
|
content = lipgloss.PlaceHorizontal(
|
||||||
|
m.width,
|
||||||
|
lipgloss.Center,
|
||||||
|
content,
|
||||||
|
styles.WhitespaceStyle(t.Background()),
|
||||||
|
)
|
||||||
|
m.cache.Set(key, content)
|
||||||
|
}
|
||||||
|
if content != "" {
|
||||||
|
partCount++
|
||||||
|
lineCount += lipgloss.Height(content) + 1
|
||||||
|
blocks = append(blocks, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case opencode.AssistantMessage:
|
||||||
|
hasTextPart := false
|
||||||
|
for partIndex, p := range message.Parts {
|
||||||
|
switch part := p.(type) {
|
||||||
|
case opencode.TextPart:
|
||||||
|
hasTextPart = true
|
||||||
|
finished := part.Time.End > 0
|
||||||
|
remainingParts := message.Parts[partIndex+1:]
|
||||||
|
toolCallParts := make([]opencode.ToolPart, 0)
|
||||||
|
|
||||||
|
// sometimes tool calls happen without an assistant message
|
||||||
|
// these should be included in this assistant message as well
|
||||||
|
if len(orphanedToolCalls) > 0 {
|
||||||
|
toolCallParts = append(toolCallParts, orphanedToolCalls...)
|
||||||
|
orphanedToolCalls = make([]opencode.ToolPart, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining := true
|
||||||
|
for _, part := range remainingParts {
|
||||||
|
if !remaining {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch part := part.(type) {
|
||||||
|
case opencode.TextPart:
|
||||||
|
// we only want tool calls associated with the current text part.
|
||||||
|
// if we hit another text part, we're done.
|
||||||
|
remaining = false
|
||||||
|
case opencode.ToolPart:
|
||||||
|
toolCallParts = append(toolCallParts, part)
|
||||||
|
if part.State.Status != opencode.ToolPartStateStatusCompleted && part.State.Status != opencode.ToolPartStateStatusError {
|
||||||
|
// i don't think there's a case where a tool call isn't in result state
|
||||||
|
// and the message time is 0, but just in case
|
||||||
|
finished = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if finished {
|
||||||
|
key := m.cache.GenerateKey(casted.ID, part.Text, width, m.showToolDetails)
|
||||||
|
content, cached = m.cache.Get(key)
|
||||||
|
if !cached {
|
||||||
|
content = renderText(
|
||||||
|
m.app,
|
||||||
|
message.Info,
|
||||||
|
part.Text,
|
||||||
|
casted.ModelID,
|
||||||
|
m.showToolDetails,
|
||||||
|
width,
|
||||||
|
"",
|
||||||
|
toolCallParts...,
|
||||||
|
)
|
||||||
|
content = lipgloss.PlaceHorizontal(
|
||||||
|
m.width,
|
||||||
|
lipgloss.Center,
|
||||||
|
content,
|
||||||
|
styles.WhitespaceStyle(t.Background()),
|
||||||
|
)
|
||||||
|
m.cache.Set(key, content)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
content = renderText(
|
content = renderText(
|
||||||
m.app,
|
m.app,
|
||||||
message.Info,
|
message.Info,
|
||||||
|
@ -273,54 +306,50 @@ func (m *messagesComponent) renderView() {
|
||||||
content,
|
content,
|
||||||
styles.WhitespaceStyle(t.Background()),
|
styles.WhitespaceStyle(t.Background()),
|
||||||
)
|
)
|
||||||
m.cache.Set(key, content)
|
|
||||||
}
|
}
|
||||||
} else {
|
if content != "" {
|
||||||
content = renderText(
|
partCount++
|
||||||
m.app,
|
lineCount += lipgloss.Height(content) + 1
|
||||||
message.Info,
|
blocks = append(blocks, content)
|
||||||
part.Text,
|
}
|
||||||
casted.ModelID,
|
case opencode.ToolPart:
|
||||||
m.showToolDetails,
|
if !m.showToolDetails {
|
||||||
width,
|
if !hasTextPart {
|
||||||
"",
|
orphanedToolCalls = append(orphanedToolCalls, part)
|
||||||
toolCallParts...,
|
}
|
||||||
)
|
continue
|
||||||
content = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
content,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if content != "" {
|
|
||||||
m.partCount++
|
|
||||||
m.lineCount += lipgloss.Height(content) + 1
|
|
||||||
blocks = append(blocks, content)
|
|
||||||
}
|
|
||||||
case opencode.ToolPart:
|
|
||||||
if !m.showToolDetails {
|
|
||||||
if !hasTextPart {
|
|
||||||
orphanedToolCalls = append(orphanedToolCalls, part)
|
|
||||||
}
|
}
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
width := width
|
width := width
|
||||||
if m.app.Config.Layout == opencode.LayoutConfigAuto &&
|
if m.app.Config.Layout == opencode.LayoutConfigAuto &&
|
||||||
part.Tool == "edit" &&
|
part.Tool == "edit" &&
|
||||||
part.State.Error == "" {
|
part.State.Error == "" {
|
||||||
width = min(m.width, app.EDIT_DIFF_MAX_WIDTH)
|
width = min(m.width, app.EDIT_DIFF_MAX_WIDTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
if part.State.Status == opencode.ToolPartStateStatusCompleted || part.State.Status == opencode.ToolPartStateStatusError {
|
if part.State.Status == opencode.ToolPartStateStatusCompleted || part.State.Status == opencode.ToolPartStateStatusError {
|
||||||
key := m.cache.GenerateKey(casted.ID,
|
key := m.cache.GenerateKey(casted.ID,
|
||||||
part.ID,
|
part.ID,
|
||||||
m.showToolDetails,
|
m.showToolDetails,
|
||||||
width,
|
width,
|
||||||
)
|
)
|
||||||
content, cached = m.cache.Get(key)
|
content, cached = m.cache.Get(key)
|
||||||
if !cached {
|
if !cached {
|
||||||
|
content = renderToolDetails(
|
||||||
|
m.app,
|
||||||
|
part,
|
||||||
|
width,
|
||||||
|
)
|
||||||
|
content = lipgloss.PlaceHorizontal(
|
||||||
|
m.width,
|
||||||
|
lipgloss.Center,
|
||||||
|
content,
|
||||||
|
styles.WhitespaceStyle(t.Background()),
|
||||||
|
)
|
||||||
|
m.cache.Set(key, content)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if the tool call isn't finished, don't cache
|
||||||
content = renderToolDetails(
|
content = renderToolDetails(
|
||||||
m.app,
|
m.app,
|
||||||
part,
|
part,
|
||||||
|
@ -332,70 +361,57 @@ func (m *messagesComponent) renderView() {
|
||||||
content,
|
content,
|
||||||
styles.WhitespaceStyle(t.Background()),
|
styles.WhitespaceStyle(t.Background()),
|
||||||
)
|
)
|
||||||
m.cache.Set(key, content)
|
|
||||||
}
|
}
|
||||||
} else {
|
if content != "" {
|
||||||
// if the tool call isn't finished, don't cache
|
partCount++
|
||||||
content = renderToolDetails(
|
lineCount += lipgloss.Height(content) + 1
|
||||||
m.app,
|
blocks = append(blocks, content)
|
||||||
part,
|
}
|
||||||
width,
|
|
||||||
)
|
|
||||||
content = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
content,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if content != "" {
|
|
||||||
m.partCount++
|
|
||||||
m.lineCount += lipgloss.Height(content) + 1
|
|
||||||
blocks = append(blocks, content)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
error := ""
|
error := ""
|
||||||
if assistant, ok := message.Info.(opencode.AssistantMessage); ok {
|
if assistant, ok := message.Info.(opencode.AssistantMessage); ok {
|
||||||
switch err := assistant.Error.AsUnion().(type) {
|
switch err := assistant.Error.AsUnion().(type) {
|
||||||
case nil:
|
case nil:
|
||||||
case opencode.AssistantMessageErrorMessageOutputLengthError:
|
case opencode.AssistantMessageErrorMessageOutputLengthError:
|
||||||
error = "Message output length exceeded"
|
error = "Message output length exceeded"
|
||||||
case opencode.ProviderAuthError:
|
case opencode.ProviderAuthError:
|
||||||
error = err.Data.Message
|
error = err.Data.Message
|
||||||
case opencode.MessageAbortedError:
|
case opencode.MessageAbortedError:
|
||||||
error = "Request was aborted"
|
error = "Request was aborted"
|
||||||
case opencode.UnknownError:
|
case opencode.UnknownError:
|
||||||
error = err.Data.Message
|
error = err.Data.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if error != "" {
|
||||||
|
error = styles.NewStyle().Width(width - 6).Render(error)
|
||||||
|
error = renderContentBlock(
|
||||||
|
m.app,
|
||||||
|
error,
|
||||||
|
width,
|
||||||
|
WithBorderColor(t.Error()),
|
||||||
|
)
|
||||||
|
error = lipgloss.PlaceHorizontal(
|
||||||
|
m.width,
|
||||||
|
lipgloss.Center,
|
||||||
|
error,
|
||||||
|
styles.WhitespaceStyle(t.Background()),
|
||||||
|
)
|
||||||
|
blocks = append(blocks, error)
|
||||||
|
lineCount += lipgloss.Height(error) + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if error != "" {
|
content := "\n" + strings.Join(blocks, "\n\n")
|
||||||
error = styles.NewStyle().Width(width - 6).Render(error)
|
return renderCompleteMsg{
|
||||||
error = renderContentBlock(
|
content: content,
|
||||||
m.app,
|
partCount: partCount,
|
||||||
error,
|
lineCount: lineCount,
|
||||||
width,
|
|
||||||
WithBorderColor(t.Error()),
|
|
||||||
)
|
|
||||||
error = lipgloss.PlaceHorizontal(
|
|
||||||
m.width,
|
|
||||||
lipgloss.Center,
|
|
||||||
error,
|
|
||||||
styles.WhitespaceStyle(t.Background()),
|
|
||||||
)
|
|
||||||
blocks = append(blocks, error)
|
|
||||||
m.lineCount += lipgloss.Height(error) + 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.viewport.SetHeight(m.height - lipgloss.Height(m.header))
|
|
||||||
m.viewport.SetContent("\n" + strings.Join(blocks, "\n\n"))
|
|
||||||
if m.tail {
|
|
||||||
m.viewport.GotoBottom()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesComponent) renderHeader() string {
|
func (m *messagesComponent) renderHeader() string {
|
||||||
|
@ -552,7 +568,7 @@ func formatTokensAndCost(
|
||||||
|
|
||||||
func (m *messagesComponent) View() string {
|
func (m *messagesComponent) View() string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
if m.rendering {
|
if m.loading {
|
||||||
return lipgloss.Place(
|
return lipgloss.Place(
|
||||||
m.width,
|
m.width,
|
||||||
m.height,
|
m.height,
|
||||||
|
@ -569,10 +585,7 @@ func (m *messagesComponent) View() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesComponent) Reload() tea.Cmd {
|
func (m *messagesComponent) Reload() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return m.renderView()
|
||||||
m.renderView()
|
|
||||||
return renderFinishedMsg{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesComponent) PageUp() (tea.Model, tea.Cmd) {
|
func (m *messagesComponent) PageUp() (tea.Model, tea.Cmd) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue