chore(release): v0.4.12 (versionCode 39) — global recent sessions + integration test (#145)

This commit is contained in:
dzianisv
2026-07-23 00:59:16 -07:00
parent bc1dbcf0cd
commit 3ac7eb786c
8 changed files with 131 additions and 7 deletions

View File

@@ -94,8 +94,8 @@ android {
applicationId 'cc.agentlabs.opencode'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 38
versionName "0.4.11"
versionCode 39
versionName "0.4.12"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "OpenCode",
"slug": "opencode-mobile",
"version": "0.4.11",
"version": "0.4.12",
"orientation": "portrait",
"scheme": "opencode",
"userInterfaceStyle": "automatic",
@@ -35,7 +35,7 @@
},
"android": {
"package": "cc.agentlabs.opencode",
"versionCode": 38,
"versionCode": 39,
"usesCleartextTraffic": true,
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",

View File

@@ -0,0 +1,5 @@
v0.4.12 — Global recent sessions
• Recent Sessions now shows every session across all your projects automatically.
• No need to pick a folder or directory first — the list is populated globally on connect.
• Falls back to per-directory listing on older servers.

View File

@@ -1,3 +1,3 @@
v0.4.11 — Release reliability fix
v0.4.12 — Global recent sessions
This maintenance release aligns Android version metadata across every build path so Google Play, direct APK, and F-Droid packages report the same version. No runtime behavior changed.
Recent Sessions now shows every session across all your projects automatically — no need to pick a folder or directory first. The list is populated globally as soon as you connect, and falls back to per-directory listing on older servers.

View File

@@ -0,0 +1,5 @@
v0.4.12 — global recent sessions
- Recent Sessions now shows every session across all your projects automatically.
- No need to pick a folder or directory first — the list is populated globally on connect.
- Falls back to per-directory listing on older servers.

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/mobile",
"version": "0.4.11",
"version": "0.4.12",
"private": true,
"main": "expo-router/entry",
"scripts": {

View File

@@ -0,0 +1,102 @@
// Integration test for the "global recent sessions" feature.
//
// Unlike session-list.test.ts (pure logic, injected fakes), this stands up the
// real mock opencode server over HTTP and drives loadSessionList through the
// SAME transport shape sdk.ts wires in production (fetch /experimental/session
// first, fall back to /session on 404). It proves the feature works end-to-end
// across the HTTP boundary: the Recent Sessions list is populated globally —
// every session across every directory — WITHOUT the user picking a folder.
//
// Run: node --test src/lib/session-list.integration.test.ts
import { test, before, after } from "node:test"
import assert from "node:assert/strict"
import { createMockOpencodeServer } from "../../tests/fixtures/mock-opencode-server.ts"
import { loadSessionList, legacySessionQuery, type SessionListTransport } from "./session-list.ts"
const PORT = 45071
let mock: ReturnType<typeof createMockOpencodeServer>
let base: string
before(async () => {
// --seed-sessions pre-populates two sessions in two DIFFERENT directories:
// seed-default -> /mock/project (updated now-120s)
// seed-other -> /mock/project/other-dir (updated now-60s, more recent)
mock = createMockOpencodeServer({ port: PORT, seedSessions: true })
await mock.listen()
base = mock.url
})
after(async () => {
await mock.close()
})
// The exact transport sdk.ts builds in production: prefer the global
// experimental endpoint, signal fallback (null) only on 404.
function realTransport(baseUrl: string): SessionListTransport {
return {
getExperimental: async () => {
const r = await fetch(`${baseUrl}/experimental/session`, { headers: { Accept: "application/json" } })
if (r.status === 404) return null
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
},
getLegacy: async (query) => {
const r = await fetch(`${baseUrl}/session${query}`, { headers: { Accept: "application/json" } })
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
},
}
}
test("global sessions: lists sessions from EVERY directory without picking a folder", async () => {
const sessions = await loadSessionList(realTransport(base), { roots: true, limit: 50 })
// Both directories are represented — this is the whole feature.
const ids = sessions.map((s) => s.id)
assert.ok(ids.includes("seed-default"), "session from /mock/project must appear")
assert.ok(ids.includes("seed-other"), "session from /mock/project/other-dir must appear")
assert.equal(sessions.length, 2)
const dirs = new Set(sessions.map((s) => s.directory))
assert.equal(dirs.size, 2, "sessions span two distinct directories (global, not directory-scoped)")
// Most-recently-updated first: seed-other (now-60s) before seed-default (now-120s).
assert.equal(sessions[0].id, "seed-other")
assert.equal(sessions[1].id, "seed-default")
})
test("hit the experimental endpoint, not the directory-scoped one", async () => {
// Proves WHY the feature was needed: a plain directory-less GET /session
// (no ?roots, no x-opencode-directory header) is directory-scoped and returns
// ONLY the default directory's session — the old behavior that left the list
// empty/partial until a folder was chosen.
const scoped = await (await fetch(`${base}/session`)).json()
assert.equal(scoped.length, 1, "directory-scoped /session returns only the default dir")
assert.equal(scoped[0].id, "seed-default")
// The global endpoint returns everything.
const global = await (await fetch(`${base}/experimental/session`)).json()
assert.equal(global.length, 2, "/experimental/session returns all sessions globally")
})
test("older servers (404 on /experimental/session) fall back to /session and still list globally", async () => {
// Simulate a server that predates /experimental/session: getExperimental
// resolves null (as it would on a 404), forcing the legacy path.
const legacyTransport: SessionListTransport = {
getExperimental: async () => null,
getLegacy: async (query) => {
const r = await fetch(`${base}/session${query}`, { headers: { Accept: "application/json" } })
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
},
}
const sessions = await loadSessionList(legacyTransport, { roots: true, limit: 50 })
// The mock's /session?roots=true returns all sessions, so the fallback still
// produces a global list on old servers.
assert.equal(sessions.length, 2)
assert.deepEqual(new Set(sessions.map((s) => s.id)), new Set(["seed-default", "seed-other"]))
// Sanity: the legacy query we sent is the exact one the old code sent.
assert.equal(legacySessionQuery({ roots: true, limit: 50 }), "?roots=true&limit=50")
})

View File

@@ -544,6 +544,16 @@ export function createMockOpencodeServer(opts: MockServerOptions) {
messagesBySession.set(id, [])
return json(res, 200, session)
}
// Global "all sessions across every directory" list, mirroring the real
// opencode server's GET /experimental/session. This is the endpoint the
// client now PREFERS (src/lib/sdk.ts session.list -> loadSessionList): on a
// real server a directory-less GET /session is directory-scoped and returns
// [] when the active dir has no sessions, so the Recent Sessions list was
// empty until the user picked a folder. /experimental/session always
// returns every session, ignoring the x-opencode-directory header.
if (method === "GET" && path === "/experimental/session") {
return json(res, 200, Array.from(sessions.values()))
}
if (method === "GET" && path === "/session") {
// Directory-less "all sessions across all projects" list: the app's
// loadSessions() (src/stores/sessions.ts) uses clientForDirectory(undefined)
@@ -551,6 +561,8 @@ export function createMockOpencodeServer(opts: MockServerOptions) {
// session.list). Only that combination returns sessions from every
// directory; otherwise the list is scoped to the request's directory, so
// directory-scoped and directory-less clients are actually distinguishable.
// NOTE: this legacy path remains the 404-fallback for older servers that
// predate /experimental/session.
if (url.searchParams.get("roots") === "true") {
return json(res, 200, Array.from(sessions.values()))
}