Skip to content

Reconcile integration/sim into main (memory, python-fs tools, search-key validation, math rendering, …)#26

Merged
fl4p merged 85 commits into
mainfrom
feat/forward-port-integration-sim
Jul 4, 2026
Merged

Reconcile integration/sim into main (memory, python-fs tools, search-key validation, math rendering, …)#26
fl4p merged 85 commits into
mainfrom
feat/forward-port-integration-sim

Conversation

@fl4p

@fl4p fl4p commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Forward-ports the whole integration/sim feature set onto current main, reconciling the two divergent evolutions of the app. Large merge — review before merging. tsc clean, lint clean (0 errors), full suite green except one pre-existing flaky test.

Why this exists

main and integration/sim had diverged: main carried the squashed #21 bundle plus later fixes (#9/#11 inline generation errors, download-speed crash fix, Add-Server, mmproj integrity #23), while integration/sim kept granular history and added a large feature set. A plain merge produced 25 conflicting files across two independent refactors of the generation pipeline, so this was resolved deliberately, per-file.

What integration/sim brings in

  • Memory subsystem: auto-capture/auto-save settings, explicit memory commands, memory-tool privacy, recall surfaced in generation meta, chat memory settings link, KB/memory tools gated when empty
  • Python filesystem tools (read_file/write_file/edit_file/list_files/grep)
  • Search-key validation: Brave optional-key upgrade + Serper key, debounced validation UI, keychain storage
  • Markdown math rendering, transcript/share utilities, RAG improvements, provider-streaming changes, conversation-model-label extraction

