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) {
248 _ = 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.PermissionAllowForSession,
260 })
261}
262
263func (w *ClientWorkspace) PermissionGrantPersistent(perm permission.PermissionRequest) {
264 _ = w.client.GrantPermission(context.Background(), w.workspaceID(), proto.PermissionGrant{
265 Permission: proto.PermissionRequest{
266 ID: perm.ID,
267 SessionID: perm.SessionID,
268 ToolCallID: perm.ToolCallID,
269 ToolName: perm.ToolName,
270 Description: perm.Description,
271 Action: perm.Action,
272 Path: perm.Path,
273 Params: perm.Params,
274 },
275 Action: proto.PermissionAllow,
276 })
277}
278
279func (w *ClientWorkspace) PermissionDeny(perm permission.PermissionRequest) {
280 _ = w.client.GrantPermission(context.Background(), w.workspaceID(), proto.PermissionGrant{
281 Permission: proto.PermissionRequest{
282 ID: perm.ID,
283 SessionID: perm.SessionID,
284 ToolCallID: perm.ToolCallID,
285 ToolName: perm.ToolName,
286 Description: perm.Description,
287 Action: perm.Action,
288 Path: perm.Path,
289 Params: perm.Params,
290 },
291 Action: proto.PermissionDeny,
292 })
293}
294
295func (w *ClientWorkspace) PermissionSkipRequests() bool {
296 skip, err := w.client.GetPermissionsSkipRequests(context.Background(), w.workspaceID())
297 if err != nil {
298 return false
299 }
300 return skip
301}
302
303func (w *ClientWorkspace) PermissionSetSkipRequests(skip bool) {
304 _ = w.client.SetPermissionsSkipRequests(context.Background(), w.workspaceID(), skip)
305}
306
307// -- FileTracker --
308
309func (w *ClientWorkspace) FileTrackerRecordRead(ctx context.Context, sessionID, path string) {
310 _ = w.client.FileTrackerRecordRead(ctx, w.workspaceID(), sessionID, path)
311}
312
313func (w *ClientWorkspace) FileTrackerLastReadTime(ctx context.Context, sessionID, path string) time.Time {
314 t, err := w.client.FileTrackerLastReadTime(ctx, w.workspaceID(), sessionID, path)
315 if err != nil {
316 return time.Time{}
317 }
318 return t
319}
320
321func (w *ClientWorkspace) FileTrackerListReadFiles(ctx context.Context, sessionID string) ([]string, error) {
322 return w.client.FileTrackerListReadFiles(ctx, w.workspaceID(), sessionID)
323}
324
325// -- History --
326
327func (w *ClientWorkspace) ListSessionHistory(ctx context.Context, sessionID string) ([]history.File, error) {
328 files, err := w.client.ListSessionHistoryFiles(ctx, w.workspaceID(), sessionID)
329 if err != nil {
330 return nil, err
331 }
332 return protoToFiles(files), nil
333}
334
335// -- LSP --
336
337func (w *ClientWorkspace) LSPStart(ctx context.Context, path string) {
338 _ = w.client.LSPStart(ctx, w.workspaceID(), path)
339}
340
341func (w *ClientWorkspace) LSPStopAll(ctx context.Context) {
342 _ = w.client.LSPStopAll(ctx, w.workspaceID())
343}
344
345func (w *ClientWorkspace) LSPGetStates() map[string]LSPClientInfo {
346 states, err := w.client.GetLSPs(context.Background(), w.workspaceID())
347 if err != nil {
348 return nil
349 }
350 result := make(map[string]LSPClientInfo, len(states))
351 for k, v := range states {
352 result[k] = LSPClientInfo{
353 Name: v.Name,
354 State: v.State,
355 Error: v.Error,
356 DiagnosticCount: v.DiagnosticCount,
357 ConnectedAt: v.ConnectedAt,
358 }
359 }
360 return result
361}
362
363func (w *ClientWorkspace) LSPGetDiagnosticCounts(name string) lsp.DiagnosticCounts {
364 diags, err := w.client.GetLSPDiagnostics(context.Background(), w.workspaceID(), name)
365 if err != nil {
366 return lsp.DiagnosticCounts{}
367 }
368 var counts lsp.DiagnosticCounts
369 for _, fileDiags := range diags {
370 for _, d := range fileDiags {
371 switch d.Severity {
372 case protocol.SeverityError:
373 counts.Error++
374 case protocol.SeverityWarning:
375 counts.Warning++
376 case protocol.SeverityInformation:
377 counts.Information++
378 case protocol.SeverityHint:
379 counts.Hint++
380 }
381 }
382 }
383 return counts
384}
385
386// -- Config (read-only) --
387
388func (w *ClientWorkspace) Config() *config.Config {
389 return w.cached().Config
390}
391
392func (w *ClientWorkspace) WorkingDir() string {
393 return w.cached().Path
394}
395
396func (w *ClientWorkspace) Resolver() config.VariableResolver {
397 return config.IdentityResolver()
398}
399
400// -- Config mutations --
401
402func (w *ClientWorkspace) UpdatePreferredModel(scope config.Scope, modelType config.SelectedModelType, model config.SelectedModel) error {
403 err := w.client.UpdatePreferredModel(context.Background(), w.workspaceID(), scope, modelType, model)
404 if err == nil {
405 w.refreshWorkspace()
406 }
407 return err
408}
409
410func (w *ClientWorkspace) SetCompactMode(scope config.Scope, enabled bool) error {
411 err := w.client.SetCompactMode(context.Background(), w.workspaceID(), scope, enabled)
412 if err == nil {
413 w.refreshWorkspace()
414 }
415 return err
416}
417
418func (w *ClientWorkspace) SetProviderAPIKey(scope config.Scope, providerID string, apiKey any) error {
419 err := w.client.SetProviderAPIKey(context.Background(), w.workspaceID(), scope, providerID, apiKey)
420 if err == nil {
421 w.refreshWorkspace()
422 }
423 return err
424}
425
426func (w *ClientWorkspace) SetConfigField(scope config.Scope, key string, value any) error {
427 err := w.client.SetConfigField(context.Background(), w.workspaceID(), scope, key, value)
428 if err == nil {
429 w.refreshWorkspace()
430 }
431 return err
432}
433
434func (w *ClientWorkspace) RemoveConfigField(scope config.Scope, key string) error {
435 err := w.client.RemoveConfigField(context.Background(), w.workspaceID(), scope, key)
436 if err == nil {
437 w.refreshWorkspace()
438 }
439 return err
440}
441
442func (w *ClientWorkspace) ImportCopilot() (*oauth.Token, bool) {
443 token, ok, err := w.client.ImportCopilot(context.Background(), w.workspaceID())
444 if err != nil {
445 return nil, false
446 }
447 if ok {
448 w.refreshWorkspace()
449 }
450 return token, ok
451}
452
453func (w *ClientWorkspace) RefreshOAuthToken(ctx context.Context, scope config.Scope, providerID string) error {
454 err := w.client.RefreshOAuthToken(ctx, w.workspaceID(), scope, providerID)
455 if err == nil {
456 w.refreshWorkspace()
457 }
458 return err
459}
460
461// -- Project lifecycle --
462
463func (w *ClientWorkspace) ProjectNeedsInitialization() (bool, error) {
464 return w.client.ProjectNeedsInitialization(context.Background(), w.workspaceID())
465}
466
467func (w *ClientWorkspace) MarkProjectInitialized() error {
468 return w.client.MarkProjectInitialized(context.Background(), w.workspaceID())
469}
470
471func (w *ClientWorkspace) InitializePrompt() (string, error) {
472 return w.client.GetInitializePrompt(context.Background(), w.workspaceID())
473}
474
475// -- MCP operations --
476
477func (w *ClientWorkspace) MCPGetStates() map[string]mcp.ClientInfo {
478 states, err := w.client.MCPGetStates(context.Background(), w.workspaceID())
479 if err != nil {
480 return nil
481 }
482 result := make(map[string]mcp.ClientInfo, len(states))
483 for k, v := range states {
484 result[k] = mcp.ClientInfo{
485 Name: v.Name,
486 State: mcp.State(v.State),
487 Error: v.Error,
488 Counts: mcp.Counts{
489 Tools: v.ToolCount,
490 Prompts: v.PromptCount,
491 Resources: v.ResourceCount,
492 },
493 ConnectedAt: v.ConnectedAt,
494 }
495 }
496 return result
497}
498
499func (w *ClientWorkspace) MCPRefreshPrompts(ctx context.Context, name string) {
500 _ = w.client.MCPRefreshPrompts(ctx, w.workspaceID(), name)
501}
502
503func (w *ClientWorkspace) MCPRefreshResources(ctx context.Context, name string) {
504 _ = w.client.MCPRefreshResources(ctx, w.workspaceID(), name)
505}
506
507func (w *ClientWorkspace) RefreshMCPTools(ctx context.Context, name string) {
508 _ = w.client.RefreshMCPTools(ctx, w.workspaceID(), name)
509}
510
511func (w *ClientWorkspace) ReadMCPResource(ctx context.Context, name, uri string) ([]MCPResourceContents, error) {
512 contents, err := w.client.ReadMCPResource(ctx, w.workspaceID(), name, uri)
513 if err != nil {
514 return nil, err
515 }
516 result := make([]MCPResourceContents, len(contents))
517 for i, c := range contents {
518 result[i] = MCPResourceContents{
519 URI: c.URI,
520 MIMEType: c.MIMEType,
521 Text: c.Text,
522 Blob: c.Blob,
523 }
524 }
525 return result, nil
526}
527
528func (w *ClientWorkspace) GetMCPPrompt(clientID, promptID string, args map[string]string) (string, error) {
529 return w.client.GetMCPPrompt(context.Background(), w.workspaceID(), clientID, promptID, args)
530}
531
532func (w *ClientWorkspace) EnableDockerMCP(ctx context.Context) error {
533 return w.client.EnableDockerMCP(ctx, w.workspaceID())
534}
535
536func (w *ClientWorkspace) DisableDockerMCP() error {
537 return w.client.DisableDockerMCP(context.Background(), w.workspaceID())
538}
539
540// -- Lifecycle --
541
542func (w *ClientWorkspace) Subscribe(program *tea.Program) {
543 defer log.RecoverPanic("ClientWorkspace.Subscribe", func() {
544 slog.Info("TUI subscription panic: attempting graceful shutdown")
545 program.Quit()
546 })
547
548 evc, err := w.client.SubscribeEvents(context.Background(), w.workspaceID())
549 if err != nil {
550 slog.Error("Failed to subscribe to events", "error", err)
551 return
552 }
553
554 for ev := range evc {
555 translated := translateEvent(ev)
556 if translated != nil {
557 program.Send(translated)
558 }
559 }
560}
561
562func (w *ClientWorkspace) Shutdown() {
563 _ = w.client.DeleteWorkspace(context.Background(), w.workspaceID())
564}
565
566// translateEvent converts proto-typed SSE events into the domain types
567// that the TUI's Update() method expects.
568func translateEvent(ev any) tea.Msg {
569 switch e := ev.(type) {
570 case pubsub.Event[proto.LSPEvent]:
571 return pubsub.Event[LSPEvent]{
572 Type: e.Type,
573 Payload: LSPEvent{
574 Type: LSPEventType(e.Payload.Type),
575 Name: e.Payload.Name,
576 State: e.Payload.State,
577 Error: e.Payload.Error,
578 DiagnosticCount: e.Payload.DiagnosticCount,
579 },
580 }
581 case pubsub.Event[proto.MCPEvent]:
582 return pubsub.Event[mcp.Event]{
583 Type: e.Type,
584 Payload: mcp.Event{
585 Type: protoToMCPEventType(e.Payload.Type),
586 Name: e.Payload.Name,
587 State: mcp.State(e.Payload.State),
588 Error: e.Payload.Error,
589 Counts: mcp.Counts{
590 Tools: e.Payload.ToolCount,
591 Prompts: e.Payload.PromptCount,
592 Resources: e.Payload.ResourceCount,
593 },
594 },
595 }
596 case pubsub.Event[proto.PermissionRequest]:
597 return pubsub.Event[permission.PermissionRequest]{
598 Type: e.Type,
599 Payload: permission.PermissionRequest{
600 ID: e.Payload.ID,
601 SessionID: e.Payload.SessionID,
602 ToolCallID: e.Payload.ToolCallID,
603 ToolName: e.Payload.ToolName,
604 Description: e.Payload.Description,
605 Action: e.Payload.Action,
606 Path: e.Payload.Path,
607 Params: e.Payload.Params,
608 },
609 }
610 case pubsub.Event[proto.PermissionNotification]:
611 return pubsub.Event[permission.PermissionNotification]{
612 Type: e.Type,
613 Payload: permission.PermissionNotification{
614 ToolCallID: e.Payload.ToolCallID,
615 Granted: e.Payload.Granted,
616 Denied: e.Payload.Denied,
617 },
618 }
619 case pubsub.Event[proto.Message]:
620 return pubsub.Event[message.Message]{
621 Type: e.Type,
622 Payload: protoToMessage(e.Payload),
623 }
624 case pubsub.Event[proto.Session]:
625 return pubsub.Event[session.Session]{
626 Type: e.Type,
627 Payload: protoToSession(e.Payload),
628 }
629 case pubsub.Event[proto.File]:
630 return pubsub.Event[history.File]{
631 Type: e.Type,
632 Payload: protoToFile(e.Payload),
633 }
634 case pubsub.Event[proto.AgentEvent]:
635 return pubsub.Event[notify.Notification]{
636 Type: e.Type,
637 Payload: notify.Notification{
638 SessionID: e.Payload.SessionID,
639 SessionTitle: e.Payload.SessionTitle,
640 Type: notify.Type(e.Payload.Type),
641 },
642 }
643 default:
644 slog.Warn("Unknown event type in translateEvent", "type", fmt.Sprintf("%T", ev))
645 return nil
646 }
647}
648
649func protoToMCPEventType(t proto.MCPEventType) mcp.EventType {
650 switch t {
651 case proto.MCPEventStateChanged:
652 return mcp.EventStateChanged
653 case proto.MCPEventToolsListChanged:
654 return mcp.EventToolsListChanged
655 case proto.MCPEventPromptsListChanged:
656 return mcp.EventPromptsListChanged
657 case proto.MCPEventResourcesListChanged:
658 return mcp.EventResourcesListChanged
659 default:
660 return mcp.EventStateChanged
661 }
662}
663
664func protoToSession(s proto.Session) session.Session {
665 return session.Session{
666 ID: s.ID,
667 ParentSessionID: s.ParentSessionID,
668 Title: s.Title,
669 SummaryMessageID: s.SummaryMessageID,
670 MessageCount: s.MessageCount,
671 PromptTokens: s.PromptTokens,
672 CompletionTokens: s.CompletionTokens,
673 Cost: s.Cost,
674 CreatedAt: s.CreatedAt,
675 UpdatedAt: s.UpdatedAt,
676 }
677}
678
679func protoToFile(f proto.File) history.File {
680 return history.File{
681 ID: f.ID,
682 SessionID: f.SessionID,
683 Path: f.Path,
684 Content: f.Content,
685 Version: f.Version,
686 CreatedAt: f.CreatedAt,
687 UpdatedAt: f.UpdatedAt,
688 }
689}
690
691func protoToMessage(m proto.Message) message.Message {
692 msg := message.Message{
693 ID: m.ID,
694 SessionID: m.SessionID,
695 Role: message.MessageRole(m.Role),
696 Model: m.Model,
697 Provider: m.Provider,
698 CreatedAt: m.CreatedAt,
699 UpdatedAt: m.UpdatedAt,
700 }
701
702 for _, p := range m.Parts {
703 switch v := p.(type) {
704 case proto.TextContent:
705 msg.Parts = append(msg.Parts, message.TextContent{Text: v.Text})
706 case proto.ReasoningContent:
707 msg.Parts = append(msg.Parts, message.ReasoningContent{
708 Thinking: v.Thinking,
709 Signature: v.Signature,
710 StartedAt: v.StartedAt,
711 FinishedAt: v.FinishedAt,
712 })
713 case proto.ToolCall:
714 msg.Parts = append(msg.Parts, message.ToolCall{
715 ID: v.ID,
716 Name: v.Name,
717 Input: v.Input,
718 Finished: v.Finished,
719 })
720 case proto.ToolResult:
721 msg.Parts = append(msg.Parts, message.ToolResult{
722 ToolCallID: v.ToolCallID,
723 Name: v.Name,
724 Content: v.Content,
725 IsError: v.IsError,
726 })
727 case proto.Finish:
728 msg.Parts = append(msg.Parts, message.Finish{
729 Reason: message.FinishReason(v.Reason),
730 Time: v.Time,
731 Message: v.Message,
732 Details: v.Details,
733 })
734 case proto.ImageURLContent:
735 msg.Parts = append(msg.Parts, message.ImageURLContent{URL: v.URL, Detail: v.Detail})
736 case proto.BinaryContent:
737 msg.Parts = append(msg.Parts, message.BinaryContent{Path: v.Path, MIMEType: v.MIMEType, Data: v.Data})
738 }
739 }
740
741 return msg
742}
743
744func protoToMessages(msgs []proto.Message) []message.Message {
745 out := make([]message.Message, len(msgs))
746 for i, m := range msgs {
747 out[i] = protoToMessage(m)
748 }
749 return out
750}
751
752func protoToFiles(files []proto.File) []history.File {
753 out := make([]history.File, len(files))
754 for i, f := range files {
755 out[i] = protoToFile(f)
756 }
757 return out
758}
759
760func sessionToProto(s session.Session) proto.Session {
761 return proto.Session{
762 ID: s.ID,
763 ParentSessionID: s.ParentSessionID,
764 Title: s.Title,
765 SummaryMessageID: s.SummaryMessageID,
766 MessageCount: s.MessageCount,
767 PromptTokens: s.PromptTokens,
768 CompletionTokens: s.CompletionTokens,
769 Cost: s.Cost,
770 CreatedAt: s.CreatedAt,
771 UpdatedAt: s.UpdatedAt,
772 }
773}