source dump of claude code
at main 126 lines 3.9 kB view raw
1import { createContext, useCallback, useContext, useMemo } from 'react' 2import { isProgressReportingAvailable, type Progress } from './terminal.js' 3import { BEL } from './termio/ansi.js' 4import { ITERM2, OSC, osc, PROGRESS, wrapForMultiplexer } from './termio/osc.js' 5 6type WriteRaw = (data: string) => void 7 8export const TerminalWriteContext = createContext<WriteRaw | null>(null) 9 10export const TerminalWriteProvider = TerminalWriteContext.Provider 11 12export type TerminalNotification = { 13 notifyITerm2: (opts: { message: string; title?: string }) => void 14 notifyKitty: (opts: { message: string; title: string; id: number }) => void 15 notifyGhostty: (opts: { message: string; title: string }) => void 16 notifyBell: () => void 17 /** 18 * Report progress to the terminal via OSC 9;4 sequences. 19 * Supported terminals: ConEmu, Ghostty 1.2.0+, iTerm2 3.6.6+ 20 * Pass state=null to clear progress. 21 */ 22 progress: (state: Progress['state'] | null, percentage?: number) => void 23} 24 25export function useTerminalNotification(): TerminalNotification { 26 const writeRaw = useContext(TerminalWriteContext) 27 if (!writeRaw) { 28 throw new Error( 29 'useTerminalNotification must be used within TerminalWriteProvider', 30 ) 31 } 32 33 const notifyITerm2 = useCallback( 34 ({ message, title }: { message: string; title?: string }) => { 35 const displayString = title ? `${title}:\n${message}` : message 36 writeRaw(wrapForMultiplexer(osc(OSC.ITERM2, `\n\n${displayString}`))) 37 }, 38 [writeRaw], 39 ) 40 41 const notifyKitty = useCallback( 42 ({ 43 message, 44 title, 45 id, 46 }: { 47 message: string 48 title: string 49 id: number 50 }) => { 51 writeRaw(wrapForMultiplexer(osc(OSC.KITTY, `i=${id}:d=0:p=title`, title))) 52 writeRaw(wrapForMultiplexer(osc(OSC.KITTY, `i=${id}:p=body`, message))) 53 writeRaw(wrapForMultiplexer(osc(OSC.KITTY, `i=${id}:d=1:a=focus`, ''))) 54 }, 55 [writeRaw], 56 ) 57 58 const notifyGhostty = useCallback( 59 ({ message, title }: { message: string; title: string }) => { 60 writeRaw(wrapForMultiplexer(osc(OSC.GHOSTTY, 'notify', title, message))) 61 }, 62 [writeRaw], 63 ) 64 65 const notifyBell = useCallback(() => { 66 // Raw BEL — inside tmux this triggers tmux's bell-action (window flag). 67 // Wrapping would make it opaque DCS payload and lose that fallback. 68 writeRaw(BEL) 69 }, [writeRaw]) 70 71 const progress = useCallback( 72 (state: Progress['state'] | null, percentage?: number) => { 73 if (!isProgressReportingAvailable()) { 74 return 75 } 76 if (!state) { 77 writeRaw( 78 wrapForMultiplexer( 79 osc(OSC.ITERM2, ITERM2.PROGRESS, PROGRESS.CLEAR, ''), 80 ), 81 ) 82 return 83 } 84 const pct = Math.max(0, Math.min(100, Math.round(percentage ?? 0))) 85 switch (state) { 86 case 'completed': 87 writeRaw( 88 wrapForMultiplexer( 89 osc(OSC.ITERM2, ITERM2.PROGRESS, PROGRESS.CLEAR, ''), 90 ), 91 ) 92 break 93 case 'error': 94 writeRaw( 95 wrapForMultiplexer( 96 osc(OSC.ITERM2, ITERM2.PROGRESS, PROGRESS.ERROR, pct), 97 ), 98 ) 99 break 100 case 'indeterminate': 101 writeRaw( 102 wrapForMultiplexer( 103 osc(OSC.ITERM2, ITERM2.PROGRESS, PROGRESS.INDETERMINATE, ''), 104 ), 105 ) 106 break 107 case 'running': 108 writeRaw( 109 wrapForMultiplexer( 110 osc(OSC.ITERM2, ITERM2.PROGRESS, PROGRESS.SET, pct), 111 ), 112 ) 113 break 114 case null: 115 // Handled by the if guard above 116 break 117 } 118 }, 119 [writeRaw], 120 ) 121 122 return useMemo( 123 () => ({ notifyITerm2, notifyKitty, notifyGhostty, notifyBell, progress }), 124 [notifyITerm2, notifyKitty, notifyGhostty, notifyBell, progress], 125 ) 126}