Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
15751be
fix(chat): handle the new terminal generation outcomes
garfiec Aug 19, 2026
4c83760
feat(chat): answer a batched ask_user_question
garfiec Aug 19, 2026
f5b8760
fix(chat): keep a parent activity phase label out of batch grouping
garfiec Aug 19, 2026
09affd5
fix(agents): normalize the MCP server name in an agent's tool keys
garfiec Aug 19, 2026
c13b3be
fix(settings): gate the shared-link update on SHARED_LINKS CREATE
garfiec Aug 19, 2026
d6789d8
feat: decode the new conversation, agent, config and MCP fields
garfiec Aug 19, 2026
31fe88b
fix(agents): reject a Swagger 2.0 action spec at parse time
garfiec Aug 19, 2026
7373252
feat(chat): map typed server errors to actionable messages
garfiec Aug 19, 2026
f163110
chore(sync): target upstream 0.8.7+dev.db431210
garfiec Aug 19, 2026
e7501ce
test(common): drop a comma Kotlin/Native rejects in a test name
garfiec Aug 19, 2026
3abc7de
fix(sync): make the MCP re-entry prompt and the reconcile mirror reac…
garfiec Aug 19, 2026
4277342
fix(sync): unreachable send retry, raw error markers, unregistered mi…
garfiec Aug 19, 2026
bade771
fix(sync): batched asks, MCP edits, and the agent list's edit verdict
garfiec Aug 19, 2026
062766c
chore(sync): target upstream v0.8.8-rc1 and simplify the rc-pending d…
garfiec Aug 19, 2026
8a397c9
feat(chat): scope activity phases by their exclusive end index
garfiec Aug 19, 2026
64207ec
fix(model): decode a text content part's annotated object form
garfiec Aug 19, 2026
3a32595
feat(files): mirror the shell-script MIME aliases behind an rc1 gate
garfiec Aug 19, 2026
fd5c7ef
feat(files): offer .potx uploads on rc1 servers across all pickers
garfiec Aug 19, 2026
5b9dc6e
test(common): pin the structured upload-rejection contract
garfiec Aug 19, 2026
45516d0
feat(chat): copy serializes every message part, not just its text
garfiec Aug 19, 2026
6c015d5
style(chat): right-align in-flight steer chips as user-side turns
garfiec Aug 19, 2026
ab600e8
docs(agents): record the agent-save stale-200 audit (clean)
garfiec Aug 19, 2026
cf922e2
feat(chat): fence resume decisions with the run's generation epoch
garfiec Aug 19, 2026
c7440d7
feat(chat): expire the pending-action card when its deadline passes
garfiec Aug 19, 2026
07e716f
fix(chat): keep a sibling ask visible when the pause names no tool call
garfiec Aug 19, 2026
91277cb
fix(agents): capability-gate the tool picker's plugin list and ask row
garfiec Aug 19, 2026
45b6d5e
feat(chat): answer a multi-question batch from the composer, one send…
garfiec Aug 19, 2026
ae9a8a6
feat(chat): quote selected text into the next message (Add to chat)
garfiec Aug 19, 2026
bbbdb60
docs(sync): record v0.8.8-rc1 shapes, divergences, and the agents 3.4…
garfiec Aug 19, 2026
2380d6a
fix(sync): complete the commit-map tag window and adapt tests to the …
garfiec Aug 19, 2026
0d7e352
build(detekt): exclude build-time codegen from detekt analysis
garfiec Aug 19, 2026
e8ff4b7
fix(sync): restore full commit-map dev window and carry quotes on reg…
garfiec Aug 19, 2026
35b6741
fix(chat): keep the HITL resume fence past the SSE created frame
garfiec Aug 19, 2026
4f8c13b
fix(files): react to late backend-version detection in the picker
garfiec Aug 19, 2026
db463e8
fix(chat): route a composer send to the first unanswered batch question
garfiec Aug 19, 2026
477b840
fix(chat): install the Add to chat item on the platform selection too…
garfiec Aug 19, 2026
1456052
fix(chat): classify in-band error parts through the typed-error path
garfiec Aug 19, 2026
dd3241b
fix(files): normalize the Android shell-script MIME type before upload
garfiec Aug 19, 2026
9097a34
fix(review): quote loss on un-send, batch answer duplication, and sha…
garfiec Aug 19, 2026
5dd9a00
fix(gates): tell an unplaceable server from a proven-old one
garfiec Aug 19, 2026
13f9f6b
test(gates): verify the three-state gate on a device against a real s…
garfiec Aug 19, 2026
77e4232
fix(chat): stop the auto-scroll follower while a run awaits human review
garfiec Aug 19, 2026
7b71dd9
docs(chat): trim branch-local narration and duplicated comments
garfiec Aug 19, 2026
0253976
fix(chat): leave a focused field in the list to foundation's bring-in…
garfiec Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .claude/skills/sync-upstream/reference/ui-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,11 @@ Maps web client component directories to mobile Compose feature modules.
| react-router-dom | Compose Navigation |
| React.memo / useMemo | remember / derivedStateOf |
| useCallback | rememberUpdatedState / LaunchedEffect |

