opencode-manager 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,837 @@
1
+ /**
2
+ * Table output formatter for CLI commands.
3
+ *
4
+ * Provides human-readable table output for terminal display.
5
+ * Supports column definitions, truncation, and alignment.
6
+ */
7
+
8
+ import type { AggregateTokenSummary, ChatMessage, ChatRole, ChatSearchResult, ProjectRecord, ProjectState, SessionRecord, TokenBreakdown, TokenSummary } from "../../lib/opencode-data"
9
+
10
+ // ========================
11
+ // Column Definition Types
12
+ // ========================
13
+
14
+ export type Alignment = "left" | "right" | "center"
15
+
16
+ export interface ColumnDefinition<T, V = string | number | Date | null | undefined> {
17
+ /** Column header label */
18
+ header: string
19
+ /** Width of the column (characters) */
20
+ width: number
21
+ /** Text alignment */
22
+ align?: Alignment
23
+ /** Function to extract the cell value from the row data */
24
+ accessor: (row: T) => V
25
+ /** Optional function to format the value for display (required for non-primitive types) */
26
+ format?: (value: V) => string
27
+ }
28
+
29
+ export interface TableFormatOptions {
30
+ /** Character used to separate columns (default: " ") */
31
+ separator?: string
32
+ /** Character used for header underline (default: "-") */
33
+ headerUnderline?: string
34
+ /** Whether to show header underline (default: true) */
35
+ showUnderline?: boolean
36
+ /** Whether to show headers (default: true) */
37
+ showHeaders?: boolean
38
+ /** Truncation suffix when text is too long (default: "…") */
39
+ truncateSuffix?: string
40
+ }
41
+
42
+ // ========================
43
+ // Core Table Utilities
44
+ // ========================
45
+
46
+ /**
47
+ * Truncate a string to fit within a specified width.
48
+ */
49
+ export function truncate(text: string, width: number, suffix = "…"): string {
50
+ if (text.length <= width) {
51
+ return text
52
+ }
53
+ if (width <= suffix.length) {
54
+ return suffix.slice(0, width)
55
+ }
56
+ return text.slice(0, width - suffix.length) + suffix
57
+ }
58
+
59
+ /**
60
+ * Pad a string to a specified width with alignment.
61
+ */
62
+ export function pad(text: string, width: number, align: Alignment = "left"): string {
63
+ if (text.length >= width) {
64
+ return text
65
+ }
66
+ const padding = width - text.length
67
+ switch (align) {
68
+ case "right":
69
+ return " ".repeat(padding) + text
70
+ case "center": {
71
+ const left = Math.floor(padding / 2)
72
+ const right = padding - left
73
+ return " ".repeat(left) + text + " ".repeat(right)
74
+ }
75
+ case "left":
76
+ default:
77
+ return text + " ".repeat(padding)
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Format a cell value, truncating and padding as needed.
83
+ */
84
+ export function formatCell(
85
+ value: string | number | null | undefined,
86
+ width: number,
87
+ align: Alignment = "left",
88
+ truncateSuffix = "…"
89
+ ): string {
90
+ const text = value == null ? "" : String(value)
91
+ const truncated = truncate(text, width, truncateSuffix)
92
+ return pad(truncated, width, align)
93
+ }
94
+
95
+ /**
96
+ * Format a row of data into a table line.
97
+ */
98
+ export function formatRow<T>(
99
+ row: T,
100
+ columns: ColumnDefinition<T>[],
101
+ options?: TableFormatOptions
102
+ ): string {
103
+ const separator = options?.separator ?? " "
104
+ const truncateSuffix = options?.truncateSuffix ?? "…"
105
+
106
+ return columns
107
+ .map((col) => {
108
+ const raw = col.accessor(row)
109
+ // If format function is provided, use it; otherwise convert to string
110
+ let formatted: string | number | null | undefined
111
+ if (col.format) {
112
+ formatted = col.format(raw)
113
+ } else if (raw instanceof Date) {
114
+ formatted = raw.toISOString()
115
+ } else {
116
+ formatted = raw as string | number | null | undefined
117
+ }
118
+ return formatCell(formatted, col.width, col.align ?? "left", truncateSuffix)
119
+ })
120
+ .join(separator)
121
+ }
122
+
123
+ /**
124
+ * Format the header row.
125
+ */
126
+ export function formatHeader<T>(
127
+ columns: ColumnDefinition<T>[],
128
+ options?: TableFormatOptions
129
+ ): string {
130
+ const separator = options?.separator ?? " "
131
+ return columns
132
+ .map((col) => pad(col.header, col.width, col.align ?? "left"))
133
+ .join(separator)
134
+ }
135
+
136
+ /**
137
+ * Format the header underline.
138
+ */
139
+ export function formatHeaderUnderline<T>(
140
+ columns: ColumnDefinition<T>[],
141
+ options?: TableFormatOptions
142
+ ): string {
143
+ const separator = options?.separator ?? " "
144
+ const underlineChar = options?.headerUnderline ?? "-"
145
+ return columns
146
+ .map((col) => underlineChar.repeat(col.width))
147
+ .join(separator)
148
+ }
149
+
150
+ /**
151
+ * Format a table from an array of records.
152
+ */
153
+ export function formatTable<T>(
154
+ data: T[],
155
+ columns: ColumnDefinition<T>[],
156
+ options?: TableFormatOptions
157
+ ): string {
158
+ const showHeaders = options?.showHeaders ?? true
159
+ const showUnderline = options?.showUnderline ?? true
160
+
161
+ const lines: string[] = []
162
+
163
+ if (showHeaders) {
164
+ lines.push(formatHeader(columns, options))
165
+ if (showUnderline) {
166
+ lines.push(formatHeaderUnderline(columns, options))
167
+ }
168
+ }
169
+
170
+ for (const row of data) {
171
+ lines.push(formatRow(row, columns, options))
172
+ }
173
+
174
+ return lines.join("\n")
175
+ }
176
+
177
+ /**
178
+ * Print a table to stdout.
179
+ */
180
+ export function printTable<T>(
181
+ data: T[],
182
+ columns: ColumnDefinition<T>[],
183
+ options?: TableFormatOptions
184
+ ): void {
185
+ console.log(formatTable(data, columns, options))
186
+ }
187
+
188
+ // ========================
189
+ // Date/Time Formatting
190
+ // ========================
191
+
192
+ /**
193
+ * Format a date for table display.
194
+ * Uses ISO format but truncated for readability.
195
+ */
196
+ export function formatDateForTable(date: Date | null | undefined): string {
197
+ if (!date) {
198
+ return "-"
199
+ }
200
+ // Format: YYYY-MM-DD HH:MM
201
+ const iso = date.toISOString()
202
+ return iso.slice(0, 16).replace("T", " ")
203
+ }
204
+
205
+ // ========================
206
+ // Projects List Columns
207
+ // ========================
208
+
209
+ /**
210
+ * Format project state with visual indicator.
211
+ */
212
+ export function formatProjectState(state: ProjectState): string {
213
+ switch (state) {
214
+ case "present":
215
+ return "✓"
216
+ case "missing":
217
+ return "✗"
218
+ case "unknown":
219
+ return "?"
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Column definitions for projects list output.
225
+ *
226
+ * Columns: #, State, Path, ProjectID, Created
227
+ */
228
+ export const projectListColumns: ColumnDefinition<ProjectRecord>[] = [
229
+ {
230
+ header: "#",
231
+ width: 4,
232
+ align: "right",
233
+ accessor: (row) => row.index,
234
+ },
235
+ {
236
+ header: "State",
237
+ width: 5,
238
+ align: "center",
239
+ accessor: (row) => row.state,
240
+ format: (state) => formatProjectState(state as ProjectState),
241
+ },
242
+ {
243
+ header: "Path",
244
+ width: 50,
245
+ align: "left",
246
+ accessor: (row) => row.worktree,
247
+ },
248
+ {
249
+ header: "Project ID",
250
+ width: 24,
251
+ align: "left",
252
+ accessor: (row) => row.projectId,
253
+ },
254
+ {
255
+ header: "Created",
256
+ width: 16,
257
+ align: "left",
258
+ accessor: (row) => row.createdAt,
259
+ format: (val) => formatDateForTable(val as Date | null | undefined),
260
+ },
261
+ ]
262
+
263
+ /**
264
+ * Compact column definitions for projects list (narrower terminals).
265
+ */
266
+ export const projectListColumnsCompact: ColumnDefinition<ProjectRecord>[] = [
267
+ {
268
+ header: "#",
269
+ width: 4,
270
+ align: "right",
271
+ accessor: (row) => row.index,
272
+ },
273
+ {
274
+ header: "St",
275
+ width: 2,
276
+ align: "center",
277
+ accessor: (row) => row.state,
278
+ format: (state) => formatProjectState(state as ProjectState),
279
+ },
280
+ {
281
+ header: "Path",
282
+ width: 40,
283
+ align: "left",
284
+ accessor: (row) => row.worktree,
285
+ },
286
+ {
287
+ header: "Project ID",
288
+ width: 20,
289
+ align: "left",
290
+ accessor: (row) => row.projectId,
291
+ },
292
+ ]
293
+
294
+ /**
295
+ * Format a projects list as a table.
296
+ */
297
+ export function formatProjectsTable(
298
+ projects: ProjectRecord[],
299
+ options?: TableFormatOptions & { compact?: boolean }
300
+ ): string {
301
+ const columns = options?.compact ? projectListColumnsCompact : projectListColumns
302
+ return formatTable(projects, columns, options)
303
+ }
304
+
305
+ /**
306
+ * Print a projects list table to stdout.
307
+ */
308
+ export function printProjectsTable(
309
+ projects: ProjectRecord[],
310
+ options?: TableFormatOptions & { compact?: boolean }
311
+ ): void {
312
+ console.log(formatProjectsTable(projects, options))
313
+ }
314
+
315
+ // ========================
316
+ // Sessions List Columns
317
+ // ========================
318
+
319
+ /**
320
+ * Column definitions for sessions list output.
321
+ *
322
+ * Columns: #, Title, SessionID, ProjectID, Updated, Created
323
+ */
324
+ export const sessionListColumns: ColumnDefinition<SessionRecord>[] = [
325
+ {
326
+ header: "#",
327
+ width: 4,
328
+ align: "right",
329
+ accessor: (row) => row.index,
330
+ },
331
+ {
332
+ header: "Title",
333
+ width: 40,
334
+ align: "left",
335
+ accessor: (row) => row.title,
336
+ },
337
+ {
338
+ header: "Session ID",
339
+ width: 24,
340
+ align: "left",
341
+ accessor: (row) => row.sessionId,
342
+ },
343
+ {
344
+ header: "Project ID",
345
+ width: 24,
346
+ align: "left",
347
+ accessor: (row) => row.projectId,
348
+ },
349
+ {
350
+ header: "Updated",
351
+ width: 16,
352
+ align: "left",
353
+ accessor: (row) => row.updatedAt,
354
+ format: (val) => formatDateForTable(val as Date | null | undefined),
355
+ },
356
+ {
357
+ header: "Created",
358
+ width: 16,
359
+ align: "left",
360
+ accessor: (row) => row.createdAt,
361
+ format: (val) => formatDateForTable(val as Date | null | undefined),
362
+ },
363
+ ]
364
+
365
+ /**
366
+ * Compact column definitions for sessions list (narrower terminals).
367
+ */
368
+ export const sessionListColumnsCompact: ColumnDefinition<SessionRecord>[] = [
369
+ {
370
+ header: "#",
371
+ width: 4,
372
+ align: "right",
373
+ accessor: (row) => row.index,
374
+ },
375
+ {
376
+ header: "Title",
377
+ width: 30,
378
+ align: "left",
379
+ accessor: (row) => row.title,
380
+ },
381
+ {
382
+ header: "Session ID",
383
+ width: 20,
384
+ align: "left",
385
+ accessor: (row) => row.sessionId,
386
+ },
387
+ {
388
+ header: "Updated",
389
+ width: 16,
390
+ align: "left",
391
+ accessor: (row) => row.updatedAt,
392
+ format: (val) => formatDateForTable(val as Date | null | undefined),
393
+ },
394
+ ]
395
+
396
+ /**
397
+ * Format a sessions list as a table.
398
+ */
399
+ export function formatSessionsTable(
400
+ sessions: SessionRecord[],
401
+ options?: TableFormatOptions & { compact?: boolean }
402
+ ): string {
403
+ const columns = options?.compact ? sessionListColumnsCompact : sessionListColumns
404
+ return formatTable(sessions, columns, options)
405
+ }
406
+
407
+ /**
408
+ * Print a sessions list table to stdout.
409
+ */
410
+ export function printSessionsTable(
411
+ sessions: SessionRecord[],
412
+ options?: TableFormatOptions & { compact?: boolean }
413
+ ): void {
414
+ console.log(formatSessionsTable(sessions, options))
415
+ }
416
+
417
+ // ========================
418
+ // Chat List Columns
419
+ // ========================
420
+
421
+ /**
422
+ * Format chat role with visual indicator.
423
+ */
424
+ export function formatChatRole(role: ChatRole): string {
425
+ switch (role) {
426
+ case "user":
427
+ return "U"
428
+ case "assistant":
429
+ return "A"
430
+ case "unknown":
431
+ return "?"
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Format token count for display.
437
+ * Shows abbreviated number with K suffix for thousands.
438
+ */
439
+ export function formatTokenCount(count: number | null | undefined): string {
440
+ if (count == null || count === 0) {
441
+ return "-"
442
+ }
443
+ if (count >= 1000) {
444
+ return `${(count / 1000).toFixed(1)}K`
445
+ }
446
+ return String(count)
447
+ }
448
+
449
+ /**
450
+ * Column definitions for chat list output.
451
+ *
452
+ * Columns: #, Role, MessageID, Preview, Tokens, Created
453
+ */
454
+ export const chatListColumns: ColumnDefinition<ChatMessage & { index: number }>[] = [
455
+ {
456
+ header: "#",
457
+ width: 4,
458
+ align: "right",
459
+ accessor: (row) => row.index,
460
+ },
461
+ {
462
+ header: "Role",
463
+ width: 4,
464
+ align: "center",
465
+ accessor: (row) => row.role,
466
+ format: (role) => formatChatRole(role as ChatRole),
467
+ },
468
+ {
469
+ header: "Message ID",
470
+ width: 24,
471
+ align: "left",
472
+ accessor: (row) => row.messageId,
473
+ },
474
+ {
475
+ header: "Preview",
476
+ width: 40,
477
+ align: "left",
478
+ accessor: (row) => row.previewText,
479
+ },
480
+ {
481
+ header: "Tokens",
482
+ width: 8,
483
+ align: "right",
484
+ accessor: (row) => row.tokens?.total,
485
+ format: (val) => formatTokenCount(val as number | null | undefined),
486
+ },
487
+ {
488
+ header: "Created",
489
+ width: 16,
490
+ align: "left",
491
+ accessor: (row) => row.createdAt,
492
+ format: (val) => formatDateForTable(val as Date | null | undefined),
493
+ },
494
+ ]
495
+
496
+ /**
497
+ * Compact column definitions for chat list (narrower terminals).
498
+ */
499
+ export const chatListColumnsCompact: ColumnDefinition<ChatMessage & { index: number }>[] = [
500
+ {
501
+ header: "#",
502
+ width: 4,
503
+ align: "right",
504
+ accessor: (row) => row.index,
505
+ },
506
+ {
507
+ header: "R",
508
+ width: 1,
509
+ align: "center",
510
+ accessor: (row) => row.role,
511
+ format: (role) => formatChatRole(role as ChatRole),
512
+ },
513
+ {
514
+ header: "Preview",
515
+ width: 50,
516
+ align: "left",
517
+ accessor: (row) => row.previewText,
518
+ },
519
+ {
520
+ header: "Tokens",
521
+ width: 8,
522
+ align: "right",
523
+ accessor: (row) => row.tokens?.total,
524
+ format: (val) => formatTokenCount(val as number | null | undefined),
525
+ },
526
+ ]
527
+
528
+ /**
529
+ * Format a chat list as a table.
530
+ * Messages are expected to have an index property added.
531
+ */
532
+ export function formatChatTable(
533
+ messages: (ChatMessage & { index: number })[],
534
+ options?: TableFormatOptions & { compact?: boolean }
535
+ ): string {
536
+ const columns = options?.compact ? chatListColumnsCompact : chatListColumns
537
+ return formatTable(messages, columns, options)
538
+ }
539
+
540
+ /**
541
+ * Print a chat list table to stdout.
542
+ */
543
+ export function printChatTable(
544
+ messages: (ChatMessage & { index: number })[],
545
+ options?: TableFormatOptions & { compact?: boolean }
546
+ ): void {
547
+ console.log(formatChatTable(messages, options))
548
+ }
549
+
550
+ // ========================
551
+ // Tokens Summary Formatting
552
+ // ========================
553
+
554
+ /**
555
+ * Row type for token breakdown table.
556
+ * Each row represents a single token category.
557
+ */
558
+ export interface TokenBreakdownRow {
559
+ category: string
560
+ count: number
561
+ percentage: number
562
+ }
563
+
564
+ /**
565
+ * Convert a TokenBreakdown into table rows.
566
+ */
567
+ export function tokenBreakdownToRows(breakdown: TokenBreakdown): TokenBreakdownRow[] {
568
+ const total = breakdown.total || 1 // avoid division by zero
569
+ return [
570
+ { category: "Input", count: breakdown.input, percentage: (breakdown.input / total) * 100 },
571
+ { category: "Output", count: breakdown.output, percentage: (breakdown.output / total) * 100 },
572
+ { category: "Reasoning", count: breakdown.reasoning, percentage: (breakdown.reasoning / total) * 100 },
573
+ { category: "Cache Read", count: breakdown.cacheRead, percentage: (breakdown.cacheRead / total) * 100 },
574
+ { category: "Cache Write", count: breakdown.cacheWrite, percentage: (breakdown.cacheWrite / total) * 100 },
575
+ { category: "Total", count: breakdown.total, percentage: 100 },
576
+ ]
577
+ }
578
+
579
+ /**
580
+ * Format a percentage for display.
581
+ */
582
+ export function formatPercentage(value: number): string {
583
+ if (value === 0) {
584
+ return "-"
585
+ }
586
+ if (value === 100) {
587
+ return "100%"
588
+ }
589
+ return `${value.toFixed(1)}%`
590
+ }
591
+
592
+ /**
593
+ * Format a large number with K/M suffix.
594
+ */
595
+ export function formatLargeNumber(value: number): string {
596
+ if (value === 0) {
597
+ return "0"
598
+ }
599
+ if (value >= 1_000_000) {
600
+ return `${(value / 1_000_000).toFixed(2)}M`
601
+ }
602
+ if (value >= 1000) {
603
+ return `${(value / 1000).toFixed(1)}K`
604
+ }
605
+ return String(value)
606
+ }
607
+
608
+ /**
609
+ * Column definitions for token breakdown table.
610
+ */
611
+ export const tokenBreakdownColumns: ColumnDefinition<TokenBreakdownRow>[] = [
612
+ {
613
+ header: "Category",
614
+ width: 12,
615
+ align: "left",
616
+ accessor: (row) => row.category,
617
+ },
618
+ {
619
+ header: "Tokens",
620
+ width: 12,
621
+ align: "right",
622
+ accessor: (row) => row.count,
623
+ format: (val) => formatLargeNumber(val as number),
624
+ },
625
+ {
626
+ header: "%",
627
+ width: 8,
628
+ align: "right",
629
+ accessor: (row) => row.percentage,
630
+ format: (val) => formatPercentage(val as number),
631
+ },
632
+ ]
633
+
634
+ /**
635
+ * Format a TokenBreakdown as a table.
636
+ */
637
+ export function formatTokenBreakdownTable(
638
+ breakdown: TokenBreakdown,
639
+ options?: TableFormatOptions
640
+ ): string {
641
+ const rows = tokenBreakdownToRows(breakdown)
642
+ return formatTable(rows, tokenBreakdownColumns, options)
643
+ }
644
+
645
+ /**
646
+ * Print a TokenBreakdown table to stdout.
647
+ */
648
+ export function printTokenBreakdownTable(
649
+ breakdown: TokenBreakdown,
650
+ options?: TableFormatOptions
651
+ ): void {
652
+ console.log(formatTokenBreakdownTable(breakdown, options))
653
+ }
654
+
655
+ /**
656
+ * Format a TokenSummary for display.
657
+ * Returns a table for known summaries, or a message for unknown.
658
+ */
659
+ export function formatTokenSummary(
660
+ summary: TokenSummary,
661
+ options?: TableFormatOptions
662
+ ): string {
663
+ if (summary.kind === "known") {
664
+ return formatTokenBreakdownTable(summary.tokens, options)
665
+ }
666
+ // Unknown summary - return reason message
667
+ switch (summary.reason) {
668
+ case "missing":
669
+ return "[Token data unavailable]"
670
+ case "parse_error":
671
+ return "[Token data parse error]"
672
+ case "no_messages":
673
+ return "[No messages found]"
674
+ default:
675
+ return "[Unknown token status]"
676
+ }
677
+ }
678
+
679
+ /**
680
+ * Print a TokenSummary to stdout.
681
+ */
682
+ export function printTokenSummary(
683
+ summary: TokenSummary,
684
+ options?: TableFormatOptions
685
+ ): void {
686
+ console.log(formatTokenSummary(summary, options))
687
+ }
688
+
689
+ /**
690
+ * Row type for aggregate token summary table.
691
+ */
692
+ export interface AggregateTokenRow {
693
+ label: string
694
+ value: string
695
+ }
696
+
697
+ /**
698
+ * Format an AggregateTokenSummary as a detailed summary.
699
+ * Includes breakdown table plus metadata about unknown sessions.
700
+ */
701
+ export function formatAggregateTokenSummary(
702
+ summary: AggregateTokenSummary,
703
+ options?: TableFormatOptions & { label?: string }
704
+ ): string {
705
+ const lines: string[] = []
706
+ const label = options?.label ?? "Token Summary"
707
+
708
+ lines.push(label)
709
+ lines.push("=".repeat(label.length))
710
+ lines.push("")
711
+
712
+ if (summary.total.kind === "known") {
713
+ lines.push(formatTokenBreakdownTable(summary.total.tokens, options))
714
+ } else {
715
+ lines.push(formatTokenSummary(summary.total, options))
716
+ }
717
+
718
+ // Add metadata if there are unknown sessions
719
+ if (summary.unknownSessions && summary.unknownSessions > 0) {
720
+ lines.push("")
721
+ lines.push(`Note: ${summary.unknownSessions} session(s) with unavailable token data`)
722
+ }
723
+
724
+ return lines.join("\n")
725
+ }
726
+
727
+ /**
728
+ * Print an AggregateTokenSummary to stdout.
729
+ */
730
+ export function printAggregateTokenSummary(
731
+ summary: AggregateTokenSummary,
732
+ options?: TableFormatOptions & { label?: string }
733
+ ): void {
734
+ console.log(formatAggregateTokenSummary(summary, options))
735
+ }
736
+
737
+ // ========================
738
+ // Chat Search Results Columns
739
+ // ========================
740
+
741
+ /**
742
+ * Indexed chat search result for display.
743
+ */
744
+ export type IndexedChatSearchResult = ChatSearchResult & { index: number }
745
+
746
+ /**
747
+ * Column definitions for chat search results output.
748
+ *
749
+ * Columns: #, Role, Session, Match, Created
750
+ */
751
+ export const chatSearchColumns: ColumnDefinition<IndexedChatSearchResult>[] = [
752
+ {
753
+ header: "#",
754
+ width: 4,
755
+ align: "right",
756
+ accessor: (row) => row.index,
757
+ },
758
+ {
759
+ header: "Role",
760
+ width: 4,
761
+ align: "center",
762
+ accessor: (row) => row.role,
763
+ format: (role) => formatChatRole(role as ChatRole),
764
+ },
765
+ {
766
+ header: "Session",
767
+ width: 30,
768
+ align: "left",
769
+ accessor: (row) => row.sessionTitle,
770
+ },
771
+ {
772
+ header: "Match",
773
+ width: 50,
774
+ align: "left",
775
+ accessor: (row) => row.matchedText,
776
+ },
777
+ {
778
+ header: "Created",
779
+ width: 16,
780
+ align: "left",
781
+ accessor: (row) => row.createdAt,
782
+ format: (val) => formatDateForTable(val as Date | null | undefined),
783
+ },
784
+ ]
785
+
786
+ /**
787
+ * Compact column definitions for chat search results (narrower terminals).
788
+ */
789
+ export const chatSearchColumnsCompact: ColumnDefinition<IndexedChatSearchResult>[] = [
790
+ {
791
+ header: "#",
792
+ width: 4,
793
+ align: "right",
794
+ accessor: (row) => row.index,
795
+ },
796
+ {
797
+ header: "R",
798
+ width: 1,
799
+ align: "center",
800
+ accessor: (row) => row.role,
801
+ format: (role) => formatChatRole(role as ChatRole),
802
+ },
803
+ {
804
+ header: "Session",
805
+ width: 25,
806
+ align: "left",
807
+ accessor: (row) => row.sessionTitle,
808
+ },
809
+ {
810
+ header: "Match",
811
+ width: 60,
812
+ align: "left",
813
+ accessor: (row) => row.matchedText,
814
+ },
815
+ ]
816
+
817
+ /**
818
+ * Format chat search results as a table.
819
+ * Results are expected to have an index property added.
820
+ */
821
+ export function formatChatSearchTable(
822
+ results: IndexedChatSearchResult[],
823
+ options?: TableFormatOptions & { compact?: boolean }
824
+ ): string {
825
+ const columns = options?.compact ? chatSearchColumnsCompact : chatSearchColumns
826
+ return formatTable(results, columns, options)
827
+ }
828
+
829
+ /**
830
+ * Print chat search results table to stdout.
831
+ */
832
+ export function printChatSearchTable(
833
+ results: IndexedChatSearchResult[],
834
+ options?: TableFormatOptions & { compact?: boolean }
835
+ ): void {
836
+ console.log(formatChatSearchTable(results, options))
837
+ }