mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-20 06:53:27 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0003689201 | |||
| d1b060e392 |
@@ -2,6 +2,7 @@ import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from "@opencode-ai/httpapi-codegen"
|
||||
import { ClientApi, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode-ai/protocol/client"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Capability } from "@opencode-ai/schema/capability"
|
||||
import { Command } from "@opencode-ai/schema/command"
|
||||
import { Config } from "@opencode-ai/schema/config"
|
||||
import { Credential } from "@opencode-ai/schema/credential"
|
||||
@@ -43,6 +44,7 @@ const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseO
|
||||
const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints })
|
||||
const effectTypeReferences = [
|
||||
...namespaceTypes("Agent", "@opencode-ai/schema/agent", Agent),
|
||||
...namespaceTypes("Capability", "@opencode-ai/schema/capability", Capability),
|
||||
...namespaceTypes("Command", "@opencode-ai/schema/command", Command),
|
||||
...namespaceTypes("Config", "@opencode-ai/schema/config", Config),
|
||||
...namespaceTypes("Credential", "@opencode-ai/schema/credential", Credential),
|
||||
|
||||
@@ -37,6 +37,7 @@ import type { Vcs } from "@opencode-ai/schema/vcs"
|
||||
import type { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
import type { WebSearch } from "@opencode-ai/schema/websearch"
|
||||
import type { Config } from "@opencode-ai/schema/config"
|
||||
import type { Capability } from "@opencode-ai/schema/capability"
|
||||
|
||||
export type Endpoint0_0Output = { readonly healthy: true; readonly version: string; readonly pid: number }
|
||||
export type HealthGetOperation<E = never> = () => Effect.Effect<Endpoint0_0Output, E>
|
||||
@@ -1631,6 +1632,25 @@ export interface ConfigApi<E = never> {
|
||||
readonly get: ConfigGetOperation<E>
|
||||
}
|
||||
|
||||
export type Endpoint29_0Input = {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}
|
||||
export type Endpoint29_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Capability.Info> }
|
||||
export type CapabilityListOperation<E = never> = (input?: Endpoint29_0Input) => Effect.Effect<Endpoint29_0Output, E>
|
||||
|
||||
export type Endpoint29_1Input = {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
readonly ref: Capability.Ref
|
||||
readonly state: "enabled" | "disabled" | "inherit"
|
||||
}
|
||||
export type Endpoint29_1Output = void
|
||||
export type CapabilityUpdateOperation<E = never> = (input: Endpoint29_1Input) => Effect.Effect<Endpoint29_1Output, E>
|
||||
|
||||
export interface CapabilityApi<E = never> {
|
||||
readonly list: CapabilityListOperation<E>
|
||||
readonly update: CapabilityUpdateOperation<E>
|
||||
}
|
||||
|
||||
export interface AppApi<E = never> {
|
||||
readonly health: HealthApi<E>
|
||||
readonly server: ServerApi<E>
|
||||
@@ -1661,4 +1681,5 @@ export interface AppApi<E = never> {
|
||||
readonly migration: MigrationApi<E>
|
||||
readonly websearch: WebsearchApi<E>
|
||||
readonly config: ConfigApi<E>
|
||||
readonly capability: CapabilityApi<E>
|
||||
}
|
||||
|
||||
@@ -224,6 +224,10 @@ import type {
|
||||
Endpoint27_1Output,
|
||||
Endpoint28_0Input,
|
||||
Endpoint28_0Output,
|
||||
Endpoint29_0Input,
|
||||
Endpoint29_0Output,
|
||||
Endpoint29_1Input,
|
||||
Endpoint29_1Output,
|
||||
} from "../api/api.js"
|
||||
import { ClientError } from "./client-error.js"
|
||||
|
||||
@@ -1259,6 +1263,21 @@ const Endpoint28_0 = (raw: RawClient["server.config"]) => (input?: Endpoint28_0I
|
||||
|
||||
const adaptGroup28 = (raw: RawClient["server.config"]) => ({ get: Endpoint28_0(raw) })
|
||||
|
||||
const Endpoint29_0 = (raw: RawClient["server.capability"]) => (input?: Endpoint29_0Input) =>
|
||||
preserveEffect<Endpoint29_0Output>()(
|
||||
raw["capability.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint29_1 = (raw: RawClient["server.capability"]) => (input: Endpoint29_1Input) =>
|
||||
preserveEffect<Endpoint29_1Output>()(
|
||||
raw["capability.update"]({
|
||||
query: { location: input["location"] },
|
||||
payload: { ref: input["ref"], state: input["state"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const adaptGroup29 = (raw: RawClient["server.capability"]) => ({ list: Endpoint29_0(raw), update: Endpoint29_1(raw) })
|
||||
|
||||
const adaptClient = (raw: RawClient) => ({
|
||||
health: adaptGroup0(raw["server.health"]),
|
||||
server: adaptGroup1(raw["server.server"]),
|
||||
@@ -1289,6 +1308,7 @@ const adaptClient = (raw: RawClient) => ({
|
||||
migration: adaptGroup26(raw["server.migration"]),
|
||||
websearch: adaptGroup27(raw["server.websearch"]),
|
||||
config: adaptGroup28(raw["server.config"]),
|
||||
capability: adaptGroup29(raw["server.capability"]),
|
||||
})
|
||||
|
||||
export const make = (options?: { readonly baseUrl?: URL | string }) =>
|
||||
|
||||
@@ -220,6 +220,10 @@ import type {
|
||||
WebsearchQueryOutput,
|
||||
ConfigGetInput,
|
||||
ConfigGetOutput,
|
||||
CapabilityListInput,
|
||||
CapabilityListOutput,
|
||||
CapabilityUpdateInput,
|
||||
CapabilityUpdateOutput,
|
||||
} from "./types.js"
|
||||
import { ClientError } from "./client-error.js"
|
||||
|
||||
@@ -1840,6 +1844,33 @@ export function make(options: ClientOptions) {
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
capability: {
|
||||
list: (input?: CapabilityListInput, requestOptions?: RequestOptions) =>
|
||||
request<CapabilityListOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/capability`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
update: (input: CapabilityUpdateInput, requestOptions?: RequestOptions) =>
|
||||
request<CapabilityUpdateOutput>(
|
||||
{
|
||||
method: "PUT",
|
||||
path: `/api/capability`,
|
||||
query: { location: input["location"] },
|
||||
body: { ref: input["ref"], state: input["state"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,8 @@ export type SkillInfo = {
|
||||
|
||||
export type PermissionReply = "once" | "always" | "reject"
|
||||
|
||||
export type CapabilityRef = { kind: "skill"; key: [string, ...Array<string>] }
|
||||
|
||||
export type Pty = {
|
||||
id: string
|
||||
title: string
|
||||
@@ -244,7 +246,7 @@ export type PromptFileAttachment = {
|
||||
|
||||
export type PromptAgentAttachment = { name: string; mention?: PromptMention }
|
||||
|
||||
export type PromptSkillAttachment = { id: string; name: string; text: string; mention?: PromptMention }
|
||||
export type PromptSkillAttachment = { id: string; name: string; mention?: PromptMention }
|
||||
|
||||
export type ToolFileContent = { type: "file"; uri: string; mime: string; name?: string | null }
|
||||
|
||||
@@ -1062,6 +1064,24 @@ export type PermissionReplied = {
|
||||
data: { sessionID: string; requestID: string; reply: PermissionReply }
|
||||
}
|
||||
|
||||
export type CapabilityUpdated = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "capability.updated"
|
||||
location?: LocationRef
|
||||
data: { ref: CapabilityRef }
|
||||
}
|
||||
|
||||
export type CapabilityInfo = {
|
||||
ref: CapabilityRef
|
||||
name: string
|
||||
description?: string
|
||||
defaultState: "enabled" | "disabled"
|
||||
state: "enabled" | "disabled"
|
||||
preference?: "enabled" | "disabled"
|
||||
}
|
||||
|
||||
export type PtyCreated = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -2077,6 +2097,7 @@ export type V2Event =
|
||||
| WorktreeResolved
|
||||
| CommandUpdated
|
||||
| ConfigUpdated
|
||||
| CapabilityUpdated
|
||||
| SkillUpdated
|
||||
| PtyCreated
|
||||
| PtyUpdated
|
||||
@@ -2567,7 +2588,6 @@ export type SessionImportInput = {
|
||||
readonly skills?: ReadonlyArray<{
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly text: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly type: "user"
|
||||
@@ -2836,7 +2856,6 @@ export type SessionImportInput = {
|
||||
readonly skills?: ReadonlyArray<{
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly text: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly type: "user"
|
||||
@@ -3105,7 +3124,6 @@ export type SessionImportInput = {
|
||||
readonly skills?: ReadonlyArray<{
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly text: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly type: "user"
|
||||
@@ -5722,3 +5740,30 @@ export type ConfigGetInput = {
|
||||
}
|
||||
|
||||
export type ConfigGetOutput = Array<ConfigEntry>
|
||||
|
||||
export type CapabilityListInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type CapabilityListOutput = {
|
||||
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } }
|
||||
data: Array<CapabilityInfo>
|
||||
}
|
||||
|
||||
export type CapabilityUpdateInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
readonly ref: {
|
||||
readonly ref: { readonly kind: "skill"; readonly key: readonly [string, ...Array<string>] }
|
||||
readonly state: "enabled" | "disabled" | "inherit"
|
||||
}["ref"]
|
||||
readonly state: {
|
||||
readonly ref: { readonly kind: "skill"; readonly key: readonly [string, ...Array<string>] }
|
||||
readonly state: "enabled" | "disabled" | "inherit"
|
||||
}["state"]
|
||||
}
|
||||
|
||||
export type CapabilityUpdateOutput = void
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
export * as Capability from "./capability.js"
|
||||
|
||||
import { Capability } from "@opencode-ai/schema/capability"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Context, Effect, Layer, Option, Schema } from "effect"
|
||||
import { Bus } from "./bus.js"
|
||||
import { KV } from "./kv.js"
|
||||
|
||||
export const Ref = Capability.Ref
|
||||
export type Ref = Capability.Ref
|
||||
export const State = Capability.State
|
||||
export type State = Capability.State
|
||||
export const Preference = Capability.Preference
|
||||
export type Preference = Capability.Preference
|
||||
export const Info = Capability.Info
|
||||
export type Info = Capability.Info
|
||||
export const Update = Capability.Update
|
||||
export type Update = Capability.Update
|
||||
export const Event = Capability.Event
|
||||
|
||||
export const skill = (id: string) => Ref.make({ kind: "skill", key: [id] })
|
||||
|
||||
const Key = "capability:preferences"
|
||||
const Preferences = Schema.Array(Preference)
|
||||
const equals = Schema.toEquivalence(Ref)
|
||||
|
||||
export interface Interface {
|
||||
readonly list: () => Effect.Effect<ReadonlyArray<Preference>>
|
||||
readonly get: (ref: Ref) => Effect.Effect<State | undefined>
|
||||
readonly resolve: (ref: Ref, fallback?: boolean) => Effect.Effect<State>
|
||||
readonly set: (update: Update) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Capability") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const kv = yield* KV.Service
|
||||
|
||||
const load = Effect.fn("Capability.load")(function* () {
|
||||
const stored = yield* kv.get(Key)
|
||||
const decoded = Schema.decodeUnknownOption(Preferences)(stored)
|
||||
if (stored !== undefined && Option.isNone(decoded)) yield* kv.remove(Key)
|
||||
return Option.getOrElse(decoded, () => [])
|
||||
})
|
||||
|
||||
const get = Effect.fn("Capability.get")(function* (ref: Ref) {
|
||||
return (yield* load()).find((item) => equals(item.ref, ref))?.state
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
list: load,
|
||||
get,
|
||||
resolve: Effect.fn("Capability.resolve")(function* (ref, fallback = true) {
|
||||
return (yield* get(ref)) ?? (fallback ? "enabled" : "disabled")
|
||||
}),
|
||||
set: Effect.fn("Capability.set")(function* (update) {
|
||||
const preferences = (yield* load()).filter((item) => !equals(item.ref, update.ref))
|
||||
yield* kv.set(
|
||||
Key,
|
||||
update.state === "inherit" ? preferences : [...preferences, { ref: update.ref, state: update.state }],
|
||||
)
|
||||
yield* bus.publish(Event.Updated, { ref: update.ref })
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node, KV.node] })
|
||||
@@ -52,6 +52,7 @@ import { Tool } from "./tool.js"
|
||||
import { ToolOutput } from "./tool-output.js"
|
||||
import { Vcs } from "./vcs.js"
|
||||
import { AbsolutePath } from "./schema.js"
|
||||
import { Capability } from "./capability.js"
|
||||
|
||||
export { LocationServiceMap } from "./location-service-map.js"
|
||||
|
||||
@@ -59,6 +60,7 @@ const locationServiceNodes = [
|
||||
Location.node,
|
||||
Environment.node,
|
||||
Config.node,
|
||||
Capability.node,
|
||||
Agent.node,
|
||||
Command.node,
|
||||
Reference.node,
|
||||
|
||||
@@ -982,7 +982,6 @@ const resolvePrompt = Effect.fn("Session.resolvePrompt")(function* (
|
||||
return Effect.succeed({
|
||||
id: skill.id,
|
||||
name: skill.name,
|
||||
text: Skill.toModelOutput(skill, []),
|
||||
mention: attachment.mention,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -138,8 +138,7 @@ const serialize = (message: SessionMessage.Info) => {
|
||||
(file) =>
|
||||
`[Attached ${file.mime}: ${file.name ?? (file.source.type === "uri" ? file.source.uri : "inline attachment")}]`,
|
||||
) ?? []
|
||||
const skills = message.skills?.map((skill) => `[Attached skill: ${skill.name}]\n${skill.text}`) ?? []
|
||||
return [`[User]: ${message.text}`, ...skills, ...files].join("\n")
|
||||
return [`[User]: ${message.text}`, ...files].join("\n")
|
||||
}
|
||||
if (message.type === "location-switched")
|
||||
return `[User]: The working directory has been changed to ${message.location.directory}.`
|
||||
|
||||
@@ -227,7 +227,6 @@ function toLLMMessage(message: SessionMessage.Info, model: Model.Ref, providerMe
|
||||
]
|
||||
case "user":
|
||||
const content = [
|
||||
...(message.skills ?? []).map((skill) => Message.text(skill.text)),
|
||||
...(message.text === "" ? [] : [Message.text(message.text)]),
|
||||
...userAttachmentContent(message.files ?? []),
|
||||
]
|
||||
|
||||
@@ -207,7 +207,6 @@ function sanitizeMessage(message: SessionMessage.Info): SessionMessage.Info {
|
||||
skills: message.skills?.map((skill, index) => ({
|
||||
...skill,
|
||||
name: Skill.Name.make(redact("skill-name", String(index), skill.name)),
|
||||
text: redact("skill", String(index), skill.text),
|
||||
mention: skill.mention
|
||||
? { ...skill.mention, text: redact("skill-mention", String(index), skill.mention.text) }
|
||||
: undefined,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { Agent } from "../agent.js"
|
||||
import { Skill } from "../skill.js"
|
||||
import { Instructions } from "../instructions/index.js"
|
||||
import { Capability } from "../capability.js"
|
||||
|
||||
const Summary = Schema.Struct({
|
||||
id: Skill.ID,
|
||||
@@ -26,6 +27,7 @@ const render = (skills: ReadonlyArray<Summary>) =>
|
||||
[
|
||||
"Skills provide specialized instructions and workflows for specific tasks.",
|
||||
"Use the skill tool to load a skill when a task matches its description.",
|
||||
"When the user references a skill with @skill-id, load that skill with the skill tool.",
|
||||
...(skills.length === 0
|
||||
? ["No skills are currently available."]
|
||||
: ["<available_skills>", ...entries(skills), "</available_skills>"]),
|
||||
@@ -66,18 +68,25 @@ const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const skills = yield* Skill.Service
|
||||
const capability = yield* Capability.Service
|
||||
|
||||
return Service.of({
|
||||
load: Effect.fn("SkillInstructions.load")(function* (selection) {
|
||||
const agent = selection.info
|
||||
if (!agent) return Instructions.empty
|
||||
const permitted = Skill.available(yield* skills.list(), agent)
|
||||
const available = permitted
|
||||
.flatMap((skill) =>
|
||||
skill.description === undefined || skill.autoinvoke === false
|
||||
? []
|
||||
: [{ id: skill.id, name: skill.name, description: skill.description }],
|
||||
)
|
||||
const available = (yield* Effect.forEach(permitted, (skill) =>
|
||||
capability
|
||||
.resolve(Capability.skill(skill.id), skill.autoinvoke !== false)
|
||||
.pipe(
|
||||
Effect.map((state) =>
|
||||
state === "disabled" || skill.description === undefined
|
||||
? undefined
|
||||
: { id: skill.id, name: skill.name, description: skill.description },
|
||||
),
|
||||
),
|
||||
))
|
||||
.filter((skill): skill is Summary => skill !== undefined)
|
||||
.toSorted((a, b) => a.id.localeCompare(b.id))
|
||||
return Instructions.make<ReadonlyArray<Summary>>({
|
||||
key: Instructions.Key.make("core/skill-guidance"),
|
||||
@@ -94,4 +103,4 @@ const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Skill.node] })
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Skill.node, Capability.node] })
|
||||
|
||||
@@ -12,7 +12,7 @@ export const name = "skill"
|
||||
const FILE_LIMIT = 10
|
||||
|
||||
export const Input = Schema.Struct({
|
||||
id: Skill.ID.annotate({ description: "The ID of the skill from the available skills list" }),
|
||||
id: Skill.ID.annotate({ description: "The ID of an available skill or a skill explicitly referenced by the user" }),
|
||||
})
|
||||
|
||||
export const Output = Schema.Struct({
|
||||
@@ -23,7 +23,7 @@ export const Output = Schema.Struct({
|
||||
export const description = [
|
||||
"Load a specialized skill's instructions and resources into the current conversation when the task at hand matches its description.",
|
||||
"",
|
||||
"The skill ID must match one of the available skills in the instructions.",
|
||||
"The skill ID must match an available skill or a skill explicitly referenced by the user.",
|
||||
].join("\n")
|
||||
|
||||
export const toModelOutput = Skill.toModelOutput
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Capability } from "@opencode-ai/core/capability"
|
||||
import { Effect } from "effect"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(AppNodeBuilder.build(Capability.node))
|
||||
|
||||
describe("Capability", () => {
|
||||
it.effect("persists explicit preferences and restores inherited defaults", () =>
|
||||
Effect.gen(function* () {
|
||||
const capability = yield* Capability.Service
|
||||
const ref = Capability.skill("effect")
|
||||
|
||||
expect(yield* capability.resolve(ref)).toBe("enabled")
|
||||
yield* capability.set({ ref, state: "disabled" })
|
||||
expect(yield* capability.get(ref)).toBe("disabled")
|
||||
expect(yield* capability.resolve(ref)).toBe("disabled")
|
||||
|
||||
yield* capability.set({ ref, state: "inherit" })
|
||||
expect(yield* capability.get(ref)).toBeUndefined()
|
||||
expect(yield* capability.resolve(ref, false)).toBe("disabled")
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -205,18 +205,18 @@ Recent work
|
||||
})
|
||||
})
|
||||
|
||||
test("lowers selected skill instructions with the original user prompt", () => {
|
||||
test("does not inject skill content for reference-only attachments", () => {
|
||||
const messages = toLLMMessages(
|
||||
[
|
||||
SessionMessage.User.make({
|
||||
id: id("user-skill"),
|
||||
id: id("user-skill-reference"),
|
||||
type: "user",
|
||||
text: "Design this API",
|
||||
text: "Use @api-design",
|
||||
skills: [
|
||||
SkillAttachment.make({
|
||||
id: Skill.ID.make("api-design"),
|
||||
name: Skill.Name.make("API design"),
|
||||
text: "Start from the ideal call site.",
|
||||
mention: { start: 4, end: 15, text: "@api-design" },
|
||||
}),
|
||||
],
|
||||
time: { created },
|
||||
@@ -225,17 +225,9 @@ Recent work
|
||||
model,
|
||||
)
|
||||
|
||||
expect(messages).toHaveLength(1)
|
||||
expect(messages[0]).toMatchObject({
|
||||
id: id("user-skill"),
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Start from the ideal call site.",
|
||||
},
|
||||
{ type: "text", text: "Design this API" },
|
||||
],
|
||||
content: [{ type: "text", text: "Use @api-design" }],
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ const it = testEffect(
|
||||
)
|
||||
|
||||
describe("Session.skill", () => {
|
||||
it.effect("attaches a resolved skill snapshot to a normal prompt", () =>
|
||||
it.effect("keeps skill mentions as references on a normal prompt", () =>
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
const database = yield* Database.Service
|
||||
@@ -67,8 +67,8 @@ describe("Session.skill", () => {
|
||||
yield* sessions.prompt({
|
||||
id,
|
||||
sessionID: session.id,
|
||||
text: "Apply this guidance",
|
||||
skills: [{ id: Skill.ID.make("effect"), mention: { start: 20, end: 27, text: "/effect" } }],
|
||||
text: "Apply @effect",
|
||||
skills: [{ id: Skill.ID.make("effect"), mention: { start: 6, end: 13, text: "@effect" } }],
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInbox.promote(database.db, bus, session.id, "steer")
|
||||
@@ -77,13 +77,12 @@ describe("Session.skill", () => {
|
||||
expect.objectContaining({
|
||||
id,
|
||||
type: "user",
|
||||
text: "Apply this guidance",
|
||||
text: "Apply @effect",
|
||||
skills: [
|
||||
{
|
||||
id: "effect",
|
||||
name: "Effect",
|
||||
text: expect.stringContaining("Use Effect"),
|
||||
mention: { start: 20, end: 27, text: "/effect" },
|
||||
mention: { start: 6, end: 13, text: "@effect" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Skill } from "@opencode-ai/core/skill"
|
||||
import { SkillInstructions } from "@opencode-ai/core/skill/instructions"
|
||||
import { Capability } from "@opencode-ai/core/capability"
|
||||
import { it } from "../lib/effect"
|
||||
import { readInitial, readUpdate } from "../lib/instructions"
|
||||
|
||||
@@ -39,9 +40,16 @@ const manual = Skill.Info.make({
|
||||
content: "Manual guidance",
|
||||
})
|
||||
|
||||
const layer = (list: () => Skill.Info[]) =>
|
||||
const layer = (list: () => Skill.Info[], preferences = new Map<string, Capability.State>()) =>
|
||||
AppNodeBuilder.build(SkillInstructions.node, [
|
||||
[Skill.node, Layer.mock(Skill.Service, { list: () => Effect.succeed(list()) })],
|
||||
[
|
||||
Capability.node,
|
||||
Layer.mock(Capability.Service, {
|
||||
resolve: (ref, fallback = true) =>
|
||||
Effect.succeed(preferences.get(ref.key[0]) ?? (fallback ? "enabled" : "disabled")),
|
||||
}),
|
||||
],
|
||||
])
|
||||
|
||||
describe("SkillInstructions", () => {
|
||||
@@ -59,6 +67,7 @@ describe("SkillInstructions", () => {
|
||||
[
|
||||
"Skills provide specialized instructions and workflows for specific tasks.",
|
||||
"Use the skill tool to load a skill when a task matches its description.",
|
||||
"When the user references a skill with @skill-id, load that skill with the skill tool.",
|
||||
"<available_skills>",
|
||||
" <skill>",
|
||||
" <id>effect</id>",
|
||||
@@ -116,6 +125,21 @@ describe("SkillInstructions", () => {
|
||||
}).pipe(Effect.provide(layer(() => skills)))
|
||||
})
|
||||
|
||||
it.effect("applies capability preferences over skill autoinvoke defaults", () => {
|
||||
const agent = Agent.Info.make(Agent.Info.default(build))
|
||||
const preferences = new Map<string, Capability.State>([
|
||||
["effect", "disabled"],
|
||||
["manual", "enabled"],
|
||||
])
|
||||
return Effect.gen(function* () {
|
||||
const instructions = yield* SkillInstructions.Service
|
||||
const initialized = yield* instructions.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(readInitial))
|
||||
|
||||
expect(initialized.text).not.toContain("<id>effect</id>")
|
||||
expect(initialized.text).toContain("<id>manual</id>")
|
||||
}).pipe(Effect.provide(layer(() => [effect, manual], preferences)))
|
||||
})
|
||||
|
||||
it.effect("restates the full skill list when a description changes", () => {
|
||||
const agent = Agent.Info.make(Agent.Info.default(build))
|
||||
let skills = [effect]
|
||||
|
||||
@@ -32,6 +32,7 @@ import { WorktreeGroup } from "./groups/worktree.js"
|
||||
import { VcsGroup } from "./groups/vcs.js"
|
||||
import { MigrationGroup } from "./groups/migration.js"
|
||||
import { ConfigGroup } from "./groups/config.js"
|
||||
import { CapabilityGroup } from "./groups/capability.js"
|
||||
|
||||
type LocationGroups<LocationId extends HttpApiMiddleware.AnyId> =
|
||||
| HttpApiGroup.AddMiddleware<typeof LocationGroup, LocationId>
|
||||
@@ -53,6 +54,7 @@ type LocationGroups<LocationId extends HttpApiMiddleware.AnyId> =
|
||||
| HttpApiGroup.AddMiddleware<typeof ReferenceGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof VcsGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof ConfigGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof CapabilityGroup, LocationId>
|
||||
|
||||
type SessionGroups<SessionLocationId extends HttpApiMiddleware.AnyId, SessionLocationService> =
|
||||
| ReturnType<typeof makeSessionGroup<SessionLocationId, SessionLocationService>>
|
||||
@@ -174,6 +176,7 @@ const makeApiFromGroup = <
|
||||
.add(MigrationGroup)
|
||||
.add(WebSearchGroup.middleware(locationMiddleware))
|
||||
.add(ConfigGroup.middleware(locationMiddleware))
|
||||
.add(CapabilityGroup.middleware(locationMiddleware))
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode HttpApi",
|
||||
|
||||
@@ -62,6 +62,7 @@ export const groupNames = {
|
||||
"server.worktree": "worktree",
|
||||
"server.vcs": "vcs",
|
||||
"server.config": "config",
|
||||
"server.capability": "capability",
|
||||
} as const
|
||||
|
||||
export const promiseOmitEndpoints = new Set(["pty.connect", "pty.connectToken"])
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Capability } from "@opencode-ai/schema/capability"
|
||||
import { Location } from "@opencode-ai/schema/location"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { LocationQuery, locationQueryOpenApi } from "./location.js"
|
||||
|
||||
export const CapabilityGroup = HttpApiGroup.make("server.capability")
|
||||
.add(
|
||||
HttpApiEndpoint.get("capability.list", "/api/capability", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(Capability.Info)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.capability.list",
|
||||
summary: "List capabilities",
|
||||
description: "List manageable tools and MCP capabilities with their effective preference state.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.put("capability.update", "/api/capability", {
|
||||
query: LocationQuery,
|
||||
payload: Capability.Update,
|
||||
success: HttpApiSchema.NoContent,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.capability.update",
|
||||
summary: "Update capability preference",
|
||||
description: "Set or inherit the global preference for one capability.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "capability" }))
|
||||
@@ -0,0 +1,42 @@
|
||||
export * as Capability from "./capability.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { ephemeral, inventory } from "./event.js"
|
||||
import { optional } from "./schema.js"
|
||||
|
||||
export const Kind = Schema.Literal("skill")
|
||||
export type Kind = typeof Kind.Type
|
||||
|
||||
export interface Ref extends Schema.Schema.Type<typeof Ref> {}
|
||||
export const Ref = Schema.Struct({
|
||||
kind: Kind,
|
||||
key: Schema.NonEmptyArray(Schema.String),
|
||||
}).annotate({ identifier: "Capability.Ref" })
|
||||
|
||||
export const State = Schema.Literals(["enabled", "disabled"])
|
||||
export type State = typeof State.Type
|
||||
|
||||
export interface Preference extends Schema.Schema.Type<typeof Preference> {}
|
||||
export const Preference = Schema.Struct({
|
||||
ref: Ref,
|
||||
state: State,
|
||||
}).annotate({ identifier: "Capability.Preference" })
|
||||
|
||||
export interface Update extends Schema.Schema.Type<typeof Update> {}
|
||||
export const Update = Schema.Struct({
|
||||
ref: Ref,
|
||||
state: Schema.Union([State, Schema.Literal("inherit")]),
|
||||
}).annotate({ identifier: "Capability.Update" })
|
||||
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
export const Info = Schema.Struct({
|
||||
ref: Ref,
|
||||
name: Schema.String,
|
||||
description: Schema.String.pipe(optional),
|
||||
defaultState: State,
|
||||
state: State,
|
||||
preference: State.pipe(optional),
|
||||
}).annotate({ identifier: "Capability.Info" })
|
||||
|
||||
const Updated = ephemeral({ type: "capability.updated", schema: { ref: Ref } })
|
||||
export const Event = { Updated, Definitions: inventory(Updated) }
|
||||
@@ -2,6 +2,7 @@ export * as EventManifest from "./event-manifest.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Agent } from "./agent.js"
|
||||
import { Capability } from "./capability.js"
|
||||
import { Catalog } from "./catalog.js"
|
||||
import { Command } from "./command.js"
|
||||
import { Config } from "./config.js"
|
||||
@@ -52,6 +53,7 @@ const featureDefinitions = Event.inventory(
|
||||
...Worktree.Event.Definitions,
|
||||
...Command.Event.Definitions,
|
||||
...Config.Event.Definitions,
|
||||
...Capability.Event.Definitions,
|
||||
...Skill.Event.Definitions,
|
||||
...Pty.Event.Definitions,
|
||||
...Shell.Event.Definitions,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { Agent } from "./agent.js"
|
||||
export { Capability } from "./capability.js"
|
||||
export { Command } from "./command.js"
|
||||
export { Config } from "./config.js"
|
||||
export { Connection } from "./connection.js"
|
||||
|
||||
@@ -57,7 +57,6 @@ export interface SkillAttachment extends Schema.Schema.Type<typeof SkillAttachme
|
||||
export const SkillAttachment = Schema.Struct({
|
||||
id: Skill.ID,
|
||||
name: Skill.Name,
|
||||
text: Schema.String,
|
||||
mention: PromptMention.pipe(optional),
|
||||
}).annotate({ identifier: "Prompt.SkillAttachment" })
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import { VcsHandler } from "./handlers/vcs"
|
||||
import { EventFeed } from "./event-feed"
|
||||
import { MigrationHandler } from "./handlers/migration"
|
||||
import { ConfigHandler } from "./handlers/config"
|
||||
import { CapabilityHandler } from "./handlers/capability"
|
||||
|
||||
export const handlers = Layer.mergeAll(
|
||||
HealthHandler,
|
||||
@@ -60,4 +61,5 @@ export const handlers = Layer.mergeAll(
|
||||
WorktreeHandler,
|
||||
VcsHandler,
|
||||
ConfigHandler,
|
||||
CapabilityHandler,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Capability } from "@opencode-ai/core/capability"
|
||||
import { Skill } from "@opencode-ai/core/skill"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { response } from "../location"
|
||||
|
||||
export const CapabilityHandler = HttpApiBuilder.group(Api, "server.capability", (handlers) =>
|
||||
handlers
|
||||
.handle(
|
||||
"capability.list",
|
||||
Effect.fn(function* () {
|
||||
const capability = yield* Capability.Service
|
||||
const skills = yield* Skill.Service
|
||||
const info = yield* Effect.forEach(yield* skills.list(), (item) =>
|
||||
Effect.gen(function* () {
|
||||
const ref = Capability.skill(item.id)
|
||||
const preference = yield* capability.get(ref)
|
||||
return Capability.Info.make({
|
||||
ref,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
defaultState: item.autoinvoke === false ? "disabled" : "enabled",
|
||||
preference,
|
||||
state: yield* capability.resolve(ref, item.autoinvoke !== false),
|
||||
})
|
||||
}),
|
||||
)
|
||||
return yield* response(Effect.succeed(info))
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"capability.update",
|
||||
Effect.fn(function* (ctx) {
|
||||
const capability = yield* Capability.Service
|
||||
yield* capability.set(ctx.payload)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -1,87 +1,93 @@
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import type { CapabilityInfo, LocationRef } from "@opencode-ai/client"
|
||||
import { DialogSelect, type DialogSelectOption } from "../ui/dialog-select"
|
||||
import { createResource, createMemo, createSignal, Match, Switch } from "solid-js"
|
||||
import { createResource, createMemo, createSignal } from "solid-js"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { errorMessage } from "../util/error"
|
||||
import { useData } from "../context/data"
|
||||
import type { LocationRef } from "@opencode-ai/client"
|
||||
import { useClient } from "../context/client"
|
||||
import { useToast } from "../ui/toast"
|
||||
|
||||
export type DialogSkillProps = {
|
||||
location?: LocationRef
|
||||
onSelect: (skill: string) => void
|
||||
}
|
||||
|
||||
export function DialogSkill(props: DialogSkillProps) {
|
||||
const dialog = useDialog()
|
||||
const data = useData()
|
||||
const client = useClient()
|
||||
const toast = useToast()
|
||||
const theme = useTheme()
|
||||
dialog.setSize("large")
|
||||
|
||||
const [loadError, setLoadError] = createSignal<unknown>()
|
||||
const [pending, setPending] = createSignal<string>()
|
||||
|
||||
const [skills] = createResource(() =>
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
const current = data.location.skill.list(props.location)
|
||||
if (current) return current
|
||||
await data.location.skill.sync(props.location)
|
||||
return data.location.skill.list(props.location) ?? []
|
||||
})
|
||||
// Catch so the rejected resource never reaches the memo below: reading
|
||||
// skills() in an errored state re-throws and tears down the dialog.
|
||||
.catch((error) => {
|
||||
const location = () =>
|
||||
props.location ? { directory: props.location.directory, workspace: props.location.workspaceID } : undefined
|
||||
const [skills, { mutate }] = createResource<CapabilityInfo[]>(() =>
|
||||
client.api.capability.list({ location: location() }).then(
|
||||
(result) => result.data,
|
||||
(error) => {
|
||||
setLoadError(error)
|
||||
return undefined
|
||||
}),
|
||||
return []
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const showError = createMemo(() => Boolean(loadError()))
|
||||
const key = (ref: CapabilityInfo["ref"]) => JSON.stringify([ref.kind, ...ref.key])
|
||||
|
||||
const toggle = async (skill: CapabilityInfo) => {
|
||||
const id = key(skill.ref)
|
||||
if (pending()) return
|
||||
const state: CapabilityInfo["state"] = skill.state === "enabled" ? "disabled" : "enabled"
|
||||
const preference: CapabilityInfo["preference"] = state === skill.defaultState ? undefined : state
|
||||
setPending(id)
|
||||
mutate((current) => current?.map((item) => (key(item.ref) === id ? { ...item, state, preference } : item)))
|
||||
const error = await client.api.capability
|
||||
.update({ ref: skill.ref, state: preference ?? "inherit", location: location() })
|
||||
.then(
|
||||
() => undefined,
|
||||
(error) => error,
|
||||
)
|
||||
if (error) {
|
||||
mutate((current) => current?.map((item) => (key(item.ref) === id ? skill : item)))
|
||||
toast.show({ title: "Could not update skill", message: errorMessage(error), variant: "error" })
|
||||
}
|
||||
setPending(undefined)
|
||||
}
|
||||
|
||||
const options = createMemo<DialogSelectOption<string>[]>(() => {
|
||||
if (showError()) return []
|
||||
const list = skills() ?? []
|
||||
const maxWidth = Math.max(0, ...list.map((s) => s.name.length))
|
||||
return list.map((skill) => ({
|
||||
title: skill.name.padEnd(maxWidth),
|
||||
title: `[${skill.state === "enabled" ? "x" : " "}] ${skill.name}`,
|
||||
description: skill.description?.replace(/\s+/g, " ").trim(),
|
||||
value: skill.id,
|
||||
onSelect: () => {
|
||||
props.onSelect(skill.id)
|
||||
dialog.clear()
|
||||
},
|
||||
searchText: `${skill.ref.key.join(" ")} ${skill.name} ${skill.description ?? ""}`,
|
||||
footer: pending() === key(skill.ref) ? "updating" : skill.preference ? "custom" : "default",
|
||||
footerColor: theme.text.subdued,
|
||||
value: key(skill.ref),
|
||||
onSelect: () => void toggle(skill),
|
||||
}))
|
||||
})
|
||||
|
||||
return (
|
||||
<DialogSelect
|
||||
title="Skills"
|
||||
placeholder="Search skills"
|
||||
options={options()}
|
||||
renderFilter={!showError() && !skills.loading}
|
||||
locked={showError() || skills.loading}
|
||||
preserveSelection
|
||||
footerHints={[{ title: "toggle", label: "enter" }]}
|
||||
locked={skills.loading && skills() === undefined}
|
||||
emptyView={
|
||||
<Switch
|
||||
fallback={
|
||||
<box paddingLeft={4} paddingRight={4}>
|
||||
<text fg={theme.text.subdued}>No skills available</text>
|
||||
</box>
|
||||
}
|
||||
>
|
||||
<Match when={showError()}>
|
||||
<box paddingLeft={4} paddingRight={4}>
|
||||
<text fg={theme.text.feedback.error.default} attributes={TextAttributes.BOLD}>
|
||||
Could not load skills
|
||||
</text>
|
||||
<text fg={theme.text.subdued}>{errorMessage(loadError())}</text>
|
||||
<text fg={theme.text.subdued}>Close and reopen Skills to try again.</text>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={skills.loading}>
|
||||
<box paddingLeft={4} paddingRight={4}>
|
||||
<text fg={theme.text.subdued}>Loading skills…</text>
|
||||
</box>
|
||||
</Match>
|
||||
</Switch>
|
||||
<box paddingLeft={4} paddingRight={4}>
|
||||
<text fg={theme.text.subdued}>
|
||||
{skills.loading
|
||||
? "Loading skills…"
|
||||
: showError()
|
||||
? `Could not load skills: ${errorMessage(loadError())}`
|
||||
: "No skills available"}
|
||||
</text>
|
||||
</box>
|
||||
}
|
||||
noMatchView={
|
||||
<box paddingLeft={4} paddingRight={4}>
|
||||
|
||||
@@ -176,7 +176,7 @@ export function Autocomplete(props: {
|
||||
|
||||
const charAfterCursor = displayCharAt(props.value, currentCursorOffset)
|
||||
const needsSpace = charAfterCursor !== " "
|
||||
const prefix = part.type === "skill" ? "/" : "@"
|
||||
const prefix = "@"
|
||||
const append = prefix + text + (needsSpace ? " " : "")
|
||||
|
||||
input.cursorOffset = store.index
|
||||
@@ -478,6 +478,22 @@ export function Autocomplete(props: {
|
||||
)
|
||||
})
|
||||
|
||||
const skillOptions = createMemo(() =>
|
||||
(data.location.skill.list(location.current) ?? []).map(
|
||||
(skill): AutocompleteOption => ({
|
||||
display: "@" + skill.id,
|
||||
description: skill.description,
|
||||
kind: "skill",
|
||||
onSelect: () => {
|
||||
insertPart(skill.id, {
|
||||
type: "skill",
|
||||
value: { id: Skill.ID.make(skill.id), mention: { start: 0, end: 0, text: "" } },
|
||||
})
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const referenceAliases = createMemo(() =>
|
||||
references()
|
||||
.filter((reference) => !reference.hidden)
|
||||
@@ -537,11 +553,7 @@ export function Autocomplete(props: {
|
||||
display: "/" + skill.id,
|
||||
description: skill.description,
|
||||
kind: "skill",
|
||||
onSelect: () =>
|
||||
insertPart(skill.id, {
|
||||
type: "skill",
|
||||
value: { id: Skill.ID.make(skill.id), mention: { start: 0, end: 0, text: "" } },
|
||||
}),
|
||||
onSelect: () => insertSlash(skill.id),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -592,10 +604,10 @@ export function Autocomplete(props: {
|
||||
const fileOptions: AutocompleteOption[] = store.visible === "reference" ? fileSearch.options : []
|
||||
const nonFileOptions: AutocompleteOption[] =
|
||||
store.visible === "reference"
|
||||
? [...referenceAliasesValue, ...agentsValue, ...mcpResources()]
|
||||
? [...skillOptions(), ...referenceAliasesValue, ...agentsValue, ...mcpResources()]
|
||||
: store.index === 0
|
||||
? [...commandsValue]
|
||||
: commandsValue.filter((item) => item.kind === "skill")
|
||||
: []
|
||||
|
||||
if (!searchValue) {
|
||||
return [...nonFileOptions, ...fileOptions]
|
||||
|
||||
@@ -30,7 +30,6 @@ import { stringWidth } from "../../util/string-width"
|
||||
import { createStore, produce, unwrap } from "solid-js/store"
|
||||
import { emptyPrompt, usePromptHistory, type PromptInfo, type PromptPartRef } from "../../prompt/history"
|
||||
import { saveDraft, takeDraft } from "./draft-stash"
|
||||
import { Skill } from "@opencode-ai/schema/skill"
|
||||
import { computePromptTraits } from "../../prompt/traits"
|
||||
import { expandPastedTextPlaceholders, expandTrackedPastedText } from "../../prompt/part"
|
||||
import { usePromptStash } from "../../prompt/stash"
|
||||
@@ -42,10 +41,10 @@ import { errorMessage } from "../../util/error"
|
||||
import { createColors, createFrames } from "../../ui/spinner"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import { DialogIntegration } from "../dialog-integration"
|
||||
import { DialogSkill } from "../dialog-skill"
|
||||
import { useConnected } from "../use-connected"
|
||||
import { useToast } from "../../ui/toast"
|
||||
import { createFadeIn } from "../../util/signal"
|
||||
import { DialogSkill } from "../dialog-skill"
|
||||
import { useArgs } from "../../context/args"
|
||||
import { useConfig } from "../../config"
|
||||
import { usePromptMove } from "./move"
|
||||
@@ -582,44 +581,6 @@ export function Prompt(props: PromptProps) {
|
||||
input.cursorOffset = stringWidth(normalized)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Skills",
|
||||
name: "prompt.skills",
|
||||
category: "Prompt",
|
||||
slash: { name: "skills" },
|
||||
run: () => {
|
||||
dialog.replace(() => (
|
||||
<DialogSkill
|
||||
location={currentLocation.current}
|
||||
onSelect={(skill) => {
|
||||
if (store.prompt.skills?.some((item) => item.id === skill)) return
|
||||
const text = `/${skill}`
|
||||
const start = input.cursorOffset
|
||||
input.insertText(text + " ")
|
||||
const extmarkId = input.extmarks.create({
|
||||
start,
|
||||
end: start + promptOffsetWidth(text),
|
||||
virtual: true,
|
||||
styleId: skillStyleId,
|
||||
typeId: promptPartTypeId,
|
||||
})
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.prompt.text = input.plainText
|
||||
const skills = (draft.prompt.skills ??= [])
|
||||
const index = skills.length
|
||||
skills.push({
|
||||
id: Skill.ID.make(skill),
|
||||
mention: { start, end: start + promptOffsetWidth(text), text },
|
||||
})
|
||||
draft.extmarkToPart.set(extmarkId, { type: "skill", index })
|
||||
}),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Move session",
|
||||
desc: "Move to another project dir",
|
||||
@@ -661,7 +622,6 @@ export function Prompt(props: PromptProps) {
|
||||
"prompt.stash",
|
||||
"prompt.stash.pop",
|
||||
"prompt.stash.list",
|
||||
"prompt.skills",
|
||||
"session.interrupt",
|
||||
"session.background",
|
||||
"session.move",
|
||||
|
||||
Reference in New Issue
Block a user