mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-11 12:10:01 -04:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 404701baac | |||
| 026e0c634f | |||
| c581b59b7f | |||
| 5cc7811ec2 | |||
| 5d32845b02 | |||
| e9f5842f29 | |||
| 7b9734593a |
@@ -194,11 +194,10 @@ async function formatTypescript(input: string) {
|
||||
|
||||
function renderRegistry(names: string[]) {
|
||||
return `import type { DatabaseMigration } from "./migration"
|
||||
${names.map((name, index) => `import m${index.toString().padStart(2, "0")} from "./migration/${name}"`).join("\n")}
|
||||
|
||||
export const migrations: DatabaseMigration.Migration[] = (
|
||||
await Promise.all([
|
||||
${names.map((name) => ` import("./migration/${name}"),`).join("\n")}
|
||||
])
|
||||
).map((module) => module.default)
|
||||
export const migrations = [
|
||||
${names.map((_, index) => ` m${index.toString().padStart(2, "0")},`).join("\n")}
|
||||
] satisfies DatabaseMigration.Migration[]
|
||||
`
|
||||
}
|
||||
|
||||
@@ -153,10 +153,12 @@ const scan = Effect.fn("ConfigPluginSource.scan")(function* (
|
||||
})
|
||||
})
|
||||
|
||||
const sourceDirectories = ["plugin", "plugins"] as const
|
||||
|
||||
function discoverDirectory(fs: FSUtil.Interface, directory: string) {
|
||||
return Effect.gen(function* () {
|
||||
const files = yield* fs
|
||||
.scan("{plugin,plugins}/*.{ts,js}", {
|
||||
.scan(`{${sourceDirectories.join(",")}}/*.{ts,js}`, {
|
||||
cwd: directory,
|
||||
absolute: true,
|
||||
include: "file",
|
||||
@@ -168,8 +170,6 @@ function discoverDirectory(fs: FSUtil.Interface, directory: string) {
|
||||
})
|
||||
}
|
||||
|
||||
const sourceDirectories = ["plugin", "plugins"] as const
|
||||
|
||||
function isPluginSource(entries: readonly Entry[], file: string) {
|
||||
return entries.some(
|
||||
(entry) =>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
export * as Credential from "./credential"
|
||||
|
||||
import { asc, eq, sql } from "drizzle-orm"
|
||||
import { asc, eq } from "drizzle-orm"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Credential } from "@opencode-ai/schema/credential"
|
||||
import { Integration } from "@opencode-ai/schema/integration"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Database } from "./database/database"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { CredentialTable } from "./credential/sql"
|
||||
@@ -51,47 +48,6 @@ export interface Interface {
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Credential") {}
|
||||
|
||||
export const importFromDatabase = Effect.fn("Credential.importFromDatabase")(function* (input: {
|
||||
readonly path: string
|
||||
}) {
|
||||
const database = yield* Database.Service
|
||||
const filename = path.isAbsolute(input.path) ? input.path : path.join(Global.Path.data, input.path)
|
||||
yield* Effect.promise(() => fs.access(filename))
|
||||
yield* database.db.run(sql`ATTACH DATABASE ${filename} AS credential_snapshot`).pipe(Effect.orDie)
|
||||
yield* Effect.gen(function* () {
|
||||
yield* database.db.run(sql`DELETE FROM credential`)
|
||||
// Snapshot OAuth credentials are access-token-only so the embedded host never rotates the user's refresh token.
|
||||
yield* database.db.run(sql`
|
||||
INSERT INTO credential (
|
||||
id,
|
||||
integration_id,
|
||||
label,
|
||||
value,
|
||||
connector_id,
|
||||
method_id,
|
||||
active,
|
||||
time_created,
|
||||
time_updated
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
integration_id,
|
||||
label,
|
||||
CASE
|
||||
WHEN json_extract(value, '$.type') = 'oauth'
|
||||
THEN json_set(value, '$.refresh', '', '$.expires', 8640000000000000)
|
||||
ELSE value
|
||||
END,
|
||||
connector_id,
|
||||
method_id,
|
||||
active,
|
||||
time_created,
|
||||
time_updated
|
||||
FROM credential_snapshot.credential
|
||||
`)
|
||||
}).pipe(Effect.orDie, Effect.ensuring(database.db.run(sql`DETACH DATABASE credential_snapshot`).pipe(Effect.orDie)))
|
||||
})
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -42,10 +42,10 @@ const databaseLayer = Layer.effect(
|
||||
export function layer(options: Options = { path: ":memory:" }) {
|
||||
return Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const global = yield* Global.Service
|
||||
const provide = (filename: string) => databaseLayer.pipe(Layer.provide(sqliteLayer({ filename })))
|
||||
const filename = options.path ?? ":memory:"
|
||||
if (filename === ":memory:" || isAbsolute(filename)) return provide(filename)
|
||||
const global = yield* Global.Service
|
||||
return provide(join(global.data, filename))
|
||||
}),
|
||||
)
|
||||
|
||||
+84
-45
@@ -1,47 +1,86 @@
|
||||
import type { DatabaseMigration } from "./migration"
|
||||
import m00 from "./migration/20260127222353_familiar_lady_ursula"
|
||||
import m01 from "./migration/20260211171708_add_project_commands"
|
||||
import m02 from "./migration/20260213144116_wakeful_the_professor"
|
||||
import m03 from "./migration/20260225215848_workspace"
|
||||
import m04 from "./migration/20260227213759_add_session_workspace_id"
|
||||
import m05 from "./migration/20260228203230_blue_harpoon"
|
||||
import m06 from "./migration/20260303231226_add_workspace_fields"
|
||||
import m07 from "./migration/20260309230000_move_org_to_state"
|
||||
import m08 from "./migration/20260312043431_session_message_cursor"
|
||||
import m09 from "./migration/20260323234822_events"
|
||||
import m10 from "./migration/20260410174513_workspace-name"
|
||||
import m11 from "./migration/20260413175956_chief_energizer"
|
||||
import m12 from "./migration/20260423070820_add_icon_url_override"
|
||||
import m13 from "./migration/20260427172553_slow_nightmare"
|
||||
import m14 from "./migration/20260428004200_add_session_path"
|
||||
import m15 from "./migration/20260501142318_next_venus"
|
||||
import m16 from "./migration/20260504145000_add_sync_owner"
|
||||
import m17 from "./migration/20260507164347_add_workspace_time"
|
||||
import m18 from "./migration/20260510033149_session_usage"
|
||||
import m19 from "./migration/20260511000411_data_migration_state"
|
||||
import m20 from "./migration/20260511173437_session-metadata"
|
||||
import m21 from "./migration/20260601010001_normalize_storage_paths"
|
||||
import m22 from "./migration/20260601202201_amazing_prowler"
|
||||
import m23 from "./migration/20260602002951_lowly_union_jack"
|
||||
import m24 from "./migration/20260602182828_add_project_directories"
|
||||
import m25 from "./migration/20260603001617_session_message_projection_indexes"
|
||||
import m26 from "./migration/20260603040000_session_message_projection_order"
|
||||
import m27 from "./migration/20260603141458_session_input_inbox"
|
||||
import m28 from "./migration/20260603160727_jittery_ezekiel_stane"
|
||||
import m29 from "./migration/20260604172448_event_sourced_session_input"
|
||||
import m30 from "./migration/20260605003541_add_session_context_snapshot"
|
||||
import m31 from "./migration/20260605042240_add_context_epoch_agent"
|
||||
import m32 from "./migration/20260611035744_credential"
|
||||
import m33 from "./migration/20260611192811_lush_chimera"
|
||||
import m34 from "./migration/20260612174303_project_dir_strategy"
|
||||
import m35 from "./migration/20260622142730_simplify_session_context_epoch"
|
||||
import m36 from "./migration/20260622170816_reset_v2_session_state"
|
||||
import m37 from "./migration/20260622202450_simplify_session_input"
|
||||
import m38 from "./migration/20260804233008_loose_psylocke"
|
||||
import m39 from "./migration/20260805200742_import_legacy_credentials"
|
||||
import m40 from "./migration/20260808023530_workspace_domain"
|
||||
|
||||
export const migrations: DatabaseMigration.Migration[] = (
|
||||
await Promise.all([
|
||||
import("./migration/20260127222353_familiar_lady_ursula"),
|
||||
import("./migration/20260211171708_add_project_commands"),
|
||||
import("./migration/20260213144116_wakeful_the_professor"),
|
||||
import("./migration/20260225215848_workspace"),
|
||||
import("./migration/20260227213759_add_session_workspace_id"),
|
||||
import("./migration/20260228203230_blue_harpoon"),
|
||||
import("./migration/20260303231226_add_workspace_fields"),
|
||||
import("./migration/20260309230000_move_org_to_state"),
|
||||
import("./migration/20260312043431_session_message_cursor"),
|
||||
import("./migration/20260323234822_events"),
|
||||
import("./migration/20260410174513_workspace-name"),
|
||||
import("./migration/20260413175956_chief_energizer"),
|
||||
import("./migration/20260423070820_add_icon_url_override"),
|
||||
import("./migration/20260427172553_slow_nightmare"),
|
||||
import("./migration/20260428004200_add_session_path"),
|
||||
import("./migration/20260501142318_next_venus"),
|
||||
import("./migration/20260504145000_add_sync_owner"),
|
||||
import("./migration/20260507164347_add_workspace_time"),
|
||||
import("./migration/20260510033149_session_usage"),
|
||||
import("./migration/20260511000411_data_migration_state"),
|
||||
import("./migration/20260511173437_session-metadata"),
|
||||
import("./migration/20260601010001_normalize_storage_paths"),
|
||||
import("./migration/20260601202201_amazing_prowler"),
|
||||
import("./migration/20260602002951_lowly_union_jack"),
|
||||
import("./migration/20260602182828_add_project_directories"),
|
||||
import("./migration/20260603001617_session_message_projection_indexes"),
|
||||
import("./migration/20260603040000_session_message_projection_order"),
|
||||
import("./migration/20260603141458_session_input_inbox"),
|
||||
import("./migration/20260603160727_jittery_ezekiel_stane"),
|
||||
import("./migration/20260604172448_event_sourced_session_input"),
|
||||
import("./migration/20260605003541_add_session_context_snapshot"),
|
||||
import("./migration/20260605042240_add_context_epoch_agent"),
|
||||
import("./migration/20260611035744_credential"),
|
||||
import("./migration/20260611192811_lush_chimera"),
|
||||
import("./migration/20260612174303_project_dir_strategy"),
|
||||
import("./migration/20260622142730_simplify_session_context_epoch"),
|
||||
import("./migration/20260622170816_reset_v2_session_state"),
|
||||
import("./migration/20260622202450_simplify_session_input"),
|
||||
import("./migration/20260804233008_loose_psylocke"),
|
||||
import("./migration/20260805200742_import_legacy_credentials"),
|
||||
import("./migration/20260808023530_workspace_domain"),
|
||||
])
|
||||
).map((module) => module.default)
|
||||
export const migrations = [
|
||||
m00,
|
||||
m01,
|
||||
m02,
|
||||
m03,
|
||||
m04,
|
||||
m05,
|
||||
m06,
|
||||
m07,
|
||||
m08,
|
||||
m09,
|
||||
m10,
|
||||
m11,
|
||||
m12,
|
||||
m13,
|
||||
m14,
|
||||
m15,
|
||||
m16,
|
||||
m17,
|
||||
m18,
|
||||
m19,
|
||||
m20,
|
||||
m21,
|
||||
m22,
|
||||
m23,
|
||||
m24,
|
||||
m25,
|
||||
m26,
|
||||
m27,
|
||||
m28,
|
||||
m29,
|
||||
m30,
|
||||
m31,
|
||||
m32,
|
||||
m33,
|
||||
m34,
|
||||
m35,
|
||||
m36,
|
||||
m37,
|
||||
m38,
|
||||
m39,
|
||||
m40,
|
||||
] satisfies DatabaseMigration.Migration[]
|
||||
|
||||
@@ -65,15 +65,15 @@ const userAttachmentContent = (files: readonly FileAttachment[]) => {
|
||||
)
|
||||
if (eligible.length < 2) return files.flatMap(attachmentContent)
|
||||
|
||||
const seen = new Map<string, string[]>()
|
||||
const seen = new Map<string, Set<string>>()
|
||||
return files.flatMap((file) => {
|
||||
if (!imageMimes.has(file.mime) || file.source.type !== "inline" || !file.mention?.text)
|
||||
return attachmentContent(file)
|
||||
const metadata = JSON.stringify([file.mime, file.name ?? null, file.description ?? null, file.mention.text])
|
||||
const matches = seen.get(metadata)
|
||||
if (matches?.includes(file.data)) return []
|
||||
if (matches) matches.push(file.data)
|
||||
if (!matches) seen.set(metadata, [file.data])
|
||||
const payloads = seen.get(metadata) ?? new Set<string>()
|
||||
if (payloads.has(file.data)) return []
|
||||
payloads.add(file.data)
|
||||
seen.set(metadata, payloads)
|
||||
return attachmentContent(file)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Integration } from "@opencode-ai/core/integration"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import path from "path"
|
||||
|
||||
const it = testEffect(Layer.empty)
|
||||
const credentialLayer = LayerNode.compile(Credential.node)
|
||||
const it = testEffect(LayerNode.compile(Credential.node))
|
||||
|
||||
describe("Credential", () => {
|
||||
it.effect("stores, updates, lists, and removes credentials", () =>
|
||||
@@ -35,56 +31,6 @@ describe("Credential", () => {
|
||||
|
||||
yield* credentials.remove(replacement.id)
|
||||
expect(yield* credentials.list(integrationID)).toEqual([])
|
||||
}).pipe(Effect.provide(credentialLayer)),
|
||||
)
|
||||
|
||||
it.effect("imports a read-only credential snapshot", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir("opencode-credential-snapshot-")),
|
||||
(directory) => Effect.promise(() => directory[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((directory) => {
|
||||
const filename = path.join(directory.path, "source.sqlite")
|
||||
const source = LayerNode.compile(Credential.node, [[Database.node, Database.configured({ path: filename })]])
|
||||
const keyIntegration = Integration.ID.make("openai")
|
||||
const oauthIntegration = Integration.ID.make("github-copilot")
|
||||
return Effect.gen(function* () {
|
||||
yield* Effect.gen(function* () {
|
||||
const credentials = yield* Credential.Service
|
||||
yield* credentials.create({
|
||||
integrationID: keyIntegration,
|
||||
value: Credential.Key.make({ type: "key", key: "secret" }),
|
||||
})
|
||||
yield* credentials.create({
|
||||
integrationID: oauthIntegration,
|
||||
value: Credential.OAuth.make({
|
||||
type: "oauth",
|
||||
methodID: Integration.MethodID.make("oauth"),
|
||||
access: "access",
|
||||
refresh: "refresh",
|
||||
expires: 1,
|
||||
}),
|
||||
})
|
||||
}).pipe(Effect.provide(source), Effect.scoped)
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
yield* Credential.importFromDatabase({ path: filename })
|
||||
const credentials = yield* Credential.Service
|
||||
expect((yield* credentials.list(keyIntegration))[0]?.value).toEqual(
|
||||
Credential.Key.make({ type: "key", key: "secret" }),
|
||||
)
|
||||
expect((yield* credentials.list(oauthIntegration))[0]?.value).toEqual(
|
||||
Credential.OAuth.make({
|
||||
type: "oauth",
|
||||
methodID: Integration.MethodID.make("oauth"),
|
||||
access: "access",
|
||||
refresh: "",
|
||||
expires: 8640000000000000,
|
||||
}),
|
||||
)
|
||||
}).pipe(Effect.provide(LayerNode.compile(LayerNode.group([Credential.node, Database.node]))), Effect.scoped)
|
||||
})
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -77,20 +77,21 @@ export function spatialPathSpans(points: readonly DiagramPoint[]): SpatialSpan[]
|
||||
.sort(([left], [right]) => left - right)
|
||||
.flatMap(([y, xs]) => {
|
||||
const sorted = [...xs].sort((left, right) => left - right)
|
||||
const [first, ...rest] = sorted
|
||||
if (first === undefined) return []
|
||||
const spans: SpatialSpan[] = []
|
||||
let start = sorted[0]
|
||||
let end = start
|
||||
if (start === undefined) return spans
|
||||
for (const x of sorted.slice(1)) {
|
||||
if (x === end! + 1) {
|
||||
let start = first
|
||||
let end = first
|
||||
for (const x of rest) {
|
||||
if (x === end + 1) {
|
||||
end = x
|
||||
continue
|
||||
}
|
||||
spans.push(normalizedSpan(y, start, end!))
|
||||
spans.push(normalizedSpan(y, start, end))
|
||||
start = x
|
||||
end = x
|
||||
}
|
||||
spans.push(normalizedSpan(y, start, end!))
|
||||
spans.push(normalizedSpan(y, start, end))
|
||||
return spans
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,22 +13,6 @@ const session = yield * opencode.sessions.get({ sessionID })
|
||||
|
||||
It also exports `Tool` for plugins that add tools with `ctx.tool.transform(...)`. Embedded plugins run through the ordinary discovery flow and register tools into each Location's `ToolRegistry` through the normal `Tools.Service.register(...)` path. Closing the owning Effect Scope releases router resources, location services, fibers, and scoped tool registrations.
|
||||
|
||||
Use credentials already stored by the local OpenCode installation while keeping
|
||||
SDK sessions in memory:
|
||||
|
||||
```ts
|
||||
const opencode =
|
||||
yield *
|
||||
OpenCode.create({
|
||||
credentials: OpenCode.Credentials.fromLocalDatabase(),
|
||||
})
|
||||
```
|
||||
|
||||
The SDK copies credentials from `OPENCODE_DB` or `opencode.db` in OpenCode's
|
||||
data directory. The source database is never updated.
|
||||
OAuth credentials are copied without a usable refresh token; if the access
|
||||
token is rejected, re-authenticate with OpenCode and create a new SDK host.
|
||||
|
||||
`sessions.events({ sessionID, after })` replays durable events after the optional aggregate sequence, then emits newly committed durable events. `sessions.interrupt(...)` targets execution owned by this host, and `sessions.message(...)` retrieves one projected Session message.
|
||||
|
||||
The same constructor is available as a service Layer:
|
||||
|
||||
@@ -1,27 +1,11 @@
|
||||
import { OpenCode } from "@opencode-ai/client/effect"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
||||
import type { ServerOptions } from "@opencode-ai/server/options"
|
||||
import { Context, Effect, Layer, ManagedRuntime } from "effect"
|
||||
import { FetchHttpClient, HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
|
||||
export type CredentialSource = {
|
||||
readonly path: string
|
||||
}
|
||||
|
||||
export const Credentials = {
|
||||
fromLocalDatabase: (input: Partial<CredentialSource> = {}): CredentialSource => ({
|
||||
path: input.path ?? process.env.OPENCODE_DB ?? "opencode.db",
|
||||
}),
|
||||
}
|
||||
|
||||
export type CreateOptions = ServerOptions & {
|
||||
readonly credentials?: CredentialSource
|
||||
}
|
||||
|
||||
export const create = Effect.fn("OpenCode.create")(function* (options: CreateOptions = {}) {
|
||||
export const create = Effect.fn("OpenCode.create")(function* (options: ServerOptions = {}) {
|
||||
const runtime = yield* Effect.acquireRelease(
|
||||
Effect.sync(() =>
|
||||
ManagedRuntime.make(
|
||||
@@ -35,10 +19,6 @@ export const create = Effect.fn("OpenCode.create")(function* (options: CreateOpt
|
||||
(runtime) => runtime.disposeEffect,
|
||||
)
|
||||
const context = yield* runtime.contextEffect
|
||||
if (options.credentials) {
|
||||
const database = Context.get(context, Database.Service)
|
||||
yield* Credential.importFromDatabase(options.credentials).pipe(Effect.provideService(Database.Service, database))
|
||||
}
|
||||
const plugins = Context.get(context, SdkPlugins.Service)
|
||||
const router = Context.get(context, HttpRouter.HttpRouter)
|
||||
const handler = HttpEffect.toWebHandler(router.asHttpEffect())
|
||||
|
||||
@@ -2,9 +2,6 @@ import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { expect } from "bun:test"
|
||||
import { Deferred, Effect, Latch, Layer, Option, Ref, Schema, Stream } from "effect"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { LayerNode } from "../../util/src/effect/layer-node"
|
||||
import { testEffect } from "../../core/test/lib/effect"
|
||||
import { tmpdir } from "../../core/test/fixture/tmpdir"
|
||||
import type { OpenCodeEvent } from "../src"
|
||||
@@ -28,30 +25,6 @@ const sessionID = (fixture: Fixture) => fixture.sdk.Session.ID.create()
|
||||
const location = (fixture: Fixture) =>
|
||||
fixture.sdk.Location.Ref.make({ directory: fixture.sdk.AbsolutePath.make(fixture.directory) })
|
||||
|
||||
it.live("starts with credentials copied from the local database", () =>
|
||||
withEmbedded("opencode-embedded-credentials-", (fixture) =>
|
||||
Effect.gen(function* () {
|
||||
const filename = path.join(fixture.directory, "credentials.sqlite")
|
||||
const integrationID = fixture.sdk.Integration.ID.make("snapshot-test")
|
||||
yield* Effect.gen(function* () {
|
||||
const credentials = yield* Credential.Service
|
||||
yield* credentials.create({
|
||||
integrationID,
|
||||
label: "Local",
|
||||
value: Credential.Key.make({ type: "key", key: "secret" }),
|
||||
})
|
||||
}).pipe(
|
||||
Effect.provide(LayerNode.compile(Credential.node, [[Database.node, Database.configured({ path: filename })]])),
|
||||
Effect.scoped,
|
||||
)
|
||||
const opencode = yield* fixture.sdk.OpenCode.create({
|
||||
credentials: fixture.sdk.OpenCode.Credentials.fromLocalDatabase({ path: filename }),
|
||||
})
|
||||
expect((yield* opencode.health.get()).healthy).toBe(true)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("exposes app metadata to plugins", () =>
|
||||
withEmbedded("opencode-embedded-app-", (fixture) =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -62,13 +62,9 @@ function createMarquee(hovered: () => string | undefined, animations: () => bool
|
||||
const leading = createAnimatable({ opacity: 0 }, { enabled: animations, transition: tween({ duration: 0.25 }) })
|
||||
|
||||
createEffect(() => {
|
||||
if (!hovered()) {
|
||||
setOffset(0)
|
||||
leading.jump({ opacity: 0 })
|
||||
return
|
||||
}
|
||||
setOffset(0)
|
||||
leading.jump({ opacity: 0 })
|
||||
if (!hovered()) return
|
||||
let interval: ReturnType<typeof setInterval> | undefined
|
||||
const delay = setTimeout(() => {
|
||||
setOffset(1)
|
||||
|
||||
@@ -25,14 +25,14 @@ function deduplicateByIdentity<T>(
|
||||
items: readonly T[],
|
||||
identity: (item: T) => { metadata: string; payload: string } | undefined,
|
||||
) {
|
||||
const seen = new Map<string, string[]>()
|
||||
const seen = new Map<string, Set<string>>()
|
||||
return items.filter((item) => {
|
||||
const key = identity(item)
|
||||
if (!key) return true
|
||||
const matches = seen.get(key.metadata)
|
||||
if (matches?.includes(key.payload)) return false
|
||||
if (matches) matches.push(key.payload)
|
||||
if (!matches) seen.set(key.metadata, [key.payload])
|
||||
const payloads = seen.get(key.metadata) ?? new Set<string>()
|
||||
if (payloads.has(key.payload)) return false
|
||||
payloads.add(key.payload)
|
||||
seen.set(key.metadata, payloads)
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export function deduplicatePromptImages(files: readonly PromptFile[] | undefined
|
||||
return deduplicateByIdentity(files, (file) =>
|
||||
file.uri.startsWith("data:image/") && file.mention?.text
|
||||
? {
|
||||
metadata: JSON.stringify([attachmentMetadata(file), file.mention.text]),
|
||||
metadata: JSON.stringify([file.name ?? null, file.description ?? null, file.mention.text]),
|
||||
payload: file.uri,
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@@ -7,13 +7,15 @@ import Config from "@npmcli/config"
|
||||
import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js"
|
||||
import { Effect } from "effect"
|
||||
|
||||
const npmPath = fileURLToPath(new URL("..", import.meta.url))
|
||||
// Lazy: on workerd import.meta.url is undefined and constructing a URL from it
|
||||
// at module scope fails startup validation; npm config is never used there.
|
||||
const npmPath = () => fileURLToPath(new URL("..", import.meta.url))
|
||||
|
||||
export const load = (dir: string) =>
|
||||
Effect.tryPromise({
|
||||
try: async () => {
|
||||
const config = new Config({
|
||||
npmPath,
|
||||
npmPath: npmPath(),
|
||||
cwd: dir,
|
||||
env: { ...process.env },
|
||||
argv: [process.execPath, process.execPath, "--prefix", dir],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as Observability from "./observability.js"
|
||||
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
|
||||
import { LayerNode } from "./effect/layer-node.js"
|
||||
import { Effect, Layer, Logger, References, Schema } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
@@ -50,4 +50,10 @@ export function layer(
|
||||
).pipe(Layer.catchCause(() => local))
|
||||
}
|
||||
|
||||
export const node = LayerNode.make({ name: "observability", layer: layer(), deps: [] })
|
||||
// Layer.suspend: constructing the loggers eagerly at module scope performs
|
||||
// I/O (file logger, run id) that workerd forbids in global scope.
|
||||
export const node = LayerNode.make({
|
||||
name: "observability",
|
||||
layer: Layer.suspend(() => layer()),
|
||||
deps: [],
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "path"
|
||||
import { Global } from "../global.js"
|
||||
import { runID } from "./shared.js"
|
||||
|
||||
function formatter(id: string = runID) {
|
||||
function formatter(id: string = runID()) {
|
||||
return Logger.map(Logger.formatStructured, (output) => {
|
||||
const messages = Array.isArray(output.message) ? output.message : [output.message]
|
||||
return [
|
||||
@@ -51,7 +51,7 @@ export function file(local = true, channel = "local") {
|
||||
return path.join(Global.Path.log, `opencode-${channel.replace(/[^a-zA-Z0-9._-]/g, "-")}.log`)
|
||||
}
|
||||
|
||||
export function fileLogger(target = file(), id: string = runID) {
|
||||
export function fileLogger(target = file(), id: string = runID()) {
|
||||
// Do not set batchWindow to 0; it causes high idle CPU usage.
|
||||
return Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
|
||||
@@ -54,8 +54,8 @@ export function resource(app: App = { client: "opencode", version: "unknown", ch
|
||||
...resourceAttributes(),
|
||||
"deployment.environment.name": app.channel,
|
||||
"opencode.client": app.client,
|
||||
"opencode.run": runID,
|
||||
"service.instance.id": runID,
|
||||
"opencode.run": runID(),
|
||||
"service.instance.id": runID(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
export const runID = crypto.randomUUID().slice(0, 8)
|
||||
// Lazy: workerd forbids generating random values in global scope, so the id
|
||||
// materializes on first call (inside a handler) and stays stable afterwards.
|
||||
let generated: string | undefined
|
||||
|
||||
export function runID(): string {
|
||||
generated ??= crypto.randomUUID().slice(0, 8)
|
||||
return generated
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user