/ ink / hooks / use-tab-status.ts
use-tab-status.ts
 1  import { useContext, useEffect, useRef } from 'react'
 2  import {
 3    CLEAR_TAB_STATUS,
 4    supportsTabStatus,
 5    tabStatus,
 6    wrapForMultiplexer,
 7  } from '../termio/osc.js'
 8  import type { Color } from '../termio/types.js'
 9  import { TerminalWriteContext } from '../useTerminalNotification.js'
10  
11  export type TabStatusKind = 'idle' | 'busy' | 'waiting'
12  
13  const rgb = (r: number, g: number, b: number): Color => ({
14    type: 'rgb',
15    r,
16    g,
17    b,
18  })
19  
20  // Per the OSC 21337 usage guide's suggested mapping.
21  const TAB_STATUS_PRESETS: Record<
22    TabStatusKind,
23    { indicator: Color; status: string; statusColor: Color }
24  > = {
25    idle: {
26      indicator: rgb(0, 215, 95),
27      status: 'Idle',
28      statusColor: rgb(136, 136, 136),
29    },
30    busy: {
31      indicator: rgb(255, 149, 0),
32      status: 'Working…',
33      statusColor: rgb(255, 149, 0),
34    },
35    waiting: {
36      indicator: rgb(95, 135, 255),
37      status: 'Waiting',
38      statusColor: rgb(95, 135, 255),
39    },
40  }
41  
42  /**
43   * Declaratively set the tab-status indicator (OSC 21337).
44   *
45   * Emits a colored dot + short status text to the tab sidebar. Terminals
46   * that don't support OSC 21337 discard the sequence silently, so this is
47   * safe to call unconditionally. Wrapped for tmux/screen passthrough.
48   *
49   * Pass `null` to opt out. If a status was previously set, transitioning to
50   * `null` emits CLEAR_TAB_STATUS so toggling off mid-session doesn't leave
51   * a stale dot. Process-exit cleanup is handled by ink.tsx's unmount path.
52   */
53  export function useTabStatus(kind: TabStatusKind | null): void {
54    const writeRaw = useContext(TerminalWriteContext)
55    const prevKindRef = useRef<TabStatusKind | null>(null)
56  
57    useEffect(() => {
58      // When kind transitions from non-null to null (e.g. user toggles off
59      // showStatusInTerminalTab mid-session), clear the stale dot.
60      if (kind === null) {
61        if (prevKindRef.current !== null && writeRaw && supportsTabStatus()) {
62          writeRaw(wrapForMultiplexer(CLEAR_TAB_STATUS))
63        }
64        prevKindRef.current = null
65        return
66      }
67  
68      prevKindRef.current = kind
69      if (!writeRaw || !supportsTabStatus()) return
70      writeRaw(wrapForMultiplexer(tabStatus(TAB_STATUS_PRESETS[kind])))
71    }, [kind, writeRaw])
72  }