|
@@ -43,6 +43,7 @@ import { Shell } from "@/shell/shell"
|
|
|
import { ShellID } from "@/tool/shell/id"
|
|
import { ShellID } from "@/tool/shell/id"
|
|
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|
|
import { Truncate } from "@/tool/truncate"
|
|
import { Truncate } from "@/tool/truncate"
|
|
|
|
|
+import { Image } from "@/image/image"
|
|
|
import { decodeDataUrl } from "@/util/data-url"
|
|
import { decodeDataUrl } from "@/util/data-url"
|
|
|
import { Process } from "@/util/process"
|
|
import { Process } from "@/util/process"
|
|
|
import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema, Types } from "effect"
|
|
import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema, Types } from "effect"
|
|
@@ -80,10 +81,10 @@ const elog = EffectLogger.create({ service: "session.prompt" })
|
|
|
|
|
|
|
|
export interface Interface {
|
|
export interface Interface {
|
|
|
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
|
|
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
|
|
|
- readonly prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts>
|
|
|
|
|
|
|
+ readonly prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts, Image.Error>
|
|
|
readonly loop: (input: LoopInput) => Effect.Effect<MessageV2.WithParts>
|
|
readonly loop: (input: LoopInput) => Effect.Effect<MessageV2.WithParts>
|
|
|
readonly shell: (input: ShellInput) => Effect.Effect<MessageV2.WithParts>
|
|
readonly shell: (input: ShellInput) => Effect.Effect<MessageV2.WithParts>
|
|
|
- readonly command: (input: CommandInput) => Effect.Effect<MessageV2.WithParts>
|
|
|
|
|
|
|
+ readonly command: (input: CommandInput) => Effect.Effect<MessageV2.WithParts, Image.Error>
|
|
|
readonly resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
|
|
readonly resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -108,6 +109,7 @@ export const layer = Layer.effect(
|
|
|
const lsp = yield* LSP.Service
|
|
const lsp = yield* LSP.Service
|
|
|
const registry = yield* ToolRegistry.Service
|
|
const registry = yield* ToolRegistry.Service
|
|
|
const truncate = yield* Truncate.Service
|
|
const truncate = yield* Truncate.Service
|
|
|
|
|
+ const image = yield* Image.Service
|
|
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
|
const scope = yield* Scope.Scope
|
|
const scope = yield* Scope.Scope
|
|
|
const instruction = yield* Instruction.Service
|
|
const instruction = yield* Instruction.Service
|
|
@@ -123,7 +125,7 @@ export const layer = Layer.effect(
|
|
|
return {
|
|
return {
|
|
|
cancel: (sessionID: SessionID) => cancel(sessionID),
|
|
cancel: (sessionID: SessionID) => cancel(sessionID),
|
|
|
resolvePromptParts: (template: string) => resolvePromptParts(template),
|
|
resolvePromptParts: (template: string) => resolvePromptParts(template),
|
|
|
- prompt: (input: PromptInput) => prompt(input),
|
|
|
|
|
|
|
+ prompt: (input: PromptInput) => prompt(input).pipe(Effect.catch(Effect.die)),
|
|
|
} satisfies TaskPromptOps
|
|
} satisfies TaskPromptOps
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -1259,7 +1261,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|
|
return [{ ...part, messageID: info.id, sessionID: input.sessionID }]
|
|
return [{ ...part, messageID: info.id, sessionID: input.sessionID }]
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- const parts = yield* Effect.forEach(input.parts, resolvePart, { concurrency: "unbounded" }).pipe(
|
|
|
|
|
|
|
+ const resolvedParts = yield* Effect.forEach(input.parts, resolvePart, { concurrency: "unbounded" }).pipe(
|
|
|
Effect.map((x) => x.flat().map(assign)),
|
|
Effect.map((x) => x.flat().map(assign)),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -1272,7 +1274,13 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|
|
messageID: input.messageID,
|
|
messageID: input.messageID,
|
|
|
variant: input.variant,
|
|
variant: input.variant,
|
|
|
},
|
|
},
|
|
|
- { message: info, parts },
|
|
|
|
|
|
|
+ { message: info, parts: resolvedParts },
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const parts = yield* Effect.forEach(resolvedParts, (part) =>
|
|
|
|
|
+ part.type === "file" && part.mime.startsWith("image/")
|
|
|
|
|
+ ? image.normalize(part)
|
|
|
|
|
+ : Effect.succeed(part),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
const parsed = MessageV2.Info.zod.safeParse(info)
|
|
const parsed = MessageV2.Info.zod.safeParse(info)
|
|
@@ -1368,7 +1376,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|
|
return { info, parts }
|
|
return { info, parts }
|
|
|
}, Effect.scoped)
|
|
}, Effect.scoped)
|
|
|
|
|
|
|
|
- const prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.prompt")(
|
|
|
|
|
|
|
+ const prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts, Image.Error> = Effect.fn("SessionPrompt.prompt")(
|
|
|
function* (input: PromptInput) {
|
|
function* (input: PromptInput) {
|
|
|
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
|
|
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
|
|
|
yield* revert.cleanup(session)
|
|
yield* revert.cleanup(session)
|
|
@@ -1788,6 +1796,7 @@ export const defaultLayer = Layer.suspend(() =>
|
|
|
Layer.provide(Session.defaultLayer),
|
|
Layer.provide(Session.defaultLayer),
|
|
|
Layer.provide(SessionRevert.defaultLayer),
|
|
Layer.provide(SessionRevert.defaultLayer),
|
|
|
Layer.provide(SessionSummary.defaultLayer),
|
|
Layer.provide(SessionSummary.defaultLayer),
|
|
|
|
|
+ Layer.provide(Image.defaultLayer),
|
|
|
Layer.provide(
|
|
Layer.provide(
|
|
|
Layer.mergeAll(
|
|
Layer.mergeAll(
|
|
|
Agent.defaultLayer,
|
|
Agent.defaultLayer,
|