on main help sign in
fix logs and add cancellation
commit36f201d5d3aaba7e0285d86cf1c0cf6b54769cff authorKujtim Hoxha <kujtimii.h@gmail.com> dateThu 10 Apr 2025, 13:29 (2 years ago) parent0b007b9
23 files changed+343−283
cmd/root.go+15−4 go.mod+2−2 internal/app/services.go+6−8 internal/db/connect.go+8−10 internal/llm/agent/agent-tool.go+1−1 internal/llm/agent/agent.go+135−13 internal/llm/agent/coder.go+3−2 internal/llm/agent/mcp-tools.go+4−6 internal/llm/agent/task.go+3−2 internal/logging/default.go−12 internal/logging/logger.go+17−119 internal/logging/logging.go−23 internal/logging/writer.go+45−4 internal/lsp/client.go+3−5 internal/lsp/handlers.go+7−6 internal/lsp/transport.go+16−15 internal/lsp/watcher/watcher.go+28−30 internal/tui/components/core/status.go+4−4 internal/tui/components/logs/details.go+1−1 internal/tui/components/logs/table.go+2−4 internal/tui/components/repl/editor.go+33−8 internal/tui/components/repl/messages.go+8−3 internal/tui/tui.go+2−1
cmd/root.go+15−4
@@ -2,6 +2,7 @@ package cmd
22
33import (
44 "context"
5+ "log/slog"
56 "os"
67 "sync"
78
@@ -10,6 +11,7 @@ import (
1011 "github.com/kujtimiihoxha/termai/internal/config"
1112 "github.com/kujtimiihoxha/termai/internal/db"
1213 "github.com/kujtimiihoxha/termai/internal/llm/agent"
14+ "github.com/kujtimiihoxha/termai/internal/logging"
1315 "github.com/kujtimiihoxha/termai/internal/tui"
1416 zone "github.com/lrstanley/bubblezone"
1517 "github.com/spf13/cobra"
@@ -26,6 +28,16 @@ var rootCmd = &cobra.Command{
2628 }
2729 debug, _ := cmd.Flags().GetBool("debug")
2830 err := config.Load(debug)
31+ cfg := config.Get()
32+ defaultLevel := slog.LevelInfo
33+ if cfg.Debug {
34+ defaultLevel = slog.LevelDebug
35+ }
36+ logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
37+ Level: defaultLevel,
38+ }))
39+ slog.SetDefault(logger)
40+
2941 if err != nil {
3042 return err
3143 }
@@ -37,14 +49,14 @@ var rootCmd = &cobra.Command{
3749
3850 app := app.New(ctx, conn)
3951 defer app.Close()
40− app.Logger.Info("Starting termai...")
52+ logging.Info("Starting termai...")
4153 zone.NewGlobal()
4254 tui := tea.NewProgram(
4355 tui.New(app),
4456 tea.WithAltScreen(),
4557 tea.WithMouseCellMotion(),
4658 )
47− app.Logger.Info("Setting up subscriptions...")
59+ logging.Info("Setting up subscriptions...")
4860 ch, unsub := setupSubscriptions(app)
4961 defer unsub()
5062
@@ -66,9 +78,8 @@ func setupSubscriptions(app *app.App) (chan tea.Msg, func()) {
6678 ch := make(chan tea.Msg)
6779 wg := sync.WaitGroup{}
6880 ctx, cancel := context.WithCancel(app.Context)
69−
7081 {
71− sub := app.Logger.Subscribe(ctx)
82+ sub := logging.Subscribe(ctx)
7283 wg.Add(1)
7384 go func() {
7485 for ev := range sub {
go.mod+2−2
@@ -33,7 +33,7 @@ require (
3333 github.com/spf13/cobra v1.9.1
3434 github.com/spf13/viper v1.20.0
3535 github.com/stretchr/testify v1.10.0
36− golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
36+ golang.org/x/net v0.34.0
3737 google.golang.org/api v0.215.0
3838)
3939
@@ -116,10 +116,10 @@ require (
116116 go.uber.org/multierr v1.9.0 // indirect
117117 golang.design/x/clipboard v0.7.0 // indirect
118118 golang.org/x/crypto v0.33.0 // indirect
119+ golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
119120 golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 // indirect
120121 golang.org/x/image v0.14.0 // indirect
121122 golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
122− golang.org/x/net v0.34.0 // indirect
123123 golang.org/x/oauth2 v0.25.0 // indirect
124124 golang.org/x/sync v0.12.0 // indirect
125125 golang.org/x/sys v0.31.0 // indirect
internal/app/services.go+6−8
@@ -3,6 +3,7 @@ package app
33import (
44 "context"
55 "database/sql"
6+ "log/slog"
67
78 "github.com/kujtimiihoxha/termai/internal/config"
89 "github.com/kujtimiihoxha/termai/internal/db"
@@ -23,16 +24,14 @@ type App struct {
2324
2425 LSPClients map[string]*lsp.Client
2526
26− Logger logging.Interface
27−
2827 ceanups []func()
2928}
3029
3130func New(ctx context.Context, conn *sql.DB) *App {
3231 cfg := config.Get()
32+ logging.Info("Debug mode enabled")
33+
3334 q := db.New(conn)
34− log := logging.Get()
35− log.SetLevel(cfg.Log.Level)
3635 sessions := session.NewService(ctx, q)
3736 messages := message.NewService(ctx, q)
3837
@@ -41,7 +40,6 @@ func New(ctx context.Context, conn *sql.DB) *App {
4140 Sessions: sessions,
4241 Messages: messages,
4342 Permissions: permission.NewPermissionService(),
44− Logger: log,
4543 LSPClients: make(map[string]*lsp.Client),
4644 }
4745
@@ -52,13 +50,13 @@ func New(ctx context.Context, conn *sql.DB) *App {
5250 })
5351 workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient)
5452 if err != nil {
55− log.Error("Failed to create LSP client for", name, err)
53+ logging.Error("Failed to create LSP client for", name, err)
5654 continue
5755 }
5856
5957 _, err = lspClient.InitializeLSPClient(ctx, config.WorkingDirectory())
6058 if err != nil {
61− log.Error("Initialize failed", "error", err)
59+ logging.Error("Initialize failed", "error", err)
6260 continue
6361 }
6462 go workspaceWatcher.WatchWorkspace(ctx, config.WorkingDirectory())
@@ -74,5 +72,5 @@ func (a *App) Close() {
7472 for _, client := range a.LSPClients {
7573 client.Close()
7674 }
77− a.Logger.Info("App closed")
75+ slog.Info("App closed")
7876}
internal/db/connect.go+8−10
@@ -16,8 +16,6 @@ import (
1616 "github.com/kujtimiihoxha/termai/internal/logging"
1717)
1818
19−var log = logging.Get()
20−
2119func Connect() (*sql.DB, error) {
2220 dataDir := config.Get().Data.Directory
2321 if dataDir == "" {
@@ -50,43 +48,43 @@ func Connect() (*sql.DB, error) {
5048
5149 for _, pragma := range pragmas {
5250 if _, err = db.Exec(pragma); err != nil {
53− log.Warn("Failed to set pragma", pragma, err)
51+ logging.Warn("Failed to set pragma", pragma, err)
5452 } else {
55− log.Warn("Set pragma", "pragma", pragma)
53+ logging.Warn("Set pragma", "pragma", pragma)
5654 }
5755 }
5856
5957 // Initialize schema from embedded file
6058 d, err := iofs.New(FS, "migrations")
6159 if err != nil {
62− log.Error("Failed to open embedded migrations", "error", err)
60+ logging.Error("Failed to open embedded migrations", "error", err)
6361 db.Close()
6462 return nil, fmt.Errorf("failed to open embedded migrations: %w", err)
6563 }
6664
6765 driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
6866 if err != nil {
69− log.Error("Failed to create SQLite driver", "error", err)
67+ logging.Error("Failed to create SQLite driver", "error", err)
7068 db.Close()
7169 return nil, fmt.Errorf("failed to create SQLite driver: %w", err)
7270 }
7371
7472 m, err := migrate.NewWithInstance("iofs", d, "ql", driver)
7573 if err != nil {
76− log.Error("Failed to create migration instance", "error", err)
74+ logging.Error("Failed to create migration instance", "error", err)
7775 db.Close()
7876 return nil, fmt.Errorf("failed to create migration instance: %w", err)
7977 }
8078
8179 err = m.Up()
8280 if err != nil && err != migrate.ErrNoChange {
83− log.Error("Migration failed", "error", err)
81+ logging.Error("Migration failed", "error", err)
8482 db.Close()
8583 return nil, fmt.Errorf("failed to apply schema: %w", err)
8684 } else if err == migrate.ErrNoChange {
87− log.Info("No schema changes to apply")
85+ logging.Info("No schema changes to apply")
8886 } else {
89− log.Info("Schema migration applied successfully")
87+ logging.Info("Schema migration applied successfully")
9088 }
9189
9290 return db, nil
internal/llm/agent/agent-tool.go+1−1
@@ -56,7 +56,7 @@ func (b *agentTool) Run(ctx context.Context, call tools.ToolCall) (tools.ToolRes
5656 return tools.NewTextErrorResponse(fmt.Sprintf("error creating session: %s", err)), nil
5757 }
5858
59− err = agent.Generate(session.ID, params.Prompt)
59+ err = agent.Generate(ctx, session.ID, params.Prompt)
6060 if err != nil {
6161 return tools.NewTextErrorResponse(fmt.Sprintf("error generating agent: %s", err)), nil
6262 }
internal/llm/agent/agent.go+135−13
@@ -13,11 +13,12 @@ import (
1313 "github.com/kujtimiihoxha/termai/internal/llm/prompt"
1414 "github.com/kujtimiihoxha/termai/internal/llm/provider"
1515 "github.com/kujtimiihoxha/termai/internal/llm/tools"
16+ "github.com/kujtimiihoxha/termai/internal/logging"
1617 "github.com/kujtimiihoxha/termai/internal/message"
1718)
1819
1920type Agent interface {
20− Generate(sessionID string, content string) error
21+ Generate(ctx context.Context, sessionID string, content string) error
2122}
2223
2324type agent struct {
@@ -28,9 +29,9 @@ type agent struct {
2829 titleGenerator provider.Provider
2930}
3031
31−func (c *agent) handleTitleGeneration(sessionID, content string) {
32+func (c *agent) handleTitleGeneration(ctx context.Context, sessionID, content string) {
3233 response, err := c.titleGenerator.SendMessages(
33− c.Context,
34+ ctx,
3435 []message.Message{
3536 {
3637 Role: message.User,
@@ -91,13 +92,16 @@ func (c *agent) processEvent(
9192 assistantMsg.AppendContent(event.Content)
9293 return c.Messages.Update(*assistantMsg)
9394 case provider.EventError:
94− c.App.Logger.PersistError(event.Error.Error())
95+ if errors.Is(event.Error, context.Canceled) {
96+ return nil
97+ }
98+ logging.ErrorPersist(event.Error.Error())
9599 return event.Error
96100 case provider.EventWarning:
97− c.App.Logger.PersistWarn(event.Info)
101+ logging.WarnPersist(event.Info)
98102 return nil
99103 case provider.EventInfo:
100− c.App.Logger.PersistInfo(event.Info)
104+ logging.InfoPersist(event.Info)
101105 case provider.EventComplete:
102106 assistantMsg.SetToolCalls(event.Response.ToolCalls)
103107 assistantMsg.AddFinish(event.Response.FinishReason)
@@ -115,12 +119,37 @@ func (c *agent) ExecuteTools(ctx context.Context, toolCalls []message.ToolCall,
115119 var wg sync.WaitGroup
116120 toolResults := make([]message.ToolResult, len(toolCalls))
117121 mutex := &sync.Mutex{}
122+ errChan := make(chan error, 1)
123+
124+ // Create a child context that can be canceled
125+ ctx, cancel := context.WithCancel(ctx)
126+ defer cancel()
118127
119128 for i, tc := range toolCalls {
120129 wg.Add(1)
121130 go func(index int, toolCall message.ToolCall) {
122131 defer wg.Done()
123132
133+ // Check if context is already canceled
134+ select {
135+ case <-ctx.Done():
136+ mutex.Lock()
137+ toolResults[index] = message.ToolResult{
138+ ToolCallID: toolCall.ID,
139+ Content: "Tool execution canceled",
140+ IsError: true,
141+ }
142+ mutex.Unlock()
143+
144+ // Send cancellation error to error channel if it's empty
145+ select {
146+ case errChan <- ctx.Err():
147+ default:
148+ }
149+ return
150+ default:
151+ }
152+
124153 response := ""
125154 isError := false
126155 found := false
@@ -133,8 +162,19 @@ func (c *agent) ExecuteTools(ctx context.Context, toolCalls []message.ToolCall,
133162 Name: toolCall.Name,
134163 Input: toolCall.Input,
135164 })
165+
136166 if toolErr != nil {
137− response = fmt.Sprintf("error running tool: %s", toolErr)
167+ if errors.Is(toolErr, context.Canceled) {
168+ response = "Tool execution canceled"
169+
170+ // Send cancellation error to error channel if it's empty
171+ select {
172+ case errChan <- ctx.Err():
173+ default:
174+ }
175+ } else {
176+ response = fmt.Sprintf("error running tool: %s", toolErr)
177+ }
138178 isError = true
139179 } else {
140180 response = toolResult.Content
@@ -160,7 +200,24 @@ func (c *agent) ExecuteTools(ctx context.Context, toolCalls []message.ToolCall,
160200 }(i, tc)
161201 }
162202
163− wg.Wait()
203+ // Wait for all goroutines to finish or context to be canceled
204+ done := make(chan struct{})
205+ go func() {
206+ wg.Wait()
207+ close(done)
208+ }()
209+
210+ select {
211+ case <-done:
212+ // All tools completed successfully
213+ case err := <-errChan:
214+ // One of the tools encountered a cancellation
215+ return toolResults, err
216+ case <-ctx.Done():
217+ // Context was canceled externally
218+ return toolResults, ctx.Err()
219+ }
220+
164221 return toolResults, nil
165222}
166223
@@ -188,14 +245,14 @@ func (c *agent) handleToolExecution(
188245 return &msg, err
189246}
190247
191−func (c *agent) generate(sessionID string, content string) error {
248+func (c *agent) generate(ctx context.Context, sessionID string, content string) error {
192249 messages, err := c.Messages.List(sessionID)
193250 if err != nil {
194251 return err
195252 }
196253
197254 if len(messages) == 0 {
198− go c.handleTitleGeneration(sessionID, content)
255+ go c.handleTitleGeneration(ctx, sessionID, content)
199256 }
200257
201258 userMsg, err := c.Messages.Create(sessionID, message.CreateMessageParams{
@@ -212,9 +269,36 @@ func (c *agent) generate(sessionID string, content string) error {
212269
213270 messages = append(messages, userMsg)
214271 for {
272+ select {
273+ case <-ctx.Done():
274+ assistantMsg, err := c.Messages.Create(sessionID, message.CreateMessageParams{
275+ Role: message.Assistant,
276+ Parts: []message.ContentPart{},
277+ })
278+ if err != nil {
279+ return err
280+ }
281+ assistantMsg.AddFinish("canceled")
282+ c.Messages.Update(assistantMsg)
283+ return context.Canceled
284+ default:
285+ // Continue processing
286+ }
215287
216− eventChan, err := c.agent.StreamResponse(c.Context, messages, c.tools)
288+ eventChan, err := c.agent.StreamResponse(ctx, messages, c.tools)
217289 if err != nil {
290+ if errors.Is(err, context.Canceled) {
291+ assistantMsg, err := c.Messages.Create(sessionID, message.CreateMessageParams{
292+ Role: message.Assistant,
293+ Parts: []message.ContentPart{},
294+ })
295+ if err != nil {
296+ return err
297+ }
298+ assistantMsg.AddFinish("canceled")
299+ c.Messages.Update(assistantMsg)
300+ return context.Canceled
301+ }
218302 return err
219303 }
220304
@@ -228,19 +312,47 @@ func (c *agent) generate(sessionID string, content string) error {
228312 for event := range eventChan {
229313 err = c.processEvent(sessionID, &assistantMsg, event)
230314 if err != nil {
315+ if errors.Is(err, context.Canceled) {
316+ assistantMsg.AddFinish("canceled")
317+ c.Messages.Update(assistantMsg)
318+ return context.Canceled
319+ }
231320 assistantMsg.AddFinish("error:" + err.Error())
232321 c.Messages.Update(assistantMsg)
233322 return err
234323 }
324+
325+ select {
326+ case <-ctx.Done():
327+ assistantMsg.AddFinish("canceled")
328+ c.Messages.Update(assistantMsg)
329+ return context.Canceled
330+ default:
331+ }
235332 }
236333
237− msg, err := c.handleToolExecution(c.Context, assistantMsg)
334+ // Check for context cancellation before tool execution
335+ select {
336+ case <-ctx.Done():
337+ assistantMsg.AddFinish("canceled")
338+ c.Messages.Update(assistantMsg)
339+ return context.Canceled
340+ default:
341+ // Continue processing
342+ }
238343
239− c.Messages.Update(assistantMsg)
344+ msg, err := c.handleToolExecution(ctx, assistantMsg)
240345 if err != nil {
346+ if errors.Is(err, context.Canceled) {
347+ assistantMsg.AddFinish("canceled")
348+ c.Messages.Update(assistantMsg)
349+ return context.Canceled
350+ }
241351 return err
242352 }
243353
354+ c.Messages.Update(assistantMsg)
355+
244356 if len(assistantMsg.ToolCalls()) == 0 {
245357 break
246358 }
@@ -249,6 +361,16 @@ func (c *agent) generate(sessionID string, content string) error {
249361 if msg != nil {
250362 messages = append(messages, *msg)
251363 }
364+
365+ // Check for context cancellation after tool execution
366+ select {
367+ case <-ctx.Done():
368+ assistantMsg.AddFinish("canceled")
369+ c.Messages.Update(assistantMsg)
370+ return context.Canceled
371+ default:
372+ // Continue processing
373+ }
252374 }
253375 return nil
254376}
internal/llm/agent/coder.go+3−2
@@ -1,6 +1,7 @@
11package agent
22
33import (
4+ "context"
45 "errors"
56
67 "github.com/kujtimiihoxha/termai/internal/app"
@@ -28,9 +29,9 @@ func (c *coderAgent) setAgentTool(sessionID string) {
2829 }
2930}
3031
31−func (c *coderAgent) Generate(sessionID string, content string) error {
32+func (c *coderAgent) Generate(ctx context.Context, sessionID string, content string) error {
3233 c.setAgentTool(sessionID)
33− return c.generate(sessionID, content)
34+ return c.generate(ctx, sessionID, content)
3435}
3536
3637func NewCoderAgent(app *app.App) (Agent, error) {
internal/llm/agent/mcp-tools.go+4−6
@@ -22,8 +22,6 @@ type mcpTool struct {
2222 permissions permission.Service
2323}
2424
25−var logger = logging.Get()
26−
2725type MCPClient interface {
2826 Initialize(
2927 ctx context.Context,
@@ -143,13 +141,13 @@ func getTools(ctx context.Context, name string, m config.MCPServer, permissions
143141
144142 _, err := c.Initialize(ctx, initRequest)
145143 if err != nil {
146− logger.Error("error initializing mcp client", "error", err)
144+ logging.Error("error initializing mcp client", "error", err)
147145 return stdioTools
148146 }
149147 toolsRequest := mcp.ListToolsRequest{}
150148 tools, err := c.ListTools(ctx, toolsRequest)
151149 if err != nil {
152− logger.Error("error listing tools", "error", err)
150+ logging.Error("error listing tools", "error", err)
153151 return stdioTools
154152 }
155153 for _, t := range tools.Tools {
@@ -172,7 +170,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
172170 m.Args...,
173171 )
174172 if err != nil {
175− logger.Error("error creating mcp client", "error", err)
173+ logging.Error("error creating mcp client", "error", err)
176174 continue
177175 }
178176
@@ -183,7 +181,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
183181 client.WithHeaders(m.Headers),
184182 )
185183 if err != nil {
186− logger.Error("error creating mcp client", "error", err)
184+ logging.Error("error creating mcp client", "error", err)
187185 continue
188186 }
189187 mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
internal/llm/agent/task.go+3−2
@@ -1,6 +1,7 @@
11package agent
22
33import (
4+ "context"
45 "errors"
56
67 "github.com/kujtimiihoxha/termai/internal/app"
@@ -13,8 +14,8 @@ type taskAgent struct {
1314 *agent
1415}
1516
16−func (c *taskAgent) Generate(sessionID string, content string) error {
17− return c.generate(sessionID, content)
17+func (c *taskAgent) Generate(ctx context.Context, sessionID string, content string) error {
18+ return c.generate(ctx, sessionID, content)
1819}
1920
2021func NewTaskAgent(app *app.App) (Agent, error) {
internal/logging/default.godeleted−12
@@ -1,12 +0,0 @@
1−package logging
2−
3−var defaultLogger Interface
4−
5−func Get() Interface {
6− if defaultLogger == nil {
7− defaultLogger = NewLogger(Options{
8− Level: "info",
9− })
10− }
11− return defaultLogger
12−}
internal/logging/logger.go+17−119
@@ -1,141 +1,39 @@
11package logging
22
3−import (
4− "context"
5− "io"
6− "log/slog"
7− "slices"
3+import "log/slog"
84
9− "github.com/kujtimiihoxha/termai/internal/pubsub"
10− "golang.org/x/exp/maps"
11−)
12−
13−const DefaultLevel = "info"
14−
15−const (
16− persistKeyArg = "$persist"
17− PersistTimeArg = "$persist_time"
18−)
19−
20−var levels = map[string]slog.Level{
21− "debug": slog.LevelDebug,
22− DefaultLevel: slog.LevelInfo,
23− "warn": slog.LevelWarn,
24− "error": slog.LevelError,
25−}
26−
27−func ValidLevels() []string {
28− keys := maps.Keys(levels)
29− slices.SortFunc(keys, func(a, b string) int {
30− if a == DefaultLevel {
31− return -1
32− }
33− if b == DefaultLevel {
34− return 1
35− }
36− if a < b {
37− return -1
38− }
39− return 1
40− })
41− return keys
42−}
43−
44−func NewLogger(opts Options) Interface {
45− logger := &Logger{}
46− broker := pubsub.NewBroker[LogMessage]()
47− writer := &writer{
48− messages: []LogMessage{},
49− Broker: broker,
50− }
51−
52− handler := slog.NewTextHandler(
53− io.MultiWriter(writer),
54− &slog.HandlerOptions{
55− Level: slog.Level(levels[opts.Level]),
56− },
57− )
58− logger.logger = slog.New(handler)
59− logger.writer = writer
60−
61− return logger
5+func Info(msg string, args ...any) {
6+ slog.Info(msg, args...)
627}
638
64−type Options struct {
65− Level string
9+func Debug(msg string, args ...any) {
10+ slog.Debug(msg, args...)
6611}
6712
68−type Logger struct {
69− logger *slog.Logger
70− writer *writer
13+func Warn(msg string, args ...any) {
14+ slog.Warn(msg, args...)
7115}
7216
73−func (l *Logger) SetLevel(level string) {
74− if _, ok := levels[level]; !ok {
75− level = DefaultLevel
76− }
77− handler := slog.NewTextHandler(
78− io.MultiWriter(l.writer),
79− &slog.HandlerOptions{
80− Level: levels[level],
81− },
82− )
83− l.logger = slog.New(handler)
17+func Error(msg string, args ...any) {
18+ slog.Error(msg, args...)
8419}
8520
86−// PersistDebug implements Interface.
87−func (l *Logger) PersistDebug(msg string, args ...any) {
21+func InfoPersist(msg string, args ...any) {
8822 args = append(args, persistKeyArg, true)
89− l.Debug(msg, args...)
23+ slog.Info(msg, args...)
9024}
9125
92−// PersistError implements Interface.
93−func (l *Logger) PersistError(msg string, args ...any) {
26+func DebugPersist(msg string, args ...any) {
9427 args = append(args, persistKeyArg, true)
95− l.Error(msg, args...)
28+ slog.Debug(msg, args...)
9629}
9730
98−// PersistInfo implements Interface.
99−func (l *Logger) PersistInfo(msg string, args ...any) {
31+func WarnPersist(msg string, args ...any) {
10032 args = append(args, persistKeyArg, true)
101− l.Info(msg, args...)
33+ slog.Warn(msg, args...)
10234}
10335
104−// PersistWarn implements Interface.
105−func (l *Logger) PersistWarn(msg string, args ...any) {
36+func ErrorPersist(msg string, args ...any) {
10637 args = append(args, persistKeyArg, true)
107− l.Warn(msg, args...)
108−}
109−
110−func (l *Logger) Debug(msg string, args ...any) {
111− l.logger.Debug(msg, args...)
112−}
113−
114−func (l *Logger) Info(msg string, args ...any) {
115− l.logger.Info(msg, args...)
116−}
117−
118−func (l *Logger) Warn(msg string, args ...any) {
119− l.logger.Warn(msg, args...)
120−}
121−
122−func (l *Logger) Error(msg string, args ...any) {
123− l.logger.Error(msg, args...)
124−}
125−
126−func (l *Logger) List() []LogMessage {
127− return l.writer.messages
128−}
129−
130−func (l *Logger) Get(id string) (LogMessage, error) {
131− for _, msg := range l.writer.messages {
132− if msg.ID == id {
133− return msg, nil
134− }
135− }
136− return LogMessage{}, io.EOF
137−}
138−
139−func (l *Logger) Subscribe(ctx context.Context) <-chan pubsub.Event[LogMessage] {
140− return l.writer.Subscribe(ctx)
38+ slog.Error(msg, args...)
14139}
internal/logging/logging.godeleted−23
@@ -1,23 +0,0 @@
1−package logging
2−
3−import (
4− "context"
5−
6− "github.com/kujtimiihoxha/termai/internal/pubsub"
7−)
8−
9−type Interface interface {
10− Debug(msg string, args ...any)
11− Info(msg string, args ...any)
12− Warn(msg string, args ...any)
13− Error(msg string, args ...any)
14− Subscribe(ctx context.Context) <-chan pubsub.Event[LogMessage]
15−
16− PersistDebug(msg string, args ...any)
17− PersistInfo(msg string, args ...any)
18− PersistWarn(msg string, args ...any)
19− PersistError(msg string, args ...any)
20− List() []LogMessage
21−
22− SetLevel(level string)
23−}
internal/logging/writer.go+45−4
@@ -2,18 +2,47 @@ package logging
22
33import (
44 "bytes"
5+ "context"
56 "fmt"
7+ "strings"
8+ "sync"
69 "time"
710
811 "github.com/go-logfmt/logfmt"
912 "github.com/kujtimiihoxha/termai/internal/pubsub"
1013)
1114
12−type writer struct {
15+const (
16+ persistKeyArg = "$_persist"
17+ PersistTimeArg = "$_persist_time"
18+)
19+
20+type LogData struct {
1321 messages []LogMessage
1422 *pubsub.Broker[LogMessage]
23+ lock sync.Mutex
24+}
25+
26+func (l *LogData) Add(msg LogMessage) {
27+ l.lock.Lock()
28+ defer l.lock.Unlock()
29+ l.messages = append(l.messages, msg)
30+ l.Publish(pubsub.CreatedEvent, msg)
31+}
32+
33+func (l *LogData) List() []LogMessage {
34+ l.lock.Lock()
35+ defer l.lock.Unlock()
36+ return l.messages
37+}
38+
39+var defaultLogData = &LogData{
40+ messages: make([]LogMessage, 0),
41+ Broker: pubsub.NewBroker[LogMessage](),
1542}
1643
44+type writer struct{}
45+
1746func (w *writer) Write(p []byte) (int, error) {
1847 d := logfmt.NewDecoder(bytes.NewReader(p))
1948 for d.ScanRecord() {
@@ -30,7 +59,7 @@ func (w *writer) Write(p []byte) (int, error) {
3059 }
3160 msg.Time = parsed
3261 case "level":
33− msg.Level = string(d.Value())
62+ msg.Level = strings.ToLower(string(d.Value()))
3463 case "msg":
3564 msg.Message = string(d.Value())
3665 default:
@@ -50,11 +79,23 @@ func (w *writer) Write(p []byte) (int, error) {
5079 }
5180 }
5281 }
53− w.messages = append(w.messages, msg)
54− w.Publish(pubsub.CreatedEvent, msg)
82+ defaultLogData.Add(msg)
5583 }
5684 if d.Err() != nil {
5785 return 0, d.Err()
5886 }
5987 return len(p), nil
6088}
89+
90+func NewWriter() *writer {
91+ w := &writer{}
92+ return w
93+}
94+
95+func Subscribe(ctx context.Context) <-chan pubsub.Event[LogMessage] {
96+ return defaultLogData.Subscribe(ctx)
97+}
98+
99+func List() []LogMessage {
100+ return defaultLogData.List()
101+}
internal/lsp/client.go+3−5
@@ -18,8 +18,6 @@ import (
1818 "github.com/kujtimiihoxha/termai/internal/lsp/protocol"
1919)
2020
21−var logger = logging.Get()
22−
2321type Client struct {
2422 Cmd *exec.Cmd
2523 stdin io.WriteCloser
@@ -377,7 +375,7 @@ func (c *Client) CloseFile(ctx context.Context, filepath string) error {
377375 }
378376
379377 if cnf.Debug {
380− logger.Debug("Closing file", "file", filepath)
378+ logging.Debug("Closing file", "file", filepath)
381379 }
382380 if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
383381 return err
@@ -416,12 +414,12 @@ func (c *Client) CloseAllFiles(ctx context.Context) {
416414 for _, filePath := range filesToClose {
417415 err := c.CloseFile(ctx, filePath)
418416 if err != nil && cnf.Debug {
419− logger.Warn("Error closing file", "file", filePath, "error", err)
417+ logging.Warn("Error closing file", "file", filePath, "error", err)
420418 }
421419 }
422420
423421 if cnf.Debug {
424− logger.Debug("Closed all files", "files", filesToClose)
422+ logging.Debug("Closed all files", "files", filesToClose)
425423 }
426424}
427425
internal/lsp/handlers.go+7−6
@@ -4,6 +4,7 @@ import (
44 "encoding/json"
55
66 "github.com/kujtimiihoxha/termai/internal/config"
7+ "github.com/kujtimiihoxha/termai/internal/logging"
78 "github.com/kujtimiihoxha/termai/internal/lsp/protocol"
89 "github.com/kujtimiihoxha/termai/internal/lsp/util"
910)
@@ -17,7 +18,7 @@ func HandleWorkspaceConfiguration(params json.RawMessage) (any, error) {
1718func HandleRegisterCapability(params json.RawMessage) (any, error) {
1819 var registerParams protocol.RegistrationParams
1920 if err := json.Unmarshal(params, &registerParams); err != nil {
20− logger.Error("Error unmarshaling registration params", "error", err)
21+ logging.Error("Error unmarshaling registration params", "error", err)
2122 return nil, err
2223 }
2324
@@ -27,13 +28,13 @@ func HandleRegisterCapability(params json.RawMessage) (any, error) {
2728 // Parse the registration options
2829 optionsJSON, err := json.Marshal(reg.RegisterOptions)
2930 if err != nil {
30− logger.Error("Error marshaling registration options", "error", err)
31+ logging.Error("Error marshaling registration options", "error", err)
3132 continue
3233 }
3334
3435 var options protocol.DidChangeWatchedFilesRegistrationOptions
3536 if err := json.Unmarshal(optionsJSON, &options); err != nil {
36− logger.Error("Error unmarshaling registration options", "error", err)
37+ logging.Error("Error unmarshaling registration options", "error", err)
3738 continue
3839 }
3940
@@ -53,7 +54,7 @@ func HandleApplyEdit(params json.RawMessage) (any, error) {
5354
5455 err := util.ApplyWorkspaceEdit(edit.Edit)
5556 if err != nil {
56− logger.Error("Error applying workspace edit", "error", err)
57+ logging.Error("Error applying workspace edit", "error", err)
5758 return protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil
5859 }
5960
@@ -88,7 +89,7 @@ func HandleServerMessage(params json.RawMessage) {
8889 }
8990 if err := json.Unmarshal(params, &msg); err == nil {
9091 if cnf.Debug {
91− logger.Debug("Server message", "type", msg.Type, "message", msg.Message)
92+ logging.Debug("Server message", "type", msg.Type, "message", msg.Message)
9293 }
9394 }
9495}
@@ -96,7 +97,7 @@ func HandleServerMessage(params json.RawMessage) {
9697func HandleDiagnostics(client *Client, params json.RawMessage) {
9798 var diagParams protocol.PublishDiagnosticsParams
9899 if err := json.Unmarshal(params, &diagParams); err != nil {
99− logger.Error("Error unmarshaling diagnostics params", "error", err)
100+ logging.Error("Error unmarshaling diagnostics params", "error", err)
100101 return
101102 }
102103
internal/lsp/transport.go+16−15
@@ -9,6 +9,7 @@ import (
99 "strings"
1010
1111 "github.com/kujtimiihoxha/termai/internal/config"
12+ "github.com/kujtimiihoxha/termai/internal/logging"
1213)
1314
1415// Write writes an LSP message to the given writer
@@ -20,7 +21,7 @@ func WriteMessage(w io.Writer, msg *Message) error {
2021 cnf := config.Get()
2122
2223 if cnf.Debug {
23− logger.Debug("Sending message to server", "method", msg.Method, "id", msg.ID)
24+ logging.Debug("Sending message to server", "method", msg.Method, "id", msg.ID)
2425 }
2526
2627 _, err = fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(data))
@@ -49,7 +50,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
4950 line = strings.TrimSpace(line)
5051
5152 if cnf.Debug {
52− logger.Debug("Received header", "line", line)
53+ logging.Debug("Received header", "line", line)
5354 }
5455
5556 if line == "" {
@@ -65,7 +66,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
6566 }
6667
6768 if cnf.Debug {
68− logger.Debug("Content-Length", "length", contentLength)
69+ logging.Debug("Content-Length", "length", contentLength)
6970 }
7071
7172 // Read content
@@ -76,7 +77,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
7677 }
7778
7879 if cnf.Debug {
79− logger.Debug("Received content", "content", string(content))
80+ logging.Debug("Received content", "content", string(content))
8081 }
8182
8283 // Parse message
@@ -95,7 +96,7 @@ func (c *Client) handleMessages() {
9596 msg, err := ReadMessage(c.stdout)
9697 if err != nil {
9798 if cnf.Debug {
98− logger.Error("Error reading message", "error", err)
99+ logging.Error("Error reading message", "error", err)
99100 }
100101 return
101102 }
@@ -103,7 +104,7 @@ func (c *Client) handleMessages() {
103104 // Handle server->client request (has both Method and ID)
104105 if msg.Method != "" && msg.ID != 0 {
105106 if cnf.Debug {
106− logger.Debug("Received request from server", "method", msg.Method, "id", msg.ID)
107+ logging.Debug("Received request from server", "method", msg.Method, "id", msg.ID)
107108 }
108109
109110 response := &Message{
@@ -143,7 +144,7 @@ func (c *Client) handleMessages() {
143144
144145 // Send response back to server
145146 if err := WriteMessage(c.stdin, response); err != nil {
146− logger.Error("Error sending response to server", "error", err)
147+ logging.Error("Error sending response to server", "error", err)
147148 }
148149
149150 continue
@@ -157,11 +158,11 @@ func (c *Client) handleMessages() {
157158
158159 if ok {
159160 if cnf.Debug {
160− logger.Debug("Handling notification", "method", msg.Method)
161+ logging.Debug("Handling notification", "method", msg.Method)
161162 }
162163 go handler(msg.Params)
163164 } else if cnf.Debug {
164− logger.Debug("No handler for notification", "method", msg.Method)
165+ logging.Debug("No handler for notification", "method", msg.Method)
165166 }
166167 continue
167168 }
@@ -174,12 +175,12 @@ func (c *Client) handleMessages() {
174175
175176 if ok {
176177 if cnf.Debug {
177− logger.Debug("Received response for request", "id", msg.ID)
178+ logging.Debug("Received response for request", "id", msg.ID)
178179 }
179180 ch <- msg
180181 close(ch)
181182 } else if cnf.Debug {
182− logger.Debug("No handler for response", "id", msg.ID)
183+ logging.Debug("No handler for response", "id", msg.ID)
183184 }
184185 }
185186 }
@@ -191,7 +192,7 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
191192 id := c.nextID.Add(1)
192193
193194 if cnf.Debug {
194− logger.Debug("Making call", "method", method, "id", id)
195+ logging.Debug("Making call", "method", method, "id", id)
195196 }
196197
197198 msg, err := NewRequest(id, method, params)
@@ -217,14 +218,14 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
217218 }
218219
219220 if cnf.Debug {
220− logger.Debug("Request sent", "method", method, "id", id)
221+ logging.Debug("Request sent", "method", method, "id", id)
221222 }
222223
223224 // Wait for response
224225 resp := <-ch
225226
226227 if cnf.Debug {
227− logger.Debug("Received response", "id", id)
228+ logging.Debug("Received response", "id", id)
228229 }
229230
230231 if resp.Error != nil {
@@ -250,7 +251,7 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
250251func (c *Client) Notify(ctx context.Context, method string, params any) error {
251252 cnf := config.Get()
252253 if cnf.Debug {
253− logger.Debug("Sending notification", "method", method)
254+ logging.Debug("Sending notification", "method", method)
254255 }
255256
256257 msg, err := NewNotification(method, params)
internal/lsp/watcher/watcher.go+28−30
@@ -16,8 +16,6 @@ import (
1616 "github.com/kujtimiihoxha/termai/internal/lsp/protocol"
1717)
1818
19−var logger = logging.Get()
20−
2119// WorkspaceWatcher manages LSP file watching
2220type WorkspaceWatcher struct {
2321 client *lsp.Client
@@ -53,7 +51,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
5351
5452 // Print detailed registration information for debugging
5553 if cnf.Debug {
56− logger.Debug("Adding file watcher registrations",
54+ logging.Debug("Adding file watcher registrations",
5755 "id", id,
5856 "watchers", len(watchers),
5957 "total", len(w.registrations),
@@ -61,26 +59,26 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
6159 )
6260
6361 for i, watcher := range watchers {
64− logger.Debug("Registration", "index", i+1)
62+ logging.Debug("Registration", "index", i+1)
6563
6664 // Log the GlobPattern
6765 switch v := watcher.GlobPattern.Value.(type) {
6866 case string:
69− logger.Debug("GlobPattern", "pattern", v)
67+ logging.Debug("GlobPattern", "pattern", v)
7068 case protocol.RelativePattern:
71− logger.Debug("GlobPattern", "pattern", v.Pattern)
69+ logging.Debug("GlobPattern", "pattern", v.Pattern)
7270
7371 // Log BaseURI details
7472 switch u := v.BaseURI.Value.(type) {
7573 case string:
76− logger.Debug("BaseURI", "baseURI", u)
74+ logging.Debug("BaseURI", "baseURI", u)
7775 case protocol.DocumentUri:
78− logger.Debug("BaseURI", "baseURI", u)
76+ logging.Debug("BaseURI", "baseURI", u)
7977 default:
80− logger.Debug("BaseURI", "baseURI", u)
78+ logging.Debug("BaseURI", "baseURI", u)
8179 }
8280 default:
83− logger.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
81+ logging.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
8482 }
8583
8684 // Log WatchKind
@@ -89,7 +87,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
8987 watchKind = *watcher.Kind
9088 }
9189
92− logger.Debug("WatchKind", "kind", watchKind)
90+ logging.Debug("WatchKind", "kind", watchKind)
9391
9492 // Test match against some example paths
9593 testPaths := []string{
@@ -99,7 +97,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
9997
10098 for _, testPath := range testPaths {
10199 isMatch := w.matchesPattern(testPath, watcher.GlobPattern)
102− logger.Debug("Test path", "path", testPath, "matches", isMatch)
100+ logging.Debug("Test path", "path", testPath, "matches", isMatch)
103101 }
104102 }
105103 }
@@ -119,7 +117,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
119117 if d.IsDir() {
120118 if path != w.workspacePath && shouldExcludeDir(path) {
121119 if cnf.Debug {
122− logger.Debug("Skipping excluded directory", "path", path)
120+ logging.Debug("Skipping excluded directory", "path", path)
123121 }
124122 return filepath.SkipDir
125123 }
@@ -139,7 +137,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
139137
140138 elapsedTime := time.Since(startTime)
141139 if cnf.Debug {
142− logger.Debug("Workspace scan complete",
140+ logging.Debug("Workspace scan complete",
143141 "filesOpened", filesOpened,
144142 "elapsedTime", elapsedTime.Seconds(),
145143 "workspacePath", w.workspacePath,
@@ -147,7 +145,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
147145 }
148146
149147 if err != nil && cnf.Debug {
150− logger.Debug("Error scanning workspace for files to open", "error", err)
148+ logging.Debug("Error scanning workspace for files to open", "error", err)
151149 }
152150 }()
153151}
@@ -164,7 +162,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
164162
165163 watcher, err := fsnotify.NewWatcher()
166164 if err != nil {
167− logger.Error("Error creating watcher", "error", err)
165+ logging.Error("Error creating watcher", "error", err)
168166 }
169167 defer watcher.Close()
170168
@@ -178,7 +176,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
178176 if d.IsDir() && path != workspacePath {
179177 if shouldExcludeDir(path) {
180178 if cnf.Debug {
181− logger.Debug("Skipping excluded directory", "path", path)
179+ logging.Debug("Skipping excluded directory", "path", path)
182180 }
183181 return filepath.SkipDir
184182 }
@@ -188,14 +186,14 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
188186 if d.IsDir() {
189187 err = watcher.Add(path)
190188 if err != nil {
191− logger.Error("Error watching path", "path", path, "error", err)
189+ logging.Error("Error watching path", "path", path, "error", err)
192190 }
193191 }
194192
195193 return nil
196194 })
197195 if err != nil {
198− logger.Error("Error walking workspace", "error", err)
196+ logging.Error("Error walking workspace", "error", err)
199197 }
200198
201199 // Event loop
@@ -217,7 +215,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
217215 // Skip excluded directories
218216 if !shouldExcludeDir(event.Name) {
219217 if err := watcher.Add(event.Name); err != nil {
220− logger.Error("Error adding directory to watcher", "path", event.Name, "error", err)
218+ logging.Error("Error adding directory to watcher", "path", event.Name, "error", err)
221219 }
222220 }
223221 } else {
@@ -232,7 +230,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
232230 // Debug logging
233231 if cnf.Debug {
234232 matched, kind := w.isPathWatched(event.Name)
235− logger.Debug("File event",
233+ logging.Debug("File event",
236234 "path", event.Name,
237235 "operation", event.Op.String(),
238236 "watched", matched,
@@ -277,7 +275,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
277275 if !ok {
278276 return
279277 }
280− logger.Error("Error watching file", "error", err)
278+ logging.Error("Error watching file", "error", err)
281279 }
282280 }
283281}
@@ -402,7 +400,7 @@ func matchesSimpleGlob(pattern, path string) bool {
402400 // Fall back to simple matching for simpler patterns
403401 matched, err := filepath.Match(pattern, path)
404402 if err != nil {
405− logger.Error("Error matching pattern", "pattern", pattern, "path", path, "error", err)
403+ logging.Error("Error matching pattern", "pattern", pattern, "path", path, "error", err)
406404 return false
407405 }
408406
@@ -413,7 +411,7 @@ func matchesSimpleGlob(pattern, path string) bool {
413411func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPattern) bool {
414412 patternInfo, err := pattern.AsPattern()
415413 if err != nil {
416− logger.Error("Error parsing pattern", "pattern", pattern, "error", err)
414+ logging.Error("Error parsing pattern", "pattern", pattern, "error", err)
417415 return false
418416 }
419417
@@ -438,7 +436,7 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
438436 // Make path relative to basePath for matching
439437 relPath, err := filepath.Rel(basePath, path)
440438 if err != nil {
441− logger.Error("Error getting relative path", "path", path, "basePath", basePath, "error", err)
439+ logging.Error("Error getting relative path", "path", path, "basePath", basePath, "error", err)
442440 return false
443441 }
444442 relPath = filepath.ToSlash(relPath)
@@ -479,14 +477,14 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan
479477 if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
480478 err := w.client.NotifyChange(ctx, filePath)
481479 if err != nil {
482− logger.Error("Error notifying change", "error", err)
480+ logging.Error("Error notifying change", "error", err)
483481 }
484482 return
485483 }
486484
487485 // Notify LSP server about the file event using didChangeWatchedFiles
488486 if err := w.notifyFileEvent(ctx, uri, changeType); err != nil {
489− logger.Error("Error notifying LSP server about file event", "error", err)
487+ logging.Error("Error notifying LSP server about file event", "error", err)
490488 }
491489}
492490
@@ -494,7 +492,7 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan
494492func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) error {
495493 cnf := config.Get()
496494 if cnf.Debug {
497− logger.Debug("Notifying file event",
495+ logging.Debug("Notifying file event",
498496 "uri", uri,
499497 "changeType", changeType,
500498 )
@@ -618,7 +616,7 @@ func shouldExcludeFile(filePath string) bool {
618616 // Skip large files
619617 if info.Size() > maxFileSize {
620618 if cnf.Debug {
621− logger.Debug("Skipping large file",
619+ logging.Debug("Skipping large file",
622620 "path", filePath,
623621 "size", info.Size(),
624622 "maxSize", maxFileSize,
@@ -651,7 +649,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
651649 if watched, _ := w.isPathWatched(path); watched {
652650 // Don't need to check if it's already open - the client.OpenFile handles that
653651 if err := w.client.OpenFile(ctx, path); err != nil && cnf.Debug {
654− logger.Error("Error opening file", "path", path, "error", err)
652+ logging.Error("Error opening file", "path", path, "error", err)
655653 }
656654 }
657655}
internal/tui/components/core/status.go+4−4
@@ -13,7 +13,7 @@ import (
1313)
1414
1515type statusCmp struct {
16− info *util.InfoMsg
16+ info util.InfoMsg
1717 width int
1818 messageTTL time.Duration
1919}
@@ -35,14 +35,14 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
3535 m.width = msg.Width
3636 return m, nil
3737 case util.InfoMsg:
38− m.info = &msg
38+ m.info = msg
3939 ttl := msg.TTL
4040 if ttl == 0 {
4141 ttl = m.messageTTL
4242 }
4343 return m, m.clearMessageCmd(ttl)
4444 case util.ClearStatusMsg:
45− m.info = nil
45+ m.info = util.InfoMsg{}
4646 }
4747 return m, nil
4848}
@@ -54,7 +54,7 @@ var (
5454
5555func (m statusCmp) View() string {
5656 status := styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render("? help")
57− if m.info != nil {
57+ if m.info.Msg != "" {
5858 infoStyle := styles.Padded.
5959 Foreground(styles.Base).
6060 Width(m.availableFooterMsgWidth())
internal/tui/components/logs/details.go+1−1
@@ -30,7 +30,7 @@ type detailCmp struct {
3030}
3131
3232func (i *detailCmp) Init() tea.Cmd {
33− messages := logging.Get().List()
33+ messages := logging.List()
3434 if len(messages) == 0 {
3535 return nil
3636 }
internal/tui/components/logs/table.go+2−4
@@ -22,8 +22,6 @@ type TableComponent interface {
2222 layout.Bordered
2323}
2424
25−var logger = logging.Get()
26−
2725type tableCmp struct {
2826 table table.Model
2927}
@@ -57,7 +55,7 @@ func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5755 if selectedRow != nil {
5856 if prevSelectedRow == nil || selectedRow[0] == prevSelectedRow[0] {
5957 var log logging.LogMessage
60− for _, row := range logging.Get().List() {
58+ for _, row := range logging.List() {
6159 if row.ID == selectedRow[0] {
6260 log = row
6361 break
@@ -112,7 +110,7 @@ func (i *tableCmp) BindingKeys() []key.Binding {
112110func (i *tableCmp) setRows() {
113111 rows := []table.Row{}
114112
115− logs := logger.List()
113+ logs := logging.List()
116114 slices.SortFunc(logs, func(a, b logging.LogMessage) int {
117115 if a.Time.Before(b.Time) {
118116 return 1
internal/tui/components/repl/editor.go+33−8
@@ -12,6 +12,7 @@ import (
1212 "github.com/kujtimiihoxha/termai/internal/tui/styles"
1313 "github.com/kujtimiihoxha/termai/internal/tui/util"
1414 "github.com/kujtimiihoxha/vimtea"
15+ "golang.org/x/net/context"
1516)
1617
1718type EditorCmp interface {
@@ -23,18 +24,20 @@ type EditorCmp interface {
2324}
2425
2526type editorCmp struct {
26− app *app.App
27− editor vimtea.Editor
28− editorMode vimtea.EditorMode
29− sessionID string
30− focused bool
31− width int
32− height int
27+ app *app.App
28+ editor vimtea.Editor
29+ editorMode vimtea.EditorMode
30+ sessionID string
31+ focused bool
32+ width int
33+ height int
34+ cancelMessage context.CancelFunc
3335}
3436
3537type editorKeyMap struct {
3638 SendMessage key.Binding
3739 SendMessageI key.Binding
40+ CancelMessage key.Binding
3841 InsertMode key.Binding
3942 NormaMode key.Binding
4043 VisualMode key.Binding
@@ -50,6 +53,10 @@ var editorKeyMapValue = editorKeyMap{
5053 key.WithKeys("ctrl+s"),
5154 key.WithHelp("ctrl+s", "send message insert mode"),
5255 ),
56+ CancelMessage: key.NewBinding(
57+ key.WithKeys("ctrl+x"),
58+ key.WithHelp("ctrl+x", "cancel current message"),
59+ ),
5360 InsertMode: key.NewBinding(
5461 key.WithKeys("i"),
5562 key.WithHelp("i", "insert mode"),
@@ -93,6 +100,8 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
93100 if m.editorMode == vimtea.ModeInsert {
94101 return m, m.Send()
95102 }
103+ case key.Matches(msg, editorKeyMapValue.CancelMessage):
104+ return m, m.Cancel()
96105 }
97106 }
98107 u, cmd := m.editor.Update(msg)
@@ -136,6 +145,16 @@ func (m *editorCmp) SetSize(width int, height int) {
136145 m.editor.SetSize(width, height)
137146}
138147
148+func (m *editorCmp) Cancel() tea.Cmd {
149+ if m.cancelMessage == nil {
150+ return util.ReportWarn("No message to cancel")
151+ }
152+
153+ m.cancelMessage()
154+ m.cancelMessage = nil
155+ return util.ReportWarn("Message cancelled")
156+}
157+
139158func (m *editorCmp) Send() tea.Cmd {
140159 return func() tea.Msg {
141160 messages, err := m.app.Messages.List(m.sessionID)
@@ -151,7 +170,13 @@ func (m *editorCmp) Send() tea.Cmd {
151170 }
152171
153172 content := strings.Join(m.editor.GetBuffer().Lines(), "\n")
154− go a.Generate(m.sessionID, content)
173+ ctx, cancel := context.WithCancel(m.app.Context)
174+ m.cancelMessage = cancel
175+ go func() {
176+ defer cancel()
177+ a.Generate(ctx, m.sessionID, content)
178+ m.cancelMessage = nil
179+ }()
155180
156181 return m.editor.Reset()
157182 }
internal/tui/components/repl/messages.go+8−3
@@ -309,7 +309,7 @@ func (m *messagesCmp) renderMessageWithToolCall(content string, tools []message.
309309 }
310310
311311 for _, msg := range futureMessages {
312− if msg.Content().String() != "" {
312+ if msg.Content().String() != "" || msg.FinishReason() == "canceled" {
313313 break
314314 }
315315
@@ -345,13 +345,18 @@ func (m *messagesCmp) renderView() {
345345 prevMessageWasUser := false
346346 for inx, msg := range m.messages {
347347 content := msg.Content().String()
348− if content != "" || prevMessageWasUser {
348+ if content != "" || prevMessageWasUser || msg.FinishReason() == "canceled" {
349349 if msg.ReasoningContent().String() != "" && content == "" {
350350 content = msg.ReasoningContent().String()
351351 } else if content == "" {
352352 content = "..."
353353 }
354− content, _ = r.Render(content)
354+ if msg.FinishReason() == "canceled" {
355+ content, _ = r.Render(content)
356+ content += lipgloss.NewStyle().Padding(1, 0, 0, 1).Foreground(styles.Error).Render(styles.ErrorIcon + " Canceled")
357+ } else {
358+ content, _ = r.Render(content)
359+ }
355360
356361 isSelected := inx == m.selectedMsgIdx
357362
internal/tui/tui.go+2−1
@@ -101,7 +101,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
101101 // Status
102102 case util.InfoMsg:
103103 a.status, cmd = a.status.Update(msg)
104− return a, cmd
104+ cmds = append(cmds, cmd)
105+ return a, tea.Batch(cmds...)
105106 case pubsub.Event[logging.LogMessage]:
106107 if msg.Payload.Persist {
107108 switch msg.Payload.Level {