Skip to content

feat: replace cloud session sharing with local export#234

Merged
Astro-Han merged 11 commits into
devfrom
claude/issue-194-local-session-export
Apr 25, 2026
Merged

feat: replace cloud session sharing with local export#234
Astro-Han merged 11 commits into
devfrom
claude/issue-194-local-session-export

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Apr 25, 2026

Copy link
Copy Markdown
Owner

Summary

Replace cloud session sharing (opncd.ai) with a local JSON session export.

  • New pure export core Export.session(rootID) that climbs to root, recursively walks children, and produces a versioned JSON snapshot. Snapshot covers full session tree (info, messages, diffs), runtime_context (app/version/platform/locale/instruction sources/model refs/stats), and reserves diagnostics: {} for [Bug] TypeScript LSP can consume excessive CPU and memory on large workspaces #232.
  • Internal-only server route GET /:sessionID/export (mounted under existing /session router; directory resolved via instance middleware as query param).
  • Electron export-session IPC: main process fetches the internal route, opens save dialog, writes file. Renderer never holds the JSON.
  • App UI: drops the Share menu item + popover, replaces with "Export session log" wired to platform.exportSession. Toast on success/failure; cancel is silent.
  • Cloud share fail-closed under PawWork runtime via a new CloudShareGate Effect Service:
    • SessionShare.share / unshare raise typed CloudShareDisabled failure
    • ShareNext.create / remove and the 5 bus subscribers short-circuit
    • Server POST/DELETE /:sessionID/share return HTTP 410 cloud_share_disabled
  • CLI opencode export rewritten to call Export.session core; sanitize helpers moved into session/export.ts and extended via Export.sanitizeSnapshot so --sanitize also redacts runtime_context.instruction_sources paths.
  • Binary attachments: data:*;base64,... URLs in FilePart.url and tool-completed attachments are replaced with empty string + metadata.redacted_binary = { mime, size_bytes, sha256 }. Regex follows RFC 2397 (allows extra params between mime and ;base64). FilePart schema gets an optional metadata field, parallel to existing TextPart/ReasoningPart/ToolPart.

Why

Closes #194. PawWork is desktop-only by strategy; routing user session snapshots through opncd.ai adds an external dependency, exposes user-facing share UI that doesn't fit our positioning, and complicates a debugging loop where the natural artifact is a local JSON file the user can grep, share by attachment, or load into a tool. The new path keeps cloud share code in tree (so opencode upstream parity stays cheap) but fail-closes it under PawWork.

Refined spec is in #194 ("Refined implementation spec after design discussion"). Diagnostics extension point reserved for #232.

Related Issue

Closes #194.

How To Verify

Targeted automated:

bun --cwd packages/opencode test test/session/export.test.ts test/cli/export.test.ts test/share/
bun --cwd packages/opencode typecheck
bun --cwd packages/app typecheck
bun --cwd packages/desktop-electron typecheck

All green locally (12 export tests, 11 cli/share tests, 3 packages typecheck).

Manual smoke walked through with bun run dev:desktop:

  • Open a session, dot-grid menu shows Export session log (Share is gone)
  • Click Export → save dialog opens → JSON file written; filename uses pawwork-session-<slug>-<stamp>.json
  • Opened JSON: schema_version: 1, format: "pawwork-session-export", runtime_context.runtime_namespace: "pawwork", diagnostics: {}, had_cloud_share: false, info.share stripped, instruction_sources includes bundled pawwork.txt with sha256, model_refs resolves the active provider/model, stats matches session content
  • No opncd.ai request reachable: code-path proof — share-next.ts short-circuits create/remove/state subscribers when gate is closed; under PawWork runtime gate is closed; covered by share-next.test.ts + session-pawwork-fail-closed.test.ts

Out of scope for this PR (deferred):

  • OpenAPI advertising the new 410 on share/unshare (SDK regen needed)
  • Sanitize coverage for any future top-level fields (current snapshot stable)
  • Streaming write in main process (current debugging-loop sessions are small)

Screenshots or Recordings

Not attaching screenshots; the UI change is a single dropdown menu item rename ("Share" → "Export session log") and removal of the share popover. Manual smoke confirmed live.

Checklist

  • I linked the related issue, or stated why there is no issue
  • This PR has type, scope, and priority labels, or I requested maintainer labeling
  • I listed the relevant verification steps, including tests when behavior changed
  • I manually checked visible UI or copy changes when needed, with screenshots or recordings
  • I considered macOS and Windows impact for desktop, packaging, updater, signing, paths, shell, or permissions changes
  • I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, or generated/local file changes when relevant
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English

Summary by CodeRabbit

  • New Features

    • Session export: export sessions as JSON to local directories with auto-generated timestamps and optional custom filenames
    • Export action added to session UI; localized strings provided for English and Chinese
    • Cloud-share gating: runtime gate added to control session sharing behavior
  • Tests

    • End-to-end and unit tests added for session export, sanitization, and cloud-share gating behavior

@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d7bc1e35-347c-4293-a9d2-465eaf084be5

📥 Commits

Reviewing files that changed from the base of the PR and between 528ec25 and be0fa3c.

📒 Files selected for processing (8)
  • packages/app/src/pages/session/message-timeline.tsx
  • packages/desktop-electron/src/main/server-client.ts
  • packages/opencode/src/server/instance/session.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/share/session.ts
  • packages/opencode/src/share/share-next.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/share/share-next.test.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-desktop
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-app
  • GitHub Check: typecheck
  • GitHub Check: unit-opencode
  • GitHub Check: analyze-js-ts
  • GitHub Check: e2e-artifacts
🧰 Additional context used
📓 Path-based instructions (4)
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/share/share-next.ts
  • packages/opencode/src/server/instance/session.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/share/session.ts
  • packages/opencode/src/session/export.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/test/session/export.test.ts
packages/app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/app/AGENTS.md)

Always prefer createStore over multiple createSignal calls in SolidJS

Files:

  • packages/app/src/pages/session/message-timeline.tsx
packages/desktop-electron/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/desktop-electron/AGENTS.md)

Renderer process should only call window.api from src/preload

Files:

  • packages/desktop-electron/src/main/server-client.ts
🧠 Learnings (43)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/ui/src/theme/context.tsx:11-16
Timestamp: 2026-04-22T09:32:58.310Z
Learning: In Astro-Han/pawwork (`packages/ui/src/theme/context.tsx` and related files), the renaming of localStorage theme keys from `opencode-*` to `pawwork-*` (THEME_ID, COLOR_SCHEME, THEME_CSS_LIGHT, THEME_CSS_DARK) is intentional and should NOT include a migration path from the old keys. Migrating would re-couple PawWork and OpenCode browser storage namespaces, which the PR is explicitly designed to avoid. A reset to the PawWork default theme on upgrade is acceptable by design.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/desktop-electron/src/main/ipc.ts:238-263
Timestamp: 2026-04-25T12:52:32.462Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/ipc.ts`), `deps.getServerReadyData()` (backed by `serverReady.promise` in `index.ts`) resolves once at server startup and remains settled; it is not expected to reject in practice. Do not flag the absence of a try-catch around it in the `export-session` IPC handler — the network/fetch layer in `server-client.ts` already has a 10-second AbortController timeout and returns a typed `{ok: false, error}` payload, covering the real failure modes.
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/session/export.ts:24-31
Timestamp: 2026-04-25T12:52:35.671Z
Learning: In `packages/opencode/src/session/export.ts`, the `hashFile` async helper intentionally uses raw `node:fs/promises` (`fs.readFile`) rather than `FileSystem.FileSystem`. It is a small, self-contained helper called inside `Effect.promise(...)` with broad failure tolerance (try/catch returning `undefined`). Using `FileSystem.FileSystem` here would add unnecessary layers without payoff. The `FileSystem.FileSystem` preference applies to Effect-service-style operations, not thin async boundary helpers like `hashFile`.
📚 Learning: 2026-04-25T12:52:47.074Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/share/session.ts:27-27
Timestamp: 2026-04-25T12:52:47.074Z
Learning: In `packages/opencode/src/share/session.ts`, the local `ensureEnabled` closure is intentionally duplicated from `ShareRuntime.ensureEnabled` in `runtime.ts`. Using the shared `ShareRuntime.ensureEnabled` effect directly would leak `CloudShareGate` back into the `R` (requirement) type of `share`, `unshare`, and `create`, because the shared effect re-yields the service rather than capturing an already-resolved gate reference. The current pattern resolves `gate` once at the top of the outer `Effect.gen` and then closes over it in a local `ensureEnabled`, keeping the inner effects' requirement types clean. A comment in the file points to `runtime.ts` to make the intentional duplication discoverable. The real fix would be refactoring `ensureEnabled` to accept a gate parameter, but that change is larger than the DRY benefit warrants. Do not flag this duplication as a maintenance issue.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/share/share-next.ts
  • packages/opencode/src/server/instance/session.ts
  • packages/opencode/src/share/session.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-22T09:32:54.556Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/opencode/test/provider/provider.test.ts:64-85
Timestamp: 2026-04-22T09:32:54.556Z
Learning: In `packages/opencode/test/provider/provider.test.ts`, the file intentionally uses AppRuntime-based async helpers (`run`, `list`, `getProvider`, etc.) rather than `testEffect(...)` for all tests. Converting individual tests to `testEffect` while leaving the rest on the async pattern would create internal inconsistency. A full harness migration of this file is the right approach if the pattern needs to change, but that should be a separate PR.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/share/session.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/share/session.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers in Effect tests when `testEffect(...)` already provides the runtime.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/share/session.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`. Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Prefer Effect-aware helpers from `fixture/fixture.ts` over building manual runtimes in tests: use `tmpdirScoped()` for scoped temp directories, `provideInstance(dir)(effect)` for low-level binding without directory creation, `provideTmpdirInstance(...)` for single temp instance binding, or `provideTmpdirServer(...)` for tests that also need the test LLM server.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/share/session.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/packages/app/src/testing/**/*.ts : Test-only hooks must be inert unless explicitly enabled and should not add normal-runtime listeners, reactive subscriptions, or per-update allocations

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/server/instance/session.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : When a test needs instance-local state, prefer `provideTmpdirInstance(...)` or `provideInstance(...)` over manual `Instance.provide(...)` inside Promise-style tests.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-25T12:52:32.462Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/desktop-electron/src/main/ipc.ts:238-263
Timestamp: 2026-04-25T12:52:32.462Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/ipc.ts`), `deps.getServerReadyData()` (backed by `serverReady.promise` in `index.ts`) resolves once at server startup and remains settled; it is not expected to reject in practice. Do not flag the absence of a try-catch around it in the `export-session` IPC handler — the network/fetch layer in `server-client.ts` already has a 10-second AbortController timeout and returns a typed `{ok: false, error}` payload, covering the real failure modes.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/share/share-next.ts
  • packages/opencode/src/server/instance/session.ts
  • packages/opencode/src/share/session.ts
  • packages/desktop-electron/src/main/server-client.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation rather than storing `Fiber | undefined` or `Promise | undefined` manually

Applied to files:

  • packages/opencode/src/share/share-next.ts
  • packages/opencode/src/share/session.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.gen(function* () { ... })` for Effect composition

Applied to files:

  • packages/opencode/src/share/share-next.ts
  • packages/opencode/src/share/session.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-25T12:52:35.671Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/session/export.ts:24-31
Timestamp: 2026-04-25T12:52:35.671Z
Learning: In `packages/opencode/src/session/export.ts`, the `hashFile` async helper intentionally uses raw `node:fs/promises` (`fs.readFile`) rather than `FileSystem.FileSystem`. It is a small, self-contained helper called inside `Effect.promise(...)` with broad failure tolerance (try/catch returning `undefined`). Using `FileSystem.FileSystem` here would add unnecessary layers without payoff. The `FileSystem.FileSystem` preference applies to Effect-service-style operations, not thin async boundary helpers like `hashFile`.

Applied to files:

  • packages/opencode/src/server/instance/session.ts
  • packages/opencode/src/share/session.ts
  • packages/app/src/pages/session/message-timeline.tsx
  • packages/desktop-electron/src/main/server-client.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-25T12:52:36.999Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 234
File: packages/opencode/src/session/export.ts:75-88
Timestamp: 2026-04-25T12:52:36.999Z
Learning: In `packages/opencode/src/session/export.ts` (Astro-Han/pawwork), the `extractReasonFromCause` function intentionally uses a lightweight cast-based inspection of the Cause `reasons` array instead of `Cause.failures()` / `Cause.defects()` utilities. This is a deliberate choice: the function is diagnostic-only (used to populate a best-effort reason string for export diagnostics), and the maintainer explicitly prefers not to add coupling to Effect's Cause API surface for this purpose. Do not flag this pattern as fragile or suggest replacing it with Cause utilities.

Applied to files:

  • packages/opencode/src/server/instance/session.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Schema.Defect` instead of `unknown` for defect-like causes in Effect code

Applied to files:

  • packages/opencode/src/server/instance/session.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-23T08:51:04.230Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:04.230Z
Learning: In Astro-Han/pawwork (`packages/opencode/src/util/filesystem.ts`), the `Filesystem` utility does NOT expose a `remove` or `unlink` helper. The established repository pattern for auth.json teardown in tests (e.g. `provider.test.ts`, `amazon-bedrock.test.ts`, `workspace-adaptor.test.ts`) is to combine `Filesystem.write` with `node:fs/promises unlink`. Do not flag this mixed usage as inconsistent — it is the correct and intentional pattern.

Applied to files:

  • packages/opencode/src/server/instance/session.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Call `project.trackSession(sessionID, directory?)` and `project.trackDirectory(directory)` for any resources created outside the fixture so teardown can clean them up

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use fixture-managed cleanup with `withSession(sdk, title, callback)` for temporary sessions instead of calling `sdk.session.delete(...)` directly

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-24T03:51:54.050Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `config` option in `tmpdir` to write an `opencode.json` config file during test setup by passing a partial Config.Info object.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `init` option in `tmpdir` to define custom setup functions that can return extra data accessible via `tmp.extra`, and use the `dispose` option for custom cleanup logic.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Import test utilities from `../fixtures` instead of `playwright/test`

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `tmpdir` function from `fixture/fixture.ts` to create temporary directories for tests with automatic cleanup. Use `await using` syntax to ensure automatic cleanup when the variable goes out of scope.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-24T00:02:53.315Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 203
File: packages/app/e2e/sidebar/sidebar-session-links.spec.ts:34-55
Timestamp: 2026-04-24T00:02:53.315Z
Learning: In Astro-Han/pawwork E2E tests (`packages/app/e2e/**/*.spec.ts`), `project.trackDirectory()` and `project.trackSession()` cannot be called before `project.open()` — the `project` fixture throws until `open()` initializes its internal state. The correct pattern is: call `project.trackSession(sessionID)` from inside the `beforeGoto` callback (where state already exists), call `project.trackDirectory(directory)` and cross-workspace `project.trackSession(id, directory)` immediately after `project.open()` returns, and rely on explicit `finally` cleanup (e.g. `cleanupSession` / `cleanupTestProject`) for any resources created before `open()` that cannot yet be tracked via the fixture.

Applied to files:

  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `makeRuntime` from `src/effect/run-service.ts` for all services; it returns `{ runPromise, runFork, runCallback }` backed by a shared `memoMap` that deduplicates layers

Applied to files:

  • packages/opencode/src/share/session.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Prefer `Path.Path`, `Config`, `Clock`, and `DateTime` services when those concerns are already inside Effect code

Applied to files:

  • packages/opencode/src/share/session.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.addFinalizer` or `Effect.acquireRelease` inside the `InstanceState.make` closure for cleanup (subscriptions, process teardown, etc.)

Applied to files:

  • packages/opencode/src/share/session.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : For background loops or scheduled tasks, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition

