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:
@@ -78,6 +78,7 @@ import { useSagiBrain } from "./useSagiBrain";
|
||||
import { useSagiPosition, SAGI_SIZE } from "./useSagiPosition";
|
||||
import { matchIntent } from "./intents";
|
||||
import { sagiPrefs } from "./prefs";
|
||||
import { api } from "../../lib/api";
|
||||
import "./sagi.css";
|
||||
|
||||
const FLYOUT_GAP = 10; // px between avatar and flyout
|
||||
@@ -196,11 +197,31 @@ export function Sagi() {
|
||||
(query: string): string | null => {
|
||||
const result = matchIntent(query, brain.status);
|
||||
if (result.kind === "answer") return result.text;
|
||||
// Handoff: spawn a real claude via the existing Run page. `autostart=1`
|
||||
// tells Run to fire the prompt automatically once it's prefilled, so the
|
||||
// question is actually sent instead of just dropped into the composer.
|
||||
navigate(`/run?prompt=${encodeURIComponent(result.prompt)}&autostart=1`);
|
||||
setOpen(false);
|
||||
// Handoff: ensure the "Sagi" lane exists for this dashboard, spawn a
|
||||
// fresh claude session in it carrying the user's prompt as its first
|
||||
// message, then jump to the Run page so the live pane is observable.
|
||||
// Fire-and-forget — the panel already clears its composer on null and
|
||||
// closes; errors surface as an answer instead of silently swallowing.
|
||||
const prompt = result.prompt;
|
||||
brain.setThinking(true);
|
||||
(async () => {
|
||||
try {
|
||||
const { lane } = await api.lanes.ensure({ title: "Sagi" });
|
||||
await api.run.start({
|
||||
laneId: lane.id,
|
||||
cwd: lane.cwd,
|
||||
initialPrompt: prompt,
|
||||
permissionMode: "acceptEdits",
|
||||
});
|
||||
navigate(`/run?laneId=${lane.id}&autostart=1`);
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
brain.setBubble(`couldn't start that session: ${msg}`);
|
||||
} finally {
|
||||
brain.setThinking(false);
|
||||
}
|
||||
})();
|
||||
return null;
|
||||
},
|
||||
[brain, navigate]
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user