Compare commits

...

10 Commits

Author SHA1 Message Date
Timo Clasen a524fc545c fix(hooks): prevent session_complete hook from firing on subagent sessions (#1149)
publish / publish (push) Has been cancelled
2025-07-19 18:20:07 -05:00
Dax Raad 4316edaf43 fix first run github copilot
publish / publish (push) Has been cancelled
2025-07-19 19:19:38 -04:00
Dax Raad d845924e8b ci: ignore
publish / publish (push) Has been cancelled
2025-07-19 19:00:17 -04:00
Dax Raad a29b322bdd ci: ignore
publish / publish (push) Has been cancelled
2025-07-19 18:54:46 -04:00
Dax Raad 9723ffa7a6 ignore: ci
publish / publish (push) Has been cancelled
2025-07-19 18:48:43 -04:00
Dax Raad f06cd88773 perf: more performance improvements
publish / publish (push) Has been cancelled
2025-07-19 18:41:21 -04:00
Dax Raad 9af92b6914 perf: scroll to bottom in thread
publish / publish (push) Has been cancelled
2025-07-19 17:55:01 -04:00
Dax Raad 8f64c4b312 disable todo tools when running as task
publish / publish (push) Has been cancelled
2025-07-19 15:54:11 -04:00
Dax Raad a32877e908 ignore: create memo abstraction
publish / publish (push) Has been cancelled
2025-07-19 15:26:26 -04:00
Dax Raad 6465c9c44a fix openrouter caching
publish / publish (push) Has been cancelled
2025-07-19 15:11:21 -04:00
10 changed files with 100 additions and 52 deletions
+1
View File
@@ -152,6 +152,7 @@ if (!snapshot) {
for (const pkg of ["opencode", "opencode-bin"]) {
await $`rm -rf ./dist/aur-${pkg}`
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
await $`cd ./dist/aur-${pkg} && git checkout master`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild.replace("${pkg}", pkg))
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
+2 -1
View File
@@ -4,11 +4,12 @@ import path from "path"
export const AuthCopilot = lazy(async () => {
const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts"))
const exists = await file.exists()
const response = fetch("https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts")
.then((x) => Bun.write(file, x))
.catch(() => {})
if (!file.exists()) {
if (!exists) {
const worked = await response
if (!worked) return
}
+5 -1
View File
@@ -31,9 +31,13 @@ export namespace ConfigHooks {
}
})
Bus.subscribe(Session.Event.Idle, async () => {
Bus.subscribe(Session.Event.Idle, async (payload) => {
const cfg = await Config.get()
if (cfg.experimental?.hook?.session_completed) {
const session = await Session.get(payload.properties.sessionID)
// Only fire hook for top-level sessions (not subagent sessions)
if (session.parentID) return
for (const item of cfg.experimental.hook.session_completed) {
log.info("session_completed", {
command: item.command,
+4 -11
View File
@@ -13,22 +13,15 @@ export namespace ProviderTransform {
anthropic: {
cacheControl: { type: "ephemeral" },
},
openaiCompatible: {
openrouter: {
cache_control: { type: "ephemeral" },
},
}
}
}
if (providerID === "amazon-bedrock" || modelID.includes("anthropic")) {
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
for (const msg of unique([...system, ...final])) {
msg.providerOptions = {
...msg.providerOptions,
bedrock: {
cachePoint: { type: "ephemeral" },
},
openaiCompatible: {
cache_control: { type: "ephemeral" },
},
}
}
}
+2
View File
@@ -325,6 +325,7 @@ export namespace Session {
providerID: z.string(),
modelID: z.string(),
mode: z.string().optional(),
tools: z.record(z.boolean()).optional(),
parts: z.array(
z.discriminatedUnion("type", [
MessageV2.TextPart.omit({
@@ -618,6 +619,7 @@ export namespace Session {
for (const item of await Provider.tools(input.providerID)) {
if (mode.tools[item.id] === false) continue
if (input.tools?.[item.id] === false) continue
if (session.parentID && item.id === "task") continue
tools[item.id] = tool({
id: item.id as any,
+4
View File
@@ -41,6 +41,10 @@ export const TaskTool = Tool.define({
sessionID: session.id,
modelID: msg.modelID,
providerID: msg.providerID,
tools: {
todoread: false,
todowrite: false,
},
parts: [
{
id: Identifier.ascending("part"),
+3
View File
@@ -91,6 +91,9 @@ func main() {
stream := httpClient.Event.ListStreaming(ctx)
for stream.Next() {
evt := stream.Current().AsUnion()
if _, ok := evt.(opencode.EventListResponseEventStorageWrite); ok {
continue
}
program.Send(evt)
}
if err := stream.Err(); err != nil {
@@ -53,6 +53,8 @@ func (m *messagesComponent) Init() tea.Cmd {
}
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
measure := util.Measure("messages.Update")
defer measure("from", fmt.Sprintf("%T", msg))
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
@@ -102,9 +104,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.loading = false
m.tail = m.viewport.AtBottom()
m.viewport = msg.viewport
if m.tail {
m.viewport.GotoBottom()
}
m.header = msg.header
if m.dirty {
cmds = append(cmds, m.renderView())
}
@@ -120,12 +120,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
type renderCompleteMsg struct {
viewport viewport.Model
header string
partCount int
lineCount int
}
func (m *messagesComponent) renderView() tea.Cmd {
m.header = m.renderHeader()
if m.rendering {
slog.Debug("pending render, skipping")
@@ -138,8 +138,10 @@ func (m *messagesComponent) renderView() tea.Cmd {
m.rendering = true
viewport := m.viewport
tail := m.tail
return func() tea.Msg {
header := m.renderHeader()
measure := util.Measure("messages.renderView")
defer measure()
@@ -402,8 +404,12 @@ func (m *messagesComponent) renderView() tea.Cmd {
content := "\n" + strings.Join(blocks, "\n\n")
viewport.SetHeight(m.height - lipgloss.Height(m.header))
viewport.SetContent(content)
if tail {
viewport.GotoBottom()
}
return renderCompleteMsg{
header: header,
viewport: viewport,
partCount: partCount,
lineCount: lineCount,
+3 -3
View File
@@ -103,7 +103,7 @@ func (a appModel) Init() tea.Cmd {
}
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
measure := util.Measure("Update")
measure := util.Measure("app.Update")
defer measure("from", fmt.Sprintf("%T", msg))
var cmd tea.Cmd
@@ -528,17 +528,17 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (a appModel) View() string {
measure := util.Measure("app.View")
defer measure()
t := theme.CurrentTheme()
var mainLayout string
measure := util.Measure("app.View")
if a.app.Session.ID == "" {
mainLayout = a.home()
} else {
mainLayout = a.chat()
}
measure()
mainLayout = styles.NewStyle().
Background(t.Background()).
Padding(0, 2).
+66 -32
View File
@@ -43,17 +43,37 @@ func New(opts ...Option) (m Model) {
opt(&m)
}
m.setInitialValues()
m.memo = &Memo{}
return m
}
type Memo struct {
dirty bool
cache string
}
func (m *Memo) View(render func() string) string {
if m.dirty {
// slog.Debug("memo dirty")
m.cache = render()
m.dirty = false
return m.cache
}
// slog.Debug("memo cache")
return m.cache
}
func (m *Memo) Invalidate() {
m.dirty = true
}
// Model is the Bubble Tea model for this viewport element.
type Model struct {
memo *Memo
width int
height int
KeyMap KeyMap
cache string
// Whether or not to wrap text. If false, it'll allow horizontal scrolling
// instead.
SoftWrap bool
@@ -160,6 +180,7 @@ func (m Model) Height() int {
// SetHeight sets the height of the viewport.
func (m *Model) SetHeight(h int) {
m.height = h
m.memo.Invalidate()
}
// Width returns the width of the viewport.
@@ -170,6 +191,7 @@ func (m Model) Width() int {
// SetWidth sets the width of the viewport.
func (m *Model) SetWidth(w int) {
m.width = w
m.memo.Invalidate()
}
// AtTop returns whether or not the viewport is at the very top position.
@@ -220,7 +242,7 @@ func (m Model) HorizontalScrollPercent() float64 {
func (m *Model) SetContent(s string) {
s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings
m.SetContentLines(strings.Split(s, "\n"))
m.render()
m.memo.Invalidate()
}
// SetContentLines allows to set the lines to be shown instead of the content.
@@ -239,7 +261,7 @@ func (m *Model) SetContentLines(lines []string) {
if m.YOffset > m.maxYOffset() {
m.GotoBottom()
}
m.render()
m.memo.Invalidate()
}
// GetContent returns the entire content as a single string.
@@ -442,12 +464,14 @@ func (m Model) setupGutter(lines []string) []string {
}
result[i] = strings.Join(line, "\n")
}
m.memo.Invalidate()
return result
}
// SetYOffset sets the Y offset.
func (m *Model) SetYOffset(n int) {
m.YOffset = clamp(n, 0, m.maxYOffset())
m.memo.Invalidate()
}
// SetXOffset sets the X offset.
@@ -457,6 +481,7 @@ func (m *Model) SetXOffset(n int) {
return
}
m.xOffset = clamp(n, 0, m.maxXOffset())
m.memo.Invalidate()
}
// EnsureVisible ensures that the given line and column are in the viewport.
@@ -483,7 +508,7 @@ func (m *Model) ViewDown() {
}
m.LineDown(m.Height())
m.render()
m.memo.Invalidate()
}
// ViewUp moves the view up by one height of the viewport. Basically, "page up".
@@ -493,7 +518,7 @@ func (m *Model) ViewUp() {
}
m.LineUp(m.Height())
m.render()
m.memo.Invalidate()
}
// HalfViewDown moves the view down by half the height of the viewport.
@@ -503,7 +528,7 @@ func (m *Model) HalfViewDown() {
}
m.LineDown(m.Height() / 2) //nolint:mnd
m.render()
m.memo.Invalidate()
}
// HalfViewUp moves the view up by half the height of the viewport.
@@ -513,7 +538,7 @@ func (m *Model) HalfViewUp() {
}
m.LineUp(m.Height() / 2) //nolint:mnd
m.render()
m.memo.Invalidate()
}
// LineDown moves the view down by the given number of lines.
@@ -527,7 +552,7 @@ func (m *Model) LineDown(n int) {
// the bottom.
m.SetYOffset(m.YOffset + n)
m.hiIdx = m.findNearedtMatch()
m.render()
m.memo.Invalidate()
}
// LineUp moves the view down by the given number of lines. Returns the new
@@ -541,7 +566,7 @@ func (m *Model) LineUp(n int) {
// greater than the number of lines we are from the top.
m.SetYOffset(m.YOffset - n)
m.hiIdx = m.findNearedtMatch()
m.render()
m.memo.Invalidate()
}
// TotalLineCount returns the total number of lines (both hidden and visible) within the viewport.
@@ -562,7 +587,7 @@ func (m *Model) GotoTop() (lines []string) {
m.SetYOffset(0)
m.hiIdx = m.findNearedtMatch()
m.render()
m.memo.Invalidate()
return m.visibleLines()
}
@@ -570,7 +595,7 @@ func (m *Model) GotoTop() (lines []string) {
func (m *Model) GotoBottom() (lines []string) {
m.SetYOffset(m.maxYOffset())
m.hiIdx = m.findNearedtMatch()
m.render()
m.memo.Invalidate()
return m.visibleLines()
}
@@ -583,6 +608,7 @@ func (m *Model) SetHorizontalStep(n int) {
}
m.horizontalStep = n
m.memo.Invalidate()
}
// MoveLeft moves the viewport to the left by the given number of columns.
@@ -590,6 +616,7 @@ func (m *Model) MoveLeft(cols int) {
m.xOffset -= cols
if m.xOffset < 0 {
m.xOffset = 0
m.memo.Invalidate()
}
}
@@ -606,6 +633,7 @@ func (m *Model) MoveRight(cols int) {
// Resets lines indent to zero.
func (m *Model) ResetIndent() {
m.xOffset = 0
m.memo.Invalidate()
}
// SetHighlights sets ranges of characters to highlight.
@@ -623,12 +651,14 @@ func (m *Model) SetHighlights(matches [][]int) {
m.highlights = parseMatches(m.GetContent(), matches)
m.hiIdx = m.findNearedtMatch()
m.showHighlight()
m.memo.Invalidate()
}
// ClearHighlights clears previously set highlights.
func (m *Model) ClearHighlights() {
m.highlights = nil
m.hiIdx = -1
m.memo.Invalidate()
}
func (m *Model) showHighlight() {
@@ -637,6 +667,7 @@ func (m *Model) showHighlight() {
}
line, colstart, colend := m.highlights[m.hiIdx].coords()
m.EnsureVisible(line, colstart, colend)
m.memo.Invalidate()
}
// HighlightNext highlights the next match.
@@ -647,6 +678,7 @@ func (m *Model) HighlightNext() {
m.hiIdx = (m.hiIdx + 1) % len(m.highlights)
m.showHighlight()
m.memo.Invalidate()
}
// HighlightPrevious highlights the previous match.
@@ -657,6 +689,7 @@ func (m *Model) HighlightPrevious() {
m.hiIdx = (m.hiIdx - 1 + len(m.highlights)) % len(m.highlights)
m.showHighlight()
m.memo.Invalidate()
}
func (m Model) findNearedtMatch() int {
@@ -728,29 +761,30 @@ func (m Model) updateAsModel(msg tea.Msg) Model {
// View renders the viewport into a string.
func (m *Model) render() {
w, h := m.Width(), m.Height()
if sw := m.Style.GetWidth(); sw != 0 {
w = min(w, sw)
}
if sh := m.Style.GetHeight(); sh != 0 {
h = min(h, sh)
}
contentWidth := w - m.Style.GetHorizontalFrameSize()
contentHeight := h - m.Style.GetVerticalFrameSize()
visible := m.visibleLines()
contents := lipgloss.NewStyle().
Width(contentWidth). // pad to width.
Height(contentHeight). // pad to height.
MaxHeight(contentHeight). // truncate height if taller.
MaxWidth(contentWidth). // truncate width if wider.
Render(strings.Join(visible, "\n"))
m.cache = m.Style.
UnsetWidth().UnsetHeight(). // Style size already applied in contents.
Render(contents)
}
func (m Model) View() string {
return m.cache
return m.memo.View(func() string {
w, h := m.Width(), m.Height()
if sw := m.Style.GetWidth(); sw != 0 {
w = min(w, sw)
}
if sh := m.Style.GetHeight(); sh != 0 {
h = min(h, sh)
}
contentWidth := w - m.Style.GetHorizontalFrameSize()
contentHeight := h - m.Style.GetVerticalFrameSize()
visible := m.visibleLines()
contents := lipgloss.NewStyle().
Width(contentWidth). // pad to width.
Height(contentHeight). // pad to height.
MaxHeight(contentHeight). // truncate height if taller.
MaxWidth(contentWidth). // truncate width if wider.
Render(strings.Join(visible, "\n"))
return m.Style.
UnsetWidth().UnsetHeight(). // Style size already applied in contents.
Render(contents)
})
}
func clamp(v, low, high int) int {