Commit Graph
1954 Commits
Author SHA1 Message Date
Bohdan Triapitsyn cc6f76faf1 chore: updated changelog 2026-06-29 00:19:59 +03:00
Bohdan Triapitsyn f9f3705ec9 docs(changelog): add unreleased entries for startup and OpenCode attach fixes 2026-06-29 00:18:22 +03:00
Bohdan Triapitsyn 6464af7ced fix(opencode): never expose a port we don't manage to the process killer
The Electron-side OpenCode killer kills by port (lsof + kill -KILL).
getOpenCodeProcessInfo returned openCodePort unconditionally, so for an
external/attached OpenCode (e.g. a user's own server on 4096) the only thing
stopping the killer from taking it down was the separate `managed` flag — a
single weak signal guarding a destructive action.

Withhold pid/port unless we actually manage the process, so the killer has no
target even if `managed` is ever miscomputed. Managed flow is unchanged.
2026-06-29 00:15:30 +03:00
Bohdan Triapitsyn 5d554eaddd fix(opencode): never auto-attach to a pre-existing OpenCode instance
A blind probe of the default port 4096 made the desktop hijack a user's
separately-running OpenCode (e.g. the OpenCode desktop app): it attached as
an external server instead of starting its own. That coupled OpenChamber's
lifecycle to the foreign instance and broke initialization against an
unexpected server version/config.

Attaching to an external OpenCode now requires explicit opt-in via env
(OPENCODE_HOST / OPENCODE_PORT / OPENCODE_SKIP_START). Without that, we always
start our own managed instance on a freshly-allocated port.
2026-06-29 00:02:03 +03:00
Bohdan Triapitsyn 9f720c66af fix(github): stop PR-status requests from starving startup connection pool
Watching N worktrees fired N PR-status requests at once (startWatching
called refresh() directly, bypassing the batch limiter). Each request can
take 20s+ under GitHub secondary-rate-limiting, and N of them saturate the
browser's ~6 HTTP/1.1 connections per origin, starving the critical path
(bootstrap session.status, diffs, sending messages) until they finish — the
UI appeared frozen for ~20s on startup.

- Gate all PR-status network calls through a global concurrency semaphore
  (max 2), so free sockets always remain for critical traffic.
- Bound resolveGitHubPrStatus with a 12s timeout so a slow request fails
  fast instead of holding a socket; the client keeps its last-known status.
- Reuse already-fetched repo metadata for the default branch instead of a
  redundant repos.get, reducing serial GitHub calls (less rate-limiting).
2026-06-28 23:41:14 +03:00
Bohdan Triapitsyn f040ea85e6 chore: remove temp design documents 2026-06-28 22:28:22 +03:00
Bohdan Triapitsyn 3faddbeae8 release v1.13.7 2026-06-28 13:43:33 +03:00
Bohdan Triapitsyn bf1d0d3a75 feat(mobile): polish composer model/agent controls and selection overlay
Redesign the mobile composer model and agent buttons as borderless, full-bleed
labels that hug their content, truncate with an ellipsis when space is tight,
and show the provider logo inline before the model name. Tighten the footer
action buttons (sessions / attach / auto-accept) so they sit close together,
with a small left inset on the group. In the mobile model selection overlay,
make the thinking-variant control text-only with a chevron, vertically center
the variant and favorite controls in each row, and place the provider logo
inline with the model name.
2026-06-28 13:33:55 +03:00
Bohdan Triapitsyn 1178c4e4f5 fix(providers): keep add-provider flow selected during sync 2026-06-28 13:05:00 +03:00
Bohdan Triapitsyn c5eab5ac1e Revert "fix(ui): actually shrink typography classes on mobile viewports (#1791)"
This reverts commit 1ac8fbad70.
2026-06-28 12:33:10 +03:00
Bohdan Triapitsyn 8b5acbf415 fix(chat): improve mobile history loading and virtualization fidelity
Give touch surfaces a larger, viewport-relative head start for loading older
history so an in-flight fetch completes before the finger reaches the top.
Raise the mobile virtualizer overscan so fast flings stay populated instead of
leaving blank gaps, and drop the fixed itemSize hint so virtua auto-estimates
row heights from measured sizes instead of a flat constant.
2026-06-28 12:27:38 +03:00
Bohdan Triapitsyn b2598fa45e fix(chat): stop scroll twitch and message skipping with expanded tools
Gate passive auto-follow on active (working/settling) state so idle layout
churn from virtualizer re-measurement no longer re-pins the viewport to the
bottom. Render default-open tool bodies synchronously on mount so the
virtualizer measures the real row height up front instead of growing a frame
later and lurching scroll past several messages.
2026-06-28 12:01:26 +03:00
Bohdan Triapitsyn b72230bc24 fix: restore update command helpers (#1857)
Exported package-manager helpers used by openchamber update
Added regression coverage for the update-available path
2026-06-28 09:58:45 +03:00
Bohdan Triapitsyn 5034ad27a8 release v1.13.6 2026-06-28 02:00:17 +03:00
1ac8fbad70 fix(ui): actually shrink typography classes on mobile viewports (#1791)
The mobile stylesheet at packages/ui/src/styles/mobile.css defines a
@media (max-width: 1024px) block that overrides the --text-ui-header,
--text-ui-label, --text-meta, and --text-micro custom properties to
shrink labels on phones. The values it was setting were equal to (or
only 1px smaller than) the desktop defaults, so the model selector,
agent selector, and any other element using these classes rendered at
the same size on a 360px-wide phone as on a 1920px desktop.

Fix: tighten the mobile values to 0.875rem (ui-header), 0.75rem
(ui-label, meta), and 0.6875rem (micro), so that mobile is genuinely
smaller than desktop. The same change is applied to the iOS PWA
standalone block lower in the file so both code paths produce the same
size.

Fixes #504

Signed-off-by: Bohdan Triapitsyn <artmore@protonmail.com>
Co-authored-by: James Pinnell <james@example.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-28 01:39:44 +03:00
Bohdan Triapitsyn 4d4b3aa9d9 fix: improve context panel session visibility
Marks active embedded chat sessions as seen only while focused
Shows real session titles for context panel chat tabs
Names review sessions after the implementation session
2026-06-28 01:39:15 +03:00
Bohdan Triapitsyn 84d7303346 feat(desktop): add macOS dock badge for chats with unseen activity
Show a count of chats (root sessions) with unseen activity on the macOS
dock icon. The count is computed in the existing tray snapshot (full
cross-project list, not the capped tray view; a subtask's unseen rolls up
to its root only when subtask notifications are enabled) and pushed to the
main process over the existing desktop_tray_update IPC, which calls
app.setBadgeCount (0 clears it). The badge clears as sessions are marked
seen on window focus.

Add a Dock badge toggle in Appearance settings (default on, persisted,
darwin desktop only), localized across all dictionaries, with a matching
settings-search entry whose availability mirrors the render guard exactly.
2026-06-28 01:07:52 +03:00
Bohdan Triapitsyn 00efd8cc4b fix: mark context panel chat sessions as seen
Clears unread state when a chat session is active in the context panel
Keeps embedded chat presence in sync while the window is focused
2026-06-28 01:05:44 +03:00
Bohdan Triapitsyn 1b67aa3c21 fix: prevent duplicate preview URL auth tokens
Stops embedded browser URLs from accumulating repeated oc_url_token params
Strips preview-only query params before persisting browser navigation
Adds regression coverage for replacing URL auth tokens
2026-06-28 00:10:35 +03:00
Bohdan Triapitsyn 757c704956 fix(chat): rebuild auto-follow on a single instant writer to kill scroll twitch
Replace the RAF easing follow loop + settle burst with an always-on
instant-follow model: while pinned, the content ResizeObserver re-pins to
the bottom synchronously (scrollTop = scrollHeight, before paint) and is
the only writer of scrollTop. A position+TTL auto marker distinguishes our
own programmatic writes from genuine user scrolling, so a scroll event that
lands at the just-written bottom never trips a false release.

This removes the feedback loop where the easing animation, growing content,
and the user's own scroll all fought for scrollTop in the same frame -- the
infinite twitch when scrolling down during streaming, and the jiggle on
send / from the queue. The public hook interface is unchanged; all
consumers keep working untouched.
2026-06-27 23:45:34 +03:00
Bohdan Triapitsyn 7f45963050 release v1.13.5 2026-06-27 09:49:38 +03:00
Bohdan Triapitsyn 5cc37d0c33 fix: restore CLI validation and test baseline (#1857)
Fixed lazy CLI helper imports for tunnel flows
Restored update command version detection
Made web test and dead-code commands run reliably
2026-06-27 09:45:34 +03:00
Bohdan Triapitsyn 38c9ff77c9 fix: restore CLI and quota provider startup (#1857)
Export ngrok tunnel capabilities for CLI startup
Import Clack spinner/progress helpers used in interactive CLI paths
Restore Google quota provider registry exports
2026-06-27 09:19:24 +03:00
Bohdan Triapitsyn 1cec2f3b1a release v1.13.4 2026-06-27 01:50:10 +03:00
Bohdan Triapitsyn 3c3abd2afe fix(chat): keep historical session entry an instant snap to bottom, not a smooth scroll
Entering a historical session sometimes showed a smooth scroll from a mid
position instead of landing instantly at the bottom. The single-writer change
had startFollowLoop stop the settle burst, so a content-measurement
ResizeObserver tick during restore would downgrade the authoritative instant
pin into an easing follow loop that scrolled in from the partially-measured
position.

- startFollowLoop now yields to an active settle burst instead of stopping it.
  The asymmetry is intentional: the settle burst is the authoritative instant
  pin (session restore / goToBottom 'instant') and must not be preempted by the
  easing loop. startSettleBurst still stops the follow loop, so the two never
  write scrollTop in the same frame.
- tickFollow snaps deltas larger than a full viewport (discrete jumps: late
  history measurement, session entry, a big block in one commit) instead of
  easing them; only small streaming-sized deltas ease.
- restoreSnapshot mirrors goToBottom('instant') — instant write + settle burst,
  no startFollowLoop.
2026-06-27 01:34:27 +03:00
Bohdan Triapitsyn 9547e3a0d2 fix(i18n): localize bootstrap messages 2026-06-27 01:03:32 +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
Bohdan Triapitsyn b384deb812 fix(chat): stop scroll jiggle and double-scroll on send while pinned
Two scroll owners were writing the chat container's scrollTop concurrently
during pinned content growth and on send, fighting frame-to-frame and
producing the reported flicker/jiggle (after a pause, from the queue, on user
interruptions) plus a visible double scroll on a normal user send.

Enforce a single-writer invariant in useChatAutoFollow:
- The easing follow loop and the instant settle burst now mutually exclude:
  starting one stops the other, so they can never write scrollTop in the same
  frame. The isFollowingProgrammatically flag (which suppresses the overlay
  scrollbar) is owned by whichever loop is active and cleared only when both
  are idle, including the settle burst's natural 280ms end.

Stop the redundant re-pin storm in useChatTimelineController:
- While pinned, route goToBottom('instant') only for a prepend (history loaded
  above), not on every bottom append / streaming part. Normal growth is owned
  by the follow loop (kicked by the content ResizeObserver and chunk handlers).

Remove the double movement on send:
- Add scrollToBottomOnSend: when already following, just (re)kick the follow
  loop for a single smooth movement instead of also firing an instant
  goToBottom that raced the ResizeObserver-driven loop. When released (scrolled
  up), keep the instant jump to the just-sent message.
2026-06-27 00:06:19 +03:00
Ibrahim Khan 903e8ecbb0 fix(providers): preserve add-provider form selection on background reload (#1838)
loadProviders dropped the __add_provider__ sentinel selection during a
background provider refresh because it is not a real provider id, which
navigated users out of the in-progress Add provider form and discarded
their unsaved input. Preserve the sentinel alongside real provider ids.

Fixes #1765
2026-06-26 19:38:09 +03:00
Serhii DziupinandSerhii Dziupin f4e90ca232 fix(agents): stop falsely reporting saved agent edits on external OpenCode; make model-selector shortcut customizable (#1839)
* feat(shortcuts): make 'Open model selector' shortcut customizable

Lets users remap the model selector shortcut (e.g. to Ctrl+M) via
Settings > OpenChamber > Shortcuts, matching OpenCode's quick
model-switch keybinding workflow.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* fix(agents): surface manual-restart needed when on external OpenCode

Agent prompt/permission/settings edits are written to disk, but an
external OpenCode server (skip-start or auto-detected on the default
port) is not owned by OpenChamber and is only health-probed on config
change, so it keeps serving its startup-cached config until restarted.
The API previously claimed a successful reload, so the UI silently
reverted the edit to the stale/default value on refresh.

Now refreshOpenCodeAfterConfigChange reports whether a real reload
happened; agent routes return requiresManualRestart for external mode;
and the agents UI keeps the saved values and warns the user to restart
their OpenCode server instead of showing a false success. Managed mode
behavior is unchanged (process is restarted and reload is live).

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-06-26 19:37:45 +03:00
Serhii DziupinandSerhii Dziupin 3e3cd82a47 feat: inherit model-picker reorder/accordion persistence and shift-delete from otto-ui (#1833)
* feat(model-picker): drag-to-reorder providers and persist accordion state

Ports two model/provider picker QoL features from otto-ui:
- Persist collapsed state of picker sections (favorites, recent, each
  provider) via a new persisted zustand store so collapse survives
  remounts and reloads, shared across every picker surface.
- Desktop drag-to-reorder of provider sections (whole header as the
  mouse activator, 8px threshold so a plain click still toggles
  collapse), with the order persisted in useUIStore.providerOrder and
  applied across ModelControls, ModelMultiSelect and ModelSelector.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* feat(sessions): shift-click quick action hard-deletes thread without prompt

Ports the shift-quick-delete feature from otto-ui. The sidebar quick
action (normally archive) becomes a no-prompt hard delete while Shift is
held: the icon switches to a trash bin, the affordance turns destructive,
and the click bypasses the confirmation dialog via a new skipConfirm
source flag. A shared useShiftKeyHeld hook (single window listener set,
useSyncExternalStore) keeps only the small action button re-rendering on
Shift state changes, and resets on window blur so the affordance can't
get stuck after alt-tab.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-06-26 19:36:47 +03:00
Serhii DziupinandSerhii Dziupin e7b952cbcb feat(chat): allow dragging queued messages to reorder the queue (#1831)
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-06-26 19:35:45 +03:00
Bohdan Triapitsyn fc9146eb7b chore: clean up dead exports after merges 2026-06-26 19:35:25 +03:00
Bohdan Triapitsyn 1f549e4525 feat: add automatic review loop (#1840) 2026-06-26 19:29:44 +03:00
Bohdan Triapitsyn 45df19c3b2 Refactor web CLI into focused modules (#1837) 2026-06-26 19:28:44 +03:00
00821700de chore: remove dead code (59 unused files + ~125 unused exports) (#1835)
* chore: remove dead/unreferenced files across ui, vscode

Remove 59 unused source files (components, hooks, lib utils, stores,
barrels, and orphaned vscode github modules) that are not imported by
any entry-reachable code. Also drop a stale test mock for the removed
execCommands module.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove unused exported symbols (types, functions, consts, hooks)

Remove exported symbols whose identifier is referenced nowhere in the
repository (verified via repo-wide search), across ui types/contracts,
lib utilities, sync layer, stores, and components. Also drop the few
imports/private helpers orphaned by these removals.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove more unused exports (desktop, shortcuts, worktree, vscode)

Continue removing repo-wide unreferenced exported functions, consts and
types across lib/desktop, shortcuts, worktreeSessionCreator, sync, and
vscode gitService, with cascading orphaned helpers/imports cleaned up.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore: add dead-code cleanup tooling

* refactor: checkpoint dead-code cleanup

* refactor: remove dead-code suppressions

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-26 19:27:53 +03:00
Bohdan Triapitsyn 4a37b9a005 fix: clean up stale file tree paths on startup
Removes missing expanded folders from persisted Files state
Prevents repeated 404 noise for stale file tree paths
Avoids startup SDK race when restoring sessions
2026-06-26 12:47:01 +03:00
8c1a24089d fix(worktree): gate sessions on bootstrap readiness (#1762)
Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-26 12:16:42 +03:00
bashrusakhandLeonid Skorobogatyy 03e6f789a4 fix(git): materialize draft session for generate (#1761)
* fix(git): materialize draft session for generate

* fix(sync): remove redundant draft session side effects

---------

Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
2026-06-26 11:52:39 +03:00
Bohdan Triapitsyn b0e476ab7c fix: prevent sidebar worktree reorder snap-back (#1827)
Invalidates cached group ordering when reorder state changes
Keeps dragged worktree groups in the new position immediately after drop
Validated with UI package type-check
2026-06-25 17:11:37 +03:00
Bohdan Triapitsyn f4646a5268 fix: preserve target session thinking variant in review handoffs
Uses the linked target session's last model choice for follow-up review transfers
Prevents reviewer thinking settings from leaking into implementer follow-ups
2026-06-25 12:25:41 +03:00
Bohdan Triapitsyn b173fd3f09 fix: align CLI lifecycle detection with live port checks (#1830)
Prevents status/stop/restart from deleting valid PID registries on transient probe failures
Adds explicit stop recovery for unresponsive PID-file instances
Covers unmanaged, stale PID, host, and desktop edge cases with CLI tests
2026-06-25 12:00:09 +03:00
Tom Rochette 9a2012c94e fix(chat-input): dismiss open question prompt when sending a message (#1740)
Sending a message while a question prompt was open left the prompt
lingering, blocked the send, or collided with the still-blocked agent
turn. Two root causes:

useSessionActivity treated pending permissions as idle but not pending
questions, so the send button became Stop during a question and Enter
queued/collided instead of sending. handleSubmit also never dismissed
the open question, stranding the session in a half-answered state.

The send path now dismisses open questions for the session subtree
(optimistic local clear so the card vanishes instantly, plus a formal
question.reject) and queues the message. The queued-message auto-send
hook then delivers it as the next turn once the rejected turn winds
down and the session returns to idle. Queueing avoids aborting the
turn, which surfaced an unwanted "running turn was stopped" notice.

Regression tests cover the no-op, subtree dismissal (root + subagent
child), and QuestionNotFoundError paths.
2026-06-25 10:55:35 +03:00
Bohdan Triapitsyn 4a068fca86 release v1.13.3 2026-06-24 19:15:26 +03:00
Bohdan Triapitsyn c97d5c79c1 fix: prioritize session sidebar status indicators
Prevents pinned icons from crowding live indicators
Shows chevrons on hover when status or pin takes priority
2026-06-24 18:45:26 +03:00
bashrusakhandLeonid Skorobogatyy 569342b3c2 fix(markdown): preserve user code block characters (#1750)
Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
2026-06-24 18:33:50 +03:00
Bohdan Triapitsyn 8c551c40aa fix(chat): stop double scroll write on prepend that resonates into oscillation
When older history is prepended while the viewport is pinned to the bottom, the
timeline controller wrote the re-pin manually (scrollTop += delta). That write
is not flagged as programmatic, so useChatAutoFollow's scroll handler treated it
as movement and issued its own correcting scroll — a redundant up/down move on
every prepend. On most setups it settles after one move, but with different
virtualizer measurement/timing it never converges, producing the reported
infinite up/down scroll glitch.

When pinned, delegate the prepend re-pin to auto-follow's goToBottom('instant'):
a single authoritative write to the bottom that IS marked programmatic, so
auto-follow ignores it instead of fighting it. The released case (user reading
back through history) is unchanged and still preserves the read position.

This also covers the on-open history auto-load (loadEarlierIfPinnedViewport-
Underfilled), which only runs while pinned, so its prepends now go through the
single writer too.
2026-06-24 18:29:55 +03:00
Bohdan Triapitsyn f2d4a7833d fix(chat): keep session switch pinned to bottom without backward jump
Release auto-follow based on position (the user has left the near-bottom zone)
instead of scroll-delta direction. The old `currentTop < previousTop` check
treated the tiny scrollTop clamp the browser applies when the composer grows —
which keeps you at the bottom — as a user scroll-up and released follow, so
content finishing loading then drifted the view backward.

Also always return to the bottom on session switch, dropping the saved-ratio
restore: it had a low success rate and, by landing 'released' partway up,
produced the same visible backward jump as content finished loading.

overflow-anchor is already disabled on the chat scroll container, so no
delta-threshold workaround is needed; this is a net simplification.
2026-06-24 18:04:59 +03:00
Hernan Javier Ardila Sanchezandherjarsa 1abb0dc103 fix(chat): set scroll position before paint on session-switch replay (#1730)
Replace useEffect with useLayoutEffect in the pendingInitialRestoreRef
replay so restoreSnapshot runs synchronously after DOM commit, before
the browser paints. Prevents visible flash of content at the wrong
scroll position when the scroll container mounts after session
hydration.

Adapted from openchamber/openchamber#1553 (Fix 2). The virtualVersion
counter (Fix 1) is not applicable: virtua (post #1651) does not use
useVirtualizer's useState-based instance pattern that motivated it.

Validation:
- bun --cwd packages/ui type-check - no new errors in useChatAutoFollow.ts
- bun --cwd packages/ui lint - passed

Co-authored-by: herjarsa <herjarsa@users.noreply.github.com>
2026-06-24 17:28:46 +03:00
Bohdan Triapitsyn 1558364b49 fix(cli): verify process identity when validating server pid files
After an ungraceful shutdown removePidFile never runs, so a stale
run/openchamber-<port>.pid outlives the process. The kernel can recycle that
PID to an unrelated process, and a liveness-only `process.kill(pid, 0)` check
then reports OpenChamber as "already running" and aborts startup — an infinite
crashloop under systemd Restart=always while the port is actually free
(issue #1721).

Verify identity, not just liveness, but only where it belongs:

- Add isOpenchamberProcessRunning(pid) = liveness + command-line identity, and
  use it ONLY at the two sites that validate a PID read from a pid file (the
  "already running" guard and the stale pid-file cleanup sweep). isProcessRunning
  stays liveness-only for PIDs we know are ours (a freshly spawned daemon child,
  processes we are stopping), so those paths cannot get a false negative.
- Identity works on Linux (/proc/<pid>/cmdline) and macOS (ps -o command=); on
  Windows or where the command line can't be read it falls back to liveness, so
  behaviour is unchanged there with no false negatives.
- Match the "openchamber" install-path segment (present for both @openchamber/web
  and a source checkout, foreground and daemon entrypoints alike) so a recycled
  stranger such as npm-cli.js or agentmemory is not mistaken for us.
- Clear the stale pid file once its recorded PID is no longer our process.

Adds unit tests for isOpenchamberCmdline and isOpenchamberProcessRunning,
covering the recycled-PID cases and a live non-OpenChamber process.
2026-06-24 17:23:20 +03:00