Repository navigation
fix(deps): upgrade adm-zip to 0.6.0 to fix GHSA-xcpc-8h2w-3j85 - #2643
Conversation
adm-zip <0.6.0 allows a crafted ZIP to trigger a ~4GB memory allocation (GHSA-xcpc-8h2w-3j85). Bump the dependency in the web and vscode packages to ^0.6.0. The new AdmZip(buffer) and extractAllTo(dir, overwrite) APIs are unchanged, so no call-site adaptation is needed. Add a vitest regression test for the ClawdHub install path that builds a real ZIP with adm-zip and asserts extractAllTo restores files (including nested subdirectories) into the target skill dir.
Code Review SummaryThis PR upgrades
No blocking correctness, security, guidance, or contribution-contract issues found. Verdict: PASS Reviewed HEAD:
|
| Source | Why applicable | Rules/invariants evaluated |
|---|---|---|
AGENTS.md |
Root rules; dependency change and server runtime code | "Do not add dependencies unless explicitly requested" (only an existing dependency's version was raised); correctness invariants around extraction/cleanup; runtime boundary: domain logic stays in packages/web server module |
CONTRIBUTING.md |
Canonical PR policy and review handoff contract | PR contract sections (Intent, Non-goals, Affected surfaces, Repository guidance, Validation, Risks) all completed with concrete evidence; "command name without a result is not evidence" — each check has a stated result; no-visible-change case explained concretely |
.github/PULL_REQUEST_TEMPLATE.md |
Required handoff structure | All template sections present and filled with content specific to this diff; non-goals explicitly stated (no @types/adm-zip removal, no version change) |
openchamber-change-discipline skill |
Dependency/lockfile change plus a new source file | Smallest complete change (4 files); validation matrix for "Dependency or lockfile" (workspace-wide checks + affected builds) and "Added source file" (dead-code) reported as run; focused regression test exercises the real extraction path rather than private helpers |
packages/web/server/lib/skills-catalog/DOCUMENTATION.md |
Owns the ClawdHub install path that uses adm-zip | Confirmed extractAllTo is used with the documented { ok, installed, skipped, error } contract; SKILL.md validation invariant preserved; "Temporary directories are cleaned up in finally blocks" note evaluated (see finding 1) |
packages/web/vitest.config.ts + package.json test script |
Determines whether the new test is actually discovered | Default vitest include (**/*.test.js) picks up server/lib/skills-catalog/clawdhub/install.test.js under vitest run; matches the established packages/web/server/**/*.test.js convention |
No other project skills apply: the diff touches no UI, CLI, sync, relay, Electron, settings, theme/locale, or sortable-interaction code, and the ClawdHub API client is only mocked in the test, not modified. ui-api-decoupling (server API routes/data access) was considered and does not govern this change because the diff neither adds nor alters API routes, runtime fetch, or SDK access.
Findings
- non-blocker: skipped installs leave a temp directory behind (pre-existing, adjacent to the new test)
File:packages/web/server/lib/skills-catalog/clawdhub/install.js:214-217
Problem: when extraction succeeds butSKILL.mdis missing, the code pushes toskippedandcontinues withoutsafeRm(tempDir), soclawdhub-<slug>-XXXXdirectories (with their extracted contents) accumulate inos.tmpdir()on every skip. The new test 2 ("skips a package whose extracted contents lack SKILL.md") codifies exactly this path and asserts the skip result, so it indirectly freezes the leak in place. Pre-existing and untouched by this diff, so not blocking; the test'safterEachalso only removes the user skill dir, not stray temp dirs.
Suggested fix (optional, in a follow-up):await safeRm(tempDir)before thecontinue, or checkSKILL.mdafter extraction viazip.getEntry('SKILL.md')and skip without extracting first.
Evidence and Residual Risk
- Review evidence: the added
install.test.jsis relevant and proportionate — it builds a real ZIP with the dependency under test, drives the actualinstallSkillsFromClawdHubextraction path (mocked only at the network boundary./api.js), asserts nested-subdirectory preservation and content integrity for the new version, and covers the missing-manifest skip path. Test/file assertions were traced againstinstall.jsand are consistent. Reported validation (type-check, lint, build, dead-code, focused web tests) matches the change-discipline matrix for a dependency/lockfile change; CI execution status was not inspected per review policy. The upstream[email protected]package carries its own regression tests for the CVE (e.g.,test/decompress_bomb.test.js), so the new test not reproducing the crafted-header case is acceptable. - Security/supply-chain: the change directly closes the high-severity DoS advisory for both declaring packages and the single lockfile resolution;
@types/[email protected]is retained but inert (no TS file importsadm-zip, and the web tsconfig does not type-checkserver/). No new dependencies, no auth/token/network behavior changes, and the remote-ZIP extraction path's path-traversal protection is unchanged (sanitize/canonical present in both versions). - Residual risk: I could not execute the tests, audit, or builds in this environment; the test file is also not covered by the web package's type-check/lint scope (
server/excluded), so any type-level drift in it would only surface at test runtime.[email protected]'s other listed fixes (data-descriptor acceptance, dir-permission restoration, proto-injection hardening) only widen accepted-input tolerance and are not expected to regress this call site.
|
Good one. |
…command feature parity (#2777) * fix(sync): route directory-less todo updates * fix(settings): persist collapsed message preference * Defer OpenCode restarts for config mutations Co-authored-by: Serhii Dziupin <[email protected]> * Accumulate OpenCode settings restarts behind Apply & Restart Defer OpenCode reloads after settings mutations, track pending changes, and expose a top-right Apply & Restart OpenCode action with a counter so sessions stay available until the user explicitly applies. Co-authored-by: Serhii Dziupin <[email protected]> * Improve pending OpenCode restart counter contrast Place a high-contrast count badge first on the Apply & Restart action so pending settings changes remain obvious in the Settings chrome. Co-authored-by: Serhii Dziupin <[email protected]> * Move Apply & Restart into Settings footer with confirm Replace the top-right pending CTA with a primary footer action that replaces Reload OpenCode when changes are pending, and warn that running chats will stop unless the user opts out of the confirmation. Co-authored-by: Serhii Dziupin <[email protected]> * Shorten Apply & Restart action label Drop OpenCode from the Settings footer apply button and confirm title so the action fits the narrow nav and stays concise. Co-authored-by: Serhii Dziupin <[email protected]> * Restyle Apply & Restart confirm dialog actions Center nearly full-width Cancel and Apply buttons and move the don't-show-again checkbox below them for a clearer decision layout. Co-authored-by: Serhii Dziupin <[email protected]> * Make Apply & Restart confirm actions nearly full-width. Keep Cancel and Apply & Restart at about 40% each and centered, with the don't-show-again checkbox stacked underneath. Co-authored-by: Serhii Dziupin <[email protected]> * Fix Apply & Restart confirm dialog vertical stack. Stop using DialogFooter (sm:flex-row) so layout is text, then centered ~40% action buttons, then the don't-show-again checkbox. Co-authored-by: Serhii Dziupin <[email protected]> * Enforce equal 40% widths for confirm dialog actions. Wrap Cancel and Apply & Restart in fixed-percentage containers so Button content sizing cannot skew the centered pair. Co-authored-by: Serhii Dziupin <[email protected]> * Force confirm dialog action widths with inline 40%. Keep text → buttons → checkbox stack and avoid DialogFooter row layout; use inline width so the pair stays nearly full-width. Co-authored-by: Serhii Dziupin <[email protected]> * Fix deferred restart review findings. Drop phantom pending records when provider disconnect removes nothing and when Behavior AGENTS.md payloads are not deferred. Clear the Apply badge after external OpenCode manual-restart guidance. Align skills and commands save toasts with deferred restart feedback, and fix French OpenCode elision. Co-authored-by: Serhii Dziupin <[email protected]> * Add German pendingRestart locale keys after main merge. de.settings.ts came from main without Apply & Restart strings; add translations so i18n key parity stays intact. Co-authored-by: Serhii Dziupin <[email protected]> * Document deferred OpenCode restart contract and drop dead helper. Add config-mutation-response.js to owning docs, describe restartDeferred and external requiresManualRestart on /api/config/reload, and remove the unused buildConfigMutationResponse export. Co-authored-by: Serhii Dziupin <[email protected]> * Load provider OAuth methods on reconnect, not only add mode. Reconnect previously showed only the API key field because provider.auth() ran solely in add-provider mode. Also keep the original OpenCode method index when listing OAuth options. Co-authored-by: Serhii Dziupin <[email protected]> * Mirror Google API key env aliases into managed OpenCode. OpenCode can mark Google connected via GEMINI_API_KEY while the Generative AI SDK only reads GOOGLE_GENERATIVE_AI_API_KEY, so chat asked for a key that was already present. Alias unset sibling names on managed launch for web and VS Code. Co-authored-by: Serhii Dziupin <[email protected]> * feat: add restore/unarchive for archived sessions Archived sessions had no way back to the active list: the only available action was "Delete permanently". Add restore per session (sidebar context menu, Archive page row) and in bulk (sidebar selection bar). The OpenCode server cannot clear time.archived over HTTP — session.update only applies the field for a finite number, so an omitted key is a no-op and null is silently ignored (verified against opencode 1.18.12). Restore therefore writes time.archived = 0: every client-side reader classifies archive state by truthiness, so 0 reads as active in the UI, the event reducer, and the OpenCode app/TUI. The server's time_archived IS NULL list filter still excludes such rows, so the global session cache no longer issues an archived:false request for its active list. Full and per-directory loads now fetch once with the inclusive flag and split client-side via splitGlobalSessionsByArchived, which also halves per-directory refresh requests. Directory bootstrap keeps the server filter because live child stores must not hold archived sessions; a restored session re-enters its live store through the authoritative session.updated event. unarchiveSession/unarchiveSessions follow the archiveSession contract: wait for server confirmation before reconciling stores, runtime-guard every reconciliation, preserve partial batch results, and fail loudly when the server keeps the session archived instead of toasting a successful no-op. Closes #2346 * feat: add german translations for session restore * fix(walkthrough): block unauthenticated providers with a friendly refusal When the walkthrough small model resolves to a provider with no usable login, readiness was still ready and generate returned a raw 500 message. Refuse up front with no-provider-login and surface a blocker instead. Closes #2607 Co-authored-by: Serhii Dziupin <[email protected]> * fix(walkthrough): hide unauthenticated models and disable Generate Do not present a provider without a login as the selected walkthrough model, and grey out Generate when readiness is false instead of showing a login-error blocker or raw auth banner. Co-authored-by: Serhii Dziupin <[email protected]> * fix(providers): hide API key form for OAuth-only providers Only show the API key credential UI when a provider declares API auth (or auth methods are still unknown). OAuth-only plugin providers such as Cursor now show Connect/OAuth only, load auth methods on reconnect, and skip an empty models section until models are discovered. Co-authored-by: Serhii Dziupin <[email protected]> * fix(walkthrough): keep unauthenticated models out of the picker Treat an empty allowedProviderIds list as allow-none, disable Generate when no usable model is selected, and mute the button styling so it reads as unavailable rather than actionable. Co-authored-by: Serhii Dziupin <[email protected]> * fix(providers): hide models until credentials exist For OAuth-only providers like Cursor, open the auth panel when credentials are missing and omit the models list until auth/env credentials are present so placeholder catalog entries are not shown before login. Co-authored-by: Serhii Dziupin <[email protected]> * fix(walkthrough): drop info tint from disabled Generate button The status-info classes were winning over muted disabled styles, so the button still looked actionable when no model was selected. Co-authored-by: Serhii Dziupin <[email protected]> * fix(ui): keep manual model override after delegated subtask completes Synthetic subagent-completion nudges were treated as the latest user model choice and rehydrated the agent default, while setAgent preferred the agent pin over the session override. Skip synthetic prompts for restore, preserve manual selection-store overrides, and prefer session agent models in setAgent. Closes #2404 Co-authored-by: Serhii Dziupin <[email protected]> * 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. * fix(walkthrough): name an outdated server instead of failing to parse its HTML A server without these routes does not answer 404 with JSON. The unmatched /api path reaches the OpenCode proxy, and OpenCode serves its embedded web UI for anything it does not recognise — HTML, status 200 — so a client newer than its server parsed a web page as JSON and put "Unexpected token '<', "<!doctype" in the panel, naming neither the cause nor the remedy. The client now checks the content type before parsing. A non-JSON answer on 2xx or 404 blocks with "this server is older than the app, update it and refresh". A non-JSON 5xx keeps its own failure: a server that answered badly is not a server missing the feature, and sending that user to upgrade chases the wrong thing. * fix(providers): complete OAuth logins that finish in the browser OpenCode's authorize response reports how the client must finish: `code` expects a pasted code, while `auto` requires the client to call oauth/callback immediately and hold it open — upstream blocks in there polling for the device code or waiting on its loopback redirect, and only that call persists the credential. Every auth plugin OpenCode ships uses `auto`; none use `code`. The page implemented only `code`. It opened the browser, showed a paste field no provider can fill, and never called back, so a successful sign-in stored nothing and the app sat unchanged. Authorization now drives the UI: `auto` chains straight into the callback behind a waiting state with a cancel, and the paste field appears only when a provider actually asks for a code. Two smaller failures shared that surface. Prompts were never collected, which put GitHub Copilot Enterprise out of reach entirely, so a method that declares them now asks first and passes the answers to authorize. Device codes are also recovered from the instructions text, where they actually live — the old code read fields the API does not return, so the copy button never appeared. The callback is exempt from the ordinary proxy deadline and gets a 15-minute budget, bounded by the shortest upstream expiry we know of. A human sign-in with 2FA does not fit in four minutes, and expiring it turned a completed login into a 504. * release v1.18.1 * fix(walkthrough): use remote default branch * feat(ui): numbered context-panel surface switching with configurable prefix - Add switch_context_surface shortcut (default Cmd/Ctrl + 1..9, 0 for the 10th surface) that opens/closes/switches context panel rail surfaces by their visible order, configurable and persisted in Settings -> Shortcuts. - Show order-number badges on rail icons while the modifier is held >500ms; dismiss on release, blur, or a number press until the next press-and-hold. - Remove the legacy mod+2/3/4 (diff/terminal/git) and switch_tab_1..9 bindings so numbered surface switching goes only through the new mechanism. - Replace the help-dialog 'Switch Project' row with the surface-switch row and update the shortcuts footer/header icons to the command icon. * fix(git): resolve the base branch from the repository instead of its name Follow-up to #2629, which stopped the walkthrough from comparing against a branch that does not exist. The same guessing, and the same near-misses in how the answer was applied, were left elsewhere: - The default branch travelled as `rootBranchHint`, whose documented meaning is "the branch the project root worktree is on". It gets its own option, because a parameter that means two things is one the next caller gets wrong. - A candidate equal to the branch being compared is skipped. In a plain checkout the root hint *is* the current branch, so it won every time and produced a comparison with itself; the repository default now wins there. - The Changes and pull-request surfaces read the default branch too. A pull request opened against a branch that does not exist is a worse failure than a walkthrough that will not generate. - `hasResolvableBaseBranch` matched `origin/feature/main` for a base of `main`, passing the check and then failing the comparison it exists to prevent. - `getRangeDiff` promoted only `origin/<base>`. A base carried by any other remote stayed a bare name, which git resolves against refs/heads and nowhere else, so it failed exactly as before. - `getBranches` dropped every branch of a remote that did not answer, turning "we could not ask" into "these branches are gone" — offline, that silently removed comparisons that work fine against local remote-tracking refs. - A remote with no `remote/HEAD` is asked once with `ls-remote --symref` rather than falling back to the guess this data exists to replace. The `defaultBranches` contract was documented under the status response; it belongs to the branches response, which now has a section of its own. * feat(usage): add xAI quota reporting packages/ui/src/components/sections/usage/UsagePage.tsx\n - What: Show selected configured-provider errors in Usage settings.\n - Why: Preserve provider-specific failures when another provider succeeds.\n\npackages/ui/src/lib/quota/providers/index.ts\n - What: Register xAI in shared quota metadata.\n - Why: Expose xAI across shared usage surfaces.\n\npackages/ui/src/types/quota.ts\n - What: Add xAI to the quota provider contract.\n - Why: Allow typed xAI results across runtimes.\n\npackages/vscode/src/opencodeAuth.ts\n - What: Preserve secure auth-file persistence for refreshed xAI credentials.\n - Why: Reuse the VS Code auth boundary safely.\n\npackages/vscode/src/quotaProviders.ts\n - What: Add xAI SuperGrok quota fetching, refresh, parsing, and failure isolation.\n - Why: Provide VS Code usage parity.\n\npackages/web/server/lib/opencode/auth.js\n - What: Enforce secure auth directory, auth file, and backup permissions.\n - Why: Protect refreshed OAuth credentials.\n\npackages/web/server/lib/quota/DOCUMENTATION.md\n - What: Document the xAI quota provider.\n - Why: Keep provider ownership documentation current.\n\npackages/web/server/lib/quota/providers/index.js\n - What: Register the xAI web quota adapter.\n - Why: Expose xAI through the existing quota route.\n\npackages/web/server/lib/quota/providers/xai.js\n - What: Add SuperGrok usage fetching, inherited OAuth refresh, strict parsing, and zero/error semantics.\n - Why: Report xAI billing-period usage without new auth architecture or credentials. * perf(ui): swap the session activity spinner for a dot and a turn timer The spinner ran a CSS animation on every active row for the whole turn, repainting a composited layer at frame rate. Rows now carry a static dot — primary while running, info while unread — and the metadata slot on the right shows how long the turn has been going, updating once per second in the dot's colour. The counter is the motion the spinner used to provide, at 1 fps. Collapsed groups, folders and projects take the dot only, since one counter cannot speak for several running turns. Elapsed time is measured client-side because SessionStatus carries no timestamps, and starts are persisted so a reload resumes the same count. Two rules keep that honest. Only a liveness stamp — refreshed while a session is observed active, stamped as the page hides, and compared against the page's navigation start so a slow bootstrap is not charged to the absence — and a 90s adoption window may expire a record; a snapshot that cannot yet see a session is not evidence its turn ended. And a busy event is never read as a turn boundary, because OpenCode republishes busy at every step of the agent loop, so after a reload one of those repeats normally beats the first status snapshot. Idle and error events do end a turn, and retire the record with it. Snapshot reconciliation walks the running turns and asks whether the snapshot covers each one, rather than being handed everything it covers: only a live start can settle, so the pass scales with timing work instead of with the directory's session list, and allocates nothing per poll. Also applied to the mobile sessions sheet and session switcher. The shared duration ticker moves to hooks/ now that it has a second consumer. * fix(test): restore the issue-2039 suite by completing its session-actions mock The suite mocks `session-actions` by listing its exports one by one, and had fallen behind `unarchiveSession`/`unarchiveSessions`. `session-ui-store` imports both, so the file threw on import and ran zero tests — the draft auto-accept and canonical-worktree-directory guarantees it covers were unprotected, and the report looked almost like silence rather than a failure. * fix(cli): generate a UI password for bare --ui-password in daemon/serve mode The grand tunnel restructuring removed the CLI's auto-generated UI password, so `openchamber -d --ui-password` (no value) silently started an unauthenticated server instead of creating a password as in 1.8.1. Restore generation for an explicit --ui-password flag without a value: the password is generated before either launch path, passed to the daemon/foreground process via OPENCHAMBER_UI_PASSWORD, persisted in the instance state file, and surfaced once in human/quiet/json output. Refs OPE-216 * feat(server): validate OPENCHAMBER_OPENCODE_HOSTNAME bind hostname The env var was already read and passed to the managed OpenCode server spawn, but any non-empty string was accepted. Reject values that are not a valid IP (IPv4/IPv6, brackets allowed) or DNS-style hostname with a clear [config] error and fall back to the secure loopback default so a typo can never silently bind a non-loopback address. Refs OPE-231 * fix(sync): route question/permission replies by the request's own session directory Answering a question tool (or a permission prompt) could leave the session permanently stuck on "asking question": resolveDirectoryForBlockingRequest returned the containing child-store key, which only proves containment. For a worktree session (or any session whose record is grouped under a parent project store), the reply was addressed to the parent directory's OpenCode instance, where the pending request does not exist - the server answered QuestionNotFoundError, the local request was removed, and the trailing question-tool part stayed running with no recovery until Stop. Resolve the directory from the request's own session record (server- confirmed ownership: session.directory, then project.worktree) before falling back to the containing store key. When a reply/reject comes back not-found, also enqueue the settled-running-tool tail materialization so the tool part converges to the server's actual state instead of leaving the UI stuck. Refs OPE-236 * fix(web): parse agent frontmatter as leniently as OpenCode parseMdFile now matches gray-matter (used by OpenCode) for file shapes OpenChamber previously failed to parse: frontmatter whose closing '---' sits at end-of-file without a trailing newline, a UTF-8 BOM prefix, and YAML with unquoted colons in scalar values (via the same sanitizer OpenCode applies). OpenCode parses these files, so OpenChamber must too: otherwise the whole file was treated as the prompt body and a save rewrote the existing YAML block into the body, prepending a duplicate frontmatter block. Refs OPE-178 * feat(chat): refocus composer after adding message to context After the add-to-context (context pin) action completes successfully, move focus back to the chat input so the user can keep typing immediately. Uses the existing focusChatInput helper and the requestAnimationFrame refocus pattern already used by the model/agent selectors. Fixes #2447 * feat(ui): show the changed-files count badge on the Git rail surface Replaces the plain activity dot on the context panel rail's Git button with a numeric badge of the changed-files count from the git store status, so the count is visible at a glance without opening the Git surface. Large counts cap at 99+ to keep the pill within the 36px button. The badge is reflected in the button's accessible label and the hover tooltip. Fixes #2364 * feat(sessions): show a pending-question indicator on session rows Adds a per-session pending-question badge to sidebar rows, driven by the live directory-store question state through a dedicated per-session subscription channel so unrelated streaming never re-renders rows. Collapsed parent rows roll up pending questions of hidden descendants from their owning directory stores without bootstrapping them. Question state is cloned on session delete/archive so badges clear when sessions disappear. Adds the questionChangeCallbacks sync performance counter, i18n keys for all locales, and unit tests for the subscription channel and scope selection. Fixes #2634 * fix(composer): restore Shift+Enter newline on iOS CodeMirror defers Enter on iOS (and Chrome Android): the real keydown is captured without running the keymaps and the keymaps then run against a synthetic keydown that dispatchKey builds from the key name alone, with no modifier keys. The composer's Shift+Enter thus arrived as a plain Enter, and on devices where Enter sends (iPad Safari/PWA, where the desktop layout applies) it submitted the message instead of inserting a newline. Record the real Enter keydown's shift state on the view's contentDOM and restore it onto the deferred synthetic event before the caller's onKeyDown policy runs, so Shift+Enter means newline again on every runtime. Plain Enter behavior is untouched: on iOS it still follows the same deferred path it used before this change. Fixes #2558 * fix(server): rebind message-stream upstreams after a managed OpenCode restart When the managed OpenCode process exits but a server survives on the old port (Windows: killProcessOnPort is a no-op, so the orphaned process tree keeps the port), restartOpenCode() times out waiting for the port and spawns a fresh server on a NEW port. HTTP/proxy traffic follows the new port, but the global message-stream hub's upstream SSE reader stays pinned to the old server's /global/event stream — that connection never closes — so new events never reach the UI and the chat stops updating until the app is restarted (#2638). Lifecycle now fires an optional onOpenCodeRestarted hook after a successful managed restart; index.js wires it to the new messageStreamRuntime.rebindUpstream(), which restarts the shared hub (its reader re-dials buildOpenCodeUrl → the current port) and closes directory-scoped sockets so their per-connection readers rebuild against the new port. External servers are untouched (their port cannot change). Fixes #2638 * feat(tasks): support markdown scheduled-task loops in .agents/loops Adds markdown-based scheduled-task definitions ("loops") discovered from .agents/loops/*.md (project scope, ancestor directories up to the worktree root) and ~/.agents/loops/*.md (user scope), mirroring the skills discovery pattern. File format: YAML frontmatter (name, schedule cron, enabled, model as provider/model, optional agent/timezone) plus the markdown body as the execution prompt. Discovery and parsing live in scheduled-tasks/loops.js; project-config gains reconcileLoopTasks which runs inside the project write lock on every syncProject: - identity by task name; a loop takes over a matching task, preserving its id and runtime state (markdown wins on conflict with JSON) - tasks whose loopFile is gone are unscheduled; JSON tasks are never removed - new loops are created under deterministic loop:<scope>:<name> ids - project scope shadows user scope on name collisions - malformed files are skipped with a warning and never block valid ones Runtime state stays in the project config/state store; it is never written to the markdown files. Module documentation updated with the file format and reconciliation rules. Fixes #2583 * fix(sync): finalize tool parts orphaned by an interrupted turn after settlement When a managed OpenCode process dies mid-turn (crash, health-check restart), the persisted turn never settles: the trailing assistant message has no time.completed and its tool parts stay pending/running forever — the server never finalizes them (anomalyco/opencode#19023). The existing settle-triggered tail refresh refetches the same stale records, so the UI kept running tool timers and working styling indefinitely (#2577). Now, when a session is authoritatively settled (session.idle/ session.error event, or an authoritative status snapshot lowering a previously busy session) and the trailing assistant message is still unfinished with active tool parts and no pending question/permission, the orphaned parts are finalized locally as error/"Interrupted" with an end time — the same shape OpenCode itself writes for cancelled tools. The mark is gated on an explicit idle status (absent status is "unknown", never judged), never applies while busy (including question/permission waits), and a later terminal event or refresh supersedes it while a stale running refresh cannot regress it (the reducer and materializer already preserve final statuses). Fixes #2577 * docs(server): document onOpenCodeRestarted lifecycle dependency * fix(ui): stop painting git activity dots on non-git rail surfaces The badge PR inverted showActivityDot to !== 'git', so editor/terminal/diff picked up the blue activity dot whenever git had changes. Git already shows a numeric badge; other surfaces should stay quiet. Also split the count aria/tooltip strings into singular/plural keys. Co-authored-by: Serhii Dziupin <[email protected]> * fix(ui): let Escape reach the terminal PTY from the context panel The desktop ContextPanel captured Escape on the panel aside and closed the whole pane before ghostty-web's bubble-phase listener could forward the key. Skip closing when the event target is inside the terminal so apps like Vim can leave insert mode. Co-authored-by: Serhii Dziupin <[email protected]> * fix(fs): keep list paths in requested space through symlinks Closes #2627 Listing a directory through a workspace symlink was returning realpath entry paths. The file tree then rejected nested expand toggles because those paths fall outside the workspace root. Co-authored-by: Serhii Dziupin <[email protected]> * fix(tasks): harden loop reconciliation against renames and malformed files Review fixes for the markdown loop feature: - Loop-owned tasks now adopt by loop file path, not task name, so renaming a loop (frontmatter name or UI rename) renames the task in place instead of leaving a stale duplicate that keeps running the old definition; orphan duplicates of the same file are unscheduled. - Unparseable loop files are reported to the scheduler as definition:null entries: a task whose file still exists is kept with its last good definition, and only a genuinely removed file unschedules it. Transiently malformed files (mid-edit, bad merge) no longer delete tasks or their runtime state. - Adoption preserves UI-only execution fields (goalEnabled, goalTokenBudget, permissionAutoAccept, variant) that the portable format does not define. - DELETE on a loop-sourced task now returns 400 with guidance to remove the loop file, instead of being silently undone by the next reconcile. - Loops default to enabled: false; discovery of repository content never auto-executes scheduled sessions unless the file explicitly enables them. Regression tests for each fix; DOCUMENTATION.md updated. * fix(tasks): guard loop name length and surface loop ownership in the UI Review follow-up: - Reject loop files whose frontmatter name exceeds MAX_TASK_NAME_LENGTH (80): task names are clamped at storage time, so a raw name longer than the limit could never match the stored task identity. The file is treated as malformed (definition: null) instead of creating an unreachable definition; MAX_TASK_NAME_LENGTH is now exported from project-config.js and shared with loops.js. - Surface loop-sourced tasks in the scheduled-tasks dialog: tasks carrying loopFile show a 'Managed by loop file <path>' note, and the enable toggle / edit / delete actions are disabled with an explanatory tooltip, since the file remains authoritative and would revert any such change. run-now stays available. New locale keys added to all 11 message files (i18n parity test enforces exact key sets). - ScheduledTask type gains an optional loopFile field (additive, unknown to older clients). * fix(tasks): cover syncProject wiring and allow deleting orphans after file removal Review follow-up: - runtime.test.js: add syncProject wiring tests with a real temp-dir project and real project-config runtime — asserts reconcileLoopTasks is driven with the discovered loops when the project path is known (task created, nextRunAt computed) and that plain listing is used when the path cannot be resolved (reconcile not called). - service.js: DELETE on a loop-owned task is rejected with a 400 only while its loop file still exists on disk; once the file is gone the orphan task can be deleted directly instead of waiting for the next reconcile. Tests use real temp files for both branches. - DOCUMENTATION.md: delete semantics updated accordingly. - PR description refreshed for the final HEAD (test counts, reconciliation contract, evidence wording). * 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. * fix(chat): expand shell output by default (#2492) * fix(desktop): preserve native taskbar minimize (#2494) * fix(ui): make overlay scrollbar persistent on desktop shells (#2581) PR #2520 fixed #2475 in the wrong layer (overflow classes on SettingsView.tsx wrapper divs that aren't the actual scroll containers), so the "no scrollbar in Settings" bug shipped in v1.17.2 unchanged, and the sessions feed has the same underlying issue (#2402). The actual scroll containers render through ScrollableOverlay -> OverlayScrollbar, which hides the native scrollbar and draws its own JS thumb that auto-hides ~1s after scroll activity stops, with a second CSS rule fully hiding that thumb inside Settings specifically. Fix both at the shared component/CSS layer so every consumer (Settings, sessions feed, git panels, chat, etc.) gets a persistent scrollbar on desktop shells (Electron + VS Code webview) in one change: - OverlayScrollbar.tsx: skip the auto-hide timer and show the thumb immediately on mount when content overflows, on desktop runtimes. userIntentOnly consumers (chat auto-follow scroll, reasoning blocks) keep today's intent-gated behavior unchanged. - index.css: scope the Settings-specific hide rule to non-desktop, so mobile/web keep existing behavior and desktop gets the thumb back. * fix(settings): preserve notification template edits (#2300) * fix(vscode): open notebook links in notebook editor (#2373) * fix(ui): never auto-send the message queue into a streaming turn (#2642) The queue auto-send gate treated a missing session status entry as idle, but the server's /session/status map only lists busy/retry sessions — a missed busy event leaves no entry while a turn is still streaming. On a client that missed the busy event (e.g. mobile reconnect window), queued follow-ups were dispatched into the running turn; OpenCode then merged both prompts into one model response instead of serializing them. Extract resolveQueuedSessionStatusType and mirror useSessionActivity's fallback: a trailing in-flight assistant message means the session is still busy, so the queue waits for the real idle edge. Subscribe the effect to messages so the queue drains as soon as the turn completes even when status events were missed. * fix(ui): cascade-remove extracted document attachments when parent is removed (#2432) When a document (PPTX, DOCX, XLSX, ODP, ODT, ODS) is attached, extracted slide images were left orphaned after removing the parent text entry. This adds a sourceDocumentId field to AttachedFile that links all entries from the same document extraction, and cascades removal of all entries in the group when any one is removed. Fixes #2426 Co-authored-by: chiamsun <[email protected]> * fix(deps): upgrade adm-zip to 0.6.0 to fix GHSA-xcpc-8h2w-3j85 (#2643) adm-zip <0.6.0 allows a crafted ZIP to trigger a ~4GB memory allocation (GHSA-xcpc-8h2w-3j85). Bump the dependency in the web and vscode packages to ^0.6.0. The new AdmZip(buffer) and extractAllTo(dir, overwrite) APIs are unchanged, so no call-site adaptation is needed. Add a vitest regression test for the ClawdHub install path that builds a real ZIP with adm-zip and asserts extractAllTo restores files (including nested subdirectories) into the target skill dir. * feat: add fallback parsing for pairing connection payloads in old Android WebViews (#2611) * fix(files): handle reveal launcher failures (#2490) * fix: wait for worktree bootstrap before prompt dispatch; resolve session directory for send/fork (#2708) * fix(web): update foreground systemd services safely (#2542) * fix(web): update foreground systemd services safely * clarify desktop and remote updates * Revert "clarify desktop and remote updates" This reverts commit aaf4516. * test-web-systemd-updates * feat(ui,server): surface active instance service URLs in About settings (#2669) Show the running instance's local server URL and tunnel URL (when a tunnel is active) as labeled, click-to-open buttons on the About page. /api/system/info now reports the instance port and tunnel URL, resolved lazily from the tunnel runtime so each Git-worktree instance identifies itself in the UI without parsing terminal output. Refs OPE-194 * fix(chat): pin existing-session sends to captured target (#2424) * fix(chat): pin sends to captured session * fix(chat): handle runtime cancellation consistently * fix(git): run post-checkout hook after worktree creation bootstrap (#2721) Worktrees are created with `git worktree add --no-checkout` and populated with `git reset --hard`, neither of which runs git's post-checkout hook. Invoke the hook explicitly after population with git's standard arguments (null ref previous HEAD, checked-out HEAD, flag 1) and the worktree as cwd, mirroring `git worktree add` without --no-checkout. A missing or non-executable hook is skipped (matching git) and a failing hook is logged as a warning, never failing worktree creation or session bootstrap. Applied to both the web server git service and the VS Code runtime's git service. * fix(desktop): recover from macOS directory permission failures * fix(ui): use readable permission error color * fix(ui): repair sync test contracts * perf: cut cold-start download 58% and startup heap 22% via measured chunk-graph fixes (#2742) * fix(ui): update session-switch-resync test to current handleEvent/setSessionTodos signatures Co-authored-by: Serhii Dziupin <[email protected]> * perf(build): split Shiki grammars/themes, CodeMirror legacy modes, and @pierre/diffs into on-demand chunks Merging @shikijs/langs into one manual vendor chunk made the first language request download every grammar (7.4 MB raw / 1 MB gzip). Letting Rollup split these packages per dynamically imported module downloads only the languages, themes, and modes actually used — matching how the worker build already behaves. @pierre/diffs is split the same way so its pure patch parser (used by the eager tool renderer) no longer drags the Shiki-importing render stack into the startup graph. Co-authored-by: Serhii Dziupin <[email protected]> * perf(ui): restore lazy heavy views and stop eager settings-graph loading - MainLayout: DiffView/FilesView/GitView/PlanView return to lazyWithChunkRecovery (they were silently made static in c545b51 while their Suspense wrappers remained), keeping the CodeMirror and @pierre/diffs stacks out of startup. - ContextPanel: same lazy treatment for its Diff/Files/Git/Plan/Walkthrough tabs, with null Suspense fallbacks. - CommandPalette imported getSettingsNavIcon from SettingsView, statically pulling the entire settings surface (SkillsPage -> CodeMirrorEditor -> vim mode, theme registry -> @pierre/diffs) into the eager graph; the helper now lives in lib/settings/metadata. - The windowed SettingsWindow mounts only after its first open: rendering the lazy component closed made React fetch the SettingsView chunk graph at startup. Co-authored-by: Serhii Dziupin <[email protected]> * perf(ui): keep @pierre/diffs + Shiki out of the eager chat graph and defer diff worker warmup - DiffWorkerProvider no longer statically imports @pierre/diffs/worker or the theme registry, and no longer spawns 3 workers plus a main-thread shared highlighter during mount. Pools are created on demand through a dynamic module load, warmed via requestIdleCallback after startup settles, and useWorkerPool notifies consumers when a pool becomes available. - ToolPart's rich diff preview moves to lazily loaded ToolPartDiffPreview; the plain-text patch (PlainDiffFallback) renders while the chunk loads, mirroring the existing error fallback. Theme registration happens during render inside the lazy module so PatchDiff never renders unregistered ids. - ChatInput mounts its lazy ToolOutputDialog only after the first attachment preview opens instead of fetching the dialog chunk on the draft screen. - getMarkdownSyntaxVars moves to a pierre-free markdownSyntaxVars module so eager code-rendering consumers stop importing the registration module. Co-authored-by: Serhii Dziupin <[email protected]> * perf(web): load ghostty-web and Nerd Fonts on first terminal use - ghostty-web (638 KB raw JS + WASM VT) is dynamically imported when a terminal actually mounts; TerminalView stays eagerly importable for the bottom dock. - The ~2 MB of CDN Nerd Fonts are no longer preloaded and force-loaded on every cold start. index.html exposes an idempotent __openchamberEnsureNerdFonts hook; TerminalViewport requests it on mount and waits up to 2s so a cached font is in place before the glyph atlas is built, while a cold CDN fetch never blocks the terminal. Runtimes without the hook (VS Code, mobile) resolve immediately, matching their existing fallback-font behavior. Co-authored-by: Serhii Dziupin <[email protected]> --------- Co-authored-by: Serhii Dziupin <[email protected]> * fix(terminal): keep default terminal tab names unique after closing tabs (#2718) (#2731) * fix(chat): do not replay entry animations for already-seen fresh messages (#2124) (#2732) * fix(git): enable core.longpaths for worktree population (#2746) (#2747) * fix(git): enable core.longpaths for worktree population Worktrees live under a deep OpenCode data-dir path, so Windows checkouts of deeply nested repos failed bootstrap with "Filename too long". Enable Git core.longpaths before git reset --hard (web + VS Code) and surface clearer path-length guidance when the filesystem still rejects a path. Fixes #2746 Co-authored-by: Serhii Dziupin <[email protected]> * chore(vscode): keep ensureWorktreeLongpaths private Avoid an unused export in the VS Code git service; the helper stays local to populateWorktreeWithLockRecovery. Co-authored-by: Serhii Dziupin <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Serhii Dziupin <[email protected]> * perf(terminal): relax transport keepalive from 20s to 45s The ping has no pong-timeout on either side — it exists only to keep intermediaries from idling out the socket. 45s stays safely under the relay host-data idle reaper (90s) and common proxy read timeouts (60s), while halving the events that wake the relay Durable Object during an open session. * fix(settings): restore integrations nav icon after main merge Co-authored-by: Serhii Dziupin <[email protected]> * feat(telegram): require multi owner user ids and link userinfobot Fail closed when no owners are configured, reject invalid id 0, accept multiple ownerUserIds with defaultUserId back-compat, and make @userinfobot open https://web.telegram.org/k/#@userinfobot in settings. Co-authored-by: Serhii Dziupin <[email protected]> * feat(telegram): multi-group sync and Discord command/critique parity Add per-chat policies and project sync for Telegram groups with restriction messaging, a Telegram command palette matching Discord, shared /critique on|off for both messengers, and type-aware /diff copy. Co-authored-by: Serhii Dziupin <[email protected]> * feat(discord): register native /critique slash command Expose critique toggle as a Discord application command with optional mode, mark it nativeSlash in the palette, and broaden integrations search keywords for critique/commands/groups. Co-authored-by: Serhii Dziupin <[email protected]> * test(discord): expect /critique among slash commands with options Co-authored-by: Serhii Dziupin <[email protected]> --------- Co-authored-by: bashrusakh <[email protected]> Co-authored-by: Pascal André <[email protected]> Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Serhii Dziupin <[email protected]> Co-authored-by: Bohdan Triapitsyn <[email protected]> Co-authored-by: RyderAsking <[email protected]> Co-authored-by: Rajat Asthana <[email protected]> Co-authored-by: Henry Moran <[email protected]> Co-authored-by: Sérgio Pedro <[email protected]> Co-authored-by: Aman Tahiliani <[email protected]> Co-authored-by: TPOB <[email protected]> Co-authored-by: Chiam <[email protected]> Co-authored-by: chiamsun <[email protected]> Co-authored-by: Mel0ny <[email protected]> Co-authored-by: CallMeBill <[email protected]> Co-authored-by: Bruno Fantauzzi <[email protected]> Co-authored-by: Syu <[email protected]> Co-authored-by: Wsyjq <[email protected]> Co-authored-by: deatheros <[email protected]>
- bump packages/web + packages/vscode from ^0.5.16 to ^0.6.0 - add vitest regression test for ClawdHub install path exercising adm-zip - ignore /tmp/ in .gitignore Refs: upstream ebb02b4 (openchamber#2643), 7e95dfa WI: openchamber#133
Intent
adm-zip <0.6.0has a high-severity vulnerability (GHSA-xcpc-8h2w-3j85): a crafted ZIP can trigger a ~4GB memory allocation. This upgrades the pinned dependency from^0.5.16to^0.6.0in the packages that declare it, closing the advisory. No behavior change for callers.Non-goals
1.18.1).@types/adm-zip(0.6.0 ships built-in types, but dropping the type package is out of scope here).Affected surfaces
packages/web— declares and usesadm-zipatserver/lib/skills-catalog/clawdhub/install.js(new AdmZip(Buffer.from(zipBuffer)).extractAllTo(tempDir, true)).packages/vscode— declaresadm-zipas a dependency; no code reference, so runtime behavior is unaffected (declaration bump only).bun.lock— resolves[email protected] → [email protected].Repository guidance
openchamber-change-disciplineskillpackages/web/server/lib/skills-catalog/DOCUMENTATION.mdnew AdmZip(buffer)+extractAllToAPIs are unchanged in 0.6.0 (only bug fixes), so documented behavior holdsValidation
bun audit(project scope)adm-zipadvisory remainingbun run type-checkbun run lintbun run buildbun run dead-codebun run --filter '@openchamber/web' testinstall.test.js: 2 passed — builds a real ZIP with adm-zip and assertsextractAllTorestores files incl. nested subdirectories into the target skill dirpr checks(oc-review.yml) on the source branchNote: 1 pre-existing failure in
packages/web/src/api/git.test.ts(getGitRangeDiffmock) is unrelated to this change and untouched by it.Visual evidence
No user-visible change. Dependency version bump plus a backend regression test; it does not alter any rendered UI.
Risks and failure behavior
Low risk. The 0.6.0 changes to
extractAllToare bug fixes only (utimes failures now ignored per upstream #379; directory permissions restored per #530); the constructor and method signatures are identical to 0.5.16, so no call-site adaptation was required. Rollback is a simple revert of the dependency bump. The added test guards the extraction path against future regressions.