mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-02 06:56:21 +00:00
Compare commits
4
Commits
acp-activation
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
519cd8c771 | ||
|
|
34e40cc4bc | ||
|
|
e0c7712f20 | ||
|
|
ddda404d99 |
@@ -417,7 +417,6 @@ jobs:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: github.ref_name == 'beta'
|
||||
with:
|
||||
name: opencode-preview-cli
|
||||
path: packages/cli/dist
|
||||
@@ -480,7 +479,7 @@ jobs:
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }}
|
||||
OPENCODE_CLI_DIST: ${{ (github.ref_name == 'beta' && format('{0}/packages/cli/dist', github.workspace)) || '' }}
|
||||
OPENCODE_CLI_DIST: ${{ github.workspace }}/packages/cli/dist
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
|
||||
@@ -87,6 +87,11 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
|
||||
cd packages/desktop
|
||||
|
||||
export OPENCODE_CLI_DIST="$TMPDIR/desktop-cli"
|
||||
cli_package=$(bun -e 'import { getCurrentCli } from "./scripts/utils.ts"; console.log(getCurrentCli().package.replace("@opencode-ai/", ""))')
|
||||
mkdir -p "$OPENCODE_CLI_DIST/$cli_package/bin"
|
||||
cp ${lib.getExe opencode} "$OPENCODE_CLI_DIST/$cli_package/bin/opencode2"
|
||||
|
||||
bun run build
|
||||
npx electron-builder --dir \
|
||||
--config electron-builder.config.ts \
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import {
|
||||
normalizeNewSessionWorktree,
|
||||
resolveNewSessionBranch,
|
||||
resolveNewSessionGit,
|
||||
resolveNewSessionWorktree,
|
||||
} from "./controller"
|
||||
import { resolveNewSessionBranch, resolveNewSessionGit, resolveNewSessionWorktree } from "./controller"
|
||||
|
||||
describe("new session workspace selection", () => {
|
||||
test("uses main when the workspace bar is unavailable", () => {
|
||||
@@ -12,8 +7,6 @@ describe("new session workspace selection", () => {
|
||||
resolveNewSessionWorktree({
|
||||
enabled: false,
|
||||
selected: "/project/feature",
|
||||
directory: "/project/feature",
|
||||
projectWorktree: "/project",
|
||||
}),
|
||||
).toBe("main")
|
||||
})
|
||||
@@ -22,31 +15,21 @@ describe("new session workspace selection", () => {
|
||||
expect(
|
||||
resolveNewSessionWorktree({
|
||||
enabled: true,
|
||||
directory: "/project/feature",
|
||||
projectWorktree: "/project",
|
||||
fallback: "create",
|
||||
}),
|
||||
).toBe("create")
|
||||
expect(
|
||||
resolveNewSessionWorktree({
|
||||
enabled: true,
|
||||
directory: "/project/feature",
|
||||
projectWorktree: "/project",
|
||||
fallback: "main",
|
||||
}),
|
||||
).toBe("/project")
|
||||
})
|
||||
|
||||
test("normalizes main to the project root outside the main worktree", () => {
|
||||
expect(normalizeNewSessionWorktree("main", "/project/feature", "/project")).toBe("/project")
|
||||
expect(normalizeNewSessionWorktree("main", "/project", "/project")).toBe("main")
|
||||
})
|
||||
|
||||
test("treats equivalent Windows roots as the main worktree", () => {
|
||||
expect(
|
||||
resolveNewSessionWorktree({ enabled: true, directory: "C:\\Repo\\", projectWorktree: "c:/repo" }),
|
||||
).toBe("main")
|
||||
expect(normalizeNewSessionWorktree("main", "C:\\Repo\\", "c:/repo")).toBe("main")
|
||||
})
|
||||
|
||||
test("keeps local selection when the cached project path is stale", () => {
|
||||
const input = { enabled: true, directory: "C:/Projects/repo", projectWorktree: "D:/Projects/repo" }
|
||||
expect(resolveNewSessionWorktree(input)).toBe("main")
|
||||
expect(resolveNewSessionWorktree({ ...input, selected: "/worktree" })).toBe("/worktree")
|
||||
})
|
||||
|
||||
test("resolves the branch from the active location", () => {
|
||||
|
||||
@@ -11,27 +11,15 @@ import { normalizeProjectInfo } from "@/runtime/server/global-sync/utils"
|
||||
import {
|
||||
isWorkspaceDirectory,
|
||||
isWorkspaceSelection,
|
||||
sameDirectory,
|
||||
workspaceDefaultSelection,
|
||||
workspaceDirectories,
|
||||
workspaceSelectionDestination,
|
||||
} from "@/workspaces/paths"
|
||||
|
||||
export function resolveNewSessionWorktree(input: {
|
||||
enabled: boolean
|
||||
selected?: string
|
||||
directory: string
|
||||
projectWorktree?: string
|
||||
fallback?: string
|
||||
}) {
|
||||
export function resolveNewSessionWorktree(input: { enabled: boolean; selected?: string; fallback?: string }) {
|
||||
if (!input.enabled) return "main"
|
||||
if (input.selected) return input.selected
|
||||
return normalizeNewSessionWorktree(input.fallback ?? "main", input.directory, input.projectWorktree)
|
||||
}
|
||||
|
||||
export function normalizeNewSessionWorktree(value: string, directory: string, projectWorktree?: string) {
|
||||
if (value === "main" && projectWorktree && !sameDirectory(directory, projectWorktree)) return projectWorktree
|
||||
return value
|
||||
return input.fallback ?? "main"
|
||||
}
|
||||
|
||||
export function resolveNewSessionBranch(input: {
|
||||
@@ -92,8 +80,6 @@ export function createNewSessionWorkspaceController(input: {
|
||||
resolveNewSessionWorktree({
|
||||
enabled: visible(),
|
||||
selected: selected(),
|
||||
directory: sdk().directory,
|
||||
projectWorktree: currentProject()?.worktree,
|
||||
fallback: fallback(),
|
||||
}),
|
||||
)
|
||||
@@ -145,7 +131,7 @@ export function createNewSessionWorkspaceController(input: {
|
||||
remember,
|
||||
set: (worktree: string) => {
|
||||
input.setSelectedBranch(undefined)
|
||||
input.setSelectedWorktree(normalizeNewSessionWorktree(worktree, sdk().directory, currentProject()?.worktree))
|
||||
input.setSelectedWorktree(worktree)
|
||||
remember(worktree)
|
||||
},
|
||||
create: (branch: string) => {
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
import { createMemo, type Accessor } from "solid-js"
|
||||
import { createMemo, createSignal, onCleanup, type Accessor } from "solid-js"
|
||||
import createPresence from "solid-presence"
|
||||
|
||||
export function createAnimatedPresence<T>(
|
||||
value: Accessor<T | undefined>,
|
||||
element: Accessor<HTMLElement | null>,
|
||||
identity?: Accessor<unknown>,
|
||||
minimumDuration = 0,
|
||||
) {
|
||||
const animation = createMemo<{ identity?: unknown; show: boolean; animate: boolean; value: T | undefined }>(
|
||||
(previous) => {
|
||||
const currentIdentity = identity?.()
|
||||
const current = value()
|
||||
const show = current !== undefined
|
||||
const same = !identity || previous?.identity === currentIdentity
|
||||
return {
|
||||
identity: currentIdentity,
|
||||
show,
|
||||
animate: previous !== undefined && same && (previous.animate || previous.show !== show),
|
||||
value: current ?? (same ? previous?.value : undefined),
|
||||
}
|
||||
},
|
||||
)
|
||||
const [tick, setTick] = createSignal(0)
|
||||
const animation = createMemo<{
|
||||
identity?: unknown
|
||||
show: boolean
|
||||
animate: boolean
|
||||
value: T | undefined
|
||||
started: number
|
||||
}>((previous) => {
|
||||
tick()
|
||||
const currentIdentity = identity?.()
|
||||
const current = value()
|
||||
const same = !identity || previous?.identity === currentIdentity
|
||||
const started = same && previous?.show ? previous.started : performance.now()
|
||||
const remaining =
|
||||
current === undefined && same && previous?.show ? minimumDuration - (performance.now() - started) : 0
|
||||
const show = current !== undefined || remaining > 0
|
||||
if (remaining > 0) {
|
||||
const timer = setTimeout(() => setTick((value) => value + 1), remaining)
|
||||
onCleanup(() => clearTimeout(timer))
|
||||
}
|
||||
return {
|
||||
identity: currentIdentity,
|
||||
show,
|
||||
started,
|
||||
animate: previous !== undefined && same && (previous.animate || previous.show !== show),
|
||||
value: current ?? (same ? previous?.value : undefined),
|
||||
}
|
||||
})
|
||||
const presence = createPresence({ show: () => animation().show, element })
|
||||
return {
|
||||
...presence,
|
||||
|
||||
@@ -539,7 +539,12 @@ function MessageTimelineView(
|
||||
.findLast((ref) => blocking.has(ref.partID))?.partID
|
||||
})
|
||||
const [backgroundHintRef, setBackgroundHintRef] = createSignal<HTMLDivElement>()
|
||||
const backgroundHintPresence = createAnimatedPresence(backgroundHintPartID, () => backgroundHintRef() ?? null)
|
||||
const backgroundHintPresence = createAnimatedPresence(
|
||||
backgroundHintPartID,
|
||||
() => backgroundHintRef() ?? null,
|
||||
sessionID,
|
||||
1000,
|
||||
)
|
||||
const showWorking = createMemo(() => {
|
||||
const id = sessionID()
|
||||
if (!id || sessionStatus().type !== "busy") return false
|
||||
|
||||
@@ -44,6 +44,8 @@ export class GrepInput extends Schema.Class<GrepInput>("FileSystem.GrepInput")({
|
||||
pattern: Schema.String,
|
||||
path: Schema.optionalKey(RelativePath),
|
||||
include: Schema.optionalKey(Schema.String),
|
||||
literal: Schema.optionalKey(Schema.Boolean),
|
||||
caseSensitive: Schema.optionalKey(Schema.Boolean),
|
||||
limit: Schema.optionalKey(PositiveInt),
|
||||
}) {}
|
||||
|
||||
|
||||
@@ -74,6 +74,8 @@ export interface GrepInput {
|
||||
readonly pattern: string
|
||||
readonly file?: string
|
||||
readonly include?: string
|
||||
readonly literal?: boolean
|
||||
readonly caseSensitive?: boolean
|
||||
readonly limit: number
|
||||
readonly signal?: AbortSignal
|
||||
}
|
||||
@@ -220,6 +222,8 @@ const layer = Layer.effect(
|
||||
"--json",
|
||||
"--hidden",
|
||||
"--no-messages",
|
||||
...(input.literal ? ["--fixed-strings"] : []),
|
||||
...(input.caseSensitive === false ? ["--ignore-case"] : []),
|
||||
...(input.include ? [`--glob=${input.include}`] : []),
|
||||
"--glob=!**/.git/**",
|
||||
"--",
|
||||
|
||||
@@ -18,7 +18,7 @@ export const Input = Schema.Struct({
|
||||
pattern: FileSystem.GrepInput.fields.pattern
|
||||
.check(Schema.isMinLength(1, { message: "Pattern must not be empty" }))
|
||||
.annotate({
|
||||
description: "Regular expression to search for in file contents (ripgrep syntax)",
|
||||
description: "Regular expression or literal text to match in file contents.",
|
||||
}),
|
||||
path: Schema.optionalKey(RelativePath).annotate({
|
||||
description: "File or directory to search. Defaults to the current working directory.",
|
||||
@@ -26,6 +26,12 @@ export const Input = Schema.Struct({
|
||||
include: FileSystem.GrepInput.fields.include.annotate({
|
||||
description: 'Glob pattern to filter files (for example, "*.js" or "*.{ts,tsx}")',
|
||||
}),
|
||||
literal: FileSystem.GrepInput.fields.literal.annotate({
|
||||
description: "Treat `pattern` as exact text instead of a regular expression (default: false).",
|
||||
}),
|
||||
caseSensitive: FileSystem.GrepInput.fields.caseSensitive.annotate({
|
||||
description: "Use case-sensitive matching (default: true).",
|
||||
}),
|
||||
limit: FileSystem.GrepInput.fields.limit.annotate({
|
||||
description: `Maximum number of matching lines to return (default: ${FileSystem.DEFAULT_SEARCH_LIMIT})`,
|
||||
}),
|
||||
@@ -70,7 +76,7 @@ export const Plugin = {
|
||||
name,
|
||||
options: { codemode: false },
|
||||
description:
|
||||
"Search file contents using regular expressions. Use it to locate specific code, symbols, or text patterns, and narrow searches with `path` or `include`. Returns matching file paths, line numbers, and line previews.",
|
||||
"Search file contents using ripgrep's regular expression syntax or literal text matching. Use it to locate specific code, symbols, or text patterns, and narrow searches with `path` or `include`. Returns matching file paths, line numbers, and line previews.",
|
||||
input: Input,
|
||||
output: Output,
|
||||
execute: (input, context) =>
|
||||
@@ -92,6 +98,8 @@ export const Plugin = {
|
||||
root: ".",
|
||||
path: input.path,
|
||||
include: input.include,
|
||||
literal: input.literal,
|
||||
caseSensitive: input.caseSensitive,
|
||||
limit: input.limit,
|
||||
},
|
||||
sessionID: context.sessionID,
|
||||
@@ -112,6 +120,8 @@ export const Plugin = {
|
||||
pattern: input.pattern,
|
||||
file: type === "file" ? path.basename(root) : undefined,
|
||||
include: input.include,
|
||||
literal: input.literal,
|
||||
caseSensitive: input.caseSensitive,
|
||||
limit: limit + 1,
|
||||
})
|
||||
.pipe(
|
||||
|
||||
@@ -17,3 +17,25 @@ bundle the assets as an application. The resulting app will be in `dist/`.
|
||||
```bash
|
||||
bun run build && bun run package
|
||||
```
|
||||
|
||||
Production builds require a prebuilt V2 CLI distribution. The release workflow supplies the artifact from the same run:
|
||||
|
||||
```bash
|
||||
OPENCODE_CHANNEL=prod OPENCODE_CLI_DIST=/absolute/path/to/packages/cli/dist bun run build
|
||||
OPENCODE_CHANNEL=prod bun run package
|
||||
```
|
||||
|
||||
Set `OPENCODE_CLI_TARGET` when packaging for a different architecture. The CLI is placed outside `app.asar` in the
|
||||
application's resources directory, and packaging fails if it is missing.
|
||||
|
||||
CLI preparation uses these channel rules:
|
||||
|
||||
| Channel | Without `OPENCODE_CLI_DIST` | With `OPENCODE_CLI_DIST` |
|
||||
| -------------------------------------- | ------------------------------ | --------------------------------------------- |
|
||||
| `dev`, `local`, unset, or unrecognized | Download the dev CLI | Download the dev CLI; ignore the distribution |
|
||||
| `beta` | Download the beta CLI | Copy the supplied CLI; fail if it is missing |
|
||||
| `prod`, `latest` | Fail before changing resources | Copy the supplied CLI; fail if it is missing |
|
||||
|
||||
`bun dev` is separate from packaging: it uses local renderer/server mode, the dev app identity, and the CLI source by
|
||||
default. `bun dev --download-server <version>` instead downloads that CLI version for local development. Neither path
|
||||
requires `OPENCODE_CLI_DIST` or runs the production prebuild.
|
||||
|
||||
@@ -190,19 +190,8 @@ for (const channel of ["dev", "beta"] as const) {
|
||||
{
|
||||
from: "resources/",
|
||||
to: "",
|
||||
filter: ["opencode-cli*"],
|
||||
filter: ["opencode-cli", "opencode-cli.exe"],
|
||||
},
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
test("does not bundle the CLI in prod builds", async () => {
|
||||
const previous = process.env.OPENCODE_CHANNEL
|
||||
process.env.OPENCODE_CHANNEL = "prod"
|
||||
const module = await import("./electron-builder.config.ts?no-cli-resource=prod")
|
||||
const config = module.default as Configuration
|
||||
if (previous === undefined) delete process.env.OPENCODE_CHANNEL
|
||||
else process.env.OPENCODE_CHANNEL = previous
|
||||
|
||||
expect(config.extraResources).toEqual([])
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { execFile } from "node:child_process"
|
||||
import { stat } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { promisify } from "node:util"
|
||||
@@ -45,6 +46,7 @@ export function macSignOptions(options: CustomMacSignOptions): CustomMacSignOpti
|
||||
const channel = (() => {
|
||||
const raw = process.env.OPENCODE_CHANNEL
|
||||
if (raw === "dev" || raw === "beta" || raw === "prod") return raw
|
||||
if (raw === "latest") return "prod"
|
||||
return "dev"
|
||||
})()
|
||||
|
||||
@@ -84,16 +86,21 @@ const getBase = (appId: string): Configuration => ({
|
||||
"!**/node_modules/js-yaml/dist/{js-yaml.js,js-yaml.min.js,*.map}",
|
||||
"!**/node_modules/js-yaml/bin{,/**/*}",
|
||||
],
|
||||
extraResources:
|
||||
channel !== "prod"
|
||||
? [
|
||||
{
|
||||
from: "resources/",
|
||||
to: "",
|
||||
filter: ["opencode-cli*"],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
extraResources: [
|
||||
{
|
||||
from: "resources/",
|
||||
to: "",
|
||||
filter: ["opencode-cli", "opencode-cli.exe"],
|
||||
},
|
||||
],
|
||||
afterPack: async (context) => {
|
||||
const cli = path.join(
|
||||
context.packager.getResourcesDir(context.appOutDir),
|
||||
context.electronPlatformName === "win32" ? "opencode-cli.exe" : "opencode-cli",
|
||||
)
|
||||
const file = await stat(cli)
|
||||
if (!file.isFile() || file.size === 0) throw new Error(`Bundled CLI must be a non-empty file: ${cli}`)
|
||||
},
|
||||
mac: {
|
||||
category: "public.app-category.developer-tools",
|
||||
icon: `resources/icons/icon.icns`,
|
||||
|
||||
@@ -34,7 +34,8 @@ export default defineConfig(({ command }) => ({
|
||||
dedupe: ["effect"],
|
||||
},
|
||||
define: {
|
||||
"import.meta.env.OPENCODE_CHANNEL": JSON.stringify(channel),
|
||||
// Local renderer/server mode still uses the dev application identity and updater policy.
|
||||
"import.meta.env.OPENCODE_CHANNEL": JSON.stringify(channel === "local" ? "dev" : channel),
|
||||
},
|
||||
build: {
|
||||
minify: command === "build",
|
||||
|
||||
@@ -4,9 +4,15 @@ import { $ } from "bun"
|
||||
import { copyBuiltCliToResources, downloadCliToResources, resolveChannel } from "./utils"
|
||||
|
||||
const channel = resolveChannel()
|
||||
if (channel === "prod" && !Bun.env.OPENCODE_CLI_DIST) {
|
||||
throw new Error("OPENCODE_CLI_DIST is required for production desktop builds")
|
||||
}
|
||||
|
||||
await $`bun ./scripts/copy-icons.ts ${channel}`
|
||||
await $`bun ./scripts/copy-metainfo.ts ${channel}`
|
||||
|
||||
if (channel === "dev") await downloadCliToResources()
|
||||
if (channel === "beta" && Bun.env.OPENCODE_CLI_DIST) await copyBuiltCliToResources(Bun.env.OPENCODE_CLI_DIST)
|
||||
if ((channel === "beta" || channel === "prod") && Bun.env.OPENCODE_CLI_DIST) {
|
||||
await copyBuiltCliToResources(Bun.env.OPENCODE_CLI_DIST)
|
||||
}
|
||||
if (channel === "beta" && !Bun.env.OPENCODE_CLI_DIST) await downloadCliToResources("beta")
|
||||
|
||||
@@ -10,6 +10,7 @@ export type Channel = "dev" | "beta" | "prod"
|
||||
export function resolveChannel(): Channel {
|
||||
const raw = Bun.env.OPENCODE_CHANNEL
|
||||
if (raw === "dev" || raw === "beta" || raw === "prod") return raw
|
||||
if (raw === "latest") return "prod"
|
||||
return "dev"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user