Applied to files:

  • packages/opencode/src/share/session.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.forkScoped` inside the `InstanceState.make` closure for background stream consumers — the fiber is interrupted when the instance is disposed

Applied to files:

  • packages/opencode/src/share/session.ts
📚 Learning: 2026-04-23T07:23:23.849Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 180
File: packages/app/src/components/session/session-new-view.tsx:13-18
Timestamp: 2026-04-23T07:23:23.849Z
Learning: In pawwork (Astro-Han/pawwork), prefer using `createStore` instead of multiple `createSignal` calls only when the signals represent **coupled** object state that is updated together (i.e., there is at least one shared batch-update site where the state is changed in the same transaction). If the state fields are **independent** and are mutated by separate handlers (e.g., one handler updates only `selectedSkill` while another updates only `mode`), keep them as individual `createSignal` calls—using `createStore` for truly independent fields adds boilerplate without behavioral benefit.

Applied to files:

  • packages/app/src/pages/session/message-timeline.tsx
📚 Learning: 2026-04-23T15:10:21.635Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 191
File: packages/app/src/components/session/pawwork-skill-meta.ts:38-39
Timestamp: 2026-04-23T15:10:21.635Z
Learning: This repo configures Tailwind v4 with `--color-*: initial`, which effectively breaks standard Tailwind palette utilities (e.g., `text-violet-500` can resolve to no CSS variable and render as a no-op/black). For brand/accent colors that are not backed by semantic design tokens, use inline styles with the exact hex value (e.g., `style={{ color: '#8B5FBF' }}` / `homeIconStyle: { color: '#8B5FBF' }`) and add a short comment explaining that Tailwind palette utilities won’t work due to the `--color-*: initial` setup. Do not suggest replacing these inline hex colors with Tailwind palette classes anywhere in this repo.

Applied to files:

  • packages/app/src/pages/session/message-timeline.tsx
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Prefer `HttpClient.HttpClient` instead of raw `fetch` in Effect services

Applied to files:

  • packages/desktop-electron/src/main/server-client.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-24T13:03:10.835Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 222
File: packages/desktop-electron/src/main/index.ts:686-692
Timestamp: 2026-04-24T13:03:10.835Z
Learning: In `packages/desktop-electron/src/main/index.ts`, the `checkForUpdates()` function intentionally uses recursive self-invocation for the "Retry" path in the update-check failure dialog. This is mandated by the v5.2 design spec (`#213`): "Await the retry recursion and log any rejection so support can see repeated failures." Because retries are user-paced (require a button click), all prior async frames have already unwound through microtasks before the next attempt, so there is no stack/frame-nesting problem in practice. Do not suggest refactoring this to an iterative loop.

Applied to files:

  • packages/desktop-electron/src/main/server-client.ts
  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-25T09:19:30.734Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 231
File: packages/desktop-electron/src/main/index.ts:537-537
Timestamp: 2026-04-25T09:19:30.734Z
Learning: In Astro-Han/pawwork (packages/desktop-electron/src/main/), follow the IPC registration convention: the bootstrap entry (packages/desktop-electron/src/main/index.ts) should directly call each module’s exported register*Ipc() function. Do not route/centralize these sub-module IPC registrations through src/main/ipc.ts. Keep sub-module IPC features cohesive (e.g., src/main/ipc/about.ts should own its types/helpers and expose register*Ipc()), and allow index.ts to aggregate by calling each register*Ipc() directly.

Applied to files:

  • packages/desktop-electron/src/main/server-client.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : In `Effect.gen` / `Effect.fn`, prefer `yield* new MyError(...)` over `yield* Effect.fail(new MyError(...))` for direct early-failure branches

Applied to files:

  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.fn("Domain.method")` for named/traced effects and `Effect.fnUntraced` for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer `.pipe()` wrappers

Applied to files:

  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.callback` for callback-based APIs

Applied to files:

  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Prefer `ChildProcessSpawner.ChildProcessSpawner` with `ChildProcess.make(...)` instead of custom process wrappers in Effect services

Applied to files:

  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : When validating routing, assert against canonical or resolved workspace slugs using shared helpers from `../actions` to account for Windows canonicalization

Applied to files:

  • packages/opencode/src/session/export.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Prefer `FileSystem.FileSystem` instead of raw `fs/promises` for effectful file I/O in Effect services

Applied to files:

  • packages/opencode/src/session/export.ts
🔇 Additional comments (16)
packages/opencode/src/share/share-next.ts (2)

292-297: LGTM! Typed failure for gate-disabled path addresses the previous concern.

The differentiation between disabled (env var opt-out returning empty stub for backwards compatibility) and !gate.isEnabled() (PawWork runtime raising typed CloudShareDisabled failure) is well-documented in the comments and ensures callers can distinguish disabled from success.


350-358: LGTM! Layer composition correctly includes the gate.

The defaultLayer properly provides ShareRuntime.cloudShareGateDefaultLayer, ensuring the gate is available throughout the sharing flow.

packages/opencode/src/share/session.ts (2)

30-35: LGTM! Local ensureEnabled closure is intentionally duplicated.

The comment correctly documents why this isn't using ShareRuntime.ensureEnabled — capturing the resolved gate reference keeps the Effect requirement types clean without leaking CloudShareGate into share/unshare/create's R type. Based on learnings: this duplication is intentional and should not be flagged.


54-62: Consistent gate handling across session share operations.

The create function correctly checks gate.isEnabled() and returns early (without auto-sharing) when disabled, while share/unshare use ensureEnabled to raise typed failures. This asymmetry makes sense: creating a session should succeed; only the auto-share side-effect is skipped.

packages/opencode/test/share/share-next.test.ts (1)

241-268: LGTM! Disabled-gate test case properly verifies fail-closed behavior.

This test addresses the previous review request by:

  1. Injecting CloudShareGate with isEnabled: () => false
  2. Using a dying HttpClient to ensure any HTTP attempt would fail loudly
  3. Asserting the exit is a failure and no share row is persisted

The test correctly verifies the gate short-circuits before any network call.

packages/opencode/src/server/instance/session.ts (2)

462-496: LGTM! Export route with proper error handling.

The GET /:sessionID/export route correctly:

  • Calls Export.session(sessionID) via AppRuntime.runPromise
  • Uses instanceof NotFoundError check (addressing the past review comment about fragile string matching)
  • Returns typed 404 response for missing sessions
  • Re-throws other errors for middleware handling

445-453: LGTM! Gate checks return HTTP 410 for disabled cloud sharing.

Both share and unshare routes now check CloudShareGate.isEnabled() and return { error: "cloud_share_disabled" } with status 410 (Gone) when disabled. This is the correct HTTP status for a feature that has been intentionally removed.

Also applies to: 593-601

packages/app/src/pages/session/message-timeline.tsx (2)

242-246: LGTM! Export availability guard prevents misleading exports.

The exportAvailable memo correctly guards the export action:

  1. Requires platform.exportSession to exist (capability check)
  2. Requires server.current?.type === "sidecar" (prevents export when connected to remote server where data may differ)

The comment explains the rationale well.


400-437: LGTM! Export handler with robust filename generation and error handling.

The onExport function:

  • Builds a user-friendly default filename from session slug with timestamp
  • Handles Unicode titles via \p{L}\p{N} regex while stripping filesystem-hostile characters
  • Falls back to session ID suffix when sanitization produces empty string
  • Treats cancelled as silent no-op (correct UX)
  • Shows appropriate success/error toasts
packages/desktop-electron/src/main/server-client.ts (1)

1-52: LGTM! Well-structured fetch client with timeout handling.

This new file correctly implements:

  • buildExportUrl: Constructs URL matching server route expectations with directory as query param
  • buildAuthHeader: Documents the "opencode" username fallback (addressing past review)
  • fetchExport: Includes 10s AbortController timeout (addressing the past concern about indefinite hangs), properly distinguishes timeout vs other errors, and always clears the timer
packages/opencode/test/session/export.test.ts (2)

248-293: LGTM! Test verifies Tree.diffs sanitization.

This test addresses the past concern about sanitizeSnapshot not redacting node.diffs. It verifies that both root and child session diffs have their file and patch fields properly redacted with placeholder markers.


1-371: LGTM! Comprehensive test coverage for the export module.

The test suite covers:

  • Runtime namespace detection
  • Single root session export shape
  • Ancestor traversal (climb to root)
  • Deterministic child ordering with tie-breaker
  • Runtime context fields (platform, locale, timezone, instruction_sources)
  • Unresolved model ref handling
  • Data URL redaction (file parts and tool attachments)
  • Snapshot sanitization (diffs and instruction paths)
packages/opencode/src/session/export.ts (4)

