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