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