Summary
Deep-linking into the chat app with query params to pre-seed and auto-send a prompt is broken in three distinct ways. All three live in web/src/views/AppPage.tsx, web/src/hooks/useChatSessionController.ts, and web/src/app/app/services/searchParams.ts.
Environment
- Deployment: self-hosted 4.4.7
- Branch/commit:
main
- Browser: Chrome Stable
Reproduction
https://onyx.example.org/app?agentId=8&user-prompt=Example%20prompt&send-on-load=true
https://onyx.example.org/app?agentId=8&user-prompt=Example%20prompt&submit-on-load=true
https://onyx.example.org/app?agentId=8&user-prompt=Example%20prompt&send-on-load=false
Expected vs. Actual
| # |
Scenario |
Expected |
Actual |
| 1 |
send-on-load=true + agentId=8 |
Prompt sent as agent 8 |
Sent as default persona |
| 2 |
submit-on-load=true + agentId=8 |
Prompt sent as agent 8, prompt visible in user bubble |
Sent as default persona and user bubble is empty |
| 3 |
send-on-load=false |
Nothing auto-sends |
Prompt auto-sends anyway |
Root causes
3 — send-on-load is truthiness-checked, not parsed
// web/src/views/AppPage.tsx
useEffect(() => {
if (searchParams?.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD)) {
processSearchParamsAndSubmitMessage(searchParams.toString());
}
}, [searchParams, router]);
searchParams.get() returns the string "false", which is truthy [1]. A correct parser already exists in the codebase and simply isn't used here:
// web/src/app/app/services/searchParams.ts
export function shouldSubmitOnLoad(searchParams) {
const raw = searchParams?.get(SEARCH_PARAM_NAMES.SUBMIT_ON_LOAD);
return raw === "true" || raw === "1";
}
[2] — note it only covers SUBMIT_ON_LOAD. SEND_ON_LOAD has no equivalent.
2 — submit-on-load submits firstMessage, not user-prompt
// web/src/app/app/page.tsx
const firstMessage = searchParams.firstMessage;
return <AppPage firstMessage={firstMessage} />;
// web/src/hooks/useChatSessionController.ts — initialSessionFetch()
setSelectedAgentFromId(null); // ← agent reset to default
...
if (shouldSubmitOnLoad(searchParams) && !submitOnLoadPerformed.current) {
submitOnLoadPerformed.current = true;
await onSubmit({
message: firstMessage || "", // ← user-prompt never read
currentMessageFiles: [],
deepResearch: false,
});
}
[3]
Two bugs in one block:
- The message comes from a different, undocumented param (
firstMessage), so user-prompt is dropped and an empty string is submitted → blank user bubble.
setSelectedAgentFromId(null) runs immediately before, actively clearing any agent selection before the send.
1 — send-on-load never reads agentId
processSearchParamsAndSubmitMessage reads user-prompt, builds filters, deletes SEND_ON_LOAD, and calls onSubmit — it never touches agentId [1]. Agent resolution is a separate async path (useAgentController → useAgents() SWR), and useChatController creates the session with liveAgent?.id || 0. On a cold load the effect fires while agents is still loading, so the session is bound to persona 0 before agentId=8 ever resolves. The effect's dependency array is [searchParams, router] — no gate on isLoadingAgents [1].
Suggested fixes
Consider consolidating send-on-load and submit-on-load. Two params doing nearly the same thing through two different code paths is very likely how this divergence happened — deprecate one, alias it to the other.
- Gate auto-submit on agent readiness. Don't fire until
!isLoadingAgents, and ideally until liveAgent?.id === Number(searchParams.get("agentId")).
- Pass
agentId explicitly through the submit path rather than relying on liveAgent having resolved — e.g. const personaId = liveAgent?.id ?? agentIdFromParams ?? DEFAULT_AGENT_ID.
- Don't call
setSelectedAgentFromId(null) on the submit-on-load path when agentId is present in the URL.
- Read
user-prompt in the submit-on-load path, or at minimum fall back: firstMessage || searchParams.get(SEARCH_PARAM_NAMES.USER_PROMPT).
- Use
shouldSubmitOnLoad-style parsing for SEND_ON_LOAD (=== "true" || === "1").
Summary
Deep-linking into the chat app with query params to pre-seed and auto-send a prompt is broken in three distinct ways. All three live in
web/src/views/AppPage.tsx,web/src/hooks/useChatSessionController.ts, andweb/src/app/app/services/searchParams.ts.Environment
mainReproduction
Expected vs. Actual
send-on-load=true+agentId=8submit-on-load=true+agentId=8send-on-load=falseRoot causes
3 —
send-on-loadis truthiness-checked, not parsedsearchParams.get()returns the string"false", which is truthy [1]. A correct parser already exists in the codebase and simply isn't used here:[2] — note it only covers
SUBMIT_ON_LOAD.SEND_ON_LOADhas no equivalent.2 —
submit-on-loadsubmitsfirstMessage, notuser-prompt[3]
Two bugs in one block:
firstMessage), souser-promptis dropped and an empty string is submitted → blank user bubble.setSelectedAgentFromId(null)runs immediately before, actively clearing any agent selection before the send.1 —
send-on-loadnever readsagentIdprocessSearchParamsAndSubmitMessagereadsuser-prompt, builds filters, deletesSEND_ON_LOAD, and callsonSubmit— it never touchesagentId[1]. Agent resolution is a separate async path (useAgentController→useAgents()SWR), anduseChatControllercreates the session withliveAgent?.id || 0. On a cold load the effect fires whileagentsis still loading, so the session is bound to persona0beforeagentId=8ever resolves. The effect's dependency array is[searchParams, router]— no gate onisLoadingAgents[1].Suggested fixes
Consider consolidating
send-on-loadandsubmit-on-load. Two params doing nearly the same thing through two different code paths is very likely how this divergence happened — deprecate one, alias it to the other.!isLoadingAgents, and ideally untilliveAgent?.id === Number(searchParams.get("agentId")).agentIdexplicitly through the submit path rather than relying onliveAgenthaving resolved — e.g.const personaId = liveAgent?.id ?? agentIdFromParams ?? DEFAULT_AGENT_ID.setSelectedAgentFromId(null)on the submit-on-load path whenagentIdis present in the URL.user-promptin thesubmit-on-loadpath, or at minimum fall back:firstMessage || searchParams.get(SEARCH_PARAM_NAMES.USER_PROMPT).shouldSubmitOnLoad-style parsing forSEND_ON_LOAD(=== "true" || === "1").