diff --git a/CHANGELOG.md b/CHANGELOG.md
index b92590e..0e7ddd6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,7 @@ All notable changes to this project will be documented in this file.
### Fixes
+- Fix workspace intent bar erroring on simple questions / read-only queries instead of showing response text
- Fix bottom bar button/input height mismatch on desktop (Send and Mic buttons now match input height at sm+ breakpoint)
- Fix missing voice and visual feedback after changeset execution via voice approval (PR `#29`)
- Add error handling and logging to Gemini Live tool response pipeline to prevent silent failures (PR `#29`)
diff --git a/components/workspace/intent-bar.tsx b/components/workspace/intent-bar.tsx
index 03fc5ea..e84e859 100644
--- a/components/workspace/intent-bar.tsx
+++ b/components/workspace/intent-bar.tsx
@@ -73,6 +73,8 @@ export function IntentBar({
cancelDraft,
draftChangeset,
executionError,
+ infoResponse,
+ dismissInfo,
} = useWorkspace();
const { isDemo } = useLayout();
const [input, setInput] = useState("");
@@ -90,13 +92,14 @@ export function IntentBar({
if (draftChangeset && (phase === "preview" || phase === "executing")) {
return "Review changeset above";
}
+ if (phase === "complete" && infoResponse) return "Ask another question or describe a change...";
if (phase === "complete") return "Changes applied";
if (selectedProducts.length === 0)
return "Describe a commerce change...";
if (selectedProducts.length === 1)
return `${selectedProducts[0].name} — what to change?`;
return `${selectedProducts.length} products — what to change?`;
- }, [selectedProducts, draftChangeset, phase, voiceActive, voiceConnecting]);
+ }, [selectedProducts, draftChangeset, phase, voiceActive, voiceConnecting, infoResponse]);
const chips = useMemo(() => {
if (selectedProducts.length === 0) return [];
@@ -107,9 +110,10 @@ export function IntentBar({
const handleSubmit = useCallback(() => {
const trimmed = input.trim();
if (!trimmed || phase === "executing" || phase === "preview") return;
+ if (phase === "complete" && !infoResponse) return;
setInput("");
submitIntent(trimmed);
- }, [input, phase, submitIntent]);
+ }, [input, phase, submitIntent, infoResponse]);
const isBusy = phase === "executing" || phase === "preview";
const hasDraft = !!draftChangeset && phase === "preview";
@@ -125,14 +129,51 @@ export function IntentBar({
)}
- {/* Completion indicator */}
- {phase === "complete" && (
+ {/* Completion indicator (changeset executed) */}
+ {phase === "complete" && !infoResponse && (
Changes applied successfully
)}
+ {/* Informational response (read-only query / no operations) */}
+ {phase === "complete" && infoResponse && (
+
+
{infoResponse.reasoning}
+ {infoResponse.readerText && (
+
+ {infoResponse.readerText}
+
+ )}
+ {infoResponse.suggestions && infoResponse.suggestions.length > 0 && (
+
+ {infoResponse.suggestions.map((s) => (
+
+ ))}
+
+ )}
+
+
+ )}
+
{/* Error indicator */}
{phase === "error" && (
@@ -203,7 +244,7 @@ export function IntentBar({
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={placeholder}
- disabled={isBusy || phase === "complete"}
+ disabled={isBusy || (phase === "complete" && !infoResponse)}
className="flex-1"
/>
void;
selectedIds: Set;
draftChangeset: ChangeSet | null;
phase: WorkspacePhase;
@@ -136,11 +139,39 @@ const ChangeSetSchema = z.object({
}).passthrough();
const OrchestratorResponseSchema = z.object({
- changeSet: ChangeSetSchema,
+ changeSet: ChangeSetSchema.nullable(),
reasoning: z.string(),
readerText: z.string().optional(),
+ suggestions: z.array(z.string()).optional(),
});
+/**
+ * Resolve an orchestrator response into either a ChangeSet (action) or an
+ * informational response (read-only query / no operations).
+ */
+function resolveOrchestratorResponse(
+ data: z.infer,
+ setDraftChangeset: (cs: ChangeSet | null) => void,
+ setInfoResponse: (info: { reasoning: string; readerText?: string; suggestions?: string[] } | null) => void,
+ setPhase: (phase: WorkspacePhase) => void,
+): ChangeSet | null {
+ const { changeSet: rawCs, reasoning, readerText, suggestions } = data;
+
+ // Read-only / informational response (no operations to draft)
+ if (!rawCs || !Array.isArray(rawCs.operations) || rawCs.operations.length === 0) {
+ setDraftChangeset(null);
+ setInfoResponse({ reasoning, readerText, suggestions });
+ setPhase("complete");
+ return null;
+ }
+
+ // Schema validates structural shape; cast through unknown since Zod passthrough
+ // infers a wider type than the full ChangeSet with its deep nested types
+ const cs = rawCs as unknown as ChangeSet;
+ setDraftChangeset(cs);
+ return cs;
+}
+
const ExecutorResponseSchema = z.object({
changeSet: ChangeSetSchema,
}).passthrough();
@@ -453,6 +484,11 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) {
const [chatActivityPhase, setChatActivityPhase] = useState("idle");
const [chatActivityChangeset, setChatActivityChangeset] = useState(null);
const [executionError, setExecutionError] = useState(null);
+ const [infoResponse, setInfoResponse] = useState<{
+ reasoning: string;
+ readerText?: string;
+ suggestions?: string[];
+ } | null>(null);
const [fetchAttempt, setFetchAttempt] = useState(0);
const executeInFlightRef = useRef(false);
@@ -645,6 +681,7 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) {
async (text: string): Promise => {
setPhase("preview");
setExecutionError(null);
+ setInfoResponse(null);
setDraftChangeset(null); // clear stale draft immediately
try {
const selectedProducts = products.filter((p) =>
@@ -655,9 +692,11 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) {
? `\n\nSelected products: ${selectedProducts.map((p) => `${p.name} (${p.sku})`).join(", ")}`
: "";
+ const fetchHeaders: Record = { "Content-Type": "application/json" };
+ if (isDemo) fetchHeaders["x-demo-session"] = "1";
const res = await fetch("/api/orchestrator", {
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: fetchHeaders,
body: JSON.stringify({ message: text + context }),
});
@@ -675,31 +714,22 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) {
setPhase("error");
return null;
}
- // Schema validates structural shape; cast through unknown since Zod passthrough
- // infers a wider type than the full ChangeSet with its deep nested types
- const cs = validated.data.changeSet as unknown as ChangeSet;
- if (!Array.isArray(cs.operations) || cs.operations.length === 0) {
- console.error("[workspace] Orchestrator returned changeset with 0 operations");
- setExecutionError("No operations generated — try a more specific request (e.g. \"Change price to $79\")");
- setDraftChangeset(null);
- setPhase("error");
- return null;
- }
- setDraftChangeset(cs);
- return cs;
+
+ return resolveOrchestratorResponse(validated.data, setDraftChangeset, setInfoResponse, setPhase);
} catch {
setDraftChangeset(null);
setPhase("error");
return null;
}
},
- [products, selectedIds],
+ [products, selectedIds, isDemo],
);
const submitIntentForProduct = useCallback(
async (text: string, productId: string): Promise => {
setPhase("preview");
setExecutionError(null);
+ setInfoResponse(null);
setDraftChangeset(null); // clear stale draft immediately
try {
const targetProduct = products.find((p) => p.id === productId);
@@ -711,9 +741,11 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) {
}
const context = `\n\nSelected products: ${targetProduct.name} (${targetProduct.sku})`;
+ const fetchHeaders: Record = { "Content-Type": "application/json" };
+ if (isDemo) fetchHeaders["x-demo-session"] = "1";
const res = await fetch("/api/orchestrator", {
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: fetchHeaders,
body: JSON.stringify({ message: text + context }),
});
@@ -731,23 +763,15 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) {
setPhase("error");
return null;
}
- const cs = validated.data.changeSet as unknown as ChangeSet;
- if (!Array.isArray(cs.operations) || cs.operations.length === 0) {
- console.error("[workspace] Orchestrator returned changeset with 0 operations");
- setExecutionError("No operations generated — try a more specific request (e.g. \"Change price to $79\")");
- setDraftChangeset(null);
- setPhase("error");
- return null;
- }
- setDraftChangeset(cs);
- return cs;
+
+ return resolveOrchestratorResponse(validated.data, setDraftChangeset, setInfoResponse, setPhase);
} catch {
setDraftChangeset(null);
setPhase("error");
return null;
}
},
- [products],
+ [products, isDemo],
);
const executeChangeset = useCallback(async (overrideCs?: ChangeSet): Promise<{ success: boolean; status?: string; error?: string }> => {
@@ -761,9 +785,11 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) {
setPhase("executing");
setExecutionError(null);
try {
+ const execHeaders: Record = { "Content-Type": "application/json" };
+ if (isDemo) execHeaders["x-demo-session"] = "1";
const res = await fetch("/api/orchestrator/execute", {
method: "POST",
- headers: { "Content-Type": "application/json" },
+ headers: execHeaders,
body: JSON.stringify({ changeSet: cs }),
});
if (!res.ok) {
@@ -824,15 +850,21 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) {
executeInFlightRef.current = false;
return { success: false, status: "failed", error: msg };
}
- }, [draftChangeset]);
+ }, [draftChangeset, isDemo]);
const cancelDraft = useCallback(() => {
setDraftChangeset(null);
setExecutionError(null);
+ setInfoResponse(null);
executeInFlightRef.current = false;
setPhase("idle");
}, []);
+ const dismissInfo = useCallback(() => {
+ setInfoResponse(null);
+ setPhase("idle");
+ }, []);
+
const applyExecutedChangeset = useCallback((cs: ChangeSet) => {
const opsToApply = filterSuccessfulOps(
cs.operations,
@@ -863,6 +895,8 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) {
loading,
fetchError,
executionError,
+ infoResponse,
+ dismissInfo,
selectedIds,
draftChangeset,
phase,
@@ -890,6 +924,8 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) {
loading,
fetchError,
executionError,
+ infoResponse,
+ dismissInfo,
selectedIds,
draftChangeset,
phase,