Carry a routed question across the move, and let the Test tab actually test

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
This commit is contained in:
2026-08-29 15:13:53 +05:30
parent 140c608f8f
commit 1d35358dd6
5 changed files with 53 additions and 8 deletions

View File

@@ -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"
/>
</div>
{/* 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 && (
<button
type="button"
onClick={() => onRunLive({
question,
scope: { contextId, disabledSkills: result.disabled, agent: draftAgent },
capability: null,
trace: { agentName: fields.name || 'This agent', surface: contextId },
})}
disabled={!question.trim() || !result.covers}
title={result.covers
? 'Ask this for real, in the Owliver panel'
: 'This agent does not cover the selected page'}
className="mt-2 inline-flex items-center gap-1.5 rounded-lg border border-border
bg-surface px-3 py-1.5 text-caption font-medium text-ink-2
hover:bg-surface-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Play className="h-3 w-3 shrink-0" aria-hidden="true" />
Ask Owliver for real
</button>
)}
</div>
</div>

View File

@@ -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

View File

@@ -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.')
);
}

View File

@@ -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);

View File

@@ -452,6 +452,7 @@ export default function AdminAgentDetail() {
)}
{view === 'test' && (
<AgentTestPanel
onRunLive={onTestCapability}
fields={fields}
dirty={dirty}
customSkills={customSkills}