47 Commits
Author SHA1 Message Date
bot-hermes bf81611bed merge: resolve v1.23.0 upstream conflicts, preserve custom git provider config
Resolved conflicts in 8 files by taking upstream refactored code:
- desktop.ts: re-export DesktopSettings from registry
- openchamberConfig.ts: simplified project setup client
- persistence.ts: registry-derived settings, add git provider hydration
- search.ts: upstream search entries + git provider entries
- useConfigStore.ts: loadDesktopSettings() path
- settings-helpers.js: add gitProviderId/gitModelId/gitProviders sanitization
- DOCUMENTATION.md: upstream walkthrough docs
- vite.config.ts: upstream SW glob patterns

Custom fork additions preserved:
- gitProviderId, gitModelId, gitProviders fields in settings registry
- Git provider domain store hydration in persistence.ts
- Git provider search entries in search.ts
- Git provider sanitization in settings-helpers.js
2026-09-10 10:11:50 +00:00
Bohdan Triapitsyn 85c4320825 Settings storage with scopes, and project setup that can live in the repository (#3413)
* refactor(settings): settings registry and intent-gated writes

Problem: every setting lived in a flat document with ten hand-maintained
key lists that had drifted (three keys the server silently dropped, five
it kept that nothing read), and three code paths wrote to the server
without a person changing anything: the theme persist effect on mount,
bootstrap seeding of server-missing keys, and the auto-save echoing
values just adopted from the server.

Approach: one registry (packages/ui/src/lib/settings/registry.ts) names
every key with its scope (instance / profile / device), a boundary parser
and its store binding; DesktopSettings, the sanitizer, the mirror, the
apply step and the auto-save derive from it. A generated JSON snapshot
carries the key list to the server and the VS Code bridge. Writes carry
intent: the theme context writes only from its user-facing setters, a
missing server key leaves the local store alone instead of resetting it,
updateDesktopSettings drops values the server already holds, and the
auto-savers treat values applied from the server as a new baseline.

Testing: bun test packages/ui (registry + persistence suites cover zero
writes on load, dedup, toggle-back cancellation, failed-save retry, and
snapshot freshness); tsc for every workspace.

* refactor(ui): read and write settings through the shared path only

Problem: fourteen pages and stores fetched /api/config/settings on their
own and re-parsed the raw document by hand, so the registry could not
guard them and two of them treated a failed load as an empty list.

Approach: loadDesktopSettings() and updateDesktopSettings() (which now
resolves { ok }) replace every direct call; SkillsCatalogPage and
AddCatalogDialog refuse to write the catalog list until it is known.

Testing: bun test packages/ui (403 files), eslint on the changed files.

* refactor(server): validate settings writes against the registry snapshot

Problem: the server whitelist was the only guard on PUT /api/config/settings
and had drifted from the client; dead keys were still persisted.

Approach: settings-helpers.js drops any key the generated registry
snapshot does not list as persistable and strips secret keys from
responses; the dead keys (markdownDisplayMode, toolCallExpansion,
typographySizes, expandedEditorToolbar, gitProviderId/gitModelId) are
gone; the profile keys that were client-only now round-trip. A drift
test requires a valid sample for every persistable registry key.

Testing: vitest run in packages/web (182 files), including the packed
tarball import.

* refactor(vscode): gate bridge settings writes by the registry

Problem: the extension host wrote any key the webview sent straight into
settings.json, and commit-message generation read the dead
gitProviderId/gitModelId pair instead of the small-model setting.

Approach: filterPersistableSettingsChanges applies the registry snapshot
before the file write; chooseBridgeGitGenerationModel honours
smallModelUseDefault/smallModelOverride ahead of the zen fallback.

Testing: bun test packages/vscode (37 files), tsc, build:extension.

* feat(settings): split the user's profile into preferences.json

Problem: one flat settings.json held instance facts, the user's
preferences and device state together, so device state travelled between
installs and the profile had no document of its own to sync from.

Approach: the server keeps one merged document for clients but routes
each key by registry scope on disk (settings-files.js): profile keys go to
preferences.json as { value, updatedAt } entries stamped when the value
changes, everything else stays in settings.json, device keys are dropped
from writes. A missing preferences.json is seeded once from settings.json,
which is left intact; an unreadable one is a failure that pauses profile
writes and never gets overwritten. Server modules that read a profile key
off the disk use the merged sync read. Electron main reads the theme mode
from both files and now owns the splash colours, handed over the
window-theme IPC instead of the settings document. Clients stop sending
device keys, seed them once from a pre-split document, and persist
inputBarOffset locally. The PWA manifest keys are instance facts.

Testing: vitest in packages/web (seed, split write, timestamp retention,
unreadable file), bun test in packages/ui and packages/electron, tsc for
every workspace.

* feat(vscode): write the profile to preferences.json from the extension host

Problem: the extension host writes the shared settings files directly and
had to follow the server's split, and its file writes reported success on
failure.

Approach: settings-files.ts mirrors the server's format and split rules
(seed once, unreadable preferences.json is a failure); persistSettings
routes profile keys to preferences.json and the rest to settings.json,
and the atomic writers now throw so a failed save reaches the webview.
Clearing a key now actually removes it from the owning file.

Testing: bun test packages/vscode (38 files), tsc, build:extension.

* feat(settings): store the per-surface profile fields by surface kind

Problem: theme, chat-layout switches and typography sizes are one value
for every client of an instance, so the phone and the desktop cannot
disagree without a hard-coded runtime branch.

Approach: every settings request carries the client's surface kind in the
x-openchamber-surface header (web, desktop, vscode, mobile — the phone app
and the hosted mobile shell are one kind). For the registry's perSurface
keys the store writes a changed value under fields[key].surfaces[kind] in
preferences.json and never touches the base from a surface; reads resolve
the kind's own value, then the base, then nothing. Writes without a
surface (migrations, the seed) set the base. The VS Code host is always
vscode; Electron main resolves desktop for the native window theme. The
Settings UI is unchanged.

Testing: vitest in packages/web (surface write/read, no base copy, unknown
surface falls back to base), bun test in packages/vscode and packages/ui,
tsc for every workspace, build:extension.

* fix(settings): keep a legacy copy of the profile in settings.json

The first write after the split rewrote settings.json with the instance
part only, and that write happens on startup (relay reconcile). A build
from before the split reads only settings.json, so rolling back would
have lost every preference: theme, default model, all of it.

Every write now stores the profile's base values in settings.json next
to the instance part (`legacySettingsDocumentOf`), on the server and in
the VS Code extension host alike. Current builds ignore the copy because
preferences.json wins in the merged read. When preferences.json is
unreadable the copy already on disk is kept rather than dropped.

Testing: settings-runtime tests updated for the copy; full web suite
(182 files), VS Code tests and extension build, tsc clean. Verified live
on a scratch OPENCHAMBER_DATA_DIR: all 136 keys survive startup, theme
changes land per surface, plain keys land in the base.

* feat(settings): make the UI password and tunnel preset tokens write-only

GET /api/config/settings returned desktopUiPassword and the managed
remote tunnel preset tokens to every authenticated client, including
paired phones and the VS Code webview that never need them.

Both keys are now `secret` in the registry: accepted on write, withheld
from reads. The server answers with a hasDesktopUiPassword flag; the
desktop network page shows "Password set" and sends a value only when
the user types a new one or presses "Remove password" (an empty string
clears it and turns LAN access off). The tunnel page already learned
token presence from the status endpoint. The VS Code bridge strips
secret keys from what it hands the webview while still merging them
from disk on write.

Testing: registry, i18n parity, server settings, VS Code gate tests and
tsc; workspace type-check. Verified against a scratch server: GET
carries the flag and no password, PUT with '' clears, PUT with a value
sets. The desktop-only page itself awaits the owner's run.

* fix(settings): send the surface kind as a query parameter, not a header

The packaged desktop shell (openchamber-ui://app) and the phone app are
cross-origin to the OpenChamber server, so the x-openchamber-surface
header turned every settings request into a CORS preflight the server
did not allow. Settings looked reset and every save reported "Save
failed" without reaching persistSettings. An older remote instance would
refuse the header the same way even with the allow-list fixed.

The client now sends ?surface=<kind>, which keeps the request
CORS-simple on every server version; the server reads the query
parameter and still honours the header. The header is also in the CORS
allow-list for completeness.

Testing: workspace type-check, persistence and registry tests, server
opencode tests. On a scratch server: PUT with ?surface=vscode lands
under surfaces.vscode, GET without or with an unknown surface serves the
base, the header fallback resolves. Confirmed in the owner's rebuilt
desktop and on the phone.

* refactor(settings): drop the show-password toggle from the desktop network page

With the password write-only, the field only ever holds a value the user
is typing right now; the reveal toggle and its strings are gone from
every locale.

* refactor(projects): serve project setup through the server, drop the legacy migration

The shared UI read and wrote ~/.config/openchamber/projects/<id>.json
itself: it resolved the home directory, composed the path, and used the
Files API, which only desktop and VS Code have natively and which cannot
see a remote instance's file at all. It also still carried the months-old
migration from <repo>/.openchamber/openchamber.json, which deleted files in
the folder the upcoming shared project config will use.

The client-owned keys (worktree setup commands, project actions, draft
starters) now live behind GET/PUT /api/projects/:projectId/config.
project-setup.js sanitizes and builds the view; the project-config runtime
merges a patch under the same cross-process lock the scheduled-task writers
hold, so unknown and server-owned keys survive. A wrongly shaped key is a
400, not a silent drop. openchamberConfig.ts keeps its exported functions
and is now an HTTP client. The VS Code webview handles the route locally
and bridges to the extension host, which owns the file with a TS mirror of
the sanitizers.

Testing: server tests for sanitizers, round trip, lock, and invalid patch;
client tests against a mocked route; VS Code sanitizer and bridge tests;
workspace type-check, both VS Code builds, UI isolated suite (409 files),
server projects and project-context suites. Live GET/PUT against a
running server with the owner's real project config.

* feat(projects): read the team's shared config and merge it with the personal one

A project can now carry <repo>/.openchamber/project.json (version 1:
setupWorktree, setupWorktreeWait, projectActions, draftStarters,
plansDir). The server finds the checkout from the path-derived project
id, parses the file, and answers GET /api/projects/:id/config with one
merged view: what runs at the top level, plus shared and personal blocks
so a page can edit the personal file without copying a teammate's entry
into it.

Merge rules: shared setup commands run first (a personal
setupWorktreeMode of "replace" uses the personal list only); the
personal wait flag wins when set; actions union by id with a personal
action replacing the shared one and personal hiddenSharedActionIds
dropping shared ones; starters union by type:name; the primary action is
personal only. A shared file that exists but cannot be parsed, or that
names a plansDir outside the repo, is reported as invalid with a reason
and never treated as "no shared setup". Nothing writes the repo file yet.

Client: getProjectSetup exposes the view; the existing helpers return
effective values, while the Projects page sections and the draft
starters hook edit the personal block only. Shared entries show a quiet
"shared" mark in the actions dropdown and read-only lists above the
editable ones on the Projects page; shared starter chips have no remove
handle. The VS Code extension host mirrors the parser and merge.

Testing: server tests for the parser, plansDir guard, merge table, id
round trip, and a runtime test against a temp checkout; client tests
against a mocked route; VS Code sanitizer, merge, and bridge tests; the
section test covers the shared row; locale parity; workspace type-check;
UI isolated suite (409 files). Live: GET against a temp repo with a
shared file and with a broken one.

* feat(projects): ask before the team's shared commands run, once per set of commands

Shared setup commands and shared actions come from a file a git pull can
change, and they run on the machine of whoever pulls. The first time one
would run, a dialog now shows exactly what would run and asks: "Trust and
run" or "Not this time". A "trust" answer is recorded in the personal
config against a SHA-256 of the executable parts (setup commands and each
action's id, command, and runIn; renames and icons do not count), so a
pull that changes a command brings the prompt back. Nothing asks when the
shared file has nothing that executes.

Worktree creation (session creator, new-worktree dialog, session store,
multi-run launcher, agent-manager empty state) resolves its commands
through the prompt; "not this time" runs only the user's own commands.
The actions dropdown asks before a shared action runs. The Projects page
shows "Trusted on this instance" with a "Reset trust" button next to the
shared actions. The dialog is mounted beside the app-link confirmation on
every shell. The VS Code extension host mirrors the hash and the record.

Testing: server tests for hash stability, ordering, and the trusted flag,
plus a runtime test that changes the shared file and sees trust drop;
client tests for the confirmation store (ask, trust, skip, replace mode,
newer request, failed record, reset); VS Code mirror tests; the actions
button, new-worktree dialog, and issue-2039 tests updated for the trust
path; locale parity; workspace type-check; UI isolated suite (410 files).

* feat(projects): share and unshare setup with the team from the Projects page

The repo file <repo>/.openchamber/project.json is now written by the app,
and only when the user shares something: nothing appears in a repository
until then. PUT /api/projects/:id/config/shared replaces the keys it
names over the current file, writes it pretty-printed with version first
and only the keys that carry something, removes the file (and an empty
.openchamber folder) when nothing is left, refuses a missing checkout or
a plansDir outside the repo, and records trust for the writer, who has
seen what they shared.

On the Projects page, actions and setup commands get "Share with team"
and "Make personal"; shared actions can be hidden for this user; a
checkbox switches to "Use only my setup commands". Project starter chips
get share and make-personal hover buttons. A new "Shared config" block
shows the file's path and status, the shared plans folder, and the trust
status with "Reset trust". A share is a repo write followed by a personal
write; a failure after the first leaves the item visible once, as
personal. The VS Code extension host mirrors the writer.

Testing: server tests for the patch, serialization, emptiness, the write
and removal round trip, the writer's trust record, and the refusals;
client test for the shared route; VS Code bridge test for write and
removal; locale parity; workspace type-check; UI isolated suite (410
files). Live on a scratch server: share, invalid plansDir (400), unshare
to removal of file and folder.

* feat(projects): list, edit, and move plans in the team's shared plans folder

When the shared config names a plansDir, every markdown file in that
folder is a plan on the Plans tab: listed after the user's own plans,
marked shared, addressed as shared:<file>, read and edited in place
(the raw document is written verbatim, so a plan another tool wrote
keeps its shape), and deletable. Share moves one of the user's plans
into the folder; make personal moves it back under a new id; a name
collision gets a numeric suffix. Sharing is refused, with a hint in the
panel, until a shared plans folder is set in Project settings. This
answers the request to read plans from an existing folder such as
docs/plans.

Server: the project-context runtime takes resolveSharedPlansDir from the
project-config runtime; readContext reports sharedPlansDir; POST
.../plans/:id/share and /unshare. Client: movePlan in the context store,
a shared badge and a share / make-personal button per plan row. Session
attachments reference plan ids, so an attached plan that moves has to be
attached again.

Testing: runtime tests for listing, foreign markdown titles, id
traversal, in-place update and delete, share and unshare with a
collision, and the refusal without a folder; HTTP route tests; store and
locale parity tests; workspace type-check; full web suite (183 files);
UI isolated suite (410 files). Live on a scratch server against a temp
repo: list, share, read, unshare.

* fix(server): make OPENCHAMBER_DATA_DIR move every folder, not just the flat files

The variable is documented as the OpenChamber data directory, but only
settings, preferences, auth, and push files followed it; projects,
themes, speech models, and the chats default stayed under
~/.config/openchamber. A second instance started with a custom
directory therefore read and wrote the default instance's project
configs.

Every folder now hangs off the one root. An instance that already used
a custom directory gets projects, themes, and speech-models copied in
once at startup; copied, not moved, so a second instance beside the
default one cannot strip it, and nothing is merged into a folder that
already exists. Existing managed chats are not copied, as with
OPENCHAMBER_CHATS_DIR.

Testing: migration tests for copy-once, no-merge, and same-root no-op;
full web suite; a scratch server with an empty data dir copied the real
project configs and kept its writes in the copy.

* fix(projects): keep a plan's id when it moves into or out of the repository folder

A plan moved into the repository plans folder used to be listed under a
new shared:<file> id, so a session that had attached it lost the
attachment. The manifest entry now stays with a `shared` flag that says
which folder holds the file; the id survives both directions. Only a
plan that never had an entry (one written by another tool) gets an id
when it is brought in. A personal file and a repository file may share
a name because they live in different folders.

Testing: runtime tests for share and unshare with a stable id, reading
and editing the moved plan, the suffix on a name collision, and the
adoption of a foreign file.

* feat(projects): default repository plans folder, "move to repository" wording, tooltips

Plans now have a repository folder without any setup: .openchamber/plans
by default. A custom plansDir replaces the default outright (only that
folder is read and written; moving files between the two is the user's
job), and the field's placeholder and hint say so. The move buttons on
plans are therefore always available.

The word "share" is gone from the UI: it read like publishing, while
the action stores an item in the repository so everyone who pulls it
gets it. Labels are "Move to repository" / "Move to my settings", the
badge is "In repo", the block is "Repository config", and every button
on the Projects page carries a tooltip that says what happens (the
"Move to repository" button explains that edits save first while the
form is dirty). The trust status with "reset trust" moved from the
repository block into the Worktree section next to the commands it
guards; the plan row's badge sits beside the title.

Testing: locale parity, section test, workspace type-check, UI isolated
suite (410 files), full web suite.

* fix(projects): leave the icon key out of the repository file when an action has none

Actions without an icon were written as "icon": null into
.openchamber/project.json. The key is now omitted; readers already fall
back to the play icon. Server and VS Code serializers, tests updated.

* docs: describe the repository config file and how items move into it

A new page in every locale: what stays personal and what can move into
the repository, the .openchamber/project.json format with an example
and every key explained (setup commands, actions with the supported icon
names, starters, plansDir), the merge rules, the trust prompt, and plans
in the repository. Linked from the sidebar and from Project Actions.
Translations written by hand.
2026-09-07 17:50:55 +03:00
bot-hermes 4db78662d8 merge: resolve v1.22.2 conflicts with custom
Per Q4 resolution (documented on kanban t_83741c53):
- .github/workflows/*: KEEP custom deletion (fork uses Gitea Actions under .gitea/workflows)
- ChatInput.tsx: KEEP custom forge picker states + provider-aware linkedPr
- WorkStatusContextSection.tsx: combine imports (WorkStatusPill + useConfigStore)
- WorkStatusPrimaryGroup.tsx: upstream nested-git/bootstrap-gate base + custom GitLab/Gitea forge rows
- NewWorktreeDialog.tsx: combine dialogs; keep custom MR/PR branch resolution
- SettingsView.tsx: custom deps minus undefined openThirdPartyProviderSetup
- search.ts + search.test.ts: KEEP upstream (enter-to-send, large-text-paste, first-party integrations)
- tr.ts: drop 4 auto-merge duplicate gitView.empty.* keys
2026-09-06 10:55:54 +00:00
Bohdan Triapitsyn f46fb718c5 fix(chat): recall the current session's prompts by default; tidy the six merged PRs
Input history (#3035) shipped with "All projects" as the default scope and
only recorded prompts sent after the upgrade, so ArrowUp showed other
sessions' prompts and, once switched to "Current session", nothing at all.
Default to the current session and merge the visible transcript's prompts
with the persisted bucket. Existing sessions recall as they did before
#3035, while new prompts keep their attachments and stay recallable after
a revert hides them from the transcript.

Cleanup across #1855, #2297, #3072, #3178, #3035 and #3135: drop the
duplicate poll guards in the file content poller, the zod schema the
VS Code package cannot depend on, a copied file-URL helper and stray
whitespace; move the Enter-to-send strings into the settings namespace;
document OPENCHAMBER_CHATS_DIR, resolve the chats root once on the server
and warm it alongside the other bootstrap calls.
2026-09-05 20:16:14 +03:00
Bohdan Triapitsyn d6f0f2f23c feat(settings): retire the third-party plugin integrations
Settings → Integrations offered install cards for the Claude Code and
Cursor provider plugins. They are gone: the section, its plugin catalog,
its own i18n module and tests, the settings search entries, the page
keywords, and the two sprite icons only it used. The page now holds the
built-in GitHub and Linear cards, so it is hidden in VS Code where neither
applies; its title and description live in the settings dictionaries.

Docs follow: the Integrations page in every locale now documents GitHub
(pointing at its own page) and Linear in full, the GitHub page names
Settings → Integrations as the place to connect and covers linking an
issue or PR to a message, and the Providers pages no longer promise Claude
or Cursor subscriptions.

Claude-Session: https://claude.ai/code/session_01HB9wdLQoZX2vfyDjwv6Rso
2026-09-04 23:34:15 +03:00
bot-hermes e70af9ae1f merge: resolve v1.21.1 conflicts with custom
Resolve all 10 upstream v1.21.1 merge conflicts into custom, keeping
custom's GitLab/Gitea forge customizations layered on upstream while
lifting upstream improvements. Restored tr.ts i18n key parity with the
custom en.ts dictionary (471 custom forge keys added, en fallback) so the
upstream-introduced locale stays in parity.

Also stage bun.lock version alignment (1.21.0 -> 1.21.1) that matches the
staged package.json bump.

Verification: ui + web type-check pass; ui/web tests pass except
pre-existing failures on the custom baseline (forge.test.ts, session-actions,
issue-1637-2270, routes.test.js fs-stat directory scope).
2026-08-30 06:56:43 -04:00
Bohdan Triapitsyn 65c7f27eb4 docs: add Turkish localization 2026-08-29 12:14:19 +03:00
bot-hermes 6b2b12382e Merge pull request 'chore: bring upstream v1.20.0 into custom' (#2) from release/v1.20.0 into custom 2026-08-28 22:05:23 -04:00
Tom Rochette ff36638cb2 docs: document where each magic prompt runs and when it fires 2026-08-24 04:46:40 +00:00
Bohdan Triapitsyn 0d50253efa refactor(integrations): retire unavailable options
Remove retired Command Code, Discord, and Telegram integration entries, search targets, and documentation. Keep Command Code provider usage and logo support available through normalized provider ID aliases.
2026-08-22 01:10:19 +03:00
Bohdan Triapitsyn 9f7d839fc6 docs(integrations): add provider account notice 2026-08-22 01:10:19 +03:00
Bohdan Triapitsyn f29844b2f1 feat(settings): mark integrations as experimental 2026-08-22 01:10:19 +03:00
bot-hermes 285f551cd5 Merge remote-tracking branch 'origin/main' into feat/gitlab-issues-mrs
# Conflicts:
#	packages/ui/src/components/chat/work-status/WorkStatusContextSection.tsx
#	packages/ui/src/components/chat/work-status/WorkStatusPrimaryGroup.tsx
2026-08-20 14:47:33 +00:00
Bohdan Triapitsyn 1ed3f1f575 feat(skills): curated GitHub catalog redesign (#3016)
* feat(skills): remove ClawHub catalog integration

Drop the ClawHub registry as a skills catalog source across web server,
shared UI, VS Code, docs, and locales. The catalog now serves git-based
sources only: the curated Anthropic repo and user-defined repositories.
Also removes the now-unused adm-zip dependency.

* feat(skills): redesign catalog around curated GitHub repositories

Replace the single-source dropdown with a card grid of curated GitHub
repositories (Anthropic, OpenAI, Cursor pstack/skills, Matt Pocock) plus
user-defined sources. Source cards show skill counts, GitHub stars, and
last-updated time; a global search covers all loaded sources.

Server: curated sources gain GitHub repo metadata (stars, pushed_at)
fetched best-effort with a 3-hour in-memory and on-disk cache; scans
run through a concurrency-limited, deduplicated cache with 3-hour TTL
persisted across restarts. Refresh still bypasses the cache.

Shared UI: source cards, global search with clear button, per-skill
GitHub links, install/installed states. VS Code curated list updated
to match. All new copy translated across 12 locales.

* fix(skills): address catalog review findings

- GitHub metadata fetch timeout drops to 1.5s (under the catalog
  client's 3s deadline) and failed lookups cache briefly (5 min) so
  repeated catalog loads do not re-hit a failing API.
- Disk cache files are written with owner-only permissions (0o600);
  rename preserves the mode.
- loadSource deduplicates concurrent in-flight requests per source and
  the shared isLoadingSource flag now clears only when the last active
  source load finishes.
2026-08-20 01:40:10 +03:00
Bohdan Triapitsyn 98b5ea2055 docs: add integrations guides and sidebar links
Adds new integrations docs for Claude Code, Command Code, and Cursor
Links the Providers pages to the new integrations guide
Updates the docs sidebar with localized integrations navigation
2026-08-19 00:56:31 +03:00
bot-hermes 4ce02ee569 fix: address bot review findings
- Add codeberg.org to deriveLinkedIssueProvider built-in Gitea check
- Fix Gitea PR review prompt: !N → #N (GitLab vs Gitea syntax)
- Update gitlab.mdx: remove 'read-only' claim (write support ships)
- Gate walkthrough Gitea PR source (server can't fulfill yet)
- Thread per-project API base URL override into walkthrough GitLab diff
- Stop following cross-origin redirects in GitLab/Gitea clients (auth leak prevention)
2026-08-18 20:35:46 +00:00
bot-hermes f16a5bab6b docs: add GitLab issues/MRs page 2026-08-16 15:42:26 +00:00
Serhii Dziupin 11537de734 Merge pull request #2924 from makeittech/feat/fix-clawhub-label-typo-5c30
Fix ClawHub display name typo in Skills Catalog (#2895)
2026-08-15 06:43:56 +03:00
Serhii DziupinandSerhii Dziupin a216b6871f Fix ClawHub display name typo in Skills Catalog (#2895)
The Skills Catalog source dropdown showed "ClawdHub"; the registry brands
itself as ClawHub. Update curated/fallback labels across web, UI, and VS Code,
align docs, and add regression tests.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-08-14 18:49:31 +00:00
Bohdan Triapitsyn a5aa32446d feat(browser): replace the preview proxy with a real browser panel and an agent web tool (#2883)
The preview panel worked by proxying a dev server through OpenChamber's own
origin and rewriting the HTML that came back. Anything the rewriter did not
anticipate broke, and pages that refuse to be embedded never loaded at all.
This deletes the proxy (-1604 lines and its tests) and merges the preview and
browser panels into one surface backed by a real Chromium view.

What the panel is now

- A `<webview>` in its own session partition: logins and cookies persist, hot
  reload works because nothing is rewritten, DevTools are one click away.
- Annotation: pick one element, drag a region, or draw freehand, write a note,
  and it reaches chat with a screenshot of the visible page with the marks on it.
- Toolbar: hard reload, page zoom, device sizes, a light/dark switch that
  applies to the page rather than the app, and cookie/cache clearing scoped to
  the panel alone.
- Several pages at once, each tab showing the page's own favicon, and an address
  bar that suggests pages already visited in this project.
- Dev servers are listed from what is actually listening on the machine, checked
  against what a project announced, so a server is offered no matter how it was
  started. One that is still starting is waited for instead of failing.

Remote dev servers

The desktop app binds a local port and pipes raw bytes to the OpenChamber host
over the existing authenticated connection, so the page keeps its own origin at
the root of its own host. The reachable set is exactly what discovery reports
and is re-checked per connection, so an authenticated client cannot dial
arbitrary local services on the host. Links and redirects to another loopback
port stay on the machine that served the page. A tunnel that cannot be opened is
reported; it is never replaced by the plain loopback URL, which would answer
from the user's own machine under a remote address.

Agent control

Browser actions are a separate `openchamber_web` tool: open, snapshot, click,
type, scroll, inspect computed styles, resize between mobile/tablet/desktop, and
capture a screenshot into `.openchamber/screenshots/` in the project. The
existing `openchamber` tool keeps sessions, worktrees and scheduled tasks. Each
has its own setting in the new Settings -> General -> OpenChamber Tools section,
and the plugin is not injected at all when both are off.

Capability belongs to the connected client, not to configuration: a client
declares on its event stream that it can drive a page, which only a Chromium
host does. Exactly one client performs each request — it claims the request
before acting, and the first claim wins — because deciding by whose result
arrives first would be too late for a click that already happened. No client
listening is answered immediately with an explanation rather than a timeout.

Runtime boundaries

Web tabs get a plain iframe that can display a page but not inspect one. The
VS Code extension no longer offers the surface at all, since nothing that makes
the panel worth having works there. Mobile is unaffected.

Native boundary

Camera, microphone, location and device-picker requests from panel pages are
denied — Electron grants them by default when no handler is set, and the panel
loads whatever address the user types. Page capture, appearance emulation and
storage clearing verify that their target belongs to the panel's own session
instead of trusting a web-contents id from the renderer.

Persisted state

Stored `preview` tabs migrate to `browser` (v13 -> v14). Context panel tab
limits are now per surface, so filling one surface no longer evicts another's
tabs. Address history is stored per project and per runtime.

Documentation

`preview.mdx` and `desktop-browser.mdx` rewritten across all locales, the agent
tool settings path corrected, new `DOCUMENTATION.md` for the browser-control
broker and the dev tunnel, and the `ui-api-decoupling` skill updated where it
still described the deleted proxy.
2026-08-13 22:44:13 +03:00
Bohdan Triapitsyn e13b4526c6 feat(tasks): manage markdown loops from scheduled tasks 2026-08-09 21:43:16 +03:00
Bohdan Triapitsyn ce6912fe40 fix(tasks): sync markdown loops when listing tasks 2026-08-09 20:32:30 +03:00
makeittech 0a4fd7c5fb docs(tasks): add loops quick-start to the scheduled-tasks page
User-facing onboarding for markdown loop tasks: where .agents/loops
files live (project + user scope), a copy-paste sample file, the
frontmatter field table, and the behavior contract (file authoritative,
off by default, rename/malformed semantics, run-now still available).
Also lists the cron schedule type in the UI task creation steps, which
the page previously omitted.
2026-08-06 09:56:11 +03:00
Bohdan Triapitsyn bcae0fcfc3 fix(walkthrough): stop the importance tag reading as a review finding
The "Critical" pill was painted in the status-error colour, so a stop marked
because it drives the change read as a severity reported against the code —
the one thing this feature never does. It is now "Key change", carries its
emphasis with weight and an outline rather than a status colour, and both tags
state their meaning in a tooltip. The panel links the guide from its header,
and the guide gained a section on what the tags mean and what they do not.

Also corrects two German strings that translated the noun "stop" as the verb.
2026-08-04 19:06:19 +03:00
Cursor AgentandSerhii Dziupin 094fb4fc40 merge main to pick up German locale for custom provider keys
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-08-03 08:53:06 +00:00
Bohdan Triapitsyn 82c540b9a3 feat(i18n): complete German localization 2026-08-03 02:26:52 +03:00
Bohdan Triapitsyn 1d17cb87b3 feat(walkthrough): write walkthroughs in the reader's language
A guided explanation is only useful in a language the reader reads, so the
panel header gets a language picker alongside the model one, defaulting to
the interface language. Like the model, it is request state rather than a
setting: the language travels with the read and the generation, and the one
a walkthrough was written in is stored with it, so reopening a review
describes what is there instead of what a fresh one would be.

Only prose is translated. Hunk aliases resolve back to hunk ids and
icon/importance are validated against fixed English values, so a translated
one would be dropped by the normalizer — silently losing an anchor or a
style. Identifiers and paths stay as they appear in the code.

The language is part of the cache key, and a read now asks the cache for the
exact request it was given before falling back to the pointer. Without that
the panel answered a request to switch languages with the text it already
had, leaving the other language unused in the cache.

Alongside it:

- The answer budget is derived from the resolved model instead of a flat 24k.
  That number was the same for a 64k-context model and for one that admits to
  384k output tokens, and on the latter it was the only reason generation
  failed: the model spent the whole allowance reasoning and returned nothing.
  It is now min(96k, max(24k, a quarter of the context)) capped by the
  catalog's output limit, decided once so the input reserve and the request
  cannot drift apart.
- A read no longer offers Cancel. It is a few hundred milliseconds of git with
  nothing to cancel, and the button flickered on every model or language
  change. When the panel is showing a fallback, a banner names what is on
  screen versus what was asked for — only once the read has settled.
- The header keeps one 32px control height and drops its labels below 680px
  instead of squeezing them to two letters and an ellipsis.

Docs and module documentation updated in every locale.
2026-08-03 01:27:27 +03:00
Bohdan Triapitsyn 34d0ff7383 feat(walkthrough): guided AI walkthrough for diffs, branches, and PRs (#2572)
A diff is ordered by file path, which is almost never the order in which a
change makes sense. This adds a Walkthrough surface that reorders it: the model
groups related hunks into stops, explains what each group changes about
behavior, and orders the stops so each builds on the last. It explains and
orders; judging code stays with the existing Review action.

Reviews uncommitted work (all, staged, unstaged), a branch against its base, or
a pull request. Generation is always user-initiated — nothing runs on a timer,
on a file change, or as a side effect of opening a panel.

Invariants worth preserving:

- Hunk identity is derived on the server and only there. Ids are content
  hashes, so an anchor that no longer resolves is proof the code it described
  changed, and staleness needs no heuristics. The client matches ids to ids and
  never recomputes them; two implementations would have to agree forever.
- The digest is never truncated. A diff that does not fit the model's context
  is refused with an actionable reason, because a walkthrough written against
  half a diff reads as confident and is wrong.
- Nothing disappears. Lockfiles and other generated output are excluded from
  the model's input by name — never by size — and everything no stop covers is
  listed at the end, so "have I seen all of it" stays answerable.
- Cost is explicit. Results are content-addressed, so returning the working
  tree to an earlier state costs nothing; generation outlives its request, so a
  refresh detaches the client rather than discarding paid-for work, and only an
  explicit cancel stops it.

Supporting changes to shared modules:

- git: expose the existing getRangeDiff as GET /api/git
  listUntrackedPaths and getUntrackedDiffs. The latter resolve the repository
  once for a batch instead of per file, taking a panel
  ~340ms on an 80-file working tree.
- small-model: structured output across four wire forma
  and abort signal, and an onOverflow policy so an oversized prompt fails
  loudly instead of being silently clipped. A provider
  remembered so the prompt-side fallback goes first next time.
- models.dev metadata: surface structured_output as tri
  false blocks a model, a missing field does not, because the catalog omits it
  for roughly half of all models.

Desktop and tablet only: VS Code serves Git through its
these routes, and the mobile shell does not consume the surface registry.

Docs: packages/docs walkthrough page in English and all eight locales.
2026-08-02 16:22:55 +03:00
Cursor AgentandSerhii Dziupin d40bb9e5a0 fix custom provider credentials, edit path, and failure UX
Require an API key or {env:VAR} on client and server, add edit/prefill for
existing custom providers, save auth before config, and surface incomplete
auth plus disconnect after partial failures. Add VS Code parity tests and
drop the unused allProvidersConnected locale key.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-08-02 11:40:02 +00:00
Cursor AgentandSerhii Dziupin be87e25c7d feat: add custom/other OpenAI-compatible LLM providers
Allow Settings → Providers to define custom providers (id, name, base URL,
API key, models, headers) without code changes. Persist config via OpenCode
layers, store keys through auth.set, and keep web/VS Code parity.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-08-02 09:12:28 +00:00
Bohdan Triapitsyn fbc064c16f docs: add agent control tool guide
Adds a new docs page explaining the OpenChamber agent control tool and its capabilities.
Includes localized versions for supported languages and adds the page to the docs sidebar.
Updates the lockfile with the new better-sqlite3 dependency.
2026-07-28 12:58:20 +03:00
Bohdan Triapitsyn bb45164ae8 feat: session goals - server-driven goal loop with independent small-model audit (#2148)
Arm the target button in the composer and the next prompt becomes a goal:
the server keeps the session working toward it (idle tick -> small-model
audit -> continuation) until the objective is verifiably complete, blocked,
or out of budget — even with the UI closed.

Server (packages/web/server/lib/session-goal):
- event-driven loop on the global SSE hub; goal state lives in
  session.metadata.openchamber.goal (merge-safe patches, stale-write guard
  by goal id), so it survives restarts and syncs to every client for free
- the small-model audit (objective + last assistant turn only, language
  pinned to the objective) is the sole termination authority; blocked needs
  3 consecutive verdicts, audit outages tolerate one unaudited continuation
  then stop the goal as resumable-blocked
- hard stops: optional token budget, auto-continuation cap (Resume grants a
  fresh allowance), turn errors; user abort pauses the goal instead of
  blocking it, and resuming over an aborted tail nudges immediately
- token accounting as a snapshot of the latest turn (input + cache.read +
  output), goal-relative via a creation baseline and segmented across
  compactions; a compaction summary skips the audit and continues
- continuations reuse the session's own provider/model/agent/variant

UI:
- three-mode target button (arm / disarm / manage dialog), informational
  goal strip with inline pause/resume and an Evaluating indicator, sidebar
  state glyph, objective length counter (2000-char server clamp),
  read-only completed goals
- goal entry points: composer (sessions and drafts), start-new-session-
  from-answer dialog, plan implement dialog (plan content becomes the
  objective), scheduled tasks (Run as goal + budget)
- Settings -> Chat -> Goal: feature toggle + default token budget with
  three-layer parity (web server, client persistence, VS Code bridge);
  VS Code renders goal state but hides the entry points (the loop runs in
  the web server only)

Notifications: per-turn "ready" notifications are suppressed while a goal
is active; settling sends one final notification (desktop, web-push, APNs
generic titles with the session name as body) honoring the completion
toggle. Error/question/permission notifications are untouched.

Docs: user guide (session-goals) in all 9 locales + sidebar entry,
scheduled-tasks cross-reference, server module DOCUMENTATION.md.
2026-07-12 01:23:22 +03:00
Bohdan Triapitsyn 6ec1797583 feat(cli): make connect-url --relay a full anywhere pairing link
- --relay links now carry both routes: direct LAN plus relay fallback,
  matching the UI's Anywhere pairing; devices prefer the direct route
- pairing sessions created by the CLI are marked with usesRelay, and the
  server reconciles relay demand on a timer, so a headless instance
  brings the relay up on its own after connect-url --relay
- warn with LAN_UNREACHABLE when the link's direct route points at
  loopback and other devices cannot use it
- document the --relay flow and the --lan binding caveat in Connect a
  Device and Remote Instances across all locales
2026-07-10 18:29:15 +03:00
Bohdan Triapitsyn 7ea974d89b docs: centralize device connection guides, add private relay docs
- new Connect a Device page: one-time QR pairing, transport choices, device management
- new Private Relay page: E2EE guarantees, demand-driven lifecycle, relay vs tunnel
- rewrite mobile page around the native iOS/Android apps (TestFlight + APK)
- update remote-instances, security, tunnels, and remote-access troubleshooting to point at the new pairing flow
- translate everything across all 8 locales and update the sidebar
2026-07-10 15:30:50 +03:00
eb051f969e Add Japanese locale (ja) support (#1810)
* Add Japanese locale (ja) support

- New: ja.ts with 2624 UI translation keys
- New: ja.settings.ts with 1789 settings translation keys
- Registered ja in runtime.ts (Locale type, LOCALES, LOCALE_LABEL_KEYS, normalizeLocale)
- Added ja-JP BCP-47 mapping in intl.ts
- Added JA_MESSAGES in bootstrap.ts
- Added ja to lazy-load dictionary map in store.ts
- Updated messages.test.ts with ja dictionary
- Added common.language.japanese to all 8 existing locale files

* fix(i18n): complete Japanese locale updates

* test(i18n): enforce locale key parity

* docs: add Japanese documentation

---------

Co-authored-by: yuchi0531 <yuchi0531@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-27 01:00:05 +03:00
Pascal AndréandBohdan Triapitsyn 49a1424e5f feat: add complete French localization (#1482)
* feat: add French locale runtime

Add French to OpenChamber's shared i18n runtime, dictionaries, and parity tests so the existing language picker can load a complete fr locale across shared UI surfaces.

* fix: localize shared UI formatting

Remove remaining shared UI locale hardcodings so dates, numbers, and first-party helper copy follow the active app locale instead of leaking English on French surfaces.

* feat: localize VS Code French surfaces

Localize VS Code bootstrap, native runtime messages, panel titles, and manifest contribution strings so French users get consistent first-party copy across the extension experience.

* fix: TASK-2026-05-30-008 correct French review findings

Fix broken French relative-time and weekday strings reported on PR #1482 and restore proper import order in quota utils without broadening scope.

* fix: TASK-2026-05-30-008 address final PR review comments

Capture the localized More Info label once in the VS Code CLI-missing flow and replace the remaining inline French-only utility strings with dictionary-driven copy plus required locale keys.

* fix: TASK-2026-05-30-008 normalize French glossary

Correct glossary-level French terminology on the live PR branch, keeping canonical technical terms like PR, worktree, stash, HEAD, Mermaid, Markdown, remote, and session while replacing misleading literal translations.

* fix: TASK-2026-05-30-008 refine French terminology pass

Clean up remaining glossary mistakes on the French PR branch, especially around Mermaid, Markdown, PR, worktree, stash, branch, remote, and commit terminology, while keeping behavior unchanged.

* fix: TASK-2026-05-30-008 clean remaining French false friends

Correct the SOCKS5 mistranslation and a final small set of obvious false-friend technical nouns on the French branch without changing behavior.

* fix: TASK-2026-05-30-008 correct French glossary terms

Replace remaining false-friend translations in the French UI dictionaries and normalize technical labels for the French PR branch.

* fix: TASK-2026-05-30-008 remove remaining French Mermaid false friend

Replace the last confirmed Sirène translation with Mermaid and re-run the requested blacklist and build verification on the PR branch.

* fix: TASK-2026-05-30-008 enforce French glossary policy

Keep skill/PR/worktree/remote terminology developer-credible in French and remove remaining machine-translated Git and settings copy.

* fix: TASK-2026-05-30-008 keep prompt terminology in French

Replace remaining technical invite translations with prompt wording across scheduled tasks, multi-run, prompt templates, and Magic Prompts.

* fix: TASK-2026-05-30-008 finalize French terminology cleanup

Polish remaining worktree/remote wording, remove visible metadata leakage, and correct final Git and settings labels on the French PR branch.

* fix: TASK-2026-05-30-008 polish final French strings

Correct the last aria-like artifacts and awkward worktree/remote/GitHub URL phrasing in the French dictionaries.

* fix: TASK-2026-05-30-008 normalize final French glossary framing

Tighten the last worktree/remote/checkout wording and fix remaining French grammar around canonical technical terms.

* fix: TASK-2026-05-30-008 align final developer glossary wording

Normalize the last French framing around canonical developer terms like worktree, remote, prompt, and checkout.

* fix: TASK-2026-05-30-008 harmonize final French sentence framing

Replace the last raw franglais around checkout, remote, worktree, and prompt-facing labels with more natural French framing while keeping the chosen technical terms.

* fix: TASK-2026-05-30-008 add compact relative date keys

Replace French-specific prefix stripping in compact session date labels with dedicated i18n keys across locale dictionaries, preserving existing compact label output while making French wording robust.

* docs: add French documentation

* docs: mention French locale folder

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-10 20:19:30 +03:00
Bohdan Triapitsyn a486e76233 chore: update runtime requirements
Require Node 22 or newer
Update project package manager to Bun 1.3.14
2026-06-10 17:37:19 +03:00
Bohdan Triapitsyn 0f6602aabd docs: add SSH hosts and proxying guide
Adds desktop SSH hosts and proxying documentation
Includes translations for all supported locales
Requires translations for new docs pages
2026-06-05 18:38:39 +03:00
Bohdan Triapitsyn 2031e3b4a8 Decouple bundled UI from runtime API and add remote instance tooling (#1228)
Add a packaged-client runtime boundary so the shared UI can talk to local,
desktop, remote, and VS Code runtimes through the right transport instead of
assuming one same-origin web server.

Centralize OpenChamber-owned API access behind RuntimeAPIs, runtimeFetch, and
runtime URL helpers, while keeping official OpenCode traffic on the SDK path.
Support runtime switching, remote host selection, desktop client credentials,
and headless connection links for pairing packaged clients with remote
OpenChamber servers.

Harden the new auth model by moving long-lived client tokens out of browser
URLs, introducing short-lived scoped URL tokens for browser-owned transports,
restricting URL-token access to explicit readable/realtime routes, and making
client-token management session-scoped or self-scoped as appropriate.

Update browser-owned assets and preview proxy flows to work with the split
runtime model, including authenticated project icons, preview token propagation,
CSP-safe preview bridge injection, and preview proxy auth that survives
short-lived URL-token expiry.

Tighten Electron security boundaries for packaged clients by gating privileged
preload state to trusted origins and requiring explicit confirmation before
connect deep-links import or switch remote runtimes.

Also refresh agent guidance and project skills so future runtime/API, auth,
preview, UI, CLI, settings, locale, and drag-to-reorder work follows the new
architecture.
2026-06-02 00:43:05 +03:00
Bohdan Triapitsyn 2014303bc0 feat: add startup launch support (#1421)
Add launch-at-startup support across the Electron desktop app and the web CLI.

Electron now supports macOS launch-at-login through the native login item API. Login launches start OpenChamber in the background without opening a window, while Dock activation, deep links, and second-instance launches still open or focus the normal app window. The desktop Settings UI now exposes a localized launch-at-login toggle in Desktop Network Access.

The web CLI now includes `openchamber startup status|enable|disable`, backed by native user services:
- macOS: launchd LaunchAgent
- Linux: systemd --user service
- Windows: Task Scheduler

Startup services run `openchamber serve --foreground` so the OS service manager owns process lifetime and restarts. Foreground service updates now defer restarts to the service manager instead of spawning duplicate CLI restarts.

Startup services snapshot useful environment variables by default so provider tokens, PATH, SSH agent settings, and OpenCode configuration survive login/reboot starts. The snapshot avoids shell/session-only state, uses systemd-compatible env quoting on Linux, and avoids unused env artifacts on macOS.

Also adds localized docs for startup services and environment variables.
2026-05-26 01:36:11 +03:00
Bohdan Triapitsyn 00a7807168 feat: add Ngrok tunnel provider (#1415)
Adds Ngrok quick tunnel support
Adds desktop tunnel docs across locales
Improves tunnel settings provider and TTL labels
2026-05-25 18:00:03 +03:00
Bohdan Triapitsyn 967704a9b9 docs: add OpenChamber feature docs and translations (#1400)
* docs: add OpenChamber feature docs and translations

Add 30 new docs pages covering OpenChamber-specific workflows and setup:
OpenCode server, providers/models/agents, MCP, skills, commands & snippets,
usage, projects, context, notes/todos/plans, scheduled tasks, project actions,
preview, worktrees, multi-run, git & GitHub, magic prompts, git identities,
mobile/PWA, security, notifications, voice, project icons, remote instances,
desktop browser, updates, and three troubleshooting pages.

Rebuild sidebar into eight task-oriented sections and translate every new
page into all six supported locales (uk, zh-cn, es, pt-br, ko, pl).

* docs: surface new sections on homepage and cross-link tunnels

Add an Explore block to the docs homepage (all seven locales) linking to
the new section anchors, and cross-link the Tunnels page to Security and
PWA & Mobile.
2026-05-24 14:15:55 +03:00
Bohdan Triapitsyn ca33c6ae57 docs: add Voice & Style guide and rewrite docs pages to match
Add a Voice & Style section to the docs authoring guide, then bring every
docs page in line with it: lead with the task, add success signals to
procedures, explain jargon on first use, keep bullet casing consistent,
and link out to Troubleshooting where steps can fail.

Applied across English source and all localized versions (uk, zh-cn, es,
pt-br, ko, pl).
2026-05-23 02:43:34 +03:00
Bohdan Triapitsyn 6f4e0068c1 docs: add localized documentation pages
Adds translated docs for Spanish, Korean, Polish, Portuguese, Ukrainian, and Chinese
Covers install, quickstart, themes, troubleshooting, tunnels, and reverse proxy guides
Updates docs workflow, sidebar, and contributor documentation
2026-05-23 00:33:39 +03:00
jwcrystal 6d5afe55db fix: harden SSE compression exclusion and add Caddy reverse proxy docs (#939)
The compression middleware filter runs before route handlers, so the
res.getHeader('Content-Type') check in shouldSkipCompression is always
undefined at decision time. SSE exclusion relied entirely on the Accept
header, which non-standard clients (curl, fetch) may omit.

Add deterministic path-based exclusion for all known SSE routes so
compression is skipped regardless of client behavior. Also add a Caddy
reverse proxy example and a CDN double-compression warning to docs.
2026-04-17 18:12:20 +03:00
jwcrystalandBohdan Triapitsyn 304b14b4b1 feat: add response compression middleware to reduce bandwidth (#928) (#935)
* feat: add response compression middleware for HTTP responses

Add compression middleware to Express server with SSE route exclusion
and 1KB threshold. Reduces bandwidth for non-streaming API responses
(history, sessions, files, static assets) by 60-80%.

Closes #928

* fix: harden proxy compression and proxy docs

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-04-17 16:26:56 +03:00
Bohdan Triapitsyn 75c90d955c docs: refresh docs and add tunnel usage guide (#666)
Add a new tunnels page with verified, up-to-date CLI examples
Remove deprecated `--daemon` usage and clarify QR/password behavior
Add docs validation tooling and docs-source workflow for packaging and sync
2026-03-15 03:30:35 +02:00