zele 0.3.10 → 0.3.12

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.
package/src/mail-tui.tsx CHANGED
@@ -8,9 +8,17 @@
8
8
  // the Raycast UI runtime) but without fetch. gaxios sees window exists → tries
9
9
  // window.fetch → gets undefined → "fetchImpl is not a function". Fix: ensure
10
10
  // window.fetch is set to the native Bun fetch.
11
- const globalWithWindow = globalThis as unknown as { window?: { fetch?: typeof globalThis.fetch } }
12
- if (typeof globalThis.fetch === 'function' && typeof globalWithWindow.window?.fetch !== 'function') {
13
- globalWithWindow.window = { ...globalWithWindow.window, fetch: globalThis.fetch }
11
+ const globalWithWindow = globalThis as unknown as {
12
+ window?: { fetch?: typeof globalThis.fetch }
13
+ }
14
+ if (
15
+ typeof globalThis.fetch === 'function' &&
16
+ typeof globalWithWindow.window?.fetch !== 'function'
17
+ ) {
18
+ globalWithWindow.window = {
19
+ ...globalWithWindow.window,
20
+ fetch: globalThis.fetch,
21
+ }
14
22
  }
15
23
 
16
24
  import {
@@ -30,10 +38,22 @@ import { useTerminalDimensions } from '@opentui/react'
30
38
  import { useCachedPromise } from '@termcast/utils'
31
39
  import { useState, useMemo, useCallback, useEffect } from 'react'
32
40
 
33
- import { getClients, getClient, listAccounts, login, logout, type AuthStatus } from './auth.js'
41
+ import {
42
+ getClients,
43
+ getClient,
44
+ listAccounts,
45
+ login,
46
+ logout,
47
+ type AuthStatus,
48
+ } from './auth.js'
34
49
  import type { GmailClient, ThreadListItem, ThreadData } from './gmail-client.js'
35
50
  import { AuthError, ApiError, isTruthy } from './api-utils.js'
36
- import { renderEmailBody, replyParser, formatDate, formatSender } from './output.js'
51
+ import {
52
+ renderEmailBody,
53
+ replyParser,
54
+ formatDate,
55
+ formatSender,
56
+ } from './output.js'
37
57
 
38
58
  // ---------------------------------------------------------------------------
39
59
  // Constants
@@ -43,7 +63,13 @@ const DEFAULT_PAGE_SIZE = 25
43
63
  const MIN_PAGE_SIZE = 10
44
64
  const VISIBLE_ROWS_OFFSET = 6
45
65
 
46
- const ACCOUNT_COLORS = [Color.Blue, Color.Green, Color.Purple, Color.Orange, Color.Magenta]
66
+ const ACCOUNT_COLORS = [
67
+ Color.Blue,
68
+ Color.Green,
69
+ Color.Purple,
70
+ Color.Orange,
71
+ Color.Magenta,
72
+ ]
47
73
 
48
74
  const ADD_ACCOUNT = '__add_account__'
49
75
  const MANAGE_ACCOUNTS = '__manage_accounts__'
@@ -80,15 +106,26 @@ function dateSection(dateStr: string): string {
80
106
  return 'Older'
81
107
  }
82
108
 
83
- const SECTION_ORDER = ['Last 10 Minutes', 'Last Hour', 'Today', 'Yesterday', 'This Week', 'This Month', 'Older']
84
-
85
- function threadStatusIcon(thread: ThreadListItem & { starred?: boolean }): { source: typeof Icon[keyof typeof Icon]; tintColor: string } {
109
+ const SECTION_ORDER = [
110
+ 'Last 10 Minutes',
111
+ 'Last Hour',
112
+ 'Today',
113
+ 'Yesterday',
114
+ 'This Week',
115
+ 'This Month',
116
+ 'Older',
117
+ ]
118
+
119
+ function threadStatusIcon(thread: ThreadListItem & { starred?: boolean }): {
120
+ source: (typeof Icon)[keyof typeof Icon]
121
+ tintColor: string
122
+ } {
86
123
  const unread = thread.unread
87
124
  const starred = thread.labelIds?.includes('STARRED') ?? false
88
125
 
89
126
  if (unread && starred) return { source: Icon.Star, tintColor: Color.Red }
90
- if (unread) return { source: Icon.CircleFilled, tintColor: Color.Orange }
91
- if (starred) return { source: Icon.Star, tintColor: Color.Yellow }
127
+ if (unread) return { source: Icon.CircleFilled, tintColor: Color.Yellow }
128
+ if (starred) return { source: Icon.Star, tintColor: Color.Orange }
92
129
  return { source: Icon.Circle, tintColor: Color.SecondaryText }
93
130
  }
94
131
 
@@ -138,7 +175,7 @@ function AccountDropdown({
138
175
 
139
176
  return (
140
177
  <List.Dropdown
141
- tooltip="Account"
178
+ tooltip='Account'
142
179
  value={value}
143
180
  onChange={(newValue) => {
144
181
  if (newValue === ADD_ACCOUNT) {
@@ -152,20 +189,31 @@ function AccountDropdown({
152
189
  onChange(newValue)
153
190
  }}
154
191
  >
155
- <List.Dropdown.Item title="All Accounts" value="all" icon={Icon.Globe} />
156
- <List.Dropdown.Section title="Accounts">
192
+ <List.Dropdown.Item title='All Accounts' value='all' icon={Icon.Globe} />
193
+ <List.Dropdown.Section title='Accounts'>
157
194
  {accounts.map((a) => (
158
195
  <List.Dropdown.Item
159
196
  key={a.email}
160
197
  title={a.email}
161
198
  value={a.email}
162
- icon={{ source: Icon.Person, tintColor: accountColor(a.email) }}
199
+ icon={{
200
+ source: Icon.Person,
201
+ tintColor: accountColor(a.email),
202
+ }}
163
203
  />
164
204
  ))}
165
205
  </List.Dropdown.Section>
166
206
  <List.Dropdown.Section>
167
- <List.Dropdown.Item title="Add Account" value={ADD_ACCOUNT} icon={Icon.Plus} />
168
- <List.Dropdown.Item title="Manage Accounts" value={MANAGE_ACCOUNTS} icon={Icon.Gear} />
207
+ <List.Dropdown.Item
208
+ title='Add Account'
209
+ value={ADD_ACCOUNT}
210
+ icon={Icon.Plus}
211
+ />
212
+ <List.Dropdown.Item
213
+ title='Manage Accounts'
214
+ value={MANAGE_ACCOUNTS}
215
+ icon={Icon.Gear}
216
+ />
169
217
  </List.Dropdown.Section>
170
218
  </List.Dropdown>
171
219
  )
@@ -201,7 +249,10 @@ function AddAccount({
201
249
  }
202
250
 
203
251
  await onAdded?.(result.email)
204
- await showToast({ style: Toast.Style.Success, title: `Added ${result.email}` })
252
+ await showToast({
253
+ style: Toast.Style.Success,
254
+ title: `Added ${result.email}`,
255
+ })
205
256
  pop()
206
257
  }
207
258
 
@@ -213,7 +264,7 @@ function AddAccount({
213
264
 
214
265
  return (
215
266
  <Detail
216
- navigationTitle="Add Account"
267
+ navigationTitle='Add Account'
217
268
  markdown={
218
269
  `# Add Account\n\n` +
219
270
  `The browser opens automatically for Google sign-in.\n\n` +
@@ -254,28 +305,43 @@ function ManageAccounts({
254
305
  const handleRemoved = async (email: string) => {
255
306
  const result = await logout(email)
256
307
  if (result instanceof Error) {
257
- await showFailureToast(result, { title: `Failed to remove ${email}` })
308
+ await showFailureToast(result, {
309
+ title: `Failed to remove ${email}`,
310
+ })
258
311
  return
259
312
  }
260
313
 
261
314
  await accounts.revalidate()
262
315
  await onRemoved?.(email)
263
- await showToast({ style: Toast.Style.Success, title: `Removed ${email}` })
316
+ await showToast({
317
+ style: Toast.Style.Success,
318
+ title: `Removed ${email}`,
319
+ })
264
320
  }
265
321
 
266
322
  return (
267
- <List navigationTitle="Manage Accounts" isLoading={accounts.isLoading}>
323
+ <List navigationTitle='Manage Accounts' isLoading={accounts.isLoading}>
268
324
  {accounts.data?.map((a: AuthStatus) => (
269
325
  <List.Item
270
326
  key={`${a.email}-${a.appId}`}
271
327
  title={a.email}
272
- icon={{ source: Icon.Person, tintColor: accountColor(a.email) }}
273
- accessories={[{ tag: { value: a.appId.slice(0, 12) + '...', color: Color.SecondaryText } }]}
328
+ icon={{
329
+ source: Icon.Person,
330
+ tintColor: accountColor(a.email),
331
+ }}
332
+ accessories={[
333
+ {
334
+ tag: {
335
+ value: a.appId.slice(0, 12) + '...',
336
+ color: Color.SecondaryText,
337
+ },
338
+ },
339
+ ]}
274
340
  actions={
275
341
  <ActionPanel>
276
- <Action.CopyToClipboard title="Copy Email" content={a.email} />
342
+ <Action.CopyToClipboard title='Copy Email' content={a.email} />
277
343
  <Action
278
- title="Logout Account"
344
+ title='Logout Account'
279
345
  icon={Icon.Trash}
280
346
  style={Action.Style.Destructive}
281
347
  onAction={() => handleRemoved(a.email)}
@@ -285,12 +351,15 @@ function ManageAccounts({
285
351
  />
286
352
  ))}
287
353
  <List.Item
288
- key="add-account"
289
- title="Add Account"
354
+ key='add-account'
355
+ title='Add Account'
290
356
  icon={Icon.Plus}
291
357
  actions={
292
358
  <ActionPanel>
293
- <Action.Push title="Add Account" target={<AddAccount onAdded={handleAdded} />} />
359
+ <Action.Push
360
+ title='Add Account'
361
+ target={<AddAccount onAdded={handleAdded} />}
362
+ />
294
363
  </ActionPanel>
295
364
  }
296
365
  />
@@ -318,7 +387,10 @@ function ReplyForm({
318
387
 
319
388
  const handleSubmit = async (values: { body: string }) => {
320
389
  if (!values.body?.trim()) {
321
- await showToast({ style: Toast.Style.Failure, title: 'Body is required' })
390
+ await showToast({
391
+ style: Toast.Style.Failure,
392
+ title: 'Body is required',
393
+ })
322
394
  return
323
395
  }
324
396
  setIsLoading(true)
@@ -344,11 +416,15 @@ function ReplyForm({
344
416
  navigationTitle={replyAll ? 'Reply All' : 'Reply'}
345
417
  actions={
346
418
  <ActionPanel>
347
- <Action.SubmitForm title="Send Reply" onSubmit={handleSubmit} />
419
+ <Action.SubmitForm title='Send Reply' onSubmit={handleSubmit} />
348
420
  </ActionPanel>
349
421
  }
350
422
  >
351
- <Form.TextArea id="body" title="Message" placeholder="Type your reply..." />
423
+ <Form.TextArea
424
+ id='body'
425
+ title='Message'
426
+ placeholder='Type your reply...'
427
+ />
352
428
  </Form>
353
429
  )
354
430
  }
@@ -371,11 +447,17 @@ function ForwardForm({
371
447
 
372
448
  const handleSubmit = async (values: { to: string; body: string }) => {
373
449
  if (!values.to?.trim()) {
374
- await showToast({ style: Toast.Style.Failure, title: 'Recipient is required' })
450
+ await showToast({
451
+ style: Toast.Style.Failure,
452
+ title: 'Recipient is required',
453
+ })
375
454
  return
376
455
  }
377
456
  setIsLoading(true)
378
- const recipients = values.to.split(',').map((e) => ({ email: e.trim() })).filter((e) => e.email)
457
+ const recipients = values.to
458
+ .split(',')
459
+ .map((e) => ({ email: e.trim() }))
460
+ .filter((e) => e.email)
379
461
  const { client } = await getClient([account])
380
462
  const result = await client.forwardThread({
381
463
  threadId,
@@ -387,7 +469,10 @@ function ForwardForm({
387
469
  await showFailureToast(result, { title: 'Failed to forward' })
388
470
  return
389
471
  }
390
- await showToast({ style: Toast.Style.Success, title: `Forwarded to ${values.to}` })
472
+ await showToast({
473
+ style: Toast.Style.Success,
474
+ title: `Forwarded to ${values.to}`,
475
+ })
391
476
  revalidate()
392
477
  pop()
393
478
  }
@@ -395,15 +480,19 @@ function ForwardForm({
395
480
  return (
396
481
  <Form
397
482
  isLoading={isLoading}
398
- navigationTitle="Forward"
483
+ navigationTitle='Forward'
399
484
  actions={
400
485
  <ActionPanel>
401
- <Action.SubmitForm title="Forward" onSubmit={handleSubmit} />
486
+ <Action.SubmitForm title='Forward' onSubmit={handleSubmit} />
402
487
  </ActionPanel>
403
488
  }
404
489
  >
405
- <Form.TextField id="to" title="To" placeholder="recipient@example.com" />
406
- <Form.TextArea id="body" title="Message" placeholder="Optional message to prepend..." />
490
+ <Form.TextField id='to' title='To' placeholder='recipient@example.com' />
491
+ <Form.TextArea
492
+ id='body'
493
+ title='Message'
494
+ placeholder='Optional message to prepend...'
495
+ />
407
496
  </Form>
408
497
  )
409
498
  }
@@ -432,7 +521,7 @@ function ThreadDetail({
432
521
  )
433
522
 
434
523
  if (thread.isLoading || !thread.data) {
435
- return <Detail markdown="" navigationTitle="Loading..." />
524
+ return <Detail markdown='' navigationTitle='Loading...' />
436
525
  }
437
526
 
438
527
  const t = thread.data
@@ -457,7 +546,9 @@ function ThreadDetail({
457
546
  body = renderEmailBody(msg.body, msg.mimeType)
458
547
  }
459
548
 
460
- return [heading, attachmentLine, '', body].filter((l) => l !== null).join('\n')
549
+ return [heading, attachmentLine, '', body]
550
+ .filter((l) => l !== null)
551
+ .join('\n')
461
552
  })
462
553
 
463
554
  const markdown = `# ${t.subject}\n\n---\n\n` + parts.join('\n\n---\n\n')
@@ -470,7 +561,9 @@ function ThreadDetail({
470
561
  }
471
562
 
472
563
  const labels = [...new Set(messages.flatMap((m) => m.labelIds))]
473
- .filter((l): l is string => typeof l === 'string' && !l.startsWith('Label_')) // skip internal IDs
564
+ .filter(
565
+ (l): l is string => typeof l === 'string' && !l.startsWith('Label_'),
566
+ ) // skip internal IDs
474
567
  .slice(0, 10)
475
568
 
476
569
  const latestMsg = messages[messages.length - 1]!
@@ -481,55 +574,96 @@ function ThreadDetail({
481
574
  markdown={markdown}
482
575
  metadata={
483
576
  <Detail.Metadata>
484
- <Detail.Metadata.Label title="From" text={formatSender(latestMsg.from)} />
485
- <Detail.Metadata.Label title="To" text={latestMsg.to.map((r) => r.name || r.email).join(', ')} />
577
+ <Detail.Metadata.Label
578
+ title='From'
579
+ text={formatSender(latestMsg.from)}
580
+ />
581
+ <Detail.Metadata.Label
582
+ title='To'
583
+ text={latestMsg.to.map((r) => r.name || r.email).join(', ')}
584
+ />
486
585
  {latestMsg.cc && latestMsg.cc.length > 0 && (
487
- <Detail.Metadata.Label title="Cc" text={latestMsg.cc.map((r) => r.name || r.email).join(', ')} />
586
+ <Detail.Metadata.Label
587
+ title='Cc'
588
+ text={latestMsg.cc.map((r) => r.name || r.email).join(', ')}
589
+ />
488
590
  )}
489
- <Detail.Metadata.Label title="Date" text={latestMsg.date} />
591
+ <Detail.Metadata.Label title='Date' text={latestMsg.date} />
490
592
  <Detail.Metadata.Separator />
491
- <Detail.Metadata.Label title="Messages" text={String(t.messageCount)} />
492
- <Detail.Metadata.Label title="Participants" text={[...participants.values()].join(', ')} />
593
+ <Detail.Metadata.Label
594
+ title='Messages'
595
+ text={String(t.messageCount)}
596
+ />
597
+ <Detail.Metadata.Label
598
+ title='Participants'
599
+ text={[...participants.values()].join(', ')}
600
+ />
493
601
  {labels.length > 0 && (
494
- <Detail.Metadata.TagList title="Labels">
602
+ <Detail.Metadata.TagList title='Labels'>
495
603
  {labels.map((l) => (
496
- <Detail.Metadata.TagList.Item key={l} text={l} color={labelColor(l)} />
604
+ <Detail.Metadata.TagList.Item
605
+ key={l}
606
+ text={l}
607
+ color={labelColor(l)}
608
+ />
497
609
  ))}
498
610
  </Detail.Metadata.TagList>
499
611
  )}
500
612
  <Detail.Metadata.Separator />
501
- <Detail.Metadata.Label title="Thread ID" text={t.id} />
502
- <Detail.Metadata.Label title="Account" text={account} />
613
+ <Detail.Metadata.Label title='Thread ID' text={t.id} />
614
+ <Detail.Metadata.Label title='Account' text={account} />
503
615
  </Detail.Metadata>
504
616
  }
505
617
  actions={
506
618
  <ActionPanel>
507
- <ActionPanel.Section title="Reply & Forward">
619
+ <ActionPanel.Section title='Reply & Forward'>
508
620
  <Action.Push
509
- title="Reply"
621
+ title='Reply'
510
622
  icon={Icon.Reply}
511
623
  shortcut={{ modifiers: ['ctrl'], key: 'r' }}
512
- target={<ReplyForm threadId={threadId} account={account} revalidate={revalidate} />}
624
+ target={
625
+ <ReplyForm
626
+ threadId={threadId}
627
+ account={account}
628
+ revalidate={revalidate}
629
+ />
630
+ }
513
631
  />
514
632
  <Action.Push
515
- title="Reply All"
633
+ title='Reply All'
516
634
  icon={Icon.Reply}
517
- shortcut={{ modifiers: ['ctrl', 'shift'], key: 'r' }}
518
- target={<ReplyForm threadId={threadId} account={account} replyAll revalidate={revalidate} />}
635
+ shortcut={{
636
+ modifiers: ['ctrl', 'shift'],
637
+ key: 'r',
638
+ }}
639
+ target={
640
+ <ReplyForm
641
+ threadId={threadId}
642
+ account={account}
643
+ replyAll
644
+ revalidate={revalidate}
645
+ />
646
+ }
519
647
  />
520
648
  <Action.Push
521
- title="Forward"
649
+ title='Forward'
522
650
  icon={Icon.Forward}
523
651
  shortcut={{ modifiers: ['ctrl'], key: 'f' }}
524
- target={<ForwardForm threadId={threadId} account={account} revalidate={revalidate} />}
652
+ target={
653
+ <ForwardForm
654
+ threadId={threadId}
655
+ account={account}
656
+ revalidate={revalidate}
657
+ />
658
+ }
525
659
  />
526
660
  </ActionPanel.Section>
527
- <ActionPanel.Section title="Copy">
528
- <Action.CopyToClipboard title="Copy Thread ID" content={t.id} />
529
- <Action.CopyToClipboard title="Copy Subject" content={t.subject} />
661
+ <ActionPanel.Section title='Copy'>
662
+ <Action.CopyToClipboard title='Copy Thread ID' content={t.id} />
663
+ <Action.CopyToClipboard title='Copy Subject' content={t.subject} />
530
664
  {latestMsg && (
531
665
  <Action.CopyToClipboard
532
- title="Copy Email Body"
666
+ title='Copy Email Body'
533
667
  content={renderEmailBody(latestMsg.body, latestMsg.mimeType)}
534
668
  />
535
669
  )}
@@ -569,7 +703,8 @@ export default function Command() {
569
703
 
570
704
  // Single selected account: standard cursor pagination.
571
705
  if (account !== 'all') {
572
- const pageToken = cursor?.mode === 'single' ? cursor.nextPageToken : undefined
706
+ const pageToken =
707
+ cursor?.mode === 'single' ? cursor.nextPageToken : undefined
573
708
  const { email, client } = clients[0]!
574
709
  const result = await client.listThreads({
575
710
  query: query || undefined,
@@ -577,10 +712,15 @@ export default function Command() {
577
712
  pageToken: pageToken || undefined,
578
713
  })
579
714
  if (result instanceof Error) {
580
- await showFailureToast(result, { title: 'Failed to fetch emails' })
715
+ await showFailureToast(result, {
716
+ title: 'Failed to fetch emails',
717
+ })
581
718
  return { data: [] as ThreadItem[], hasMore: false }
582
719
  }
583
- const data: ThreadItem[] = result.threads.map((t) => ({ ...t, account: email }))
720
+ const data: ThreadItem[] = result.threads.map((t) => ({
721
+ ...t,
722
+ account: email,
723
+ }))
584
724
  return {
585
725
  data,
586
726
  hasMore: !!result.nextPageToken,
@@ -592,7 +732,8 @@ export default function Command() {
592
732
  }
593
733
 
594
734
  // Multi-account: keep one token per account and merge sorted pages.
595
- const previousByAccount = cursor?.mode === 'multi' ? cursor.nextByAccount : {}
735
+ const previousByAccount =
736
+ cursor?.mode === 'multi' ? cursor.nextByAccount : {}
596
737
 
597
738
  const results = await Promise.all(
598
739
  clients.map(async ({ email, client }) => {
@@ -630,20 +771,29 @@ export default function Command() {
630
771
  .filter(isTruthy)
631
772
 
632
773
  const merged: ThreadItem[] = successfulResults
633
- .flatMap(({ email, result }) => result.threads.map((t) => ({ ...t, account: email })))
634
- .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
774
+ .flatMap(({ email, result }) =>
775
+ result.threads.map((t) => ({ ...t, account: email })),
776
+ )
777
+ .sort(
778
+ (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
779
+ )
635
780
 
636
781
  const nextByAccount: Record<string, string | null> = {}
637
782
  for (const { email, nextPageToken } of results) {
638
783
  nextByAccount[email] = nextPageToken
639
784
  }
640
785
 
641
- const hasMore = Object.values(nextByAccount).some((token) => token !== null)
786
+ const hasMore = Object.values(nextByAccount).some(
787
+ (token) => token !== null,
788
+ )
642
789
 
643
790
  return {
644
791
  data: merged,
645
792
  hasMore,
646
- cursor: { mode: 'multi', nextByAccount } satisfies MailCursor,
793
+ cursor: {
794
+ mode: 'multi',
795
+ nextByAccount,
796
+ } satisfies MailCursor,
647
797
  }
648
798
  }
649
799
  },
@@ -695,7 +845,9 @@ export default function Command() {
695
845
  // Selection helpers
696
846
  const toggleSelection = useCallback((threadId: string) => {
697
847
  setSelectedThreads((prev) =>
698
- prev.includes(threadId) ? prev.filter((id) => id !== threadId) : [...prev, threadId],
848
+ prev.includes(threadId)
849
+ ? prev.filter((id) => id !== threadId)
850
+ : [...prev, threadId],
699
851
  )
700
852
  }, [])
701
853
 
@@ -721,12 +873,17 @@ export default function Command() {
721
873
  const { client } = await getClient([acct])
722
874
  const result = await fn(client, ids)
723
875
  if (result instanceof Error) {
724
- await showFailureToast(result, { title: `Failed to ${actionName}` })
876
+ await showFailureToast(result, {
877
+ title: `Failed to ${actionName}`,
878
+ })
725
879
  return
726
880
  }
727
881
  }
728
882
 
729
- await showToast({ style: Toast.Style.Success, title: `${actionName}: ${selectedThreads.length} thread(s)` })
883
+ await showToast({
884
+ style: Toast.Style.Success,
885
+ title: `${actionName}: ${selectedThreads.length} thread(s)`,
886
+ })
730
887
  setSelectedThreads([])
731
888
  revalidate()
732
889
  },
@@ -737,7 +894,7 @@ export default function Command() {
737
894
  <List
738
895
  isLoading={isLoading || accounts.isLoading}
739
896
  isShowingDetail={isShowingDetail}
740
- searchBarPlaceholder="Search emails..."
897
+ searchBarPlaceholder='Search emails...'
741
898
  onSearchTextChange={setSearchText}
742
899
  throttle
743
900
  pagination={pagination ? { ...pagination, pageSize } : undefined}
@@ -761,16 +918,33 @@ export default function Command() {
761
918
 
762
919
  // Icon: selection mode or status
763
920
  const icon = hasSelection
764
- ? { source: isSelected ? Icon.CheckCircle : Icon.Circle, tintColor: isSelected ? Color.Blue : Color.SecondaryText }
921
+ ? {
922
+ source: isSelected ? Icon.CheckCircle : Icon.Circle,
923
+ tintColor: isSelected ? Color.Blue : Color.SecondaryText,
924
+ }
765
925
  : threadStatusIcon(thread)
766
926
 
767
927
  // Accessories
768
- const accessories: Array<{ text?: string; tag?: string | { value: string; color?: string }; icon?: string | null }> = []
928
+ const accessories: Array<{
929
+ text?: string
930
+ tag?: string | { value: string; color?: string }
931
+ icon?: string | null
932
+ }> = []
769
933
  if (thread.messageCount > 1) {
770
- accessories.push({ tag: { value: String(thread.messageCount), color: Color.SecondaryText } })
934
+ accessories.push({
935
+ tag: {
936
+ value: String(thread.messageCount),
937
+ color: Color.SecondaryText,
938
+ },
939
+ })
771
940
  }
772
941
  if (multiAccount || selectedAccount === 'all') {
773
- accessories.push({ tag: { value: thread.account.split('@')[0] ?? thread.account, color: accountColor(thread.account) } })
942
+ accessories.push({
943
+ tag: {
944
+ value: thread.account.split('@')[0] ?? thread.account,
945
+ color: accountColor(thread.account),
946
+ },
947
+ })
774
948
  }
775
949
  accessories.push({ text: formatDate(thread.date) })
776
950
 
@@ -780,24 +954,43 @@ export default function Command() {
780
954
  markdown={`# ${thread.subject}\n\n${thread.snippet}`}
781
955
  metadata={
782
956
  <List.Item.Detail.Metadata>
783
- <List.Item.Detail.Metadata.Label title="From" text={formatSender(thread.from)} />
784
- <List.Item.Detail.Metadata.Label title="Date" text={thread.date} />
957
+ <List.Item.Detail.Metadata.Label
958
+ title='From'
959
+ text={formatSender(thread.from)}
960
+ />
961
+ <List.Item.Detail.Metadata.Label
962
+ title='Date'
963
+ text={thread.date}
964
+ />
785
965
  <List.Item.Detail.Metadata.Separator />
786
966
  {thread.labelIds.length > 0 && (
787
- <List.Item.Detail.Metadata.TagList title="Labels">
967
+ <List.Item.Detail.Metadata.TagList title='Labels'>
788
968
  {thread.labelIds
789
969
  .filter((l) => !l.startsWith('Label_'))
790
970
  .slice(0, 8)
791
971
  .map((l) => (
792
- <List.Item.Detail.Metadata.TagList.Item key={l} text={l} color={labelColor(l)} />
972
+ <List.Item.Detail.Metadata.TagList.Item
973
+ key={l}
974
+ text={l}
975
+ color={labelColor(l)}
976
+ />
793
977
  ))}
794
978
  </List.Item.Detail.Metadata.TagList>
795
979
  )}
796
980
  <List.Item.Detail.Metadata.Separator />
797
- <List.Item.Detail.Metadata.Label title="Messages" text={String(thread.messageCount)} />
798
- <List.Item.Detail.Metadata.Label title="Thread ID" text={thread.id} />
981
+ <List.Item.Detail.Metadata.Label
982
+ title='Messages'
983
+ text={String(thread.messageCount)}
984
+ />
985
+ <List.Item.Detail.Metadata.Label
986
+ title='Thread ID'
987
+ text={thread.id}
988
+ />
799
989
  {multiAccount && (
800
- <List.Item.Detail.Metadata.Label title="Account" text={thread.account} />
990
+ <List.Item.Detail.Metadata.Label
991
+ title='Account'
992
+ text={thread.account}
993
+ />
801
994
  )}
802
995
  </List.Item.Detail.Metadata>
803
996
  }
@@ -811,15 +1004,21 @@ export default function Command() {
811
1004
  subtitle={formatSender(thread.from)}
812
1005
  icon={icon}
813
1006
  accessories={accessories}
814
- keywords={[thread.from.email, thread.from.name ?? '', thread.account]}
1007
+ keywords={[
1008
+ thread.from.email,
1009
+ thread.from.name ?? '',
1010
+ thread.account,
1011
+ ]}
815
1012
  detail={detail}
816
1013
  actions={
817
1014
  <ActionPanel>
818
1015
  {/* Selection actions (when items are selected) */}
819
1016
  {hasSelection && (
820
- <ActionPanel.Section title="Selection">
1017
+ <ActionPanel.Section title='Selection'>
821
1018
  <Action
822
- title={isSelected ? 'Deselect Thread' : 'Select Thread'}
1019
+ title={
1020
+ isSelected ? 'Deselect Thread' : 'Select Thread'
1021
+ }
823
1022
  icon={isSelected ? Icon.CheckCircle : Icon.Circle}
824
1023
  onAction={() => toggleSelection(thread.id)}
825
1024
  />
@@ -827,21 +1026,33 @@ export default function Command() {
827
1026
  title={`Archive ${selectedThreads.length} Selected`}
828
1027
  icon={Icon.Tray}
829
1028
  onAction={() =>
830
- handleBulkAction('Archived', (c, ids) => c.archive({ threadIds: ids }))
1029
+ handleBulkAction('Archived', (c, ids) =>
1030
+ c.archive({
1031
+ threadIds: ids,
1032
+ }),
1033
+ )
831
1034
  }
832
1035
  />
833
1036
  <Action
834
1037
  title={`Mark ${selectedThreads.length} as Read`}
835
1038
  icon={Icon.Eye}
836
1039
  onAction={() =>
837
- handleBulkAction('Marked as read', (c, ids) => c.markAsRead({ threadIds: ids }))
1040
+ handleBulkAction('Marked as read', (c, ids) =>
1041
+ c.markAsRead({
1042
+ threadIds: ids,
1043
+ }),
1044
+ )
838
1045
  }
839
1046
  />
840
1047
  <Action
841
1048
  title={`Star ${selectedThreads.length} Selected`}
842
1049
  icon={Icon.Star}
843
1050
  onAction={() =>
844
- handleBulkAction('Starred', (c, ids) => c.star({ threadIds: ids }))
1051
+ handleBulkAction('Starred', (c, ids) =>
1052
+ c.star({
1053
+ threadIds: ids,
1054
+ }),
1055
+ )
845
1056
  }
846
1057
  />
847
1058
  <Action
@@ -851,13 +1062,15 @@ export default function Command() {
851
1062
  onAction={() =>
852
1063
  handleBulkAction('Trashed', async (c, ids) => {
853
1064
  for (const id of ids) {
854
- await c.trash({ threadId: id })
1065
+ await c.trash({
1066
+ threadId: id,
1067
+ })
855
1068
  }
856
1069
  })
857
1070
  }
858
1071
  />
859
1072
  <Action
860
- title="Deselect All"
1073
+ title='Deselect All'
861
1074
  icon={Icon.XMarkCircle}
862
1075
  onAction={() => setSelectedThreads([])}
863
1076
  />
@@ -867,7 +1080,7 @@ export default function Command() {
867
1080
  {/* Primary actions */}
868
1081
  <ActionPanel.Section>
869
1082
  <Action.Push
870
- title="Open Thread"
1083
+ title='Open Thread'
871
1084
  icon={Icon.Eye}
872
1085
  target={
873
1086
  <ThreadDetail
@@ -879,57 +1092,90 @@ export default function Command() {
879
1092
  />
880
1093
  {!hasSelection && (
881
1094
  <Action
882
- title="Select Thread"
1095
+ title='Select Thread'
883
1096
  icon={Icon.CheckCircle}
884
- shortcut={{ modifiers: ['ctrl'], key: 'x' }}
1097
+ shortcut={{
1098
+ modifiers: ['ctrl'],
1099
+ key: 'x',
1100
+ }}
885
1101
  onAction={() => toggleSelection(thread.id)}
886
1102
  />
887
1103
  )}
888
1104
  <Action
889
- title={thread.unread ? 'Mark as Read' : 'Mark as Unread'}
1105
+ title={
1106
+ thread.unread ? 'Mark as Read' : 'Mark as Unread'
1107
+ }
890
1108
  icon={thread.unread ? Icon.Eye : Icon.EyeDisabled}
891
- shortcut={{ modifiers: ['ctrl'], key: 'u' }}
1109
+ shortcut={{
1110
+ modifiers: ['ctrl'],
1111
+ key: 'u',
1112
+ }}
892
1113
  onAction={async () => {
893
1114
  const { client } = await getClient([thread.account])
894
1115
  const result = thread.unread
895
- ? await client.markAsRead({ threadIds: [thread.id] })
896
- : await client.markAsUnread({ threadIds: [thread.id] })
1116
+ ? await client.markAsRead({
1117
+ threadIds: [thread.id],
1118
+ })
1119
+ : await client.markAsUnread({
1120
+ threadIds: [thread.id],
1121
+ })
897
1122
  if (result instanceof Error) {
898
1123
  await showFailureToast(result)
899
1124
  return
900
1125
  }
901
1126
  await showToast({
902
1127
  style: Toast.Style.Success,
903
- title: thread.unread ? 'Marked as read' : 'Marked as unread',
1128
+ title: thread.unread
1129
+ ? 'Marked as read'
1130
+ : 'Marked as unread',
904
1131
  })
905
1132
  revalidate()
906
1133
  }}
907
1134
  />
908
1135
  <Action
909
- title="Archive"
1136
+ title='Archive'
910
1137
  icon={Icon.Tray}
911
- shortcut={{ modifiers: ['ctrl'], key: 'e' }}
1138
+ shortcut={{
1139
+ modifiers: ['ctrl'],
1140
+ key: 'e',
1141
+ }}
912
1142
  onAction={async () => {
913
1143
  const { client } = await getClient([thread.account])
914
- const result = await client.archive({ threadIds: [thread.id] })
1144
+ const result = await client.archive({
1145
+ threadIds: [thread.id],
1146
+ })
915
1147
  if (result instanceof Error) {
916
1148
  await showFailureToast(result)
917
1149
  return
918
1150
  }
919
- await showToast({ style: Toast.Style.Success, title: 'Archived' })
1151
+ await showToast({
1152
+ style: Toast.Style.Success,
1153
+ title: 'Archived',
1154
+ })
920
1155
  revalidate()
921
1156
  }}
922
1157
  />
923
1158
  <Action
924
- title={thread.labelIds.includes('STARRED') ? 'Unstar' : 'Star'}
1159
+ title={
1160
+ thread.labelIds.includes('STARRED')
1161
+ ? 'Unstar'
1162
+ : 'Star'
1163
+ }
925
1164
  icon={Icon.Star}
926
- shortcut={{ modifiers: ['ctrl'], key: 's' }}
1165
+ shortcut={{
1166
+ modifiers: ['ctrl'],
1167
+ key: 's',
1168
+ }}
927
1169
  onAction={async () => {
928
1170
  const { client } = await getClient([thread.account])
929
1171
  const isStarred = thread.labelIds.includes('STARRED')
930
1172
  const result = isStarred
931
- ? await client.unstar({ threadIds: [thread.id] })
932
- : await client.star({ threadIds: [thread.id] })
1173
+ ? await client.unstar({
1174
+ threadIds: [thread.id],
1175
+ })
1176
+ : await client.star({
1177
+ threadIds: [thread.id],
1178
+ })
933
1179
  if (result instanceof Error) {
934
1180
  await showFailureToast(result)
935
1181
  return
@@ -944,11 +1190,14 @@ export default function Command() {
944
1190
  </ActionPanel.Section>
945
1191
 
946
1192
  {/* Reply & Forward */}
947
- <ActionPanel.Section title="Reply & Forward">
1193
+ <ActionPanel.Section title='Reply & Forward'>
948
1194
  <Action.Push
949
- title="Reply"
1195
+ title='Reply'
950
1196
  icon={Icon.Reply}
951
- shortcut={{ modifiers: ['ctrl'], key: 'r' }}
1197
+ shortcut={{
1198
+ modifiers: ['ctrl'],
1199
+ key: 'r',
1200
+ }}
952
1201
  target={
953
1202
  <ReplyForm
954
1203
  threadId={thread.id}
@@ -958,9 +1207,12 @@ export default function Command() {
958
1207
  }
959
1208
  />
960
1209
  <Action.Push
961
- title="Reply All"
1210
+ title='Reply All'
962
1211
  icon={Icon.Reply}
963
- shortcut={{ modifiers: ['ctrl', 'shift'], key: 'r' }}
1212
+ shortcut={{
1213
+ modifiers: ['ctrl', 'shift'],
1214
+ key: 'r',
1215
+ }}
964
1216
  target={
965
1217
  <ReplyForm
966
1218
  threadId={thread.id}
@@ -971,9 +1223,12 @@ export default function Command() {
971
1223
  }
972
1224
  />
973
1225
  <Action.Push
974
- title="Forward"
1226
+ title='Forward'
975
1227
  icon={Icon.Forward}
976
- shortcut={{ modifiers: ['ctrl'], key: 'f' }}
1228
+ shortcut={{
1229
+ modifiers: ['ctrl'],
1230
+ key: 'f',
1231
+ }}
977
1232
  target={
978
1233
  <ForwardForm
979
1234
  threadId={thread.id}
@@ -985,23 +1240,40 @@ export default function Command() {
985
1240
  </ActionPanel.Section>
986
1241
 
987
1242
  {/* Copy */}
988
- <ActionPanel.Section title="Copy">
989
- <Action.CopyToClipboard title="Copy Thread ID" content={thread.id} />
990
- <Action.CopyToClipboard title="Copy Subject" content={thread.subject} />
991
- <Action.CopyToClipboard title="Copy Sender Email" content={thread.from.email} />
1243
+ <ActionPanel.Section title='Copy'>
1244
+ <Action.CopyToClipboard
1245
+ title='Copy Thread ID'
1246
+ content={thread.id}
1247
+ />
1248
+ <Action.CopyToClipboard
1249
+ title='Copy Subject'
1250
+ content={thread.subject}
1251
+ />
1252
+ <Action.CopyToClipboard
1253
+ title='Copy Sender Email'
1254
+ content={thread.from.email}
1255
+ />
992
1256
  </ActionPanel.Section>
993
1257
 
994
1258
  {/* Danger */}
995
1259
  <ActionPanel.Section>
996
1260
  <Action
997
- title="Trash"
1261
+ title='Trash'
998
1262
  icon={Icon.Trash}
999
1263
  style={Action.Style.Destructive}
1000
- shortcut={{ modifiers: ['ctrl'], key: 'backspace' }}
1264
+ shortcut={{
1265
+ modifiers: ['ctrl'],
1266
+ key: 'backspace',
1267
+ }}
1001
1268
  onAction={async () => {
1002
1269
  const { client } = await getClient([thread.account])
1003
- await client.trash({ threadId: thread.id })
1004
- await showToast({ style: Toast.Style.Success, title: 'Trashed' })
1270
+ await client.trash({
1271
+ threadId: thread.id,
1272
+ })
1273
+ await showToast({
1274
+ style: Toast.Style.Success,
1275
+ title: 'Trashed',
1276
+ })
1005
1277
  revalidate()
1006
1278
  }}
1007
1279
  />
@@ -1010,15 +1282,21 @@ export default function Command() {
1010
1282
  {/* Utility */}
1011
1283
  <ActionPanel.Section>
1012
1284
  <Action
1013
- title="Refresh"
1285
+ title='Refresh'
1014
1286
  icon={Icon.ArrowClockwise}
1015
- shortcut={{ modifiers: ['ctrl', 'shift'], key: 'r' }}
1287
+ shortcut={{
1288
+ modifiers: ['ctrl', 'shift'],
1289
+ key: 'r',
1290
+ }}
1016
1291
  onAction={() => revalidate()}
1017
1292
  />
1018
1293
  <Action
1019
- title="Toggle Detail"
1294
+ title='Toggle Detail'
1020
1295
  icon={Icon.Sidebar}
1021
- shortcut={{ modifiers: ['ctrl'], key: 'd' }}
1296
+ shortcut={{
1297
+ modifiers: ['ctrl'],
1298
+ key: 'd',
1299
+ }}
1022
1300
  onAction={() => setIsShowingDetail((v) => !v)}
1023
1301
  />
1024
1302
  </ActionPanel.Section>