From 402325d0a3a1442b719a2775770dd592e26da579 Mon Sep 17 00:00:00 2001 From: Filip Hejmowski Date: Tue, 8 Sep 2026 23:31:22 +0200 Subject: [PATCH 1/5] labels for mentions --- packages/core/test/subagent-skills.test.ts | 132 ++++++++++++++++++ .../tui/src/component/prompt/autocomplete.tsx | 45 +++++- packages/tui/test/prompt-mentions.test.tsx | 78 +++++++++++ 3 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 packages/core/test/subagent-skills.test.ts create mode 100644 packages/tui/test/prompt-mentions.test.tsx diff --git a/packages/core/test/subagent-skills.test.ts b/packages/core/test/subagent-skills.test.ts new file mode 100644 index 000000000000..20de947008be --- /dev/null +++ b/packages/core/test/subagent-skills.test.ts @@ -0,0 +1,132 @@ +import path from "node:path" +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { LanguageModel } from "@opencode/ai" +import { OpenAIChat } from "@opencode/ai/protocols" +import { TestLLM } from "@opencode/ai/testing" +import { Agent } from "@opencode/core/agent" +import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder" +import { LayerNodePlatform } from "@opencode/core/effect/app-node-platform" +import { LocationServiceMap } from "@opencode/core/location-service-map" +import { Plugin } from "@opencode/core/plugin" +import { Session } from "@opencode/core/session" +import { SessionExecution } from "@opencode/core/session/execution" +import { SessionRunnerModel } from "@opencode/core/session/runner/model" +import { Skill } from "@opencode/core/skill" +import { AbsolutePath } from "@opencode/core/schema" +import { Global } from "@opencode/util/global" +import { LayerNode } from "@opencode/util/effect/layer-node" +import { tempGlobalLayer } from "./fixture/global" +import { offlineModels } from "./fixture/models" +import { tmpdir } from "./fixture/tmpdir" +import { testEffect } from "./lib/effect" + +const llm = TestLLM.testLayer() +const it = testEffect( + AppNodeBuilder.build(LayerNode.group([Session.node, SessionExecution.node, LocationServiceMap.node]), [ + Global.node.replace(tempGlobalLayer), + offlineModels, + LayerNodePlatform.llmClient.replace(llm), + SessionRunnerModel.node.replace( + Layer.succeed(SessionRunnerModel.Service, { + resolve: () => + Effect.succeed( + SessionRunnerModel.resolved( + LanguageModel.make({ id: "fixture", provider: "test", route: OpenAIChat.route }), + { + capabilities: { tools: true, input: ["text"], output: ["text"] }, + cost: [], + limit: { context: 200_000, output: 32_000 }, + }, + ), + ), + }), + ), + ]).pipe(Layer.merge(llm)), +) + +describe("Subagent skills", () => { + it.live("loads an explicit skill before inference and a model-selected skill on the next step", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((dir) => + Effect.gen(function* () { + const sessions = yield* Session.Service + const execution = yield* SessionExecution.Service + const locations = yield* LocationServiceMap.Service + const model = yield* TestLLM.Test + const parent = yield* sessions.create({ location: { directory: AbsolutePath.make(dir.path) } }) + const child = yield* sessions.create({ + parentID: parent.id, + agent: Agent.ID.make("reviewer"), + title: "Skill fixture", + }) + const info = Skill.Info.make({ + id: Skill.ID.make("fixture-guide"), + name: Skill.Name.make("Fixture guide"), + description: "Use this skill for the fixture review.", + location: AbsolutePath.make(path.join(dir.path, "guide.md")), + content: "Use the unique review marker SKILL_CONTENT_CANARY.", + }) + yield* Effect.gen(function* () { + const plugins = yield* Plugin.Service + yield* plugins.awaitActivation + const agents = yield* Agent.Service + yield* agents.transform((editor) => + editor.update(Agent.ID.make("reviewer"), (agent) => { + agent.mode = "subagent" + agent.permissions.push({ action: "*", resource: "*", effect: "allow" }) + }), + ) + const skills = yield* Skill.Service + yield* skills.transform((editor) => editor.add(info)) + }).pipe(Effect.provide(locations.get(child.location))) + + yield* model.push(TestLLM.text("Explicit skill received", "explicit")) + yield* sessions.prompt({ + sessionID: child.id, + text: "Use @fixture-guide", + skills: [{ id: info.id, mention: { start: 4, end: 18, text: "@fixture-guide" } }], + resume: false, + }) + yield* execution.resume(child.id) + const explicit = yield* model.requests() + expect(explicit).toHaveLength(1) + expect( + explicit[0].messages.some( + (message) => + message.role === "user" && + message.content.some((part) => part.type === "text" && part.text.includes(info.content)), + ), + ).toBe(true) + + const other = yield* sessions.create({ + parentID: parent.id, + agent: Agent.ID.make("reviewer"), + title: "Tool skill fixture", + }) + yield* model.push( + TestLLM.tool("load-guide", "skill", { id: info.id }), + TestLLM.text("Loaded skill received", "loaded"), + ) + yield* sessions.prompt({ sessionID: other.id, text: "Review using the fixture guide", resume: false }) + yield* execution.resume(other.id) + const requests = (yield* model.requests()).slice(explicit.length) + expect(requests).toHaveLength(2) + expect(requests[0].tools?.some((tool) => tool.name === "skill")).toBe(true) + expect(JSON.stringify(requests[0].system)).toContain(info.id) + expect( + requests[0].messages.some((message) => + message.content.some((part) => part.type === "text" && part.text.includes(info.content)), + ), + ).toBe(false) + expect(JSON.stringify(requests[1].messages.filter((message) => message.role === "tool"))).toContain( + info.content, + ) + }), + ), + ), + ) +}) diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index bb35ea31d293..857107f1a7ec 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -12,7 +12,7 @@ import { getScrollAcceleration } from "../../util/scroll" import { useTuiPaths } from "../../context/runtime" import { useConfig } from "../../config" import { useLocation } from "../../context/location" -import { useTheme } from "../../context/theme" +import { useTheme, useThemes } from "../../context/theme" import { SplitBorder } from "../../ui/border" import { useTerminalDimensions } from "@opentui/solid" import { Locale } from "../../util/locale" @@ -44,7 +44,7 @@ export type AutocompleteOption = { path?: string absolute?: string destructive?: { id: string; confirm: string; run: () => void } - kind?: "skill" + kind?: "skill" | "agent" | "file" | "directory" | "reference" | "resource" queueable?: boolean } @@ -78,6 +78,7 @@ export function Autocomplete(props: { const keymap = Keymap.use() const keymapCommands = Keymap.useCommands() const theme = useTheme("overlay") + const themes = useThemes() const dimensions = useTerminalDimensions() const frecency = useFrecency() const config = useConfig().data @@ -376,6 +377,7 @@ export function Autocomplete(props: { const { filename, part } = createFilePart(item, path.join(result.location.directory, item.path), lineRange) return { display: Locale.truncateMiddle(filename, width), + kind: item.type === "directory" ? "directory" : "file", value: filename, isDirectory: item.type === "directory", path: item.path, @@ -414,6 +416,7 @@ export function Autocomplete(props: { for (const res of data.location.mcp.resource.list(location.current) ?? []) { options.push({ display: Locale.truncateMiddle(res.name, width), + kind: "resource", // Match the name only; matching the URI caused unrelated fuzzy hits. value: res.name, description: res.description, @@ -440,6 +443,7 @@ export function Autocomplete(props: { .map( (agent): AutocompleteOption => ({ display: "@" + agent.id, + kind: "agent", onSelect: () => { insertPart(agent.id, { type: "agent", @@ -475,6 +479,7 @@ export function Autocomplete(props: { .map( (reference): AutocompleteOption => ({ display: "@" + reference.name, + kind: "reference", description: ` ${reference.source.type === "git" ? reference.source.repository : reference.source.path}`, onSelect: () => { insertPart(reference.name, { @@ -882,6 +887,14 @@ export function Autocomplete(props: { return "No matching files, agents, or references" }) const emptyError = createMemo(() => store.visible === "reference" && !files.loading && visibleFiles().failed) + const labels = { + skill: "Skill", + agent: "Agent", + file: "File", + directory: "Dir", + reference: "Reference", + resource: "MCP", + } return ( {(option, index) => { const destructive = () => option().destructive + const label = () => { + const kind = option().kind + return kind ? labels[kind] : undefined + } + const labelColor = () => { + if (index === store.selected) return theme.text.action.primary.focused + const kind = option().kind + const scope = kind === "skill" ? "extmark.skill" : kind === "agent" ? "extmark.agent" : "extmark.file" + return themes.currentSyntax().getStyle(scope)?.fg ?? theme.text.subdued + } + const contentWidth = () => { + const text = label() + return Math.max(1, position().width - 4 - (text ? stringWidth(text) + 2 : 0)) + } const confirmingAction = () => { const action = destructive() return action !== undefined && action.id === confirming() @@ -944,17 +971,29 @@ export function Autocomplete(props: { : theme.text.default } flexShrink={0} + wrapMode="none" > - {confirmingAction() ? destructive()?.confirm : option().display} + {Locale.truncateMiddle( + confirmingAction() ? (destructive()?.confirm ?? "") : option().display, + contentWidth(), + )} {" " + option().description?.replace(/\s+/g, " ").trim()} + + + + {label()} + + ) }} diff --git a/packages/tui/test/prompt-mentions.test.tsx b/packages/tui/test/prompt-mentions.test.tsx new file mode 100644 index 000000000000..1cff0470fcad --- /dev/null +++ b/packages/tui/test/prompt-mentions.test.tsx @@ -0,0 +1,78 @@ +import { expect, test } from "bun:test" +import { createAppFixture } from "./fixture/app" +import { tmpdir } from "./fixture/fixture" +import { directory, json } from "./fixture/tui-client" + +test.each([44, 100])("mention labels keep skills and agents distinct at width %s", async (width) => { + await using state = await tmpdir() + const location = { directory, project: { id: "project", directory, canonical: directory } } + const session = { + id: "ses_mentions", + title: "Mention fixture", + projectID: "project", + location: { directory }, + agent: "build", + model: { providerID: "provider", id: "model" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: 0, updated: 0 }, + } + const bodies: unknown[] = [] + await using setup = await createAppFixture({ + width, + state: state.path, + config: { animations: false, tabs: { enabled: false }, session: { sidebar: "hide" } }, + args: { sessionID: session.id }, + fetch: async (url, request) => { + if (url.pathname === "/api/agent") + return json({ + location, + data: [ + { id: "build", mode: "primary", hidden: false, permissions: [] }, + { id: "review", mode: "subagent", hidden: false, permissions: [] }, + ], + }) + if (url.pathname === "/api/model") + return json({ location, data: [{ id: "model", providerID: "provider", name: "Model", variants: [] }] }) + if (url.pathname === "/api/provider") return json({ location, data: [{ id: "provider", name: "Provider" }] }) + if (url.pathname === "/api/skill") + return json({ + location, + data: [ + { + id: "review", + name: "Review", + description: "Review guidance with a deliberately long description that must not hide the type label", + }, + ], + }) + if (url.pathname === "/api/fs/find") return json({ location, data: [] }) + if (url.pathname === `/api/session/${session.id}`) return json({ data: session }) + if (/^\/api\/session\/ses_mentions\/(message|inbox|permission)$/.test(url.pathname)) + return json({ data: [], cursor: {} }) + if (url.pathname === `/api/session/${session.id}/prompt`) { + bodies.push(await request.json()) + return new Response(null, { status: 204 }) + } + return undefined + }, + }) + await setup.ready + await setup.waitForFrame((frame) => frame.includes("Model")) + setup.mockInput.pressKey("u", { ctrl: true }) + await setup.mockInput.typeText("@review") + const frame = await setup.waitForFrame((frame) => frame.includes("Skill") && frame.includes("Agent")) + const skill = frame.split("\n").find((line) => line.includes("Skill"))! + expect(skill).toContain("@review") + expect(skill).toContain("Review") + expect(frame.split("\n").find((line) => line.includes("Agent"))).toContain("@review") + if (frame.indexOf("Agent") < frame.indexOf("Skill")) setup.mockInput.pressArrow("down") + setup.mockInput.pressEnter() + await setup.waitForFrame((frame) => !frame.includes("Skill") && frame.includes("@review")) + setup.mockInput.pressEnter() + await setup.waitFor(() => bodies.length === 1) + expect(bodies[0]).toMatchObject({ + text: "@review ", + skills: [{ id: "review", mention: { text: "@review", start: 0, end: 7 } }], + }) +}) From 596b9430c3bba34e3c497d882a20af1d5c462c6c Mon Sep 17 00:00:00 2001 From: Filip Hejmowski Date: Fri, 11 Sep 2026 21:53:42 +0200 Subject: [PATCH 2/5] clean up --- .../tui/src/component/prompt/autocomplete.tsx | 49 +++---------------- 1 file changed, 7 insertions(+), 42 deletions(-) diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 857107f1a7ec..9e67c1d8e419 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -12,7 +12,7 @@ import { getScrollAcceleration } from "../../util/scroll" import { useTuiPaths } from "../../context/runtime" import { useConfig } from "../../config" import { useLocation } from "../../context/location" -import { useTheme, useThemes } from "../../context/theme" +import { useTheme } from "../../context/theme" import { SplitBorder } from "../../ui/border" import { useTerminalDimensions } from "@opentui/solid" import { Locale } from "../../util/locale" @@ -44,7 +44,7 @@ export type AutocompleteOption = { path?: string absolute?: string destructive?: { id: string; confirm: string; run: () => void } - kind?: "skill" | "agent" | "file" | "directory" | "reference" | "resource" + kind?: "skill" | "agent" | "file" | "directory" | "reference" queueable?: boolean } @@ -78,7 +78,6 @@ export function Autocomplete(props: { const keymap = Keymap.use() const keymapCommands = Keymap.useCommands() const theme = useTheme("overlay") - const themes = useThemes() const dimensions = useTerminalDimensions() const frecency = useFrecency() const config = useConfig().data @@ -407,36 +406,6 @@ export function Autocomplete(props: { return { options: [], failed: false, query: "", resolved: false } }) - const mcpResources = createMemo(() => { - if (store.visible !== "reference") return [] - - const options: AutocompleteOption[] = [] - const width = props.anchor().width - 4 - - for (const res of data.location.mcp.resource.list(location.current) ?? []) { - options.push({ - display: Locale.truncateMiddle(res.name, width), - kind: "resource", - // Match the name only; matching the URI caused unrelated fuzzy hits. - value: res.name, - description: res.description, - onSelect: () => { - insertPart(res.name, { - type: "file", - value: { - uri: res.uri, - name: res.name, - description: res.description, - mention: { start: 0, end: 0, text: "" }, - }, - }) - }, - }) - } - - return options - }) - const agents = createMemo(() => { return (data.location.agent.list() ?? []) .filter((agent) => !agent.hidden && agent.mode !== "primary") @@ -585,7 +554,7 @@ export function Autocomplete(props: { const fileOptions: AutocompleteOption[] = store.visible === "reference" ? fileSearch.options : [] const nonFileOptions: AutocompleteOption[] = store.visible === "reference" - ? [...skillOptions(), ...referenceAliasesValue, ...agentsValue, ...mcpResources()] + ? [...skillOptions(), ...referenceAliasesValue, ...agentsValue] : store.index === 0 ? [...commandsValue] : [] @@ -893,7 +862,6 @@ export function Autocomplete(props: { file: "File", directory: "Dir", reference: "Reference", - resource: "MCP", } return ( @@ -932,12 +900,6 @@ export function Autocomplete(props: { const kind = option().kind return kind ? labels[kind] : undefined } - const labelColor = () => { - if (index === store.selected) return theme.text.action.primary.focused - const kind = option().kind - const scope = kind === "skill" ? "extmark.skill" : kind === "agent" ? "extmark.agent" : "extmark.file" - return themes.currentSyntax().getStyle(scope)?.fg ?? theme.text.subdued - } const contentWidth = () => { const text = label() return Math.max(1, position().width - 4 - (text ? stringWidth(text) + 2 : 0)) @@ -990,7 +952,10 @@ export function Autocomplete(props: { - + {label()} From 6ef90c15b09d1e83fe678256c1534ef29dc2a0dc Mon Sep 17 00:00:00 2001 From: Filip Hejmowski Date: Fri, 11 Sep 2026 21:56:19 +0200 Subject: [PATCH 3/5] chore: remove mention and subagent skill tests --- packages/core/test/subagent-skills.test.ts | 132 --------------------- packages/tui/test/prompt-mentions.test.tsx | 78 ------------ 2 files changed, 210 deletions(-) delete mode 100644 packages/core/test/subagent-skills.test.ts delete mode 100644 packages/tui/test/prompt-mentions.test.tsx diff --git a/packages/core/test/subagent-skills.test.ts b/packages/core/test/subagent-skills.test.ts deleted file mode 100644 index 20de947008be..000000000000 --- a/packages/core/test/subagent-skills.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import path from "node:path" -import { describe, expect } from "bun:test" -import { Effect, Layer } from "effect" -import { LanguageModel } from "@opencode/ai" -import { OpenAIChat } from "@opencode/ai/protocols" -import { TestLLM } from "@opencode/ai/testing" -import { Agent } from "@opencode/core/agent" -import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder" -import { LayerNodePlatform } from "@opencode/core/effect/app-node-platform" -import { LocationServiceMap } from "@opencode/core/location-service-map" -import { Plugin } from "@opencode/core/plugin" -import { Session } from "@opencode/core/session" -import { SessionExecution } from "@opencode/core/session/execution" -import { SessionRunnerModel } from "@opencode/core/session/runner/model" -import { Skill } from "@opencode/core/skill" -import { AbsolutePath } from "@opencode/core/schema" -import { Global } from "@opencode/util/global" -import { LayerNode } from "@opencode/util/effect/layer-node" -import { tempGlobalLayer } from "./fixture/global" -import { offlineModels } from "./fixture/models" -import { tmpdir } from "./fixture/tmpdir" -import { testEffect } from "./lib/effect" - -const llm = TestLLM.testLayer() -const it = testEffect( - AppNodeBuilder.build(LayerNode.group([Session.node, SessionExecution.node, LocationServiceMap.node]), [ - Global.node.replace(tempGlobalLayer), - offlineModels, - LayerNodePlatform.llmClient.replace(llm), - SessionRunnerModel.node.replace( - Layer.succeed(SessionRunnerModel.Service, { - resolve: () => - Effect.succeed( - SessionRunnerModel.resolved( - LanguageModel.make({ id: "fixture", provider: "test", route: OpenAIChat.route }), - { - capabilities: { tools: true, input: ["text"], output: ["text"] }, - cost: [], - limit: { context: 200_000, output: 32_000 }, - }, - ), - ), - }), - ), - ]).pipe(Layer.merge(llm)), -) - -describe("Subagent skills", () => { - it.live("loads an explicit skill before inference and a model-selected skill on the next step", () => - Effect.acquireRelease( - Effect.promise(() => tmpdir()), - (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()), - ).pipe( - Effect.flatMap((dir) => - Effect.gen(function* () { - const sessions = yield* Session.Service - const execution = yield* SessionExecution.Service - const locations = yield* LocationServiceMap.Service - const model = yield* TestLLM.Test - const parent = yield* sessions.create({ location: { directory: AbsolutePath.make(dir.path) } }) - const child = yield* sessions.create({ - parentID: parent.id, - agent: Agent.ID.make("reviewer"), - title: "Skill fixture", - }) - const info = Skill.Info.make({ - id: Skill.ID.make("fixture-guide"), - name: Skill.Name.make("Fixture guide"), - description: "Use this skill for the fixture review.", - location: AbsolutePath.make(path.join(dir.path, "guide.md")), - content: "Use the unique review marker SKILL_CONTENT_CANARY.", - }) - yield* Effect.gen(function* () { - const plugins = yield* Plugin.Service - yield* plugins.awaitActivation - const agents = yield* Agent.Service - yield* agents.transform((editor) => - editor.update(Agent.ID.make("reviewer"), (agent) => { - agent.mode = "subagent" - agent.permissions.push({ action: "*", resource: "*", effect: "allow" }) - }), - ) - const skills = yield* Skill.Service - yield* skills.transform((editor) => editor.add(info)) - }).pipe(Effect.provide(locations.get(child.location))) - - yield* model.push(TestLLM.text("Explicit skill received", "explicit")) - yield* sessions.prompt({ - sessionID: child.id, - text: "Use @fixture-guide", - skills: [{ id: info.id, mention: { start: 4, end: 18, text: "@fixture-guide" } }], - resume: false, - }) - yield* execution.resume(child.id) - const explicit = yield* model.requests() - expect(explicit).toHaveLength(1) - expect( - explicit[0].messages.some( - (message) => - message.role === "user" && - message.content.some((part) => part.type === "text" && part.text.includes(info.content)), - ), - ).toBe(true) - - const other = yield* sessions.create({ - parentID: parent.id, - agent: Agent.ID.make("reviewer"), - title: "Tool skill fixture", - }) - yield* model.push( - TestLLM.tool("load-guide", "skill", { id: info.id }), - TestLLM.text("Loaded skill received", "loaded"), - ) - yield* sessions.prompt({ sessionID: other.id, text: "Review using the fixture guide", resume: false }) - yield* execution.resume(other.id) - const requests = (yield* model.requests()).slice(explicit.length) - expect(requests).toHaveLength(2) - expect(requests[0].tools?.some((tool) => tool.name === "skill")).toBe(true) - expect(JSON.stringify(requests[0].system)).toContain(info.id) - expect( - requests[0].messages.some((message) => - message.content.some((part) => part.type === "text" && part.text.includes(info.content)), - ), - ).toBe(false) - expect(JSON.stringify(requests[1].messages.filter((message) => message.role === "tool"))).toContain( - info.content, - ) - }), - ), - ), - ) -}) diff --git a/packages/tui/test/prompt-mentions.test.tsx b/packages/tui/test/prompt-mentions.test.tsx deleted file mode 100644 index 1cff0470fcad..000000000000 --- a/packages/tui/test/prompt-mentions.test.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { expect, test } from "bun:test" -import { createAppFixture } from "./fixture/app" -import { tmpdir } from "./fixture/fixture" -import { directory, json } from "./fixture/tui-client" - -test.each([44, 100])("mention labels keep skills and agents distinct at width %s", async (width) => { - await using state = await tmpdir() - const location = { directory, project: { id: "project", directory, canonical: directory } } - const session = { - id: "ses_mentions", - title: "Mention fixture", - projectID: "project", - location: { directory }, - agent: "build", - model: { providerID: "provider", id: "model" }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: 0, updated: 0 }, - } - const bodies: unknown[] = [] - await using setup = await createAppFixture({ - width, - state: state.path, - config: { animations: false, tabs: { enabled: false }, session: { sidebar: "hide" } }, - args: { sessionID: session.id }, - fetch: async (url, request) => { - if (url.pathname === "/api/agent") - return json({ - location, - data: [ - { id: "build", mode: "primary", hidden: false, permissions: [] }, - { id: "review", mode: "subagent", hidden: false, permissions: [] }, - ], - }) - if (url.pathname === "/api/model") - return json({ location, data: [{ id: "model", providerID: "provider", name: "Model", variants: [] }] }) - if (url.pathname === "/api/provider") return json({ location, data: [{ id: "provider", name: "Provider" }] }) - if (url.pathname === "/api/skill") - return json({ - location, - data: [ - { - id: "review", - name: "Review", - description: "Review guidance with a deliberately long description that must not hide the type label", - }, - ], - }) - if (url.pathname === "/api/fs/find") return json({ location, data: [] }) - if (url.pathname === `/api/session/${session.id}`) return json({ data: session }) - if (/^\/api\/session\/ses_mentions\/(message|inbox|permission)$/.test(url.pathname)) - return json({ data: [], cursor: {} }) - if (url.pathname === `/api/session/${session.id}/prompt`) { - bodies.push(await request.json()) - return new Response(null, { status: 204 }) - } - return undefined - }, - }) - await setup.ready - await setup.waitForFrame((frame) => frame.includes("Model")) - setup.mockInput.pressKey("u", { ctrl: true }) - await setup.mockInput.typeText("@review") - const frame = await setup.waitForFrame((frame) => frame.includes("Skill") && frame.includes("Agent")) - const skill = frame.split("\n").find((line) => line.includes("Skill"))! - expect(skill).toContain("@review") - expect(skill).toContain("Review") - expect(frame.split("\n").find((line) => line.includes("Agent"))).toContain("@review") - if (frame.indexOf("Agent") < frame.indexOf("Skill")) setup.mockInput.pressArrow("down") - setup.mockInput.pressEnter() - await setup.waitForFrame((frame) => !frame.includes("Skill") && frame.includes("@review")) - setup.mockInput.pressEnter() - await setup.waitFor(() => bodies.length === 1) - expect(bodies[0]).toMatchObject({ - text: "@review ", - skills: [{ id: "review", mention: { text: "@review", start: 0, end: 7 } }], - }) -}) From d814fca082425385e0abad640028690753bcfc1a Mon Sep 17 00:00:00 2001 From: Filip Hejmowski Date: Mon, 14 Sep 2026 18:25:45 +0200 Subject: [PATCH 4/5] adjust labels --- packages/tui/src/component/prompt/autocomplete.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 9e67c1d8e419..9957729dac0e 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -44,7 +44,7 @@ export type AutocompleteOption = { path?: string absolute?: string destructive?: { id: string; confirm: string; run: () => void } - kind?: "skill" | "agent" | "file" | "directory" | "reference" + kind?: "skill" | "agent" | "file" | "reference" queueable?: boolean } @@ -376,7 +376,7 @@ export function Autocomplete(props: { const { filename, part } = createFilePart(item, path.join(result.location.directory, item.path), lineRange) return { display: Locale.truncateMiddle(filename, width), - kind: item.type === "directory" ? "directory" : "file", + kind: "file", value: filename, isDirectory: item.type === "directory", path: item.path, @@ -857,11 +857,10 @@ export function Autocomplete(props: { }) const emptyError = createMemo(() => store.visible === "reference" && !files.loading && visibleFiles().failed) const labels = { - skill: "Skill", - agent: "Agent", - file: "File", - directory: "Dir", - reference: "Reference", + skill: "skill", + agent: "agent", + file: "file", + reference: "reference", } return ( From e6748779d23ccef9865bc5615ad1ffa446b6043c Mon Sep 17 00:00:00 2001 From: Filip Hejmowski Date: Mon, 14 Sep 2026 18:58:46 +0200 Subject: [PATCH 5/5] drop labels for paths --- packages/tui/src/component/prompt/autocomplete.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 9957729dac0e..970720572e9f 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -44,7 +44,7 @@ export type AutocompleteOption = { path?: string absolute?: string destructive?: { id: string; confirm: string; run: () => void } - kind?: "skill" | "agent" | "file" | "reference" + kind?: "skill" | "agent" | "reference" queueable?: boolean } @@ -376,7 +376,6 @@ export function Autocomplete(props: { const { filename, part } = createFilePart(item, path.join(result.location.directory, item.path), lineRange) return { display: Locale.truncateMiddle(filename, width), - kind: "file", value: filename, isDirectory: item.type === "directory", path: item.path, @@ -859,7 +858,6 @@ export function Autocomplete(props: { const labels = { skill: "skill", agent: "agent", - file: "file", reference: "reference", }