Skip to content

Release v0.4.0 - #59

Merged
linletian merged 53 commits into
mainfrom
release/v0.4.0
Aug 12, 2026
Merged

Release v0.4.0#59
linletian merged 53 commits into
mainfrom
release/v0.4.0

Conversation

@linletian

Copy link
Copy Markdown
Owner

Release v0.4.0 — Reasonix native UI integration, PTY in-memory ring buffer logs, and workspace visibility improvements.

CHANGELOG updated (## Unreleased## v0.4.0 (2026-08-12)), plus 4 backfilled Highlights entries (text file preview, branch divergence badge, mw config regen, tags config directory).

linletian and others added 30 commits May 26, 2026 22:25
- effective head calculation: prefer origin if ahead > local, otherwise
  pick the most-ahead remote among non-origin remotes
- aheadCount via git rev-list --count local..effective
- branchExists check for develop branch detection
- CheckDiverged returns DivergedResult map per upstream (main/develop)
- register GET /api/worktrees/diverged (batch) and GET /api/worktree/diverged (single)
- handleWorktreesDiverged: iterate all worktrees, call CheckDiverged per branch
- handleWorktreeDiverged: resolve worktree path by id, reuse status handler logic
- main worktree always gets empty DivergedResult (isMain branch returns early)
- CurrentBranch failure returns 500 instead of silent empty result
feat: add branch divergence detection with diverge badge and API handlers
Render a small GitHub Mark icon to the right of the sidebar main
workspace project name when the main repo's git remote resolves to
github.com. Clicking opens the canonical https://github.com/<owner>/<repo>
URL in a new tab via <a target="_blank" rel="noopener noreferrer">,
with event.stopPropagation() so the icon click does not trigger the
row's selectWorktree handler.

Backend:
- Add gitx.GitHubURL(root) resolver. Prefers 'origin', then iterates
  'git remote'. Normalizes SCP / HTTPS / ssh:// forms, strips .git and
  trailing slashes, and returns '' for any host other than github.com
  (case-insensitive). GitHub Enterprise and non-Git remotes are
  intentionally not surfaced.
- /api/main response gains a github_url string field (always present;
  empty when no link can be resolved).

Tests cover all three URL formats, malformed inputs, GitHub Enterprise,
non-GitHub hosts, .git suffix, and the /api/main field plumbing.

Docs (per docs-first convention): CHANGELOG, API, ARCHITECTURE, PRD,
README.md, README.zh-CN.md.
The githubSCPRE regex hardcoded 'git@github.com:...' as the only
recognized SCP-style prefix. Repositories whose origin URL uses a
non-'git' username (e.g. a '\~/.ssh/config' alias mapping
'github.com' to 'User mywork', or a CI bot configured as
'ci-bot@github.com:...') would silently fail to surface a GitHub
icon in the sidebar — the URL was still valid, but the resolver
returned '' so the icon was never rendered.

Loosen the regex to '[^@/]+@github\.com:...' so it accepts any
non-empty user segment, mirroring the existing githubSSHRE pattern
and bringing the two ssh-shaped forms back into symmetry.

- remote.go: relax githubSCPRE; update Go doc to mention arbitrary
  user and replace the misleading 'default SSH port only' line.
- gitx_test.go: add three new parse cases (ssh config alias user,
  CI bot user, dotted user) so the relaxation is covered.
- docs/API.md: update the supported URL formats list to call out
  the arbitrary user segment.

No behavior change for existing 'git@' URLs; tests + vet + build
all green.
Eliminate ~100,000× disk write amplification caused by the old
enforceMaxLogSize loop (read 10MB + truncate + write 10MB per 1024-byte
PTY chunk). Under TUI redraw workloads, this could generate 280MB+ of
accumulated disk writes in the data directory.

Changes:
- Add RingBuffer (internal/instance/logbuf.go): bounded, thread-safe,
  in-memory ring buffer. Backing slice pre-allocated eagerly. Hot-path
  optimized with WriteString (no []byte(chunk) copy) and atomic pointer
  access (no stateMu per chunk).
- Add adaptive sizing (internal/instance/sizing.go): per-instance cap
  clamp(available/16, 16MB, 256MB), default 32MB. Global budget capped
  at 25% system RAM. User override via config.GlobalConfig.LogBufferBytes.
- Refactor Manager (internal/instance/manager.go): pumpLogs writes to
  ring buffer instead of disk. Tail/ReadSince read from buffer. Removed
  enforceMaxLogSize, logPathByID, and all LogPath references.
- Remove LogPath from ManagedInstance (internal/store/state.go).
  Old state.json files with log_path load cleanly (JSON silently ignores).
- Add PurgeOrphanLogFiles() called at daemon startup to clean up dead
  .log artifacts left by pre-buffer code.
- HTTP/MCP: budget-exceeded returns 503 with structured
  log_buffer_budget_exceeded body. UI shows dedicated modal.
- Test coverage: RingBuffer (15 tests), sizing/budget (9 tests),
  manager budget integration, backward-compat state loading.
- Docs: PRD, ARCHITECTURE §4.1, API (error shape + cursor semantics),
  CHANGELOG Unreleased section, and detailed implementation plan.

BREAKING: state.json schema — log_path field removed. Old files
continue to load; external tooling should drop log_path dependency.
- Stats API now includes daemon_cpu_percent/daemon_memory_bytes in global totals
- Per-instance stats expose memory_buffer_bytes (used) and memory_buffer_cap_bytes (cap)
- Collector refactored: system calls moved outside lock, 3-phase pipeline
- UI adds mw daemon row and buffer used/cap annotation per instance
- Add .omo/ to .gitignore for OpenCode internal session data
Halves the per-second process.NewProcess() syscall storm when the
resource monitor modal is open, reducing daemon CPU overhead.
fix: replace per-instance disk log with in-memory ring buffer
Add Diff and Content APIs, preview.html, highlight.js/marked.js vendors, security helpers, and 16 test cases. Based on docs/plans/text-file-preview/.
…ked diff

Build on the text file preview feature with three iteration-driven
additions (FEASIBILITY/TASK updated to record design deviations):

- preview.html: per-line flex containers with line-number gutter; hljs
  splitHighlightedLines tracks unclosed <span> across lines so
  multi-line tokens (block comments, raw strings) keep their colour.
  Diff keeps hljs token-level text + CSS row-level background.
- app.go: pass -c core.quotePath=false to every git call and add
  unquoteGitPath as defense-in-depth, so non-ASCII paths survive
  core.quotePath quoting in numstat, status, and diff outputs.
- app.go: handleWorktreeFileDiff synthesises a full unified diff when
  git returns empty (untracked file), with Stat pre-check + post-build
  re-check to honour the 500KB cap and \ No newline at end of file.
- app_test.go: TestUnquoteGitPath (12 cases incl. UTF-8), plus
  Synthetic / SyntheticNoNewline / SyntheticTooLarge for the new path.
- docs/API.md: document the two endpoints and the synthetic diff
  behaviour so external consumers know what to expect.
gofmt -l flagged both files for missing final newline.
Pure whitespace fix; no behavioural change.
feat(preview): line numbers, quotePath handling, synthetic untracked diff
# Conflicts:
#	CHANGELOG.md
#	docs/PRD.md
#	internal/app/app_test.go
#	internal/ui/static/index.html
PR #40 (GitHub sidebar link) and PR #42 (branch divergence detection) both
inlined a ~10-line `git remote` parser in their respective files. Git's
content-level merge cannot detect duplicate top-level symbols across files,
so the collision only surfaced as a Go compiler error after the merge. The
fix was to drop the divergent.go copy in the merge commit, leaving a single
implementation in remote.go.

This commit goes one step further: move that single implementation to a
new remotes.go file and export it as ListRemotes, so future features that
need the same helper have a clearly signposted canonical entry point
instead of being tempted to inline their own copy.

No behavior change. The two existing callers (GitHubURL in remote.go and
effectiveHead in diverged.go) now invoke ListRemotes directly.
`gofmt -l` on Linux/macOS CI rejects the file without a trailing
newline. Run gofmt to comply.
feat: surface github.com link in sidebar main workspace
整合 reasonix web UI 嵌入调研(方案 A 反代+iframe 推荐)+ 三项实测结论:
- session lease 按 session 文件路径互斥,多 worktree 各起一个 serve 可并行
- 端口以 --port-file 为权威;serve 无自动递增、web 自动 +1
- /events 每 15s 自带 : ping 保活;Cookie: reasonix_token 注入实测 200 无重定向
- 布局问题:侧栏 220px 固定,需反代 + 轻量注入解锁折叠(§9)
Add a second instance type: kind=reasonix runs a per-worktree
'reasonix serve' subprocess and renders its web chat UI in an iframe
via a same-origin reverse proxy at /rx/<id>/.

- store: ManagedInstance.Kind (tty|reasonix, omitempty, no migration)
- internal/instance/reasonix: driver with isolated REASONIX_HOME,
  ~/.reasonix config/.env symlinked in, kernel-assigned port via
  --port-file, fixed session file, cross-platform liveness
  (kill(pid,0) + TCP probe, no /proc), SIGTERM->SIGKILL stop
- manager: kind branch for Start/Stop/Restart/Delete; Reconcile
  re-attaches live serve processes on startup (R-02); Delete stops
  before wiping state dir (N-01)
- internal/app/reasonix_proxy: /rx/<id>/ reverse proxy with
  reasonix_token cookie injection, RawQuery stripped, Accept-Encoding
  forced to identity, SSE streaming, HTML URL-prefix rewrite for
  fetch/EventSource/XHR
- frontend: iframe rendering branch, create-dialog checkbox, rx badge
- docs: API §5.10, ARCHITECTURE §3.2/4.2, PRD §7, plans index +
  FEASIBILITY §9.5 security tradeoff, DEFERRED notes

Two rounds of code review applied (R-01..R-08, N-01..N-06, T-01..T-03);
follow-ups tracked in issues #43..#49. Pre-existing app test failures
(TestHandleInstanceUpdate/Reorder, real-user state dir) tracked in #43.
…r-side

Runtime issues found when exercising the MVP:

- iframe reloaded every 2s poll tick: renderWorkspace() -> ensureWebFrame()
  assigned frame.src unconditionally; guard it with a dataset.instance
  check so the embedded page only navigates when the instance changes
  (trailing slash added so in-page relative URLs resolve under /rx/<id>/).
- <img src="/assets/logo-wordmark.svg"> 404'd: the URL-prefix shim only
  covered fetch/EventSource/XHR. Rewrite root-relative src/href/action/
  poster attributes server-side in ModifyResponse (rewriteRootAttrs) so
  the browser never issues a wrong first request, and keep a
  MutationObserver for dynamically inserted nodes.

rewriteRootAttrs (two-level tag->attr regex):
- all attributes in one tag rewritten (multiple-attrs case)
- case-insensitive (?i), unquoted values, idempotent (already-prefixed and
  protocol-relative //host URLs skipped), data-src/xlink:href and JS
  property assignments (el.src=) not matched
- documented regex tradeoffs (value containing '>' truncates; no second
  pass for initial page; CSS url()/srcset/SVG <use> uncovered)

Tests: TestRewriteRootAttrs grown to 13 cases incl. the previously
uncovered risk path (two attrs in one tag), verified via control
experiment; proxy HTML-injection test now asserts real rewritten output
instead of grepping for script source.
linletian and others added 23 commits August 11, 2026 15:51
GitHub macOS runners have broken/unresponsive DNS, and
HTTPServer.server_bind() calls socket.getfqdn() for a reverse lookup
that hangs indefinitely. The fake reasonix serve scripts (python3
http.server) never wrote --port-file, so every reasonix test timed
out with 'serve did not become ready within 15s' on macOS only.
Patch socket.getfqdn to skip the DNS lookup in the test scripts.
feat(reasonix): embed reasonix web chat UI as an instance kind (MVP)
Finish every open reasonix issue plus the deferred env/preStart injection:

- #44 security: independent loopback listener serves only /rx/, iframe
  loads the cross-origin web_url from /api/instances so embedded chat
  content can no longer silently call the management API; TLS mode falls
  back to the same-origin /rx/ route (documented in DEFERRED.md).
- #48 layout: single HTML injection point adds desktop sidebar collapse
  (collapsed by default, own toggle button, --mw-sidebar-w width),
  narrow screens keep the native mobile UI.
- #46 perf: driver caches {port, token} for the instance lifetime
  (Start writes, Stop/Cleanup drop) so the proxy path reads no files.
- #47 locking: Delete and startReasonix failure paths run Reasonix.Stop/
  Cleanup outside stateMu (5s worst case no longer stalls the manager).
- #45 robustness: pre-start 'reasonix --version' gate (>= 1.22.0),
  readiness errors carry serve.log tail, reasonix.CookieName single
  source; fixes a latent Start deadlock on early serve exit.
- #43 tests: app handler tests use isolated temp data dirs + seeded
  state, reorder passes the optimistic-lock version (new 409 case).
- deferred env/preStart: StartInput.Env + PreStart hook, tag env/
  preStart flow into the serve process (command still ignored).
- #49 closed as intended: Restart stays a brand-new conversation.

Docs (API/ARCHITECTURE/PRD/DEFERRED/README) and full test suite updated;
all tests pass, all 7 issues closed.
Upstream .app is a 2x2 grid whose .transcript/.footer rely on
auto-placement: .sidebar (grid-row:1/3) claims column 1, pushing them to
column 2. display:none on the collapsed sidebar reflows auto-placement —
.transcript lands in the 0px column (chat area vanishes) while .footer
stretches across row 1 (only the input bar stays visible). Pin
.mw-rx .transcript{grid-column:2;grid-row:1} and .mw-rx .footer{grid-column:2;grid-row:2}
so collapsed layout keeps upstream's column-2 row-1/row-2 slots.
…twork-open

The independent #44 listener reports an absolute http://127.0.0.1:<port>
web_url, which a remote browser resolves to ITSELF — with the default
0.0.0.0 listen (or an explicit LAN IP) the reasonix iframe was blank on
LAN access. Enable the independent listener only when the main listener
is loopback-only (and non-TLS); network-open or TLS modes fall back to
the relative same-origin /rx/<id>/ path, which follows the browser's
current origin, so LAN/remote access works and switching IPs after
startup keeps working. Frontend needed no change (web_url empty → it
already falls back). Docs (API/ARCHITECTURE/DEFERRED/README) updated;
new TestReasonixNonLoopbackFallsBack covers the fallback.
Behavior parity with tty instances: a tty shell dies when its PTY hangs
up on process exit, but a reasonix serve has no PTY and previously
survived shutdown as an orphan (re-attached by Reconcile on next start).
Server.Shutdown now calls Manager.StopAllReasonix before closing the HTTP
servers: every running reasonix serve gets a graceful Stop (TERM→KILL),
state dirs are kept so the next Start --resumes the same session.jsonl
and the conversation survives. kill -9 still bypasses Shutdown — the
serve then lingers and Reconcile re-attaches it (documented). Tests:
TestStopAllReasonix, TestServerShutdownStopsReasonix.
…tart lock, encoding negotiation

- proxy: strip only myworktree's ?token= and forward other query params
  (e.g. ?session=) intact to the reasonix backend
- proxy: force Accept-Encoding identity only on HTML navigation requests;
  explicitly negotiate gzip for subresource/API so Go's Transport forwards
  compressed bodies verbatim; ModifyResponse refuses to splice into a
  still-compressed HTML response
- driver: serialize Start per instance id (in-flight lock) so concurrent
  Starts cannot both spawn serve processes racing on the same port/pid
  files; second caller sees the healthy process and returns idempotently
- driver: remove stale pid file alongside port file before spawning so
  waitReady cannot pair an old pid with the new port
- tests: query pass-through, encoding negotiation, compressed-HTML guard,
  concurrent-Start lock (verified it fails without the lock); instanceView
  JSON round-trip intent documented
feat(reasonix): complete native-ui backlog (#43-#49) + LAN & shutdown fixes
- issue #53: hide the web frame (display:none) instead of destroying it on
  tab switch, so chat drafts / scroll / sidebar session list survive;
  re-navigate only when the resolved src changes (new port after restart)
- issue #54: hide leftover positioned xterm hosts when a reasonix tab is
  active so they cannot overlay/block the static iframe
- issue #55: reset terminal-container opacity/filter on switching to a
  reasonix instance so a leftover grayscale never greys out the web UI
- stop->start regression: invalidate the frame's src cache when an instance
  is stopped so the next start re-navigates instead of showing the stale
  pre-stop page
- add TestIndexHTMLCoversReasonixWebFrame regression test
…on pool (issue #56)

Each reasonix instance previously ran with an isolated REASONIX_HOME, so the embedded web UI never saw the project's existing history (sidebar empty, 'branches: none'). Requirement revision: myworktree should not intervene in the agent's product logic — the serve now uses the user's real ~/.reasonix, so sessions/history/config/credentials are shared per project exactly like a terminal-run reasonix, and the same project's history (including terminal CLI/TUI sessions) is visible/switchable in the sidebar. Cross-project isolation is reasonix's own (per-cwd).

- Remove home dir, config/.env symlinks, session.jsonl, --resume and the REASONIX_HOME env injection; state dir now only holds token/port/pid/serve.log.
- Strip inherited REASONIX_HOME/REASONIX_STATE_HOME from the serve env (log key names only) with tag-env opt-out; legacy home/ leftovers get a migration hint on Start.
- Cleanup never touches the shared ~/.reasonix pool; issue #49 'Restart = fresh session' semantics stay.
- Tests: cross-platform env-strip via fake-serve echo, same-cwd concurrent instances, removeEnv edge cases.
- Docs: PRD/ARCHITECTURE/DEFERRED/FEASIBILITY/README/CHANGELOG synced; upstream contract + owner recorded.
Add scripts/verify-reasonix-contract.sh as the single source of truth for
the upstream reasonix behavior contract the driver depends on (version
gate, per-cwd session layout, fresh-session uniqueness, lease refusal),
reachable via an opt-in go test (REASONIX_CONTRACT=1) and a new CI
workflow. Both CI paths SKIP (green) on runners without a reasonix
binary until an install step is added. Document the contract and the
REASONIX_HOME shared-pool behavior in README/ARCHITECTURE.
fix(reasonix): keep embedded web UI alive across tab switches
The injected hide/expand sidebar button in the embedded Reasonix web chat
was 34x34px at (8,8), so its right side overlapped the conversation. It is
now a vertical 24x64px pill at (2,8) showing a CSS arrow glyph that
switches with state (▶ collapsed / ◀ expanded, pointing at where the
sidebar moves — no text, no i18n). Measured against the upstream
.transcript padding (24px 28px on desktop), the button's right edge (26px)
stays inside the chat's 28px left gutter, so it never covers message text
in either state; the taller target is easier to see and click.

Layout tests now use grep-style per-property regexp assertions instead of
tight substring matches, so CSS property reordering no longer breaks them.
feat(reasonix): native-ui polish — session sharing, shutdown, UI fixes + CI/test (issues #43-#49, #56)
…y, lock cleanup)

- PATCH /api/instances and POST /api/instances/restart responses now render
  through instanceView, so reasonix instances keep their web_url like GET/POST
- instanceView builds the response map directly, dropping the per-call
  Marshal->Unmarshal round-trip
- preStart env now strips inherited REASONIX_HOME/REASONIX_STATE_HOME exactly
  like serve (shared reasonix.RemoveEnv), keeping both phases on the same
  ~/.reasonix home; host-exported overrides still apply via tag env
- Manager.Stop tears down the per-instance serve-management dir and Start
  lock (Cleanup) after stopping, since the id is never reused; the driver's
  starts map no longer grows across start/stop cycles
- delete confirmation copy corrected to "Delete instance?"
- tests: PATCH web_url, Cleanup drops Start lock, preStart env strip,
  Stop cleans management dir; Restart cleanup test adapted (Stop now
  pre-cleans, seeds a stale dir instead)
@linletian
linletian merged commit 99aed42 into main Aug 12, 2026
4 checks passed
@linletian
linletian deleted the release/v0.4.0 branch August 12, 2026 14:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant