on main help sign in
fix the memory bug
commite7bb99baab5e6968ce0351d6ad219ed21ceec4df authorKujtim Hoxha <kujtimii.h@gmail.com> dateMon 21 Apr 2025, 13:33 (2 years ago) parent1da298e
6 files changed+127−55
README.md+5−2 cmd/root.go+11−13 internal/pubsub/broker.go+47−25 internal/tui/components/chat/list.go+1 internal/tui/components/core/status.go+12−3 internal/tui/tui.go+51−12
README.md+5−2
@@ -351,9 +351,12 @@ go build -o opencode
351351
352352## Acknowledgments
353353
354−OpenCode builds upon the work of several open source projects and developers:
354+OpenCode gratefully acknowledges the contributions and support from these key individuals:
355355
356−- [@isaacphi](https://github.com/isaacphi) - LSP client implementation
356+- [@isaacphi](https://github.com/isaacphi) - For the [mcp-language-server](https://github.com/isaacphi/mcp-language-server) project which provided the foundation for our LSP client implementation
357+- [@adamdottv](https://github.com/adamdottv) - For the design direction and UI/UX architecture
358+
359+Special thanks to the broader open source community whose tools and libraries have made this project possible.
357360
358361## License
359362
cmd/root.go+11−13
@@ -79,7 +79,7 @@ var rootCmd = &cobra.Command{
7979 initMCPTools(ctx, app)
8080
8181 // Setup the subscriptions, this will send services events to the TUI
82− ch, cancelSubs := setupSubscriptions(app)
82+ ch, cancelSubs := setupSubscriptions(app, ctx)
8383
8484 // Create a context for the TUI message handler
8585 tuiCtx, tuiCancel := context.WithCancel(ctx)
@@ -174,21 +174,21 @@ func setupSubscriber[T any](
174174 defer wg.Done()
175175 defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
176176
177+ subCh := subscriber(ctx)
178+
177179 for {
178180 select {
179− case event, ok := <-subscriber(ctx):
181+ case event, ok := <-subCh:
180182 if !ok {
181183 logging.Info("%s subscription channel closed", name)
182184 return
183185 }
184186
185− // Convert generic event to tea.Msg if needed
186187 var msg tea.Msg = event
187188
188− // Non-blocking send with timeout to prevent deadlocks
189189 select {
190190 case outputCh <- msg:
191− case <-time.After(500 * time.Millisecond):
191+ case <-time.After(2 * time.Second):
192192 logging.Warn("%s message dropped due to slow consumer", name)
193193 case <-ctx.Done():
194194 logging.Info("%s subscription cancelled", name)
@@ -202,23 +202,21 @@ func setupSubscriber[T any](
202202 }()
203203}
204204
205−func setupSubscriptions(app *app.App) (chan tea.Msg, func()) {
205+func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) {
206206 ch := make(chan tea.Msg, 100)
207− // Add a buffer to prevent blocking
207+
208208 wg := sync.WaitGroup{}
209− ctx, cancel := context.WithCancel(context.Background())
210− // Setup each subscription using the helper
209+ ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
210+
211211 setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch)
212212 setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
213213 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
214214 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
215215
216− // Return channel and a cleanup function
217216 cleanupFunc := func() {
218217 logging.Info("Cancelling all subscriptions")
219218 cancel() // Signal all goroutines to stop
220219
221− // Wait with a timeout for all goroutines to complete
222220 waitCh := make(chan struct{})
223221 go func() {
224222 defer logging.RecoverPanic("subscription-cleanup", nil)
@@ -229,11 +227,11 @@ func setupSubscriptions(app *app.App) (chan tea.Msg, func()) {
229227 select {
230228 case <-waitCh:
231229 logging.Info("All subscription goroutines completed successfully")
230+ close(ch) // Only close after all writers are confirmed done
232231 case <-time.After(5 * time.Second):
233232 logging.Warn("Timed out waiting for some subscription goroutines to complete")
233+ close(ch)
234234 }
235−
236− close(ch) // Safe to close after all writers are done or timed out
237235 }
238236 return ch, cleanupFunc
239237}
internal/pubsub/broker.go+47−25
@@ -5,47 +5,53 @@ import (
55 "sync"
66)
77
8−const bufferSize = 1024
8+const bufferSize = 64
99
10−// Broker allows clients to publish events and subscribe to events
1110type Broker[T any] struct {
12− subs map[chan Event[T]]struct{} // subscriptions
13− mu sync.Mutex // sync access to map
14− done chan struct{} // close when broker is shutting down
11+ subs map[chan Event[T]]struct{}
12+ mu sync.RWMutex
13+ done chan struct{}
14+ subCount int
15+ maxEvents int
1516}
1617
17−// NewBroker constructs a pub/sub broker.
1818func NewBroker[T any]() *Broker[T] {
19+ return NewBrokerWithOptions[T](bufferSize, 1000)
20+}
21+
22+func NewBrokerWithOptions[T any](channelBufferSize, maxEvents int) *Broker[T] {
1923 b := &Broker[T]{
20− subs: make(map[chan Event[T]]struct{}),
21− done: make(chan struct{}),
24+ subs: make(map[chan Event[T]]struct{}),
25+ done: make(chan struct{}),
26+ subCount: 0,
27+ maxEvents: maxEvents,
2228 }
2329 return b
2430}
2531
26−// Shutdown the broker, terminating any subscriptions.
2732func (b *Broker[T]) Shutdown() {
28− close(b.done)
33+ select {
34+ case <-b.done: // Already closed
35+ return
36+ default:
37+ close(b.done)
38+ }
2939
3040 b.mu.Lock()
3141 defer b.mu.Unlock()
3242
33− // Remove each subscriber entry, so Publish() cannot send any further
34− // messages, and close each subscriber's channel, so the subscriber cannot
35− // consume any more messages.
3643 for ch := range b.subs {
3744 delete(b.subs, ch)
3845 close(ch)
3946 }
47+
48+ b.subCount = 0
4049}
4150
42−// Subscribe subscribes the caller to a stream of events. The returned channel
43−// is closed when the broker is shutdown.
4451func (b *Broker[T]) Subscribe(ctx context.Context) <-chan Event[T] {
4552 b.mu.Lock()
4653 defer b.mu.Unlock()
4754
48− // Check if broker has shutdown and if so return closed channel
4955 select {
5056 case <-b.done:
5157 ch := make(chan Event[T])
@@ -54,18 +60,16 @@ func (b *Broker[T]) Subscribe(ctx context.Context) <-chan Event[T] {
5460 default:
5561 }
5662
57− // Subscribe
5863 sub := make(chan Event[T], bufferSize)
5964 b.subs[sub] = struct{}{}
65+ b.subCount++
6066
61− // Unsubscribe when context is done.
6267 go func() {
6368 <-ctx.Done()
6469
6570 b.mu.Lock()
6671 defer b.mu.Unlock()
6772
68− // Check if broker has shutdown and if so do nothing
6973 select {
7074 case <-b.done:
7175 return
@@ -74,21 +78,39 @@ func (b *Broker[T]) Subscribe(ctx context.Context) <-chan Event[T] {
7478
7579 delete(b.subs, sub)
7680 close(sub)
81+ b.subCount--
7782 }()
7883
7984 return sub
8085}
8186
82−// Publish an event to subscribers.
87+func (b *Broker[T]) GetSubscriberCount() int {
88+ b.mu.RLock()
89+ defer b.mu.RUnlock()
90+ return b.subCount
91+}
92+
8393func (b *Broker[T]) Publish(t EventType, payload T) {
84− b.mu.Lock()
85− defer b.mu.Unlock()
94+ b.mu.RLock()
95+ select {
96+ case <-b.done:
97+ b.mu.RUnlock()
98+ return
99+ default:
100+ }
86101
102+ subscribers := make([]chan Event[T], 0, len(b.subs))
87103 for sub := range b.subs {
104+ subscribers = append(subscribers, sub)
105+ }
106+ b.mu.RUnlock()
107+
108+ event := Event[T]{Type: t, Payload: payload}
109+
110+ for _, sub := range subscribers {
88111 select {
89− case sub <- Event[T]{Type: t, Payload: payload}:
90− case <-b.done:
91− return
112+ case sub <- event:
113+ default:
92114 }
93115 }
94116}
internal/tui/components/chat/list.go+1
@@ -370,6 +370,7 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
370370 delete(m.cachedContent, msg.ID)
371371 }
372372 m.uiMessages = make([]uiMessage, 0)
373+ m.renderView()
373374 return nil
374375}
375376
internal/tui/components/core/status.go+12−3
@@ -18,6 +18,11 @@ import (
1818 "github.com/kujtimiihoxha/opencode/internal/tui/util"
1919)
2020
21+type StatusCmp interface {
22+ tea.Model
23+ SetHelpMsg(string)
24+}
25+
2126type statusCmp struct {
2227 info util.InfoMsg
2328 width int
@@ -146,7 +151,7 @@ func (m *statusCmp) projectDiagnostics() string {
146151 break
147152 }
148153 }
149−
154+
150155 // If any server is initializing, show that status
151156 if initializing {
152157 return lipgloss.NewStyle().
@@ -154,7 +159,7 @@ func (m *statusCmp) projectDiagnostics() string {
154159 Foreground(styles.Peach).
155160 Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon))
156161 }
157−
162+
158163 errorDiagnostics := []protocol.Diagnostic{}
159164 warnDiagnostics := []protocol.Diagnostic{}
160165 hintDiagnostics := []protocol.Diagnostic{}
@@ -235,7 +240,11 @@ func (m statusCmp) model() string {
235240 return styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render(model.Name)
236241}
237242
238−func NewStatusCmp(lspClients map[string]*lsp.Client) tea.Model {
243+func (m statusCmp) SetHelpMsg(s string) {
244+ helpWidget = styles.Padded.Background(styles.Forground).Foreground(styles.BackgroundDarker).Bold(true).Render(s)
245+}
246+
247+func NewStatusCmp(lspClients map[string]*lsp.Client) StatusCmp {
239248 return &statusCmp{
240249 messageTTL: 10 * time.Second,
241250 lspClients: lspClients,
internal/tui/tui.go+51−12
@@ -39,12 +39,18 @@ var keys = keyMap{
3939 key.WithKeys("ctrl+_"),
4040 key.WithHelp("ctrl+?", "toggle help"),
4141 ),
42+
4243 SwitchSession: key.NewBinding(
4344 key.WithKeys("ctrl+a"),
4445 key.WithHelp("ctrl+a", "switch session"),
4546 ),
4647}
4748
49+var helpEsc = key.NewBinding(
50+ key.WithKeys("?"),
51+ key.WithHelp("?", "toggle help"),
52+)
53+
4854var returnKey = key.NewBinding(
4955 key.WithKeys("esc"),
5056 key.WithHelp("esc", "close"),
@@ -61,7 +67,7 @@ type appModel struct {
6167 previousPage page.PageID
6268 pages map[page.PageID]tea.Model
6369 loadedPages map[page.PageID]bool
64− status tea.Model
70+ status core.StatusCmp
6571 app *app.App
6672
6773 showPermissions bool
@@ -75,6 +81,8 @@ type appModel struct {
7581
7682 showSessionDialog bool
7783 sessionDialog dialog.SessionDialog
84+
85+ editingMode bool
7886}
7987
8088func (a appModel) Init() tea.Cmd {
@@ -101,7 +109,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
101109 msg.Height -= 1 // Make space for the status bar
102110 a.width, a.height = msg.Width, msg.Height
103111
104− a.status, _ = a.status.Update(msg)
112+ s, _ := a.status.Update(msg)
113+ a.status = s.(core.StatusCmp)
105114 a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
106115 cmds = append(cmds, cmd)
107116
@@ -118,45 +127,56 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
118127 cmds = append(cmds, sessionCmd)
119128
120129 return a, tea.Batch(cmds...)
121−
130+ case chat.EditorFocusMsg:
131+ a.editingMode = bool(msg)
122132 // Status
123133 case util.InfoMsg:
124− a.status, cmd = a.status.Update(msg)
134+ s, cmd := a.status.Update(msg)
135+ a.status = s.(core.StatusCmp)
125136 cmds = append(cmds, cmd)
126137 return a, tea.Batch(cmds...)
127138 case pubsub.Event[logging.LogMessage]:
128139 if msg.Payload.Persist {
129140 switch msg.Payload.Level {
130141 case "error":
131− a.status, cmd = a.status.Update(util.InfoMsg{
142+ s, cmd := a.status.Update(util.InfoMsg{
132143 Type: util.InfoTypeError,
133144 Msg: msg.Payload.Message,
134145 TTL: msg.Payload.PersistTime,
135146 })
147+ a.status = s.(core.StatusCmp)
148+ cmds = append(cmds, cmd)
136149 case "info":
137− a.status, cmd = a.status.Update(util.InfoMsg{
150+ s, cmd := a.status.Update(util.InfoMsg{
138151 Type: util.InfoTypeInfo,
139152 Msg: msg.Payload.Message,
140153 TTL: msg.Payload.PersistTime,
141154 })
155+ a.status = s.(core.StatusCmp)
156+ cmds = append(cmds, cmd)
157+
142158 case "warn":
143− a.status, cmd = a.status.Update(util.InfoMsg{
159+ s, cmd := a.status.Update(util.InfoMsg{
144160 Type: util.InfoTypeWarn,
145161 Msg: msg.Payload.Message,
146162 TTL: msg.Payload.PersistTime,
147163 })
148164
165+ a.status = s.(core.StatusCmp)
166+ cmds = append(cmds, cmd)
149167 default:
150− a.status, cmd = a.status.Update(util.InfoMsg{
168+ s, cmd := a.status.Update(util.InfoMsg{
151169 Type: util.InfoTypeInfo,
152170 Msg: msg.Payload.Message,
153171 TTL: msg.Payload.PersistTime,
154172 })
173+ a.status = s.(core.StatusCmp)
174+ cmds = append(cmds, cmd)
155175 }
156− cmds = append(cmds, cmd)
157176 }
158177 case util.ClearStatusMsg:
159− a.status, _ = a.status.Update(msg)
178+ s, _ := a.status.Update(msg)
179+ a.status = s.(core.StatusCmp)
160180
161181 // Permission
162182 case pubsub.Event[permission.PermissionRequest]:
@@ -243,7 +263,16 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
243263 }
244264 a.showHelp = !a.showHelp
245265 return a, nil
266+ case key.Matches(msg, helpEsc):
267+ if !a.editingMode {
268+ if a.showQuit {
269+ return a, nil
270+ }
271+ a.showHelp = !a.showHelp
272+ return a, nil
273+ }
246274 }
275+
247276 }
248277
249278 if a.showQuit {
@@ -275,7 +304,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
275304 }
276305 }
277306
278− a.status, _ = a.status.Update(msg)
307+ s, _ := a.status.Update(msg)
308+ a.status = s.(core.StatusCmp)
279309 a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
280310 cmds = append(cmds, cmd)
281311 return a, tea.Batch(cmds...)
@@ -326,6 +356,12 @@ func (a appModel) View() string {
326356 )
327357 }
328358
359+ if a.editingMode {
360+ a.status.SetHelpMsg("ctrl+? help")
361+ } else {
362+ a.status.SetHelpMsg("? help")
363+ }
364+
329365 if a.showHelp {
330366 bindings := layout.KeyMapToSlice(keys)
331367 if p, ok := a.pages[a.currentPage].(layout.Bindings); ok {
@@ -337,7 +373,9 @@ func (a appModel) View() string {
337373 if a.currentPage == page.LogsPage {
338374 bindings = append(bindings, logsKeyReturnKey)
339375 }
340−
376+ if !a.editingMode {
377+ bindings = append(bindings, helpEsc)
378+ }
341379 a.help.SetBindings(bindings)
342380
343381 overlay := a.help.View()
@@ -398,6 +436,7 @@ func New(app *app.App) tea.Model {
398436 sessionDialog: dialog.NewSessionDialogCmp(),
399437 permissions: dialog.NewPermissionDialogCmp(),
400438 app: app,
439+ editingMode: true,
401440 pages: map[page.PageID]tea.Model{
402441 page.ChatPage: page.NewChatPage(app),
403442 page.LogsPage: page.NewLogsPage(),