1package workspace
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "strings"
8 "sync"
9 "time"
10
11 tea "charm.land/bubbletea/v2"
12 "github.com/charmbracelet/crush/internal/agent/notify"
13 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
14 "github.com/charmbracelet/crush/internal/client"
15 "github.com/charmbracelet/crush/internal/config"
16 "github.com/charmbracelet/crush/internal/history"
17 "github.com/charmbracelet/crush/internal/log"
18 "github.com/charmbracelet/crush/internal/lsp"
19 "github.com/charmbracelet/crush/internal/message"
20 "github.com/charmbracelet/crush/internal/oauth"
21 "github.com/charmbracelet/crush/internal/permission"
22 "github.com/charmbracelet/crush/internal/proto"
23 "github.com/charmbracelet/crush/internal/pubsub"
24 "github.com/charmbracelet/crush/internal/session"
25 "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
26)
27
28// ClientWorkspace implements the Workspace interface by delegating all
29// operations to a remote server via the client SDK. It caches the
30// proto.Workspace returned at creation time and refreshes it after
31// config-mutating operations.
32type ClientWorkspace struct {
33 client *client.Client
34
35 mu sync.RWMutex
36 ws proto.Workspace
37}
38
39// NewClientWorkspace creates a new ClientWorkspace that proxies all
40// operations through the given client SDK. The ws parameter is the
41// proto.Workspace snapshot returned by the server at creation time.
42func NewClientWorkspace(c *client.Client, ws proto.Workspace) *ClientWorkspace {
43 if ws.Config != nil {
44 ws.Config.SetupAgents()
45 }
46 return &ClientWorkspace{
47 client: c,
48 ws: ws,
49 }
50}
51
52// refreshWorkspace re-fetches the workspace from the server, updating
53// the cached snapshot. Called after config-mutating operations.
54func (w *ClientWorkspace) refreshWorkspace() {
55 updated, err := w.client.GetWorkspace(context.Background(), w.ws.ID)
56 if err != nil {
57 slog.Error("Failed to refresh workspace", "error", err)
58 return
59 }
60 if updated.Config != nil {
61 updated.Config.SetupAgents()
62 }
63 w.mu.Lock()
64 w.ws = *updated
65 w.mu.Unlock()
66}
67
68// cached returns a snapshot of the cached workspace.
69func (w *ClientWorkspace) cached() proto.Workspace {
70 w.mu.RLock()
71 defer w.mu.RUnlock()
72 return w.ws
73}
74
75// workspaceID returns the cached workspace ID.
76func (w *ClientWorkspace) workspaceID() string {
77 return w.cached().ID
78}
79
80// -- Sessions --
81
82func (w *ClientWorkspace) CreateSession(ctx context.Context, title string) (session.Session, error) {
83 sess, err := w.client.CreateSession(ctx, w.workspaceID(), title)
84 if err != nil {
85 return session.Session{}, err
86 }
87 return protoToSession(*sess), nil
88}
89
90func (w *ClientWorkspace) GetSession(ctx context.Context, sessionID string) (session.Session, error) {
91 sess, err := w.client.GetSession(ctx, w.workspaceID(), sessionID)
92 if err != nil {
93 return session.Session{}, err
94 }
95 return protoToSession(*sess), nil
96}
97
98func (w *ClientWorkspace) ListSessions(ctx context.Context) ([]session.Session, error) {
99 protoSessions, err := w.client.ListSessions(ctx, w.workspaceID())
100 if err != nil {
101 return nil, err
102 }
103 sessions := make([]session.Session, len(protoSessions))
104 for i, s := range protoSessions {
105 sessions[i] = protoToSession(s)
106 }
107 return sessions, nil
108}
109
110func (w *ClientWorkspace) SaveSession(ctx context.Context, sess session.Session) (session.Session, error) {
111 saved, err := w.client.SaveSession(ctx, w.workspaceID(), sessionToProto(sess))
112 if err != nil {
113 return session.Session{}, err
114 }
115 return protoToSession(*saved), nil
116}
117
118func (w *ClientWorkspace) DeleteSession(ctx context.Context, sessionID string) error {
119 return w.client.DeleteSession(ctx, w.workspaceID(), sessionID)
120}
121
122func (w *ClientWorkspace) CreateAgentToolSessionID(messageID, toolCallID string) string {
123 return fmt.Sprintf("%s$$%s", messageID, toolCallID)
124}
125
126func (w *ClientWorkspace) ParseAgentToolSessionID(sessionID string) (string, string, bool) {
127 parts := strings.Split(sessionID, "$$")
128 if len(parts) != 2 {
129 return "", "", false
130 }
131 return parts[0], parts[1], true
132}
133
134// -- Messages --
135
136func (w *ClientWorkspace) ListMessages(ctx context.Context, sessionID string) ([]message.Message, error) {
137 msgs, err := w.client.ListMessages(ctx, w.workspaceID(), sessionID)
138 if err != nil {
139 return nil, err
140 }
141 return protoToMessages(msgs), nil
142}
143
144func (w *ClientWorkspace) ListUserMessages(ctx context.Context, sessionID string) ([]message.Message, error) {
145 msgs, err := w.client.ListUserMessages(ctx, w.workspaceID(), sessionID)
146 if err != nil {
147 return nil, err
148 }
149 return protoToMessages(msgs), nil
150}
151
152func (w *ClientWorkspace) ListAllUserMessages(ctx context.Context) ([]message.Message, error) {
153 msgs, err := w.client.ListAllUserMessages(ctx, w.workspaceID())
154 if err != nil {
155 return nil, err
156 }
157 return protoToMessages(msgs), nil
158}
159
160// -- Agent --
161
162func (w *ClientWorkspace) AgentRun(ctx context.Context, sessionID, prompt string, attachments ...message.Attachment) error {
163 return w.client.SendMessage(ctx, w.workspaceID(), sessionID, prompt, attachments...)
164}
165
166func (w *ClientWorkspace) AgentCancel(sessionID string) {
167 _ = w.client.CancelAgentSession(context.Background(), w.workspaceID(), sessionID)
168}
169
170func (w *ClientWorkspace) AgentIsBusy() bool {
171 info, err := w.client.GetAgentInfo(context.Background(), w.workspaceID())
172 if err != nil {
173 return false
174 }
175 return info.IsBusy
176}
177
178func (w *ClientWorkspace) AgentIsSessionBusy(sessionID string) bool {
179 info, err := w.client.GetAgentSessionInfo(context.Background(), w.workspaceID(), sessionID)
180 if err != nil {
181 return false
182 }
183 return info.IsBusy
184}
185
186func (w *ClientWorkspace) AgentModel() AgentModel {
187 info, err := w.client.GetAgentInfo(context.Background(), w.workspaceID())
188 if err != nil {
189 return AgentModel{}
190 }
191 return AgentModel{
192 CatwalkCfg: info.Model,
193 ModelCfg: info.ModelCfg,
194 }
195}
196
197func (w *ClientWorkspace) AgentIsReady() bool {
198 info, err := w.client.GetAgentInfo(context.Background(), w.workspaceID())
199 if err != nil {
200 return false
201 }
202 return info.IsReady
203}
204
205func (w *ClientWorkspace) AgentQueuedPrompts(sessionID string) int {
206 count, err := w.client.GetAgentSessionQueuedPrompts(context.Background(), w.workspaceID(), sessionID)
207 if err != nil {
208 return 0
209 }
210 return count
211}
212
213func (w *ClientWorkspace) AgentQueuedPromptsList(sessionID string) []string {
214 prompts, err := w.client.GetAgentSessionQueuedPromptsList(context.Background(), w.workspaceID(), sessionID)
215 if err != nil {
216 return nil
217 }
218 return prompts
219}
220
221func (w *ClientWorkspace) AgentClearQueue(sessionID string) {
222 _ = w.client.ClearAgentSessionQueuedPrompts(context.Background(), w.workspaceID(), sessionID)
223}
224
225func (w *ClientWorkspace) AgentSummarize(ctx context.Context, sessionID string) error {
226 return w.client.AgentSummarizeSession(ctx, w.workspaceID(), sessionID)
227}
228
229func (w *ClientWorkspace) UpdateAgentModel(ctx context.Context) error {
230 return w.client.UpdateAgent(ctx, w.workspaceID())
231}
232
233func (w *ClientWorkspace) InitCoderAgent(ctx context.Context) error {
234 return w.client.InitiateAgentProcessing(ctx, w.workspaceID())
235}
236
237func (w *ClientWorkspace) GetDefaultSmallModel(providerID string) config.SelectedModel {
238 model, err := w.client.GetDefaultSmallModel(context.Background(), w.workspaceID(), providerID)
239 if err != nil {
240 return config.SelectedModel{}
241 }
242 return *model
243}
244
245// -- Permissions --
246
247func (w *ClientWorkspace) PermissionGrant(perm permission.PermissionRequest) bool {
248 resolved, _ := w.client.GrantPermission(context.Background(), w.workspaceID(), proto.PermissionGrant{
249 Permission: proto.PermissionRequest{
250 ID: perm.ID,
251 SessionID: perm.SessionID,
252 ToolCallID: perm.ToolCallID,
253 ToolName: perm.ToolName,
254 Description: perm.Description,
255 Action: perm.Action,
256 Path: perm.Path,
257 Params: perm.Params,
258 },
259 Action: proto.PermissionAllow,
260 })
261 return resolved
262}
263
264func (w *ClientWorkspace) PermissionGrantPersistent(perm permission.PermissionRequest) bool {
265 resolved, _ := w.client.GrantPermission(context.Background(), w.workspaceID(), proto.PermissionGrant{
266 Permission: proto.PermissionRequest{
267 ID: perm.ID,
268 SessionID: perm.SessionID,
269 ToolCallID: perm.ToolCallID,
270 ToolName: perm.ToolName,
271 Description: perm.Description,
272 Action: perm.Action,
273 Path: perm.Path,
274 Params: perm.Params,
275 },
276 Action: proto.PermissionAllowForSession,
277 })
278 return resolved
279}
280
281func (w *ClientWorkspace) PermissionDeny(perm permission.PermissionRequest) bool {
282 resolved, _ := w.client.GrantPermission(context.Background(), w.workspaceID(), proto.PermissionGrant{
283 Permission: proto.PermissionRequest{
284 ID: perm.ID,
285 SessionID: perm.SessionID,
286 ToolCallID: perm.ToolCallID,
287 ToolName: perm.ToolName,
288 Description: perm.Description,
289 Action: perm.Action,
290 Path: perm.Path,
291 Params: perm.Params,
292 },
293 Action: proto.PermissionDeny,
294 })
295 return resolved
296}
297
298func (w *ClientWorkspace) PermissionSkipRequests() bool {
299 skip, err := w.client.GetPermissionsSkipRequests(context.Background(), w.workspaceID())
300 if err != nil {
301 return false
302 }
303 return skip
304}
305
306func (w *ClientWorkspace) PermissionSetSkipRequests(skip bool) {
307 _ = w.client.SetPermissionsSkipRequests(context.Background(), w.workspaceID(), skip)
308}
309
310// -- FileTracker --
311
312func (w *ClientWorkspace) FileTrackerRecordRead(ctx context.Context, sessionID, path string) {
313 _ = w.client.FileTrackerRecordRead(ctx, w.workspaceID(), sessionID, path)
314}
315
316func (w *ClientWorkspace) FileTrackerLastReadTime(ctx context.Context, sessionID, path string) time.Time {
317 t, err := w.client.FileTrackerLastReadTime(ctx, w.workspaceID(), sessionID, path)
318 if err != nil {
319 return time.Time{}
320 }
321 return t
322}
323
324func (w *ClientWorkspace) FileTrackerListReadFiles(ctx context.Context, sessionID string) ([]string, error) {
325 return w.client.FileTrackerListReadFiles(ctx, w.workspaceID(), sessionID)
326}
327
328// -- History --
329
330func (w *ClientWorkspace) ListSessionHistory(ctx context.Context, sessionID string) ([]history.File, error) {
331 files, err := w.client.ListSessionHistoryFiles(ctx, w.workspaceID(), sessionID)
332 if err != nil {
333 return nil, err
334 }
335 return protoToFiles(files), nil
336}
337
338// -- LSP --
339
340func (w *ClientWorkspace) LSPStart(ctx context.Context, path string) {
341 _ = w.client.LSPStart(ctx, w.workspaceID(), path)
342}
343
344func (w *ClientWorkspace) LSPStopAll(ctx context.Context) {
345 _ = w.client.LSPStopAll(ctx, w.workspaceID())
346}
347
348func (w *ClientWorkspace) LSPGetStates() map[string]LSPClientInfo {
349 states, err := w.client.GetLSPs(context.Background(), w.workspaceID())
350 if err != nil {
351 return nil
352 }
353 result := make(map[string]LSPClientInfo, len(states))
354 for k, v := range states {
355 result[k] = LSPClientInfo{
356 Name: v.Name,
357 State: v.State,
358 Error: v.Error,
359 DiagnosticCount: v.DiagnosticCount,
360 ConnectedAt: v.ConnectedAt,
361 }
362 }
363 return result
364}
365
366func (w *ClientWorkspace) LSPGetDiagnosticCounts(name string) lsp.DiagnosticCounts {
367 diags, err := w.client.GetLSPDiagnostics(context.Background(), w.workspaceID(), name)
368 if err != nil {
369 return lsp.DiagnosticCounts{}
370 }
371 var counts lsp.DiagnosticCounts
372 for _, fileDiags := range diags {
373 for _, d := range fileDiags {
374 switch d.Severity {
375 case protocol.SeverityError:
376 counts.Error++
377 case protocol.SeverityWarning:
378 counts.Warning++
379 case protocol.SeverityInformation:
380 counts.Information++
381 case protocol.SeverityHint:
382 counts.Hint++
383 }
384 }
385 }
386 return counts
387}
388
389// -- Config (read-only) --
390
391func (w *ClientWorkspace) Config() *config.Config {
392 return w.cached().Config
393}
394
395func (w *ClientWorkspace) WorkingDir() string {
396 return w.cached().Path
397}
398
399func (w *ClientWorkspace) Resolver() config.VariableResolver {
400 return config.IdentityResolver()
401}
402
403// -- Config mutations --
404
405func (w *ClientWorkspace) UpdatePreferredModel(scope config.Scope, modelType config.SelectedModelType, model config.SelectedModel) error {
406 err := w.client.UpdatePreferredModel(context.Background(), w.workspaceID(), scope, modelType, model)
407 if err == nil {
408 w.refreshWorkspace()
409 }
410 return err
411}
412
413func (w *ClientWorkspace) SetCompactMode(scope config.Scope, enabled bool) error {
414 err := w.client.SetCompactMode(context.Background(), w.workspaceID(), scope, enabled)
415 if err == nil {
416 w.refreshWorkspace()
417 }
418 return err
419}
420
421func (w *ClientWorkspace) SetProviderAPIKey(scope config.Scope, providerID string, apiKey any) error {
422 err := w.client.SetProviderAPIKey(context.Background(), w.workspaceID(), scope, providerID, apiKey)
423 if err == nil {
424 w.refreshWorkspace()
425 }
426 return err
427}
428
429func (w *ClientWorkspace) SetConfigField(scope config.Scope, key string, value any) error {
430 err := w.client.SetConfigField(context.Background(), w.workspaceID(), scope, key, value)
431 if err == nil {
432 w.refreshWorkspace()
433 }
434 return err
435}
436
437func (w *ClientWorkspace) RemoveConfigField(scope config.Scope, key string) error {
438 err := w.client.RemoveConfigField(context.Background(), w.workspaceID(), scope, key)
439 if err == nil {
440 w.refreshWorkspace()
441 }
442 return err
443}
444
445func (w *ClientWorkspace) ImportCopilot() (*oauth.Token, bool) {
446 token, ok, err := w.client.ImportCopilot(context.Background(), w.workspaceID())
447 if err != nil {
448 return nil, false
449 }
450 if ok {
451 w.refreshWorkspace()
452 }
453 return token, ok
454}
455
456func (w *ClientWorkspace) RefreshOAuthToken(ctx context.Context, scope config.Scope, providerID string) error {
457 err := w.client.RefreshOAuthToken(ctx, w.workspaceID(), scope, providerID)
458 if err == nil {
459 w.refreshWorkspace()
460 }
461 return err
462}
463
464// -- Project lifecycle --
465
466func (w *ClientWorkspace) ProjectNeedsInitialization() (bool, error) {
467 return w.client.ProjectNeedsInitialization(context.Background(), w.workspaceID())
468}
469
470func (w *ClientWorkspace) MarkProjectInitialized() error {
471 return w.client.MarkProjectInitialized(context.Background(), w.workspaceID())
472}
473
474func (w *ClientWorkspace) InitializePrompt() (string, error) {
475 return w.client.GetInitializePrompt(context.Background(), w.workspaceID())
476}
477
478// -- MCP operations --
479
480func (w *ClientWorkspace) MCPGetStates() map[string]mcp.ClientInfo {
481 states, err := w.client.MCPGetStates(context.Background(), w.workspaceID())
482 if err != nil {
483 return nil
484 }
485 result := make(map[string]mcp.ClientInfo, len(states))
486 for k, v := range states {
487 result[k] = mcp.ClientInfo{
488 Name: v.Name,
489 State: mcp.State(v.State),
490 Error: v.Error,
491 Counts: mcp.Counts{
492 Tools: v.ToolCount,
493 Prompts: v.PromptCount,
494 Resources: v.ResourceCount,
495 },
496 ConnectedAt: v.ConnectedAt,
497 }
498 }
499 return result
500}
501
502func (w *ClientWorkspace) MCPRefreshPrompts(ctx context.Context, name string) {
503 _ = w.client.MCPRefreshPrompts(ctx, w.workspaceID(), name)
504}
505
506func (w *ClientWorkspace) MCPRefreshResources(ctx context.Context, name string) {
507 _ = w.client.MCPRefreshResources(ctx, w.workspaceID(), name)
508}
509
510func (w *ClientWorkspace) RefreshMCPTools(ctx context.Context, name string) {
511 _ = w.client.RefreshMCPTools(ctx, w.workspaceID(), name)
512}
513
514func (w *ClientWorkspace) ReadMCPResource(ctx context.Context, name, uri string) ([]MCPResourceContents, error) {
515 contents, err := w.client.ReadMCPResource(ctx, w.workspaceID(), name, uri)
516 if err != nil {
517 return nil, err
518 }
519 result := make([]MCPResourceContents, len(contents))
520 for i, c := range contents {
521 result[i] = MCPResourceContents{
522 URI: c.URI,
523 MIMEType: c.MIMEType,
524 Text: c.Text,
525 Blob: c.Blob,
526 }
527 }
528 return result, nil
529}
530
531func (w *ClientWorkspace) GetMCPPrompt(clientID, promptID string, args map[string]string) (string, error) {
532 return w.client.GetMCPPrompt(context.Background(), w.workspaceID(), clientID, promptID, args)
533}
534
535func (w *ClientWorkspace) EnableDockerMCP(ctx context.Context) error {
536 return w.client.EnableDockerMCP(ctx, w.workspaceID())
537}
538
539func (w *ClientWorkspace) DisableDockerMCP() error {
540 return w.client.DisableDockerMCP(context.Background(), w.workspaceID())
541}
542
543// -- Lifecycle --
544
545func (w *ClientWorkspace) Subscribe(program *tea.Program) {
546 defer log.RecoverPanic("ClientWorkspace.Subscribe", func() {
547 slog.Info("TUI subscription panic: attempting graceful shutdown")
548 program.Quit()
549 })
550
551 evc, err := w.client.SubscribeEvents(context.Background(), w.workspaceID())
552 if err != nil {
553 slog.Error("Failed to subscribe to events", "error", err)
554 return
555 }
556
557 for ev := range evc {
558 translated := translateEvent(ev)
559 if translated != nil {
560 program.Send(translated)
561 }
562 }
563}
564
565func (w *ClientWorkspace) Shutdown() {
566 _ = w.client.DeleteWorkspace(context.Background(), w.workspaceID())
567}
568
569// translateEvent converts proto-typed SSE events into the domain types
570// that the TUI's Update() method expects.
571func translateEvent(ev any) tea.Msg {
572 switch e := ev.(type) {
573 case pubsub.Event[proto.LSPEvent]:
574 return pubsub.Event[LSPEvent]{
575 Type: e.Type,
576 Payload: LSPEvent{
577 Type: LSPEventType(e.Payload.Type),
578 Name: e.Payload.Name,
579 State: e.Payload.State,
580 Error: e.Payload.Error,
581 DiagnosticCount: e.Payload.DiagnosticCount,
582 },
583 }
584 case pubsub.Event[proto.MCPEvent]:
585 return pubsub.Event[mcp.Event]{
586 Type: e.Type,
587 Payload: mcp.Event{
588 Type: protoToMCPEventType(e.Payload.Type),
589 Name: e.Payload.Name,
590 State: mcp.State(e.Payload.State),
591 Error: e.Payload.Error,
592 Counts: mcp.Counts{
593 Tools: e.Payload.ToolCount,
594 Prompts: e.Payload.PromptCount,
595 Resources: e.Payload.ResourceCount,
596 },
597 },
598 }
599 case pubsub.Event[proto.PermissionRequest]:
600 return pubsub.Event[permission.PermissionRequest]{
601 Type: e.Type,
602 Payload: permission.PermissionRequest{
603 ID: e.Payload.ID,
604 SessionID: e.Payload.SessionID,
605 ToolCallID: e.Payload.ToolCallID,
606 ToolName: e.Payload.ToolName,
607 Description: e.Payload.Description,
608 Action: e.Payload.Action,
609 Path: e.Payload.Path,
610 Params: e.Payload.Params,
611 },
612 }
613 case pubsub.Event[proto.PermissionNotification]:
614 return pubsub.Event[permission.PermissionNotification]{
615 Type: e.Type,
616 Payload: permission.PermissionNotification{
617 ToolCallID: e.Payload.ToolCallID,
618 Granted: e.Payload.Granted,
619 Denied: e.Payload.Denied,
620 },
621 }
622 case pubsub.Event[proto.Message]:
623 return pubsub.Event[message.Message]{
624 Type: e.Type,
625 Payload: protoToMessage(e.Payload),
626 }
627 case pubsub.Event[proto.Session]:
628 return pubsub.Event[session.Session]{
629 Type: e.Type,
630 Payload: protoToSession(e.Payload),
631 }
632 case pubsub.Event[proto.File]:
633 return pubsub.Event[history.File]{
634 Type: e.Type,
635 Payload: protoToFile(e.Payload),
636 }
637 case pubsub.Event[proto.AgentEvent]:
638 return pubsub.Event[notify.Notification]{
639 Type: e.Type,
640 Payload: notify.Notification{
641 SessionID: e.Payload.SessionID,
642 SessionTitle: e.Payload.SessionTitle,
643 Type: notify.Type(e.Payload.Type),
644 },
645 }
646 default:
647 slog.Warn("Unknown event type in translateEvent", "type", fmt.Sprintf("%T", ev))
648 return nil
649 }
650}
651
652func protoToMCPEventType(t proto.MCPEventType) mcp.EventType {
653 switch t {
654 case proto.MCPEventStateChanged:
655 return mcp.EventStateChanged
656 case proto.MCPEventToolsListChanged:
657 return mcp.EventToolsListChanged
658 case proto.MCPEventPromptsListChanged:
659 return mcp.EventPromptsListChanged
660 case proto.MCPEventResourcesListChanged:
661 return mcp.EventResourcesListChanged
662 default:
663 return mcp.EventStateChanged
664 }
665}
666
667func protoToSession(s proto.Session) session.Session {
668 return session.Session{
669 ID: s.ID,
670 ParentSessionID: s.ParentSessionID,
671 Title: s.Title,
672 SummaryMessageID: s.SummaryMessageID,
673 MessageCount: s.MessageCount,
674 PromptTokens: s.PromptTokens,
675 CompletionTokens: s.CompletionTokens,
676 Cost: s.Cost,
677 Todos: protoToTodos(s.Todos),
678 CreatedAt: s.CreatedAt,
679 UpdatedAt: s.UpdatedAt,
680 }
681}
682
683func protoToTodos(todos []proto.Todo) []session.Todo {
684 if len(todos) == 0 {
685 return nil
686 }
687 out := make([]session.Todo, len(todos))
688 for i, t := range todos {
689 out[i] = session.Todo{
690 Content: t.Content,
691 Status: session.TodoStatus(t.Status),
692 ActiveForm: t.ActiveForm,
693 }
694 }
695 return out
696}
697
698func protoToFile(f proto.File) history.File {
699 return history.File{
700 ID: f.ID,
701 SessionID: f.SessionID,
702 Path: f.Path,
703 Content: f.Content,
704 Version: f.Version,
705 CreatedAt: f.CreatedAt,
706 UpdatedAt: f.UpdatedAt,
707 }
708}
709
710func protoToMessage(m proto.Message) message.Message {
711 msg := message.Message{
712 ID: m.ID,
713 SessionID: m.SessionID,
714 Role: message.MessageRole(m.Role),
715 Model: m.Model,
716 Provider: m.Provider,
717 CreatedAt: m.CreatedAt,
718 UpdatedAt: m.UpdatedAt,
719 }
720
721 for _, p := range m.Parts {
722 switch v := p.(type) {
723 case proto.TextContent:
724 msg.Parts = append(msg.Parts, message.TextContent{Text: v.Text})
725 case proto.ReasoningContent:
726 msg.Parts = append(msg.Parts, message.ReasoningContent{
727 Thinking: v.Thinking,
728 Signature: v.Signature,
729 StartedAt: v.StartedAt,
730 FinishedAt: v.FinishedAt,
731 })
732 case proto.ToolCall:
733 msg.Parts = append(msg.Parts, message.ToolCall{
734 ID: v.ID,
735 Name: v.Name,
736 Input: v.Input,
737 Finished: v.Finished,
738 })
739 case proto.ToolResult:
740 msg.Parts = append(msg.Parts, message.ToolResult{
741 ToolCallID: v.ToolCallID,
742 Name: v.Name,
743 Content: v.Content,
744 Data: v.Data,
745 MIMEType: v.MIMEType,
746 Metadata: v.Metadata,
747 IsError: v.IsError,
748 })
749 case proto.Finish:
750 msg.Parts = append(msg.Parts, message.Finish{
751 Reason: message.FinishReason(v.Reason),
752 Time: v.Time,
753 Message: v.Message,
754 Details: v.Details,
755 })
756 case proto.ImageURLContent:
757 msg.Parts = append(msg.Parts, message.ImageURLContent{URL: v.URL, Detail: v.Detail})
758 case proto.BinaryContent:
759 msg.Parts = append(msg.Parts, message.BinaryContent{Path: v.Path, MIMEType: v.MIMEType, Data: v.Data})
760 }
761 }
762
763 return msg
764}
765
766func protoToMessages(msgs []proto.Message) []message.Message {
767 out := make([]message.Message, len(msgs))
768 for i, m := range msgs {
769 out[i] = protoToMessage(m)
770 }
771 return out
772}
773
774func protoToFiles(files []proto.File) []history.File {
775 out := make([]history.File, len(files))
776 for i, f := range files {
777 out[i] = protoToFile(f)
778 }
779 return out
780}
781
782func sessionToProto(s session.Session) proto.Session {
783 return proto.Session{
784 ID: s.ID,
785 ParentSessionID: s.ParentSessionID,
786 Title: s.Title,
787 SummaryMessageID: s.SummaryMessageID,
788 MessageCount: s.MessageCount,
789 PromptTokens: s.PromptTokens,
790 CompletionTokens: s.CompletionTokens,
791 Cost: s.Cost,
792 Todos: todosToProto(s.Todos),
793 CreatedAt: s.CreatedAt,
794 UpdatedAt: s.UpdatedAt,
795 }
796}
797
798func todosToProto(todos []session.Todo) []proto.Todo {
799 if len(todos) == 0 {
800 return nil
801 }
802 out := make([]proto.Todo, len(todos))
803 for i, t := range todos {
804 out[i] = proto.Todo{
805 Content: t.Content,
806 Status: string(t.Status),
807 ActiveForm: t.ActiveForm,
808 }
809 }
810 return out
811}