feat(sagi): spawn a real claude session for Sagi's Ask handoff

Unmatched Ask questions now adopt a dedicated "Sagi" lane (POST
/api/lanes/ensure, cwd defaults to $HOME) and spawn a tmux-backed claude
run there instead of just prefilling the Run page composer.
This commit is contained in:
2026-08-24 09:39:28 +07:00
parent 510f015fdf
commit b1bca07c47
6 changed files with 99 additions and 26 deletions
+29 -5
View File
@@ -87,6 +87,10 @@ export interface SagiBrain {
status: SagiStatus;
bubble: string | null;
dismissBubble: () => void;
/** Imperative bubble setter for one-off messages from the shell (e.g. spawn
* errors). Bypasses the throttle because the caller decided this message
* must be seen right now. Auto-clears after the standard bubble TTL. */
setBubble: (text: string) => void;
muted: boolean;
toggleMute: () => void;
clearAlerts: () => void;
@@ -103,7 +107,7 @@ export function useSagiBrain(): SagiBrain {
connected: true,
}));
const [tick, setTick] = useState(now0);
const [bubble, setBubble] = useState<string | null>(null);
const [bubble, setBubbleState] = useState<string | null>(null);
const [muted, setMuted] = useState<boolean>(() => sagiPrefs.getMuted());
const bubbleTimer = useRef<ReturnType<typeof setTimeout>>();
@@ -121,8 +125,8 @@ export function useSagiBrain(): SagiBrain {
if (!force && t - lastBubbleAt.current < BUBBLE_THROTTLE_MS) return;
lastBubbleAt.current = t;
clearTimeout(bubbleTimer.current);
setBubble(text);
bubbleTimer.current = setTimeout(() => setBubble(null), BUBBLE_MS);
setBubbleState(text);
bubbleTimer.current = setTimeout(() => setBubbleState(null), BUBBLE_MS);
}, []);
// Seed from the REST snapshot on mount so counts are accurate immediately -
@@ -176,7 +180,7 @@ export function useSagiBrain(): SagiBrain {
const dismissBubble = useCallback(() => {
clearTimeout(bubbleTimer.current);
setBubble(null);
setBubbleState(null);
}, []);
const toggleMute = useCallback(() => {
@@ -193,5 +197,25 @@ export function useSagiBrain(): SagiBrain {
[]
);
return { mood, status, bubble, dismissBubble, muted, toggleMute, clearAlerts, setThinking };
// One-shot bubble: clears itself after BUBBLE_MS. Used by the shell for
// imperative messages (e.g. spawn errors) that must appear now and fade,
// bypassing the throttled mood-driven showBubble path.
const setBubble = useCallback((text: string) => {
if (!text) return;
clearTimeout(bubbleTimer.current);
setBubbleState(text);
bubbleTimer.current = setTimeout(() => setBubbleState(null), BUBBLE_MS);
}, []);
return {
mood,
status,
bubble,
dismissBubble,
setBubble,
muted,
toggleMute,
clearAlerts,
setThinking,
};
}