Skip to content

🎛️ refactor: Supply App-Global Chat Settings From the Host - #16466

Merged
berry-13 merged 3 commits into
canaryfrom
berry-13/chat-host-supplied
Sep 28, 2026
Merged

berry-13 merged 3 commits into
canaryfrom
berry-13/chat-host-supplied

Conversation

@berry-13

@berry-13 berry-13 commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Pull Request

Summary

The chat hooks in hooks/Chat and hooks/SSE read app-global preferences (the during-run default action, steer interrupts, and the artifacts panel reset) straight out of Recoil ~/store, so the chat cannot run under a host that keeps its settings anywhere else. The AI SDK-shaped useChat facade also had no production consumer, only its spec.

This adds a small ChatSettings context that the host supplies. Root fills it from the existing Recoil atoms, so every preference behaves as before, and useSteering, DuringRunSendButton and useIdChangeEffect read it instead of ~/store. A preference joins the context only when every chat reader of it can move, so no reader acts on a different value than the host supplies. A render with no host falls back to the stock defaults. liveAppliedSteerIds, which only the steering feature reads and writes, moves to Jotai with its readers (SteerPart) and its writer (useResumableSSE). Manual compaction (useCompactConversation) now submits and reads its status through the facade's new useChatActions: the useChat surface without messages, which re-renders only when the status changes, so the facade has a real call site that does not map the conversation on every stream frame. Giving it one exposed a facade bug: it re-read on every query cache event, synchronously, so a component mounting a messages query mid-render updated the facade's consumer inside that render (a "Maximum update depth" crash on the first send of a new chat). It now listens only to data events and delivers them after the render, and useCompactConversation leaves the hooks/Chat barrel so importing ~/hooks does not pull the facade's message components into a cycle.

