From faca2b90c1d9ace0cc2741cba3713d77ef59df56 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 15 May 2026 03:24:14 +0530 Subject: [PATCH] refactor(flags): migrate icon discovery runtime flag (#27609) --- packages/core/src/flag/flag.ts | 1 - packages/opencode/src/effect/runtime-flags.ts | 1 + packages/opencode/src/project/project.ts | 7 +++- .../test/effect/runtime-flags.test.ts | 29 +++++++++++++ .../opencode/test/project/project.test.ts | 42 +++++++++++++++++++ 5 files changed, 77 insertions(+), 3 deletions(-) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index fd01b09b2b..2fad5e15e7 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -52,7 +52,6 @@ export const Flag = { OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER").pipe( Config.withDefault(false), ), - OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY"), OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT: copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"), OPENCODE_ENABLE_EXA: truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA"), diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts index 88ad4dabbd..5c640f9eb3 100644 --- a/packages/opencode/src/effect/runtime-flags.ts +++ b/packages/opencode/src/effect/runtime-flags.ts @@ -36,6 +36,7 @@ export class Service extends ConfigService.Service()("@opencode/Runtime experimentalPlanMode: enabledByExperimental("OPENCODE_EXPERIMENTAL_PLAN_MODE"), experimentalEventSystem: enabledByExperimental("OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"), experimentalWorkspaces: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"), + experimentalIconDiscovery: enabledByExperimental("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY"), bashDefaultTimeoutMs: positiveInteger("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"), client: Config.string("OPENCODE_CLIENT").pipe(Config.withDefault("cli")), }) {} diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 643685539d..399412cdce 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -19,6 +19,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema" import { serviceUse } from "@/effect/service-use" +import { RuntimeFlags } from "@/effect/runtime-flags" const log = Log.create({ service: "project" }) @@ -130,7 +131,7 @@ type GitResult = { code: number; text: string; stderr: string } export const layer: Layer.Layer< Service, never, - AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Bus.Service + AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Bus.Service | RuntimeFlags.Service > = Layer.effect( Service, Effect.gen(function* () { @@ -138,6 +139,7 @@ export const layer: Layer.Layer< const pathSvc = yield* Path.Path const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const bus = yield* Bus.Service + const flags = yield* RuntimeFlags.Service const git = Effect.fnUntraced( function* (args: string[], opts?: { cwd?: string }) { @@ -282,7 +284,7 @@ export const layer: Layer.Layer< time: { created: Date.now(), updated: Date.now() }, } - if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) yield* discover(existing).pipe(Effect.ignore, Effect.forkIn(scope)) + if (flags.experimentalIconDiscovery) yield* discover(existing).pipe(Effect.ignore, Effect.forkIn(scope)) const result: Info = { ...existing, @@ -505,6 +507,7 @@ export const defaultLayer = layer.pipe( Layer.provide(CrossSpawnSpawner.defaultLayer), Layer.provide(AppFileSystem.defaultLayer), Layer.provide(NodePath.layer), + Layer.provide(RuntimeFlags.defaultLayer), ) export const use = serviceUse(Service) diff --git a/packages/opencode/test/effect/runtime-flags.test.ts b/packages/opencode/test/effect/runtime-flags.test.ts index c7ca1061df..4e6c7f9fad 100644 --- a/packages/opencode/test/effect/runtime-flags.test.ts +++ b/packages/opencode/test/effect/runtime-flags.test.ts @@ -39,6 +39,7 @@ describe("RuntimeFlags", () => { expect(flags.experimentalPlanMode).toBe(true) expect(flags.experimentalEventSystem).toBe(true) expect(flags.experimentalWorkspaces).toBe(true) + expect(flags.experimentalIconDiscovery).toBe(true) expect(flags.client).toBe("desktop") }), ) @@ -53,6 +54,7 @@ describe("RuntimeFlags", () => { expect(flags.disableDefaultPlugins).toBe(true) expect(flags.disableClaudeCodeSkills).toBe(false) expect(flags.enableExa).toBe(false) + expect(flags.experimentalIconDiscovery).toBe(false) expect(flags.experimentalOxfmt).toBe(false) expect(flags.bashDefaultTimeoutMs).toBe(1_000) expect(flags.enableExperimentalModels).toBe(false) @@ -60,6 +62,32 @@ describe("RuntimeFlags", () => { }), ) + it.effect("experimentalIconDiscovery defaults to false", () => + Effect.gen(function* () { + const flags = yield* readFlags.pipe(Effect.provide(fromConfig({}))) + + expect(flags.experimentalIconDiscovery).toBe(false) + }), + ) + + it.effect("experimentalIconDiscovery reads OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", () => + Effect.gen(function* () { + const flags = yield* readFlags.pipe( + Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true" })), + ) + + expect(flags.experimentalIconDiscovery).toBe(true) + }), + ) + + it.effect("experimentalIconDiscovery inherits OPENCODE_EXPERIMENTAL", () => + Effect.gen(function* () { + const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL: "true" }))) + + expect(flags.experimentalIconDiscovery).toBe(true) + }), + ) + it.effect("experimentalOxfmt defaults to false", () => Effect.gen(function* () { const flags = yield* readFlags.pipe(Effect.provide(fromConfig({}))) @@ -147,6 +175,7 @@ describe("RuntimeFlags", () => { expect(flags.disableDefaultPlugins).toBe(false) expect(flags.disableClaudeCodeSkills).toBe(false) expect(flags.enableExa).toBe(false) + expect(flags.experimentalIconDiscovery).toBe(false) expect(flags.experimentalOxfmt).toBe(false) expect(flags.bashDefaultTimeoutMs).toBeUndefined() expect(flags.client).toBe("cli") diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index c9257d6575..88fc71718f 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -13,6 +13,7 @@ import { NodePath } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { testEffect } from "../lib/effect" +import { RuntimeFlags } from "@/effect/runtime-flags" void Log.init({ print: false }) @@ -69,12 +70,39 @@ function projectLayerWithFailure(failArg: string) { Layer.provide(Bus.defaultLayer), Layer.provide(AppFileSystem.defaultLayer), Layer.provide(NodePath.layer), + Layer.provide(RuntimeFlags.defaultLayer), + ) +} + +function projectLayerWithRuntimeFlags(flags: Parameters[0]) { + return Project.layer.pipe( + Layer.provide(Bus.defaultLayer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(NodePath.layer), + Layer.provide(RuntimeFlags.layer(flags)), ) } const failureIt = (failArg: string) => testEffect(Layer.mergeAll(projectLayerWithFailure(failArg), CrossSpawnSpawner.defaultLayer)) +const iconDiscoveryIt = testEffect( + Layer.provideMerge( + projectLayerWithRuntimeFlags({ experimentalIconDiscovery: true }), + CrossSpawnSpawner.defaultLayer, + ), +) + +function waitForProjectIcon(id: ProjectID, attempts = 50): Effect.Effect { + return Effect.gen(function* () { + const project = Project.get(id) + if (project?.icon?.url) return project + if (attempts <= 0) throw new Error(`Project icon was not discovered: ${id}`) + yield* Effect.sleep("10 millis") + return yield* waitForProjectIcon(id, attempts - 1) + }) +} + describe("Project.fromDirectory", () => { it.live("should handle git repository with no commits", () => Effect.gen(function* () { @@ -284,6 +312,20 @@ describe("Project.fromDirectory with worktrees", () => { }) describe("Project.discover", () => { + iconDiscoveryIt.live("discovers favicon from fromDirectory when enabled", () => + Effect.gen(function* () { + const tmp = yield* tmpdirScoped({ git: true }) + const pngData = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) + yield* Effect.promise(() => Bun.write(path.join(tmp, "favicon.png"), pngData)) + + const { project } = yield* run((svc) => svc.fromDirectory(tmp)) + const updated = yield* waitForProjectIcon(project.id) + + expect(updated.icon?.url).toStartWith("data:") + expect(updated.icon?.url).toContain("base64") + }), + ) + it.live("should discover favicon.png in root", () => Effect.gen(function* () { const tmp = yield* tmpdirScoped({ git: true })