Compare commits

..

7 Commits

Author SHA1 Message Date
adamdottv c389e0ed43 fix(tui): redundant tool calls in each message in collapsed mode 2025-07-03 10:42:27 -05:00
Dax Raad 204801052a flag for disabling file watcher 2025-07-03 10:37:08 -04:00
Dax Raad 2528d8cb88 increase max retries to 10 2025-07-03 10:32:55 -04:00
adamdottv 6b73ffd1c1 fix(tui): include orphaned tool calls 2025-07-03 09:32:44 -05:00
adamdottv 0eadc50a33 fix(tui): selected message visuals 2025-07-03 09:03:04 -05:00
Dax Raad aeea84a877 fix webdomain 2025-07-03 09:58:25 -04:00
GitHub Action a54c5c6298 Update download stats 2025-07-03 2025-07-03 12:26:51 +00:00
7 changed files with 80 additions and 59 deletions
+1
View File
@@ -6,3 +6,4 @@
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
+3
View File
@@ -9,6 +9,9 @@ const bucket = new sst.cloudflare.Bucket("Bucket")
export const api = new sst.cloudflare.Worker("Api", {
domain: `api.${domain}`,
handler: "packages/function/src/api.ts",
environment: {
WEB_DOMAIN: domain,
},
url: true,
link: [bucket],
transform: {
+2 -1
View File
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto"
type Env = {
SYNC_SERVER: DurableObjectNamespace<SyncServer>
Bucket: R2Bucket
WEB_DOMAIN: string
}
export class SyncServer extends DurableObject<Env> {
@@ -127,7 +128,7 @@ export default {
return new Response(
JSON.stringify({
secret,
url: "https://opencode.ai/s/" + short,
url: `https://${env.WEB_DOMAIN}/s/${short}`,
}),
{
headers: { "Content-Type": "application/json" },
+33 -30
View File
@@ -3,6 +3,7 @@ import { Bus } from "../bus"
import fs from "fs"
import { App } from "../app/app"
import { Log } from "../util/log"
import { Flag } from "../flag/flag"
export namespace FileWatcher {
const log = Log.create({ service: "file.watcher" })
@@ -16,37 +17,39 @@ export namespace FileWatcher {
}),
),
}
const state = App.state(
"file.watcher",
() => {
const app = App.use()
try {
const watcher = fs.watch(
app.info.path.cwd,
{ recursive: true },
(event, file) => {
log.info("change", { file, event })
if (!file) return
// for some reason async local storage is lost here
// https://github.com/oven-sh/bun/issues/20754
App.provideExisting(app, async () => {
Bus.publish(Event.Updated, {
file,
event,
})
})
},
)
return { watcher }
} catch {
return {}
}
},
async (state) => {
state.watcher?.close()
},
)
export function init() {
App.state(
"file.watcher",
() => {
const app = App.use()
try {
const watcher = fs.watch(
app.info.path.cwd,
{ recursive: true },
(event, file) => {
log.info("change", { file, event })
if (!file) return
// for some reason async local storage is lost here
// https://github.com/oven-sh/bun/issues/20754
App.provideExisting(app, async () => {
Bus.publish(Event.Updated, {
file,
event,
})
})
},
)
return { watcher }
} catch {
return {}
}
},
async (state) => {
state.watcher?.close()
},
)()
if (Flag.OPENCODE_DISABLE_WATCHER) return
state()
}
}
+1
View File
@@ -1,5 +1,6 @@
export namespace Flag {
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
export const OPENCODE_DISABLE_WATCHER = truthy("OPENCODE_DISABLE_WATCHER")
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
+1
View File
@@ -583,6 +583,7 @@ export namespace Session {
// return step
// },
toolCallStreaming: true,
maxRetries: 10,
maxTokens: Math.max(0, model.info.limit.output) || undefined,
abortSignal: abort.signal,
maxSteps: 1000,
@@ -80,15 +80,11 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.showToolDetails = !m.showToolDetails
m.rendering = true
return m, m.Reload()
case app.SessionLoadedMsg:
case app.SessionLoadedMsg, app.SessionClearedMsg:
m.cache.Clear()
m.tail = true
m.rendering = true
return m, m.Reload()
case app.SessionClearedMsg:
m.cache.Clear()
m.rendering = true
return m, m.Reload()
case renderFinishedMsg:
m.rendering = false
if m.tail {
@@ -129,6 +125,8 @@ func (m *messagesComponent) renderView(width int) {
m.partCount = 0
m.lineCount = 0
orphanedToolCalls := make([]opencode.ToolInvocationPart, 0)
for _, message := range m.app.Messages {
var content string
var cached bool
@@ -153,30 +151,39 @@ func (m *messagesComponent) renderView(width int) {
m.cache.Set(key, content)
}
if content != "" {
if m.selectedPart == m.partCount {
m.viewport.SetYOffset(m.lineCount - 4)
m.selectedText = part.Text
}
m = m.updateSelected(content, part.Text)
blocks = append(blocks, content)
m.partCount++
m.lineCount += lipgloss.Height(content) + 1
}
}
}
case opencode.MessageRoleAssistant:
for i, p := range message.Parts {
hasTextPart := false
for partIndex, p := range message.Parts {
switch part := p.AsUnion().(type) {
case opencode.TextPart:
hasTextPart = true
finished := message.Metadata.Time.Completed > 0
remainingParts := message.Parts[i+1:]
remainingParts := message.Parts[partIndex+1:]
toolCallParts := make([]opencode.ToolInvocationPart, 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.ToolInvocationPart, 0)
}
remaining := true
for _, part := range remainingParts {
if !remaining {
break
}
switch part := part.AsUnion().(type) {
case opencode.TextPart:
// we only want tool calls associated with the current text part.
// if we hit another text part, we're done.
break
remaining = false
case opencode.ToolInvocationPart:
toolCallParts = append(toolCallParts, part)
if part.ToolInvocation.State != "result" {
@@ -216,16 +223,14 @@ func (m *messagesComponent) renderView(width int) {
)
}
if content != "" {
if m.selectedPart == m.partCount {
m.viewport.SetYOffset(m.lineCount - 4)
m.selectedText = p.Text
}
m = m.updateSelected(content, p.Text)
blocks = append(blocks, content)
m.partCount++
m.lineCount += lipgloss.Height(content) + 1
}
case opencode.ToolInvocationPart:
if !m.showToolDetails {
if !hasTextPart {
orphanedToolCalls = append(orphanedToolCalls, part)
}
continue
}
@@ -258,13 +263,8 @@ func (m *messagesComponent) renderView(width int) {
)
}
if content != "" {
if m.selectedPart == m.partCount {
m.viewport.SetYOffset(m.lineCount - 4)
m.selectedText = ""
}
m = m.updateSelected(content, "")
blocks = append(blocks, content)
m.partCount++
m.lineCount += lipgloss.Height(content) + 1
}
}
}
@@ -295,9 +295,20 @@ func (m *messagesComponent) renderView(width int) {
}
m.viewport.SetContent("\n" + strings.Join(blocks, "\n\n"))
if m.selectedPart == m.partCount-1 {
if m.selectedPart == m.partCount {
m.viewport.GotoBottom()
}
}
func (m *messagesComponent) updateSelected(content string, selectedText string) *messagesComponent {
if m.selectedPart == m.partCount {
m.viewport.SetYOffset(m.lineCount - (m.viewport.Height() / 2) + 4)
m.selectedText = selectedText
}
m.partCount++
m.lineCount += lipgloss.Height(content) + 1
return m
}
func (m *messagesComponent) header(width int) string {
@@ -343,7 +354,7 @@ func (m *messagesComponent) View(width, height int) string {
height,
lipgloss.Center,
lipgloss.Center,
styles.NewStyle().Background(t.Background()).Render("Loading session..."),
styles.NewStyle().Background(t.Background()).Render(""),
styles.WhitespaceStyle(t.Background()),
)
}