The bot session monitor paging controls use `common.next` and
`common.previous`, but those keys never existed in any locale. They were
only rendered because of the hardcoded `defaultValue: 'Next'`/`'Previous'`.
Removing those fallbacks (previous commit) made i18next render the raw key
name, so the Playwright smoke tests could no longer find the "Next" button.
Add `common.previous` and `common.next` to all 8 locales (reusing each
language's existing `guidedTour.previous`/`next` wording) and re-align the
locale key order with the `en-US` reference.
Verified locally:
- scripts/check-i18n.mjs passes for all locales
- every t('...') key referenced by BotSessionMonitor.tsx now exists in en-US
- tests/e2e/bot-session-tool-timeline.spec.ts: 13/13 passed (including the
two request-recovery cases that failed in CI)
- prettier / tsc / eslint clean
The bot session monitor referenced several translation keys that did not
exist in any locale file. Because i18next `fallbackLng` is `zh-Hans`, the
missing keys fell back to the hardcoded English `defaultValue`, leaking
untranslated strings (e.g. "0 sessions", "User ID or name", "Search") into
every non-English UI.
- Add the missing keys to all 8 locales with proper translations:
`common.search`, `bots.sessionMonitor.{totalSessions,userSearch,startDate,endDate}`
and `monitoring.toolCalls.{showDetails,hideDetails}`.
- Remove the hardcoded `defaultValue` fallbacks in `BotSessionMonitor.tsx`
so translations are always driven by the locale files.
- Reorder nested keys in every locale file to match the `en-US.ts` reference
order, keeping values untouched. This removes all structural drift between
locale files (previously `guidedTour.*`, `plugins.*`, etc. were ordered
differently).
Verified: `scripts/check-i18n.mjs` reports matching keys for all locales,
every key/value pair is preserved, Prettier passes, and `tsc --noEmit` is clean.
The invitation error copy also renders as a transient sonner toast, so the
plain text locator resolved to two elements and tripped Playwright strict
mode while the toast was on screen.
Tag the inline error region with data-testid="invitation-error" and assert
against it, keeping the check deterministic under CI parallelism.
The workspace assistant added 39 keys under `assistant.*` to en-US,
zh-Hans and ja-JP, but es-ES, ru-RU, th-TH, vi-VN and zh-Hant were never
updated. The i18n key consistency check fails on all five, blocking the
release PR.
Add the full assistant namespace with translated copy so every locale
matches the en-US reference. No source changes; keys and placeholders
(`{{count}}`) mirror en-US exactly.
Run prettier over the assistant files and the new dock unit test so the
web lint job passes, and remove the unused ASSISTANT_BUTTON_SIZE import
flagged by code quality review.
Opening the panel left the user at the oldest message because the scroll
effect did not depend on `open`. Add it and defer the scroll to the next
frame so the popover has laid out before the sentinel is measured.
A click or an aborted swipe never arms the long press, so no drop handler
ran and a hover-expanded button stayed expanded after the pointer left.
Track real pointer presence and re-sync the rail on release.
`shouldCollapseRail` was keyed on `!railExpanded`, mirroring the expand
helper instead of negating it. Once a hover revealed the button it could
never collapse back into the rail, so the collapse looked broken after the
first restore.
Key the predicate on `railExpanded` being true, restore the mirror
relationship with `shouldExpandRail`, and add regression coverage for the
expand -> leave -> collapse round trip. Also lengthen the rail strip.
Resolve conflicts in the workspace assistant integration:
- DynamicFormItemComponent: keep HEAD's compact model selector and
`disabled` field support while adopting master's `sortModelsByCatalog`
ordering and `MODEL_SELECT_TRIGGER_CLASS`.
- i18n (en-US, ja-JP, zh-Hans): keep both the `assistant` namespace from
HEAD and master's `sidebarGuide` / `pipelineMigration` additions.
Add 0030_merge_assistant to join the assistant conversations branch with
the released chain so the migration graph converges on a single head.
Rename it from 0030_merge_assistant_conversations to stay within the 32
character revision limit enforced by test_migrations.
Also add assistant button docking (long-press drag, edge collapse to a
short blue rail, hover restore) with pure helpers in assistant-dock.ts
and unit coverage, plus auto-collapse for finished tool result cards.
Resolve the revision-graph conflict introduced by the release line.
Conflicts
- tests/integration/persistence/test_rag_document_identity.py: master had
independently introduced the same dynamic-head helper (`current_head`) plus
a `DOCUMENT_IDENTITY_REVISION` constant, and it explicitly upgrades to that
revision. Keep master's semantics (the explicit revision matters because
upgrading to the head now also traverses the TOTP branch) while keeping the
module-level alembic imports on this branch.
New migration
- 0030_merge_totp_into_release joins the TOTP branch's own merge revision
(0026_merge_totp_and_rag_identity) with the release head
(0029_merge_rag_identity). Both reached 0025_rag_document_identity without
including each other, which left Alembic with two heads and made
`upgrade head` fail with "Multiple head revisions are present".
Verification
- Single Alembic head confirmed (0030_merge_totp_into_release).
- tests/integration/persistence/test_rag_document_identity.py: 34 passed.
- Full fast integration suite: 356 passed, 84 skipped.
resetPassword() unconditionally added `method` and an empty `totp_code` to
every request, which broke the Playwright smoke test that asserts the
recovery-key flow posts exactly {user, recovery_key, new_password}.
Send only the second-factor fields that apply to the selected method, so the
default recovery-key flow stays byte-compatible with existing callers while
the totp / recovery_code methods still carry their inputs.
Address the migration and formatting failures reported on the TOTP branch.
Migrations
- Register totp_credentials and totp_recovery_codes in
_ALEMBIC_TENANT_TABLES. On a legacy PostgreSQL install these two tables
reference users.uuid, which only exists after 0009, so create_all() must
not run ahead of Alembic the way it did for the other tenant tables.
Without this the PostgreSQL migration test failed with
"column uuid referenced in foreign key constraint does not exist".
Tests
- Resolve the Alembic head dynamically in the RAG document identity
regression instead of pinning 0025_rag_document_identity. The TOTP and
RAG branches now meet at a merge revision, so the pinned value was no
longer the head. This matches the convention already used by
test_migrations_postgres.
Formatting
- Apply ruff format to the new backend modules and prettier to the locale
files and TOTP components, so the lint jobs pass.
Add a per-Account time-based one-time password (TOTP) second sign-in
factor, plus the owner/admin tooling needed to operate it.
Backend
- TotpService: enrolment, constant-time verification with a +/- one step
drift window, single-use recovery codes and disablement.
* shared secret is only ever persisted as a Fernet token whose key is
derived (HKDF-SHA256) from the instance JWT secret and gated by a
key_version epoch;
* recovery codes are only ever persisted as salted
PBKDF2-HMAC-SHA256 digests (600k iterations) and are single-use;
* the consumed counter advances monotonically so a captured code cannot
be replayed inside the same time window.
- New persistence entities and alembic migrations for credentials and
recovery codes.
- Login second-factor challenge, bound to the Account that passed the
password step.
- Owner/admin oversight endpoints to inspect and re-bind the second
factor of any Account.
Frontend
- Account settings panel for enrolling, managing, re-binding and
disabling the second factor.
- Login second-factor step and the matching client methods.
- Strings for all eight locales.
The shared secret never crosses the API boundary: enrolment returns a
server-rendered QR code (as a data: URL) and the plaintext secret is
discarded as soon as the image is produced.
* fix(pipeline): stop emitting empty final assistant messages
An empty final streaming chunk was appended as an empty message chain when no sandbox outbox attachment was present, so platforms received an empty reply. Only append the chain when attachments were actually collected, and gate the "Call ..." tool notice behind output.misc.track-function-calls so it no longer becomes the final chunk of a stream.
* fix(wecombot): do not close stream on a blank final snapshot
A blank final snapshot closed the WeCom stream with an empty bubble and pushed the real answer into a separate reply_text message. Skip blank final snapshots and keep the session open so the following non-blank chunk can finalize it. Covered by new regression tests.
Preserve the published timezone-less schema and epoch API contract across run lifecycle, deadlines, leases, heartbeat registry, events, transcripts and retention cutoffs. Exercise the real Host journal with asyncpg under a non-superuser role on metadata and published-migration schemas; 32 PostgreSQL regressions fail before the fix and pass after it.
Resolved one conflict in src/langbot/pkg/plugin/connector.py. Master added
archive admission (`_admit_plugin_archive` + the certification digest check
before Runtime apply) at the same points this branch added install-stage
reporting, so both sides are kept:
- the archive is admitted first, then the 'inspecting plugin package' stage is
reported
- the certification digest is verified before Runtime apply, then the
'installing or starting plugin' stage is reported
Verified after merge: py_compile + ruff, tsc --noEmit, 98/98 unit tests.
Addresses review feedback on the marketplace installed-state change.
- Progress: byte-derived progress no longer has time-based drift layered on
top, and fallback drift is clamped to the current stage range, so the bar
cannot exceed the download band. 90/100 bytes at 40s elapsed used to report
61% against a declared 5-45% band; it now reports 41%. Drift is measured
from when the stage was entered rather than from task start.
- Skills: a marketplace skill is no longer reported installed from a bare
skill name. The backend names skills from their own SKILL.md and records no
publisher, so alice/review and bob/review install identically; matching the
bare name marked every publisher's skill as installed. Skill cards now
resolve to not-installed until the installed skill carries a publisher.
- Stages: "installing plugin dependencies" and "launching plugin" described
work this task context cannot observe (installation persistence ran under
the former; the runtime installs dependencies and starts the plugin inside
apply_plugin_installation under the latter). They become "persisting the
installation" and "installing or starting plugin", and the frontend maps
that combined step to the dependency stage rather than the launch stage.
- Removed the per-dependency progress fields: the backend never populated
them, so the UI could never have displayed them.
The stage mapping and progress maths move to install-progress.ts, and the
installed-state matching to a React-free marketplace-installed.ts, so both
are covered by executable tests (+9).
Verified: ruff, tsc --noEmit, prettier --check, eslint (0 errors), 98/98 unit
tests.