Conflict resolution (kept from main)

  • generation error handling #9/show request on generation error #11 inline generation-error message + request details (re-grafted onto integration's refactored useChatGenerationActions; both catch paths now call handleGenerationFailure)
  • Download-speed !!(...) crash guard; Add-Server on NoModelScreen
  • getLastVisibleMessage in conversation previews (kept alongside integration's resolveConversationModelName extraction)
  • The reviewed remote-tools error path (resetAfterGenerationError, does not false-offline the server)

Notably integration/sim had independently fixed the same #9 stuck-spinner bug (tearDownFailedRemoteGeneration); reconciled to main's reviewed version.

Bugs found & fixed during the merge

  • Duplicate finalizeStreamingMessage in the remote-tools success path (both main's and integration's finalize blocks would have run) — collapsed to one.
  • Updated the remote-tools branch test to assert the server is not marked offline on a tool-loop failure (matches the reviewed behavior).
  • Extracted ErrorMessage into its own component to keep files within the 500-line limit.

Validation

  • tsc --noEmit: clean
  • eslint src: 0 errors (pre-existing warnings only)
  • Full suite: 6563 passing; the only failure is the pre-existing downloadStore EMA toBeCloseTo flake (unmocked Date.now, fails on base too — not a source bug)

Once this merges, the small feature branches built on integration/sim (gate-empty-kb-memory-tools, memory-command-dedupe, recent-project-name, online-tools-switch, edit-message-selection, html-workspace-preview, remote-endpoint-system-prompt) rebase cleanly and can be PR'd normally.

fl4p added 30 commits July 3, 2026 11:16
Adds the AndroidJUnitRunner config and androidx.test dependencies so
emulator-based instrumented tests can exercise paths that need real
native libs (PDFium rendering + ML Kit OCR), starting with
PDFExtractorModuleInstrumentedTest.
- Cap recognizeImage decode at 2048px long side on both platforms
  (inSampleSize pre-pass + manual EXIF rotation on Android,
  CGImageSourceCreateThumbnailAtIndex on iOS) so full-resolution
  photos cannot OOM the process.
- Cap iOS PDF page render dimension to match Android.
- Android: close doc/fd in finally, catch Throwable in OCR paths so
  a decode OutOfMemoryError rejects the promise instead of killing
  the process, release the ML Kit recognizer in invalidate().
- Gate .heic attachments on Android 9+ (API 28) where platform HEIF
  decoding exists.
- iOS recognizeImage: reject unparseable file:// URIs and take
  security-scoped resource access, matching extractText.
- Skip page separators for empty pages; normalize file:// in both
  Android methods; build the unsupported-file alert from
  getSupportedExtensions() instead of a hardcoded list.
- Instrumented test: use BridgeReactContext (ReactApplicationContext
  is abstract in RN 0.8x).

Verified: unit + integration jest suites, tsc, eslint; Kotlin
compiles; 3/3 instrumented OCR tests pass on an Android 14 arm64
emulator (ML Kit + pdfium, scanned-PDF fallback and image OCR).
- Guard ML Kit recognizer close behind an in-flight counter so
  invalidate() (bridge teardown / dev reload) never closes the native
  detector while an OCR worker holds it across the JNI boundary; the
  last in-flight worker performs the deferred close. Post-shutdown
  OCR calls reject cleanly.
- Apply the full EXIF orientation matrix (rotation and mirroring,
  tags 2/4/5/7 included) to decoded bitmaps; InputImage rotation
  alone cannot express mirrored orientations.
- Log doc/fd close failures that would otherwise be silent after the
  promise has settled.
- Instrumented tests: capped decode of a 4400x3400 image, mirrored
  FLIP_HORIZONTAL EXIF recovery, reject-after-invalidate with
  idempotent teardown.

Verified: 6/6 instrumented tests pass on a physical Pixel 8
(Android 16); Kotlin compiles clean.
Documents reading AsyncStorage (chat transcripts) and rag.db (knowledge
base) from the iOS simulator container and via adb run-as on Android
debug builds, for E2E verification of features like OCR attachment text.
…ction

Issue #4: model selection modal disappears when selecting a text model.

Root cause: ChatScreen's early return swaps NoModelScreen ↔ main view
when hasActiveModel flips. Each renders its own ModelSelectorModal
instance. For remote models, setActiveRemoteTextModelId fires
synchronously inside setActiveRemoteTextModel, causing the tree swap
(and modal unmount) before onSelectionComplete could close the modal.

Fix: call onSelectionComplete() BEFORE the async store update in
handleSelectRemoteTextModel and handleSelectRemoteVisionModel. This
sets showModelSelector=false before hasActiveModel flips, so the modal
is already closing when the tree swaps — no visible flash.

Also pass onSelectionComplete from both NoModelScreen and
ChatModalSection (was missing in both).
- .githooks/pre-commit (active via core.hooksPath): blocks direct
  commits to main in every worktree, delegates to the husky pipeline
  when present. Note: .githooks did not exist before, so NO pre-commit
  hooks were running at all despite CLAUDE.md describing them.
- CLAUDE.md: multi-agent coordination rules (one worktree per agent,
  no branch switching in shared trees, stage only your own files,
  primary checkout owns Metro/simulator, serialize native builds,
  coordinate via channels) and restores the transcript-pulling docs.

Context: 5 concurrent agent sessions shared one working tree today,
causing branch switches under active sessions, 19 unpushed commits on
local main, reverted files, torn fast-refresh app hangs, and corrupted
native build caches.
A tool-less model answers 'what tools can you use' and 'run this' from training
priors — claiming code execution and pretending to produce plots (a real
transcript showed exactly this: no tool was ever called, yet the model said
'I'm now running the Python code'). When no tools reach the model this turn,
append a one-line note stating it has none and must give code for the user to
run instead. Kept to a single sentence since target models can have a 4k
context. Consolidates the per-turn tool-prompt logic into buildPromptWithToolNote
in the registry, used by both the send and regenerate paths.
ChatMessage passes onRemember since the Remember feature landed, but
ActionMenuSheetProps never declared it, breaking tsc on a clean
checkout of main. Fix extracted from the memory agent's working-tree
WIP (type + menu item only).
# Conflicts:
#	android/app/src/androidTest/java/ai/offgridmobile/pdf/PDFExtractorModuleInstrumentedTest.kt
Review of the no-tools note found it fired on activeTools.length===0, which
only counts built-in tools. MCP/extension tools are tracked out-of-band, so a
user with an MCP tool but no built-in tools would get a system prompt saying
'you have no tools' while the tool loop simultaneously injected the MCP schema
and 'make good use of the tools available' — a self-contradiction every turn.

buildPromptWithToolNote now takes hasOtherTools; both call sites pass whether
any extension tool is enabled, so the note only fires when the model truly has
zero tools. Switched to an options arg to stay within the param limit.
Second half of the type-level unblock for the half-landed Remember
feature on main (companion to the ActionMenuSheet prop fix).
Track connectedModalShown per RemoteServer. handleConnectServer shows the
success modal only the first time an endpoint connects; later reconnects go
straight to Main without the interrupt.
The header model selector rendered a hardcoded 'Models'. It now shows the
currently-selected model's name (reusing chat.activeModelName), falling back
to 'Models' when no model is resolved.
Conversations now persist the remote serverId alongside modelId. Re-opening a
chat restores the model it was created with (restoreConversationModelFn) instead
of using whatever model was globally active. New chats use the last selected
model (useNewChatModel) rather than resetting to the first downloaded model. The
chat list shows each conversation's model name.
Recent conversation cards resolve and display the model each chat used
(local downloaded model name, or discovered remote model name, falling
back to the stored model id). Adds resolveConversationModelName plus a
model row under each card, wired through HomeScreen from downloadedModels
and remoteDiscoveredModels. Adds serverId to the conversation test factory.
ChatsListScreen had its own copy of the model-name resolver. Point it at
the shared src/screens/conversationModelLabel module so the full chat list
and the Recent list on Home resolve model names identically.
Initial route only checked downloadedModels, so a user with no local model
but a configured network server landed on ModelDownload ("Set Up Your AI")
on every launch. Treat a configured remote server as a valid model state:
route to Main when downloadedModels or remote servers exist.
fl4p added 27 commits July 3, 2026 18:47
Opening Settings > Web Search fired a validation probe against the
provider for the already-stored key, because the validation effect
watched apiKey and the keychain load set it on mount.

Now validation runs only on an explicit edit (debounced via a ref-held
timer + AbortController, cancelled on keystroke / provider switch /
unmount). A mounted screen with a stored key shows the neutral 'Stored on
device' hint instead of a background-probed verified/rejected badge.

Adds an RNTL test asserting no validation on mount and debounced
validation on edit.
…e on the forced-final turn

When the tool loop hits its iteration/call cap it forces one final text turn.
DeepSeek-family remote models (e.g. opencode zen big-pickle) ignore
tool_choice:'none' AND still emit tool-call markup even with no tools declared,
so the forced turn produced only strippable markup and the user dead-ended on
"I could not find any results" despite the data already gathered.

Add a second synthesis pass: when the first forced pass yields no prose (just
stripped markup), retry with no tools and an explicit in-band "stop searching,
answer now from the results above" instruction. Request-level tool_choice does
not reach the model's intent to keep searching, but a direct instruction turn
does — the model then writes a real answer from the gathered tool results.

Verified end-to-end against big-pickle through the snoop proxy with a live
harness: the PIV crypto-tax research prompt now returns a full sourced answer
instead of the fallback, with no raw DSML markup reaching the user.

Also:
- Add __tests__/harness/liveToolLoop.test.ts: an opt-in (LIVE_HARNESS=1) harness
  that drives the real runToolLoop against a real OpenAI-compatible endpoint and
  real web_search, for fast iteration without a device build. CI skips it.
- Fix Brave web_search unit-test fetch mocks that lacked ok/status, which the
  earlier "surface Brave HTTP errors" commit turned into failures.
…odide MEMFS

Give the model the standard coding-agent file tools, operating on the same
in-memory Pyodide filesystem run_python uses. A file written by run_python is
readable by read_file and vice versa - one shared, per-session scratch workspace.

Tools (all backed by pythonRuntimeService.execute):
- read_file  - contents with line numbers, offset/limit paging
- write_file - create/overwrite, auto-creates parent dirs
- edit_file  - exact-string replace, unique unless replace_all
- list_files - directory listing (dirs marked with a trailing slash)
- grep       - regex content search with optional path/include glob

Design:
- Each op runs a small Python snippet through the existing execute() bridge and
  prints a <<FSJSON>> envelope. Args are double-JSON-encoded into the source so
  arbitrary content (quotes, newlines, unicode) round-trips with no escaping bug
  or injection. Read/grep are char/match capped so a big file can't overflow the
  WebView stdout ceiling and truncate mid-parse.
- Unlocked as a group by the Python toggle (resolveEnabledToolIds) rather than 5
  extra rows: hidden + requiresPython, auto-added to the schema/text-hint when
  run_python is enabled. Shared ensurePythonRuntimeReady self-heals a missing
  runtime like run_python.

Tests: unit (per-handler formatting + the double-encode contract), integration
(JS fake interpreter proving cross-tool shared-workspace coherence), and registry
(the unlock expansion). Full suite green.

Renames branch focus to the broader python agent-tools feature.
From three review passes:
- edit_file replace_all: read stringified booleans correctly ('false' was
  truthy via !!, silently bypassing the uniqueness guard -> mass replace).
- Envelope uses ensure_ascii=False so non-ASCII content doesn't inflate 6-12x
  and overflow the 100k stdout cap into a truncated, unparseable JSON.
- Coerce/clamp offset & limit (string '0'/negative no longer mean 'zero lines').
- read_file reports a line that exceeds the size cap instead of '(empty)'.
- Reject empty old_string (str.replace('', x) interleaves the whole file) and
  missing write_file content (was silently truncating the file).
- Confine paths to the workspace (no absolute paths / stdlib walks) and wrap
  every op so a Python error still emits an envelope (surrogate writes, etc).
- Namespace-isolate the helper (aliased _fs_ imports + _fs_req) so it can't
  rebind the user's run_python globals (e.g. a shadowed 'json').
- Fix the trailing-newline off-by-one line count.
- getToolLabel cases for the fs tools (were showing the raw snake_case id).

Adds coercion/validation tests; updates the fake-interpreter contract regexes.
Gated jest harness (ZEN_E2E=1, no API key) that drives the real big-pickle GLM
endpoint through our tool schema + extraction path. Verifies the endpoint accepts
the run_python + filesystem-tool schema and that whatever tool-call format it
returns is recovered into the right tool name + object args by the same code the
app uses. Skipped by default so normal/offline/CI runs are unaffected.

Confirmed live: big-pickle calls run_python for compute and write_file with
{path, content} for file creation - the fs-tool contract holds end-to-end.
…g onFirstToken

Adversarial review of 5debebb found two issues in the forced-final synthesis pass:

1. (High) On the LiteRT on-device path the synthesis pass was actively harmful.
   It appends a synthetic "stop and answer" user turn and drops tools to [], but
   LiteRT rebuilds model context from the message array: buildLiteRTSendText keys
   the tool-result text off a TRAILING run of `tool` messages, and buildLiteRTHistory
   excludes `tool` messages entirely. The appended user turn hides the trailing tool
   run, and tools:[] flips toolsChanged → a native session reset. Net: the model is
   told to synthesize "from the tool results above" against a freshly reset session
   with no tool results in context — worse than the pre-change fallback. The synthesis
   pass is a remote-parser remedy (DeepSeek models whose server-side parser is gated on
   `tools` and ignores tool_choice); LiteRT does native tool calling and never has that
   problem. Gate the pass behind !isLiteRTActive(), restoring pre-change on-device
   behavior. Adds a unit test asserting LiteRT does not run the extra pass.

2. (Low, latent) runForcedPass reset firstTokenFired per sub-pass, so a streaming
   second pass could re-fire onThinkingDone/onFirstToken for the same turn. No caller
   wires onFirstToken to the tool loop today so it is currently a no-op, but it would
   double-count a future TTFT metric. Reset firstTokenFired once per
   forceFinalTextResponse instead.

Remote path unchanged and re-verified live against big-pickle (full synthesized
answer, no DSML). Full jest green (6308).
…ver name

Chat model picker would not open: ChatScreen opened the picker synchronously
while the MODELS manager sheet was still dismissing, and presenting a second
modal mid-dismiss wedges iOS's modal system so the picker never appeared.
HomeScreen already deferred the picker open until the sheet's onClosed fired;
ChatScreen never got that treatment. Mirror the same closeManagerThen /
runPendingAfterClose / onClosed pattern in ChatScreen.

Also surface the remote server name next to the model for remote models:
- ModelsManagerSheet gains an optional subLabels prop rendering a second line
  under a row's value; chat + home pass the active text server name.
- The empty-chat line reads 'begin chatting with <model> on <server>'.
- activeServerName is derived in useChatScreen / useHomeScreen from the active
  model's serverId via the remote server store.

Updates the ChatScreen test's ModelsManagerSheet mock to fire onClosed so the
deferred-open flow is exercised.
…corrupt each other

The OpenAI-compatible streaming provider accumulated tool-call deltas by "start a new
call whenever an id arrives, append all argument fragments to the most recent call."
That only holds when a model streams its tool calls contiguously (all of call 0, then
call 1). Models that emit several tool calls at once — GLM 5.2, Kimi K2 — stream them
INTERLEAVED and keyed by `index` (index 0 frag, index 1 frag, index 0 frag, …). The old
logic concatenated both calls' JSON arguments onto one call, so JSON.parse of the
arguments threw "Unexpected end of JSON input" and the whole turn crashed.

Fix processToolCallChunk to accumulate by `tc.index` (the standard OpenAI streaming
contract), creating/locating the slot per index and appending arguments there, with the
previous id/last-call behavior kept as a fallback for providers that omit index. Verified
live via the harness against Fireworks: GLM 5.2 and Kimi K2.6 now complete the PIV
research prompt with full, sourced answers (previously crashed); gpt-oss-120b and
deepseek-v4-pro (which don't interleave) are unaffected.

Adds unit tests for interleaved-by-index accumulation and for an id that first appears on
a later fragment. Also hardens the live harness's XHR polyfill to finalize on a
post-data connection close (like real XHR) instead of erroring, which is how some
upstreams end a stream.
Shows EMA-smoothed download speed (e.g. '2.5 MB/s') alongside the
existing bytes-progress text in all download progress UIs:
- ModelCard (used by TextModelsTab, ImageModelsTab, ModelDownloadScreen)
- DownloadManagerScreen ActiveDownloadCard

Speed is computed in downloadStore from combined bytes-delta between
progress events using an exponential moving average (alpha=0.3) and
is cleared on terminal/processing state transitions. Also adds
downloadBytes to ModelDownloadScreen cards that were previously
missing it.

New util: src/utils/formatSpeed.ts
Tests: 6 formatSpeed + 8 downloadStore speed/clearing + 3 ModelCard
integration + 1 DownloadManagerScreen integration.
Assistant messages render Markdown but showed $...$ / \(...\) as raw text.
Add a markdown-it tokenizer for $...$ / $$...$$ and \(...\) / \[...\]
math and render the tokens as MathJax-generated SVG (react-native-mathjax-svg,
which bundles MathJax es5 - fully offline, no WebView).

- src/utils/markdownItMath.ts: vendored dollar/bracket math tokenizer (no
  katex dep); delimiter checks reject currency ($5 and $3) and escaped \$.
- src/components/MathText.tsx: MathJax wrapper with theme color/size and an
  error boundary that falls back to raw TeX on malformed input.
- src/components/MarkdownText.tsx: register the plugin + math_inline/math_block
  rules; preprocessMarkdown now skips math spans so multiplication-asterisk
  escaping can't corrupt a formula.
- jest.setup.ts: mock the heavy MathJax module.
- 38 tests across a new unit suite + the RNTL suite.
The Pyodide filesystem was RAM-only and wiped on every interpreter reload. Now:

- Both run_python and the fs tools work in a persistent /workspace (cwd), which
  the runtime service snapshots to native storage (react-native-fs) after each
  run (debounced) and on shutdown, and restores on the next run - so files
  survive a reload/app restart.
- Scope is PER PROJECT, shared across a project's conversations (matching the RAG
  KB model): the projectId flows from the tool call through execute(); a call for
  a different project than the one loaded snapshots the old and restores the new
  (restore clears first, so nothing bleeds across projects). No-project chats
  share a global workspace.
- The snapshot/restore/zip cross the WebView bridge via dedicated messages
  (outside the 100k stdout cap) with the manifest as JSON {path, base64}.
- New 'Export Python workspace' button in Settings > Tools writes a .zip and
  opens the share sheet.

Tests: per-project restore/swap/isolation/export integration (control-aware fake
page), page-source setup, and the export button. Full suite green.
Add react-native-share so the .zip export attaches a real file on Android too
(RN's built-in Share can only share text there). shareLocalFile prefers it and
falls back to the built-in Share when the native module isn't linked yet, so the
export works before a rebuild and gets more robust after.

NOTE: react-native-share is a native dependency - needs npm install + pod install
(iOS) / gradle sync (Android) to activate. Until then the fallback path runs.
…e-drain fix

Stall watchdog (generationStallWatchdog.ts): races each local/remote generation
against a 60s no-progress timer; on timeout it aborts the engine, clears the
stuck streaming bubble, and rejects so the UI shows an error instead of spinning
forever. Re-armed on token flush; paused during tool execution so a slow tool is
not misread as a stall; reset after the tool-routing pass. Reentrant no-op calls
(retry on an older message, double-tap send) never touch the live watchdog.

Mid-turn steering: runToolLoop folds messages queued for the generating
conversation into the current turn between tool-call rounds, with a fresh capped
iteration/tool budget. Display text stays separate from model context (no
document dump in the bubble). Skipped when aborted. LiteRT falls back to a
next-turn message (documented).

Queue-drain fix: processNextInQueue drains one conversation per pass (no cross-
conversation merging) and guards on isGenerating; setQueueProcessor re-drains
messages stranded while no screen owned the processor.
Add the conversation's project name after the model name in the Home
screen Recent list, matching the chats list. The meta row reads
[model] . [folder] [Project name], with the project name in the primary
accent color. Resolved via useProjectStore.getProject so no new prop
threading through HomeScreen is needed.
Add an in-app WebView preview so a self-contained HTML file the agent writes
to the Python workspace (e.g. a minigame) can actually run on the device,
instead of only being exportable as a .zip.

- pythonPage: new __fsReadFile control op reads one workspace file's full text
  over the control channel (bypasses the stdout cap, so a large page survives),
  path-confined to the workspace and size-capped at 8 MB.
- pythonRuntimeService.readWorkspaceFile(path, projectId) loads the right
  project workspace first (mirrors exportProjectZip), then reads the file.
- HtmlPreviewScreen renders the file in an isolated WebView: JS + DOM storage
  on so a game runs and keeps state, no message bridge back to the app, and
  top-frame navigation to a remote origin is blocked.
- Chat surfaces an 'Open in browser' button on write_file/edit_file results
  whose path ends in .html; onOpenHtml threads through MessageRenderer like
  onImagePress and navigates with the conversation's projectId.
- jest.setup stubs react-native-webview globally so suites importing the
  navigator load; tests asserting WebView props override it locally.
The on-device interpreter is a WebAssembly sandbox with no OS processes,
shell, or network sockets, so models that try subprocess/os.system/git clone
or requests/urllib hit 'emscripten does not support processes' (Errno 138) and
give up. Two changes so the agent uses the primitives that actually work:

- run_python description now states the sandbox has no processes/shell/sockets
  and tells the model to fetch remote files/scripts/skills with the read_url
  tool (raw file URL) then write_file them into the workspace and import/exec -
  never git clone or shell out; install PyPI packages via the packages arg.
- handleRunPython appends a corrective [hint] when an error looks like a
  blocked process or socket operation, so the tool loop self-recovers on the
  next turn instead of repeating the failing approach.
Chat text is drawn in the monospace Menlo font, which has no emoji glyphs,
so emoji in assistant markdown and user messages fell back to the OS missing-
glyph box (the tofu ? square). Split text into emoji / non-emoji runs and
render emoji runs in the system font (Apple Color Emoji / Noto Color Emoji),
keeping monospace for everything else. Non-emoji text renders identically.

- src/utils/emojiText.ts: splitEmojiRuns/hasEmoji (emoji-regex, keeps ZWJ
  families, skin tones and flags as single runs)
- src/components/EmojiText.tsx: renderEmojiRuns helper + EmojiText <Text> drop-in
- MarkdownText: emoji-aware text rule (assistant path)
- MessageContent: user bubbles use EmojiText
- emoji-regex promoted to a direct dependency (already present transitively)
The message FlatList is not inverted (newest at the bottom), but it set
maintainVisibleContentPosition.autoscrollToTopThreshold: 100. On a
non-inverted list that makes it snap to the real top whenever a
content-size change lands the anchor within the threshold of the top -
which is exactly what happens when a reply finalizes its markdown, so the
view intermittently jumped back to the beginning of the conversation.

Drop autoscrollToTopThreshold (an inverted-list setting) and keep
minIndexForVisible: 0 so content changes above the viewport still don't
shift the read position. Add a regression test asserting the config.
The default system prompt says the assistant is 'running locally on the
user's device' and 'prioritizes user privacy'. That is false when the active
model is a remote endpoint - requests go to the server the user connected to -
so the model would answer questions about itself dishonestly.

Add APP_CONFIG.defaultSystemPromptRemote (no local/privacy claim, states it
answers through a remote endpoint the user connected to) and select it in
resolveBaseSystemPrompt when the generation is remote. A user/project custom
prompt still always wins. Extracted the resolver to baseSystemPrompt.ts (keeps
useChatGenerationActions under the line limit) with unit tests for the
selection logic and the honesty of both default prompts.
…-port-integration-sim

# Conflicts:
#	__tests__/integration/tools/searchProvider.test.ts
#	__tests__/rntl/components/GenerationSettingsModal.test.tsx
#	__tests__/rntl/screens/DownloadManagerScreen.test.tsx
#	__tests__/rntl/screens/ModelSettingsScreen.test.tsx
#	__tests__/unit/services/tools/registry.test.ts
#	__tests__/unit/services/tools/search/searchProviders.test.ts
#	__tests__/utils/testHelpers.ts
#	src/components/GenerationSettingsModal/index.tsx
#	src/screens/ChatScreen/ChatModalSection.tsx
#	src/screens/ChatScreen/ChatScreenComponents.tsx
#	src/screens/ChatScreen/index.tsx
#	src/screens/ChatScreen/useChatGenerationActions.ts
#	src/screens/ChatScreen/useChatScreen.ts
#	src/screens/ChatsListScreen.tsx
#	src/screens/DownloadManagerScreen/items.tsx
#	src/screens/HomeScreen/components/RecentConversations.tsx
#	src/screens/SettingsScreen.tsx
#	src/screens/WebSearchSettingsScreen.tsx
#	src/services/generationServiceHelpers.ts
#	src/services/tools/handlers.ts
#	src/services/tools/search/braveProvider.ts
#	src/services/tools/search/index.ts
#	src/services/tools/search/serperProvider.ts
#	src/services/tools/search/types.ts
#	src/stores/appStore.ts
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