* fix(ui): update session-switch-resync test to current handleEvent/setSessionTodos signatures
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* 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 <makeittech@users.noreply.github.com>
* 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 2031e3b4 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 <makeittech@users.noreply.github.com>
* 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 <makeittech@users.noreply.github.com>
* 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 <makeittech@users.noreply.github.com>
---------
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
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.
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
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.
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 <chiamsun@users.noreply.github.com>
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.
PR #2520fixed#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.
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.
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).
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).
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.
Closesopenchamber/openchamber#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 <makeittech@users.noreply.github.com>
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 <makeittech@users.noreply.github.com>
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 <makeittech@users.noreply.github.com>
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
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
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
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