mirror of
https://github.com/sst/opencode.git
synced 2025-07-23 07:45:00 +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 { McpCommand } from "./cli/cmd/mcp"
|
||||
import { InstallGithubCommand } from "./cli/cmd/install-github"
|
||||
import { Trace } from "./trace"
|
||||
|
||||
Trace.init()
|
||||
|
||||
const cancel = new AbortController()
|
||||
|
||||
|
@ -42,7 +45,7 @@ const cli = yargs(hideBin(process.argv))
|
|||
type: "boolean",
|
||||
})
|
||||
.middleware(async () => {
|
||||
await Log.init({ print: process.argv.includes("--print-logs") })
|
||||
await Log.init({ print: process.argv.includes("--print-logs"), dev: Installation.isDev() })
|
||||
|
||||
try {
|
||||
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 {
|
||||
print: boolean
|
||||
dev?: boolean
|
||||
level?: Level
|
||||
}
|
||||
|
||||
|
@ -63,7 +64,10 @@ export namespace Log {
|
|||
await fs.mkdir(dir, { recursive: true })
|
||||
cleanup(dir)
|
||||
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)
|
||||
await fs.truncate(logpath).catch(() => {})
|
||||
const writer = logfile.writer()
|
||||
|
@ -75,15 +79,16 @@ export namespace Log {
|
|||
}
|
||||
|
||||
async function cleanup(dir: string) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
const files = entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".log"))
|
||||
.map((entry) => path.join(dir, entry.name))
|
||||
|
||||
const glob = new Bun.Glob("????-??-??T??????.log")
|
||||
const files = await Array.fromAsync(
|
||||
glob.scan({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
}),
|
||||
)
|
||||
if (files.length <= 5) return
|
||||
|
||||
const filesToDelete = files.slice(0, -10)
|
||||
|
||||
await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {})))
|
||||
}
|
||||
|
||||
|
|
|
@ -68,9 +68,6 @@ type SendMsg struct {
|
|||
type SetEditorContentMsg struct {
|
||||
Text string
|
||||
}
|
||||
type OptimisticMessageAddedMsg struct {
|
||||
Message opencode.MessageUnion
|
||||
}
|
||||
type FileRenderedMsg struct {
|
||||
FilePath string
|
||||
}
|
||||
|
@ -508,7 +505,6 @@ func (a *App) SendChatMessage(
|
|||
}
|
||||
|
||||
a.Messages = append(a.Messages, Message{Info: message, Parts: parts})
|
||||
cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: message}))
|
||||
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
partsParam := []opencode.SessionChatParamsPartUnion{}
|
||||
|
|
|
@ -36,13 +36,14 @@ type messagesComponent struct {
|
|||
header string
|
||||
viewport viewport.Model
|
||||
cache *PartCache
|
||||
rendering bool
|
||||
loading bool
|
||||
showToolDetails bool
|
||||
rendering bool
|
||||
dirty bool
|
||||
tail bool
|
||||
partCount int
|
||||
lineCount int
|
||||
}
|
||||
type renderFinishedMsg struct{}
|
||||
|
||||
type ToggleToolDetailsMsg struct{}
|
||||
|
||||
|
@ -62,34 +63,24 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.width = effectiveWidth
|
||||
m.height = msg.Height - 7
|
||||
m.viewport.SetWidth(m.width)
|
||||
m.header = m.renderHeader()
|
||||
m.loading = true
|
||||
return m, m.Reload()
|
||||
case app.SendMsg:
|
||||
m.viewport.GotoBottom()
|
||||
m.tail = true
|
||||
return m, nil
|
||||
case app.OptimisticMessageAddedMsg:
|
||||
m.tail = true
|
||||
m.rendering = true
|
||||
return m, m.Reload()
|
||||
case dialog.ThemeSelectedMsg:
|
||||
m.cache.Clear()
|
||||
m.rendering = true
|
||||
m.loading = true
|
||||
return m, m.Reload()
|
||||
case ToggleToolDetailsMsg:
|
||||
m.showToolDetails = !m.showToolDetails
|
||||
m.rendering = true
|
||||
return m, m.Reload()
|
||||
case app.SessionLoadedMsg, app.SessionClearedMsg:
|
||||
m.cache.Clear()
|
||||
m.tail = true
|
||||
m.rendering = true
|
||||
m.loading = true
|
||||
return m, m.Reload()
|
||||
case renderFinishedMsg:
|
||||
m.rendering = false
|
||||
if m.tail {
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
|
||||
case opencode.EventListResponseEventSessionUpdated:
|
||||
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:
|
||||
if msg.Properties.Info.SessionID == m.app.Session.ID {
|
||||
m.renderView()
|
||||
if m.tail {
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
cmds = append(cmds, m.renderView())
|
||||
}
|
||||
case opencode.EventListResponseEventMessagePartUpdated:
|
||||
if msg.Properties.Part.SessionID == m.app.Session.ID {
|
||||
m.renderView()
|
||||
cmds = append(cmds, m.renderView())
|
||||
}
|
||||
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,16 +117,32 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *messagesComponent) renderView() {
|
||||
measure := util.Measure("messages.renderView")
|
||||
defer measure("messageCount", len(m.app.Messages))
|
||||
type renderCompleteMsg struct {
|
||||
content string
|
||||
partCount int
|
||||
lineCount int
|
||||
}
|
||||
|
||||
func (m *messagesComponent) renderView() tea.Cmd {
|
||||
m.header = m.renderHeader()
|
||||
|
||||
if m.rendering {
|
||||
m.dirty = true
|
||||
return func() tea.Msg {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
m.dirty = false
|
||||
m.rendering = true
|
||||
|
||||
return func() tea.Msg {
|
||||
measure := util.Measure("messages.renderView")
|
||||
defer measure()
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
blocks := make([]string, 0)
|
||||
m.partCount = 0
|
||||
m.lineCount = 0
|
||||
partCount := 0
|
||||
lineCount := 0
|
||||
|
||||
orphanedToolCalls := make([]opencode.ToolPart, 0)
|
||||
|
||||
|
@ -209,8 +223,8 @@ func (m *messagesComponent) renderView() {
|
|||
m.cache.Set(key, content)
|
||||
}
|
||||
if content != "" {
|
||||
m.partCount++
|
||||
m.lineCount += lipgloss.Height(content) + 1
|
||||
partCount++
|
||||
lineCount += lipgloss.Height(content) + 1
|
||||
blocks = append(blocks, content)
|
||||
}
|
||||
}
|
||||
|
@ -222,7 +236,7 @@ func (m *messagesComponent) renderView() {
|
|||
switch part := p.(type) {
|
||||
case opencode.TextPart:
|
||||
hasTextPart = true
|
||||
finished := casted.Time.Completed > 0
|
||||
finished := part.Time.End > 0
|
||||
remainingParts := message.Parts[partIndex+1:]
|
||||
toolCallParts := make([]opencode.ToolPart, 0)
|
||||
|
||||
|
@ -294,8 +308,8 @@ func (m *messagesComponent) renderView() {
|
|||
)
|
||||
}
|
||||
if content != "" {
|
||||
m.partCount++
|
||||
m.lineCount += lipgloss.Height(content) + 1
|
||||
partCount++
|
||||
lineCount += lipgloss.Height(content) + 1
|
||||
blocks = append(blocks, content)
|
||||
}
|
||||
case opencode.ToolPart:
|
||||
|
@ -349,8 +363,8 @@ func (m *messagesComponent) renderView() {
|
|||
)
|
||||
}
|
||||
if content != "" {
|
||||
m.partCount++
|
||||
m.lineCount += lipgloss.Height(content) + 1
|
||||
partCount++
|
||||
lineCount += lipgloss.Height(content) + 1
|
||||
blocks = append(blocks, content)
|
||||
}
|
||||
}
|
||||
|
@ -387,14 +401,16 @@ func (m *messagesComponent) renderView() {
|
|||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
blocks = append(blocks, error)
|
||||
m.lineCount += lipgloss.Height(error) + 1
|
||||
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()
|
||||
content := "\n" + strings.Join(blocks, "\n\n")
|
||||
return renderCompleteMsg{
|
||||
content: content,
|
||||
partCount: partCount,
|
||||
lineCount: lineCount,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -552,7 +568,7 @@ func formatTokensAndCost(
|
|||
|
||||
func (m *messagesComponent) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
if m.rendering {
|
||||
if m.loading {
|
||||
return lipgloss.Place(
|
||||
m.width,
|
||||
m.height,
|
||||
|
@ -569,10 +585,7 @@ func (m *messagesComponent) View() string {
|
|||
}
|
||||
|
||||
func (m *messagesComponent) Reload() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
m.renderView()
|
||||
return renderFinishedMsg{}
|
||||
}
|
||||
return m.renderView()
|
||||
}
|
||||
|
||||
func (m *messagesComponent) PageUp() (tea.Model, tea.Cmd) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue