Apply PR #40845: feat(app): redesign non-modal settings

This commit is contained in:
opencode-agent[bot] 2026-08-07 13:12:27 +00:00
commit cd44746a5e
109 changed files with 6872 additions and 4666 deletions

View file

@ -0,0 +1,12 @@
id: no-drizzle-column-name
snapshots:
? |
const table = sqliteTable("session", {
projectID: text("project_id").notNull(),
createdAt: integer("time_created").notNull(),
})
: labels:
- source: text("project_id")
style: primary
start: 52
end: 70

View file

@ -0,0 +1,30 @@
id: no-effect-die-string
snapshots:
Effect.die("boom"):
labels:
- source: Effect.die("boom")
style: primary
start: 0
end: 18
- source: '"boom"'
style: secondary
start: 11
end: 17
- source: ("boom")
style: secondary
start: 10
end: 18
Effect.die(`boom ${value}`):
labels:
- source: Effect.die(`boom ${value}`)
style: primary
start: 0
end: 27
- source: '`boom ${value}`'
style: secondary
start: 11
end: 26
- source: (`boom ${value}`)
style: secondary
start: 10
end: 27

View file

@ -0,0 +1,42 @@
id: no-import-alias
snapshots:
import { baz, foo as bar } from "./foo":
labels:
- source: foo as bar
style: primary
start: 14
end: 24
- source: bar
style: secondary
start: 21
end: 24
import { foo as bar } from "./foo":
labels:
- source: foo as bar
style: primary
start: 9
end: 19
- source: bar
style: secondary
start: 16
end: 19
import { foo as bar, baz } from "./foo":
labels:
- source: foo as bar
style: primary
start: 9
end: 19
- source: bar
style: secondary
start: 16
end: 19
import { type Foo as Bar, baz } from "./foo":
labels:
- source: type Foo as Bar
style: primary
start: 9
end: 24
- source: Bar
style: secondary
start: 21
end: 24

View file

@ -0,0 +1,8 @@
id: no-json-parse-cast
snapshots:
const value = JSON.parse(input) as Record<string, unknown>:
labels:
- source: JSON.parse(input) as Record<string, unknown>
style: primary
start: 14
end: 58

View file

@ -0,0 +1,20 @@
id: no-nested-effect-service-yield
snapshots:
? |
Effect.gen(function* () {
yield* (yield* Foo.Service).client.run()
})
: labels:
- source: (yield* Foo.Service).client.run()
style: primary
start: 35
end: 68
? |
Effect.gen(function* () {
yield* (yield* Foo.Service).run()
})
: labels:
- source: (yield* Foo.Service).run()
style: primary
start: 35
end: 61

View file

@ -0,0 +1,22 @@
id: no-star-import
snapshots:
import * as Foo from "./foo":
labels:
- source: '* as Foo'
style: primary
start: 7
end: 15
- source: import * as Foo from "./foo"
style: secondary
start: 0
end: 28
import type * as Foo from "./foo":
labels:
- source: '* as Foo'
style: primary
start: 12
end: 20
- source: import type * as Foo from "./foo"
style: secondary
start: 0
end: 33

View file

@ -0,0 +1,14 @@
id: no-drizzle-column-name
valid:
- |
const table = sqliteTable("session", {
project_id: text().notNull(),
time_created: integer().notNull(),
payload: text({ mode: "json" }),
})
invalid:
- |
const table = sqliteTable("session", {
projectID: text("project_id").notNull(),
createdAt: integer("time_created").notNull(),
})

View file

@ -0,0 +1,7 @@
id: no-effect-die-string
valid:
- Effect.die(new Error("boom"))
- Effect.fail("boom")
invalid:
- Effect.die("boom")
- Effect.die(`boom ${value}`)

View file

@ -0,0 +1,13 @@
id: no-import-alias
valid:
- import { foo } from "./foo"
- import type { Foo } from "./foo"
- import foo from "./foo"
- export { foo as bar } from "./foo"
- import type { Plugin as EffectPlugin } from "./foo"
- import type { Foo as Bar, Baz } from "./foo"
invalid:
- import { foo as bar } from "./foo"
- import { baz, foo as bar } from "./foo"
- import { foo as bar, baz } from "./foo"
- import { type Foo as Bar, baz } from "./foo"

View file

@ -0,0 +1,6 @@
id: no-json-parse-cast
valid:
- const value = JSON.parse(input)
- const value = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(input)
invalid:
- const value = JSON.parse(input) as Record<string, unknown>

View file

@ -0,0 +1,21 @@
id: no-nested-effect-service-yield
valid:
- |
Effect.gen(function* () {
const service = yield* Foo.Service
yield* service.run()
})
- |
Effect.gen(function* () {
const db = (yield* Database.Service).db
yield* db.run()
})
invalid:
- |
Effect.gen(function* () {
yield* (yield* Foo.Service).run()
})
- |
Effect.gen(function* () {
yield* (yield* Foo.Service).client.run()
})

View file

@ -0,0 +1,8 @@
id: no-star-import
valid:
- import { Foo } from "./foo"
- import Foo from "./foo"
- export * as Foo from "./foo"
invalid:
- import * as Foo from "./foo"
- import type * as Foo from "./foo"

View file

@ -0,0 +1,22 @@
id: no-drizzle-column-name
language: TypeScript
message: Use snake_case object keys instead of explicit drizzle column names.
severity: error
files:
- packages/core/src/**/sql.ts
- packages/core/src/**/*.sql.ts
rule:
any:
- pattern: text($NAME)
- pattern: text($NAME, $$$ARGS)
- pattern: integer($NAME)
- pattern: integer($NAME, $$$ARGS)
- pattern: blob($NAME)
- pattern: blob($NAME, $$$ARGS)
- pattern: real($NAME)
- pattern: real($NAME, $$$ARGS)
- pattern: numeric($NAME)
- pattern: numeric($NAME, $$$ARGS)
constraints:
NAME:
kind: string

View file

@ -0,0 +1,22 @@
id: no-effect-die-string
language: TypeScript
message: die with `new Error(...)`.
severity: error
rule:
any:
- all:
- pattern: Effect.die($MESSAGE)
- has:
field: arguments
all:
- kind: arguments
- has:
kind: string
- all:
- pattern: Effect.die($MESSAGE)
- has:
field: arguments
all:
- kind: arguments
- has:
kind: template_string

View file

@ -0,0 +1,14 @@
id: no-import-alias
language: TypeScript
message: Do not alias value imports. For type name collisions, alias inside a dedicated `import type` statement.
severity: error
rule:
all:
- kind: import_specifier
- has:
field: alias
kind: identifier
- not:
inside:
pattern: import type { $$$SPECS } from "$MOD"
stopBy: end

View file

@ -0,0 +1,6 @@
id: no-json-parse-cast
language: TypeScript
message: Prefer Effect Schema JSON decoding over JSON.parse casts.
severity: error
rule:
pattern: JSON.parse($INPUT) as $TYPE

View file

@ -0,0 +1,11 @@
id: no-nested-effect-service-yield
language: TypeScript
message: Bind Effect services before calling methods instead of nesting service yields.
severity: error
rule:
any:
- pattern: (yield* $SERVICE).$METHOD($$$ARGS)
- pattern: (yield* $SERVICE).$PROPERTY.$METHOD($$$ARGS)
constraints:
SERVICE:
regex: \.Service$

View file

@ -0,0 +1,10 @@
id: no-star-import
language: TypeScript
message: Do not use star imports.
severity: error
rule:
all:
- kind: namespace_import
- inside:
kind: import_statement
stopBy: end

View file

@ -0,0 +1,4 @@
ruleDirs:
- rules
testConfigs:
- testDir: rule-tests

View file

@ -2,8 +2,8 @@
import { $ } from "bun"
await $`bun ./packages/sdk/js/script/build.ts`
await $`bun run generate`.cwd("packages/protocol")
await $`bun dev generate > ../sdk/openapi.json`.cwd("packages/opencode")
await $`bun run generate`.cwd("packages/www")
await $`./script/format.ts`

View file

@ -0,0 +1,66 @@
#!/usr/bin/env bun
import path from "path"
const root = path.resolve(import.meta.dir, "..")
const proc = Bun.spawn(
[
"bun",
"turbo",
"typecheck",
"--concurrency=1",
"--force",
"--continue=always",
"--summarize",
"--output-logs=errors-only",
],
{
cwd: root,
stdout: "pipe",
stderr: "pipe",
},
)
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited])
const output = stdout + stderr
if (exitCode !== 0) {
process.stdout.write(stdout)
process.stderr.write(stderr)
process.exit(exitCode)
}
const summary = output.match(/Summary:\s+(.+\.json)/)?.[1]?.trim()
if (!summary) {
process.stdout.write(stdout)
process.stderr.write(stderr)
throw new Error("Turbo did not report a run summary")
}
const report = (await Bun.file(summary).json()) as {
tasks: Array<{
taskId: string
execution: { startTime: number; endTime: number; exitCode: number } | null
}>
}
const tasks = report.tasks
.flatMap((task) =>
task.execution
? [
{
task: task.taskId.replace(/#typecheck$/, ""),
durationMs: task.execution.endTime - task.execution.startTime,
},
]
: [],
)
.sort((a, b) => b.durationMs - a.durationMs)
const total = tasks.reduce((duration, task) => duration + task.durationMs, 0)
const width = Math.max(...tasks.map((task) => task.task.length), "Package".length)
console.log(`Package${" ".repeat(width - "Package".length)} Time Share`)
tasks.forEach((task) => {
const duration = `${(task.durationMs / 1000).toFixed(2)}s`.padStart(7)
const share = `${((task.durationMs / total) * 100).toFixed(1)}%`.padStart(6)
console.log(`${task.task.padEnd(width)} ${duration} ${share}`)
})
console.log(`\nTotal serial task time: ${(total / 1000).toFixed(2)}s`)
console.log(`Turbo summary: ${path.relative(root, summary)}`)

172
script/profile-typecheck.ts Normal file
View file

@ -0,0 +1,172 @@
#!/usr/bin/env bun
import { mkdir } from "fs/promises"
import path from "path"
if (process.platform !== "darwin") throw new Error("System typecheck profiling currently supports macOS only")
const root = path.resolve(import.meta.dir, "..")
const startedAt = new Date()
const args = Bun.argv.slice(2)
const command = [
"bun",
"turbo",
"typecheck",
...(args.some((arg) => arg.startsWith("--concurrency")) ? [] : ["--concurrency=3"]),
...args,
]
const before = systemSnapshot()
const proc = Bun.spawn(command, {
cwd: root,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
})
const samples = [processTreeSnapshot(proc.pid, startedAt)]
const timer = setInterval(() => samples.push(processTreeSnapshot(proc.pid, startedAt)), 200)
const exitCode = await proc.exited
clearInterval(timer)
samples.push(processTreeSnapshot(proc.pid, startedAt))
const finishedAt = new Date()
const after = systemSnapshot()
const active = samples.filter((sample) => sample.processes > 0)
const report = {
command,
cwd: root,
startedAt: startedAt.toISOString(),
finishedAt: finishedAt.toISOString(),
durationSeconds: (finishedAt.getTime() - startedAt.getTime()) / 1000,
exitCode,
summary: {
peakCpuPercent: Math.max(0, ...active.map((sample) => sample.cpuPercent)),
averageCpuPercent: average(active.map((sample) => sample.cpuPercent)),
peakAggregateRssMB: Math.max(0, ...active.map((sample) => sample.aggregateRssMB)),
peakProcesses: Math.max(0, ...active.map((sample) => sample.processes)),
peakTsgoRelatedProcesses: Math.max(0, ...active.map((sample) => sample.tsgoRelatedProcesses)),
swapDeltaMB: after.swapUsedMB - before.swapUsedMB,
compressedMemoryDeltaMB: after.compressedMemoryMB - before.compressedMemoryMB,
pageoutDelta: after.pageouts - before.pageouts,
},
system: { before, after },
samples,
}
const directory = path.join(root, ".typecheck-profiles")
const file = path.join(directory, `${startedAt.toISOString().replaceAll(":", "-")}.json`)
await mkdir(directory, { recursive: true })
await Bun.write(file, JSON.stringify(report, null, 2) + "\n")
console.log(`
Typecheck profile
Duration: ${report.durationSeconds.toFixed(1)}s
Average CPU: ${report.summary.averageCpuPercent.toFixed(0)}%
Peak CPU: ${report.summary.peakCpuPercent.toFixed(0)}%
Aggregate RSS: ${report.summary.peakAggregateRssMB.toFixed(0)} MB
Peak processes: ${report.summary.peakProcesses} (${report.summary.peakTsgoRelatedProcesses} tsgo-related)
Swap delta: ${signed(report.summary.swapDeltaMB)} MB
Compressed: ${signed(report.summary.compressedMemoryDeltaMB)} MB
Pageouts: ${signed(report.summary.pageoutDelta)}
Report: ${path.relative(root, file)}
`)
process.exit(exitCode)
function processTreeSnapshot(rootPID: number, startedAt: Date) {
const processes = processList()
const pids = new Set([rootPID])
const pending = [rootPID]
while (pending.length > 0) {
const parent = pending.shift()
processes
.filter((process) => process.ppid === parent && !pids.has(process.pid))
.forEach((process) => {
pids.add(process.pid)
pending.push(process.pid)
})
}
const tree = processes.filter((process) => pids.has(process.pid))
return {
elapsedSeconds: (Date.now() - startedAt.getTime()) / 1000,
processes: tree.length,
tsgoRelatedProcesses: tree.filter((process) => /\btsgo\b/.test(process.command)).length,
cpuPercent: sum(tree.map((process) => process.cpuPercent)),
aggregateRssMB: sum(tree.map((process) => process.rssKB)) / 1024,
}
}
function systemSnapshot() {
const vm = text(["vm_stat"])
const pageSize = Number(vm.match(/page size of (\d+) bytes/)?.[1] ?? 4096)
const fields = Object.fromEntries(
vm
.split("\n")
.map((line) => line.match(/^([^:]+):\s+(\d+)\.?$/))
.filter((match): match is RegExpMatchArray => match !== null)
.map((match) => [match[1], Number(match[2])]),
)
const swap = text(["sysctl", "-n", "vm.swapusage"])
return {
loadAverage: text(["sysctl", "-n", "vm.loadavg"]).trim(),
thermalState: text(["pmset", "-g", "therm"]).trim(),
swapUsedMB: Number(swap.match(/used = ([\d.]+)M/)?.[1] ?? 0),
freeMemoryMB: ((fields["Pages free"] ?? 0) * pageSize) / 1024 / 1024,
compressedMemoryMB: ((fields["Pages occupied by compressor"] ?? 0) * pageSize) / 1024 / 1024,
pageouts: fields.Pageouts ?? 0,
relevantProcesses: processList()
.filter((process) =>
/opencode|tsgo|tsserver|vtsls|eslintServer|tailwindcss-language-server/.test(process.command),
)
.sort((a, b) => b.rssKB - a.rssKB)
.map(processSummary),
topCpuProcesses: processList()
.sort((a, b) => b.cpuPercent - a.cpuPercent)
.slice(0, 15)
.map(processSummary),
topMemoryProcesses: processList()
.sort((a, b) => b.rssKB - a.rssKB)
.slice(0, 15)
.map(processSummary),
}
}
function processSummary(process: ReturnType<typeof processList>[number]) {
return {
pid: process.pid,
ppid: process.ppid,
rssMB: process.rssKB / 1024,
cpuPercent: process.cpuPercent,
command: process.command,
}
}
function processList() {
return text(["ps", "-axo", "pid=,ppid=,rss=,%cpu=,command="])
.split("\n")
.map((line) => line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+([\d.]+)\s+(.*)$/))
.filter((match): match is RegExpMatchArray => match !== null)
.map((match) => ({
pid: Number(match[1]),
ppid: Number(match[2]),
rssKB: Number(match[3]),
cpuPercent: Number(match[4]),
command: match[5],
}))
}
function text(command: string[]) {
return Bun.spawnSync(command).stdout.toString()
}
function sum(values: number[]) {
return values.reduce((total, value) => total + value, 0)
}
function average(values: number[]) {
if (values.length === 0) return 0
return sum(values) / values.length
}
function signed(value: number) {
return `${value >= 0 ? "+" : ""}${value.toFixed(0)}`
}

View file

@ -3,6 +3,7 @@
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
import { fileURLToPath } from "url"
import { UpdateArtifact } from "./update-artifact"
console.log("=== publishing ===\n")
@ -25,7 +26,6 @@ async function prepareReleaseFiles() {
}
await $`bun install`
await $`./packages/sdk/js/script/build.ts`
}
if (Script.release && !Script.preview) {
@ -35,15 +35,27 @@ if (Script.release && !Script.preview) {
await prepareReleaseFiles()
console.log("\n=== schema ===\n")
await $`bun ./packages/schema/script/publish.ts`
console.log("\n=== theme ===\n")
await $`bun ./packages/theme/script/publish.ts`
console.log("\n=== ai ===\n")
await $`bun ./packages/ai/script/publish.ts`
console.log("\n=== util ===\n")
await $`bun ./packages/util/script/publish.ts`
console.log("\n=== protocol ===\n")
await $`bun ./packages/protocol/script/publish.ts`
console.log("\n=== client ===\n")
await $`bun ./packages/client/script/publish.ts`
console.log("\n=== cli ===\n")
await $`bun ./packages/opencode/script/publish.ts`
console.log("\n=== preview cli ===\n")
await $`bun ./packages/cli/script/publish.ts`
console.log("\n=== sdk ===\n")
await $`bun ./packages/sdk/js/script/publish.ts`
console.log("\n=== plugin ===\n")
await $`bun ./packages/plugin/script/publish.ts`
@ -70,4 +82,13 @@ if (Script.release && !Script.preview) {
if (Script.release) {
await $`gh release edit ${tag} --draft=false --repo ${process.env.GH_REPO}`
const repo = process.env.GH_REPO
if (!repo) throw new Error("GH_REPO is required")
await UpdateArtifact.publish({
channel: Script.channel,
name: "desktop",
distribution: "github",
version: Script.version,
metadata: await UpdateArtifact.desktopMetadata(Script.version, repo),
})
}

View file

@ -120,7 +120,7 @@ async function commits(from: string, to: string) {
}
const log =
await $`git log ${base}..${head} --format=%H -- packages/opencode packages/sdk packages/plugin packages/desktop packages/app sdks/vscode packages/extensions github`.text()
await $`git log ${base}..${head} --format=%H -- packages/opencode packages/plugin packages/desktop packages/app sdks/vscode packages/extensions github`.text()
const list: Commit[] = []
for (const hash of log.split("\n").filter(Boolean)) {
@ -136,7 +136,7 @@ async function commits(from: string, to: string) {
else if (file.startsWith("packages/opencode/")) areas.add("core")
else if (file.startsWith("packages/desktop/src-tauri/")) areas.add("tauri")
else if (file.startsWith("packages/desktop/") || file.startsWith("packages/app/")) areas.add("app")
else if (file.startsWith("packages/sdk/") || file.startsWith("packages/plugin/")) areas.add("sdk")
else if (file.startsWith("packages/plugin/")) areas.add("sdk")
else if (file.startsWith("sdks/vscode/") || file.startsWith("github/")) areas.add("extensions/vscode")
}

104
script/update-artifact.ts Normal file
View file

@ -0,0 +1,104 @@
type Artifact = {
channel: string
name: string
distribution: string
version: string
metadata: Record<string, unknown>
}
type DesktopFile = {
url: string
sha512: string
size: number
blockMapSize?: number
}
export namespace UpdateArtifact {
export async function publish(artifact: Artifact) {
if (process.env.GITHUB_ACTIONS !== "true") {
console.log("skipped update artifact publication outside GitHub Actions")
return
}
const requestURL = process.env.ACTIONS_ID_TOKEN_REQUEST_URL
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
if (!requestURL || !requestToken) throw new Error("GitHub Actions OIDC is unavailable")
const url = new URL(requestURL)
url.searchParams.set("audience", "https://update.opencode.ai")
const tokenResponse = await fetch(url, { headers: { Authorization: `Bearer ${requestToken}` } })
if (!tokenResponse.ok) throw new Error(`Failed to request GitHub OIDC token: ${tokenResponse.status}`)
const token: unknown = await tokenResponse.json()
if (!isRecord(token) || typeof token.value !== "string") throw new Error("GitHub OIDC response did not include a token")
const response = await fetch("https://update.opencode.ai/api/publish", {
method: "POST",
headers: {
Authorization: `Bearer ${token.value}`,
"Content-Type": "application/json",
},
body: JSON.stringify(artifact),
})
if (response.ok) return
throw new Error(`Failed to publish update artifact: ${response.status} ${await response.text()}`)
}
export async function desktopMetadata(version: string, repo: string) {
const directory = process.env.RUNNER_TEMP ?? "/tmp"
const entries = await Promise.all(
[
["desktop.yml", "latest.yml"],
["desktop-mac.yml", "latest-mac.yml"],
["desktop-linux.yml", "latest-linux.yml"],
["desktop-linux-arm64.yml", "latest-linux-arm64.yml"],
].map(async ([name, source]) => {
const file = Bun.file(`${directory}/${source}`)
if (!(await file.exists())) return
return [name, parseDesktop(await file.text(), version, repo)] as const
}),
)
const manifests = Object.fromEntries(entries.filter((entry) => entry !== undefined))
if (!Object.keys(manifests).length) throw new Error("No desktop update metadata found")
return { manifests }
}
}
function parseDesktop(content: string, version: string, repo: string) {
const lines = content.split("\n")
const found = lines.find((line) => line.startsWith("version:"))?.slice("version:".length).trim()
if (found !== version) throw new Error(`Desktop metadata version mismatch: expected ${version}, got ${found}`)
const releaseDate = lines
.find((line) => line.startsWith("releaseDate:"))
?.slice("releaseDate:".length)
.trim()
.replace(/^['"]|['"]$/g, "")
if (!releaseDate) throw new Error("Desktop metadata did not include a release date")
const files: DesktopFile[] = []
lines.forEach((line) => {
const value = line.trim()
if (value.startsWith("- url:")) {
const name = value.slice("- url:".length).trim()
files.push({
url: name.startsWith("http")
? name
: `https://github.com/${repo}/releases/download/v${version}/${encodeURIComponent(name)}`,
sha512: "",
size: 0,
})
return
}
const current = files.at(-1)
if (!current) return
if (value.startsWith("sha512:")) current.sha512 = value.slice("sha512:".length).trim()
if (value.startsWith("size:")) current.size = Number(value.slice("size:".length).trim())
if (value.startsWith("blockMapSize:")) current.blockMapSize = Number(value.slice("blockMapSize:".length).trim())
})
if (!files.length || files.some((file) => !file.sha512 || !file.size)) {
throw new Error("Desktop metadata contained an incomplete file")
}
return { files, releaseDate }
}
function isRecord(input: unknown): input is Record<string, unknown> {
return typeof input === "object" && input !== null && !Array.isArray(input)
}