24-31: LGTM! hashFile intentionally uses raw fs/promises.

Based on learnings: This is a small, self-contained helper called inside Effect.promise(...) with broad failure tolerance (try/catch returning undefined). Using FileSystem.FileSystem would add unnecessary layers without payoff for this thin async boundary helper.


75-88: LGTM! extractReasonFromCause intentionally uses lightweight cast-based inspection.

Based on learnings: This diagnostic-only function intentionally avoids coupling to Effect's Cause API surface. The maintainer explicitly prefers this pattern for best-effort reason extraction.


572-586: LGTM! sanitizeTree now correctly redacts node.diffs.

This addresses the past review concern. Line 578 applies the diff("tree-diff", node.diffs) helper to sanitize file paths and patch content in the tree's diffs, ensuring --sanitize properly redacts all sensitive path/content data.


275-302: LGTM! Export.session produces a complete, well-structured snapshot.

The function correctly:

  • Climbs to root session when given any session ID
  • Builds the full session tree with redacted binary attachments
  • Collects instruction sources with hashes
  • Resolves model references (with unresolved reasons)
  • Computes accurate stats including omitted attachment count
  • Returns a properly typed Snapshot object

📝 Walkthrough

Walkthrough

Adds local session export: new Platform/exportSession API, renderer + preload + IPC + main handling to fetch session export from server, save to user-selected path, server-side GET /:sessionID/export endpoint, a comprehensive Export module with sanitization, and feature-gating to disable cloud sharing.

Changes

