(cursor: MessageActionsState | null, setCursor: React.Dispatch<React.SetStateAction<MessageActionsState | null>>, navRef: RefObject<MessageActionsNav | null>, caps: MessageActionCaps)
| 215 | |
| 216 | // Can't call useKeybindings here — hook runs outside <KeybindingSetup> provider. Returns handlers instead. |
| 217 | export function useMessageActions(cursor: MessageActionsState | null, setCursor: React.Dispatch<React.SetStateAction<MessageActionsState | null>>, navRef: RefObject<MessageActionsNav | null>, caps: MessageActionCaps): { |
| 218 | enter: () => void; |
| 219 | handlers: Record<string, () => void>; |
| 220 | } { |
| 221 | // Refs keep handlers stable — no useKeybindings re-register per message append. |
| 222 | const cursorRef = useRef(cursor); |
| 223 | cursorRef.current = cursor; |
| 224 | const capsRef = useRef(caps); |
| 225 | capsRef.current = caps; |
| 226 | const handlers = useMemo(() => { |
| 227 | const h: Record<string, () => void> = { |
| 228 | 'messageActions:prev': () => navRef.current?.navigatePrev(), |
| 229 | 'messageActions:next': () => navRef.current?.navigateNext(), |
| 230 | 'messageActions:prevUser': () => navRef.current?.navigatePrevUser(), |
| 231 | 'messageActions:nextUser': () => navRef.current?.navigateNextUser(), |
| 232 | 'messageActions:top': () => navRef.current?.navigateTop(), |
| 233 | 'messageActions:bottom': () => navRef.current?.navigateBottom(), |
| 234 | 'messageActions:escape': () => setCursor(c => c?.expanded ? { |
| 235 | ...c, |
| 236 | expanded: false |
| 237 | } : null), |
| 238 | // ctrl+c skips the collapse step — from expanded-during-streaming, two-stage |
| 239 | // would mean 3 presses to interrupt (collapse→null→cancel). |
| 240 | 'messageActions:ctrlc': () => setCursor(null) |
| 241 | }; |
| 242 | for (const key of new Set(MESSAGE_ACTIONS.map(a_1 => a_1.key))) { |
| 243 | h[`messageActions:${key}`] = () => { |
| 244 | const c_0 = cursorRef.current; |
| 245 | if (!c_0) return; |
| 246 | const a_0 = MESSAGE_ACTIONS.find(a => a.key === key && isApplicable(a, c_0)); |
| 247 | if (!a_0) return; |
| 248 | if (a_0.stays) { |
| 249 | setCursor(c_1 => c_1 ? { |
| 250 | ...c_1, |
| 251 | expanded: !c_1.expanded |
| 252 | } : null); |
| 253 | return; |
| 254 | } |
| 255 | const m = navRef.current?.getSelected(); |
| 256 | if (!m) return; |
| 257 | (a_0.run as (m: NavigableMessage, c_0: MessageActionCaps) => void)(m, capsRef.current); |
| 258 | setCursor(null); |
| 259 | }; |
| 260 | } |
| 261 | return h; |
| 262 | }, [setCursor, navRef]); |
| 263 | const enter = useCallback(() => { |
| 264 | logEvent('tengu_message_actions_enter', {}); |
| 265 | navRef.current?.enterCursor(); |
| 266 | }, [navRef]); |
| 267 | return { |
| 268 | enter, |
| 269 | handlers |
| 270 | }; |
| 271 | } |
| 272 | |
| 273 | // Must mount inside <KeybindingSetup>. |
| 274 | export function MessageActionsKeybindings(t0) { |
no test coverage detected