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