From 1d35358dd679862e0a1a1f093d4f158696815199 Mon Sep 17 00:00:00 2001 From: Suriyakumarvijayanayagam Date: Sat, 29 Aug 2026 15:13:53 +0530 Subject: [PATCH] Carry a routed question across the move, and let the Test tab actually test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TWO fixes, both found by using the product rather than reading it. A question asked on a page that cannot answer it was lost. resolveIntent routes to the page that can, and routing.js answered "That is on Candidates. Taking you there now. Ask again once the page loads." — but the panel is page-scoped, so it re-mounts on the new route and the navigation destroys the very message explaining why the reader moved. What the reader saw was a different page and a fresh greeting, with no trace of what they asked. The question now travels with the destination. AssistantPanelContext sits ABOVE the router and already has `ask` for exactly this — a page handing a question to the panel — so the panel that mounts on the other side asks it. That is what the reader wanted, and what "ask again once the page loads" was apologising for. It cannot loop: resolveIntent only routes when the destination differs from the page you are on, and answerableHere short-circuits before that. The Test tab did not test. "Simulation & Scope Diagnostics" reads where a question WOULD route — which skills are reachable, which tools are in scope, what the classifier makes of it — and never calls the model. That is genuinely useful and it is not what a tab called Test leads anyone to expect. A real test did exist, but under Skills, as "Test in Owliver" on a capability card. So the Simulated User Query the tab has always shown is now runnable, through the same path that card uses: the existing Owliver panel, scoped to the draft's own agent and disabled skills, so the answer comes from the agent being edited rather than the published one. The diagnostics stay — they answer a different and still useful question. The button is disabled when the agent does not cover the selected page, with the reason in its title, rather than offering a run that would decline. 924/924 checks pass and the production build is clean. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g --- src/components/agents/AgentTestPanel.jsx | 30 ++++++++++++++++++- src/components/ai-assistant/KrowAssistant.jsx | 12 ++++++-- src/components/ai-assistant/routing.js | 2 +- src/components/ai-assistant/useAssistant.js | 16 +++++++--- src/pages/admin/AgentDetail.jsx | 1 + 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/components/agents/AgentTestPanel.jsx b/src/components/agents/AgentTestPanel.jsx index 6ca5613..9c0867f 100644 --- a/src/components/agents/AgentTestPanel.jsx +++ b/src/components/agents/AgentTestPanel.jsx @@ -47,7 +47,7 @@ const CLASSIFICATION_COPY = { }; /** @param {any} props */ -export function AgentTestPanel({ fields, dirty, customSkills = [] }) { +export function AgentTestPanel({ fields, dirty, customSkills = [], onRunLive }) { const [contextId, setContextId] = React.useState( () => CONTEXT_OPTIONS.find((o) => fields.pages.includes(pageKeyForContext(o.contextId)))?.contextId || CONTEXT_OPTIONS[0].contextId @@ -79,6 +79,7 @@ export function AgentTestPanel({ fields, dirty, customSkills = [] }) { return { covers, + disabled, pageOnly, scoped, matched, @@ -152,6 +153,33 @@ export function AgentTestPanel({ fields, dirty, customSkills = [] }) { className="pl-9 w-full bg-surface" /> + {/* Everything else on this tab is a STATIC read of where a + question would route. Useful, and not the same as asking. The + tab is called Test, so a reader reasonably expects to be able + to run the thing — this is that, through the existing Owliver + panel and the draft's own scope, so the answer comes from the + agent being edited rather than the published one. */} + {onRunLive && ( + + )} diff --git a/src/components/ai-assistant/KrowAssistant.jsx b/src/components/ai-assistant/KrowAssistant.jsx index 4a17a0f..0a46248 100644 --- a/src/components/ai-assistant/KrowAssistant.jsx +++ b/src/components/ai-assistant/KrowAssistant.jsx @@ -281,13 +281,21 @@ export default function KrowAssistant({ request: panelRequest, consumeRequest, notice: panelNotice, consumeNotice, test: panelTest, reportTest, clearTest, + ask: askAssistant, } = useAssistantPanel(); /* The app's own router, not a location assignment: a full page load would discard the thread and the panel state along with it. */ const goToPage = React.useCallback( - (destination) => navigate(destination.route), - [navigate] + (destination, question) => { + navigate(destination.route); + /* Hand the question to the panel that mounts on the other side. The + request lives in AssistantPanelContext, which sits ABOVE the router, so + it survives the navigation that discards this panel's thread. Without + this the reader has to retype what they just typed. */ + if (question) askAssistant({ question }); + }, + [navigate, askAssistant] ); /* Skills available here, and the roles they can act on — both read from what diff --git a/src/components/ai-assistant/routing.js b/src/components/ai-assistant/routing.js index f6382e4..a82fa00 100644 --- a/src/components/ai-assistant/routing.js +++ b/src/components/ai-assistant/routing.js @@ -159,7 +159,7 @@ export function answerableHere(context, question) { export function navigationAnswer(destination) { return doc( text(`That is on **${destination.page}**. Taking you there now.`), - note('Ask again once the page loads and I will answer from it.') + note('Bringing your question with me — I will answer it from that page.') ); } diff --git a/src/components/ai-assistant/useAssistant.js b/src/components/ai-assistant/useAssistant.js index 6701278..a2c1df9 100644 --- a/src/components/ai-assistant/useAssistant.js +++ b/src/components/ai-assistant/useAssistant.js @@ -723,12 +723,20 @@ export function useConversation({ abortRef.current = null; } - /* Navigate after the reply is on screen, so the user reads why they moved. - The panel re-resolves its context from the new route, which is what - makes the next question answer from the page they land on. */ + /* Navigate after the reply is on screen — except the reply does not + survive the move. The panel is page-scoped, so it re-mounts on the new + route and the message explaining why the reader moved is destroyed by + the navigation that message was explaining. The reader landed somewhere + else with a fresh greeting and no trace of what they asked. + + So the question travels with the destination. The panel on the other + side asks it, which is what the reader wanted in the first place and + what the old copy ("ask again once the page loads") was apologising + for. Re-asking cannot loop: resolveIntent only navigates when the + destination differs from the current page. */ if (controller.signal.aborted) return; - if (intent.kind === 'navigate') onNavigate?.(intent.destination); + if (intent.kind === 'navigate') onNavigate?.(intent.destination, text); /* A skill's action runs after its reply, for the same reason: the user should read why the form opened before it opens. */ if (intent.kind === 'skill' && intent.action) onAction?.(intent.action, intent.skill); diff --git a/src/pages/admin/AgentDetail.jsx b/src/pages/admin/AgentDetail.jsx index 3625525..444d5b0 100644 --- a/src/pages/admin/AgentDetail.jsx +++ b/src/pages/admin/AgentDetail.jsx @@ -452,6 +452,7 @@ export default function AdminAgentDetail() { )} {view === 'test' && (