Cohort / File(s) Summary
Platform & Renderer API
packages/app/src/context/platform.tsx, packages/desktop-electron/src/preload/types.ts, packages/desktop-electron/src/renderer/index.tsx, packages/desktop-electron/src/preload/index.ts
Added optional exportSession(sessionID, directory, defaultName?) signature across Platform, Electron preload types, and renderer implementation to expose session export capability.
UI
packages/app/src/pages/session/message-timeline.tsx
Removed cloud "share" UI/logic; added "export" option that calls platform.exportSession, constructs sanitized default filename, and shows success/error toasts; deleted share-specific state and mutations.
Desktop Main / IPC / Server Client
packages/desktop-electron/src/main/ipc.ts, packages/desktop-electron/src/main/index.ts, packages/desktop-electron/src/main/server-client.ts
Added export-session IPC handler and getServerReadyData accessor; implemented fetchExport, URL/auth builders, timeouts, response handling, save-dialog invocation, and file write with typed {ok
Preload API & Types
packages/desktop-electron/src/preload/index.ts, packages/desktop-electron/src/preload/types.ts
Exposed api.exportSession in preload and extended ElectronAPI type with the exportSession return union.
Server Export Endpoint & Export Module
packages/opencode/src/server/instance/session.ts, packages/opencode/src/session/export.ts
Added GET /:sessionID/export route and large new Export module: builds full session snapshot tree, gathers runtime_context, model refs, diagnostics, and provides sanitization/redaction helpers and deterministic ordering.
Share Gate & Runtime Composition
packages/opencode/src/share/runtime.ts, packages/opencode/src/share/session.ts, packages/opencode/src/share/share-next.ts, packages/opencode/src/effect/app-runtime.ts
Introduced ShareRuntime.CloudShareGate, default layer, and ensureEnabled check; gate short-circuits cloud share/unshare flows and is composed into default runtime layers.
Core Types
packages/opencode/src/session/message-v2.ts
Added optional metadata field to FilePart schema to attach arbitrary string-keyed metadata.
CLI & Tests
packages/opencode/src/cli/cmd/export.ts, packages/opencode/test/*
CLI export now delegates to shared Export module and sanitization; tests updated/added for export behavior, sanitization, model refs, and cloud-share gate behavior.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (Renderer)
    participant UI as Message Timeline (Renderer)
    participant Preload as Preload API (contextBridge)
    participant Main as Electron Main (IPC)
    participant Server as Backend Server
    participant FS as File System

    User->>UI: Click "Export" option
    UI->>UI: Build sanitized default filename
    UI->>Preload: api.exportSession(sessionID, directory, defaultName)
    Preload->>Main: ipcRenderer.invoke("export-session", args)
    Main->>Main: validate args, getServerReadyData()
    Main->>Server: GET /session/<id>/export
    Server-->>Main: return export JSON body
    Main->>Main: open save dialog (JSON filter)
    User->>Main: choose path / confirm
    Main->>FS: write file (UTF-8)
    FS-->>Main: write success
    Main-->>Preload: { ok: true, path }
    Preload-->>UI: result
    UI->>User: show success or error toast
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement, P2, app, desktop, upstream

Poem

🐇 I hop through code, with tiny paws so steady,
I tuck sessions safe where files sit ready.
No cloud surprise, just timestamps and lore,
Exported and local—keep secrets ashore. ✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-194-local-session-export

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a local session export feature, replacing cloud sharing in certain environments like PawWork. It introduces a new Export service to handle session tree construction, statistics, and data sanitization, alongside Electron IPC handlers for file saving. A CloudShareGate was also added to manage the availability of cloud sharing. Feedback highlights the need for proper percent-decoding of data URLs per RFC 2397, more defensive property access when handling Effect causes, and a more robust approach to path discovery than using __dirname.

Comment thread packages/opencode/src/session/export.ts
Comment thread packages/opencode/src/session/export.ts
Comment thread packages/opencode/src/session/export.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/opencode/test/share/session-pawwork-fail-closed.test.ts (1)

84-88: 🧹 Nitpick | 🔵 Trivial

Remove unused imports instead of suppressing warnings.

The Cause and Option imports are not used in the test logic. If they're intended for future tests, add them when needed. Keeping unused imports with void suppression adds noise.

🧹 Remove unused imports
-import { Effect, Cause, Layer, Option } from "effect"
+import { Effect, Layer } from "effect"
...
-// Suppress unused-import warning for Cause/Option which are exported from this test surface
-// to make the failure-extraction pattern reusable by other share tests later.
-void Cause
-void Option
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/test/share/session-pawwork-fail-closed.test.ts` around
lines 84 - 88, Remove the unused imports and their suppression lines: delete the
import entries for Cause and Option and the two lines "void Cause" and "void
Option" (they're the unused-symbol suppression) so the test file no longer
contains dead imports; if you plan to use Cause/Option later, add them back when
needed in the functions/classes that reference them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/desktop-electron/src/main/ipc.ts`:
- Around line 238-263: The ipcMain handler for "export-session" currently calls
deps.getServerReadyData() without error handling; wrap the await
deps.getServerReadyData() call in a try-catch inside the "export-session"
handler (the function passed to ipcMain.handle) and on failure return a graceful
IPC error result (e.g., { ok: false, error: (err as Error).message } or a
specific code like "server_unavailable") instead of allowing a thrown rejection
to bubble up; keep the subsequent logic using the local server variable
unchanged and ensure typings remain compatible with the existing
fetchExport(server, directory, sessionID) call.

In `@packages/desktop-electron/src/main/server-client.ts`:
- Around line 15-23: The buildAuthHeader function currently falls back to the
literal username "opencode" when server.username is null but server.password is
provided; add a concise inline comment above or inside buildAuthHeader (or next
to the Authorization line) explaining this is an intentional
compatibility/convention fallback for cases where only a password is supplied
(reference function name buildAuthHeader and type ServerReadyData) so future
readers know this behavior is deliberate.

In `@packages/opencode/src/server/instance/session.ts`:
- Around line 487-492: The catch block in Export.session currently relies on
fragile string matching of err.message; instead import and use the NotFoundError
class from "@/storage/db" and check with "err instanceof NotFoundError"
(matching the pattern used in middleware.ts/AppRuntime.runPromise) so that when
Session.get throws NotFoundError you return c.json({ error: "session_not_found",
sessionID }, 404); otherwise rethrow the error.

In `@packages/opencode/src/session/export.ts`:
- Around line 72-85: extractReasonFromCause currently inspects a private
"reasons" array on the Cause which is fragile; replace the manual
cast-and-iterate logic with Effect's Cause utilities by using
Cause.failures(cause) and Cause.defects(cause) (or their TypeScript equivalents)
to collect failure/defect payloads, then derive a string by checking for string
payloads, .message, or _tag on those collected values; update
extractReasonFromCause to call these utilities (instead of accessing reasons)
and return the first meaningful string or "unknown".
- Around line 21-28: The hashFile function currently uses fs.readFile directly;
replace this raw fs usage with the FileSystem.FileSystem service per guidelines:
obtain the FileSystem instance (e.g., via dependency injection or the existing
Effect service pattern used in this module) and call its readFile API inside
hashFile, then compute the sha256 from the returned buffer and preserve the same
try/catch behavior returning "sha256:..." on success or undefined on error;
update imports to remove fs/promises and reference FileSystem.FileSystem instead
to locate the change around the hashFile function.

In `@packages/opencode/src/share/session.ts`:
- Line 27: Replace the direct gate access (e.g., the yield*
ShareRuntime.CloudShareGate usage in this file) with the shared effect exported
from runtime by calling ShareRuntime.ensureEnabled so this module reuses the
centralized gate/failure semantics; update all similar occurrences in this file
(the block around the current const gate = yield* ShareRuntime.CloudShareGate
and the code covering lines ~30–46) to invoke ShareRuntime.ensureEnabled instead
of duplicating the gate check.

In `@packages/opencode/src/share/share-next.ts`:
- Around line 292-293: The disabled branch in Effect.fn("ShareNext.create")
currently returns an empty Share ({ id: "", url: "", secret: ""}) which hides
the failure; update the disabled/gate.isEnabled() path to signal failure
explicitly instead of a fake Share—either return a widened type
(nullable/Option/Result) and return null/None/Error for this branch, or throw a
clear error (e.g., throw new Error("sharing disabled")) so callers of
create(SessionID) can distinguish disabled from a valid Share; adjust the
function signature and callers of create accordingly (or document the thrown
error) to handle the new failure shape.

In `@packages/opencode/test/share/share-next.test.ts`:
- Line 16: Add a disabled-gate test case in the share-next.test harness: when
instantiating ShareRuntime (or the test harness that provides CloudShareGate),
pass an override CloudShareGate with isEnabled: () => false and run the same
create/remove flows; assert that create and remove do not call the HTTP layer
(mock fetch or the network mock) and do not persist any share rows (verify
DB/store mock methods were not called). Specifically target the ShareRuntime
usage and the create/remove calls so the disabled branch is exercised alongside
the existing enabled tests.

---

Outside diff comments:
In `@packages/opencode/test/share/session-pawwork-fail-closed.test.ts`:
- Around line 84-88: Remove the unused imports and their suppression lines:
delete the import entries for Cause and Option and the two lines "void Cause"
and "void Option" (they're the unused-symbol suppression) so the test file no
longer contains dead imports; if you plan to use Cause/Option later, add them
back when needed in the functions/classes that reference them.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 263acca9-5e4f-4b59-ac2b-95cb3c677697

📥 Commits

Reviewing files that changed from the base of the PR and between 3124070 and 528ec25.

📒 Files selected for processing (22)
  • packages/app/src/context/platform.tsx
  • packages/app/src/i18n/en.ts
  • packages/app/src/i18n/zh.ts
  • packages/app/src/pages/session/message-timeline.tsx
  • packages/desktop-electron/src/main/index.ts
  • packages/desktop-electron/src/main/ipc.ts
  • packages/desktop-electron/src/main/server-client.ts
  • packages/desktop-electron/src/preload/index.ts
  • packages/desktop-electron/src/preload/types.ts
  • packages/desktop-electron/src/renderer/index.tsx
  • packages/opencode/src/cli/cmd/export.ts
  • packages/opencode/src/effect/app-runtime.ts
  • packages/opencode/src/server/instance/session.ts
  • packages/opencode/src/session/export.ts
  • packages/opencode/src/session/message-v2.ts
  • packages/opencode/src/share/runtime.ts
  • packages/opencode/src/share/session.ts
  • packages/opencode/src/share/share-next.ts
  • packages/opencode/test/cli/export.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
  • packages/opencode/test/share/share-next.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: unit-windows-opencode-server-tools
  • GitHub Check: unit-windows-opencode-config-project
  • GitHub Check: unit-windows-desktop
  • GitHub Check: unit-windows-opencode-session
  • GitHub Check: unit-windows-app
  • GitHub Check: unit-opencode
  • GitHub Check: unit-desktop
  • GitHub Check: typecheck
  • GitHub Check: smoke-macos-arm64
  • GitHub Check: e2e-artifacts
  • GitHub Check: analyze-js-ts
🧰 Additional context used
📓 Path-based instructions (5)
packages/app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/app/AGENTS.md)

Always prefer createStore over multiple createSignal calls in SolidJS

Files:

  • packages/app/src/i18n/zh.ts
  • packages/app/src/i18n/en.ts
  • packages/app/src/context/platform.tsx
  • packages/app/src/pages/session/message-timeline.tsx
packages/desktop-electron/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/desktop-electron/AGENTS.md)

Renderer process should only call window.api from src/preload

Files:

  • packages/desktop-electron/src/renderer/index.tsx
  • packages/desktop-electron/src/main/index.ts
  • packages/desktop-electron/src/preload/index.ts
  • packages/desktop-electron/src/preload/types.ts
  • packages/desktop-electron/src/main/ipc.ts
  • packages/desktop-electron/src/main/server-client.ts
packages/opencode/**/*.ts

📄 CodeRabbit inference engine (packages/opencode/AGENTS.md)

packages/opencode/**/*.ts: Use Effect.gen(function* () { ... }) for Effect composition
Use Effect.fn("Domain.method") for named/traced effects and Effect.fnUntraced for internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer .pipe() wrappers
Use Effect.callback for callback-based APIs
Prefer DateTime.nowAsDate over new Date(yield* Clock.currentTimeMillis) when you need a Date in Effect code
Use Schema.Class for multi-field data in Effect schemas
Use branded schemas (Schema.brand) for single-value types in Effect
Use Schema.TaggedErrorClass for typed errors in Effect schemas
Use Schema.Defect instead of unknown for defect-like causes in Effect code
In Effect.gen / Effect.fn, prefer yield* new MyError(...) over yield* Effect.fail(new MyError(...)) for direct early-failure branches
Use makeRuntime from src/effect/run-service.ts for all services; it returns { runPromise, runFork, runCallback } backed by a shared memoMap that deduplicates layers
Use InstanceState from src/effect/instance-state.ts for per-directory or per-project state that needs per-instance cleanup; do work directly in the InstanceState.make closure where ScopedCache handles run-once semantics
Use Effect.addFinalizer or Effect.acquireRelease inside the InstanceState.make closure for cleanup (subscriptions, process teardown, etc.)
Use Effect.forkScoped inside the InstanceState.make closure for background stream consumers — the fiber is interrupted when the instance is disposed
Prefer FileSystem.FileSystem instead of raw fs/promises for effectful file I/O in Effect services
Prefer ChildProcessSpawner.ChildProcessSpawner with ChildProcess.make(...) instead of custom process wrappers in Effect services
Prefer HttpClient.HttpClient instead of raw fetch in Effect services
Prefer Path.Path, Config, Clock, and DateTime services when those concerns are already inside Effect code
For backgroun...

Files:

  • packages/opencode/src/session/message-v2.ts
  • packages/opencode/src/effect/app-runtime.ts
  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/share/share-next.ts
  • packages/opencode/src/cli/cmd/export.ts
  • packages/opencode/test/cli/export.test.ts
  • packages/opencode/src/server/instance/session.ts
  • packages/opencode/src/share/runtime.ts
  • packages/opencode/src/share/session.ts
  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
  • packages/opencode/test/session/export.test.ts
  • packages/opencode/src/session/export.ts
packages/opencode/test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (packages/opencode/test/AGENTS.md)

packages/opencode/test/**/*.test.{ts,tsx}: Use the tmpdir function from fixture/fixture.ts to create temporary directories for tests with automatic cleanup. Use await using syntax to ensure automatic cleanup when the variable goes out of scope.
When using the tmpdir function with git repository support, pass the git: true option to initialize a git repo with a root commit.
Use the config option in tmpdir to write an opencode.json config file during test setup by passing a partial Config.Info object.
Use the init option in tmpdir to define custom setup functions that can return extra data accessible via tmp.extra, and use the dispose option for custom cleanup logic.
Use testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
Use it.effect(...) when the test should run with TestClock and TestConsole. Use it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
Prefer Effect-aware helpers from fixture/fixture.ts over building manual runtimes in tests: use tmpdirScoped() for scoped temp directories, provideInstance(dir)(effect) for low-level binding without directory creation, provideTmpdirInstance(...) for single temp instance binding, or provideTmpdirServer(...) for tests that also need the test LLM server.
Define const it = testEffect(...) near the top of the test file and keep the test body inside Effect.gen(function* () { ... }). Yield services directly with yield* MyService.Service or yield* MyTool.
Avoid custom ManagedRuntime, attach(...), or ad hoc run(...) wrappers in Effect tests when testEffect(...) already provides the runtime.
When a test needs instance-local state, prefer provideTmpdirInstance(...) or provideInstance(...) over manual Instance.provide(...) inside Promise-style tests.

Files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/test/cli/export.test.ts
  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
  • packages/opencode/test/session/export.test.ts
packages/desktop-electron/src/main/ipc.ts

📄 CodeRabbit inference engine (packages/desktop-electron/AGENTS.md)

Main process should register IPC handlers in src/main/ipc.ts

Files:

  • packages/desktop-electron/src/main/ipc.ts
🧠 Learnings (35)
📓 Common learnings
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/ui/src/theme/context.tsx:11-16
Timestamp: 2026-04-22T09:32:58.310Z
Learning: In Astro-Han/pawwork (`packages/ui/src/theme/context.tsx` and related files), the renaming of localStorage theme keys from `opencode-*` to `pawwork-*` (THEME_ID, COLOR_SCHEME, THEME_CSS_LIGHT, THEME_CSS_DARK) is intentional and should NOT include a migration path from the old keys. Migrating would re-couple PawWork and OpenCode browser storage namespaces, which the PR is explicitly designed to avoid. A reset to the PawWork default theme on upgrade is acceptable by design.
📚 Learning: 2026-04-22T05:32:29.012Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 98
File: packages/desktop-electron/src/main/menu-labels.ts:1-2
Timestamp: 2026-04-22T05:32:29.012Z
Learning: In Astro-Han/pawwork, the app i18n layer (`packages/app/src/i18n/`) only contains `en.ts` and `zh.ts`, and `normalizeLocale` (in `packages/app/src/context/language.tsx`) only returns `"en"` or `"zh"`. The desktop `MenuLocale = "en" | "zh"` union in `packages/desktop-electron/src/main/menu-labels.ts` is intentionally limited to these two locales and is not a broader restriction — do not flag it as overly restrictive or suggest adding other locales.

Applied to files:

  • packages/app/src/i18n/zh.ts
  • packages/app/src/i18n/en.ts
📚 Learning: 2026-04-24T17:08:44.294Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 224
File: packages/app/src/i18n/zh.ts:0-0
Timestamp: 2026-04-24T17:08:44.294Z
Learning: In Astro-Han/pawwork PR `#224`, the first-occurrence `PawWork 爪印` branding rule originally specified in issue `#196` was superseded by an updated Chinese-branding spec. On all zh UI surfaces in `packages/app/src/i18n/zh.ts` (e.g., `dialog.model.unpaid.freeModels.title`, `session.new.subtitle`, `sidebar.gettingStarted.line1`), the correct and intentional target is fully localized `爪印` branding — no `PawWork` prefix. Do NOT flag these strings as missing the first-occurrence `PawWork 爪印` rule in future reviews.

Applied to files:

  • packages/app/src/i18n/zh.ts
📚 Learning: 2026-04-23T07:23:23.849Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 180
File: packages/app/src/components/session/session-new-view.tsx:13-18
Timestamp: 2026-04-23T07:23:23.849Z
Learning: In pawwork (Astro-Han/pawwork), prefer using `createStore` instead of multiple `createSignal` calls only when the signals represent **coupled** object state that is updated together (i.e., there is at least one shared batch-update site where the state is changed in the same transaction). If the state fields are **independent** and are mutated by separate handlers (e.g., one handler updates only `selectedSkill` while another updates only `mode`), keep them as individual `createSignal` calls—using `createStore` for truly independent fields adds boilerplate without behavioral benefit.

Applied to files:

  • packages/app/src/i18n/zh.ts
  • packages/app/src/i18n/en.ts
  • packages/app/src/context/platform.tsx
  • packages/app/src/pages/session/message-timeline.tsx
📚 Learning: 2026-04-23T15:10:21.635Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 191
File: packages/app/src/components/session/pawwork-skill-meta.ts:38-39
Timestamp: 2026-04-23T15:10:21.635Z
Learning: This repo configures Tailwind v4 with `--color-*: initial`, which effectively breaks standard Tailwind palette utilities (e.g., `text-violet-500` can resolve to no CSS variable and render as a no-op/black). For brand/accent colors that are not backed by semantic design tokens, use inline styles with the exact hex value (e.g., `style={{ color: '#8B5FBF' }}` / `homeIconStyle: { color: '#8B5FBF' }`) and add a short comment explaining that Tailwind palette utilities won’t work due to the `--color-*: initial` setup. Do not suggest replacing these inline hex colors with Tailwind palette classes anywhere in this repo.

Applied to files:

  • packages/app/src/i18n/zh.ts
  • packages/app/src/i18n/en.ts
  • packages/app/src/context/platform.tsx
  • packages/app/src/pages/session/message-timeline.tsx
📚 Learning: 2026-04-20T14:36:08.774Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/desktop-electron/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:08.774Z
Learning: Applies to packages/desktop-electron/src/**/*.{ts,tsx,js,jsx} : Renderer process should only call `window.api` from `src/preload`

Applied to files:

  • packages/desktop-electron/src/renderer/index.tsx
  • packages/desktop-electron/src/preload/index.ts
  • packages/desktop-electron/src/preload/types.ts
  • packages/desktop-electron/src/main/ipc.ts
📚 Learning: 2026-04-25T09:19:30.734Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 231
File: packages/desktop-electron/src/main/index.ts:537-537
Timestamp: 2026-04-25T09:19:30.734Z
Learning: In Astro-Han/pawwork (packages/desktop-electron/src/main/), follow the IPC registration convention: the bootstrap entry (packages/desktop-electron/src/main/index.ts) should directly call each module’s exported register*Ipc() function. Do not route/centralize these sub-module IPC registrations through src/main/ipc.ts. Keep sub-module IPC features cohesive (e.g., src/main/ipc/about.ts should own its types/helpers and expose register*Ipc()), and allow index.ts to aggregate by calling each register*Ipc() directly.

Applied to files:

  • packages/desktop-electron/src/main/index.ts
  • packages/desktop-electron/src/main/ipc.ts
  • packages/desktop-electron/src/main/server-client.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `makeRuntime` from `src/effect/run-service.ts` for all services; it returns `{ runPromise, runFork, runCallback }` backed by a shared `memoMap` that deduplicates layers

Applied to files:

  • packages/opencode/src/effect/app-runtime.ts
  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/share/runtime.ts
  • packages/opencode/src/share/session.ts
  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers in Effect tests when `testEffect(...)` already provides the runtime.

Applied to files:

  • packages/opencode/src/effect/app-runtime.ts
  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/share/runtime.ts
  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
📚 Learning: 2026-04-22T09:32:54.556Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/opencode/test/provider/provider.test.ts:64-85
Timestamp: 2026-04-22T09:32:54.556Z
Learning: In `packages/opencode/test/provider/provider.test.ts`, the file intentionally uses AppRuntime-based async helpers (`run`, `list`, `getProvider`, etc.) rather than `testEffect(...)` for all tests. Converting individual tests to `testEffect` while leaving the rest on the async pattern would create internal inconsistency. A full harness migration of this file is the right approach if the pattern needs to change, but that should be a separate PR.

Applied to files:

  • packages/opencode/src/effect/app-runtime.ts
  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/share/runtime.ts
  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Prefer Effect-aware helpers from `fixture/fixture.ts` over building manual runtimes in tests: use `tmpdirScoped()` for scoped temp directories, `provideInstance(dir)(effect)` for low-level binding without directory creation, `provideTmpdirInstance(...)` for single temp instance binding, or `provideTmpdirServer(...)` for tests that also need the test LLM server.

Applied to files:

  • packages/opencode/src/effect/app-runtime.ts
  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/share/runtime.ts
  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-25T09:19:30.734Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 231
File: packages/desktop-electron/src/main/index.ts:537-537
Timestamp: 2026-04-25T09:19:30.734Z
Learning: In Astro-Han/pawwork (`packages/desktop-electron/src/main/`), the convention for IPC registration is that `index.ts` (the bootstrap entry) directly calls each registration function. `registerIpcHandlers({...})` in `src/main/ipc.ts` is a single fat options-bag with inline handler bodies. New, cohesive IPC features (e.g., About) are placed in sub-modules like `src/main/ipc/about.ts`, which own their own exported types, helpers, and a `register*Ipc()` function. The bootstrap in `index.ts` calls each `register*Ipc()` directly — this is intentional, not fragmentation. Do NOT suggest routing sub-module IPC registrations through `ipc.ts`.

Applied to files:

  • packages/desktop-electron/src/preload/index.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Import test utilities from `../fixtures` instead of `playwright/test`

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/test/cli/export.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/packages/app/src/testing/**/*.ts : Test-only hooks must be inert unless explicitly enabled and should not add normal-runtime listeners, reactive subscriptions, or per-update allocations

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Define `const it = testEffect(...)` near the top of the test file and keep the test body inside `Effect.gen(function* () { ... })`. Yield services directly with `yield* MyService.Service` or `yield* MyTool`.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/src/share/runtime.ts
  • packages/opencode/src/share/session.ts
  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
📚 Learning: 2026-04-23T08:51:00.819Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 186
File: packages/opencode/test/plugin/workspace-adaptor.test.ts:139-144
Timestamp: 2026-04-23T08:51:00.819Z
Learning: For pawwork tests under packages/opencode/test/**, auth.json teardown may intentionally combine `Filesystem.write` (from `packages/opencode/src/util/filesystem.ts`) with `node:fs/promises` `unlink` for cleanup. Do not flag this as inconsistent style; it is the established/intentional pattern because `Filesystem` does not provide a `remove`/`unlink` helper.

Applied to files:

  • packages/opencode/test/share/share-next.test.ts
  • packages/opencode/test/cli/export.test.ts
  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Call `project.trackSession(sessionID, directory?)` and `project.trackDirectory(directory)` for any resources created outside the fixture so teardown can clean them up

Applied to files:

  • packages/desktop-electron/src/preload/types.ts
  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-22T08:49:47.800Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:47.800Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/opencode/test/cli/export.test.ts
  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `tmpdir` function from `fixture/fixture.ts` to create temporary directories for tests with automatic cleanup. Use `await using` syntax to ensure automatic cleanup when the variable goes out of scope.

Applied to files:

  • packages/opencode/test/cli/export.test.ts
  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Prefer `Path.Path`, `Config`, `Clock`, and `DateTime` services when those concerns are already inside Effect code

Applied to files:

  • packages/opencode/src/share/runtime.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation rather than storing `Fiber | undefined` or `Promise | undefined` manually

Applied to files:

  • packages/opencode/src/share/runtime.ts
  • packages/opencode/src/share/session.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Prefer `FileSystem.FileSystem` instead of raw `fs/promises` for effectful file I/O in Effect services

Applied to files:

  • packages/opencode/src/share/runtime.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : For background loops or scheduled tasks, use `Effect.repeat` or `Effect.schedule` with `Effect.forkScoped` in the layer definition

Applied to files:

  • packages/opencode/src/share/runtime.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Prefer `ChildProcessSpawner.ChildProcessSpawner` with `ChildProcess.make(...)` instead of custom process wrappers in Effect services

Applied to files:

  • packages/opencode/src/share/runtime.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.gen(function* () { ... })` for Effect composition

Applied to files:

  • packages/opencode/src/share/session.ts
📚 Learning: 2026-04-20T14:36:21.288Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:21.288Z
Learning: Applies to packages/opencode/**/*.ts : Use `Effect.forkScoped` inside the `InstanceState.make` closure for background stream consumers — the fiber is interrupted when the instance is disposed

Applied to files:

  • packages/opencode/src/share/session.ts
📚 Learning: 2026-04-24T03:51:54.050Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 206
File: packages/app/e2e/prompt/prompt-footer-focus.spec.ts:131-143
Timestamp: 2026-04-24T03:51:54.050Z
Learning: In Astro-Han/pawwork E2E tests (packages/app/e2e/fixtures.ts), `project.prompt(text)` internally calls `trackSession(next.sessionID, active.directory)` after the prompt submission is observed and the active session is resolved. Tests that obtain a `sessionID` from `project.prompt()` do NOT need to call `project.trackSession(sessionID)` manually — it is already registered for teardown. Calling it again would be duplicate ownership.

Applied to files:

  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-24T00:02:53.315Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 203
File: packages/app/e2e/sidebar/sidebar-session-links.spec.ts:34-55
Timestamp: 2026-04-24T00:02:53.315Z
Learning: In Astro-Han/pawwork E2E tests (`packages/app/e2e/**/*.spec.ts`), `project.trackDirectory()` and `project.trackSession()` cannot be called before `project.open()` — the `project` fixture throws until `open()` initializes its internal state. The correct pattern is: call `project.trackSession(sessionID)` from inside the `beforeGoto` callback (where state already exists), call `project.trackDirectory(directory)` and cross-workspace `project.trackSession(id, directory)` immediately after `project.open()` returns, and rely on explicit `finally` cleanup (e.g. `cleanupSession` / `cleanupTestProject`) for any resources created before `open()` that cannot yet be tracked via the fixture.

Applied to files:

  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`. Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.

Applied to files:

  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : When a test needs instance-local state, prefer `provideTmpdirInstance(...)` or `provideInstance(...)` over manual `Instance.provide(...)` inside Promise-style tests.

Applied to files:

  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
📚 Learning: 2026-04-23T15:25:31.118Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 193
File: packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts:5-55
Timestamp: 2026-04-23T15:25:31.118Z
Learning: In Astro-Han/pawwork E2E tests, reaching a real "running" session state requires the `project` fixture (for model bootstrap) plus `llm.wait(1)` orchestration. The bare `sdk` fixture used by `withSession` does not trigger an LLM call even with `agent: "build"` + `system` prompt set via `sdk.session.promptAsync`, so simulating running state is not lightweight in the current test infrastructure.

Applied to files:

  • packages/opencode/test/share/session-pawwork-fail-closed.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `config` option in `tmpdir` to write an `opencode.json` config file during test setup by passing a partial Config.Info object.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:31.032Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/opencode/test/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:31.032Z
Learning: Applies to packages/opencode/test/**/*.test.{ts,tsx} : Use the `init` option in `tmpdir` to define custom setup functions that can return extra data accessible via `tmp.extra`, and use the `dispose` option for custom cleanup logic.

Applied to files:

  • packages/opencode/test/session/export.test.ts
📚 Learning: 2026-04-20T14:36:04.113Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.113Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use fixture-managed cleanup with `withSession(sdk, title, callback)` for temporary sessions instead of calling `sdk.session.delete(...)` directly

Applied to files:

  • packages/opencode/test/session/export.test.ts
🔇 Additional comments (29)
packages/opencode/src/share/runtime.ts (1)

9-30: Nice fail-closed gate abstraction.

Centralizing the default gate and typed disabled error here gives the rest of the share stack a single source of truth for PawWork behavior.

packages/opencode/src/effect/app-runtime.ts (1)

49-49: Good runtime wiring.

Providing the gate at the app-layer level keeps the fail-closed behavior consistent anywhere the share services are resolved.

Also applies to: 98-98

packages/opencode/src/share/share-next.ts (1)

16-16: Good gate integration for the background sync path.

Skipping subscriber setup when the gate is closed is the right fail-closed behavior for PawWork.

Also applies to: 119-119, 161-161, 325-325, 351-351

packages/opencode/src/share/session.ts (1)

10-10: Good local-only short-circuit.

Skipping the background share fork when the gate is closed matches the PR objective and keeps root-session creation fully local in PawWork.

Also applies to: 54-69

packages/app/src/i18n/zh.ts (1)

636-638: LGTM.

These keys are consistent with the new export flow and read naturally in the existing zh UI copy.

packages/app/src/i18n/en.ts (1)

700-702: LGTM.

The new copy is clear and matches the rest of the session action vocabulary.

packages/opencode/test/cli/export.test.ts (1)

3-5: Good test realignment.

Pulling sanitize from the shared export module keeps the CLI assertions locked to the implementation that now owns the export format.

packages/opencode/src/session/message-v2.ts (1)

187-187: LGTM!

The addition of an optional metadata field to FilePart is consistent with other part types (TextPart, ReasoningPart, ToolPart, etc.) that already have this pattern. This enables the export pipeline to attach redacted_binary metadata when redacting binary attachments.

packages/desktop-electron/src/main/index.ts (1)

499-499: LGTM!

The getServerReadyData accessor follows the established pattern for providing dependencies to IPC handlers. It exposes the same serverReady.promise already used by awaitInitialization, enabling the new export-session IPC handler to obtain server credentials without duplicating initialization logic.

packages/desktop-electron/src/preload/index.ts (1)

62-63: LGTM!

The preload bridge correctly delegates exportSession to the main process via IPC. The method signature aligns with the ElectronAPI type definition and the main-process handler.

packages/desktop-electron/src/renderer/index.tsx (1)

178-180: LGTM!

The exportSession method correctly delegates to window.api.exportSession, following the established pattern for platform capabilities in the desktop renderer. This adheres to the coding guideline that renderer process should only call window.api from the preload bridge.

packages/desktop-electron/src/preload/types.ts (1)

77-81: LGTM!

The exportSession type definition uses a clear discriminated union for success/failure states, enabling type-safe error handling in consumers. The optional defaultName parameter and the error: string field accommodate the various error codes returned by the IPC handler (invalid_args, cancelled, fetch_failed, etc.).

packages/opencode/src/cli/cmd/export.ts (1)

76-79: LGTM!

The refactoring correctly delegates to the shared Export module:

  • Export.session() produces the versioned snapshot with runtime context
  • Export.sanitizeSnapshot() handles path/URL redaction when --sanitize is enabled

This consolidates the export logic and ensures CLI and server endpoints produce consistent output.

packages/app/src/context/platform.tsx (1)

98-106: LGTM!

The optional exportSession method is well-documented and follows the established pattern for desktop-only capabilities in the Platform type. The return type correctly matches the ElectronAPI contract.

packages/opencode/src/server/instance/session.ts (4)

13-14: LGTM!

The imports are correctly added for the new export endpoint and cloud share gating functionality.


444-452: LGTM!

The cloud share gating correctly uses Effect.gen(function* () { ... }) per coding guidelines. Returning HTTP 410 with { error: "cloud_share_disabled" } is appropriate for indicating the feature is intentionally disabled.


461-486: LGTM!

The export endpoint is well-documented with OpenAPI metadata and correctly delegates to Export.session(). The description accurately explains the climb-to-root behavior for child sessions.


592-600: LGTM!

The cloud share gating for the DELETE endpoint mirrors the POST endpoint pattern, ensuring consistent behavior when cloud sharing is disabled.

packages/app/src/pages/session/message-timeline.tsx (2)

394-431: LGTM! Well-structured export handler with comprehensive error handling.

The onExport function correctly:

  • Guards against missing platform.exportSession capability
  • Builds a filesystem-safe filename with Unicode letter/number support for CJK titles
  • Silently handles user cancellation ("cancelled")
  • Distinguishes between thrown exceptions and returned errors with appropriate toast feedback

856-867: LGTM! Export menu item correctly integrated.

The conditional rendering with Show when={platform.exportSession} properly gates this desktop-only feature, and closing the menu before invoking the async export prevents UI state issues.

packages/desktop-electron/src/main/ipc.ts (1)

42-42: LGTM!

The getServerReadyData dependency injection follows the established pattern and matches the implementation in index.ts.

packages/desktop-electron/src/main/server-client.ts (1)

25-37: LGTM! Clean fetch wrapper with proper error handling.

The function correctly:

  • Returns a discriminated union for type-safe error handling
  • Maps HTTP errors to identifiable server_<status> strings
  • Catches and surfaces network/runtime exceptions
packages/opencode/test/session/export.test.ts (3)

14-51: LGTM! Comprehensive test coverage for core export functionality.

The tests thoroughly validate:

  • Runtime namespace detection
  • Session export structure (schema version, format, timestamps, IDs)
  • Correct handling of share field removal and had_cloud_share tracking
  • Empty messages/diffs/children for root-only sessions

74-99: LGTM! Well-designed deterministic ordering test.

The test correctly:

  • Uses a timing gap to ensure distinct time.created values
  • Validates the precondition independently before checking export order
  • Uses hard-coded expected order to avoid tautological assertions

183-324: LGTM! Thorough redaction test coverage.

Excellent coverage including:

  • Standard data URL redaction with metadata preservation
  • Non-data URL passthrough
  • RFC 2397 edge case with extra parameters (charset)
  • Instruction source path/URL sanitization
  • Nested tool attachment redaction
packages/opencode/src/session/export.ts (3)

272-299: LGTM! Well-structured session export effect.

The function cleanly composes:

  • Root session resolution via climbToRoot
  • Full tree export with redaction tracking
  • Instruction source and model reference collection
  • Complete Snapshot structure with stats

569-580: LGTM! Sanitize helpers are comprehensive and well-documented.

The sanitization pipeline:

  • Handles all 12 MessageV2.Part types systematically
  • Uses consistent [redacted:kind:id] markers for traceability
  • Documents the type cast necessity at the sanitizeTree boundary
  • Recursively processes the entire session tree

213-219: No action needed. The bundled prompt file path is correct and already verified by tests.

The file exists at the expected location (packages/opencode/src/session/prompt/pawwork.txt), and the test inventory in system.test.ts explicitly validates its presence alongside other prompt files. The defensive code—using hashFile() which returns undefined for missing files—already handles the graceful fallback correctly.

packages/opencode/test/share/session-pawwork-fail-closed.test.ts (1)

41-47: The suggested Cause.failures() API does not exist in Effect 4.x; use direct .reasons access with proper filtering instead.

Effect 4.x removed Cause.failures() and restructured Cause as a flat array of Reason objects. The current cast to access .reasons is pragmatic, but can be made more explicit by filtering for Fail reasons:

♻️ Replace unsafe type cast with explicit Fail reason filtering
-          const reasons = (exit.cause as unknown as { reasons?: ReadonlyArray<{ error?: unknown }> }).reasons ?? []
-          const failed = reasons.find((r) => r.error instanceof ShareRuntime.CloudShareDisabled)
+          const failures = exit.cause.reasons.filter((r) => r._tag === "Fail")
+          const failed = failures.find((r) => r.error instanceof ShareRuntime.CloudShareDisabled)
			> Likely an incorrect or invalid review comment.

Comment thread packages/desktop-electron/src/main/ipc.ts
Comment thread packages/desktop-electron/src/main/server-client.ts
Comment thread packages/opencode/src/server/instance/session.ts
Comment thread packages/opencode/src/session/export.ts
Comment thread packages/opencode/src/session/export.ts
Comment thread packages/opencode/src/share/session.ts
Comment thread packages/opencode/src/share/share-next.ts Outdated
Comment thread packages/opencode/test/share/share-next.test.ts
Comment thread packages/desktop-electron/src/main/ipc.ts
Comment thread packages/opencode/src/session/export.ts
Comment thread packages/desktop-electron/src/main/server-client.ts Outdated
…fetch timeout)

- sanitize covers Tree.diffs (file/patch) on every node + regression test
- renderer hides Export when active server isn't sidecar (UI Show guard)
- fetchExport adds 10s AbortController timeout, distinguishes timeout vs other errors
- ShareNext.create/remove fail closed with typed CloudShareDisabled instead of empty Share stub
- export route uses NotFoundError instanceof instead of string match
- session/export.ts uses fileURLToPath(import.meta.url) for ESM-safe __dirname
- share-next.test.ts: add disabled-gate regression test
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.

[Feature] Replace cloud session sharing with local session export

1 participant