1package cmd
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "os"
10 "os/exec"
11 "runtime"
12 "sort"
13 "strings"
14 "syscall"
15 "time"
16
17 "charm.land/lipgloss/v2"
18 "github.com/charmbracelet/colorprofile"
19 "github.com/charmbracelet/crush/internal/agent/tools"
20 "github.com/charmbracelet/crush/internal/config"
21 "github.com/charmbracelet/crush/internal/db"
22 "github.com/charmbracelet/crush/internal/event"
23 "github.com/charmbracelet/crush/internal/message"
24 "github.com/charmbracelet/crush/internal/session"
25 "github.com/charmbracelet/crush/internal/ui/chat"
26 "github.com/charmbracelet/crush/internal/ui/styles"
27 "github.com/charmbracelet/x/ansi"
28 "github.com/charmbracelet/x/exp/charmtone"
29 "github.com/charmbracelet/x/term"
30 "github.com/spf13/cobra"
31)
32
33var sessionCmd = &cobra.Command{
34 Use: "session",
35 Aliases: []string{"sessions", "s"},
36 Short: "Manage sessions",
37 Long: "Manage Crush sessions. Agents can use --json for machine-readable output.",
38}
39
40var (
41 sessionListJSON bool
42 sessionShowJSON bool
43 sessionLastJSON bool
44 sessionDeleteJSON bool
45 sessionRenameJSON bool
46)
47
48var sessionListCmd = &cobra.Command{
49 Use: "list",
50 Aliases: []string{"ls"},
51 Short: "List all sessions",
52 Long: "List all sessions. Use --json for machine-readable output.",
53 RunE: runSessionList,
54}
55
56var sessionShowCmd = &cobra.Command{
57 Use: "show <id>",
58 Short: "Show session details",
59 Long: "Show session details. Use --json for machine-readable output. ID can be a UUID, full hash, or hash prefix.",
60 Args: cobra.ExactArgs(1),
61 RunE: runSessionShow,
62}
63
64var sessionLastCmd = &cobra.Command{
65 Use: "last",
66 Short: "Show most recent session",
67 Long: "Show the last updated session. Use --json for machine-readable output.",
68 RunE: runSessionLast,
69}
70
71var sessionDeleteCmd = &cobra.Command{
72 Use: "delete <id>",
73 Aliases: []string{"rm"},
74 Short: "Delete a session",
75 Long: "Delete a session by ID. Use --json for machine-readable output. ID can be a UUID, full hash, or hash prefix.",
76 Args: cobra.ExactArgs(1),
77 RunE: runSessionDelete,
78}
79
80var sessionRenameCmd = &cobra.Command{
81 Use: "rename <id> <title>",
82 Short: "Rename a session",
83 Long: "Rename a session by ID. Use --json for machine-readable output. ID can be a UUID, full hash, or hash prefix.",
84 Args: cobra.MinimumNArgs(2),
85 RunE: runSessionRename,
86}
87
88func init() {
89 sessionListCmd.Flags().BoolVar(&sessionListJSON, "json", false, "output in JSON format")
90 sessionShowCmd.Flags().BoolVar(&sessionShowJSON, "json", false, "output in JSON format")
91 sessionLastCmd.Flags().BoolVar(&sessionLastJSON, "json", false, "output in JSON format")
92 sessionDeleteCmd.Flags().BoolVar(&sessionDeleteJSON, "json", false, "output in JSON format")
93 sessionRenameCmd.Flags().BoolVar(&sessionRenameJSON, "json", false, "output in JSON format")
94 sessionCmd.AddCommand(sessionListCmd)
95 sessionCmd.AddCommand(sessionShowCmd)
96 sessionCmd.AddCommand(sessionLastCmd)
97 sessionCmd.AddCommand(sessionDeleteCmd)
98 sessionCmd.AddCommand(sessionRenameCmd)
99}
100
101type sessionServices struct {
102 sessions session.Service
103 messages message.Service
104 cfg *config.ConfigStore
105}
106
107func sessionSetup(cmd *cobra.Command) (context.Context, *sessionServices, func(), error) {
108 dataDir, _ := cmd.Flags().GetString("data-dir")
109 ctx := cmd.Context()
110
111 cfg, err := config.Init("", dataDir, false)
112 if err != nil {
113 return nil, nil, nil, fmt.Errorf("failed to initialize config: %w", err)
114 }
115 if dataDir == "" {
116 dataDir = cfg.Config().Options.DataDirectory
117 }
118 if shouldEnableMetrics(cfg.Config()) {
119 event.Init()
120 }
121
122 conn, err := db.Connect(ctx, dataDir)
123 if err != nil {
124 return nil, nil, nil, fmt.Errorf("failed to connect to database: %w", err)
125 }
126
127 queries := db.New(conn)
128 svc := &sessionServices{
129 sessions: session.NewService(queries, conn),
130 messages: message.NewService(queries),
131 cfg: cfg,
132 }
133 return ctx, svc, func() { conn.Close() }, nil
134}
135
136func runSessionList(cmd *cobra.Command, _ []string) error {
137 event.SetNonInteractive(true)
138
139 ctx, svc, cleanup, err := sessionSetup(cmd)
140 if err != nil {
141 return err
142 }
143 defer cleanup()
144
145 event.SessionListed(sessionListJSON)
146
147 list, err := svc.sessions.List(ctx)
148 if err != nil {
149 return fmt.Errorf("failed to list sessions: %w", err)
150 }
151
152 if sessionListJSON {
153 out := cmd.OutOrStdout()
154 output := make([]sessionJSON, len(list))
155 for i, s := range list {
156 output[i] = sessionJSON{
157 ID: session.HashID(s.ID),
158 UUID: s.ID,
159 Title: s.Title,
160 Created: time.Unix(s.CreatedAt, 0).Format(time.RFC3339),
161 Modified: time.Unix(s.UpdatedAt, 0).Format(time.RFC3339),
162 }
163 }
164 enc := json.NewEncoder(out)
165 enc.SetEscapeHTML(false)
166 return enc.Encode(output)
167 }
168
169 w, cleanup, usingPager := sessionWriter(ctx, len(list))
170 defer cleanup()
171
172 hashStyle := lipgloss.NewStyle().Foreground(charmtone.Malibu)
173 dateStyle := lipgloss.NewStyle().Foreground(charmtone.Damson)
174
175 width := sessionOutputWidth
176 if tw, _, err := term.GetSize(os.Stdout.Fd()); err == nil && tw > 0 {
177 width = tw
178 }
179 // 7 (hash) + 1 (space) + 25 (RFC3339 date) + 1 (space) = 34 chars prefix.
180 titleWidth := max(width-34, 10)
181
182 var writeErr error
183 for _, s := range list {
184 hash := session.HashID(s.ID)[:7]
185 date := time.Unix(s.CreatedAt, 0).Format(time.RFC3339)
186 title := strings.ReplaceAll(s.Title, "\n", " ")
187 title = ansi.Truncate(title, titleWidth, "…")
188 _, writeErr = fmt.Fprintln(w, hashStyle.Render(hash), dateStyle.Render(date), title)
189 if writeErr != nil {
190 break
191 }
192 }
193 if writeErr != nil && usingPager && isBrokenPipe(writeErr) {
194 return nil
195 }
196 return writeErr
197}
198
199type sessionJSON struct {
200 ID string `json:"id"`
201 UUID string `json:"uuid"`
202 Title string `json:"title"`
203 Created string `json:"created"`
204 Modified string `json:"modified"`
205}
206
207type sessionMutationResult struct {
208 ID string `json:"id"`
209 UUID string `json:"uuid"`
210 Title string `json:"title"`
211 Deleted bool `json:"deleted,omitempty"`
212 Renamed bool `json:"renamed,omitempty"`
213}
214
215// resolveSessionID resolves a session ID that can be a UUID, full hash, or hash prefix.
216// Returns an error if the prefix is ambiguous (matches multiple sessions).
217func resolveSessionID(ctx context.Context, svc session.Service, id string) (session.Session, error) {
218 // Try direct UUID lookup first
219 if s, err := svc.Get(ctx, id); err == nil {
220 return s, nil
221 }
222
223 // List all sessions and check for hash matches
224 sessions, err := svc.List(ctx)
225 if err != nil {
226 return session.Session{}, err
227 }
228
229 var matches []session.Session
230 for _, s := range sessions {
231 hash := session.HashID(s.ID)
232 if hash == id || strings.HasPrefix(hash, id) {
233 matches = append(matches, s)
234 }
235 }
236
237 if len(matches) == 0 {
238 return session.Session{}, fmt.Errorf("session not found: %s", id)
239 }
240
241 if len(matches) == 1 {
242 return matches[0], nil
243 }
244
245 // Ambiguous - show matches like Git does
246 var sb strings.Builder
247 fmt.Fprintf(&sb, "session ID '%s' is ambiguous. Matches:\n\n", id)
248 for _, m := range matches {
249 hash := session.HashID(m.ID)
250 created := time.Unix(m.CreatedAt, 0).Format("2006-01-02")
251 // Keep title on one line by replacing newlines with spaces, and truncate.
252 title := strings.ReplaceAll(m.Title, "\n", " ")
253 title = ansi.Truncate(title, 50, "…")
254 fmt.Fprintf(&sb, " %s... %q (created %s)\n", hash[:12], title, created)
255 }
256 sb.WriteString("\nUse more characters or the full hash")
257 return session.Session{}, errors.New(sb.String())
258}
259
260func runSessionShow(cmd *cobra.Command, args []string) error {
261 event.SetNonInteractive(true)
262
263 ctx, svc, cleanup, err := sessionSetup(cmd)
264 if err != nil {
265 return err
266 }
267 defer cleanup()
268
269 event.SessionShown(sessionShowJSON)
270
271 sess, err := resolveSessionID(ctx, svc.sessions, args[0])
272 if err != nil {
273 return err
274 }
275
276 msgs, err := svc.messages.List(ctx, sess.ID)
277 if err != nil {
278 return fmt.Errorf("failed to list messages: %w", err)
279 }
280
281 msgPtrs := messagePtrs(msgs)
282 if sessionShowJSON {
283 return outputSessionJSON(cmd.OutOrStdout(), sess, msgPtrs)
284 }
285 return outputSessionHuman(ctx, svc.cfg, sess, msgPtrs)
286}
287
288func runSessionDelete(cmd *cobra.Command, args []string) error {
289 event.SetNonInteractive(true)
290
291 ctx, svc, cleanup, err := sessionSetup(cmd)
292 if err != nil {
293 return err
294 }
295 defer cleanup()
296
297 event.SessionDeletedCommand(sessionDeleteJSON)
298
299 sess, err := resolveSessionID(ctx, svc.sessions, args[0])
300 if err != nil {
301 return err
302 }
303
304 if err := svc.sessions.Delete(ctx, sess.ID); err != nil {
305 return fmt.Errorf("failed to delete session: %w", err)
306 }
307
308 out := cmd.OutOrStdout()
309 if sessionDeleteJSON {
310 enc := json.NewEncoder(out)
311 enc.SetEscapeHTML(false)
312 return enc.Encode(sessionMutationResult{
313 ID: session.HashID(sess.ID),
314 UUID: sess.ID,
315 Title: sess.Title,
316 Deleted: true,
317 })
318 }
319
320 fmt.Fprintf(out, "Deleted session %s\n", session.HashID(sess.ID)[:12])
321 return nil
322}
323
324func runSessionRename(cmd *cobra.Command, args []string) error {
325 event.SetNonInteractive(true)
326
327 ctx, svc, cleanup, err := sessionSetup(cmd)
328 if err != nil {
329 return err
330 }
331 defer cleanup()
332
333 event.SessionRenamed(sessionRenameJSON)
334
335 sess, err := resolveSessionID(ctx, svc.sessions, args[0])
336 if err != nil {
337 return err
338 }
339
340 newTitle := strings.Join(args[1:], " ")
341 if err := svc.sessions.Rename(ctx, sess.ID, newTitle); err != nil {
342 return fmt.Errorf("failed to rename session: %w", err)
343 }
344
345 out := cmd.OutOrStdout()
346 if sessionRenameJSON {
347 enc := json.NewEncoder(out)
348 enc.SetEscapeHTML(false)
349 return enc.Encode(sessionMutationResult{
350 ID: session.HashID(sess.ID),
351 UUID: sess.ID,
352 Title: newTitle,
353 Renamed: true,
354 })
355 }
356
357 fmt.Fprintf(out, "Renamed session %s to %q\n", session.HashID(sess.ID)[:12], newTitle)
358 return nil
359}
360
361func runSessionLast(cmd *cobra.Command, _ []string) error {
362 event.SetNonInteractive(true)
363
364 ctx, svc, cleanup, err := sessionSetup(cmd)
365 if err != nil {
366 return err
367 }
368 defer cleanup()
369
370 event.SessionLastShown(sessionLastJSON)
371
372 list, err := svc.sessions.List(ctx)
373 if err != nil {
374 return fmt.Errorf("failed to list sessions: %w", err)
375 }
376
377 if len(list) == 0 {
378 return fmt.Errorf("no sessions found")
379 }
380
381 sess := list[0]
382
383 msgs, err := svc.messages.List(ctx, sess.ID)
384 if err != nil {
385 return fmt.Errorf("failed to list messages: %w", err)
386 }
387
388 msgPtrs := messagePtrs(msgs)
389 if sessionLastJSON {
390 return outputSessionJSON(cmd.OutOrStdout(), sess, msgPtrs)
391 }
392 return outputSessionHuman(ctx, svc.cfg, sess, msgPtrs)
393}
394
395const (
396 sessionOutputWidth = 80
397 sessionMaxContentWidth = 120
398)
399
400func messagePtrs(msgs []message.Message) []*message.Message {
401 ptrs := make([]*message.Message, len(msgs))
402 for i := range msgs {
403 ptrs[i] = &msgs[i]
404 }
405 return ptrs
406}
407
408func outputSessionJSON(w io.Writer, sess session.Session, msgs []*message.Message) error {
409 skills := extractSkillsFromMessages(msgs)
410 output := sessionShowOutput{
411 Meta: sessionShowMeta{
412 ID: session.HashID(sess.ID),
413 UUID: sess.ID,
414 Title: sess.Title,
415 Created: time.Unix(sess.CreatedAt, 0).Format(time.RFC3339),
416 Modified: time.Unix(sess.UpdatedAt, 0).Format(time.RFC3339),
417 Cost: sess.Cost,
418 PromptTokens: sess.PromptTokens,
419 CompletionTokens: sess.CompletionTokens,
420 TotalTokens: sess.PromptTokens + sess.CompletionTokens,
421 Skills: skills,
422 },
423 Messages: make([]sessionShowMessage, len(msgs)),
424 }
425
426 for i, msg := range msgs {
427 output.Messages[i] = sessionShowMessage{
428 ID: msg.ID,
429 Role: string(msg.Role),
430 Created: time.Unix(msg.CreatedAt, 0).Format(time.RFC3339),
431 Model: msg.Model,
432 Provider: msg.Provider,
433 Parts: convertParts(msg.Parts),
434 }
435 }
436
437 enc := json.NewEncoder(w)
438 enc.SetEscapeHTML(false)
439 return enc.Encode(output)
440}
441
442func outputSessionHuman(ctx context.Context, cfg *config.ConfigStore, sess session.Session, msgs []*message.Message) error {
443 var providerID string
444 if cfg != nil {
445 providerID = cfg.Config().Models[config.SelectedModelTypeLarge].Provider
446 }
447 styles := styles.ThemeForProvider(providerID)
448 toolResults := chat.BuildToolResultMap(msgs)
449
450 width := sessionOutputWidth
451 if w, _, err := term.GetSize(os.Stdout.Fd()); err == nil && w > 0 {
452 width = w
453 }
454 contentWidth := min(width, sessionMaxContentWidth)
455
456 keyStyle := lipgloss.NewStyle().Foreground(charmtone.Damson)
457 valStyle := lipgloss.NewStyle().Foreground(charmtone.Malibu)
458
459 hash := session.HashID(sess.ID)[:12]
460 created := time.Unix(sess.CreatedAt, 0).Format("Mon Jan 2 15:04:05 2006 -0700")
461
462 skills := extractSkillsFromMessages(msgs)
463
464 // Render to buffer to determine actual height
465 var buf strings.Builder
466
467 fmt.Fprintln(&buf, keyStyle.Render("ID: ")+valStyle.Render(hash))
468 fmt.Fprintln(&buf, keyStyle.Render("UUID: ")+valStyle.Render(sess.ID))
469 fmt.Fprintln(&buf, keyStyle.Render("Title: ")+valStyle.Render(sess.Title))
470 fmt.Fprintln(&buf, keyStyle.Render("Date: ")+valStyle.Render(created))
471 if len(skills) > 0 {
472 skillNames := make([]string, len(skills))
473 for i, s := range skills {
474 timestamp := time.Unix(sess.CreatedAt, 0).Format("15:04:05 -0700")
475 if s.LoadedAt != "" {
476 if t, err := time.Parse(time.RFC3339, s.LoadedAt); err == nil {
477 timestamp = t.Format("15:04:05 -0700")
478 }
479 }
480 skillNames[i] = fmt.Sprintf("%s (%s)", s.Name, timestamp)
481 }
482 fmt.Fprintln(&buf, keyStyle.Render("Skills: ")+valStyle.Render(strings.Join(skillNames, ", ")))
483 }
484 fmt.Fprintln(&buf)
485
486 first := true
487 for _, msg := range msgs {
488 items := chat.ExtractMessageItems(&styles, msg, toolResults)
489 for _, item := range items {
490 if !first {
491 fmt.Fprintln(&buf)
492 }
493 first = false
494 fmt.Fprintln(&buf, item.Render(contentWidth))
495 }
496 }
497 fmt.Fprintln(&buf)
498
499 contentHeight := strings.Count(buf.String(), "\n")
500 w, cleanup, usingPager := sessionWriter(ctx, contentHeight)
501 defer cleanup()
502
503 _, err := io.WriteString(w, buf.String())
504 // Ignore broken pipe errors when using a pager. This happens when the user
505 // exits the pager early (e.g., pressing 'q' in less), which closes the pipe
506 // and causes subsequent writes to fail. These errors are expected user behavior.
507 if err != nil && usingPager && isBrokenPipe(err) {
508 return nil
509 }
510 return err
511}
512
513func isBrokenPipe(err error) bool {
514 if err == nil {
515 return false
516 }
517 // Check for syscall.EPIPE (broken pipe)
518 if errors.Is(err, syscall.EPIPE) {
519 return true
520 }
521 // Also check for "broken pipe" in the error message
522 return strings.Contains(err.Error(), "broken pipe")
523}
524
525// sessionWriter returns a writer, cleanup function, and a bool indicating if a pager is used.
526// When the content fits within the terminal (or stdout is not a TTY), it returns
527// a colorprofile.Writer wrapping stdout. When content exceeds terminal height,
528// it starts a pager process (respecting $PAGER, defaulting to "less -R").
529func sessionWriter(ctx context.Context, contentHeight int) (io.Writer, func(), bool) {
530 // Use NewWriter which automatically detects TTY and strips ANSI when redirected
531 if runtime.GOOS == "windows" || !term.IsTerminal(os.Stdout.Fd()) {
532 return colorprofile.NewWriter(os.Stdout, os.Environ()), func() {}, false
533 }
534
535 _, termHeight, err := term.GetSize(os.Stdout.Fd())
536 if err != nil || contentHeight <= termHeight {
537 return colorprofile.NewWriter(os.Stdout, os.Environ()), func() {}, false
538 }
539
540 // Detect color profile from stderr since stdout is piped to the pager.
541 profile := colorprofile.Detect(os.Stderr, os.Environ())
542
543 pager := os.Getenv("PAGER")
544 if pager == "" {
545 pager = "less -R"
546 }
547
548 parts := strings.Fields(pager)
549 cmd := exec.CommandContext(ctx, parts[0], parts[1:]...) //nolint:gosec
550 cmd.Stdout = os.Stdout
551 cmd.Stderr = os.Stderr
552
553 pipe, err := cmd.StdinPipe()
554 if err != nil {
555 return colorprofile.NewWriter(os.Stdout, os.Environ()), func() {}, false
556 }
557
558 if err := cmd.Start(); err != nil {
559 return colorprofile.NewWriter(os.Stdout, os.Environ()), func() {}, false
560 }
561
562 return &colorprofile.Writer{
563 Forward: pipe,
564 Profile: profile,
565 }, func() {
566 pipe.Close()
567 _ = cmd.Wait()
568 }, true
569}
570
571type sessionShowMeta struct {
572 ID string `json:"id"`
573 UUID string `json:"uuid"`
574 Title string `json:"title"`
575 Created string `json:"created"`
576 Modified string `json:"modified"`
577 Cost float64 `json:"cost"`
578 PromptTokens int64 `json:"prompt_tokens"`
579 CompletionTokens int64 `json:"completion_tokens"`
580 TotalTokens int64 `json:"total_tokens"`
581 Skills []sessionShowSkill `json:"skills,omitempty"`
582}
583
584type sessionShowSkill struct {
585 Name string `json:"name"`
586 Description string `json:"description"`
587 LoadedAt string `json:"loaded_at"`
588}
589
590type sessionShowMessage struct {
591 ID string `json:"id"`
592 Role string `json:"role"`
593 Created string `json:"created"`
594 Model string `json:"model,omitempty"`
595 Provider string `json:"provider,omitempty"`
596 Parts []sessionShowPart `json:"parts"`
597}
598
599type sessionShowPart struct {
600 Type string `json:"type"`
601
602 // Text content
603 Text string `json:"text,omitempty"`
604
605 // Reasoning
606 Thinking string `json:"thinking,omitempty"`
607 StartedAt int64 `json:"started_at,omitempty"`
608 FinishedAt int64 `json:"finished_at,omitempty"`
609
610 // Tool call
611 ToolCallID string `json:"tool_call_id,omitempty"`
612 Name string `json:"name,omitempty"`
613 Input string `json:"input,omitempty"`
614
615 // Tool result
616 Content string `json:"content,omitempty"`
617 IsError bool `json:"is_error,omitempty"`
618 MIMEType string `json:"mime_type,omitempty"`
619
620 // Binary
621 Size int64 `json:"size,omitempty"`
622
623 // Image URL
624 URL string `json:"url,omitempty"`
625 Detail string `json:"detail,omitempty"`
626
627 // Finish
628 Reason string `json:"reason,omitempty"`
629 Time int64 `json:"time,omitempty"`
630}
631
632func extractSkillsFromMessages(msgs []*message.Message) []sessionShowSkill {
633 var skills []sessionShowSkill
634 seen := make(map[string]bool)
635
636 for _, msg := range msgs {
637 for _, part := range msg.Parts {
638 if tr, ok := part.(message.ToolResult); ok && tr.Metadata != "" {
639 var meta tools.ViewResponseMetadata
640 if err := json.Unmarshal([]byte(tr.Metadata), &meta); err == nil {
641 if meta.ResourceType == tools.ViewResourceSkill && meta.ResourceName != "" {
642 if !seen[meta.ResourceName] {
643 seen[meta.ResourceName] = true
644 skills = append(skills, sessionShowSkill{
645 Name: meta.ResourceName,
646 Description: meta.ResourceDescription,
647 LoadedAt: time.Unix(msg.CreatedAt, 0).Format(time.RFC3339),
648 })
649 }
650 }
651 }
652 }
653 }
654 }
655
656 sort.Slice(skills, func(i, j int) bool {
657 if skills[i].LoadedAt == skills[j].LoadedAt {
658 return skills[i].Name < skills[j].Name
659 }
660 return skills[i].LoadedAt < skills[j].LoadedAt
661 })
662
663 return skills
664}
665
666func convertParts(parts []message.ContentPart) []sessionShowPart {
667 result := make([]sessionShowPart, 0, len(parts))
668 for _, part := range parts {
669 switch p := part.(type) {
670 case message.TextContent:
671 result = append(result, sessionShowPart{
672 Type: "text",
673 Text: p.Text,
674 })
675 case message.ReasoningContent:
676 result = append(result, sessionShowPart{
677 Type: "reasoning",
678 Thinking: p.Thinking,
679 StartedAt: p.StartedAt,
680 FinishedAt: p.FinishedAt,
681 })
682 case message.ToolCall:
683 result = append(result, sessionShowPart{
684 Type: "tool_call",
685 ToolCallID: p.ID,
686 Name: p.Name,
687 Input: p.Input,
688 })
689 case message.ToolResult:
690 result = append(result, sessionShowPart{
691 Type: "tool_result",
692 ToolCallID: p.ToolCallID,
693 Name: p.Name,
694 Content: p.Content,
695 IsError: p.IsError,
696 MIMEType: p.MIMEType,
697 })
698 case message.BinaryContent:
699 result = append(result, sessionShowPart{
700 Type: "binary",
701 MIMEType: p.MIMEType,
702 Size: int64(len(p.Data)),
703 })
704 case message.ImageURLContent:
705 result = append(result, sessionShowPart{
706 Type: "image_url",
707 URL: p.URL,
708 Detail: p.Detail,
709 })
710 case message.Finish:
711 result = append(result, sessionShowPart{
712 Type: "finish",
713 Reason: string(p.Reason),
714 Time: p.Time,
715 })
716 default:
717 result = append(result, sessionShowPart{
718 Type: "unknown",
719 })
720 }
721 }
722 return result
723}
724
725type sessionShowOutput struct {
726 Meta sessionShowMeta `json:"meta"`
727 Messages []sessionShowMessage `json:"messages"`
728}