## Deliberate UI Divergences (do NOT "fix" toward upstream)

| Upstream behavior | Mobile divergence | Rationale |
|-------------------|-------------------|-----------|
| App-bar model-selector button (`ModelSelectorButton` in the chat header) | Removed — model selection lives in the composer's selector sheet | Mobile chrome budget; long-standing, re-adding it in a sync is a regression |
| Streaming word fade-in (ae24461146f4/8f1f43f33e73, default ON; cursor CSS deleted) | Streaming cursor KEPT (`StreamingCursor.kt` inline cursor); fade-in NOT adopted | The markdown renderer has no efficient per-word fade path — it would re-render the whole tail per word, the per-flush cost class the cursor work eliminated; cursor is device-approved |
| Message-row layout reorg (7694428c / d920328b: unified MessageRow, right-aligned user bubbles) | Not ported wholesale; only the in-flight steer chips' right alignment was taken | Standing rule: web layout reorgs are out of sync scope |
216 changes: 208 additions & 8 deletions DISCOVERY.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Switchboard: LibreChat Mobile Client

[![LibreChat](https://img.shields.io/badge/LibreChat-v0.8.4_–_v0.8.7_%2Bdev-blue)](https://github.com/danny-avila/LibreChat/commit/91adcf3f2c3a16266d29e4cee7916518e807f9b3)
[![LibreChat](https://img.shields.io/badge/LibreChat-v0.8.4_–_v0.8.8--rc1-blue)](https://github.com/danny-avila/LibreChat/releases/tag/v0.8.8-rc1)

A third-party native mobile client for [LibreChat](https://www.librechat.ai/) (Android & iOS). Not affiliated with the official LibreChat project — this is an independent app that connects to any self-hosted LibreChat server, no backend modifications required.

> **Backend compatibility:** Tested against LibreChat **v0.8.4 – v0.8.7 +dev** (partial sync to an untagged `dev` commit past v0.8.7). Older releases may work but are not guaranteed; newer releases are supported on a best-effort basis until the next sync.
> **Backend compatibility:** Tested against LibreChat **v0.8.4 – v0.8.8-rc1**. Older releases may work but are not guaranteed; newer releases are supported on a best-effort basis until the next sync.

## Why use this instead of the web app?

Expand Down
6 changes: 3 additions & 3 deletions UPSTREAM_VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
# for a PARTIAL sync (untagged target) it is PAST the tag, and
# version.properties' backendTargetVersion carries a +dev.<sha> suffix pinning it.
# Diffs for the next sync are computed from commit=, not tag=.
tag=v0.8.7
commit=91adcf3f2c3a16266d29e4cee7916518e807f9b3
date=2026-07-29
tag=v0.8.8-rc1
commit=eaef87fa2684025627e25d649a56f4f2a63417a7
date=2026-08-14
132 changes: 123 additions & 9 deletions VERSION_GATES.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,15 @@ class DetektConventionPlugin : Plugin<Project> {
// Committed generated sources (e.g. BackendCommitMap) are machine-written; skip lint
// (large packed literals trip formatting/line-length rules that don't apply to codegen).
// Scoped to the specific codegen package so hand-written code elsewhere stays linted.
// Build-time codegen (KSP output such as Room *_Impl classes, picked up by the
// per-target detekt tasks) is machine-written too and excluded wholesale — by file
// path, because string patterns match relative to each source root and the codegen
// roots live under build/generated themselves.
tasks.withType<Detekt>().configureEach {
exclude("**/com/garfiec/librechat/core/common/generated/**")
exclude { element ->
element.file.invariantSeparatorsPath.contains("/build/generated/")
}
}

dependencies {
Expand Down
17 changes: 17 additions & 0 deletions core/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ val generateBackendCommitMap = tasks.register("generateBackendCommitMap") {
return out.trim()
}

// A shallow submodule clone silently truncates rev-list, shrinking the dev window
// (and often the tag set) without any error — fail loudly instead of emitting a map
// that resolves hundreds of real upstream builds to null.
if (git("rev-parse", "--is-shallow-repository") == "true") {
error(
"upstream/ submodule is a shallow clone; the commit map would be silently " +
"truncated. Run: git -C upstream fetch --unshallow",
)
}

val versionTagRegex = Regex("""^v?\d+\.\d+""")
val versionLineRegex = Regex(""""version"\s*:\s*"([^"]+)"""")
fun normalize(raw: String): String = raw.trim().trimStart('v', 'V')
Expand Down Expand Up @@ -118,6 +128,13 @@ val generateBackendCommitMap = tasks.register("generateBackendCommitMap") {
?.takeIf { it.isNotEmpty() } ?: git("rev-parse", "HEAD")
val revList = git("rev-list", "-n", devCommitCount.toString(), pinnedHead)
.lineSequence().filter { it.isNotBlank() }.toList() // newest -> oldest
if (revList.size < devCommitCount) {
error(
"rev-list returned only ${revList.size} of $devCommitCount dev commits from " +
"$pinnedHead — the upstream/ history is truncated (partial fetch?). " +
"Run: git -C upstream fetch --unshallow",
)
}
var boundaryShows = 0
if (revList.isNotEmpty()) {
val oldest = revList.last()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ object BackendVersion {
/**
* Feature-gate check that also understands servers built from UNTAGGED upstream commits.
*
* Boolean shorthand for [featureSupport] `== PRESENT`. It answers "is the feature known to be
* there", which folds "known absent" and "cannot place this server" into one `false` — use
* [featureSupport] instead wherever those two deserve different handling (see its docs).
*
* Upstream only bumps package.json at rc prep, so a server running a dev commit with
* next-release features still REPORTS the previous release version (e.g. a 0.8.8-cycle
* dev build reports "0.8.7") — [isCompatibleOrNewer] alone would hide the feature. This
Expand Down Expand Up @@ -209,15 +213,54 @@ object BackendVersion {
detected: DetectedBackend?,
minVersion: String,
landedDate: String? = null,
): Boolean {
if (detected == null) return false
if (isCompatibleOrNewer(detected.version, minVersion)) return true
): Boolean = featureSupport(detected, minVersion, landedDate).isPresent

/**
* The three-state form of [supportsFeature]: does this server carry the feature, is it
* known NOT to, or can it not be placed at all?
*
* [supportsFeature] answers PRESENT-or-not, which is the right question only for a gate that
* treats "don't know" the same as "no" — see [FeatureSupport] for why most gates should not.
* Ask this instead whenever "unplaceable" deserves different handling than "too old", in
* practice whenever the feature can be discovered by probing for it.
*
* How each state is reached:
* - **PRESENT** — the reported version already meets [minVersion], or [landedDate] is given
* and a DEV build's commit date is at or past it.
* - **ABSENT** — the version falls short AND the build commit resolved to a release
* ([BackendBuildClass.OFFICIAL]) or prerelease ([BackendBuildClass.RC]) TAG, where
* package.json is exact in both directions; or [landedDate] is given and a DEV build's
* commit date predates it.
* - **UNKNOWN** — everything else: a null [detected], and any DEV build that no [landedDate]
* settles. [BackendBuildClass.UNKNOWN] is UNKNOWN too — it covers a config-supplied
* `version` field, which upstream would source from the same package.json that dev builds
* under-report.
*
* Deliberately NOT inferred: that a DEV build reporting a version far below [minVersion]
* (say 0.8.5 against a 0.8.8-rc1 gate) must be ABSENT because a dev build can only
* under-report by one release line. It is true of upstream's current release habit and
* nothing enforces it — one 0.8.8 → 0.9.0 bump turns the inference into a silent
* feature-suppression, which is the exact failure this three-state split exists to end. The
* cost of not inferring it is one probe against an ancient server, cached by the callsite.
*/
fun featureSupport(
detected: DetectedBackend?,
minVersion: String,
landedDate: String? = null,
): FeatureSupport {
if (detected == null) return FeatureSupport.UNKNOWN
if (isCompatibleOrNewer(detected.version, minVersion)) return FeatureSupport.PRESENT
if (landedDate != null &&
detected.classification == BackendBuildClass.DEV &&
detected.commitDate != null
) {
return detected.commitDate >= landedDate
return if (detected.commitDate >= landedDate) FeatureSupport.PRESENT else FeatureSupport.ABSENT
}
return when (detected.classification) {
// A tagged build reports the version of its tag, so falling short of the threshold
// is proof, not evidence.
BackendBuildClass.OFFICIAL, BackendBuildClass.RC -> FeatureSupport.ABSENT
BackendBuildClass.DEV, BackendBuildClass.UNKNOWN -> FeatureSupport.UNKNOWN
}
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.garfiec.librechat.core.common

/**
* What the detected backend identity actually tells us about one gated feature.
*
* A `Boolean` cannot carry this: it folds a server we KNOW predates the feature together with one
* we could not place at all, and those want opposite handling — the first must never be called,
* the second is usually a server built PAST this app's commit-map pin, i.e. the population most
* likely to HAVE the feature.
*
* See `BackendVersion.featureSupport` for how each state is derived, and `VERSION_GATES.md`
* for the per-gate rules on which state gets which behaviour.
*/
enum class FeatureSupport {
/** The server is known to carry the feature. Call the route. */
PRESENT,

/**
* The server is known NOT to carry the feature: its build commit resolved to a release or
* prerelease TAG below the threshold (where the reported version is exact), or a dev build
* whose commit date settles a date-gated feature. Suppress the call — it cannot succeed.
*/
ABSENT,

/**
* The server could not be placed on either side. Either its build commit resolved to
* nothing (no `buildInfo`, a commit past the app's pin, or one outside the map's window),
* or it is a dev build, whose reported version is a floor and not a ceiling — upstream
* bumps `package.json` only at rc prep, so a whole release cycle's worth of servers report
* the PREVIOUS version while carrying the next one's features.
*
* The callsite decides. Where the feature is discoverable by asking (a cheap probe whose
* 404 is a definitive "no"), probe once and cache the verdict. Where it is not, fail closed
* and say so in the gate's `VERSION_GATES.md` row.
*/
UNKNOWN,
;

val isPresent: Boolean get() = this == PRESENT

/** The route is known to be missing — the only state in which suppressing a call is free. */
val isRuledOut: Boolean get() = this == ABSENT
}
Loading