client_workspace.go

  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}