Skip to content

Latest commit

 

History

History
530 lines (274 loc) · 227 KB

File metadata and controls

530 lines (274 loc) · 227 KB

Progress

2026-07-22 20:20 · feat(composer): inline model + reasoning-effort switcher (issues #48, #40). A compact chip left of the send button opens a picker — search, All/Favorites/Recent/per-provider filters, model list (reusing the Settings flattenModelOptions/modelPreferences helpers) — as an anchored popover on desktop/wide-web and a bottom sheet on phone-width (useIsMobile). Reasoning effort (#40) rides the same surface: SDK now surfaces each model's OpenCode variants (ProviderModelInfo.variants, parsed from /config/providers, ordered none<minimal<low<medium<high<xhigh<max) and sendPrompt forwards a per-turn variant; the store keeps providers + reasoningVariant and only sends the variant when the current model actually exposes it. The picker's "Advanced" section renders a segmented effort control built from the selected model's own variants, shown only for reasoning-capable models. Runtime-verified against the bundled opencode 1.17.13: /config/providers returns variants with per-model vocabularies (e.g. deepseek-v4-flash-free → low/medium/high/max, north-mini-code-free → none/high). Shown when connected && !webReadOnly. Also fixed a pre-existing lint error (SettingsPage k literal). +9 tests (SDK variant parse/order, store variant-forward guard, ModelPicker render/interaction), 594 vitest green, tsc + lint + build clean. On branch feat/composer-model-picker (uncommitted); DMG rebuilt for on-device testing.

2026-07-22 19:55 · fix(markdown): render \(…\) / \[…\] LaTeX delimiters (issue #51). remark-math only recognizes dollar delimiters, so bracket-delimited math from models showed as raw source; MarkdownViewer now normalizes brackets to dollars before parsing, skipping fenced/inline code and \\[2pt] row breaks. 3 new tests, 585 vitest green, tsc clean.

2026-07-23 00:52 · release: v0.2.4 published — tag v0.2.4 built all 4 platforms via CI (8 installers), changelog applied (headline: #49 local-model long-chat looping fix + custom-endpoint model/context auto-detect, #50 streaming-markdown throttle), draft published as Latest. Synced the new Zenodo version DOI 10.5281/zenodo.21501479 into CITATION.cff + 7 README BibTeX blocks (concept DOI 10.5281/zenodo.21351225 stays in the badge). Replied to and closed issues #49 and #50.

2026-07-22 07:48 · perf(thread): throttle streaming-message markdown re-parse (issue #50). The live agent message grew its text on every token and re-ran the whole react-markdown + KaTeX pipeline each time — the main UI-freeze/lag source in long turns. AgentMessage now feeds its markdown through a new useThrottledValue hook (90 ms trailing), capping the parse to ~11×/s; finished messages settle in one tick and never re-render. A second change (wrapping each BlockList row in content-visibility:auto for off-screen paint skipping) was implemented but dropped after a high-effort review confirmed two regressions the containment forces: single-shot scroll restoration (useScrollMemory) lands wrong on long transcripts, and paint containment clips card box-shadows/focus rings. Model-context compression is already handled server-side (#49), so no client summarization added. 3 new throttle tests, 582 vitest green, tsc + lint + vite build clean.

2026-07-22 03:09 · feat(settings): custom endpoint form auto-detects models + context windows (follow-up to #49). New Rust probe_endpoint_models command (webview CORS can't reach local model servers) probes Ollama native (/api/tags + /api/show → exact context length), then OpenAI-compatible /models (vLLM max_model_len, OpenRouter context_length, LM Studio max_context_length), then Anthropic-compatible; results render as toggleable chips (id · 131k) that fill the model field. Optional context-window input for hand-typed ids; per-model contexts flow into the OpenCode config (form value > stored limit > 128k default). Desktop-only button (isTauri); 7 locales updated. 7 new Rust tests (incl. 2 live-HTTP fallback tests), 579 vitest green, DMG rebuilt.

2026-07-22 02:21 · fix(sdk): custom-endpoint models without a configured context limit default to 128k (issue #49 — OpenCode 1.17.13 never auto-compacts a model with limit.context === 0, so long chats on local models silently overflowed and looped). addCustomProvider writes limit: {context: 128000, output: 0} only when no limit is configured; a once-per-run connect-time backfill repairs pre-existing endpoints; hand-set limits always win (verified against the live sandboxed sidecar: add, backfill, idempotency, re-add). DMG rebuilt.

2026-07-21 20:39 · fix(web): 7 mobile/web issues — drawer now closes on any navigation incl. same-path "New" (location.key, not pathname); FilesPage single-pane on phones; logo/brand click returns to /live; vertical CJK text fixed (settings Row stacks below sm, ProjectsPage drops the 14rem Sources column below md, Beta badge removed — it kept squeezing the brand name); the POST /session 403 was a read-only gateway token — the web client now reads mode from /v1/whoami, disables the composer with a "read-only" placeholder and hides New/starters instead of failing opaquely. 579 tests green, DMG rebuilt.

2026-07-21 15:20 · release: v0.2.3 published — re-pointed the v0.2.3 tag to include the drag-drop fix (385d50e), rebuilt all 4 platforms, applied the changelog, and published the GitHub Release. Synced the new Zenodo version DOI 10.5281/zenodo.21477879 into CITATION.cff + README BibTeX (concept DOI 10.5281/zenodo.21351225 stays in the badge). README refreshed for the new features: Remote Access (CLI / LAN web / phone) + browser control, with 4 new webp screenshots (mobile home + completed run, own-Chrome harvest, remote-A100 reproduce).

2026-07-21 15:10 · fix(desktop): drag-drop file into chat no longer copies it ~150× into project root (issue #44). Root cause: Composer's native onDragDropEvent effect keyed on [onSend], which is a fresh function every LiveSessionPage render, so it re-subscribed (leaking listeners) during stream churn — one drop fired every live listener. Now subscribes once ([]) with the drop logic held in onDropRef. Also fixed the secondary dup: add_paths_to_workspace now references a file already inside the workspace by its relative path (new workspace_relative helper) instead of copying it to root as foo-1.png. typecheck/lint/ComposerAttach + 12 Rust artifact_file tests green.

2026-07-21 14:45 · release: v0.2.3 — bumped version across package.json (root + desktop), tauri.conf.json, Cargo.toml/lock, CITATION.cff (+date), README BibTeX. Headline: Remote Access gateway (drive from CLI / LAN web / phone) + this session's web/mobile fixes and the parallel browser-control fixes. Tag v0.2.3 triggers .github/workflows/build.yml → draft GitHub Release (4 platforms). Post-publish TODO: prepend changelog + publish the draft, then sync the new Zenodo version DOI into CITATION.cff + README BibTeX (concept DOI 10.5281/zenodo.21351225).

2026-07-21 14:30 · feat(web): one-click copy link with token — Remote Access URL copy now copies <url>/#token=<token> so opening it on another device connects with no manual token entry; the web client reads #token= (query ?token= fallback) on load, stores it, and scrubs it from the address bar (hash keeps it out of server-seen requests). openHint reworded across 7 locales. (Also reverted the orphan cloudflared-tunnel i18n keys — feature not built.) typecheck/lint/579 frontend green. DMG rebuilding; not yet committed (test-first).

2026-07-21 14:10 · fix(web): notebooks display + projects collapsed by default — (1) opening a .ipynb rendered nothing in the browser: NotebookEditor loaded via readArtifact (Tauri, null in web); it now reads the raw .ipynb from the gateway (/v1/fs/read, session-dir scoped) and renders cells + saved outputs read-only (execution stays desktop-only). Live-reload poll routed through the same reader. (2) Web sidebar opened with every project's sessions expanded — now projects start collapsed in the web client (once, when no saved preference; a manual toggle persists and wins later). typecheck/lint/579 frontend tests green. DMG rebuilding; not yet committed (test-first).

2026-07-21 13:45 · fix(web): artifact preview works on mobile — (a) FilePreviewInspector used readArtifact (Tauri, null in browser) for text/bytes, so .py/.md/.csv and .docx/.xlsx/.pptx showed a misleading "desktop only" note; web now fetches text + bytes from the gateway /v1/fs/read (images/PDF/video/html already worked via the served URL), + a universal open/Download action and a friendly "can't preview — download" fallback. (b) 404 on images: the gateway resolved root=workspace to the HOST's active session, but a web client views a DIFFERENT session — now the client passes the VIEWED session's dir (from SessionMeta) as ?dir= and the gateway resolves under it (sandboxed to the base workspace) with basename search (locate_under) like the desktop preview server. +2 i18n keys ×7 locales. 11 gateway rust tests + typecheck/lint/579 frontend green. DMG rebuilding; not yet committed (test-first).

2026-07-21 13:30 · feat(desktop): confirm before turning the browser window off when reusing a real Chrome login — agent-browser drives your live Chrome in place (no profile copy; verified in browser.rs + the bundled binary's --headless=new/AGENT_BROWSER_HEADED behavior), so a named profile can't truly go headless (Chrome's single-instance hands the launch to your visible window). Toggling off now pops a ConfirmDialog explaining this and pointing to the private browser for real headless; private/isolated profiles switch without a prompt. +3 i18n keys across all 7 locales; 579 tests + typecheck/lint green.

2026-07-21 13:00 · fix(desktop): browser-control "Show the browser window" toggle can now be turned off — OpenCode's config PATCH deep-merges the nested environment, so re-adding with the flag omitted left the stale AGENT_BROWSER_HEADED="true" behind (same class: profile swap, private-browser switch, cleared domain allowlist). enableBrowser now removes the entry first (waiting for the sidecar restart), then re-adds a clean config; self-heals on next apply. +2 setup-store tests; typecheck/lint/5 tests green.

2026-07-21 12:45 · fix(gateway web): token re-auth + stable port on rotation — (1) an invalid/rotated token now drops the web client back to the token gate: a window.fetch guard (installed in main.tsx before OpenCodeClient binds fetch) catches same-origin 401s → clears token + AppShell shows WebTokenGate; entering a new token reconnects. (2) Token rotation (and mode change) no longer changes the port — refactored the gateway so the running listener reads token/mode from shared state per-request (Mutex + AtomicBool); regenerate/mode-change update in place, only enable/disable or loopback↔LAN rebinds. 11 gateway rust tests + typecheck/lint/577 frontend green. DMG rebuilding.

2026-07-21 12:15 · fix(gateway web): model-set, mobile Files pane, project actions — (1) allowed the benign default-model write (PATCH /global/config {model} only; response still redacted, key/provider/auth writes still 403) so web model switching works. (2) ROOT CAUSE of "can't see workspace files" + folder button "does nothing" + messy files on mobile: RightPane was hidden lg:block (display:none < 1024px), so the Files/inspector pane never rendered on a phone — now full-screen overlay on mobile (verified the gateway /v1/fs API returns files via curl, so it was purely the hidden pane). (3) Hid host-local project actions in web (rename/remove/open-folder/pin + sidebar new/import); folder-path button disabled in web. +1 gateway test (model-only write); 11 gateway rust tests + typecheck/lint/577 frontend green. DMG rebuilding.

2026-07-21 11:40 · feat(gateway web): read-only Projects + Runs, hide local-only features, fix mobile header — verified feature-by-feature what the web client can/can't do (agent-mapped to data sources). Added read-only gateway endpoints /v1/projects, /v1/runs, /v1/runs/query, /v1/runs/log (call the existing list_projects / list_runs / query_runs_cmd / read_run_log fns) + web branches in listProjects/queryRuns/readRunLog/listRuns + refreshProjects now runs in web → projects & run history now show. Hid local-only affordances in web: Notebooks nav, Skills environment section, composer approval-mode switch; fixed FilesPage empty-state label (listing works via /v1/fs). Fixed mobile double-header (my top bar no longer stacks on pages that own a header). Green: gateway compiles, typecheck/lint/577 frontend tests. Deferred: #3 folder/Files-pane toggle on mobile (needs screenshot), model-switch (blocked by design). DMG rebuilding.

2026-07-21 11:00 · feat/fix(gateway web): model picker + mobile pass — (1) config reads (/global/config,/config/providers,/provider/*) were 403'd, breaking the web model picker ("model not set"); now proxied with API keys REDACTED (recursive strip of apiKey/secret/token/password/authorization/credential; writes + /auth still 403) so model/provider names load but keys never leave. (2) Mobile (<768px): sidebar becomes an off-canvas overlay drawer (backdrop + hamburger in a mobile top bar, auto-close on nav) instead of squeezing content — desktop untouched. (3) Web client hides desktop-only Settings sections (runtime/connectors/browser/compute/remote). +2 gateway tests (config redaction, config-path); 10 gateway rust tests + typecheck/lint/577 frontend tests green; DMG rebuilt. TODO from user feedback: top-right folder button unclickable on mobile (needs screenshot), settings mobile polish.

2026-07-21 10:35 · fix(gateway): API paths (SSE + all JSON) were served index.html — serve_asset ran before the OpenCode proxy and the Tauri asset resolver answers unknown paths with the SPA index.html fallback, so /event came back as text/html (EventSource "MIME text/html" errors) and /experimental/session etc. returned HTML (sessions/runs/notebooks blank). Now only static-looking GETs (/assets/* or known extension) hit the asset resolver; real API paths fall through to the proxy. +2 tests (looks_static, socket-blocking); 9 gateway tests green. Mobile-responsive web UI still pending (next).

2026-07-21 10:15 · fix(gateway): assets truncated / intermittent 400 in the web client — the accept loop's non-blocking listener yielded non-blocking accepted sockets (inherited on macOS/Linux), so mid-request read/write hit WouldBlock: request parse failed → 400, and a partial write of a large asset (the SPA JS bundle) → ERR_CONTENT_LENGTH_MISMATCH. Force each accepted socket back to blocking after accept(). +1 deterministic regression test (5 MiB body over the exact accept pattern arrives un-truncated); 8 gateway rust tests green; DMG rebuilt (0.2.2).

2026-07-21 10:10 · feat: Remote Access gateway (Phase 1) — new std-only Rust gateway.rs (zero new deps) turns the desktop into one bearer-token HTTP API (loopback default, LAN opt-in; sidecar stays loopback-only). It SERVES THE REAL DESKTOP SPA from the app's embedded assets and transparently proxies the OpenCode API, so a browser/phone runs the identical OpenCodeClient+UI (a window.__OS_WEB__ marker + token gate switch the same app into web mode; connect()/bootstrap()/artifactFile gained web branches) — not a re-implementation (deleted the earlier hand-rolled client). Secrets never cross the wire: /global/config,/provider,/auth → 403. Also a small /v1 contract (whoami/sessions/prompt/events/permissions/fs) for CLI/curl; full/read-only token ceiling; remote-created sessions surface locally via gateway:sessions-changedrefreshSessions(); Settings → Remote Access card in all 7 locales. RFC docs/rfc/remote-access-gateway.md. Green: 7 gateway rust tests + typecheck/lint/577 frontend tests; DMG built (0.2.2). NOT yet live-verified in a browser (see notes). CLI/tunnel/Feishu/allowlist-auto-approve/remote-Jupyter are later phases.

2026-07-21 07:38 · fix(desktop): example-session header no longer double-stacks the titlebar (added /example to AppShell.pageOwnsTitlebar; ThreadView header mirrors LiveSessionPage — fixed h-12, drag region, traffic-light clearance) + 3 new promo example sessions (browser-control web harvest, reproduce-scVI-on-A100 with provenance/review, multi-agent protein-LM survey→PDF); added on-brand SVG figure generators (browserShot/barChart/lineCurve); 577/577 tests green, DMG rebuilt.

2026-07-21 07:00 · refactor(sdk) #36 merged + roadmap reframed — landed BaseAgentRuntime base class (listener/status plumbing extracted from OpenCodeClient; verified locally: typecheck/lint clean, 18/18 sdk tests green, behavior-preserving, no scope creep) and rewrote PRD §9 roadmap around the analysis: CLI / LAN web (#3) / cloud tunnel / Feishu (#20) / ACP-server (#14) collapse into one northbound authenticated API gateway over the AgentRuntime seam (v0.4.0), and ACP-client (#14/#25/#28) / remote agent runtime / remote Jupyter (#35) are the same seam consumed southbound via pluggable transport (v0.5.0).

2026-07-21 06:15 · feat(desktop): guard message edit/revert as destructive ops — Edit and new Revert (revert-here + prefill composer, no auto-send) both go through a confirm dialog (default Cancel) warning that later messages + their file changes are dropped; verified the two file-history layers are git-isolated (our append-only shadow refs refs/openscience/snapshots/* in the workspace .git vs OpenCode's revert against an isolated git-dir under ~/Library/Application Support/opencode), so revert rolls back working-tree files but pre-revert state stays recoverable; +5 tests, 577 green, DMG rebuilt.

2026-07-21 05:40 · feat(desktop): edit past user messages — SDK gains session revert/unrevert (+ message id on history/live blocks); editMessage stops any running turn, reverts to the message (drops it + everything after, rolls back its files), and resends; user turns now render right-aligned in a content-hugging bubble with hover Copy/Edit and inline editing; +11 tests, typecheck/lint/573 tests green.

2026-07-21 02:44 · feat(#38): agent activity now visible — stream reasoning/"thinking" (was dropped in SDK), show model step count in the working line, mark the tool blocked on approval as waiting-approval in the transcript, and add a cross-session running spinner in the sidebar; +5 tests, typecheck/lint/557 tests green.

2026-07-21 01:25 · docs: v0.2.2 released (GitHub Release published, 8 installers live) + synced Zenodo v0.2.2 DOI 10.5281/zenodo.21465187 into CITATION.cff and all READMEs (concept DOI badge unchanged).

2026-07-20 03:39 · chore(release): v0.2.2 — Settings UI redesign (flat low-chrome controls), SSE render-storm fix (#34), model-switch self-heal guard + custom-provider disconnect auth (#37), privacy header fix. Tagged v0.2.2; CI (build.yml) builds the macOS/Windows/Linux installers into a draft GitHub Release. Zenodo DOI to sync after publishing.

2026-07-20 02:58 · refactor(desktop): redesign Settings UI (UI-only, no behavior change) — replaced bordered controls-inside-cards ("box-in-box") with one low-chrome language: borderless filled "well" inputs, right-aligned transparent dropdown chips (fill on hover), and soft-filled ghost buttons (inputCls.ts + btnGhost). Grouped Runtime's server/proxy/mirrors into one card of rows; flattened the surface-2 add-form panels in Providers/MCP/Compute and the ModelBrowser's inner frame + shaded sidebar (transparent filters, faint dividers, borderless rows); read-only paths (workspace, Python) are plain mono text; Appearance theme lost its segmented track and language became a chip; Privacy reworked into a two-side data-flow with tinted icons + hairline rows. +1 i18n key (runtime.serverLabel) across 7 locales. lint + tsc + vite build clean; 35 settings tests pass. (Worktree.)

2026-07-20 02:24 · fix(desktop): stop the connect-time self-heal from reverting a just-switched model (#37). Root cause of "model switch doesn't take": setDefaultModel does a masked reconnect, and connect() fires loadCatalog() UN-AWAITED — so loadCatalog's dangling-model self-heal (added for #18) runs a beat AFTER switching clears, reads the reconnecting instance's not-yet-complete provider list, judges the just-picked model "dangling" via fallbackDefaultModel, and re-points it back to an old model (repeats until the catalog warms → the multi-turn lag / "not recognized even after reboot"). The switching guard never covered this because the fired loadCatalog resolves after the flag is cleared. Fix (safe, no dispose — disposing to force a fresh list would kill the freshly-opened event stream, the exact reason setDefaultModel avoids it): remember the model the user last DELIBERATELY switched to + when (lastSwitchModel/lastSwitchAt), and skip the self-heal for that model within a 15s grace window. A genuinely-dangling model still heals once the window lapses (#18 preserved), and the send-time "model not found" error covers the gap. NOTE: the separately-observed 1-turn lag (asking right as you switch) is a benign race — a turn already dispatched can't change model — not this bug. Added a regression test (self-heal must not revert a just-switched model transiently absent from the catalog); 552 frontend tests green, tsc + eslint clean.

2026-07-20 01:39 · fix(desktop): cure the SSE tool-event render storm that froze the WebView (#34) + clear stale custom-provider auth on disconnect (#37). #34 root cause (the CPU/render half the last commit deferred): LiveSessionPage — the conversation on screen during a tool-heavy session — used a bare useRuntimeStore(), subscribing to the WHOLE store, so every SSE fold of every event for any session (background subagents included) re-rendered the entire conversation; combined with BlockList rendering all N blocks per event, that is the O(N·M) microtask/GC storm. Same trap the codebase already fixed for SettingsPage but never applied to LiveSessionPage/Sidebar/SkillsPage. Fix: (1) selector-ize those three pages; LiveSessionPage selects the ACTIVE thread on its own, so a background subagent's folds no longer touch it; (2) React.memo every block leaf (ToolRow, ToolCallRow, AgentMessage, UserMessage, DataTable, RunningJobsOverlay, StatusLine, ReviewerCard, StepSummaryRow, FigureBlock, ArtifactCard) and BlockList itself — foldEvent's [...blocks] copy preserves every unchanged block's reference, so a fold now re-renders only the one row it touched; (3) subagent activity moved into a self-subscribing SubagentActivity leaf (selector returns the activity string, re-renders only when it changes) so a child's folds never re-render the parent row. #37: disconnecting a CUSTOM provider removed only its config entry, leaving a panel-set key in the auth store keyed by the name-derived id — re-adding "only worked when the name matched exactly" as the stale key silently re-attached; now also best-effort removeProviderAuth. tsc + eslint + 551 frontend tests green.

2026-07-19 14:30 · fix(desktop): self-heal a dangling default model on connect (#18 follow-up) — a stale default model (its provider removed, its id renamed, or the config edited outside the app, e.g. kimi-for-coding/k2p7) made every send fail with ProviderModelNotFoundError: Model not found. The fallbackDefaultModel re-point existed but only ran after a Settings provider action (SettingsPage.refreshAll), which never fires while the user is just chatting. Moved the check into loadCatalog (runtime.ts) — it runs on every connect regardless of which page is mounted: now also fetches listProviders (via the concrete opencodeClient, since it's not on the AgentRuntime port), and if the persisted default is no longer in the flattened list, applies the closest surviving model and toasts settings:toast.defaultModelReset. Skips while switching (a switch owns the model) and when providers failed to load (empty ⇒ no fallback), so no false re-points; nested loadCatalog from the heal's reconnect is a no-op (valid model). tsc + eslint clean.

2026-07-20 03:20 · feat(desktop): workspace snapshots commit to dedicated PER-BRANCH refs, never a branch — commit() (git_snapshot.rs) stages into a dedicated index (GIT_INDEX_FILE=.git/openscience-index, leaving the user's .git/index untouched), writes a tree, and records it via commit-tree + update-ref refs/openscience/snapshots/<branch> — no refs/heads/*, HEAD, working-tree, or staging change ever. One snapshot chain per user branch (git-wip's refs/wip/<branch> convention; slashy names percent-encoded to dodge git D/F ref conflicts; detached HEAD → _detached bucket). First snapshot on a branch parents on that branch's tip (HEAD) so history reads continuously and diffs are meaningful; unborn branch → no parent. This lets us snapshot a repo the user brought in for the first time (branches/staged work stay byte-for-byte intact; inspect via git log refs/openscience/snapshots/<branch>); only imported plain folders (.no-snapshots) stay opt-out. Evaluated dura/git-wip/git-snapshot for reuse — all standalone daemons/scripts with their own watchers/refs, so reused the convention, not the code (~15 lines on our tested plumbing). oversize/bulk guards ported to the dedicated index (ls-files + update-index --force-remove); empty/unchanged-tree skip retained. UI provenance reads .openscience/ (not git) → backend-only. Tests assert the branch/index-untouched + per-branch guarantees; cargo test 107/107, clippy clean.

2026-07-20 02:40 · fix(desktop): workspace git snapshots no longer freeze the UI or spam commits (#32) — inline commit_best_effort on every file add ran git on the UI thread and produced one commit per file (46 commits in 47 s, window hung). Now a background debounced snapshotter (git_snapshot.rs): callers/watcher request_snapshot(root), a single "git-snapshot" thread coalesces via trailing debounce (3 s) + max-wait cap (30 s), commits off-thread. The four add_*/write_workspace_file commands enqueue instead of committing and are #[tauri::command(async)] (file IO off main thread too). Added a notify FS watcher on the active workspace (re-pointed in set_workspace, started in lib.rs setup) so external-editor/detached-process changes snapshot too, ignoring .git/ to avoid a commit loop. Existing oversize/bulk guards + empty-commit skip retained. +1 timing unit test; cargo test 106/106, clippy clean. (Done in a git worktree to avoid colliding with a concurrent agent on the main checkout.)

2026-07-19 21:12 · fix(desktop): #31 import-by-faithful-copy + sidecar log capture, hardened after high-effort review — import_project copies the picked folder into a base-dir project (sidecar only touches app-created paths, so ~/Documents no longer fails TCC/EPERM); copy preserves files+perms, .git history, and symlinks (skips FIFO/socket, rolls back on failure, drops the source's .openscience/); delete removes the whole copy via new imported_from marker; copied git repos get app files hidden via .git/info/exclude; sidecar stderr/exit now logged to debug.log; default workspace unchanged; cargo 105/105 + tsc green; Developer ID signing still deferred, real macOS TCC untested here.

2026-07-17 06:18 · fix(desktop): passive run-capture now sees through transparent launch wrappers — stripPrefixes (runs.ts, detection-only) strips a leading nohup/time/timeout <dur>/stdbuf <-flags> so the real interpreter head shows through; nohup python3 x.py & is now recorded instead of silently dropped. Original command stored verbatim (wrapper, redirects, trailing & all preserved — Reproduce re-runs it exactly); interpreter allowlist still gates, so wrapped non-runs (nohup rsync …) stay out. +2 unit tests; runs.test.ts 19/19, tsc green. (Root cause: gate matched only the command HEAD, and nohup isn't in the interpreter allowlist.)

2026-07-17 03:46 · feat(desktop): make the private/downloaded browser a first-class, always-visible choice — new "Private browser (no login)" option in the Browse-as picker (sentinel PRIVATE_BROWSER) selectable even when Chrome is installed. Picking it leaves AGENT_BROWSER_EXECUTABLE_PATH unset (agent-browser uses its own Chrome-for-Testing, never touches the user's Chrome) and surfaces the Download action (setup_browser_chrome → agent-browser install, proxy+progress); no Chrome ⇒ auto-coerced to private. enableBrowser gained useSystemChrome; config reverse-parse infers system-vs-private from executablePath. i18n browser.privateBrowser/privateNote across 7 locales (parity 13/13). tsc/eslint green; DMG rebuilt. (Before: download only appeared when no Chrome was detected, so Chrome users never saw it.)

2026-07-17 03:31 · feat(desktop): move browser control to its own reconfigurable Settings page — new "browser" section (SETTINGS_SECTIONS + nav.browser) instead of a one-shot Connectors card. Full config UI: enabled/disabled status, detected-browser row (+ download-when-none), Browse-as profile picker, Capabilities (--tools core|core,network|all), Allowed domains textarea (AGENT_BROWSER_ALLOWED_DOMAINS guardrail), show-window toggle, Enable/Apply/Disable. Current values reverse-parsed from the live browser-control MCP config; Apply re-PATCHes the same key. i18n browser.* expanded to 27 keys + nav.browser across 7 locales (parity 13/13). Verified tsc/eslint/parity; DMG rebuilt. Open: default-select first profile? finer capability multi-select?

2026-07-17 02:10 · feat(desktop): browser control via bundled agent-browser (vercel-labs v0.32.1) sidecar — registered as the browser-control local MCP (agent-browser mcp --tools core) through OpenCodeClient.addMcpServer, mirroring the science-connector pattern. New scripts/dev/fetch-agent-browser.sh (raw per-triple binaries, ~11 MB) + externalBin entry; Rust browser.rs (agent_browser_bin path resolve, agent_browser_profiles via profiles --json, detect_chrome, setup_browser_chrome download w/ proxy+progress). Settings "Browser control" card: Chrome-profile picker (reuse a copied profile, never modifies the original; isolated default), headed toggle, download-when-no-Chrome. i18n browser.* added to all 7 locales (parity test green). Reuses detected Chrome via AGENT_BROWSER_EXECUTABLE_PATH (avoids CfT download; clean cookie decrypt on macOS). Verified: cargo check, tsc, eslint, parity 13/13, and a live MCP stdio handshake (tools/list returns agent_browser_* + allowedDomains). Not yet: bundle binaries for non-macOS triples in CI/release; DMG rebuild for on-device test.

2026-07-17 00:09 · chore(release): v0.2.1 — bumped version across all six locations (root + desktop package.json, tauri.conf.json, Cargo.toml/.lock, CITATION.cff + date-released, README BibTeX). Ships since v0.2.0: Projects page (search/pin/recency/expandable sessions/delete-index), import an existing repo as a project, image upload (paste + drag-drop + paperclip), LaTeX rendering, and run-tracking/venv/model-binding fixes (#22, #23, #27). Tag v0.2.1 triggers CI (build.yml → tauri-action) to build all-platform installers into a DRAFT release; publish after prepending the changelog. Per-version Zenodo DOI stays v0.2.0's until minted on publish, then synced into CITATION.cff + README.

2026-07-16 23:49 · test(sdk): fix flaky opencode-client.node suite under parallel load — the mock server was shared across the file (beforeAll) and broadcasts turn events to ALL connected SSE clients, so under load one test's timer-driven turn streamed into another test's client and a stray session.idle satisfied its waitFor before the expected events (session.retry seen as undefined). Root cause of the last assertion was the maps time.completed test reading getMessages eagerly — the mock stores history inside the turn (on a 5ms timer), so an unsynchronized read raced it (empty history) once isolation removed the accidental cross-test leftover. Fix: per-test server (beforeEach/afterEach) for full isolation + wait for session.idle before reading history. 12/12 consecutive full-suite runs green (was ~50–100% failing that one test); no product code touched.

2026-07-16 22:18 · feat(desktop): paste & drag-drop files/images into the composer (#22 pt.2) — a clipboard image becomes a workspace file chip (pasted.png) via onPaste (finds an image/* item, guarded items ?? [], FileReader→base64) through a new std-only-base64 add_binary_to_workspace; a long-text paste still becomes pasted.txt. Drag-and-drop any files onto the window → chips: Tauri captures OS file-drops natively (DOM drop never fires), so we subscribe to its webview onDragDropEvent (absolute paths) and copy them via add_paths_to_workspace, with a drag-over highlight. All land in the draft's own folder (ensureDraftWorkspace) so the session sees them. Cross-platform: image/* clipboard + FileReader and Tauri's drag-drop event all work across macOS/Windows/Linux webviews. (Paperclip file-attach already accepted images; it just needed the draft-folder fix.) 547 FE tests (+ image-paste case, + Rust base64 round-trip), typecheck, lint, clippy pass.

2026-07-16 21:26 · fix(desktop): composer files (paste / attach) on a draft no longer orphaned — a long paste (→ pasted.txt) or the paperclip add-files wrote into whatever workspace was active at that moment, but sending a fresh draft first creates a NEW dated folder (datedWorkspaceName), so the file landed in the pre-send folder and the agent couldn't find it (repro: "Files added: pasted-1.txt" then the session's dated workspace was empty). New ensureDraftWorkspace store action materializes + pins the draft's dated folder before the write (no-op once a session exists / folder is pinned); Composer calls it before addTextToWorkspace and addFilesToWorkspace, so files and the session share one workspace. 546 FE tests, typecheck, lint pass.

2026-07-16 21:07 · feat(desktop): render LaTeX math in messages & markdown previews (#22 pt.1) — added remark-math + rehype-katex + bundled KaTeX to the shared MarkdownViewer, so $…$/$$…$$ render as math in chat bubbles and .md document previews (both variants). throwOnError:false shows bad expressions in red instead of blanking the message; a lone $5 stays literal. KaTeX fonts bundle into the offline build. 546 FE tests (incl. new KaTeX cases), typecheck, lint pass.

2026-07-16 03:22 · feat(desktop): Projects page + sidebar recency/pin + Codex-style create menu — new /projects route (ProjectsPage): search, sort-by-recency table (Name/Sources/Updated), expandable per-project Sessions (open on click), pin, inline rename, and Remove (confirm dialog). Remove deletes only the index — delete_project drops an imported project's stub or an app-created project's .openscience/project.json marker; files on disk are never deleted. New set_project_pinned/delete_project/open_project_folder commands; ProjectMeta/Info gain pinned. Clicking a project's Sources chip opens its folder in the OS file manager (via the opener crate; resolved server-side from the project id). Sidebar shows every pinned project + the 5 most-recent others (recency from newest session), with an "All projects (+N)" link and a clearly-clickable Projects heading (folder icon + chevron) → page. Surfaced session time.created/updated through SessionMeta (was dropped). New-project + is a Codex-style dropdown (Start from scratch → name modal / Use an existing folder → import). 543 FE tests, 104 Rust tests, typecheck, lint, clippy pass.

2026-07-16 01:58 · fix(desktop): record artifact versions for apply_patch writes — provenance only recorded writes whose tool exposed a path field (write/edit), but apply_patch names each file inside patchText and can touch many files per call, so deriveArtifact found no path and dropped every one → sessions editing via apply_patch had an empty .openscience/provenance.jsonl and the History panel showed "No versions recorded yet." Added parsePatchFiles (artifacts.ts) to split a patch into per-file sections and provenanceInputsFromEvent (provenance.ts) that fans an apply_patch event out to one record per add/update (delete skipped; add → full text, update → diff); runtime now dedupes per (callId, path). Verified against real session data: 22 apply_patch calls → 49 file-version records incl. 5 PROGRESS.md (was 0). Forward-only — past sessions aren't backfilled. 33 FE provenance/artifacts tests pass, typecheck clean.

2026-07-16 01:40 · feat(desktop): import an existing repo as a project (referenced in place), never auto-commit imported repos — New-project sidebar gains an "Import repo" action (pickFolder → import_project). An imported project is a lightweight pointer: a stub folder under the base dir holds project.json with sourcePath; the user's repo is not moved, not scaffolded (no harness seed), and never written a project.json. Safety: git_snapshot only ever commits into a repo it git init'd itself (marker .git/.openscience-snapshots), so a brought-in git repo is already skipped; mark_imported additionally plants a .openscience/.no-snapshots opt-out for imported non-repo folders (so a later commit never git inits them) and adds .openscience/ to the repo's local .git/info/exclude (not the tracked .gitignore). rename_project is now keyed by project id (an import's meta lives in its stub, not at its external path). ProjectInfo gains imported; imported projects show a badge. 539/539 FE tests, 103 Rust tests, typecheck, lint, clippy pass.

2026-07-15 23:18 · fix(desktop): run tracking recognizes path-form/venv Python + exact-interpreter env + old-session model rebind (#23, mona-aliye) — three independent fixes. (1) runs.ts classifies a run by the head command's executable basename (unwrap PowerShell &, strip dir prefix /+\, quotes, .exe/.cmd/.bat), so C:\proj\.venv\Scripts\python.exe, /proj/.venv/bin/python, ./.venv/bin/python, "C:\Program Files\…python.exe" all record — not only bare python. (2) provenance.rs capture_env now extracts the explicit interpreter path from the command and snapshots THAT venv's --version/pip freeze (cache keyed per-interpreter, not one global OnceLock); bare python/writes fall back to app default. (3) OpenCodeClient.sendPrompt passes the current default model {providerID,modelID} every turn so old sessions (OpenCode persists session.model at creation) follow a model switch — no SQLite rebind. 537/537 FE tests, Rust provenance/runs tests, typecheck, clippy pass.

2026-07-15 21:45 · feat(desktop): native permission-request notifications (#26, Moonerss, closes #21) — on a permission.asked event, fire a native OS notification (deduped by requestId) so a backgrounded user sees the agent is blocked; the in-app card stays the only approval surface. Adds @tauri-apps/plugin-notification + tauri-plugin-notification, notification:default capability. Fixed one regression in the PR: it dropped the tauri macos-private-api feature (breaks the vibrancy sidebar) — restored. Cross-platform build verified: CI matrix green on Windows + Linux + macOS×2 (run 29471963358). 534/534 FE tests, typecheck, lint pass.

2026-07-15 21:35 · feat(sdk): AgentRuntime interface boundary (#24, Justin Gao) — merged encyc's RFC Phase 1 (formalizes the packages/sdk seam AGENTS.md already mandates as interface AgentRuntime; OpenCodeClient implements it; store types its internal client against it while getClient() keeps the concrete OpenCode provider/MCP surface). No behavior change. Fixed one staleness: interface sendPrompt gained the agent? param plan-mode (#20) added, else typecheck failed TS2554. 530/530 FE tests, typecheck, lint pass.

2026-07-15 21:30 · feat(plan): Codex-style Plan mode (#20) — composer gains a Build/Plan switch (per-session, in-memory) that pins OpenCode's built-in read-only plan agent per prompt (PromptInput.agent; no config write/restart); plan mode tints the composer blue with its own placeholder; state syncs from user-message agent fields (new SDK message.agent event + history seed), so opencode's own plan_exit "Yes" flips the pill to Build automatically; switch hidden when the runtime lacks a plan agent. Verified on 1.17.13: agents list has plan, plan sends stamp agent:"plan", default sends resolve to build, plan turns are read-only. 530/530 FE tests, typecheck, lint pass, DMG rebuilt.

2026-07-15 19:40 · fix(ui): reopened /goal sessions showed the raw expanded command template as the "user message" — historyToThread's collapse only handled trailing-args templates; it now matches prefix+suffix around a mid-template $ARGUMENTS. And CJK-named deliverables (青云录_剧情.docx) never got a clickable artifact chip — extractArtifactRefs used ASCII \w; now Unicode \p{L}\p{N} with /u. 520/520 FE tests, typecheck, lint pass, DMG rebuilt.

2026-07-15 18:50 · fix(goal): pause→resume threw the plugin's StateDecodeError — goal_update wrote history type "pause"/"resume" but the plugin's Effect Schema only accepts "paused"/"resumed" (and status has no "blocked": the real enum is unmet/budgetLimited/usageLimited); the earlier "pause honored" pass was a false positive (the loop stopped because decode CRASHED). Fixed literals, pill now maps the real status enum (warn tone for limited states), and reads/writes self-heal poisoned history entries so broken state recovers on session open. Re-verified end-to-end on 1.17.13: pause honored (0 decode errors), resume+nudge completes the goal. 518/518 FE + 96/96 Rust, DMG rebuilt.

2026-07-15 17:40 · feat(goal): Codex-style /goal shipped — vendored @prevalentware/opencode-goal-plugin 0.1.24 as ONE esbuild-bundled file (fetch-goal-plugin.sh; opencode 1.17 ignores npm plugin specs) deployed into the app profile + injected into the config plugin array (ensure_goal_plugin, idempotent); new goal.rs reads/writes the plugin's goals.json directly so the header GoalPill (objective + live status + auto-turn count + pause/resume/clear) never costs a model turn; verified on 1.17.13: spaced-path load, external pause stops the loop race-free, resume needs a one-turn nudge (GOAL_RESUME_NUDGE wired); 517/517 FE + 95/95 Rust tests, DMG rebuilt with the plugin resource inside.

2026-07-15 15:15 · research(goal): verified @prevalentware/opencode-goal-plugin 0.1.24 (MIT, Codex goal-mode port) against our bundled opencode 1.17.13 in serve mode — full loop works: /goal registers via config hook (composer will list it automatically), evidence-gated completion with history/checkpoints, and session.idle auto-continuation (autoTurns 0→2 on a forced multi-turn goal). Caveat: 1.17.13 does NOT auto-install npm plugin specs (silent no-op) — must vendor + load via file:// path. Ready to integrate.

2026-07-14 01:20 · roadmap: triaged all 5 open issues (8 asks) into GitHub milestones — v0.2.1 patch (fixes on master), v0.3.0 Research UX (#20 #21 #22: LaTeX, image upload, plan-first, adaptive approvals, notifications), v0.4.0 Reach (#3 LAN web UI, #20 messaging, #14 ACP) — and rewrote the stale PRD §9 roadmap to match reality.

2026-07-14 00:55 · fix(ui): code was unreadable in dark — CodeViewer hardcoded highlight.js's light github.css; token colors now live in index.css as per-theme --hl-* variables (.hljs-* rules mirroring github/github-dark), so all three themes highlight correctly; 511/511 FE tests, typecheck, lint pass, DMG rebuilt.

2026-07-14 · fix(desktop): traffic lights drift on some Macs — tao re-applies the configured inset only from drawRect, which the transparent+vibrancy window can stop firing; new src-tauri/src/macos.rs re-pins the lights on Focused/Resized/ThemeChanged. fix(session): a hard reload on /live/:id showed a permanent skeleton — openSession ran before bootstrap had a client and bailed with no retry; the page effect now re-fires on connected and on the session's directory arriving. 511/511 FE tests, cargo check, typecheck, lint pass, DMG rebuilt.

2026-07-13 23:50 · release: v0.2.0 published (6 installers via CI) and the first Zenodo DOI minted — concept 10.5281/zenodo.21351225 (badge in 7 READMEs), version 10.5281/zenodo.21351226 (BibTeX + CITATION.cff).

2026-07-13 23:15 · release: v0.2.0 tagged (new UI: 3 themes, vibrancy sidebar, Codex-style settings, in-app zoom; fixes: provider retries/errors #17 #18, git snapshot bloat #19, uv/python provisioning, sidecar lifecycle) — CI builds the draft release; publish mints the first Zenodo DOI, then the DOI badge + CITATION.cff doi still need adding.

2026-07-13 22:55 · fix(ui): settings row dividers glowed white in dark — divide-border-faint is not a valid Tailwind token (color key is faint), so divide-y fell back to Tailwind's default gray-200; both usages now divide-faint; 511/511 FE tests pass, DMG rebuilt.

2026-07-13 23:40 · feat(ui): light is now the default theme; per-theme accents (light #2563eb blue, dark #4d9df6 blue on dark text, warm keeps terracotta); Settings › Appearance gains a Zoom row (−/%/+/reset wired to the in-app ZoomProvider, shortcut hint ⌘/Ctrl +−0); 511/511 FE tests, typecheck, lint pass, DMG rebuilt.

2026-07-13 23:20 · fix(ui): sidebar collapse/expand button now stays aligned with the macOS traffic lights at any Cmd/Ctrl +/- zoom — the native lights don't zoom but the strip's h-12/pl-[78px] did; app now owns zoom (ZoomProvider, zoomHotkeysEnabled: false, persisted ai4s.zoom) and exposes it as --zoom so titlebar strips counter-scale height + inset by 1/zoom (overlayTitlebarStyle); 511/511 FE tests, typecheck, lint, FE build pass.

2026-07-13 23:00 · fix(ui): root-caused the washed-out dark sidebar — setTheme was silently denied (missing core:window:allow-set-theme capability), so the vibrancy material never followed the in-app theme; capability added, tint now uses each theme's own --bg (warm stays warm, dark 80%), and the AppShell titlebar strip renders on every non-live page under the overlay titlebar so the whole content-area top is draggable (was only when the sidebar was collapsed); 511/511 FE tests, typecheck, lint pass, DMG rebuilt.

2026-07-13 22:25 · fix(ui): dark sidebar no longer washes out to gray — vibrancy translucency now theme-scoped (55% light/warm, 92% dark, [data-vibrancy][data-theme="dark"] override); all native <select> chrome replaced with a flat selectCls/.select-chrome (appearance:none + own chevron) across settings and inspector; 511/511 FE tests, typecheck, lint pass, DMG rebuilt.

2026-07-13 21:45 · feat(settings): Codex-style settings — sidebar becomes the settings navigation on /settings/* ("Back to app" on top, 7 section routes general/appearance/models/runtime/connectors/compute/privacy, collapse disabled so the exit can't vanish), pages rebuilt on flat Section/Row/Switch primitives (heading outside one bordered container, no nested cards; language grid → select, update checkboxes → switches); sidebar vibrancy opacity 80%→55% (was imperceptible over the light material); lint + 511/511 FE tests pass, DMG rebuilt.

2026-07-13 21:05 · feat(ui): three themes — new light (Codex-style clean white, neutral grays), warm (the old paper light, still the default; storage key bumped to ai4s.theme.v2 with legacy lightwarm migration), dark; settings picker + palette cycle + 7 locales updated, and on macOS the window gains a sidebar vibrancy material (tauri.macos.conf.json, macos-private-api) with the sidebar at 80% opacity so the desktop tints through, native appearance synced to the in-app theme; 511/511 FE tests, typecheck, lint pass, DMG rebuilt.

2026-07-13 20:20 · fix(session): a failing provider no longer looks like an infinite "Working…" — repro'd with an apevon relay whose key group has no channel for the model: OpenCode classifies the 5xx as retryable and retries with UNCAPPED exponential backoff, emitting only session.status {type:"retry"} events the SDK dropped, and a turn whose live session.error is missed reloads with info.error stripped by getMessages (empty reply + "done", nothing else). Now the SDK maps session.statussession.retry, the store keeps per-session retryNotices (cleared by any other sign of life), LiveSessionPage shows "Provider error — retrying (attempt N): " in the working row, getMessages carries info.error, and historyToThread renders it as a red status line (abort errors stay quiet). Mock server grew a flaky-turn mode; 511/511 FE tests, typecheck pass, DMG rebuilt.

2026-07-13 19:45 · fix(settings): resolve GitHub issues #17 & #18 — (#18) the send path never names a model, so OpenCode's global default (opencode.jsonc model) is the only source of truth, and no provider mutation ever reconciled it: editing/removing a custom provider left a dangling provider/model and every send failed with ProviderModelNotFoundError; refreshAll now re-points a dangling default at the closest surviving model (new pure fallbackDefaultModel in modelCatalog, toast on reset) and session-scoped "model not found" errors now say where the fix lives. (#17) OAuth "browser shows success but the app never updates" — refreshAll was triggered ONLY by the hanging /oauth/callback long-poll, so a lost loopback redirect (port collision, proxy) froze the UI while the token was already in the sidecar's auth.json; the wait now races the callback against a 2s credential-store watcher (new Rust provider_auth_exists reading the sidecar's auth.json) and re-checks the store before declaring failure/timeout, refreshing the provider cache when the store wins. 508/508 FE tests, 91/91 Rust tests, typecheck pass, DMG rebuilt.

2026-07-13 06:35 · fix(git): stop snapshot auto-commit from bloating .gitcommit() ran git add -A with no .gitignore and no size guard, so run-produced media/datasets/weights were committed forever (worse under per-run commits, cost ≈ runs×size). Added three defenses in git_snapshot.rs: (1) a comprehensive default .gitignore at repo init (secrets/.env, OS junk, Python/Conda/R/Node envs & caches, and bulk binary media jpg/png/wav/mp4/… — keeps .svg/.pdf; never clobbers a user's file); (2) unstage_oversized drops any single staged file ≥20 MB; (3) unstage_bulk_dirs drops any directory whose freshly-staged files sum ≥50 MB (catches thousands-of-small-files datasets that per-file misses), grouped by immediate parent so sibling source trees survive. All unstage-only (files kept on disk, logged). Thresholds from worst-case-bloat math. 90/90 Rust lib tests pass (5 git_snapshot); clippy clean for the module.

2026-07-13 05:23 · fix(mcp): harden first-run Python provisioning against the Windows "uv venv failed: Using CPython …" report — uv venv reused a broken/locked system Python; it now tries reuse first and falls back to a downloaded managed Python (new uv::create_venv, science-MCP + jupyter), and the bundled uv finally inherits the configured proxy plus new optional PyPI / Python-download mirror settings (uv_network_env; Settings › Agent runtime) since a GUI-launched uv otherwise ignored the proxy and hung on restricted networks; 87/87 Rust + SettingsPage FE tests, typecheck, lint pass (Windows-only repro — DMG verify-only on macOS).

2026-07-12 23:45 · fix(runtime): model switches no longer flash the page — OpenCode's instance rebuild closes /event ~1s after the PATCH (outside the switching mask) and the SDK self-recovers in ~250ms, but the store mirrored the transient ready→connecting flip straight into the UI; a ready→connecting flip is now held for a 2s grace window and surfaced only if the stream stays down (errors still show immediately); 504/504 tests, typecheck pass, DMG rebuilt.

2026-07-12 23:30 · fix(settings): the model picker no longer bounces back to the previous model after a switch — getDefaultModel read the instance-scoped /config, which lags the /global/config PATCH by ~1s while OpenCode rebuilds its instance, so the reconnect's loadCatalog could clobber the new selection with a stale read; now the read targets /global/config and loadCatalog leaves defaultModel alone mid-switch; 501/501 tests, typecheck pass, DMG rebuilt.

2026-07-12 08:01 · fix(settings): PR #13 review fixes on feat/model-browser-fixes — the store now owns model-switch failure state (modelSwitchError; fixes the null===null first-boot misrender, the retry that collapsed the browser, and the stale-catalog-after-URL-change write hazard), catalog gained a loading state scoped to listProviders, the providers card stays collapsible when disconnected, model rows keep focus via aria-disabled, the dead model.notSet key is live again, and the two force-added docs/superpowers/ files are removed; 500/500 tests, typecheck, lint, build pass.

2026-07-12 15:10 · fix(desktop): #12 — the app had no zoom mechanism at all, so WSLg users hit by its HiDPI scale-factor bug (fonts shrink on maximize; upstream wslg#23/#388/#1335) had no way to recover; enabled zoomHotkeysEnabled + the core:webview:allow-set-webview-zoom capability so Ctrl/Cmd +/- zoom now works on all three platforms; DMG rebuilt.

2026-07-12 11:25 · feat(settings): replaced the large native model dropdown with a searchable two-column browser (all/favorites/recent/provider filters), local favorite/recent persistence, immediate masked switching, and a separate collapsed provider-management card; full frontend tests, typecheck, lint, Windows Tauri build, and visual QA pass.

2026-07-12 01:32 · fix(runtime): serialized the complete OpenCode sidecar lifecycle behind one lock, cleared stale URLs on failed restarts, and deduplicated React StrictMode bootstrap calls so concurrent starts cannot double-spawn, overwrite the owned child, or launch dueling reconnect loops; Rust 87/87 + frontend 448/448 tests and the production build pass.

2026-07-10 21:55 · fix(mcp): #10 root cause — enabling a second Python MCP re-ran uv venv on the shared env, and uv deletes + rewrites the interpreter even with --allow-existing (verified: inode changes per run); Windows cannot replace the python.exe the first connector's running MCP server holds, so the second enable always died with "uv venv failed". Venv is now created only when the interpreter is missing (science-MCP + jupyter setup); uv pip install into the existing env verified end-to-end with the bundled uv.

2026-07-10 10:45 · feat(projects): project concept shipped — a project is a named shared-workspace folder under the base dir, marked only by .openscience/project.json (no registry/DB); sessions group under it by their directory. Sidebar gains a Projects section (collapsible groups, inline create + double-click rename, per-project new session); loose dated-folder sessions unchanged; same-project concurrency allowed and git snapshots now name the session that made them. All tests pass (447 FE + 87 Rust); DMG rebuilt.

2026-07-10 07:35 · docs(citation): repo is now academically citable — CITATION.cff + a citation section in all 7 READMEs (team authorship "The Open Science Desktop Contributors"); Zenodo↔GitHub archiving enabled, first DOI mints on the next release (then: add the badge + doi field, and sync the cff version each release). Sibling repo ai4s-skills shipped v0.1.0 the same way and already has DOI 10.5281/zenodo.21297455.

2026-07-10 04:04 · release: v0.1.9 published (network-proxy setting, connection self-heal, #6 #7 #8 #9 fixes) — all 8 platform installers built by CI.

2026-07-10 03:35 · fix(runtime): #9 root cause found by live probe — the browser delivers the xAI OAuth code fine; the sidecar's token exchange to auth.x.ai hangs because GUI-launched children inherit no proxy config. Added a network-proxy setting (follow system via scutil / custom URL / direct) in Settings → Agent runtime, injected as HTTP(S)_PROXY env at sidecar spawn with NO_PROXY for loopback; system+none apply on select via the masked-restart flow.

2026-07-10 01:40 · fix(settings): OAuth browser-login hardening (#9) — retry the callback wait on webview network drops (WKWebView kills idle fetches ~60s, far short of xai's 5-min window; opencode's pending closure is re-invocable, verified in v1.17.13 source), never re-authorize while the same login is waiting (a second authorize makes the xai plugin close the loopback server the new attempt needs), and fix a latent oauth method-index mismatch after filtering. Provider-side token-exchange rejections (no SuperGrok, blocked auth.x.ai) remain possible and are swallowed upstream — awaiting reporter feedback.

2026-07-10 00:55 · fix(sdk): self-heal the event stream (#8) — the global-config PATCH kills /event ~2s AFTER acknowledging, past af58765's masked reconnect, and EventSource never retries a terminally closed stream; the client now reopens it itself with backoff (verified in debug.log: drop → ready in ~300ms, no manual Connect). "Old model" self-reports are a prompt-level illusion — the app never pins a model per session.

2026-07-09 23:06 · fix: force UTF-8 on the Python kernel stdio (#6, garbled Chinese output on Windows); surface the active session folder new notebooks are created in on the Notebooks page (#7).

2026-07-09 13:05 · fix(models): switching the default model now reconnects transparently (masked by switching, like setApprovalMode) instead of closing the event stream and stranding the app disconnected until a manual Connect.

2026-07-09 11:52 · fix(sessions): stopped the open-session effect firing twice (currentId dep) and made openSession bail before a duplicate reconnect, fixing connection-pool exhaustion that left later sessions stuck on the loading spinner; also unstuck the /new·/clear guard when clearing from a draft.

2026-07-09 11:50 · fix(git-snapshot): only auto-commit into app-created workspace repos (marker-gated) so a user's own repo is never touched, and serialized snapshot commits behind a process lock to avoid index.lock races.

2026-07-09 11:25 · fix(sessions): added timeouts for session list/history/recovery requests and made failed history loads render an error row instead of an endless skeleton.

2026-07-09 11:18 · fix(sessions): made session idle folding idempotent so duplicate OpenCode idle events render only one done line.

2026-07-09 11:15 · fix(sessions): reconnected the current-folder runtime before the first post-clear turn and added timeouts for hanging session creation.

2026-07-09 11:11 · fix(sessions): preserved the local /new and /clear divider state when clearing from an existing session route.

2026-07-09 11:03 · fix(runtime): added an OpenCode SSE handshake timeout so first-launch auto-connect can keep retrying instead of hanging forever.

2026-07-09 10:02 · docs(readme): added the ResearchClawBench #1 recognition line across all README languages.

2026-07-09 09:39 · feat(sessions): added local /new and /clear commands that clear chat context while keeping the same workspace folder, and added best-effort local git snapshots for new workspaces and workspace file changes.

2026-07-09 08:53 · fix(updates): moved the update card to the bottom of Settings, made failed checks display as failed instead of "up to date", and switched desktop checks to the GitHub Releases Atom feed to avoid anonymous API rate limits.

2026-07-09 08:14 · feat(updates): added 24-hour-throttled GitHub Release update checks with manual checking, dismissible Settings badge, improved language switching UI, tests, and verified web/Tauri builds.

2026-07-09 06:00 · release(v0.1.8): bumped version to 0.1.8 and tagged; CI builds macOS/Windows/Linux installers into a draft GitHub Release. Covers the full i18n rollout (7 UI languages) and Open Science Desktop rebrand since v0.1.7.

2026-07-09 05:40 · docs(brand): updated the Open Science Desktop tagline and GitHub description/topics to include Linux alongside macOS and Windows.

2026-07-09 04:02 · docs(brand): repositioned the project as Open Science Desktop in README/agent metadata, added a neutral OpenScience comparison page, and updated the GitHub repo description/topics.

2026-07-09 03:25 · chore(ignore): removed tracked docs/superpowers planning artifacts and ignored docs/superpowers/ to keep local superpowers files out of git.

2026-07-09 02:36 · docs(readme): folded language support into the capability tables, restored emoji in README tables of contents, and added the Open Science Discord invite.

2026-07-09 01:14 · docs(readme): refreshed README for v0.1.7 current capabilities and added README translations for all seven shipped UI languages.

2026-07-08 20:47 · fix(remote-compute): remote/modal run outputs are now immutable per run. Root cause: skills required recording but still fetched reruns into stable paths like results/humanoid-walk/humanoid_walk.mp4, so .openscience/remote-runs.jsonl kept distinct hashes while old bytes were overwritten. Updated remote-compute/modal-run skills to use fresh RESULT directories, and both record_run.py helpers now reject any output path already recorded by an earlier remote run. Added a dev regression test covering both helpers; red → green verified.

2026-07-08 20:28 · fix(preview): right-pane GIF/image previews no longer generate dead URLs for bare artifact names. Root cause: preview_url returned a URL without validating relative paths, so humanoid_walk.gif could become a 404 <img> URL (question mark) while the Files browser worked via an already-resolved path; relativize now validates literal paths and falls back to the existing bounded basename resolver before building the URL. Verified with a failing-then-passing regression test and full Rust lib tests (78 passed).

2026-07-08 09:45 · fix(clipboard): copy failed in the desktop app ("could not copy to the clipboard"). WKWebView's navigator.clipboard.writeText rejects even inside a user gesture. Added tauri-plugin-clipboard-manager (+ capability clipboard-manager:allow-write-text) and a shared copyText helper that uses the native clipboard in-app, navigator as browser fallback; routed the file context menu's Copy path/relative-path and Runs' Copy command through it. tsc + clippy + eslint clean.

2026-07-08 09:30 · feat(files): right-click context menu in the file explorer — Reveal in Finder/Explorer/File-manager (label adapts per OS; via opener's cross-platform reveal, macOS open -R / Windows explorer /select / Linux portal+DBus with folder fallback), Copy path (absolute), Copy relative path, Open in default app. New Rust reveal_path + absolute_path commands (sandboxed under the chosen root); shared FileContextMenu (Radix context-menu) wraps rows in both the global Files page and the session files pane; clipboard via navigator + toast feedback. tsc + clippy + eslint clean; 372 frontend tests green.

2026-07-08 08:55 · fix(remote-compute): make run recording mandatory in the skill. A casual "re-run run.sh" showed the run in NEITHER Runs view — because the agent (Kimi) did the mechanical relaunch+scp but skipped record_run.py entirely, and passive capture deliberately ignores remote (ssh) commands. Hardened both SKILLs: §4 is now "REQUIRED, every time" and the tracking step spells out that a finished run MUST be fetched AND recorded — including quick re-runs — or it's invisible. (Prompt-level; the structural fix — folding fetch+record so recording can't be skipped — is proposed pending user confirmation.) Scratchpad script back-records the orphaned run into its session.

2026-07-08 08:20 · fix(remote-compute): attach remote runs to their session. Symptom: the humanoid-walk SSH run showed in the GLOBAL Runs view (with mp4/env/code — provenance intact) but NOT in the session's Runs pane, which filters by sessionId — and the skill's record_run.py call carried none (OpenCode exposes no session-id env var to bash; the id lives only frontend-side). Fix: new mark_session command writes the active session's id to <workspace>/.openscience/session.txt; openSession calls it (it already has id+directory); the remote-compute/modal-run SKILLs now pass --session-id "$(cat .openscience/session.txt)" (empty-safe — record_run.py drops a blank id). Future remote runs attach; a scratchpad script back-attaches the existing run (stamp JSONL + drop the runs.db cache to force re-ingest). tsc + clippy + eslint clean; 372 frontend + rust lib green.

2026-07-08 07:45 · feat(preview): inline video (mp4/webm/mov/m4v) preview. A robotics-sim session (ses_0be546ef) produced humanoid_walk.mp4 — recorded correctly in the run's outputs (the SSH-provenance fix worked) but the app treated it as an opaque binary ("no preview"), so it felt lost. Added a video PreviewKind → native

2026-07-08 05:15 · fix(remote-compute): complete provenance for SSH runs. Root cause found by auditing a real session (ses_0bec2fed0ffe): record_run.py was called with only the wrapper (--code run.sh, not the real humanoid_sim.py) and one output (result.json, not trajectory.npz), no environment, and a GPU hardware string for a CPU-only job — because the SKILL template modelled exactly that. Fixed: record_run.py gains --env-file (parses a run-time manifest → structured env with a content-addressed pip-freeze lockfile under .openscience/env/, matching local runs) and warns on missing/empty code/outputs; remote-compute SKILL now has run.sh emit env.txt at run time (ambient SSH env is otherwise lost), fetch every output, and record every script + output + env + honest hardware; modal-run SKILL records every output (env stays anchored by the versioned modal.Image, no --env-file). runtime.rs injects OPENSCIENCE_APP_VERSION so the off-app helper can stamp env.app. 9 rust runs-tests green (added env round-trip); clippy clean on changed files.

2026-07-08 03:34 · Release v0.1.7 — remote compute over SSH shipped, plus cross-platform hardening (single-line probe, atomic persist), base-workspace global machine config auto-materialized into each session, ssh run-surface in Runs/Reproduce, and stale-skill prune (hpc-slurm → remote-compute). Tagged v0.1.7 → CI builds macOS/Windows/Linux installers + draft GitHub Release.

2026-07-08 01:35 · feat(remote-compute): generalized Cluster (HPC) → Remote compute — connect any SSH machine (CPU or GPU, Slurm optional), one-round-trip capability+usage probe, multi-machine compute.json (migrates legacy hpc.json), RemoteComputeCard with capability chips + expandable snapshot/queue, and a unified remote-compute agent skill that runs jobs directly (setsid+PID+exit_code) or via Slurm. Built via subagent-driven TDD (9 tasks); 72 rust + 372 frontend green.

2026-07-08 01:27 · Design: documented the App Operator protocol as a session-scoped control lease with a visible controller session, generic action registry, return-to-owner flow, busy/interruption rules, and safety boundaries in docs/APP_OPERATOR.md.

2026-07-07 23:15 · Fix (Cluster): connecting a reachable host with no Slurm ("home-3090", a plain workstation — verified over SSH it has no sbatch/squeue/scancel anywhere, no slurm pkg/service) saved the host then unconditionally read the queue → squeue not found → "Could not read the queue" error. ClusterCard now reads the queue only when Slurm is detected; a reachable-but-no-Slurm host shows an explanatory note instead of a failing queue table (doesn't lock out module-based clusters or the agent's submit-time module load). Also fixed ClusterCard's own btnAccent opacity flicker (same WKWebView compositing bug as SettingsPage). Updated the empty-queue test to assert the queue read is skipped. 373 frontend tests green.

2026-07-07 23:00 · Fix (Bug 2 root cause): Settings controls flickering in packaged WKWebView was NOT the store subscription (store is quiet at rest) — it was opacity. btnAccent/btnGhost used transition-opacity hover:opacity-90 disabled:opacity-50; CSS opacity promotes a GPU compositing layer, so hovering one button mis-repainted neighbouring disabled (opacity-50) buttons. Confirmed by user repro: hovering one Enable flickered another. Replaced opacity states with alpha bg/text COLOR (plain paint, no layer), and swapped the two -translate-y-1/2 overlay-icon centerings (transform → layer) for margin centering. Kept the per-field selector change from before. DMG rebuild for verification.

2026-07-07 21:05 · Fix: Jupyter/connector provisioning no longer breaks when you leave Settings — the setup state + progress listener lived on the SettingsPage component, so navigating to a chat unmounted them (download looked frozen; a re-click collided on the same env dir). Moved the whole flow into a new app-lifetime useSetupStore with a single app-level progress listener and a double-start guard (the uv download was never actually killed — only the UI lost it). Also fixed Settings controls flickering in the packaged WKWebView: SettingsPage subscribed to the WHOLE runtime store (useRuntimeStore() with no selector) and re-rendered on every unrelated mutation — switched to per-field selectors. 373 frontend tests green (+3 setup-store). DMG rebuild pending user verification of the WKWebView flicker.

2026-07-07 20:10 · Follow-up (v0.1.7): the "Open JupyterLab" button now also lives in the notebook editor header (conversation inspector + full page), not just the Notebooks list, and deep-links to the open file (/lab/tree/<workspace-relative path>?token=…) instead of the lab home — verified the URL returns 200 against a live lab rooted where the notebook lives. Base/cross-session notebooks (outside the lab root) still open the lab home. 370 frontend + 66 rust tests green.

2026-07-07 19:20 · Follow-up (v0.1.7): the app-managed Jupyter env is now the DEFAULT local kernel when present (order: manual override → jupyter-env → system auto-detect), so the Run button and the agent's MCP share one Python; added numpy/pandas/matplotlib to that env (~500 MB) so the unified kernel is usable out of the box; split python discovery + enriched PATH per-OS (macOS Homebrew/framework vs Linux /opt/conda·Linuxbrew vs Windows); added a one-click "Open JupyterLab" button (Notebooks page) that starts the headless server and opens it in the browser with token. 370 frontend + 66 rust tests green.

2026-07-07 18:55 · Fix (v0.1.7): notebook "Run" said "no Python found" even after the ~300 MB Jupyter setup, because the local kernel and the Jupyter MCP env were two disconnected Python worlds and Windows discovery was weak. kernel::python_bin now resolves in order (manual override in runtime/python-path.txt → system discovery → the uv jupyter-env fallback), broadened Windows candidates (ProgramData/C:\ Anaconda + versioned python.org dirs) with a Windows enriched_path (conda Library\bin on PATH so numpy loads); new python_interpreter/set_python_path commands drive a Settings "Local Python kernel" card + a notebook-header indicator. setup_jupyter/setup_science_mcp now stream live setup-progress events via uv::run_uv and abort with a readable error after 10 min of silence instead of hanging forever. 370 frontend + 66 rust tests green; DMG rebuilt.

2026-07-07 18:40 · Fix: clicking a file chip in the chat showed a broken image in Artifacts while the same file opened fine from the Files tree — locate_under returned an absolute agent-prose path (e.g. /Users/…/2026-07-07-0902/test_image.png) with only the leading slash stripped instead of root-relative, so the preview server resolved <workspace>/Users/… and 404'd; it now strip_prefixes against the canonicalized root. 370 frontend + 63 rust tests green.

2026-07-07 09:25 · v0.1.6 — Runs redesign + SQLite global index + global/session dual-view. Reworked the Runs experience on aesthetics, usability, AND scale. AESTHETICS: replaced the boxed/striped cards + green left-bar with a calm borderless "log ledger" — status is a small dot, sticky day-group labels (Today/Yesterday/…), aligned tabular meta, relative timestamps. USABILITY: search (command + output path), clickable facet chips (OK/Failed + remote surfaces, with counts) that combine (ANDed in SQL), a time filter (Anytime/24h/7d/30d), clickable outputs (open in OS), copy-command; removed the duplicate count display. SCALE (best practice): the read path is now an event-log + materialized-read-model — runs.jsonl stays the durable append log, and a GLOBAL SQLite index (<base>/.openscience/runs.db, new rusqlite bundled dep) is derived lazily by byte-watermark across ALL session folders; queries are indexed, keyset-paginated (infinite scroll, 50/page), faceted, and searched server-side — fast + low-memory at 100k+. DUAL-VIEW (matches Files/Notebooks): global Runs (sidebar, all sessions) + per-session Runs (session-header toggle, shown only when the session has runs, filtered by session_id) share one RunsView; the pane is mutually exclusive with the artifact/Files panes (new PaneState.showRuns). 370 frontend + 63 rust tests green, tsc + clippy + eslint clean.

2026-07-07 06:10 · v0.1.5 — crash fix + HPC/Modal remote back-fill. CRASH (shipped in v0.1.4, hit on first real test): the Runs view threw "undefined is not an object (d.code.length)" for any run with no captured code or no outputs — the Rust RunRecord serialized code/outputs with skip_serializing_if = Vec::is_empty, omitting empty arrays, so the frontend's r.code.length/r.outputs.length hit undefined (most runs produce no files). Fixed: code/outputs now always serialized (even empty); TS type made optional and every access guarded (tsc-enforced); survives the empty-array records v0.1.4 already wrote. REMOTE BACK-FILL: remote runs (HPC/Modal) now recorded by the driving skills, not passive capture (which no longer stamps the laptop's env/hardware on off-box runs) — a bundled record_run.py helper appends accurate records (host, jobId, remoteHardware, status, wall, fetched outputs) to .openscience/remote-runs.jsonl, which read_runs merges; both SKILL.md files gained a "Record the run" step (gather sacct NodeList/state for HPC, gpu= for Modal). Reproducibility anchor stays the spec (sbatch modules / Modal Image pins, already provenance-versioned), not enumeration. NOTE: remote path verified at unit+contract level only (no cluster/Modal to E2E test). 367 frontend + 58 rust tests green, tsc + clippy + eslint clean.

2026-07-07 05:30 · Reproducibility — high-effort review pass + hardening (v0.1.4). A workflow-backed adversarial review (16 agents) of the runs/provenance feature surfaced 8 real defects, 6 fixed: (1) parse_code_files misclassified a data file named as an argument (--out results.csv) as "code" and dropped it from outputs — now only the first source-extension file is the entry script; (2) env-var-prefixed commands (CUDA_VISIBLE_DEVICES=0 python train.py) were never recorded — command classification now strips leading VAR=val/cd prefixes and matches per &&/;/| segment; (3) HPC markers matched anywhere, so git commit -m "…sbatch…" was a false run — markers now anchor at a segment head (ssh-unwrapped), never inside quoted args; (4) failed runs stamped their partial outputs as "produced by run" — provenance linking now happens only for ok runs; (5) the mtime attribution window truncated the start to whole seconds + 2s grace — now millisecond precision; (6) record_run appended to provenance.jsonl under the wrong mutex (race w/ record_provenance) and re-read the whole store per output — replaced with a single batched link (link_run_outputs) under ProvenanceState. Deliberately NOT changed: the runtime.ts interrupt-guard .delete.has (intentional/documented, pre-existing). A second manual pass over the fix code itself found 2 more latent bugs: (7) ./run.sh / python ./train.py captured no code entry because root.join("./x") leaves a CurDir component that rel_key rejects — now strips the leading ./; (8) record_run held ProvenanceState across capture_env's one-time pip-freeze/nvidia-smi shell-out (blocking concurrent writes) — env is now captured before the locks. 365 frontend + 57 rust tests green, tsc + clippy + eslint clean.

2026-07-07 04:05 · Reproducibility P2 — the three deferred pieces. ① edit diff lineage: provenance now keeps an edit's unified diff (from the event) when the full content wasn't captured, so the History shows what changed instead of "content not captured"; extracted a shared DiffView (ProvenancePanel + ToolGroup both use it). ② standalone Runs view: new /runs route + sidebar entry (RunsPage) listing every recorded run as an expandable recipe card — command, status, duration, env + hardware, code/outputs, Reproduce + Open conversation. ③ surface capture: surfaceForCommand tags each run local/hpc/modal/jupyter; HPC (sbatch/srun, even over ssh) / Modal (modal run) / notebook-batch (papermill, nbconvert --execute) submissions are now recognized as runs even when the marker isn't the command head, recorded honestly with a surface badge and a "outputs live off-box" note (remote outputs not captured); interactive local-kernel and single Jupyter MCP cells are deliberately NOT recorded (exploration, not reproducible batch runs). 361 frontend + 55 rust tests green, tsc + eslint clean.

2026-07-07 03:45 · Reproducibility, reframed from files to runs (P1). Root-caused the user's confusion: provenance only fired on write tools, so authored source files (train.py/model.py) got versions + a meaningless "Reproduce" (re-author the code?) while the results that actually need reproducing — produced by running code — got no record at all; the "content not captured (produced by running code)" message was doubly misleading (a content-less version was an edit, whose text lives in newString ∉ CONTENT_KEYS). Reframed: a run (an execution) is the reproducibility unit. New .openscience/runs.jsonl (Rust runs.rs) captures every experiment execution passively from the bash tool.updated events the app already receives — command, code version (entry scripts hashed), env + NEW hardware capture (nvidia-smi/sysctl/proc, cached per app-run), captured log, and outputs (files whose mtime falls in the run window, bounded scan). No new agent tool, no behavior change — rejected the initial "run experiments via a special tool" design as fragile. Produced files link back via a new runId on their provenance version; Reproduce on a run drafts a real recipe (re-run the command in the recorded env, compare outputs — not re-author source). ProvenancePanel now shows "Produced by run …" + recipe Reproduce, and an honest fallback message. 353 frontend + 55 rust tests green, tsc + eslint clean. DMG rebuild pending.

2026-07-07 02:27 · Fixed blank PPTX previews. A generated deck (valid single-master, opens fine in WPS/PowerPoint) declared phantom [Content_Types].xml Override entries for slideMaster2..10 — parts that were never written. pptx-preview tried to load the missing parts and silently rendered ZERO slides (no exception, so OfficePreview's try/catch never fired — the user saw a blank pane). Root-caused by reproducing the real file through the preview path (a blank vs full render hinged entirely on those overrides; also learned jsdom's cross-realm instanceof ArrayBuffer fails, so JSZip/pptx-preview need a Uint8Array in tests). Fix: normalizePptxForPreview now also strips Content_Types overrides whose part is absent (new pure dropMissingContentTypeOverrides), in-memory only, before feeding pptx-preview — same "never break the preview" philosophy as the existing defRPr merge. 344 tests green (+3), tsc clean; DMG rebuilt.

2026-07-07 02:07 · Fixed the macOS fullscreen titlebar gap. In native fullscreen the traffic lights slide away, but overlayTitlebar = isTauri && isMac (computed independently in AppShell, LiveSessionPage, Sidebar, RightPane) stayed true, so the ~78px traffic-light inset became an empty gap — the collapse/expand buttons floated oddly indented. Added isFullscreen to the UI store, synced by one watchFullscreen listener in AppShell (Tauri getCurrentWindow().isFullscreen() + onResized), a pure trafficLightsPresent(tauri, mac, fullscreen) helper, and a shared useOverlayTitlebar() hook now used by all four components. Verified core:window:default/core:event:default already grant allow-is-fullscreen + allow-listen, so no ACL denial (unlike the earlier drag-region miss). 341 tests green (+3), tsc clean; DMG rebuilt.

2026-07-07 01:39 · Session header cleaned up + stop-button noise fixed. Header: title moved to the left as the identity anchor; the Files toggle moved to the right and restyled as a quiet ghost control (no border/fill until hover/active), and notebook chips unified to the same ghost style so the right cluster reads as one language. Bug: pressing stop mid-turn printed "Aborted / done / done / Interrupted" — two root causes: the interruptedSessions guard was armed AFTER the abort POST's await (so the server's trailing SSE burst raced in first), and it was consumed by the FIRST session.idle (an abort emits two). Fixed by arming the guard before the await and holding it across every trailing idle (.has, cleared on the next turn); added a regression test that fires the burst during the await. 338 tests green, tsc clean; DMG not yet rebuilt.

2026-07-07 00:23 · New sessions now seed an agent harness instead of an empty folder: the evolve-agent scaffold (AGENTS.md rules, KNOWLEDGE.md, knowledge/, notes/) is maintained in-repo at runtime/harness/, bundled as a Tauri resource, and copied into each new dated folder by harness::seed_harness (non-clobbering, only on new_dated_workspace — never on switching to an existing session); per-session state files reset to blank templates, CLAUDE.md symlink dropped for cross-platform safety; DMG rebuilt and verified to contain Contents/Resources/harness.

2026-07-07 00:10 · Failed tool steps no longer break out as prominent cards (they read like generated artifacts and drown the thread): failures/warnings stay quiet rows inside the group — red ✗, error output expandable — with a red "· N failed" count on the collapsed summary; only waiting-approval still renders standalone; 337 tests green, DMG rebuilt.

2026-07-06 23:55 · Session-open jank fixed: collapsed tool details now lazy-mount (closed content stays out of the DOM; opening mounts collapsed and expands next frame, closing unmounts after the 300ms transition) — a history like the training session (16×50KB webfetch outputs, 20 file contents, 12 line-per-div diffs) no longer mounts ~1MB of hidden text on click; 335 tests green, DMG rebuilt.

2026-07-06 23:32 · Codex-style tool-call UI: consecutive quiet steps fold into a one-line summary group with per-step expandable detail (shell panel, colored diff, inline file content); bash titles are de-noised commands (cd-prefix stripped, verb + subject, full command in the detail); running commands show a live stdout tail + elapsed timer — the server already streamed state.metadata.output, the SDK just never read it; folds coalesce at 250ms and \r progress bars collapse in place — 335 tests green, DMG rebuilt.

2026-07-07 05:35 · v0.1.2 published — collapsible sidebar + single-row titlebar + resizable/maximizable right pane + 48px headers, plus the provider-credential and macOS-13 fixes; CI matrix green, 6 installers attached (https://github.com/ai4s-research/open-science/releases/tag/v0.1.2).

2026-07-06 21:45 · Header polish: all header rows (session titlebar, sidebar strip, every pane header) unified at 48px (1.5× the old 32px); macOS traffic lights re-centered for the taller row via trafficLightPosition {x:13, y:22} (measured on screen: lights and icons both center at 23.75px); header icons normalized to 14px / 1.5 stroke / solid text color so they match the 12px lights' height and no longer look washed out — 319 tests green, DMG rebuilt.

2026-07-06 21:20 · Maximized pane header is now a single row with the traffic lights: PaneTitlebarInset (a 62px drag spacer rendered at the start of every pane header, macOS+maximized only) replaced the extra titlebar strip; cross-platform audit of the recent titlebar/pane work found no Windows/Linux issues — every drag region and pl-[78px] inset is gated on isTauri+Mac UA, titleBarStyle/hiddenTitle are macOS-only config keys, and allow-start-dragging is valid on all desktop platforms — 319 tests green, DMG rebuilt.

2026-07-06 21:05 · Right pane (artifacts/Files) is now resizable like the sidebar: left-edge divider drags within 360–960px (persisted, capped at 70% of the window), dragging far right snaps it closed; new maximize/restore button in every pane header covers the whole window with just the artifact; session header separators softened — draft pages have none, open sessions (and examples) use a new faint border token — 319 tests green, DMG rebuilt.

2026-07-06 20:37 · Window-drag root-caused: no capabilities file existed, and core:window:default does not include start-dragging, so every data-tauri-drag-region invoke was silently ACL-denied since day one — fixed with capabilities/default.json (core:default + allow-start-dragging, double-click-zoom now works too); drafts also lost the header title/folder chip (workspace picker now sits in the composer row right of attach) — 319 tests green, DMG rebuilt.

2026-07-06 20:20 · Session header is now a Codex-style single-row titlebar: collapsed = traffic lights → expand button → folder toggle → title in one 32px draggable row (drafts hide the Files toggle until the session folder exists), expanded = collapse button lives in the sidebar's top strip right of the lights so the toggle never moves; non-live routes keep a fallback strip — 319 tests green, DMG rebuilt.

2026-07-06 20:12 · Collapsed-sidebar titlebar overlap fixed: with the sidebar closed the page header slid under the macOS traffic lights (and the floating expand button sat on top of it) — the main pane now reserves the same h-8 drag strip the sidebar uses, with the expand button in-flow right of the lights, restoring window dragging when collapsed — 319 tests green, DMG rebuilt.

2026-07-06 20:05 · First-boot page flicker root-caused and fixed: every reconnect attempt tore down the old client, which emitted "offline" into the store, so while macOS TCC held the sidecar the page flipped between the offline card and the connecting screen ~1×/s — reconnects now unhook the status listener before close (explicit Disconnect unchanged), regression test added, 319 tests green, DMG rebuilt.

2026-07-06 19:55 · Sidebar is now collapsible (Codex-style): toggle button in the header + ⌘/Ctrl+B, floating expand button next to the traffic lights when closed, divider drags between 184–340px and snaps shut below 140px, width/state persisted — 318 tests green, DMG rebuilt.

2026-07-06 18:30 · Code review of the connector fixes surfaced 4 confirmed bugs, all fixed: instance dispose now also hits the ?directory= workspace instance (chats see new credentials), the OAuth wait captures its generation at flow start and is abortable (cancel/save/retry no longer leak or resurrect waits), a post-cancel completed login still refreshes the provider list, and every SDK error now carries the server diagnostic — 318 tests green, DMG rebuilt.

2026-07-06 17:45 · Model-connector bugs fixed against a live opencode 1.17.13 sidecar (verified E2E): the SDK disposes the server's cached instance after every credential change so new providers appear immediately, "auto" OAuth logins wait for the browser in the background (cancellable, no page-wide busy lock), and SDK errors now carry the server's diagnostic message.

2026-07-06 16:40 · macOS 12 incompatibility root-caused: the Bun-built opencode sidecar declares LC_BUILD_VERSION minos 13.0 (verified via otool on the v0.1.1 dmg), so on Monterey the app shell (minos 11.0) opens but the sidecar is killed on exec → endless "connecting" → "could not open OpenCode event stream". Fix: bundle.macOS.minimumSystemVersion=13.0 (next release refuses cleanly on old macOS) + macOS 13+ requirement documented in both READMEs and the v0.1.0/v0.1.1 release notes.

2026-07-06 16:05 · v0.1.1 PUBLISHED — Windows performance release live at https://github.com/ai4s-research/open-science/releases/tag/v0.1.1 (6 installers, all 4 CI targets green, ~13 min tag-to-release); updater tar.gz assets removed as in v0.1.0. Ships the UI-freeze / console-window / fake-python fixes below. Field verification on a real Windows machine still pending — the fix addressed root causes confirmed in code, not yet re-tested on the reporting machine.

2026-07-06 14:58 · Windows perf/UX bugs root-caused and fixed: (1) heavy Tauri commands were synchronous and ran on the WebView2 UI thread — kernel_execute blocked the whole app for the duration of every notebook cell (now async + spawn_blocking), and detect_tools/record_provenance/start_runtime/start_jupyter/file-preview commands froze startup and browsing (now #[tauri::command(async)], 17 commands total); (2) every direct std::process::Command spawn popped a black console window on Windows — new quiet_command helper sets CREATE_NO_WINDOW at all 8 spawn sites; (3) interpreter_ok/tool probe accepted the Windows Store fake python.exe alias (runs but exits non-zero), causing endless kernel respawn + window spam — both now require --version to succeed, with regression tests. 51 Rust + 314 frontend tests green, zero warnings; Windows-specific code cross-checked against x86_64-pc-windows-msvc. Needs a CI Windows build to confirm on a real Windows machine.

2026-07-06 14:57 · Product promo video shipped: videos/open-science-claude-science-alt/ contains a 28s portrait HyperFrames promo positioning Open Science as "Claude Science, but open", reusing real project showcase assets; npm run check is green (lint 0/0, validate no console errors and WCAG text pass, inspect 0 layout issues), and the rendered MP4 is renders/open-science-claude-science-alt.mp4 (28.000s, 18 MB).

2026-07-06 14:20 · v0.1.0 PUBLISHED — first public release live at https://github.com/ai4s-research/open-science/releases/tag/v0.1.0 with 6 installers (mac arm64/x64 dmg, Windows exe/msi, Linux deb/rpm), release notes incl. unsigned-build bypass instructions; updater tar.gz assets removed (no updater configured).

2026-07-06 13:45 · Release flow VERIFIED end-to-end: tag v0.1.0 → CI builds all 4 targets green (mac arm64 4m, mac x64 3m, Windows 5m, Linux 13m) → draft GitHub Release auto-created with 8 assets (dmg×2, exe, msi, deb, rpm, app.tar.gz×2); publishing = press Publish on the draft. Five CI iterations fixed real bugs: pnpm version pin conflict with packageManager; GITHUB_TOKEN needed permissions: contents.write for release creation; redundant .app upload doubled mac artifacts; Linux AppImage permanently dropped (linuxdeploy runs ldd on the Bun-built opencode sidecar, ldd exits 1, hard abort — tauri#8929) so Linux ships deb+rpm only. Remaining known gaps: no code signing (Gatekeeper/SmartScreen warnings on browser downloads) and Windows/Linux UI never eyeballed (titleBarStyle Overlay is mac-only). CI-built arm64 dmg handed over for install testing.

2026-07-06 10:30 · Release pipeline now covers Linux: fetch-opencode.sh/fetch-uv.sh accept *-unknown-linux-gnu triples (opencode ships linux tar.gz, uv ships gnu tarballs — both asset names verified against the pinned releases), CI matrix gains ubuntu-22.04 (webkit2gtk-4.1 deps) producing .deb/.rpm/.AppImage, and a v* tag push now auto-creates a draft GitHub Release with all installers attached. Blocker: zero CI runs so far — macOS arm64 is the only locally-verified platform; Windows/Linux/x64-mac remain unverified until the first tag push.

2026-07-06 03:45 · P0-5 chemistry gate upgraded to a real RDKit round-trip (the named "library round-trip vs static patterns" gap). New _rdkit_verdict(smi) → "valid"/"invalid"/None: when RDKit is installed, Chem.MolFromSmiles (which sanitizes) is the AUTHORITATIVE judge — it catches far more than the static five-bond carbon (bad ring closures, impossible aromaticity, over-valent N/O/S) AND clears molecules the heuristic would wrongly flag; when RDKit is absent (verdict None) it degrades to the existing stdlib bond-counter, so the gate still works offline with zero deps. _molecule_finding routes every SMILES literal through this precedence, replacing the direct static calls in check_chemistry (python + R paths). TDD: the RDKit integration logic is driven deterministically by patching _rdkit_verdict (invalid→flag / valid→suppress-static-FP / None→static-decides), so the unit suite needs no RDKit — 37 domain-check tests green. VERIFIED end-to-end with REAL RDKit (2026.3.3 in a throwaway uv env): it flags 5-bond nitrogen and an unclosed ring c1ccc the static path misses, and clears caffeine + aspirin. DMG rebuilding to bundle the updated skill. Gap left in P0-5: POSCAR→pymatgen validity round-trip.

2026-07-06 03:20 · P0-6 CLOSED (✅ Done) — the large-file probe is now a one-click UI action, not just an agent skill. The "too large to preview" card gained Inspect without loading: a new Rust probe_large_file command (sandbox-resolves the path via scope_root/resolve_under, finds Python via kernel::python_bin, locates the bundled skills-core/large-file/large_file_probe.py resource with a dev fallback, runs it under the enriched PATH, returns the compact JSON pointer) → the pane renders a readable fact sheet (LargeFilePointerPanel: format, size+gzipped, rows/reads/sequences/variants, read-length min/max/mean, schema chips, HDF5 datasets, sample ids, "not loaded" note) or the probe's error. So a USER — not only the agent — can introspect a file bigger than the context window in one click. TDD: Rust pure first_existing helper (+1 test, 49 total); frontend probeLargeFile bridge + PreviewError inspect flow (+2 component tests: renders the pointer, surfaces a probe error) + updated the existing too-large test — 314 JS tests, tsc, eslint green. VERIFIED end-to-end: the probe bundled in the fresh DMG, run against a real 25 MB CSV (over the preview cap), returns schema + 900k row count + head/tail sample — the exact chain the command executes. This meets P0-6's acceptance (analysis over an over-window file completes by introspection/sampling, no whole-file load); status → ✅ Done in the summary table. DMG rebuilt (02:48).

2026-07-06 02:55 · P0-5 social-science domain gate shipped (5th discipline; the last unbuilt domain-check field). Two deterministic rules in the bundled domain-check skill, distinct from the P1-6 stats-integrity skill (which checks reporting/reproducibility) — this catches CODE that computes the wrong statistic: social · multiple-comparisons (a significance test — ttest/pearsonr/f_oneway/chi2/… — run inside a loop or ≥3 times with NO multipletests/FDR/Bonferroni correction anywhere; the named silent-p-hacking failure that inflates the family-wise false-positive rate) and social · categorical (a numeric reduction .mean()/.median()/.std() taken directly on a nominal code — gender/race/region/condition/… — treating an unordered label as interval). Precision-first, matching the other four gates: a SINGLE test isn't flagged (needs no correction), a correction anywhere silences it, and a groupby('gender') KEY is correct usage and not flagged (verified adversarially — groupby+col.mean, bare groupby.mean, single corr, 2-tests-no-loop all stay silent). Reuses the same review contract + per-discipline tag → zero UI change. TDD: +7 validator tests (loop/≥3/correction-ok/single-ok; categorical mean, groupby-ok, continuous-ok); 34 domain-check tests total, all green; end-to-end run() emits both social tags on a realistic file. DMG rebuilding. P0-5 now covers all five targeted disciplines; only library round-tripping (SMILES→RDKit, POSCAR→pymatgen) remains.

2026-07-06 02:30 · P0-6 genomics + remaining physical-science large-file formats shipped (the named gap; a 90 GB FASTQ is exactly the requirement's motivating case). Extended the bundled large-file probe: FASTQ/FASTA/VCF are STDLIB and gzip-aware (a new is_gzip/stream_lines/count_lines_any layer + double-extension detection that sees through .gz, so reads.fastq.gz → FASTQ not a gzip blob) — FASTQ returns read count + read-length min/max/mean over a bounded 100k-read scan + sample ids (never full sequences), VCF returns variant count + sample names from #CHROM + contigs, FASTA returns sequence count + residues. Binary BAM/CRAM (pysam header only), GRIB (cfgrib/pygrib), ROOT (uproot, metadata) introspect via their library or degrade to an install-hint pointer, never a raw dump. VERIFIED on this host: a 500k-read .fastq.gz → 462 B pointer streamed in CONSTANT memory (whole file never loaded), a 3,000-variant VCF → samples + contigs parsed. TDD: +7 probe tests (FASTQ reads/lengths, FASTQ gzip, FASTA, VCF samples, BAM/GRIB/ROOT install-hints); 18 probe tests total, all green. Only P0-6 remainder: wiring the probe as an automatic UI pre-read step. DMG rebuilding.

2026-07-06 02:05 · P1-2 physics + earth/climate connectors shipped — the two previously-EMPTY disciplines now have a domain database, so all five targeted disciplines meet the acceptance ("≥1 non-bio DB per discipline"). Added three vetted open-source MCP servers to the curated catalog (same one-click-into-isolated-uv mechanism): spaceweather-mcp (physics — NOAA SWPC/NASA DONKI/USGS: solar wind, flares, Kp/Dst, radiation storms, aurora; 15 tools; no key), mcp-weather-server (earth — Open-Meteo weather/climate/air quality; 8 tools; no key), usgs-mcp (earth — USGS streamflow/flood/sites; 10 tools; no key). METHOD (the lesson from the earlier false-friend episode): each candidate was proven with a REAL MCP initialize + tools/list stdio handshake inside the bundled-uv env BEFORE adding — wrote a mcp_handshake.py verifier and confirmed all three init and enumerate their tools via the EXACT launch shapes connectorConfig emits (console-script and -m module), in a fresh shared env. This also DISPROVED the earlier "usgs/open-meteo don't start" doubt (that was an inadequate check — a SOCKS-proxy env var on the test box, not the servers) and re-confirmed astro-mcp is Airflow, not astronomy. TDD: +4 connector tests (physics + earth disciplines present, three launch-command shapes); 312 JS tests, tsc, eslint green. Docs: CONNECT_YOUR_TOOLS.md lists all seven connectors with disciplines. DMG rebuilding. Gap: astronomy catalogs (ADS/SIMBAD/Gaia) have no pip-installable stdio MCP — GitHub-only, would need vendoring.

2026-07-06 01:40 · P0-5 chemistry domain gate shipped (4th discipline; the requirement's named next frontier — the C&EN caffeine "five-bond carbon" class of "runs but wrong"). New chem · valence rule in the bundled domain-check skill: a stdlib SMILES bond-counter walks a string literal (assigned to a smiles/smi/mol variable, or passed to MolFromSmiles/MolFromSmarts) — tracking chain/branch/ring bonds and bond orders (=/#/aromatic) — and flags any organic-subset atom whose EXPLICIT bonds exceed its valence: carbon >4 (the five-bond carbon) or a halogen >1. Precision-first: it BAILS (no finding) on bracket atoms [..] (which carry their own valence/charge/H), unbalanced branches, open rings, or any unmodeled token, so it never guesses — and only flags elements whose valence is unambiguous. Reuses the same review contract + per-discipline tag, so zero UI change. TDD: +8 tests (5-bond carbon via literal and via MolFromSmiles arg, over-bonded F, valid caffeine + acetic acid stay silent, non-chem string ignored, bracket-atom bail, R regex fallback); 27 domain-check tests total, all green; verified end-to-end that run() emits the review block naming "C has 5 bonds (max 4)". DMG rebuilt to bundle the updated skill. Gaps: full RDKit round-trip sanitization, a social-science correctness gate.

2026-07-06 01:25 · P0-7 moderate: tool detection now searches the enriched PATH. Settings' tool list probed with the app's own environment — a Finder/Dock-launched app gets a minimal PATH, so the user's anaconda/homebrew Python, R, uv, and Jupyter showed as "missing" while the agent and kernel (which already use enriched_path()) could run them fine. probe now searches the SAME enriched PATH (pure probe_with_path + fake-tool-on-temp-PATH test). 48 Rust (+1) + 308 JS tests, tsc, eslint green. DMG rebuilt.

2026-07-06 01:15 · P0-7 moderate: sidecar restart consolidated under a lifecycle lock. The kill→spawn sequence was copy-pasted in four config-changing commands (import login / remove config entry / approval mode / configure provider) with no serialization — two concurrent restarts could double-spawn and orphan a child. Now one restart_sidecar() holds the child mutex across the whole kill→spawn (the mutex IS the lifecycle lock), and all four commands route through it (−30 lines). 47 Rust + 308 JS tests, tsc, eslint green. DMG rebuilt.

2026-07-06 01:05 · P0-7 plaintext-keys interim minimums met — the last critical safety item is now addressed (keychain-at-rest stays deferred to signed releases per P2-3's deliberate trade-off). The gap: provider/connector keys + the Jupyter token live in opencode.jsonc at mode 644 (world-readable), and the sidecar rewrites that file at runtime with its own umask, so write-site chmod alone wouldn't hold. FIX: tighten_private (unix: dirs 700, files 600; Windows %APPDATA% is per-user ACL'd) applied to the whole app-private runtime root + config file on EVERY sidecar start (repairs existing installs) and after each Rust-side config write — the 700 directory is the load-bearing guarantee (unreachable to other users whatever the sidecar sets on files inside). VERIFIED empirically: an authenticated PATCH /global/config against the real bundled binary preserves a 600 config (in-place rewrite), so both layers hold; +1 Rust test (47). With this, every P0-7 CRITICAL checkbox is done or at its documented interim bar: approval modes ✓, sidecar auth + CORS ✓, workspace-bash binding ✓, keys ✓(interim), Windows injection ✓, kernel deadlock ✓. Remaining: moderate robustness (sidecar restart dedup/liveness, async-worker blocking, detect_tools PATH) + structure cleanup. DMG rebuilt.

2026-07-06 00:55 · P0-7 Windows command injection fixed (the last critical Rust defect — both are now closed). ROOT CAUSE: open_url/os_open used cmd /C start "" <arg> on Windows; cmd re-parses &/^/| after Rust's quoting, so an agent-emitted link like https://x.com/?a=1&calc would EXECUTE calc — and every legit URL containing & broke. FIX: both functions now call the opener crate — verified in its vendored source to use ShellExecuteW on Windows (no shell re-parsing), open on macOS, xdg-open on Linux; it also wait()s the helper (the old spawn-and-forget leaked one zombie per open, even on macOS). The http(s)-only scheme gate stays and gained a regression test (javascript:/file:/bare-command rejected). Verified: opener cross-compiles clean for x86_64-pc-windows-msvc (full Windows app build stays host-bound in CI — llvm-rc + real sidecar binaries, P1-4). 46 Rust tests (+1), 308 JS, tsc, eslint green. DMG rebuilt. P0-7 critical remainder: plaintext keys at rest (deliberate P2-3 trade-off, revisit for signed releases).

2026-07-06 00:45 · P0-7 kernel deadlock fixed + notebook Stop button (the 2nd critical Rust defect; acceptance: a while True: pass cell can be reset without restarting the app). ROOT CAUSE: kernel_execute held the GLOBAL kernel-map mutex across an unbounded blocking read_line, so a hung cell wedged every kernel command including kernel_reset — only an app restart recovered. FIX (per-kernel locks): the map holds Arc<Kernel> and is locked only for lookup/insert/remove; each kernel has an io lock (one cell at a time) and an INDEPENDENT child lock (kill/reap) — reset kills without waiting on io, the blocked read sees EOF and errors out, and the error path removes only its own Arc (ptr_eq) so a respawned kernel is never clobbered. Killed kernels are now wait()ed (zombie fix, same class). kernel_reset gained notebook-precise targeting (kills exactly that notebook's kernel). UI: a running cell now shows an ALWAYS-VISIBLE Stop (was: no way to interrupt at all + Run row hidden unless hovered); Stop resets this notebook's kernel and the cell reports "Interrupted — the kernel was restarted; variables were reset" instead of a raw error. VERIFIED with a REAL hung Python kernel in a Rust test: reset returns promptly (<5s guard, actual ms), the hung exec errors, the map empties for a clean respawn. 308 JS tests (+2: Stop resets + labels interruption, crash still reports error), 45 Rust tests (+1), tsc, eslint green. DMG rebuilt. Still open in P0-7: plaintext keys at rest, Windows cmd injection.

2026-07-06 00:31 · P0-7 sidecar auth + preview-server token shipped (the audit's remaining CRITICAL network exposure; REQUIREMENTS said "fix permission block + CORS before next release" — both now closed). MECHANISM (verified in the OpenCode 1.17.13 source): --cors "*" was an exact-match literal (never a wildcard) — the real hole was the built-in CORS allowlist trusting ALL http://localhost:*/127.0.0.1:* origins, so any local webpage could drive agent turns and read API keys via the unauthenticated /global/config. FIX: (1) sidecar — OpenCode's built-in Basic auth enabled via a per-run CSPRNG password (OPENCODE_SERVER_PASSWORD, in-memory OnceLock, never on disk; new runtime_password command hands it to the webview over app-only IPC); the SDK sends Authorization on every fetch and KEEPS the reliable-in-WKWebView EventSource SSE path via the server's ?auth_token= query (same Basic payload); --cors "*" removed. (2) preview server — a per-run CSPRNG token as the URL's FIRST PATH SEGMENT (/<token>/w/…), so relative subresources in previewed HTML inherit it and the sandboxed iframe (opaque origin, no cookies) still works; tokenless/wrong-token → 403 before any filesystem work; dropped Access-Control-Allow-Origin: * (previews are iframe/img, never cross-origin fetch). (3) same-class fix: Windows Jupyter token was guessable pid+nanos — now the shared random_hex CSPRNG (getrandom). VERIFIED against the real bundled binary: no/wrong password → 401 on /global/config, /session, /event; correct → 200; ?auth_token= streams SSE. TDD throughout: 306 JS tests (+3: Basic header on API calls, auth_token on the ES URL, store passes the password), 44 Rust tests (+2: token gating + no-CORS-header, random_hex shape), tsc, eslint green. DMG rebuilt. Still open in P0-7: plaintext keys at rest, Windows cmd injection, kernel deadlock.

2026-07-06 00:20 · Approval switch polished after user testing: (1) UI restyled to the Codex layout — the composer gains a bottom action row with a "✋ Approve for me ⌄" pill bottom-left and a titled upward menu (visually verified via browser screenshots); (2) FIX: menu didn't dismiss on outside click — ROOT CAUSE: WKWebView never focuses a clicked button, so the onBlur close never fired; replaced with a document-level mousedown listener (+1 test); (3) FIX: switching modes flashed the whole page — the sidecar restart flipped status to "connecting" and the page rendered as disconnected; reused the switching flag (same mechanism as workspace moves) so a deliberate restart renders as connected (+1 test). 303 JS tests, tsc, eslint green. DMG rebuilt (20:03).

2026-07-05 23:59 · P0-7 approval modes shipped (the audit's #1 safety-defaults gap). Codex-style two-mode switch in the composer: Approve for me (DEFAULT — deletion/install/remote/privilege commands + webfetch prompt first) and Full access (OpenCode builtin defaults, explicit opt-out that survives restarts). MECHANISM (verified from OpenCode 1.17.13 source + bundled binary): permission rules evaluate last-match-wins with user config appended after the builtin "*":"allow", so the approve preset is pure ask rules — per dangerous token both "T *" (prefix; also matches bare T) and "* T *" (catches compound commands like cd x && rm -rf y without false-positives on words merely containing the token). Full access = "permission": {} (zero rules; the key's presence means "user chose", so startup seeding never overrides it). Rust: pure set_permission_mode/permission_mode_of/seed_default_permission (+6 tests) + get_approval_mode/set_approval_mode commands + first-run seeding in spawn_sidecar (compliance floor even if the UI never runs); switching restarts the sidecar via the configure_opencode flow. Frontend: store loads the mode on connect, setApprovalMode persists + reconnects (+2 store tests); composer dropdown with active-mode check (+3 component tests). VERIFIED on the real bundled binary: approve config accepted — all 86 bash ask rules land in the build agent's resolved ruleset AFTER the builtin allow; {} accepted (24 builtin rules, zero asks); a 22-case evaluation with OpenCode's verbatim wildcard/findLast algorithm classified every case as designed (rm / pip install / ssh / curl / git push / cd-&&-rm → ask; ls / python / git status / "echo confirm" → allow). 292 JS tests, 42 Rust tests, tsc, eslint green. DMG rebuilt. Still open in P0-7: --cors * unauthenticated sidecar, plaintext keys, Windows cmd injection, kernel deadlock.

2026-07-05 19:30 · fix(pptx-preview) + feat(office skills). (1) Decks styled via paragraph-level <a:pPr><a:defRPr> (valid OOXML; WPS/PowerPoint resolve it) rendered as 18px black text in pptx-preview (run-level rPr only) — invisible on dark slides. New pure lib/pptx.ts: applyParagraphDefaults merges each paragraph's defRPr into runs lacking rPr (run's own values win; covers <a:r>+<a:fld>), normalizePptxForPreview rewrites the slide XMLs in-memory before rendering. VERIFIED end-to-end on the failing deck (title: 18px black → >30px white); 4 regression tests. (2) Anthropic document skills (docx/pdf/pptx/xlsx, Apache-2.0) now bundled: fetch-skills.sh pins anthropics/skills@9d2f1ae into external/anthropic-skills/, shipped as skills-office/ resource, deployed by deploy_bundled_skills on every sidecar start. VERIFIED inside the built DMG. 287 JS (+5) / 36 Rust tests, tsc, eslint green. DMG rebuilt.

2026-07-05 19:00 · fix(session): eternal "Working…" spinner + no interrupt. ROOT CAUSE (live-diagnosed on ses_0cb07e522f…): the turn had finished server-side (last assistant message time.completed set, no pending asks) but session.idle was lost in a deliberate SSE reconnect window 1.4 s earlier — runningSessions is event-driven with no recovery, and the /event stream is directory-scoped, so every cross-folder session open/workspace switch is a loss window. FIX: (1) reconcileRunning() checks each locked session against server truth (new turnIsOver: last message is a completed assistant message) on every reconnect + openSession + a 15 s poll while working, reloading the missed history tail and unlocking; (2) interrupt — SDK abortSession (POST /session/:id/abort), Stop button next to Working… + Esc (modals/palette keep priority), "Interrupted" line in the thread, abort's own error/idle events suppressed once; (3) visibility — the Working… row now names the currently running tool call. VERIFIED: turnIsOver validated against the real stuck session's live payload (→ unlocks); 282 JS tests (+8: reconcile stale/genuine/on-connect, interrupt lifecycle ×4, SDK completed-mapping+abort), tsc green. DMG rebuilt.

2026-07-05 23:30 · Architecture audit (4 parallel tracks: layering, Rust backend, frontend, safety defaults). Verdict: skeleton is high quality — SDK boundary real (zero raw HTTP in app), clean package deps, zero any/ts-ignore strict TS, 274 behavioral tests, uniform Rust path sandboxing. BUT the AGENTS.md non-negotiable safety defaults are NOT met: no permission config is ever written and the bundled OpenCode 1.17.13 defaults to {"*":"allow"} (bash/edit/installs run unprompted — contradicts P2-3's "approval mode ✅"; only the permission UI ships), sidecar runs --cors * unauthenticated (any local webpage can drive turns + read keys via /global/config), API keys plaintext in opencode.json. Two critical Rust defects: Windows cmd /C start command injection in open_url/os_open (agent link with & executes commands; also breaks legit URLs), and kernel_execute holding the global kernel mutex across an unbounded blocking read (a while True: pass cell wedges even kernel_reset). Full severity-ranked backlog recorded as REQUIREMENTS.md P0-7. Blocker: fix permission block + CORS before next release.

2026-07-05 22:40 · fix(preview): text files with unlisted extensions (.bib, .rst, .c, …) showed "Preview is available in the desktop app." INSIDE the desktop app. ROOT CAUSE: Rust mime_for defaulted unknown extensions to binary→base64, and the frontend text path only accepts utf8 — text stayed null and fell through to the misleading note. FIX: read_artifact now sniffs unknown extensions (valid UTF-8 without NUL bytes → utf8 text/plain, else base64); frontend reports genuinely binary files explicitly ("binary and has no preview — open it externally") instead of the desktop-app note. VERIFIED: Rust sniff test (bib text / NUL / invalid UTF-8 / known-binary never sniffed) + component test (base64 behind a text preview shows the binary message). 274 JS tests (+1), 10 artifact Rust tests (+1) green. DMG rebuilt.

2026-07-05 22:15 · P1-5 native table→chart surface shipped (closes P1-5's "native categorical chart surface when a real dataset needs one" gap). Any CSV/TSV (or xlsx-parsed) table preview now has a Table ↔ Chart toggle. New pure lib/tableChart.ts: analyzeColumns detects numeric columns (parses %, thousands separators, tolerates a minority of NA/labels via a 60% finite threshold), defaultChartSpec picks a sensible default (first categorical column as X → grouped bar; numeric X → line; numeric columns as Y series, capped at 8), canChart gates the toggle. New TableChart.tsx: renders line/bar/scatter with the app's shared --series-1..8 palette (the SAME hues the agent's matplotlib figures use, theme-aware for light+dark), with a chart-type segmented control, an X-column select (incl. "row #"), per-series show/hide chips, y-gridlines, thinned x-labels, and a legend — so a generated figure and this native chart tile read as one design system (P1-5 acceptance a). Wired via a TableView wrapper in FilePreviewInspector (Chart tab appears only when the data has a numeric column). VERIFIED: numeric detection + default-spec (categorical→bar, numeric→line, 8-series cap, null when nothing numeric) — 6 logic tests; component renders type controls + series toggles + bar rects, switches to line polylines, and shows the no-numeric message — 3 component tests. 270 JS tests (+15 across tableChart+modal), tsc, eslint green. DMG rebuilt.

2026-07-05 21:45 · P2-2 Modal runner shipped (the named P2-2 gap; SSH+Slurm was already done). Same shape as the HPC integration — the app drives the user's OWN credentials, never handles tokens. New Rust modal.rs: modal_status runs modal --version under the enriched PATH (so a conda/user-installed modal is found) and reports {installed, version, authenticated, hint}; authentication is a pure, unit-tested is_authenticated(home, token_env) (a non-empty MODAL_TOKEN_ID or a ~/.modal.toml file). New Settings "Cloud compute (Modal)" card (ModalCard.tsx): a status dot (ready/installed-not-authed/not-installed) + the fix hint + a Re-check button. New bundled modal-run skill: verify readiness, write a Modal function INTO the workspace (pinned image packages + fixed seed for reproducibility), modal run it with the user's token, capture results as artifacts; explicit rules — user's account only, never store tokens, cost-aware (bounded timeout, no GPU unless needed). VERIFIED: is_authenticated via token-env and via a real ~/.modal.toml fixture (empty home never authed) — 2 Rust tests; detection correctly reports "not installed" on this host (no modal); ModalCard mounts — 1 component test. 264 JS tests (+1), 35 Rust tests (+2), tsc, eslint green. DMG rebuilt. Gap: multi-environment management; a live Modal run needs the user's own account.

2026-07-05 21:10 · P1-3 materials binary phase-diagram viewer shipped — completes the materials electronic/thermodynamic trio (DOS + band structure + phase diagram). New lib/phase.ts parses a .phase JSON (2 elements + entries with atomic composition + formation energy/atom), computes each phase's composition x = n_B/(n_A+n_B), builds the LOWER CONVEX HULL (Andrew's monotone chain) and marks each phase stable/metastable by its energy-above-hull (defined via eAboveHull ≤ tol, NOT hull-vertex membership — a phase exactly on a tie-line is collinear and dropped as a redundant vertex, but is still marginally stable; caught this in testing). PhaseView.tsx: SVG scatter of formation energy vs composition with the convex-hull tie-lines, stable phases filled + labeled, metastable phases open (series-6 ring) with their eAboveHull on hover, endpoints labeled with the element symbols, app palette. Registry: previewKind "phase" (.phase), needsText, Rust mime_for serves .phase as JSON text, FilesPage image icon. VERIFIED against a Li–O fixture: composition fractions, stable (Li2O/LiO2/endpoints) vs metastable (LiO at +1.0 eV above the Li2O–O2 tie-line), hull sorted by x — 5 parser tests; component renders system/counts/hull path + 4 circles + stable labels + error — 2 component tests. 263 JS tests (+7), 33 Rust tests, tsc, eslint green. DMG rebuilt. The materials row of P1-3 (band/DOS + phase diagrams) is now ✅. Gap: ternary phase diagrams; FITS corner plots; anomaly-map coastlines.

2026-07-05 20:40 · P2-4 file-open reliability: fixed an ineffective large-file preview guard (real OOM/freeze risk). ROOT CAUSE: read_artifact (the choke point for every preview — Files explorer + artifact clicks) did std::fs::read(full) — loading the ENTIRE file into memory — and only THEN checked the 25 MB cap, so opening a multi-GB file (exactly what P0-6 is about) could exhaust memory and freeze the app before the cap ever fired. FIX: stat first via metadata().len(), reject oversized files BEFORE reading a single byte, then read only within the cap; extracted PREVIEW_CAP_BYTES + a pure exceeds_preview_cap() helper. Frontend: a new PreviewError component special-cases the "too large" error into a helpful card — explains the cap, offers "Open externally", and points to the large-file probe (schema/sample instead of loading the whole file) — other errors still render as a plain line. Considered P1-2 physics/earth connectors this iteration but REJECTED them on the "no bugs / vetted" bar: astro-mcp is actually Astronomer/Airflow (not astronomy — a name mismatch I caught before adding), and usgs-mcp/open-meteo-mcp couldn't be confidently verified to start as stdio servers (both exited with no error even with stdin held open) — I won't ship a connector I can't verify launches. VERIFIED: exceeds_preview_cap boundary (0/at-cap allowed, +1/2 GB rejected) — Rust test; the too-large card renders + Open-externally fires, plain errors don't — 2 component tests. 256 JS tests (+2), 33 Rust tests (+1), tsc, eslint green. DMG rebuilt.

2026-07-05 20:05 · P1-3 materials band-structure viewer shipped — completes the standard electronic-structure pair (band structure + DOS) for materials. New lib/bands.ts parses VASP EIGENVAL (5 header lines + NELECT NKPTS NBANDS + per-k-point blocks of bandIndex energy [energyDown] [occ…]), structurally reading NKPTS blocks × NBANDS bands, detecting spin from the two-energy-column form; BandView.tsx plots every band's energy across the k-point index as thin polylines (spin-up series-1, spin-down series-6 with a legend), energy gridlines, and a k/energy hover readout, in the app palette. Registry: previewKind "bands" (extensionless EIGENVAL via previewKindForName, or .eigenval), needsBytes (TextDecoder — no Rust change), FilesPage flask icon. VERIFIED: parser reads non-spin (3 k-points × 3 bands, correct per-band energy series + eMin/eMax) and spin-polarized (two energy columns → up/down) EIGENVAL — 3 parser tests; component renders one polyline per band + axis labels + error — 2 component tests; previewKindForName EIGENVAL — registry test. 254 JS tests (+5), 32 Rust tests, tsc, eslint green. DMG rebuilt. Gap: phase diagrams (materials); FITS corner plots; anomaly-map coastlines.

2026-07-05 19:35 · P0-4 reviewer PDF-manuscript hardening shipped (the P0-4 gap was "hardening across document formats (PDF)"). New bundled runtime/skills/core/traceability-review/pdf_extract.py: a deterministic, multi-backend PDF extractor (tries PyMuPDF fitzpypdfPyPDF2pdfminer.six, degrades to a clear "no backend" message — never a traceback) that returns {backend, pages, chars, citations:{dois,arxiv,pmids}, claims:[{kind,text,context}], text}. Citation identifiers (DOI 10.x/…, arXiv new+old, PMID) and quantitative claims (p-values, percentages, N=, CIs) are pulled by regex with surrounding context, so the reviewer audits REAL identifiers from the manuscript rather than ones recalled from memory (the same tool-over-recall principle as P0-5). Updated the traceability-review SKILL.md: for a PDF, run pdf_extract.py FIRST and feed citations→Check 1, claims→Check 2, text→Check 3; on {error} say so and never fabricate. VERIFIED end to end: generated a real PDF (PyMuPDF) and the extractor pulled the text, all three identifier types (10.1234/example.2026, arXiv 2401.00001, PMID 31234567), and p-value/percent/sample-size claims — 7 Python tests (incl. backend-independent regex tests + clean error on a missing file). Found + fixed a test-fixture bug (PyMuPDF insert_text clips long lines at the page edge → identifiers past the margin were lost; fixture now uses short lines — the extractor itself was correct). DMG rebuilt to bundle the extractor. Gap: robustness for models weaker at tool use.

2026-07-05 19:05 · P1-3 native climate-anomaly map shipped — the LAST P1-3 discipline viewer, so ALL FOUR targeted disciplines (materials, physics/astro, earth, social) now have a native renderer, meeting P1-3's "≥1 domain renderer per discipline" acceptance. New lib/anomaly.ts parses a gridded .anom field in two portable text shapes (long CSV lat,lon,value with header sniffing, or a labeled grid with lons in the header row + lats in the first column) into an ascending lat/lon grid + a symmetric zero-centered range; divergingColor gives a blue↔white↔red scale. AnomalyMapView.tsx: a canvas renders the field on an equirectangular (plate carrée) projection (north up, missing cells gray), with an SVG overlay drawing a graticule + °N/°S/°E/°W labels + hover crosshair, a diverging colorbar (±absMax), and a lat/lon/value readout — the correct transform + diverging colormap the discipline expects. Registry: previewKind "anomaly" (ext .anom), needsText, Rust mime_for serves .anom as text/csv, FilesPage image icon. VERIFIED: parser reads long-CSV + labeled-grid + diverging color endpoints — 5 tests; component renders grid dims + canvas + graticule labels + error — 2 tests. 249 JS tests (+7), 32 Rust tests, tsc, eslint green. DMG rebuilt. Gap: materials band structure + phase diagrams; coastline basemap for the anomaly map; FITS corner plots.

2026-07-05 18:35 · P1-3 native qualitative-coding traceback viewer shipped (social-science renderer row ⬜→🟡; pairs with P1-6). New lib/qcode.ts parses a portable .qcode JSON (sources + codebook + span annotations), validates each annotation (flags unknown source/code and out-of-range spans instead of throwing), and — crucially — derives every quote as source.slice(start,end), plus a boundary-segmentation pass that renders overlapping codes without loss. QCodeView.tsx: a codebook sidebar (per-code color from the app palette + counts), the source text with highlighted coded spans, two-way linking (click a code to isolate its spans; hover a span to see its codes), a skipped-annotation warning banner, and a "quotes are exact source spans" assurance — the decisive social-science integrity property is guaranteed by construction (a highlight is literally a slice of the source, so a code can never point at an invented quote). Registry: previewKind "qcode" (ext .qcode), needsText, Rust mime_for serves .qcode as JSON text, FilesPage highlighter icon. VERIFIED: parser reads sources/codes/annotations + exact quotes + overlap segmentation + warnings — 6 tests; component renders codebook + spans sliced from source + error — 2 tests. 242 JS tests (+16 across dos+qcode), 32 Rust tests, tsc, eslint green. DMG rebuilt. Gap: materials band structure + phase diagrams, earth climate anomaly maps.

2026-07-05 18:05 · P1-3 native materials DOS viewer shipped (second P1-3 domain viewer this session; materials band/DOS row ⬜→🟡). New lib/dos.ts parses VASP DOSCAR total density-of-states (5 header lines + Emax Emin NEDOS Efermi control line + NEDOS rows; 3 cols = non-spin, 5 cols = spin-polarized) + DosView.tsx: an SVG chart in the app palette — non-spin as one filled curve, spin-polarized with spin-up above the baseline and spin-down mirrored below (the canonical DOS presentation), the Fermi level as a dashed vertical marker, an "E − E_F ↔ absolute E" alignment toggle, and a hover readout. Made the previewer registry filename-aware: new previewKindForName(filename) recognizes extensionless scientific files (VASP DOSCAR) and the .dos extension, delegating to the ext registry otherwise; routed FilePreviewInspector + FilesPage (icon + inspector) through it. DOSCAR loads via needsBytes (TextDecoder-decoded), so no Rust text/binary change was needed. VERIFIED: parser reads non-spin + spin DOSCAR (energies, up/down channels, Efermi) — 3 parser tests; component renders two filled spin areas + Fermi marker + toggle, and a friendly error — 2 component tests; previewKindForName DOSCAR/.dos/fallback — 2 registry tests. 234 JS tests (+8), tsc, eslint green. DMG rebuilt. Gap: band structure + phase diagrams (materials), earth anomaly maps, social qualitative-coding traceback.

2026-07-05 17:35 · P1-3 native FITS astronomy viewer shipped (physics/astro was the lowest-completion discipline — no connector AND no viewer; now it has a renderer). New dependency-free FITS reader lib/fits.ts (parses 80-char header cards in 2880-byte blocks, big-endian data per BITPIX 8/16/32/64/-32/-64 with BZERO/BSCALE + BLANK→NaN, primary HDU; WCS keywords; 1-D spectrum vs 2-D image by NAXIS) + FitsView.tsx: a 2-D image HDU renders to a canvas with a scientific colormap (magma/viridis/gray), linear/log/asinh stretch, a colorbar, vertical flip for FITS bottom-left origin, and a hover readout of pixel value + approximate WCS sky coordinate (linear CDELT, cos-dec corrected); a 1-D HDU renders as a palette-matched SVG line chart with world-axis labels from CTYPE/CRVAL/CDELT. Dark astro backdrop, theme-independent like the molecule/mesh viewers. Wired the previewer registry: previewKind "fits" (+FITS_EXTS fits/fit/fts), FilePreviewInspector needsBytes→FitsView, Rust mime_for serves them as binary (base64→ArrayBuffer), extToKind "figure". VERIFIED against REAL astropy files (8×8 float32 image with a TAN WCS; 16-pt int16 spectrum): parser reads dims, decodes big-endian values (ramp 0..63; int16 0,3,6…), extracts WCS, maps the reference pixel to exactly CRVAL, and the 1D linear axis — 7 parser tests; component mounts with the right controls/DOM + friendly error — 3 component tests. 223 JS tests (+10), 32 Rust tests, tsc, eslint green. DMG rebuilt. Gap: materials band/DOS + phase diagrams, earth anomaly maps, social qualitative-coding traceback; FITS corner plots + full (non-linear) WCS.

2026-07-05 16:55 · P1-2 first non-bio domain connectors shipped (was 🟡 with ZERO non-bio DBs). Added two vetted, real open-source MCP servers to the curated catalog (lib/scienceConnectors.ts), same one-click-provision-into-isolated-uv-env mechanism as the literature/bio ones — we integrate, don't reimplement: Materials Project (mcp-materials-project, the requirement's prioritized materials DB; MP_API_KEY) and FRED (fred-mcp, Federal-Reserve economic time series for social science; FRED_API_KEY). Package names + entry points were verified against PyPI + each wheel's entry_points.txt BEFORE adding them (no invented names — the exact risk P0-5 guards against). Two mechanism upgrades: (1) connectorConfig now launches console-script servers (the common MCP shape) by resolving the script next to the managed interpreter — cross-platform (<env>/bin/<s> unix, <env>/Scripts/<s>.exe Windows) — in addition to -m module; (2) a per-connector free-API-key field passes the key via the MCP environment map (never into provenance/logs/exports — safety default) and Enable is gated until a required key is entered. Settings card gained a discipline chip + "Get a free key ↗" link + large-install note. VERIFIED end to end on this host: fred-mcp pip-installs, its console script lands EXACTLY at the path connectorConfig computes (<venv>/bin/fred-mcp), it rejects startup with no key, and with a key supplied via the environment it passes key validation and proceeds to run its transport — proving the launch + env-key chain (a live query needs the user's real key + OpenCode as the stdio client, same bar as the existing connectors). 216 JS tests (+8 connectorConfig: module/console-script/Windows-.exe/env-key/blank-key/breadth), tsc, eslint green. DMG rebuilt. Gap: physics/astro + earth/climate connectors (no sufficiently mature open MCP found yet), more chem/social DBs.

2026-07-05 16:20 · P0-6 large-file "reference, don't load" memory-pointer contract shipped (was 🟡 with the core contract unbuilt). New bundled skill large-file (runtime/skills/core/large-file/large_file_probe.py, stdlib-first, agent-facing tool — emits a compact JSON POINTER, not the review contract, so zero frontend change): given any data file it returns schema / shape / approx row count / head+tail sample / extracted log numbers in BOUNDED memory (streamed line count, tail via a 64 KB seek, per-cell truncation), so the agent references data instead of loading a file bigger than the context window. Coverage: tables (CSV/TSV, delimiter sniff + dtype inference), NDJSON (key union + count), Parquet (pyarrow metadata only), HDF5 (h5py dataset tree, no array read), FITS (astropy memmapped headers), NetCDF, and text/logs with DETERMINISTIC numeric extraction for VASP OUTCAR/OSZICAR (last free energy TOTEN, energy(sigma->0), convergence flag — the numbers, not prose). Binary formats without their lib degrade to a clear install-hint pointer, never a raw dump. SKILL.md tells the agent to probe BEFORE reading and then read only bounded slices (nrows/usecols/sub-array). VERIFIED end to end on real files on this host: 13.5 MB CSV → 788 B pointer (700k rows, head+tail sample), real 1.6 MB Parquet → 447 B (100k rows, schema from metadata), 16 MB HDF5 → 456 B (dataset tree with shapes) — each ~18,000–37,000× smaller than the file, no whole-file load (matches the requirement's >16,000× memory-pointer finding). 11 Python probe tests (incl. an explicit "pointer stays <4 KB and <file/100" assertion). DMG rebuilt. Gaps (🟡): genomics BAM/CRAM/FASTQ (pysam), GRIB, ROOT; auto pre-read wiring in the UI.

2026-07-05 15:45 · P1-6 social-science analysis integrity shipped (the other ⬜, pairs with P0-5). New bundled skill stats-integrity (runtime/skills/core/stats-integrity/stats_integrity_check.py), same deterministic + pluggable shape and same review contract — added integrity to shared ReviewCheck (findings carry a stats · … tag, so no per-check UI change). Three checks map to the three acceptance points: (a) execute-don't-interpret — a stats · interpretation check flags causal/"provocative" language ("causes/leads to/drives/the effect of…") in a report that is reporting an association, and the SKILL.md instructs surface-estimates-don't-volunteer-causation (report coef+SE, name the design before any causal claim); (b) prereg-aware — a stats · prereg check parses regression formulas (y ~ x1 + x2*x3) in the code and flags any predictor/interaction absent from a preregistration.*/analysis_plan.*/prereg.* plan (HARKing guard); (c) reproducibility — a stats · seed check flags randomised analysis (np.random/train_test_split/bootstrap/R sample) with no fixed seed. VERIFIED the .dta→R round-trip acceptance ON THIS HOST: pandas to_stata → R foreign::read.dtalm reproduces Python OLS to the printed precision (β=0.600870, SE=0.075000 both sides; the SKILL.md documents this recipe with fixed seeds — no new engine, reuses the shipped R kernel). Rules favour precision (association wording and non-stats docs stay silent; no plan → no prereg findings). 13 Python validator tests; CLI catches all three risks on a realistic workspace; 208 JS tests (+1 integrity-tag parse), tsc, eslint green. DMG rebuilt. Gaps (🟡): a first-class in-app preregistration artifact + auto pipeline↔plan diff every run; packaged Stata/SPSS reader UI; wrong-test-selection detection.

2026-07-05 15:10 · P0-5 domain-correctness gates shipped (was the only ⬜ P0, "runs" ≠ "right" — the highest-value new build). New bundled skill domain-check (runtime/skills/core/domain-check/domain_check.py): a DETERMINISTIC, pluggable validator layer — one check_<field> rule set per discipline, stdlib-only, analysing the code the agent actually wrote (AST for Python/notebooks, regex for R) rather than model recall. Three gates ship, each catching its acceptance case: physics · units (dimensional mismatch t_seconds + d_meters; trig on a degree-valued angle), earth · crs (Euclidean distance on lat/lon incl. the classic (lat1-lat2)**2+(lon1-lon2)**2; geopandas geometric op with no CRS set), biology · coords/strand (BED off-by-one — 0-based half-open so length is end-start, no +1, caught via AST so int(end)-int(start)+1 is seen through the cast; strand-unaware sequence extraction). Rules favour precision (unknown units / no discipline signal stay silent; findings dedupe when rules overlap). Reuses P0-4's review contract end to end: extended shared ReviewCheck with domain + optional per-discipline tag (so new fields need NO UI change), parser + ReviewerCard render the tag; the skill tells the agent to run the gate before/after execution and relay the emitted ```review block. VERIFIED: 19 Python validator tests (3 acceptance catches + true-negative precision guards + syntax-error-no-crash + R fallback); CLI emits clean deduped findings on a multi-discipline sample; 207 JS tests (+2: domain-tag parse), tsc, eslint green; skill bundles into Open Science.app/Contents/Resources/skills-core/domain-check with NO test/pycache leakage; runs portably from the deployed $XDG_CONFIG_HOME path; and the bundled OpenCode sidecar DISCOVERS domain-check in its directory-scoped skill list alongside traceability-review. Gaps (kept 🟡): full library round-tripping (SMILES→RDKit, POSCAR→pymatgen), chemistry/materials + social-science gates, deeper per-field rules. DMG rebuilt.

2026-07-05 07:30 · Rewrote docs/REQUIREMENTS.md on a multi-discipline evidence base (v2). Five parallel community-research passes (physics/astro, chemistry/materials, earth/climate, social science, bio+cross-cutting) saved to docs/research/2026-07-05-multidiscipline-needs.md. Key finding: needs are ~80% shared / ~20% discipline-specific; the shared 80% (the moat) is largely shipped. Every item now carries a ✅/🟡/⬜ status marker. Added two NEW requirements from the evidence: P0-5 domain-correctness gates ("runs" ≠ "right" — units/CRS/off-by-one/stoichiometry/stat-selection; the largest gap, highest-value build) and P1-6 social-science analysis integrity (execute-don't-interpret + prereg + Stata/SPSS); split out P0-6 large-file reference-don't-load; expanded P1-2 connectors and P1-3 renderers into per-discipline tables (physics/chem/earth/social all ⬜ planned). Docs only; no code change.

2026-07-05 06:20 · Session header slimmed from 5 elements to 3 so it stays one line (user: slim the header content down or it wraps to two lines). The Files toggle now names the session folder itself (e.g. 2026-07-05-0319, full path on hover), absorbing the redundant read-only folder chip — WorkspaceChip no longer renders for open sessions (draft picker unchanged); ConnBadge shows only the green dot when ready (full text on hover), text only for connecting/error. 206 JS tests (WorkspaceChip open-session test now expects empty render), tsc, eslint green; DMG rebuilt.

2026-07-05 05:10 · Right pane is now per-session with scroll memory (user: switching sessions leaked another session's open PDF into a workspace where the path no longer resolved; wanted each session to restore its own pane and positions). Store: global activeArtifact → panes map keyed by session id (DRAFT_KEY for drafts) holding {artifact, showFiles} — mutually exclusive by construction; draft pane grafts onto the created session id, startDraft/switchWorkspace reset the draft pane, deleteSession forgets its pane. New useScrollMemory hook (module-level Map, zero re-renders): remembers scrollTop per key, restores once content is ready, ignores clamp events while loading; wired into chat (chat:<sid>), NotebookEditor + FilePreviewInspector (file:<path>, history separate), Docx/Xlsx/Pptx inner scrollers (office:<path>), and the demo inspectors. Limitation: PDF/HTML render in native iframes — the file reopens but the viewer's internal position can't be captured. 206 JS tests (+13), tsc, eslint green; DMG rebuilt (user re-tested BEFORE the rebuild and saw the old global-pane behavior — the fix wasn't in the installed app yet).

2026-07-05 04:10 · Files/Notebooks are now GLOBAL pages; the session gets its own quick entry (user: pages looked global but silently showed per-session content; demanded the kernel-cwd gap be solved properly, "需要1个最佳实践"). Design per user: Files browses from the Settings base folder (root crumb names it), Notebooks lists every .ipynb across all session folders newest-first with a folder chip, and the chat header gains a Files button that opens the CURRENT session's folder in the right inspector (SessionFilesPane, like Artifacts). Plumbing: file commands (list_dir/read_artifact/preview_url/write_workspace_file/open_path/list_notebooks) take an explicit root scope "workspace"|"base" (no fallback guessing); preview URLs are scope-prefixed /w/… vs /b/…. Kernel best practice = Jupyter semantics: one kernel PER NOTEBOOK, cwd = the notebook's own folder (kernel map keyed by lang+absolute path, per-kernel R code file) — relative paths in any notebook now resolve correctly from any page, and state no longer bleeds between notebooks. 32 Rust + 194 JS tests, tsc, eslint green; DMG rebuilt.

2026-07-05 03:40 · Files page now names the folder it is browsing (user: "Workspace" root showed 3 files but no way to tell WHICH folder — expected the OpenScience base, was actually the session's dated folder). The breadcrumb root now shows the active workspace folder's basename (e.g. 2026-07-05-0319) with the full path on hover, reusing WorkspaceChip's baseName (now exported). Browser dev keeps the "Workspace" fallback. 192 JS tests, tsc, eslint green; DMG rebuilt.

2026-07-05 03:45 · Follow-up on "still broken" report: the re-root fix WAS working (new jupyter correctly rooted + owns the port; e2e-verified by creating a notebook through the jupyter API — it landed in the active workspace). The remaining symptoms were pre-fix residue: nature_figure.ipynb had been misplaced into 2026-07-04-2204 by the old build (moved back to its session folder 2026-07-05-0319), and a Friday orphan jupyter-lab had survived TWO SIGTERM pkills (wedged in graceful shutdown) — force-killed it and hardened kill_orphan_jupyter to SIGKILL (pkill -9; Windows already used /F). 31 Rust tests green; DMG rebuilt.

2026-07-05 03:25 · Agent-created notebooks now land in the ACTIVE workspace (user: agent made a notebook but the preview said "file not found" and the Notebooks sidebar showed "No notebooks yet" despite many existing). ROOT CAUSE, same class as the 07-04 preview-server bug: jupyter-lab pins --ServerApp.root_dir at spawn time and start_jupyter is idempotent, so after workspace-per-session switches the agent's jupyter MCP kept writing notebooks into the OLD folder (proven live: running lab rooted at 2026-07-04-2204 + a Friday orphan rooted at the base folder, both on port 50868, while the UI scanned 2026-07-05-0319). FIX: set_workspace (the choke point for every switch path) now calls reroot_jupyter — background kill + respawn rooted in the new folder, serialized by a lifecycle lock; port/token are fixed in server meta so the MCP entry stays valid. Orphans self-heal on next start via the existing pkill. Existing stray notebooks remain in ~/Documents/OpenScience (by-design per-folder isolation). 31 Rust tests green; needs app rebuild + restart to take effect.

2026-07-05 04:35 · 3D viewer warns on unrenderable files (user: add a notice so people know the file is the problem). After framing, MeshView renders the model once offscreen over a magenta clear (backdrop off) and counts painted pixels; if ~none (<0.08% of the canvas) it overlays a "File problem — this 3D file has no visible geometry, the export looks incomplete or corrupt" banner. Catches the malformed sample (renders empty) and genuinely empty/corrupt exports; verified NOT to false-positive on a valid cube glTF or the torus-knot STL. 194 JS tests, tsc, eslint green; DMG rebuilt. 2026-07-05 04:20 · 3D viewer fidelity: glTF/GLB visibility + obj z-fighting (user: cube.gltf/glb showed nothing; cube.obj bottom face flickered at some angles in Shaded). ROOT CAUSES: (1) glTF meshes kept their own single-sided material and a default fully-metallic PBR material renders black without an environment map — added a PMREM RoomEnvironment so PBR renders correctly, and forced DoubleSide on all mesh materials so inconsistent winding never culls a face to invisible; (2) the shadow-catching ground sat coplanar with a flat model bottom (a cube) → z-fighting; dropped the ground by radius*0.01. Decisive check: a hand-built VALID cube glTF renders as a clean shaded cube — so the viewer is correct; the user's sample cube.gltf/glb are MALFORMED (only 2 of 6 faces, and a wrong indices bufferView byteLength), produced by the sample generator, which is also why the same-cube STL/OBJ/PLY render fine. Verified valid-gltf + obj by screenshot. 194 JS tests, tsc, eslint green; DMG rebuilt. 2026-07-05 03:15 · 3D mesh viewer for stl/obj/ply/gltf/glb (user: can 3D models / CAD show? then: add three.js, beautiful + workable). New MeshView (three.js + WebGL, self-contained, lazy-loaded loaders) added as its own preview kind: warm studio lighting (hemisphere + key/fill), soft contact shadow, warm gradient backdrop matching the app palette, auto-frames the model, OrbitControls (drag-orbit / scroll-zoom / right-pan), Shaded↔Wireframe toggle, triangle-count badge; theme-independent like the molecule/Office previews. Wiring: previewKind→"mesh" (+MESH_EXTS), FilePreviewInspector needsBytes, Rust mime_for returns these as binary→base64→ArrayBuffer, new "model" ArtifactKind (Box icon) so written models surface as artifacts. Three fidelity fixes found via screenshot: STL face normals are often zero → always computeVertexNormals (else unlit black); wireframe swaps to a dark high-contrast material (light clay drew near-invisible lines on the warm bg); material ref-swap toggles without rebuilding. Added three/@types/three deps. Verified with a torus-knot STL: shaded + wireframe both render cleanly. CAD note: DXF (mid) and STEP via occt-import-js WASM (heavier) possible later; DWG has no open reader. 192 JS tests (1 new: mesh mapping), tsc, eslint green; DMG rebuilt. 2026-07-05 02:35 · Office preview overflow fixes (user: wide xlsx changes background color when scrolled right; docx renders too large to see both page edges). (1) xlsx: the shadow .page was block-level (viewport-wide), so its white background stopped mid-table and cells without a fill turned a different color past that point when scrolled right — set .page { width: max-content; min-width: 100% } so the background covers the table's full width. (2) docx: a page has a fixed physical width (Letter/A4, portrait or landscape) usually wider than the pane, overflowing with both edges cut off — DocxView now scales the rendered page to fit the pane width via zoom (shrinks the layout box, no leftover scroll; never upscales) and re-fits on pane resize via ResizeObserver. Both verified by screenshot in a narrow pane. 191 JS tests, tsc, eslint green; DMG rebuilt. 2026-07-05 02:10 · xlsx preview now shows real formatting — fills, font size/color, bold, alignment, borders, column widths (user: docx/pptx/xlsx looked very different from WPS; xlsx had no background colors etc). ROOT CAUSE for xlsx: the open-source SheetJS build's sheet_to_html emits values ONLY (no styles), so every cell was an unstyled grid. Rewrote lib/xlsx.ts on ExcelJS (already a transitive dep, now explicit) — reads each cell's fill/font/alignment/border and emits inline styles + a <colgroup> of Excel column widths + merge spans; ARGB alpha ignored (files store 00 alpha that still means visible). workbookSheets is now async; XlsxView awaits it; SHEET_CSS reduced to a neutral gridline scaffold cells override. Verified with the user's real spec.xlsx: terracotta header fill + white bold text, and the Color-Palette sheet's hex cells render their actual swatch colors — matches WPS. docx (docx-preview) and pptx (pptx-preview, black bg already fixed) were checked and already apply document styles, so left as-is; remaining gaps there are font substitution + inherent browser-render approximation. 191 JS tests (xlsx suite rewritten, +1 style assertion), tsc, eslint green; DMG rebuilt. 2026-07-05 01:45 · File preview no longer bleeds the previous file's content (user: clicking a file's Open button sometimes shows crossed/mixed content). ROOT CAUSE: one FilePreviewInspector instance is reused across files (no key), and its load effect only called setText when data.content === undefined — so opening a second file that carries its own inline content never replaced the first (useState initializers don't re-run on prop change); url/bytes lingered too. FIX: the effect now resets text/url/bytes to the new file's baseline up front, before the async loads fill in only what the new file needs. Covers both the deterministic inline-content bleed and the fast-switch race (the existing cancelled guard drops the stale file's async result). 190 JS tests (1 new: reuse-across-files shows the new file), tsc, eslint green; DMG rebuilt. 2026-07-05 01:20 · CSV preview: dirty commas no longer explode the table (user: a design_tokens.csv row with 0 1px 3px rgba(0,0,0,0.04) rendered as many extra columns). ROOT CAUSE: agents emit CSV with unquoted CSS/JSON values whose internal commas a strict RFC parser splits into extra fields; TablePreview renders one per field, so that row spilled past the 4-col header and misaligned. FIX in parseDelimited: track bracket depth outside quotes — a delimiter inside ()/[]/{} is field content, not a separator (quoted fields still parse strictly, so well-formed CSV is unchanged; depth resets per line since brackets don't span rows). Rows are then normalized to header width (pad short, fold an over-long row's surplus into the last cell) so the table is always rectangular. Verified with the user's real file: 4 clean columns, shadow/clamp() values intact. 189 JS tests (2 new), tsc, eslint green; DMG rebuilt. 2026-07-05 00:45 · Markdown preview (editorial-blog paper) + pptx black-background fix (user: md should preview like a document; wanted a minimal blog feel with a touch of color; pptx slides sat on black). (1) .md gets its own preview kind with a Preview/Code toggle: MarkdownViewer grew a document variant styled as blog paper — a white card on the pane, warm ink (#2b2620), serif display headings (Iowan Old Style → Charter → Georgia; CJK → Songti), the app's terracotta brand (#c15f3c) as a layered accent (H2 accent bar, list markers, links, inline code), tuned tracking (CJK-safe near-zero on headings, +0.006em body), text-wrap balance/pretty, tabular-nums tables. Theme-independent by design (a document reads the same in dark mode). The shared component's element set (headings/ol/blockquote/pre/table/hr) also lifts chat markdown. (2) pptx black background ROOT CAUSE, reproduced in a standalone harness with the user's own deck + DOM inspection: pptx-preview hardcodes background: black on its .pptx-preview-wrapper, so every gap around slides showed black; fixed with a shadow-DOM override (transparent !important beats the inline style) — slides now float on the light canvas with shadows. Libraries stay: docx-preview / SheetJS / pptx-preview are the strongest pure-JS options (browser pptx rendering is inherently approximate). All three variants (EN report, CN blog, mixed elements) verified by screenshot. 187 JS tests (3 new), tsc, eslint green; DMG rebuilt. 2026-07-04 22:40 · HTML/PDF/image Preview fixed (user: index.html opened with code visible but Preview said "not found"). ROOT CAUSE, two layers in the preview file server: (1) artifact paths from write tools are ABSOLUTE (/Users/…/2026-07-04-2204/index.html) but preview_url pasted them into the URL as if workspace-relative — the server joined them under its root and 404'd every absolute-path artifact; (2) the server's root was captured once at first use and cached forever, so after the workspace-per-session change any session switch left it serving the wrong folder. FIXES: preview_url now relativizes absolute paths against the current workspace (sandbox-checked: outside-workspace paths are a clear error, not a silent 404); the server resolves each REQUEST against the workspace as it is now (serve(root_of) closure). User-verified in the app. 31 Rust tests (2 new), DMG rebuilt. 2026-07-04 22:30 · Live streaming text + file-path tool rows (user: a landing-page turn showed only "write / Working…" for ~50s — couldn't see what was being written or worked on). ROOT CAUSE, verified in opencode v1.17.13 source + the bundled binary: (1) OpenCode streams assistant text as message.part.delta events (full message.part.updated only fires at text-start with "" and at text-end with the whole passage) — the SDK client ignored deltas, so ALL agent prose popped in at once at the end; (2) during a write tool's argument streaming (the dominant wall time — tonight's 20KB index.html streamed 22:06:34→22:07:24) OpenCode 1.17.13 emits NOTHING (tool-input-delta is a no-op upstream; the part sits at {status:"pending", input:{}}), so no client can show live write content at this pinned version — upstream limitation, revisit on upgrade; (3) once input DID arrive, the tool row still ignored it — title fell back to input.command (bash) then the bare tool name. FIXES: SDK accumulates message.part.delta per partID into progressive text.updated events (cleared on session.idle; reasoning parts never seeded, so their deltas drop out); tool-row titles fall back to input.filePath so write/edit rows show the target file as soon as args parse; per-token events skip the debug-log IPC. VERIFIED LIVE against a real opencode server: text.updated arrived progressively 0→15→34→44 chars, final text identical to text-end. 184 JS tests (2 new; mock server made protocol-faithful: text streams as deltas), tsc, eslint green; DMG rebuilt. 2026-07-04 21:20 · Settings shows and changes the BASE workspace (user: Settings displayed a dated session folder like ".../2026-07-04-2103" instead of ~/Documents/OpenScience, and Reveal offered no way to pick a folder). Settings' Workspace card was reading the ACTIVE workspace (which follows each session into its dated subfolder); it now shows the BASE folder — the parent every new session's dated subfolder is created under — with two buttons: "Change…" (native picker → new set_workspace_base, persisted to base-workspace.txt; base_workspace_dir reads it first, so new sessions land under the chosen base; existing sessions keep their folders) and "Reveal" (new open_workspace_base — the sandboxed open_path can only resolve inside the active workspace). The per-session dated folder still shows in the conversation header chip, by design. 183 JS + 29 Rust tests, tsc, eslint, release build green; DMG rebuilt. 2026-07-04 20:55 · Session switching 9-20 ms, was 3-6 s (user: why is switching history so slow?). ROOT CAUSE: workspace-per-session + set_workspace killed and respawned the whole OpenCode sidecar on every cross-folder open (process boot ~2s + 1s-granularity reconnect polling + a 1.5s blind listSkills retry inside connect()). VERIFIED LIVE first: OpenCode serves many folders from ONE process via per-directory instances — a bare /event stream can't see other folders (the old comment was right) but /event?directory= CAN; /session/:id/* calls route by the session's own recorded folder (pwd proof); a directory-less createSession lands in the boot cwd (so it MUST carry the param). FIXES: SDK /event + createSession carry ?directory=; Rust set_workspace no longer restarts the sidecar; store connectRetry backs off 250ms×8 then 1s; loadCatalog moved off connect()'s critical path with the 1.5s blind sleep replaced by 400ms polling. MEASURED with the real SDK against the live sidecar: 3-folder hop = 20/9/9 ms including getMessages; scoped SSE delivers a real turn's tool + idle events; the shell runs in the session's folder, not the boot cwd. 183 JS + 29 Rust tests, tsc, eslint, release build green; DMG rebuilt. 2026-07-04 20:25 · Skeleton while switching sessions (user: the right pane sat blank for a moment when opening a history session). LiveSessionPage shows pulsing placeholder shapes that mirror the real thread layout (user card / agent text lines / a quiet tool row) whenever a session id is set but its history hasn't loaded (!thread.loaded) — covers the getMessages fetch and the sidecar restart on cross-folder opens. 183 JS tests, tsc, eslint green; DMG rebuilt. 2026-07-04 20:15 · Pasting "/name args" now chips too (user: paste didn't activate the command chip). The commit rule generalized from "name + trailing space only" to "known name + whitespace + rest": one change event with the whole pasted text commits the chip and the remainder (multi-line included) becomes the arguments; unknown names ("/etc/hosts …") stay plain text. 183 JS tests (2 new), tsc, eslint green; DMG rebuilt. 2026-07-04 20:05 · Composer: command chip + terminal-style input history (user: "/xxx should be a distinct block, not degrade into string text; ↑ should recall past inputs like a terminal"). (1) Committing a slash command — palette pick, or typing the full known name + space — turns it into an accent chip on the left (mirrors the shell-mode chip); the input then holds only arguments (mono font, accent border, argless Enter allowed); Backspace on empty input or the chip's × dissolves it back to editable "/name [args]" text; palette/shell detection are disabled while chipped so args starting with "/" or "!" stay arguments. (2) Every sent input (prompt, "!cmd", "/name args" in typed form) is recorded to localStorage (global, cap 100, consecutive-dedupe); ↑ with the caret at position 0 enters history navigation, ↑/↓ walk older/newer, walking past the newest restores the unsent draft, any edit exits navigation, the open palette keeps ↑/↓ priority. 181 JS tests (7 new composer tests; 1 updated: palette pick now chips instead of inserting "/name "), tsc, eslint green; DMG rebuilt. 2026-07-04 19:50 · Slash commands in history show as typed, not expanded (user: "/growth-marketing …" reloaded as the skill's full 6.7k-char template text as if they had pasted it). OpenCode stores the EXPANDED template + appended arguments as the user message with NO marker; GET /command exposes each command's template, so historyToThread reverse-maps: a user text that starts with a known template (longest-first) renders as "/name args". Caveat found live: MCP prompts report template as an argument-schema OBJECT, not a string — SDK now passes template through only when it is a string (would have crashed .trim()). VERIFIED against the real session over the live sidecar: the message renders exactly as typed. 174 JS tests (2 new), tsc, eslint green; DMG rebuilt. 2026-07-04 19:35 · History rendering cleaned up (user screenshots: repeated "The following tool was executed by the user" bubbles with bare "bash" rows; a subagent session opened from the sidebar showing its raw task prompt and read/glob spinning forever). Three fixes: (1) historyToThread now renders a user-run "!" shell turn like the live path — OpenCode stores it as a synthetic user text (synthetic:true, new field on HistoryPart) + a bash tool part; we show "! cmd" + inline output and never the marker text; bash rows also fall back to input.command as their title (was showing just "bash"); (2) steps frozen mid-run (runtime restarted / turn killed — e.g. the 18:35 stuck explore turn whose pending permission asks died with the old sidecar process) render as quiet "pending" + ONE red "Interrupted — this turn did not finish" line instead of spinning forever; (3) subagent child sessions (parentId set) no longer get their own sidebar row — they are internals of the parent conversation. NOTE: also repaired 3 literal NUL bytes accidentally written into runtime.ts's permission-sig template (worked fine, but grep treated the file as binary). 172 JS tests (4 new history tests), tsc, eslint green; DMG rebuilt. 2026-07-04 19:05 · Fixed the stuck "Explore …" turn + "Send failed: Load failed" (user session ses_0d01a1073ffe…: a /growth-marketing turn froze forever and the send reported failure). ROOT CAUSE (one bug, two symptoms): a task-tool subagent runs in a CHILD session; its external_directory permission asks carried the child id, but the UI only rendered asks where sessionId === currentId — the approval card never appeared, the subagent's read/glob blocked forever, and the still-open synchronous /command POST was killed by WKWebView's ~60 s fetch timeout → WebKit's generic "Load failed" shown as a send failure. FIXES: (1) SDK task tool events now carry childSessionId (from part metadata) and SessionMeta.parentId; store keeps a sessionParents map (live task events + session list on recovery) and the page resolves asks through rootSessionOf — subagent asks surface in the parent conversation with an "Asked by " label; (2) SDK permission mapping fixed for the V2 field names (permission/patterns, was reading nonexistent action/resources → cards would have shown "action" with no paths); (3) a sync shell/command POST that rejects AFTER SSE events arrived for the session is treated as "connection died, turn alive" — lock kept, no false failure line; session.idle ends the turn (SSE-sequence mark, no clock); (4) one reply answers all identical pending asks (same session+action+resources — the explore agent fired three at once); (5) working indicator says "Paused — the agent needs your answer below" while an ask is pending. VERIFIED: real SDK against the live stuck server session — all 3 pending asks now map to action external_directory with real paths and resolve to the parent session. 168 JS tests (4 new: child→parent mapping, dropped-POST-alive, genuine-failure line, batched reply), tsc, eslint, vite build green.

2026-07-04 10:45 · Composer command modes shipped (user: wanted a Claude-Code-style "!" command-line mode and "/" slash commands for skills/MCP/shortcuts). Mechanism — OpenCode-native, no fakery: "!" flips the input into shell mode (amber border, terminal chip, mono font) and Enter runs the line via POST /session/:id/shell — no model turn, executes directly in the session's workspace folder, recorded in session history as a bash tool part; "/" opens a command palette above the input fed by GET /command (OpenCode merges config commands + skills + MCP prompts into that one list) with filter-as-you-type, ↑/↓ + Tab/Enter autocomplete, Esc close, and POST /session/:id/command on send — an unknown "/name" stays a plain prompt (paths like /etc/hosts). Send lifecycle unified into one performTurn (prompt/shell/command). TWO RACES FOUND + FIXED during verification: (1) shell/command POSTs are SYNCHRONOUS (resolve after the turn; session.idle arrives first), so the running lock is set before the POST — set after, it would never clear; (2) new shellTurns flag (shows the bash OUTPUT inline for user-run commands only — agent bash steps stay quiet lines) must be cleared by the session.idle EVENT, not the POST settling, because SSE frames race the POST response on separate connections. VERIFIED live (Playwright against the real 1.17.13 sidecar + vite): palette filters "/ini"→/init, autocompletes; "!pwd && echo E2E-SHELL-OK" echoes, runs in the workspace, output shows inline in the thread, session lazily created, turn ends with no model configured. 164 JS tests (13 new: SDK endpoints, store lifecycle/races, palette + shell mode UI, inline output), tsc, lint, vite build green.

2026-07-04 10:15 · Proper send lifecycle, new → input → send → response (user: first message gave no feedback then the page suddenly switched; a second send was possible before any reply; errors — occasional "Load failed" — were invisible). Mechanism: (1) the message echoes into the thread INSTANTLY — a draft echoes under a local DRAFT_KEY thread whose blocks are grafted onto the real session id on create, so the page never visibly resets; (2) sending (click → POST accepted) plus runningSessions (until session.idle/error) lock the composer ("Waiting for the reply…") and show a bottom "Starting the session in its folder… / Working…" spinner row; (3) session-scoped errors fold into the conversation as red status lines (new "error" tone) and end the turn — no more silent failures; (4) the first POSTs after a sidecar restart retry once (kills most transient "Load failed"). 148 JS tests (7 new: instant echo/graft, second-send lock, idle/error turn end, create retry, hard-failure line), tsc, lint green.

2026-07-04 10:00 · Top-right status no longer churns on history/folder switches (user: badge flipped ready→connecting→ready and something flashed at the top-right; it should not change unless the runtime is really disconnected). The flash was the ConnBadge state flip plus the blue Connect button appearing while !connected. New store flag switching, set around the three deliberate sidecar-restart paths (cross-folder openSession, workspace pick, dated-folder first message); while set, the page treats the runtime as connected — badge stays "ready", no Connect button, no help card, composer stays enabled. Real failures still surface once the retry window is exhausted (switching clears in finally). 142 JS tests (1 new: switching lifecycle), tsc, lint green.

2026-07-04 09:55 · No more "New session"+workflow-starters flash while a history session loads (user: switching between history sessions showed the draft empty state before the real content). Cause: the empty state keyed off "thread has no blocks", which is also true for a session whose history hasn't loaded yet (seconds, on cross-folder opens). Fix: a URL with a session id is never a draft — the title stays blank instead of "New session" and WorkflowStarters render only on the real draft route (/live with no id). 141 JS tests, tsc, lint green.

2026-07-04 09:50 · Removed the "Connecting to the runtime…" in-content line (user: it appearing/disappearing made the page height shake). While connecting, nothing renders in the content flow — the header status badge is the feedback (its dot now pulses during connecting). The help card remains for real error/offline states. 141 JS tests, tsc, lint green.

2026-07-04 09:45 · No more error-card flash on folder switches (user: opening a history session in another folder — or starting a dated-default session — briefly showed "OpenCode runtime … Could not open OpenCode event stream" before the content). Cause: every cross-folder move restarts the sidecar on purpose, and (a) each failed reconnect attempt wrote its error into the store, (b) the big runtime help card rendered whenever status ≠ ready, including the deliberate "connecting" window. Fix: connectRetry now masks status AND error during the retry loop (the last error surfaces only if the whole window is exhausted), and LiveSessionPage shows a quiet "Connecting to the runtime…" one-liner while connecting, keeping the help card for real error/offline states. 141 JS tests (2 new: masking + exhausted-window), tsc, lint green.

2026-07-04 09:35 · History is now global across all workspace folders (user: switching folders changed the History list). Root cause: GET /session is scoped to the project the sidecar's cwd resolves to — non-git folders share one "global" project but a git folder is its own project, so picking one swapped the visible history. Fix in the SDK only: listSessions now uses GET /experimental/session (lists every project's sessions, newest first, each with its directory — verified against a live sidecar: 23 sessions across 2 projects) with a fallback to /session if the route is missing. Cross-project DELETE /session/{id} verified to resolve globally (200), and openSession already follows a session into its own folder, so no other changes. 139 JS tests, tsc, lint green.

2026-07-04 09:25 · Simplified the draft folder chip per user feedback ("a choose-folder icon is enough; dated is the default; keep-current not needed"): the dropdown menu is gone — a fresh draft's header now shows just a folder icon (tooltip names the dated folder the session will get); clicking it opens the native picker directly, and only after an actual pick does the chip show the chosen folder's name. Removed the setWorkspacePinned store action (pin = explicit pick via switchWorkspace; unpin = "New"/startDraft). 139 JS tests, tsc, lint, vite build green.

2026-07-04 09:20 · Moved the workspace-folder control out of the sidebar (user: the top-left switcher chip felt unnatural). The folder choice now lives where it matters — the session header: while a fresh draft is open, a small "New dated folder ▾" chip next to the "New session" title offers New dated folder (default, ✓-marked) / Keep current folder (…) / Choose folder…; once the session exists the chip becomes a quiet read-only indicator of the session's folder (tooltip = full path). New WorkspaceChip component (thread/), sidebar WorkspaceSwitcher deleted, store action generalized to setWorkspacePinned(bool). 138 JS tests (3 new WorkspaceChip component tests), tsc, lint, vite build green.

2026-07-04 09:00 · New sessions now default to their own fresh dated folder (user: pwd in a new session answered the bare ~/Documents/OpenScience; wanted <base>/<date-time> as default, or an explicit choice). A fresh draft's first message now runs new_dated_workspace(<YYYY-MM-DD-HHMM>) + kernel reset + reconnect BEFORE lazy session creation, so the session, kernel, Files and provenance all start inside the dated folder. Explicit choice = "pinning": picking a folder in the sidebar switcher ("New dated folder" / "Open folder…") pins the destination for the next session; a new "Keep current folder (…)" menu item pins without switching; "New" (startDraft) un-pins back to the dated default. While a draft is unpinned the switcher chip shows "New dated folder" (FolderPlus icon) so the destination is visible before sending. Frontend-only change (runtime.ts store + Sidebar.tsx; datedWorkspaceName moved to the store module); Rust untouched. 135 JS tests (5 new: dated-name format + store pin/unpin/lazy-create behavior via mocked sidecar), tsc, lint, vite build green.

2026-07-04 08:30 · Switchable workspace folders (user: sessions all dumped in one dir, no dating, no user choice). Sidebar now has a workspace switcher (folder chip + menu): "New dated folder" creates ~/Documents/OpenScience/<YYYY-MM-DD-HHMM> and switches to it; "Open folder…" picks any existing folder. The choice persists (app-data active-workspace.txt) and workspace_dir returns it, so the kernel / Jupyter / Files explorer / provenance / previews all operate in the active folder; the agent's OpenCode sessions land there too. New Rust commands: workspace_base, set_workspace, new_dated_workspace, pick_folder; workspace_dir = active override else base (base_workspace_dir keeps the migrations). Store switchWorkspace + openSession (follows a session into its own folder). KEY MECHANISM FINDING (via API probing + a wrong first cut): OpenCode's directory is a per-request query param and its /event SSE stream is directory-scoped — passing ?directory= on session/prompt routes the turn's events to a scope the app's global /event never sees, so turns silently produced NOTHING (empty assistant message). So scoping is done by the SIDECAR CWD (restart on switch, the same proven path as import-login), NOT a query param; reverted the ?directory= additions on createSession/sendPrompt/event. VERIFIED (model-independent, since the free Mimo model is currently returning empty completions — confirmed via an empty assistant message, unrelated to this change): switched to a dated folder → chip updates, folder created on disk, sidecar restarts + reconnects, a Python notebook cell os.getcwd() returned exactly /Users/asq/Documents/OpenScience/2026-07-04-0827 — proving kernel/Files/provenance follow the active folder. 130 JS + 29 Rust tests, tsc, lint, vite build green; installed to /Applications.

26-07-04 06:16 · Fixed "No module named matplotlib" in local notebook cells (user report). Root cause (systematic-debugging): the local kernel (kernel.rs) spawned bare python3 with NO PATH set, so a Finder-launched app (minimal PATH) resolved it to a system Python (CommandLineTools / /usr/bin, no matplotlib) — while the agent/sidecar runs python3 under the enriched PATH → the user's anaconda (WITH matplotlib). Kernel and agent used different Pythons. Not a regression from this session (kernel never set PATH); surfaced when running a matplotlib cell from Finder. Fix: the kernel now spawns with the SAME crate::runtime::enriched_path() the sidecar uses (made pub(crate)), and interpreter detection probes under that same PATH so detection and execution agree — so python3 resolves to the agent's scientific Python. VERIFIED on the packaged app (launched from Finder): new Python notebook cell import matplotlib, numpy, scipy; print(matplotlib.__version__)matplotlib 3.10.1 (was ModuleNotFoundError). 130 JS + 29 Rust tests, tsc, lint, vite build green; installed to /Applications.

2026-07-04 05:15 · Usability pass (user asked to review comfort/naturalness of the UI) — verified on the packaged app. (1) Conversation was noisy: every mechanical step rendered as a heavy bordered card with raw absolute paths, reading like a shell trace. Fixed: ToolCallRow now renders successful/pending/running steps as quiet single-line log entries (muted monospace, no card); only waiting-approval/warning/failed keep a prominent card. tidyToolTitle shows workspace files by relative path (demo_analysis/analyze.py). todo* tool rows (opaque "N todos") are now dropped from the thread. Verified: the dose-response demo thread now reads as prose + artifacts, not command cards. (2) Sidebar had "Customize" (top) and "Settings" (bottom gear) both opening /settings — removed the redundant "Customize"; Settings is the single bottom entry. (3) DECISION (user): reverted the OS-keychain credential storage from earlier today back to the app-private mode-600 file — on unsigned/self-built copies a signature change makes macOS prompt for the login-keychain password every launch, a worse experience than the mode-600 file for a marginal gain; P2-3's no-leak Acceptance was already met. Removed keychain.rs + the keyring dep; KEPT the independent P2-4 exit-cleanup fix (Cmd+Q → RunEvent::Exit reaps the sidecar/kernel/Jupyter, which previously orphaned). Verified: reverted build launches with no keychain prompt, connects on free Zen, sidebar clean. 130 JS + 29 Rust tests, tsc, lint, vite build green; installed to /Applications.

2026-07-04 03:30 · Credentials at rest in the OS keychain (P2-3's open gap) + fixed a real orphaned-sidecar exit bug (P2-4). Provider credentials no longer sit in a plaintext file between runs: new keychain.rs (via the keyring crate — macOS Keychain / Windows Credential Manager) hydrates OpenCode's auth.json from the keychain before the sidecar starts and, on exit, writes it back and deletes the on-disk file. Never-lose invariant: the plaintext file is removed ONLY after a successful keychain write; any failure (or a keychain-less platform) keeps the file exactly as before — no regression (unit-tested via a SecretStore trait with an in-memory store: round-trip, save-failure-keeps-file, hydrate-never-clobbers, skip-empty). WHILE VERIFYING, found + fixed a genuine pre-existing bug: on macOS Cmd+Q/Quit the app terminates via RunEvent::Exit, NOT ExitRequested, so the exit cleanup NEVER ran — the OpenCode sidecar (and kernel/Jupyter) orphaned on every quit (confirmed: main app gone, sidecar PID still alive; 8 stale sidecars had accumulated), and my keychain persist wouldn't fire either. Cleanup now runs on BOTH events (idempotent). VERIFIED end to end on the packaged app: launch → Cmd+Q → sidecar reaped ✓ and auth.json moved into the keychain + removed from disk ✓ → relaunch → auth.json restored byte-identical (sha match) ✓ → OpenCode ready with the configured model (Mimo-V2.5-Free) ✓. (macOS keychain ACL trusts the app, so the item is readable by the app without a prompt but NOT by the security CLI — expected; verified via the app's own restore path.) Data-flow card copy updated (keychain, not "app-private file"). 127 JS + 33 Rust (4 new keychain, +1 ignored real-keychain) tests, tsc, lint, vite build green; installed to /Applications.

2026-07-04 02:55 · Workspace Files explorer shipped (P2-1's multi-file/"IDE for larger projects" gap). New sidebar "Files" page (FilesPage) browses the whole workspace tree — a breadcrumb for folder navigation, type-aware icons (folder/notebook/image/table/molecule/genome/text) + human sizes — and opens ANY file in the existing native viewers (reuses FilePreviewInspector + NotebookEditor, so figures/tables/PDF/molecule/genome/notebooks all work), not just files the agent mentioned. New Rust list_dir(rel) — non-recursive, workspace-sandboxed via resolve_under, skips dotfiles/node_modules/pycache, dirs-first sort (extracted to a testable dir_entries helper). VERIFIED end to end on the packaged app: Files → workspace listing with correct icons/sizes → clicked demo.pdb → opened in the 3D protein cartoon viewer (327 atoms) → navigated into canvas-project (breadcrumb "Workspace › canvas-project", subfolder contents listed). CAUGHT A REAL BUG only visible in the running app: DirEntry serialized is_dir (snake_case) while the frontend reads isDir, so folders rendered as files and clicking one hit "read failed: Is a directory" — fixed with #[serde(rename_all = "camelCase")] (the Rust unit test used the Rust field name so it passed regardless; the serde-boundary mismatch surfaced only in real-app verification). 127 JS (3 new) + 29 Rust (2 new) tests, tsc, lint, vite build green; installed to /Applications.

2026-07-04 02:45 · Native genome-track viewer shipped (P1-3's "then … genome tracks" — now BOTH a structure renderer and a track renderer ship, beyond the "at least one domain renderer" acceptance). New lib/genome.ts: pure, offline parser for BED / bedGraph / GFF3 / GTF / VCF → a uniform feature model, all coords normalized to 1-based inclusive (BED 0-based half-open, VCF POS+REF span, GFF/GTF as-is), attributes → name (GFF Name/gene_name/ID; GTF gene_name/gene_id), grouped by contig (busiest first), 50k-feature cap; plus greedy packRows so overlapping features stay visible. New GenomeView.tsx: an SVG track viewer — bp ruler with 1/2/5×10ⁿ "nice" ticks, features as row-packed rects colored by type (GFF/GTF) or strand via theme-aware --series-* tokens, contig <select>, hover tooltip (name/coords/strand/type/score), drag-to-pan + scroll-to-zoom (cursor-anchored, clamped to the contig), zoom/reset buttons, legend, MAX_ROWS/truncation notes. Wired the previewer registry: artifacts.ts previewKind "genome" for bed/bedgraph/bdg/gff/gff3/gtf/vcf (+REF_EXTS/MIME/EXT_KIND), FilePreviewInspector routes it with a Preview/Code toggle, Rust mime_for serves them as utf8 text so on-disk files reach the viewer. VERIFIED end to end on the packaged app: opened a real GFF3 (gene/mRNA/exon/CDS across chr1+chr2) → tracks rendered with the type legend, gene/mRNA on one row and exons/CDS packed below, contig selector "chr1 (10)", range readout; clicking zoom-in narrowed the view 720–15,280 → 5,379–10,621 and the ruler re-ticked (6k–9k) — interactive zoom confirmed. Debris removed. 124 JS (12 new: 8 parser + 4 view) + 27 Rust tests, tsc, lint, vite build green; installed to /Applications.

2026-07-04 02:35 · Windows robustness fix toward P1-4 (Windows parity). Audited the cross-platform paths since Windows is the one requirement whose acceptance can't be met/verified on this macOS host (needs a Windows runner + code signing — CI already builds NSIS/.msi via the build.yml matrix, and both sidecar fetch scripts correctly emit *-x86_64-pc-windows-msvc.exe). Found and fixed a real Windows-only defect: start_jupyter's orphaned-jupyter-lab cleanup (a crash/force-quit leaves one holding the fixed port; a second instance then wedges — the exact bug the Unix path guards against) was #[cfg(unix)]-only, so Windows had NO cleanup. Extracted it into kill_orphan_jupyter: Unix keeps the proven, path-scoped pkill -f <env>/bin/jupyter-lab (zero behavior change → no macOS regression); Windows now taskkills the recorded PID filtered to IMAGENAME eq python.exe (so a recycled PID on another process is spared). The managed jupyter-lab's PID is now written to jupyter-env/jupyter.pid on spawn for that precise, portable kill. VERIFIED: macOS unix path is a pure refactor (27 Rust tests green, no warnings); the Windows branch typechecks against x86_64-pc-windows-msvc (rustc --emit=metadata on the extracted std-only logic — llvm-rc for a full cross-build isn't on this host, but CI compiles the whole app on windows-latest). Remaining P1-4 gap is inherent to the host: producing + signing the installer and a real first-run on Windows can only happen on a Windows machine/CI.

2026-07-04 02:25 · Package-level environment capture shipped (closes P0-3's last gap — was pip-freeze/lockfile). Every provenance record now captures the installed Python packages, not just python/OS/app version. Design keeps the store small: pip freeze runs ONCE per app run (cached in a OnceLock, same as python_version — a per-write spawn would tax every agent edit), its output is written content-addressed to .openscience/env/<hash>.txt (DefaultHasher's fixed keys → deterministic addressing; identical environments dedupe to one lockfile, only written if absent), and the record's env carries just {count, hash}. New Rust read_env_lockfile(hash) (hash validated hex, can't escape the env dir) + write_lockfile/content_hash; capture_env now takes the workspace root. Frontend: ProvenanceEnv gains packages; ProvenancePanel shows an "N packages" chip that lazy-loads and reveals the full pip-freeze list in a scroll box; the Reproduce prompt now points the agent at the lockfile to reinstall matching versions if a result differs. VERIFIED end to end on the packaged app: a real agent turn wrote demo_pkg.py → its record carried packages {count 664, hash b19b27e709614985}, the 34 KB lockfile appeared at .openscience/env/b19b27e709614985.txt (real freeze: absl-py==2.1.0 … 664 pkgs), and the History panel rendered the "664 packages" chip → click revealed the scrollable list. Debris removed (file, provenance line, lockfile). 112 JS + 27 Rust tests, tsc, lint, vite build green; installed to /Applications.

2026-07-04 02:15 · Local R kernel shipped (closes P0-2's last gap — R was the only not-started P0 item). The notebook now runs Python OR R on a persistent local kernel, keyed by the notebook's kernelspec language. New base-R-only bridge runtime/kernel/kernel_bridge.R (no IRkernel/jsonlite/ZMQ — runs against any installed R, offline): holds one global env across cells, mirrors Jupyter semantics (final visible expression → result, intermediate visible values + cat/message → stdout, warnings inline via options(warn=1)), and emits the SAME JSON response shape as the Python bridge so Rust's read side is shared. kernel.rs generalized to a HashMap of kernels-by-language; request side branches (Python: inline JSON; R: host writes the cell to a code file, then pokes the id — zero escaping on the request). Rscript discovered via absolute-path candidates (incl. /opt/homebrew + /Library/Frameworks/R.framework) so the Finder-minimal-PATH GUI app finds it. Frontend: notebook-file.ts reads/writes the kernelspec (python↔R, code cells stay cell_type "code"); NotebookEditor shows a kernel badge, runs R cells, dispatches per-language; Notebooks page "New notebook" is now a Python/R menu. VERIFIED end to end on the packaged app (installed R 4.6.1 via brew to test): created an R notebook → cell ran cat+summary+sd → correct stdout ("mean is 4"), summary table, and last value [1] 2; a second cell used x from the first (y <- x*10[1] 20 40 60, sum(y)[1] 120), proving cross-cell shared state; a syntax error rendered R's Error: <text>:1:5: unexpected symbol with the caret. Rust r_round_trips_and_keeps_state drives real Rscript over the exact protocol. 111 JS + 26 Rust tests, tsc, lint, vite build green; installed to /Applications. (IME note: pinyin input mangled typed <-/quotes during UI automation — verified via clipboard paste instead.)

2026-07-04 00:55 · Molecule preview is now INTERACTIVE 3D, not a static 2D image (P1-3 redesign, per user + borrowing OpenClaudeScience's 3Dmol.js approach). Replaced the openchemlib→static-SVG renderer with a 3Dmol.js WebGL viewer: drag to rotate, wheel to zoom, stick/sphere/cartoon toggle, reset, live atom count. Expanded formats from 4 to 12 — cif/mcif/mmcif/pdb/mol/mol2/sdf/xyz/pqr/cube all render directly; macromolecules (proteins/crystals, detected via HELIX/SHEET or many Cα) open in cartoon, small molecules in stick. SMILES (.smi/.smiles) has no coordinates, so it is converted to a coordinate-bearing molblock via openchemlib and handed to the same viewer — one code path, no static fallback. molecule.ts is now pure helpers (format map, macromolecule heuristic, default style, smilesToMolblock), unit-tested; the WebGL component is tested with 3Dmol mocked (asserts model+format handed over, style re-applied on toggle). Rust mime_for serves all 12 as utf8 text so on-disk files reach the viewer. 3Dmol + openchemlib both lazy-loaded into their own chunks (out of the main bundle). NOT screenshot-verified on the packaged app (headless WebGL); covered by build + mocked-viewer tests + a real openchemlib coordinate-generation test. 108 JS + 24 Rust tests, tsc, lint, vite build green.

2026-07-04 00:15 · Pre-commit review pass (27-agent high-effort review of the pending work): 10 confirmed defects fixed before the commit. Worst three would have shipped broken features — SDF records with a BLANK title line parsed to 0 atoms (trim() shifted the fixed-position molfile header; delimiter regex now consumes the $$$$ newline instead), on-disk .mol/.sdf/.smi files never rendered (mime_for served them as base64 → viewer text stayed null; now text types), and Reproduce silently overwrote text the user was typing (draft now appends). Also: reproduce prompts fence-escape embedded ``` and flag truncated records; Slurm array/het job ids ('123_[0-15]', '123+0') can be canceled (charset + remote single-quoting); squeue --me → `squeue -u "$USER"` (works on Slurm < 20.02); ssh host-key policy accept-new → strict `yes` with a run-`ssh `-once message (CLAUDE.md safety default: no silent first-contact trust); ssh-config aliases the connect path would reject are no longer suggested (and '+' allowed in hosts); corrupt hpc.json now surfaces a toast instead of a silent empty card + unhandled rejection; hpc-slurm skill's scp recipe used a $REMOTE that never survives across shell calls — replaced with the literal remote dir. 103 JS + 24 Rust tests, tsc, lint green.

2026-07-03 23:05 · Non-bio example project shipped (P1-1's last gap + P0-1's example gap). examples/climate-trends/: the REAL NASA GISTEMP v4 land-ocean global means CSV (12.9 KB, public domain, retrieved 2026-07-03, citation in its README) bundled into the installer as a resource. New Rust install_example (allowlisted names; recursive copy that NEVER overwrites — a re-install keeps user edits, unit-tested) + a 4th workflow starter "Explore an example: climate trends" whose prepare hook installs the files before sending the analysis prompt (install failure → toast, prompt NOT sent). VERIFIED on the packaged app: one click → README+CSV appear in the workspace → the agent (free mimo) loaded the real data (skiprows=1, ***→NaN), ran OLS via scipy, and produced warming_trend.png (190 KB) + report.md with defensible numbers (+0.08 °C/decade full record, +0.21 °C/decade 1975–present, R²=0.90, full decadal table, dataset citation) — a genuinely non-bio, real-data showcase. Testing gotcha: vitest/tinyspy derives an extra promise from a REJECTING vi.fn (its result tracking), reported as an unhandled rejection — mock rejection paths with plain closures instead. E2E debris removed. 90 JS + 23 Rust tests, tsc, lint green; installed to /Applications.

2026-07-03 22:55 · Provenance env capture + Reproduce shipped (closes P0-3's two gaps). Every new provenance record now carries env {python version (detected interpreter, cached once per app run), os-arch, app version}; History panel shows it as a chip next to the model. New per-version Reproduce action: drafts a structured prompt (recorded code + env note + "compare regenerated vs current") into the composer of the originating session — prefilled, never auto-sent, human in the loop. Plumbing: composerDraft one-shot field in the ui store, consumed by the Composer on render. VERIFIED on the packaged app end to end: real agent turn wrote demo_env/mean_demo.py → record carried env {python 3.11.7, macos-aarch64, app 0.1.0}; History showed py 3.11.7 · macos-aarch64 · app 0.1.0; clicked Reproduce → composer prefilled → sent → agent re-ran the recorded code, diffed, replied "files match exactly — nothing changed". Old records without env render unchanged (field optional). UI automation note: a concurrent agent on this machine steals focus — clicks now retry until the app process is verifiably frontmost. Debris removed (demo session, files, records). 88 JS + 22 Rust tests, tsc, lint green; installed to /Applications.

2026-07-03 22:45 · HPC / Slurm over SSH shipped (P2-2, was the only not-started requirement). Shape: the app drives the user's cluster with the system ssh + their own keys — nothing installed remotely, no credentials of ours. Rust hpc.rs: parse ~/.ssh/config Host aliases (wildcards/comments skipped), probe host (sbatch --version, distinguishes unreachable vs no-Slurm), list (squeue --me, name last so pipes in job names survive) and cancel jobs; host/job-id validated against option/shell injection (unit-tested). Chosen host lives in <workspace>/.openscience/hpc.json — the single source shared with the agent. Settings "Cluster (HPC)" card: connect (datalist of real ssh-config hosts + free text; unreachable hosts are NOT saved), status line (green/amber dot + slurm version), live queue with per-job cancel, Remove. New bundled skill hpc-slurm: read hpc.json (never guess the host), write sbatch script INTO the workspace (provenance picks it up), scp+sbatch, track via squeue→sacct (quote the state, don't assume success), fetch outputs back. VERIFIED end to end on the packaged app against a localhost "cluster" (real sshd + fake sbatch/squeue/scancel/sacct shims): card connect→"slurm 23.11.4"→queue row→cancel (toast + state file emptied over real ssh); then a real agent turn followed the skill mechanically — wrote slurm/os-e2e.sbatch (provenance v1), submitted (job 7744), read the empty output, self-corrected the script (v2), resubmitted (3258), sacct COMPLETED, fetched slurm-3258.out (sum=100) into the workspace and reported the quoted state. All test scaffolding removed after (shims, .zshenv PATH line, authorized_keys entry, workspace/session debris). 87 JS + 22 Rust tests, tsc, lint green; installed to /Applications.

2026-07-04 03:00 · Fixed a session-deadlock bug the user hit (P2-4): the agent's question tool (pick-an-option) and OpenCode permission prompts rendered as blank/stuck rows with NO way to respond — the run just hung, unusable. Wired OpenCode's interactive-request system end to end. SDK: normalize question.asked/question.v2.asked (+ replied/rejected) and permission.asked/.v2. into typed events; new methods answerQuestion/rejectQuestion/replyPermission/listQuestions/listPermissions. IMPORTANT contract discoveries (verified against the live server, not assumed): these are DIRECTORY-scoped GLOBAL endpoints GET/POST /question[/:id/reply|reject] and /permission[...], NOT the /api/session/:id/... variants (those returned empty); reply body is {answers:[[label,…],…]} / {reply:"once"|"always"|"reject"}; and the workspace query param is a wrk_ id — passing a path 500s the server, so send only ?directory=<path>. Store holds pending questions/permissions, updates from live events, and recovers them on session open (an ask can predate connect/reload). New InteractionPrompt card renders above the composer: single-select answers on click, multi-select/custom behind Submit, permission Allow-once/Always/Reject; blank interactive tool rows suppressed from the thread. VERIFIED on the packaged app's real runtime: triggered the question tool → answered via the corrected endpoint → question went running→completed and the agent PROCEEDED with the chosen file (read atlas.csv, began analysis). Live card render not screenshot-verified — a second autonomous agent on this machine kept moving/closing the app window across displays — but the UI is covered by component tests (quick-pick, multi-select, reject, permission) and the wiring is tsc-clean. 83 JS + 17 Rust tests, tsc, lint green; installed to /Applications.

2026-07-03 13:10 · Chart design system + command palette + empty-session redesign (P1-5). One validated palette (dataviz standard, checked against the app's real light #ffffff / dark #1e1d24 surfaces) is the single source of truth in three synced places: @ai4s/shared chartPalette (categorical/sequential/status, light+dark), index.css --series-* tokens, and runtime/skills/core/publication-figures/openscience.mplstyle — a first-party skill that makes every agent matplotlib figure publication-grade (2px lines, hairline y-grid, no top/right spines, left title, frameless legend) in the SAME 8 categorical hues as native UI. VERIFIED: rendered a 4-series figure with the style → exact palette (#2a78d6/#1baf7a/#eda100/#008300), clean chrome. Command palette rewritten so all 7 items are real actions (was 3 dead ones that just closed): new session, analyze-data + audit-report workflows (send the starter prompts), notebooks, skills, settings, theme — verified all visible in the packaged app. Course-correction on user feedback: my first cut put a Sessions/Notebooks/Skills stat-tile row on the empty session — vanity metrics that dominated the view; removed it (and the speculative StatTile/WorkspaceGlance) and redesigned the empty session as a quiet, centered welcome (serif headline, one refined 3-row starter card) — screenshot-confirmed. Dark mode is token-only (no hardcoded color), structurally assured. NOTE: the app got moved to a second display mid-verification; combined with a second autonomous agent on this machine, UI automation is unreliable here — a blind keystroke once landed in another app's browser form (no submit; harmless but noted) — so I verify via window-scoped capture when possible and structure otherwise. 78 JS + 17 Rust tests, tsc, lint green; installed to /Applications.

2026-07-03 12:15 · Curated open-source science connectors, one-click (P1-2). Rather than reimplement literature/DB access, we one-click provision EXISTING open-source MCP servers into a shared isolated env via the bundled uv, then register them — same shape as the Jupyter integration. New Rust science_mcp (setup_science_mcp installs one pip package into runtime/science-mcp-env and returns the managed python; science_mcp_python reports it; package name validated against arg/shell injection, unit-tested). Curated catalog lib/scienceConnectors.ts: paper-search-mcp (arXiv/PubMed/Crossref/Semantic Scholar/bioRxiv) and biomcp (PubMed/ClinicalTrials/variants), launched as python -m <module>. Settings MCP card shows them as "OPEN SOURCE" rows with source repo + Enable. New docs/CONNECT_YOUR_TOOLS.md (BYO MCP local/remote + minimal fastmcp example + skill install + safety). VERIFIED on the packaged app's real runtime: provisioned paper-search-mcp through the bundled uv into the app env → registered via the live config API → OpenCode reports it connected → drove a real agent turn: paper-search_search_arxiv returned 3 papers with real arXiv ids (incl. 2207.07410 AlphaFold-knots), exactly the auditable identifiers traceability-review consumes. (UI Enable button is the same enable path as Jupyter, tsc-checked; verified the underlying provision+register+connect chain directly because a second autonomous agent sharing this machine made window screenshots unreliable.) Verification debris removed (config entry, env, test sessions). 77 JS + 17 Rust tests, tsc, lint green; installed to /Applications.

2026-07-03 11:55 · First-boot reliability fix (P2-4): connectRetry window raised 30×0.5s → 120×1s. Root cause observed twice today: on a fresh install (or any re-signed rebuild) macOS TCC's "access Documents" consent blocks the sidecar inside getcwd until the user answers, so the webview's ~15–30 s retry budget expired and stranded users on an error screen a single manual Connect would fix. Verified on the packaged app: relaunch → TCC allowed → auto-connected in ~9 s with no manual Connect. 77 JS + 15 Rust tests, tsc, lint green; installed to /Applications.

2026-07-03 11:50 · One-click full workflows shipped (P0-1). The empty live session now shows three starter cards (components/thread/WorkflowStarters): "Demo: analysis end to end" (simulate → fit → figure → report, numbers traced to code), "Analyze my data", "Audit a report" (routes into traceability-review). One click sends the complete-workflow prompt. VERIFIED on the packaged app: single click → 65 s → run_analysis.py + figure1.png (real 4-param logistic fit with 95% CI band, R²=0.985) + report.md (numbers from code output) + summary_stats.json, all as clickable artifact chips with provenance records; figure renders natively in the right pane. Also re-observed the boot race (P2-4): webview's 30×connectRetry gives up while a TCC-delayed sidecar is still booting — one manual Connect fixes it; a longer/smarter retry is the natural next reliability fix. 77 JS + 15 Rust tests, tsc, lint green; installed to /Applications.

2026-07-03 11:40 · Plain-language data-flow disclosure shipped (closes P0-2's statement gap + P2-3). New "Privacy & data flow" card in Settings (components/settings/DataFlowCard): two columns — stays local (workspace files with real path, local kernel/Jupyter execution, session history + provenance, credentials in an app-private mode-600 file) vs. sent to the provider (messages + task-relevant file/command content, only during a turn, provider policy governs retention) + a skills/MCP third-party note; model chip is live (verified both "no model configured" and opencode/mimo-v2.5-free states on the packaged app). Copy promises scope, not guarantees — unit test asserts no "no errors / zero hallucination" phrasing. IMPORTANT correction while verifying: provider credentials are NOT in the OS keychain — OpenCode stores them in app-private auth.json (mode 600); REQUIREMENTS P2-3 status fixed, keychain migration recorded as open. 75 JS + 15 Rust tests, tsc, lint green; installed to /Applications.

2026-07-03 11:35 · Reviewer's three traceable checks shipped (P0-4). New first-party skill runtime/skills/core/traceability-review (bundled as the skills-core/ resource; deploy_bundled_skills now syncs both packs and skips SKILL.md-less placeholder dirs): citation audit via Crossref/arXiv/PubMed public APIs, untraceable-number flagging, and figure↔code staleness read from provenance.jsonl — output is the existing ```review contract, now with a check field (citation|number|figure). UI: ReviewerCard findings carry a check-type tag and are dismissible one by one (session-local; "N dismissed" in the header, "All findings dismissed." when empty). VERIFIED end to end on the packaged app with a fixture report (fake DOI + unsourced 37.4% + figure older than its code): the free mimo model followed the skill mechanically (Loaded skill → curl Crossref 404 → stat mtimes) and the card rendered exactly the three acceptance findings with correct levels/tags; dismiss click → "2 findings · 1 dismissed". Observed: the model used the mtime fallback because it claimed the hidden `.openscience/provenance.jsonl` didn't exist — skill now tells it to cat the file directly. Note copy never claims "no errors". 73 JS + 15 Rust tests, tsc, lint green; installed to /Applications.

2026-07-03 11:20 · Artifact provenance shipped (P0-3, the #1 gap): every successful agent write now appends a version record to <workspace>/.openscience/provenance.jsonl (workspace-relative path, version, timestamp, tool, sessionId, model, written content capped at 100 KB, log) via new Rust record_provenance/list_provenance (append-only, mutex-serialized, corrupt lines skipped, path-escape rejected); recording happens on live tool.updated events (deduped per callId; jupyter reads excluded, mutations included) — note it is live-only: turns that stream while the webview is still connecting are not backfilled, and failures now go to debug.log instead of vanishing. UI: FilePreviewInspector AND NotebookEditor gained a History toggle → ProvenancePanel (newest first, latest expanded: code, model chip, log, "Open conversation" → /live/:sessionId); artifact cards are now fully row-clickable. VERIFIED end-to-end on the packaged app: three API-driven real agent turns produced v1/v2 (two writes, one turn) and v3 (edit from a second session, correct cross-session increment); History panel screenshot-confirmed on the final build. Found along the way: rebuilt (re-signed) apps re-trigger macOS TCC "Documents" consent, and the pending dialog wedges the sidecar in getcwd — looks exactly like the app hanging; also open -a "Open Science" can launch the repo-target bundle instead of /Applications (use the explicit path). 72 JS + 15 Rust tests, tsc, lint green; installed to /Applications.

2026-07-03 09:40 · Live notebook UX fixes from the user's first real Jupyter-MCP run (4 real bugs). (1) Tool rows went blank on completion: finished MCP parts report title "" and ?? tool doesn't catch empty strings — now title || tool, unit-tested. (2) No live panel while the agent drives a notebook: jupyter_* tools now derive a notebook artifact from their notebook_path, and the session auto-opens that notebook in the right pane (once per path, manual close respected); NotebookEditor polls the file every 2 s while idle so the agent's cell inserts/executions appear live (own saves excluded via raw-text compare). (3) Figures were invisible: image/png outputs now parse into NotebookCell.image, render as , and round-trip through serialize. (4) Duplicate jupyter-lab after restart: app quit left an orphan; the fixed port then wedged and all MCP calls timed out — start_jupyter now pkills the env's stale jupyter-lab first. Verified on the packaged app via an API-driven real agent turn + window-scoped screenshot (no focus stealing): tool names persist, right pane auto-opens, scatter figure renders. 65 JS + 12 Rust tests, tsc, lint green.

2026-07-03 08:50 · Jupyter MCP integrated, one-click, in the installer (user ask). Best-practice shape: bundle uv as a second sidecar (fetch-uv.sh pinned 0.11.26, +~20 MB dmg → 67 MB) instead of bundling Python; "Set up & enable" in the MCP card provisions an ISOLATED env in app data (uv venv + pinned jupyterlab 4.4.1 / jupyter-collaboration 4.0.2 / jupyter-mcp-server / ipykernel — ~361 MB, user's system untouched), the app manages a headless jupyter-lab (new jupyter.rs: stable port+token persisted in server.json so the MCP entry survives restarts; root_dir = workspace so agent and Notebooks page share files; killed on exit; auto-restarts on launch via ensureJupyter), and the MCP entry (JUPYTER_URL/TOKEN/ALLOW_IMG_OUTPUT) is written into OpenCode's config. VERIFIED end-to-end on the packaged app: clicked the real button → env provisioned → jupyter-lab up → GET /mcp reports jupyter "connected" — the agent now has notebook tools. Placement note: MCP in Settings matches Claude Desktop (Connectors), Cursor (Settings→MCP), Goose (Settings→Extensions). 62 JS + 12 Rust tests, tsc, lint green.

2026-07-03 08:20 · MCP server management (user ask: skills + MCP support). New "MCP servers" card in Settings: rows show name, local/remote type, LIVE status (connected/failed/disabled dot from GET /mcp) and the command/URL; add form (name, type, command-or-URL) writes via PATCH /global/config (applies live, verified); Remove goes through the generalized remove_config_entry(section, key) Rust command (replaces remove_custom_provider; allowlist provider|mcp; pure JSON transform tested) + sidecar restart — full add/remove loop E2E-verified on the packaged app (clicked Remove in the real UI, config file confirmed cleaned). SDK: listMcpServers (status ⋈ config), addMcpServer, McpConfig/McpServer types. Skills were already manageable (live list + install-with-agent on the Skills page); fixed its stale .opencode/skill/ prose to skills/. 62 JS + 12 Rust tests, tsc, lint green; installed to /Applications.

2026-07-03 08:00 · Settings visual redesign (user: elements looked bad). Rebuilt the page as a consistent card system in the app's paper aesthetic: serif-titled cards with hairline-divided headers (Agent runtime / Model / Workspace / Appearance); one shared control kit (inputCls/btnGhost/btnAccent — every input and button h-9, 13px); providers as a single bordered list container (Zen gets an uppercase ring badge, connect-search row with inline icon on a tinted band, custom endpoint as a chevron disclosure row); dashed borders eliminated; select got a custom chevron; Appearance became a segmented control; runtime status line now shows the active model inline; import-CLI-login is an icon'd tertiary action. No logic changes. 62 JS + 12 Rust tests, tsc, lint green; installed + screenshot-verified.

2026-07-03 07:45 · Custom endpoints + provider-row semantics. (1) New "Custom endpoint" form in Settings (name, OpenAI-/Anthropic-compatible style, base URL, optional key, model ids) for self-hosted gateways and LOCAL Ollama (catalog only has ollama-cloud); writes provider config via PATCH /global/config (verified live: models selectable immediately, no restart); removal via new remove_custom_provider Rust command (edits opencode.jsonc + restarts sidecar; pure JSON transform unit-tested). Noted: Kimi coding plan (kimi-for-coding), DeepSeek, zhipu/minimax coding plans, LM Studio were ALREADY in the 150-provider catalog — searchable directly. (2) Fixed provider-row action semantics (user report): OpenCode Zen is built-in with no stored credentials — its dead "Disconnect" replaced with a "Built-in · free" label; other providers' action renamed Remove (credentials/config) to stop colliding with the runtime's Disconnect. 62 JS + 12 Rust tests, tsc, lint green; installed + screenshot-verified.

2026-07-03 07:25 · Settings provider fixes + workspace rename (3 user questions). (1) The connect list was wrongly sourced from GET /provider/auth (only 10 special-flow providers — Anthropic wasn't connectable); now a searchable box over the FULL catalog GET /provider (~150 providers, datalist autocomplete). Every provider gets a generic API-key row (PUT /auth works universally, placeholder shows its env var); special OAuth flows (incl. Copilot-style extra prompts — select/text inputs now supported and passed as authorize inputs) stack on top. (2) Clarified: the "MiMo V2.5 Free" default model was set by ME while verifying PATCH /global/config — not a prior user config; left in place as a working out-of-box default. (3) Workspace renamed ~/Documents/"Open Science" → ~/Documents/OpenScience (agent shell commands break on unquoted spaces); migration chain: OpenScience > rename "Open Science" > rename old app-data dir; verified live — files moved, old dir gone, sidecar cwd = new path. 62 JS + 11 Rust tests, tsc, lint green; installed to /Applications, search box verified by screenshot.

2026-07-03 07:05 · Removed the Review button from the artifact inspector (user: unnecessary). The ```review → ReviewerCard rendering stays — reviews surface when the agent produces them (skills / chat), not via a dedicated button; unused reviewPrompt deleted. 62 JS tests, tsc, lint green; rebuilt + installed.

2026-07-03 06:50 · Unified model configuration to ONE OpenCode-native path (user: configure OpenCode itself, not raw model keys). Settings rewritten around the sidecar's own API: Default model = grouped dropdown from GET /config/providers (includes OpenCode Zen free models — works out of the box), saved via PATCH /global/config (verified live: applies without restart); Providers = connected list with Disconnect (DELETE /auth) + connect flow per provider from GET /provider/auth (API key → PUT /auth/{id}; OAuth → authorize → system browser → paste code → callback); silent seed_auth copy REMOVED from sidecar spawn — now an explicit "Import OpenCode CLI login" button (new import_opencode_login command, restarts sidecar). Removed the old provider/key/baseURL form and two fake controls (workspace path input → real path + Reveal; Docker backend select). Sidebar Model pill is now real (shows the configured model name from /config). SDK: 8 new client methods + mock routes. Old configure_opencode Rust command kept but no longer wired to UI (candidate for an Advanced custom-endpoint flow or deletion). 62 JS + 11 Rust tests, tsc, lint green; installed to /Applications, verified by screenshot (model dropdown, Zen provider row, workspace path all live).

2026-07-03 06:35 · Conversation-first notebook UX (user's design: chat is the main surface, the agent drives notebooks, session ↔ notebook mapping visible). NotebookEditor extracted into a shared component (components/notebook/) with a new Reload button to pick up the agent's file edits. (1) .ipynb artifacts now open in the RUNNABLE notebook editor in the right pane next to the conversation (new "notebook-file" inspector variant) instead of raw-JSON preview. (2) Session header shows chips for every notebook the agent touched in that session (deduped artifact blocks) — click opens it beside the chat; active one highlighted. Notebooks page stays as the global library. Kernel-level agent control still pends the jupyter-server + jupyter-mcp route (PROGRESS 05:00 note). 62 JS + 11 Rust tests, tsc, lint green; installed to /Applications, editor re-verified (previous run's output loads back from file).

2026-07-03 05:50 · Closed two Claude-Science gaps: Notebooks entry + real Reviewer cards. (1) New sidebar "Notebooks" page: lists workspace .ipynb (new Rust list_notebooks), create/open/edit/delete cells, Shift/⌘+Enter runs on the real local Python kernel, debounced autosave writes valid nbformat back (new write_workspace_file, sandboxed) — the agent works on the same files. Verified END-TO-END by UI automation on the packaged app: clicked through sidebar → notebook → ran a cell → output "42" rendered AND persisted into the .ipynb. Two real bugs found+fixed by that E2E: .ipynb missed the text-MIME map (read came back base64 → "could not read"), and save-in-handler raced setState (persisted "running…" instead of the result) — now a debounced autosave effect. (2) Reviewer: "Review" button on the live file inspector sends a structured review prompt (uses integrity-auditor skill); a ```review fenced JSON block in the agent's reply is parsed (splitReview) into the existing ReviewerCard — streaming-safe, malformed JSON left as text. Not done (next): versioned artifact tabs with real per-version data (#1 of the gap table). 61 JS + 11 Rust tests, tsc, lint green; installed to /Applications.

2026-07-03 05:00 · Three fixes from user feedback. (1) History delete now confirms via a new in-app ConfirmDialog (window.confirm is unreliable in the webview); examples confirm as "Hide". (2) Workspace moved from the hidden app-data dir to the OS documents folder (~/Documents/Open Science, Windows/Linux via Tauri document_dir with $HOME/Documents fallback) — user-facing files belong in user-visible places; one-time migration renames the old dir (falls back to keeping the old location if rename fails), verified live: files moved, skills still listed, sessions intact. (3) Root-caused "no Python environment": Finder-launched apps get a minimal PATH so the agent couldn't see conda/Homebrew — sidecar now gets an enriched PATH (existing anaconda/miniconda/pyenv/homebrew/.local dirs prepended); verified by launching the packaged app with env -i and confirming /opt/anaconda3/bin in the sidecar's PATH. Bundling a managed Python/R runtime (micromamba) is a pending product decision. 56 JS + 11 Rust tests, tsc, lint green; installed to /Applications.

2026-07-03 04:30 · Composer attachment UX rework (3 user items). (1) Adding files is now silent on success — no toast. (2) Attached files render as removable chips (paperclip + name + X, surface-2 background) above the input instead of plain text in it — can't be mangled while editing; on send they become a "Files added to the workspace: …" note appended to the prompt, chips clear after. (3) Input is now an auto-growing textarea (caps at 160px then scrolls; Shift+Enter for newline), and a paste >2000 chars or >25 lines is auto-saved to the workspace as pasted.txt (new add_text_to_workspace Rust command, collision-safe) and becomes a chip instead of flooding the box. 56 JS (3 new mocked-bridge attach tests) + 11 Rust tests, tsc, lint green; rebuilt + installed to /Applications.

2026-07-03 03:50 · Composer fixes from user feedback (3 items). (1) Removed the divider line above the composer (live + example pages). (2) Fixed pinyin IME: Enter during composition was sending the message instead of picking the candidate — keydown now ignores isComposing / legacy keyCode 229 (WebKit), covered by a new Composer test. (3) Added a real "Add files" paperclip button (desktop only): native open dialog → files copied into the agent workspace (collision-safe naming name-1.ext, unit-tested) → names inserted into the prompt + success toast. New Rust command add_files_to_workspace. 53 JS + 11 Rust tests, tsc, lint green; rebuilt + installed to /Applications, verified by screenshot.

2026-07-03 03:40 · UI refinement pass from user feedback (5 items). Sidebar 272→232px; logo/brand shrunk to one compact 17px line with inline BETA; session header slimmed (18px→13px title, py-4→py-2.5) and its New/Disconnect buttons removed (New lives in the sidebar; users never need Disconnect — Connect still appears when offline); composer reduced to a single input+send row; Settings/Skills titles 2xl→xl. New: external http(s) links now open in the system browser (global click interceptor in AppShell + open_url Rust command) — previously a clicked link navigated the webview away with no way back. 51 JS + 10 Rust tests, tsc, lint green; rebuilt aarch64 .app/.dmg, installed to /Applications, verified by screenshot.

2026-07-03 03:20 · Fixed three UI polish bugs from user testing. (1) Downloads now use a native "Save As" dialog (new tauri-plugin-dialog + async save_text_file Rust command; browser dev falls back to Blob) with a new bottom-center toast for saved/failed feedback (silent on cancel) — wired into ArtifactInspector + FigureBlock; removed PdfInspector's dead Download button. (2) Removed the composer's three fake buttons (Plus/Tools-grid/Mic — all were no-ops). (3) Fixed the mismatched titlebar strip: macOS window now uses titleBarStyle Overlay + hiddenTitle, sidebar reserves a draggable 32px strip under the traffic lights — window background is now one continuous surface (verified by screenshot on the packaged app). Installed the new build to /Applications (old instance quit; session history intact). 51 JS + 10 Rust tests, tsc, lint, aarch64 .app/.dmg green.

2026-07-03 03:10 · Bundled the ai4s-skills pack (7 scientific skills: research-explorer/literature-survey/experiment-suite/paper-writer/integrity-auditor/mindmap-render/ai4s-agent) into the one-click installer. scripts/dev/fetch-skills.sh pins ai4s-research/ai4s-skills@8fa2ab0 into git-ignored runtime/skills/external/ (wired into CI); tauri.conf bundles it as a resource; on every sidecar start runtime.rs::deploy_bundled_skills syncs it into the app-private profile's global skills dir (xdg-config/opencode/skills/ — the workspace .opencode/skills/ stays for user installs). Found+fixed along the way: OpenCode skill instances are lazy per-directory, so bare GET /api/skill can return [] — SDK now sends ?directory=<workspace> (new workspace_path Tauri command) and loadCatalog retries once; fixed the install-skill prompt's wrong .opencode/skill/ (singular) path. Verified end-to-end on the built .app: 7 skills deployed to the private profile and listed by the sidecar's /api/skill. 49 JS + 10 Rust tests (2 new for skill sync), tsc, lint, aarch64 .app/.dmg (46 MB) green. Known issue (pre-existing): sidecars orphan when the app is killed instead of quit — found 6 stale opencode serve processes from earlier runs (cleaned up).

2026-07-03 02:20 · Published to GitHub: https://github.com/ai4s-research/open-science (public, master). Two commits: accumulated feature work (previews/artifact resolution/kernel) + the Open Science rebrand. Secret scan on pending changes came back clean; sidecar binaries and installers stay git-ignored.

2026-07-03 02:10 · Rebranded to "Open Science" (slogan: "An open AI workbench for scientists — Your research partner for rigorous science"). README now opens with the banner (docs/assets/banner.webp) + new title/slogan; sidebar shows the cloud logo (src/assets/logo.webp) left of "Open Science"; productName/window title/index.html title renamed — bundles are now "Open Science.app" / "Open Science_0.1.0_aarch64.dmg". Deliberately unchanged: bundle identifier com.ai4s.workbench (renaming it would orphan the existing Application Support workspace/data) and internal @ai4s/* package names. AGENTS.md records the branding rule. 49 JS tests, tsc, packaged build green.

2026-07-03 01:55 · Set the app icon from the user's cloud/terminal artwork (webp, 1254px, transparent corners verified, content plate ~82% of canvas — matches macOS icon proportions). Converted to 1024px PNG and ran tauri icon: regenerated icon.icns (Dock), icon.ico (Windows), and all png/Android/Windows-store sizes in src-tauri/icons/; repacked aarch64 .app/.dmg with the new icns embedded. Note: Finder/Dock may show a cached old icon until the app is moved/relaunched.

2026-07-03 01:35 · Fixed messy Office preview styling (user: WPS looks far better). Root cause was mostly OUR CSS, not the renderers: Tailwind preflight resets (list-style, margins, img display/height) hit elements the renderers leave to browser defaults, and the app theme's inherited font/color (light text in dark mode) bled into document content expecting black-on-white. Fix: all three previews now render inside a Shadow DOM (document stylesheets can't cross it) with an explicit base reset — black text, CJK-aware font stack, neutral backdrop; docx-preview's own page chrome (white sheet on gray) now applies cleanly, pptx slides get centered cards with shadows. xlsx switched from our first-row-header grid to SheetJS sheet_to_html (merged cells → rowspan/colspan, gridline look like WPS; 500×50 cap preserved, escaping verified by test). Found+fixed along the way: useShadowPage used useRef+useEffect so a late-mounted host (XlsxView after data load) never got its shadow root — now a callback ref. Verified by mounting the real components with the user's actual docx/xlsx/pptx in jsdom (content lands inside shadow root, tables/merges present); 49 JS + 8 Rust tests, tsc, build green; repacked aarch64 .app/.dmg. Remaining gap vs WPS is inherent: font substitution (no embedded-font rendering) and renderer approximations.

2026-07-03 00:30 · Added inline docx/xlsx/pptx preview (was: "Open in the default app" only; user expects Codex-desktop-like inline viewing). All local, no conversion service, each renderer dynamic-imported into its own lazy chunk: docx → docx-preview 0.3.7 (HTML render); xlsx → SheetJS 0.20.3 from the official CDN tarball (npm's 0.18.5 is stale + ReDoS advisory) with sheet tabs + capped grid (500×50, first row as header, "charts not rendered" note); pptx → pptx-preview 1.0.7 (slide list, bundles echarts so pptx charts render). Chose SheetJS over exceljs after exceljs crashed on the real user file: workbooks containing charts hit "Cannot read properties of undefined (reading 'anchors')" in reconcile — SheetJS ignores drawings. New previewKind values docx/xlsx/pptx replace last night's "external"; FilePreviewInspector reads binary artifacts as base64 → ArrayBuffer; TablePreview extracted for csv/xlsx reuse. Verified against the user's actual canvas-project files in jsdom: docx renders DOM, pptx reports 5 slides, xlsx grids 3 sheets (chart file included); 48 JS + 8 Rust tests, tsc, vite build green; repacked aarch64 .app/.dmg. Caveat: jsdom needed same-realm bytes for jszip instanceof checks — webview unaffected. Pending: visual check in the packaged app.

2026-07-03 00:10 · Fixed file chips: only 2 of 5 generated files were clickable and index.html previewed as "not found" while canvas.pdf worked. Root causes: (a) REF_EXTS lacked docx/xlsx/pptx so Office files never became chips; (b) refs extracted from prose (bare filenames like index.html) were resolved verbatim against the workspace root — the file actually lives in canvas-project/; the pdf only worked because a stale copy sat at the root. Fixes: new Rust resolve_artifact (literal path if it exists, else bounded basename search — skips hidden/node_modules/pycache, ≤10k entries/depth 8, newest mtime wins on duplicates); AgentMessage now resolves every mention and only renders chips for files that exist, carrying the resolved path; docx/xlsx/pptx added to ref/MIME maps on both sides with a new "external" preview kind (note + "Open in the default app" button — no inline viewer for Office formats). Verified: repro test with the exact agent message extracts all 5 files; 47 JS + 8 Rust tests green (new: locate literal/bare/missing/duplicate); tsc + vite build clean. Pending: restart the app to pick up the new Rust command. · Fixed PDF preview not rendering in the packaged app (WKWebView): silent blank, no error, while "Open externally" worked. Two macOS WKWebView gaps: (a) pdf.js 6 calls Promise.withResolvers() (Safari 17.4+) — added a polyfill in main.tsx; (b) loading the worker as a .mjs module over tauri:// via workerSrc failed silently — switched to Vite's ?worker import + GlobalWorkerOptions.workerPort, so Vite bundles/instantiates it as a CLASSIC worker (new Worker(url), no type:module) that WKWebView runs reliably. Re-verified render in Chrome (no regression); rebuilt aarch64 .app/.dmg. Pending user confirmation it now renders in the WKWebView build.

2026-07-02 23:20 · Made execution-produced files first-class previewable artifacts (fixes two test findings: HTML showed code but no render; a PDF produced by running python was un-clickable prose only). Root cause: deriveArtifact only caught write/edit tool calls, so files created as a side effect of bash/code runs never surfaced. Added: (1) extractArtifactRefs() — pulls workspace file paths out of agent prose (strips backticks/quotes, ignores URLs) so canvas-project/canvas.pdf becomes a clickable chip under the message; (2) Rust read_artifact (workspace-sandboxed: canonicalize + starts_with check, 25 MB cap, text→utf8 / binary→base64 via a std-only encoder) and open_path (OS opener, per-OS); (3) new FilePreviewInspector that renders by type — HTML → live <iframe srcdoc sandbox> with a Preview/Code toggle, PDF → pdf.js (pdfjs-dist, worker bundled as an asset; packaged app has csp:null so no restriction), image → <img> (svg/png/jpg/gif/webp), text → CodeViewer — loading bytes lazily from disk or using inline write-tool content. Also fixed the kernel to run in the agent's workspace (current_dir) so notebook code sees the same files. Verified in Chrome with a real 39 KB matplotlib month-night PDF: pdf.js rendered all layers; HTML srcdoc executed canvas JS live. JS typecheck/lint/40 tests (new: extractArtifactRefs/previewKind/refToArtifactBlock) + 5 Rust tests (new: base64 vectors) green; rebuilt aarch64 .app/.dmg (45 MB), confirmed pdf.worker asset + read_artifact/open_path embedded.

2026-07-02 22:15 · Made the notebook a REAL local Python kernel (was: dispatch-to-agent stub) — the honest local-first answer to "can't this run locally". Added runtime/kernel/kernel_bridge.py: a stdlib-only persistent process speaking line-delimited JSON ({id,code}→{ok,stdout,result,error}), one shared namespace across cells, Jupyter-style last-expression value, tracebacks on error. Rust kernel.rs embeds the bridge (include_str!), spawns it (cross-platform Python detection: py/python + %USERPROFILE% dirs on Windows, python3 + /opt/anaconda3 etc. on mac/Linux — GUI apps launched from Finder/Explorer have a minimal PATH), and does synchronous write-line/read-line over piped stdio; commands kernel_execute/kernel_reset registered + killed on exit. Frontend lib/kernel.ts + NotebookInspector run cells on the real kernel in the desktop app (async, "running…" → real output), falling back to the agent/hint in browser dev. Verified: bridge driven directly (2+2→4, state persists across cells x*10, 1/0→traceback); a Rust integration test drives the real bridge over the same protocol (4 Rust tests green); JS typecheck/lint/35 tests/build green. Note: remote-cluster timers, real PDF (pdf.js + local pdflatex/xelatex — both present), and figure-byte rendering (Tauri file read) are all equally local-capable and remain as follow-ups.

2026-07-02 21:50 · Closed the gap between the three Claude-Science reference shots (live workbench) and our build (static mock + bare live chat) by making the workbench interactive and wiring the live agent to it. (1) Live artifact surfacing: the SDK was dropping tool state.input/output; now it captures them, a pure deriveArtifact classifies file-writing tool calls (figure/script/report/table/notebook by extension), and foldEvent/historyToThread surface them as deduped ArtifactCard blocks openable in the live inspector — a real agent session now shows the files it produces instead of only text+tool rows. (2) Figure annotation authoring (shot 1): click a figure to drop a numbered pin, write a note, Send → forwarded to the agent as a follow-up prompt (coords clamped to bounds); multiple pins. (3) Artifact version switching + downloads (shot 1): per-version code/log/review; prev/next chevrons + pill switch content; Download writes a real Blob. (4) Notebook expression input (shot 2): the "Type an expression and press Enter" line is now a working input that appends a cell and dispatches to the agent's kernel. Verified in a real browser: v1↔v2 switches code and the Review check, annotation Send box appears, notebook input renders. typecheck/lint/35 JS tests (new: artifacts + foldEvent-artifact + figure-authoring)/build all green.

2026-07-02 11:10 · Bundled OpenCode as a Tauri sidecar (one-click install) with full isolation from any user-installed OpenCode. Rust runtime module spawns the bundled binary on a dedicated free port, with an app-private XDG config/data dir (~/Library/Application Support/com.ai4s.workbench/runtime/), --cors "*", and kills it on exit; configure_opencode writes the key into that private config and restarts the sidecar. Frontend auto-starts + connects on launch (browser dev still uses a manual server). Sidecar binary is git-ignored + fetched by scripts/dev/fetch-opencode.sh (wired into CI). Verified end-to-end on the built .app: bundled opencode runs from inside the bundle on a random port (e.g. 54229) while a user's own opencode serve on 4096 keeps running untouched; global ~/.config/opencode unchanged; app auto-creates sessions on its bundled runtime. dmg 44 MB. Rust 3 tests + JS 24 tests + typecheck/lint/build green.

2026-07-02 10:05 · Wired the in-app API key into OpenCode (closes the "usable" gap): Tauri Rust command configure_opencode merges provider key + model + baseURL into ~/.config/opencode/opencode.json (3 Rust unit tests). Settings "Save" calls it (browser falls back with guidance). Verified end-to-end at the OpenCode level: writing that config flipped OpenCode's error from "Model not found" to "invalid x-api-key" — i.e. it now reads the config, resolves the model, and uses the key; a valid BYOK key completes a real turn. typecheck/lint/24 JS tests/3 Rust tests/build all green.

2026-07-02 09:35 · Switched agent runtime Hermes → OpenCode (pinned v1.17.13). Rewrote packages/sdk to the real OpenCode HTTP+SSE protocol (OpenCodeClient: POST /session, POST /session/:id/prompt_async, GET /event SSE; normalizes message.part.updated text/tool + session.idle/error). Installed the real OpenCode binary, ran opencode serve, and drove the actual app against it: verified GET /event 200, POST /session 200, live "OpenCode · ready", prompt sent (204), streamed events rendered. Found+fixed two real bugs while driving it: (a) unbound fetch → "Illegal invocation" in browser; (b) session.error message nested at error.data.message. Labeled the three mock sessions honestly as "Examples"; live agent work is the OpenCode-backed "New" session. Full agent turn still needs a configured model (opencode auth login or a provider key) — currently surfaces the real "Model not found" error. typecheck/lint/24 tests/build all green.

2026-07-02 09:00 · Added the Tauri 2 desktop shell (chosen architecture: bundle Hermes as a sidecar for one-click install). Installed Rust 1.96; scaffolded apps/desktop/src-tauri (Cargo/tauri.conf/main/lib + shell plugin), generated app icons, and built a real macOS bundle: AI4S Workbench.app + AI4S Workbench_0.1.0_aarch64.dmg (5.0 MB, shell only — Hermes not yet bundled). Verified the .app launches and runs the UI. Added GitHub Actions matrix (macOS aarch64/x86_64 + Windows x86_64) to produce .dmg and NSIS/.msi installers — Windows must be built in CI. Confirmed real Hermes uses an OpenAI-compatible API server (hermes gateway, 127.0.0.1:8642, Bearer API_SERVER_KEY, streams chat.completion.chunk + hermes.tool.progress) and owns provider keys; SDK protocol still to be re-pointed from the placeholder gateway to this.

2026-07-02 08:20 · Slice #2 (Hermes integration layer): packages/sdk HermesClient speaks the Hermes TUI Gateway JSON-RPC contract over WebSocket (session.create/prompt, streamed message.delta/tool.start/tool.complete/approval.request/session.done), transport injectable. Wired into the desktop: live /live session folds streamed events into thread blocks, real runtime status in the sidebar + Settings Gateway URL + Connect. Proven against a protocol-compatible mock gateway both in an integration test and live in the browser UI (connect → prompt → streamed tool + text + done). 24 vitest tests, typecheck/lint/build green. Note: a real agent turn still needs the actual Hermes binary + a model API key (kept empty per requirement).

2026-07-02 08:05 · Slice #1 (UI shell + static workspace) built browser-first: pnpm workspace + Vite/React/TS/Tailwind/Radix. Three-column Claude-Science-style UI (sessions sidebar / thread / contextual inspector) with warm paper theme, reproducing all three reference screenshots (figure+artifact, table+notebook, literature+PDF) from mock data. Settings has fillable, empty API-key fields with Hermes as runtime. Verified on Mac: typecheck ✓, eslint ✓, 19 vitest tests ✓, vite build ✓; screenshots confirmed against references.

2026-07-02 07:04 · Initialized project skeleton: monorepo directory tree (apps / packages / runtime / docs / examples / scripts), AGENTS.md + CLAUDE.md symlink, README, MIT LICENSE, .gitignore, and English PRD + TECHNICAL_DESIGN docs. No build tooling yet.