saveDrafts and isTemporary fail that test, since ChatView (#15684), ChatForm (#16457) and the resume hook (#13811) read them too, so they stay on the store. Atoms that files in open PRs read or write stay on Recoil for now: #13811 (useResumeOnLoad, useTemporaryChat, useBookmarkItems), #16457 (ChatForm) and #15684 (ChatView). The queue run-end atoms also stay on Recoil, because they share Recoil transactions with queuedMessagesByConvoId. The per-atom inventory is in a comment below.

Type of change

  • Refactor

Testing

  1. On a custom OpenAI-compatible endpoint through the agents route: send in a new chat, stop (abort POST, partial kept), regenerate (2 / 2), reload mid-stream in an existing conversation (stream reattached and completed).
  2. Steer during a run (steer POST 202, steer part rendered) and during-run default set to queue (no steer POST, queued turn sent after the run).
  3. Compact from the token usage menu starts the summarize turn.
  4. Reloading a brand-new chat before its first content does not resume; same on the base branch.

Tested environments/configuration: local dev servers, production client build, headless Chromium.

Automated tests: added routes/__tests__/ChatSettings.spec.tsx. Added a facade regression test for a query mounted mid-render (fails without the fix) and useChatActions tests (no re-render across frames that keep the status). Updated the useSteering, useNewChat, useCompactConversation (now through the real facade) and SteerPart specs. jest --findRelatedTests on the changed hooks: 482 suites, 6327 tests passed. Client tsc --noEmit and eslint clean.

Screenshots / recordings

No user-facing change.

The chat hooks read the during-run default, steer interrupts, save drafts,
temporary chat and the artifacts reset from a ChatSettings context that Root
fills from the existing Recoil atoms. liveAppliedSteerIds moves to Jotai with
its reader and writer, and manual compaction submits through the useChat
facade.
@berry-13

berry-13 commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

Inventory of the Recoil and ~/store reads in hooks/Chat and hooks/SSE, grouped by atom.

App-global: stay in Recoil, now host-supplied through ChatSettings

  • duringRunDefaultAction (read and write), steerInterruptsByDefault: useSteering
  • visibleArtifacts (reset): useIdChangeEffect

App-global, stay on the store until every reader can move

App-global conversation state: stays in Recoil, read by the shell too

  • conversationByIndex, conversationByKeySelector, conversationEndpointByIndex, conversationIdByIndex, conversationKeysAtom, presetByIndex, filesByIndex, useCreateConversationAtom: useMultiConvo, useGetAddedConvo, useAddedResponse, useExportShare, useSteerRecovery, useNewChat, useChatHelpers, plus useBookmarkItems, useTemporaryChat and useResumeOnLoad (⏳ feat: Add Ephemeral Retention Mode for Forced Temporary Chats #13811)

Feature-owned: converted to Jotai here

  • liveAppliedSteerIds, liveAppliedSteerFamily: now liveAppliedSteerIdsAtom and liveAppliedSteerFamily in store/steer.ts. Writer: useResumableSSE. Reader and consumer: SteerPart.

Feature-owned: blocked, because useResumeOnLoad or its spec (#13811) reads or writes them

  • pendingSteersByConvoId, acceptedSteerClientIdsByConvoId, appliedSteerIdsByConvoId, activeGenerationCreatedAtByConvoId, activeGenerationProtocolVersionByConvoId, pendingQuotesByConvoId, submissionByIndex, submissionStartFamily, isSubmittingFamily, queuedMessagesByConvoId (seeded by useResumeOnLoad.spec)

Feature-owned: blocked by #16457 (ChatForm)

  • showStopButtonByIndex

Feature-owned: deferred, because they share Recoil transactions with queuedMessagesByConvoId

  • runEndByIndex, pendingRunEndByConvoId, drainAfterAbortByIndex, settledQueuedTurnReceiptsByConvoId, pendingQueuedTurnEnqueueIdsByConvoId: useQueueDrain, useSteering, useChatHelpers, useResumableSSE

Feature state also cleared or read outside the feature: deferred

  • By useClearStates: abortScrollFamily, activeRunFamily (also read by the audio hooks), showPopoverFamily, optionSettingsFamily, pendingManualSkillsByConvoId (also read by the Input hooks)
  • By messages and share scrolling: abortScroll
  • By the Files and Messages hooks: messageAttachmentsMap

Already on Jotai: store/usage, store/steer (escalating, revealed and cancel families), recoveryDispositionsFamily, compactingConversationAtom

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-28T21:50:50.820616Z 4e189a1 New commits
🔒 Security Review ✅ Completed 2026-09-28T20:57:55.739576Z 4bd832a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-09-28T20:52:57.934233Z 4bd832a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

Lighthouse CI failed. The last 80 log lines contain the measured budgets and assertion failures.

│ 23      │ 'http://localhost:3080/api/permissions/mcpServer/effective/all'                                                 │ 3043.536000000022  │ 3808.070000000065  │ 200    │
│ 24      │ 'http://localhost:3080/api/prompts/groups?limit=10'                                                             │ 3044.256000000052  │ 4311.64300000004   │ 200    │
│ 25      │ 'http://localhost:3080/api/keys?name=openAI'                                                                    │ 3343.262000000046  │ 3955.5800000000163 │ 200    │
│ 26      │ 'http://localhost:3080/api/presets'                                                                             │ 3343.5929999999935 │ 3958.0470000000205 │ 200    │
│ 27      │ 'http://localhost:3080/api/tags'                                                                                │ 3343.808000000019  │ 3969.5080000000307 │ 200    │
│ 28      │ 'http://localhost:3080/api/share/link/16390000-0000-4000-8000-000000000001'                                     │ 3345.624000000069  │ 4316.347000000009  │ 200    │
│ 29      │ 'http://localhost:3080/api/messages/16390000-0000-4000-8000-000000000001'                                       │ 3346.0830000000424 │ 4462.138000000035  │ 200    │
│ 30      │ 'http://localhost:3080/api/files/config'                                                                        │ 3346.2780000000494 │ 4211.829000000027  │ 200    │
│ 31      │ 'http://localhost:3080/api/user/settings/favorites/tools'                                                       │ 3346.994000000006  │ 4475.8450000000885 │ 200    │
│ 32      │ 'http://localhost:3080/api/endpoints/token-config'                                                              │ 3347.3880000000354 │ 4468.138000000035  │ 200    │
│ 33      │ 'http://localhost:3080/api/user/settings/skills/active'                                                         │ 3347.5990000000456 │ 4819.945000000065  │ 200    │
│ 34      │ 'http://localhost:3080/api/agents/tools/web_search/auth'                                                        │ 3348.0290000000386 │ 7332.949000000081  │ 200    │
│ 35      │ 'http://localhost:3080/api/agents/tools/calls?conversationId=16390000-0000-4000-8000-000000000001'              │ 3348.829000000027  │ 4822.292000000016  │ 200    │
│ 36      │ 'http://localhost:3080/api/agents/chat/status/16390000-0000-4000-8000-000000000001?generationProtocolVersion=2' │ 4573.958000000042  │ 4829.247000000032  │ 200    │
└─────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────┴────────────────────┴────────┘

Inspect .lighthouse HTML/JSON and e2e/lighthouse/README.md. Reuse loaded user/config data; overlap independent reads without bypassing authorization.

┌─────────┬────────────────────────────┬─────────────────────┬───────┐
│ (index) │ audit                      │ median              │ limit │
├─────────┼────────────────────────────┼─────────────────────┼───────┤
│ 0       │ 'largest-contentful-paint' │ 4530.942            │ 4500  │
│ 1       │ 'cumulative-layout-shift'  │ 0.01790118914718501 │ 0.1   │
│ 2       │ 'total-blocking-time'      │ 337.22199999999975  │ 500   │
└─────────┴────────────────────────────┴─────────────────────┴───────┘

  1) [chrome] › e2e/lighthouse/load.spec.ts:10:5 › serial database latency stays within web-vitals budgets 

    Error: Median largest-contentful-paint must stay within 4500

    expect(received).toBeLessThanOrEqual(expected)

    Expected: <= 4500
    Received:    4530.942

       at audit.ts:159

      157 |   console.table(measured);
      158 |   for (const { audit, median, limit } of measured) {
    > 159 |     expect(median, `Median ${audit} must stay within ${limit}`).toBeLessThanOrEqual(limit);
          |                                                                 ^
      160 |   }
      161 |   return results;
      162 | }
        at auditPage (/home/runner/work/LibreChat/LibreChat/e2e/lighthouse/audit.ts:159:65)
        at /home/runner/work/LibreChat/LibreChat/e2e/lighthouse/load.spec.ts:33:19

    attachment #1: screenshot (image/png) ──────────────────────────────────────────────────────────
    e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/test-failed-1.png
    ────────────────────────────────────────────────────────────────────────────────────────────────

    Error Context: e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/error-context.md

    attachment #3: trace (application/zip) ─────────────────────────────────────────────────────────
    e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/trace.zip
    Usage:

        npx playwright show-trace e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/trace.zip

    ────────────────────────────────────────────────────────────────────────────────────────────────


🤖: global teardown has been started
2026-09-28 20:57:05 �[32minfo�[39m: �[32mMongo Connection options�[39m
2026-09-28 20:57:05 �[32minfo�[39m: �[32m{�[39m
�[32m  "bufferCommands": false�[39m
�[32m}�[39m
🤖:  ✅  Connected to Database
🤖:  ✅  Found user in Database
🤖:  ✅  Deleted 1 convos & 2 messages
🤖:  ✅  Deleted user from Database
🤖: global teardown has been started
2026-09-28 20:57:06 �[32minfo�[39m: �[32mMongo Connection options�[39m
2026-09-28 20:57:06 �[32minfo�[39m: �[32m{�[39m
�[32m  "bufferCommands": false�[39m
�[32m}�[39m
🤖:  ✅  Connected to Database
🤖:  ⚠️  User not found in Database
  1 failed
    [chrome] › e2e/lighthouse/load.spec.ts:10:5 › serial database latency stays within web-vitals budgets 

Open the full run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4bd832a1e0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +26 to +27
const { sendMessage, status } = useChat();
const isSubmitting = status === 'submitted' || status === 'streaming';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid subscribing the compaction control to every message frame

When contextUsage is enabled, TokenUsageIndicator mounts this hook even if compaction itself is disabled. Calling the full useChat() facade here subscribes that control to every message-cache write and makes facade.ts iterate and map the complete conversation on each streamed frame, in addition to the existing useLatestMessage and token-usage projections; long conversations therefore gain another O(history) pass and render on every token. Keep using the lightweight contract's ask/isSubmitting, or expose an action/status-only facade that does not materialize messages.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 03e1ef3: compaction now reads status and submits through useChatActions, the facade's action/status surface. Its snapshot is the status string, so a stream frame re-renders only when the status changes and never maps the conversation. Covered by the new useChatActions tests in facade.spec.tsx (no re-render across frames that keep the status).

Comment on lines +599 to +603
const {
duringRunDefaultAction: defaultAction,
setDuringRunDefaultAction: setDefaultAction,
steerInterruptsByDefault,
} = useChatSettings();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the injected interrupt preference to the send button

When a host supplies steerInterruptsByDefault from somewhere other than the app's Recoil store, submitDuringRun now honors this injected value, but DuringRunSendButton.tsx:43 still independently reads store.steerInterruptsByDefault to choose its shortcut labels. If the two values differ, Enter performs an interrupt while the menu advertises ordinary steer, or vice versa; the button can use the already exposed steering.steerInterruptsByDefault instead of retaining a second source of truth.

AGENTS.md reference: AGENTS.md:L120-L124

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 03e1ef3: DuringRunSendButton reads steering.steerInterruptsByDefault instead of the store, so the menu labels and Enter share one source. DuringRunSendButton.test.tsx now drives the preference through the steering controls.

…ault

Compaction reads status through useChatActions, which re-renders only when
the status changes instead of mapping every stream frame. The during-run send
button reads the interrupt default from the steering controls instead of a
second store read.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 03e1ef3901

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/hooks/Chat/useNewChat.ts Outdated
const conversationId = useRecoilValue(store.conversationIdByIndex(index));
const files = useRecoilValue(store.filesByIndex(index));
const saveDrafts = useRecoilValue(store.saveDrafts);
const { saveDrafts } = useChatSettings();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use one injected value for all draft cleanup

When a non-Recoil host supplies a saveDrafts value that differs from the Recoil default, this hook uses the injected value but immediately calls useNewConvo(), which still reads store.saveDrafts at useNewConvo.ts:100 and branches on it at lines 426 and 459; composer autosave also retains its independent Recoil read in useAutoSave.ts:57. With injected false and Recoil true, startNewChat skips its saved-draft deletion path while newConversation skips its drafts-disabled deletion path, orphaning discarded uploads and continuing to persist drafts. Pass the host value through all of these consumers instead of splitting the behavior between two stores.

AGENTS.md reference: AGENTS.md:L120-L124

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4e189a1 by narrowing the host surface instead of chasing readers: saveDrafts is also read by ChatView (#15684), which this stack cannot edit, so it stays on the store and useNewChat reads it there again. A preference joins ChatSettings only once every chat reader of it can take it from there (now stated on the type); moving saveDrafts is a follow-up for after #15684.

const getEphemeralAgent = useGetEphemeralAgent();
const agentsMap = useAgentsMapContext();
const isTemporary = useRecoilValue(store.isTemporary);
const { isTemporary } = useChatSettings();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate temporary mode to file uploads

When an alternate host supplies isTemporary: true while the Recoil atom remains at its default false, turn submission is marked temporary here, but useFileHandling.ts:169,387-389 still reads the Recoil atom and omits isTemporary from the upload form. For a new conversation there is no stored conversation from which the server can infer retention, and packages/api/src/files/retention.ts:175-177 consequently returns no expiry when this flag is absent, so attachments from a temporary chat can be retained as ordinary files. Make the upload path consume the same host-supplied setting.

AGENTS.md reference: AGENTS.md:L120-L124

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4e189a1 the same way: isTemporary is read by ChatForm (#16457), useTemporaryChat and useResumeOnLoad (#13811) as well as the upload path, so it stays on the store and useChatFunctions reads it there again. Uploads and turns share one source; moving it to the host is a follow-up for after those PRs.

saveDrafts and isTemporary are also read by useNewConvo, useAutoSave and the
upload path, and by ChatView, ChatForm and the resume hook, so a host value
for them would split one preference across two sources. They stay on the
store until all of their readers can move together.
@berry-13
berry-13 merged commit 1d5062c into canary Sep 28, 2026
28 checks passed
@berry-13
berry-13 deleted the berry-13/chat-host-supplied branch September 28, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant