authorgravatar for 118054752+extoci@users.noreply.github.comExotic <118054752+extoci@users.noreply.github.com> 2026-08-23 21:19:13+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-23 14:19:13-04:00
log9da0fab08e0ee444affdfb4916e294c579158bb6
tree759221c5b3c41836e939db57c59ef49689b9073c
parent55c9093344a5fdbc390d713675429599fff27dbd
signature Signed by PGP key B5690EEEBB952194

feat(web): redesign skills in `$` menu and in `/` menu (#8009)


12 files changed, 219 insertions(+), 71 deletions(-)

apps/desktop/src/settings/DesktopClientSettings.test.ts+1
......@@ -36,6 +36,7 @@ const clientSettings: ClientSettings = {
3636 fontSmoothing: true,
3737 glassOpacity: 80,
3838 planModeEnabled: false,
39 showSkillsInSlashMenu: false,
3940 providerModelPreferences: {},
4041 sidebarAutoSettleAfterDays: 3,
4142 sidebarAutoSettleOnMerge: true,
apps/web/src/components/chat/ChatComposer.tsx+31-23
......@@ -252,7 +252,11 @@ import type { SessionPhase, Thread } from "../../types";
252252import type { PendingUserInputDraftAnswer } from "../../pendingUserInput";
253253import type { PendingApproval, PendingUserInput } from "../../session-logic";
254254import { deriveLatestContextWindowSnapshot } from "../../lib/contextWindow";
255import { formatProviderSkillDisplayName } from "@t3tools/client-runtime/providerSkills";
255import {
256 formatProviderSkillDisplayName,
257 getProviderSlashCommandsForSlashMenu,
258 getProviderSkillsForSlashMenu,
259} from "@t3tools/client-runtime/providerSkills";
256260import { searchProviderSkills } from "../../providerSkillSearch";
257261import { useMediaQuery } from "../../hooks/useMediaQuery";
258262import type { ReviewCommentContext } from "../../reviewCommentContext";
......@@ -1107,30 +1111,33 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
11071111 ] as const)
11081112 : []),
11091113 ] satisfies ReadonlyArray<Extract<ComposerCommandItem, { type: "slash-command" }>>;
1110 const providerSlashCommandItems = (selectedProviderStatus?.slashCommands ?? []).map(
1111 (command) => ({
1112 id: `provider-slash-command:${selectedProvider}:${command.name}`,
1113 type: "provider-slash-command" as const,
1114 provider: selectedProvider,
1115 command,
1116 label: `/${command.name}`,
1117 description: command.description ?? command.input?.hint ?? "Run provider command",
1118 }),
1114 const slashMenuSkills = getProviderSkillsForSlashMenu(
1115 selectedProviderStatus?.skills ?? [],
1116 settings.showSkillsInSlashMenu,
11191117 );
1118 const providerSlashCommandItems = getProviderSlashCommandsForSlashMenu(
1119 selectedProviderStatus?.slashCommands ?? [],
1120 slashMenuSkills,
1121 ).map((command) => ({
1122 id: `provider-slash-command:${selectedProvider}:${command.name}`,
1123 type: "provider-slash-command" as const,
1124 provider: selectedProvider,
1125 command,
1126 label: `/${command.name}`,
1127 description: command.description ?? command.input?.hint ?? "Run provider command",
1128 }));
11201129 const query = composerTrigger.query.trim().toLowerCase();
1121 const skillItems = (selectedProviderStatus?.skills ?? [])
1122 .filter((skill) => skill.enabled)
1123 .map((skill) => ({
1124 id: `skill:${selectedProvider}:${skill.name}`,
1125 type: "skill" as const,
1126 provider: selectedProvider,
1127 skill,
1128 label: `skill:${skill.name}`,
1129 description:
1130 skill.shortDescription ??
1131 skill.description ??
1132 (skill.scope ? `${skill.scope} skill` : ""),
1133 }));
1130 const skillItems = slashMenuSkills.map((skill) => ({
1131 id: `skill:${selectedProvider}:${skill.name}`,
1132 type: "skill" as const,
1133 provider: selectedProvider,
1134 skill,
1135 label: `/skill:${skill.name}`,
1136 description:
1137 skill.shortDescription ??
1138 skill.description ??
1139 (skill.scope ? `${skill.scope} skill` : ""),
1140 }));
11341141 const slashCommandItems = [
11351142 ...builtInSlashCommandItems,
11361143 ...providerSlashCommandItems,
......@@ -1159,6 +1166,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
11591166 planModeUiEnabled,
11601167 selectedProvider,
11611168 selectedProviderStatus,
1169 settings.showSkillsInSlashMenu,
11621170 workspaceEntries.entries,
11631171 ]);
11641172
apps/web/src/components/chat/ComposerCommandMenu.test.tsx+29-14
......@@ -51,10 +51,12 @@ describe("ComposerCommandMenu", () => {
5151 expect(markup).not.toContain("<svg");
5252 expect(markup).toContain("font-sans text-xs font-medium");
5353 expect(markup).not.toContain("font-mono");
54 expect(markup).toContain("text-right");
54 expect(markup).not.toContain("grid-cols-");
55 expect(markup).toContain("max-w-[45%]");
56 expect(markup).toContain("text-left");
5557 });
5658
57 it("renders a skill source icon with an accessible source label", () => {
59 it("renders the skill source icon inside its badge", () => {
5860 const markup = renderToStaticMarkup(
5961 <ComposerCommandMenu
6062 items={[
......@@ -82,40 +84,53 @@ describe("ComposerCommandMenu", () => {
8284 );
8385
8486 expect(markup).toContain("Browser");
85 expect(markup).toContain('<span class="sr-only">App skill</span>');
87 expect(markup).toContain('data-slot="badge"');
88 expect(markup).toContain(">App Skill</span>");
89 expect(markup).toContain("Open and control the in-app browser");
90 expect(markup).toContain("max-w-[48ch]");
91 expect(markup).toContain("text-secondary-label text-xs");
92 expect(markup).toContain("ms-auto");
93 expect(markup).toContain("text-current");
94 expect(markup.indexOf("Open and control the in-app browser")).toBeLessThan(
95 markup.indexOf(">App Skill</span>"),
96 );
8697 expect(markup).toContain("<svg");
87 expect(markup).toContain("text-icon-muted");
98 expect(markup.indexOf('data-slot="badge"')).toBeLessThan(markup.indexOf("<svg"));
8899 });
89100
90 it("renders slash skill results with only the skill prefix dimmed", () => {
101 it("keeps slash skills aligned with the source icon inside the badge", () => {
91102 const markup = renderToStaticMarkup(
92103 <ComposerCommandMenu
93104 items={[
94105 {
95 id: "skill:codex:browser",
106 id: "skill:codex:ask-matt",
96107 type: "skill",
97108 provider: ProviderDriverKind.make("codex"),
98109 skill: {
99 name: "browser",
100 path: "/skills/browser/SKILL.md",
110 name: "ask-matt",
111 displayName: "Ask Matt",
112 path: "/skills/ask-matt/SKILL.md",
113 scope: "repo",
101114 enabled: true,
102115 },
103 label: "skill:browser",
104 description: "Open and control the in-app browser",
116 label: "/skill:ask-matt",
117 description: "Find the right skill or workflow",
105118 },
106119 ]}
107120 resolvedTheme="dark"
108121 isLoading={false}
109122 triggerKind="slash-command"
110 activeItemId="skill:codex:browser"
123 activeItemId="skill:codex:ask-matt"
111124 onHighlightedItemChange={() => {}}
112125 onSelect={() => {}}
113126 />,
114127 );
115128
116 expect(markup).toContain('<span class="text-secondary-label">skill:</span>browser');
117 expect(markup).toContain("Open and control the in-app browser");
129 expect(markup).toContain('<span class="text-secondary-label">/skill:</span>Ask Matt');
130 expect(markup).toContain('data-slot="badge"');
131 expect(markup).toContain("lucide-folder");
132 expect(markup).toContain(">Repo</span>");
133 expect(markup).toContain("Find the right skill or workflow");
118134 expect(markup).not.toContain("font-medium text-secondary-label");
119 expect(markup).not.toContain("<svg");
120135 });
121136});
apps/web/src/components/chat/ComposerCommandMenu.tsx+23-17
......@@ -1,4 +1,5 @@
11import {
2 formatProviderSkillDisplayName,
23 resolveProviderSkillSourceKind,
34 type ProviderSkillSourceKind,
45} from "@t3tools/client-runtime/providerSkills";
......@@ -10,7 +11,6 @@ import {
1011} from "@t3tools/contracts";
1112import {
1213 BlocksIcon,
13 FolderGit2Icon,
1414 FolderIcon,
1515 PackageIcon,
1616 SettingsIcon,
......@@ -21,6 +21,7 @@ import { memo, useLayoutEffect, useRef } from "react";
2121
2222import { type ComposerSlashCommand, type ComposerTriggerKind } from "../../composer-logic";
2323import { cn } from "~/lib/utils";
24import { Badge } from "../ui/badge";
2425import { Command, CommandGroup, CommandItem, CommandList } from "../ui/command";
2526import { PierreEntryIcon } from "./PierreEntryIcon";
2627
......@@ -139,7 +140,7 @@ const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: {
139140}) {
140141 const skillSourceKind =
141142 props.item.type === "skill" ? resolveProviderSkillSourceKind(props.item.skill) : null;
142 const slashSkill =
143 const isSlashSkill =
143144 props.triggerKind === "slash-command" && props.item.type === "skill" ? props.item.skill : null;
144145
145146 return (
......@@ -166,23 +167,27 @@ const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: {
166167 kind={props.item.pathKind}
167168 theme={props.resolvedTheme}
168169 />
169 ) : skillSourceKind && !slashSkill ? (
170 <SkillSourceIcon kind={skillSourceKind} />
171170 ) : null}
172 <span className="flex min-w-0 flex-1 items-baseline gap-3">
173 <span className="shrink-0 font-sans text-xs font-medium">
174 {slashSkill ? (
171 <span className="flex min-w-0 flex-1 items-center gap-2">
172 <span className="min-w-0 max-w-[45%] shrink-0 truncate font-sans text-xs font-medium">
173 {isSlashSkill ? (
175174 <>
176 <span className="text-secondary-label">skill:</span>
177 {slashSkill.name}
175 <span className="text-secondary-label">/skill:</span>
176 {formatProviderSkillDisplayName(isSlashSkill)}
178177 </>
179178 ) : (
180179 props.item.label
181180 )}
182181 </span>
183 <span className="min-w-0 flex-1 truncate text-right text-secondary-label text-xs">
182 <span className="min-w-0 max-w-[48ch] flex-1 truncate text-left text-secondary-label text-xs">
184183 {props.item.description}
185184 </span>
185 {skillSourceKind ? (
186 <SkillSourceBadge
187 kind={skillSourceKind}
188 showSkillSuffix={props.triggerKind === "skill"}
189 />
190 ) : null}
186191 </span>
187192 </CommandItem>
188193 );
......@@ -190,7 +195,7 @@ const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: {
190195
191196const SKILL_SOURCE_ICON_BY_KIND: Record<ProviderSkillSourceKind, LucideIcon> = {
192197 app: BlocksIcon,
193 repo: FolderGit2Icon,
198 repo: FolderIcon,
194199 project: FolderIcon,
195200 personal: UserRoundIcon,
196201 system: SettingsIcon,
......@@ -203,15 +208,16 @@ const SKILL_SOURCE_LABEL_BY_KIND: Record<ProviderSkillSourceKind, string> = {
203208 project: "Project",
204209 personal: "Personal",
205210 system: "System",
206 other: "Other",
211 other: "Provider",
207212};
208213
209function SkillSourceIcon(props: { kind: ProviderSkillSourceKind }) {
214function SkillSourceBadge(props: { kind: ProviderSkillSourceKind; showSkillSuffix: boolean }) {
210215 const Icon = SKILL_SOURCE_ICON_BY_KIND[props.kind];
211216 return (
212 <>
213 <Icon aria-hidden="true" className="size-4 shrink-0 text-icon-muted" />
214 <span className="sr-only">{SKILL_SOURCE_LABEL_BY_KIND[props.kind]} skill</span>
215 </>
217 <Badge className="ms-auto" variant="secondary">
218 <Icon aria-hidden="true" className="text-current" />
219 {SKILL_SOURCE_LABEL_BY_KIND[props.kind]}
220 {props.showSkillSuffix ? " Skill" : null}
221 </Badge>
216222 );
217223}
apps/web/src/components/chat/composerSlashCommandSearch.test.ts+18-13
......@@ -81,7 +81,7 @@ describe("searchSlashCommandItems", () => {
8181 enabled: true,
8282 shortDescription: "Open and control the in-app browser",
8383 },
84 label: "skill:browser",
84 label: "/skill:browser",
8585 description: "Open and control the in-app browser",
8686 },
8787 ] satisfies Array<Extract<ComposerCommandItem, { type: "skill" }>>;
......@@ -97,23 +97,26 @@ describe("searchSlashCommandItems", () => {
9797 it("matches skills by display name", () => {
9898 const items = [
9999 {
100 id: "skill:claudeAgent:browser",
100 id: "skill:claudeAgent:ask-matt",
101101 type: "skill",
102102 provider: claudeDriver,
103103 skill: {
104 name: "browser",
105 displayName: "Web Navigator",
106 path: "/skills/browser/SKILL.md",
104 name: "ask-matt",
105 displayName: "Ask Matt",
106 path: "/skills/ask-matt/SKILL.md",
107107 enabled: true,
108 shortDescription: "Open and control the in-app browser",
108 shortDescription: "Find the right skill or workflow",
109109 },
110 label: "skill:browser",
111 description: "Open and control the in-app browser",
110 label: "/skill:ask-matt",
111 description: "Find the right skill or workflow",
112112 },
113113 ] satisfies Array<Extract<ComposerCommandItem, { type: "skill" }>>;
114114
115 expect(searchSlashCommandItems(items, "navigator").map((item) => item.id)).toEqual([
116 "skill:claudeAgent:browser",
115 expect(searchSlashCommandItems(items, "ask matt").map((item) => item.id)).toEqual([
116 "skill:claudeAgent:ask-matt",
117 ]);
118 expect(searchSlashCommandItems(items, "/skill:ask-matt").map((item) => item.id)).toEqual([
119 "skill:claudeAgent:ask-matt",
117120 ]);
118121 });
119122
......@@ -128,7 +131,7 @@ describe("searchSlashCommandItems", () => {
128131 path: "/skills/browser/SKILL.md",
129132 enabled: true,
130133 },
131 label: "skill:browser",
134 label: "/skill:browser",
132135 description: "Open and control the in-app browser",
133136 },
134137 ] satisfies Array<Extract<ComposerCommandItem, { type: "skill" }>>;
......@@ -136,7 +139,9 @@ describe("searchSlashCommandItems", () => {
136139 expect(searchSlashCommandItems(items, "/skill:brow").map((item) => item.id)).toEqual([
137140 "skill:claudeAgent:browser",
138141 ]);
139 expect(searchSlashCommandItems(items, "/sk")).toEqual([]);
142 expect(searchSlashCommandItems(items, "/sk").map((item) => item.id)).toEqual([
143 "skill:claudeAgent:browser",
144 ]);
140145 expect(searchSlashCommandItems(items, "/ill")).toEqual([]);
141146 });
142147
......@@ -158,7 +163,7 @@ describe("searchSlashCommandItems", () => {
158163 path: "/skills/unslop/SKILL.md",
159164 enabled: true,
160165 },
161 label: "skill:unslop",
166 label: "/skill:unslop",
162167 description: "Cut AI tells from writing",
163168 },
164169 ] satisfies Array<Extract<ComposerCommandItem, { type: "slash-command" | "skill" }>>;
apps/web/src/components/chat/composerSlashCommandSearch.ts+9-3
......@@ -14,9 +14,15 @@ type SlashSearchItem = Extract<
1414
1515function scoreSlashCommandItem(item: SlashSearchItem, query: string): number | null {
1616 if (item.type === "skill") {
17 const skillQuery =
18 query === "skill" ? "" : query.startsWith("skill:") ? query.slice("skill:".length) : query;
19 return skillQuery ? scoreProviderSkill(item.skill, skillQuery) : 0;
17 if (query === "skill") {
18 return 0;
19 }
20 const skillQuery = query.startsWith("skill:") ? query.slice("skill:".length) : query;
21 const skillScore = skillQuery ? scoreProviderSkill(item.skill, skillQuery) : 0;
22 if (skillScore !== null) {
23 return skillScore;
24 }
25 return "skill".startsWith(query) ? Number.MAX_SAFE_INTEGER : null;
2026 }
2127
2228 const primaryValue =
apps/web/src/components/settings/SettingsPanels.tsx+31
......@@ -507,6 +507,9 @@ export function useSettingsRestore(onRestored?: () => void) {
507507 ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace
508508 ? ["Diff whitespace changes"]
509509 : []),
510 ...(settings.showSkillsInSlashMenu !== DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu
511 ? ["Show skills in slash menu"]
512 : []),
510513 ...(settings.enableLegacyTokenStreaming !==
511514 DEFAULT_UNIFIED_SETTINGS.enableLegacyTokenStreaming
512515 ? ["Stream token by token"]
......@@ -573,6 +576,7 @@ export function useSettingsRestore(onRestored?: () => void) {
573576 settings.sidebarAutoSettleOnMerge,
574577 settings.sidebarProjectGroupingMode,
575578 settings.sidebarThreadPreviewCount,
579 settings.showSkillsInSlashMenu,
576580 settings.timestampFormat,
577581 settings.wordWrap,
578582 followSystem,
......@@ -648,6 +652,7 @@ export function useSettingsRestore(onRestored?: () => void) {
648652 timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,
649653 wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
650654 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
655 showSkillsInSlashMenu: DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu,
651656 environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode,
652657 glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,
653658 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
......@@ -2076,6 +2081,32 @@ export function GeneralSettingsPanel() {
20762081 }
20772082 />
20782083
2084 <SettingsRow
2085 {...searchableSetting("skills-in-slash-menu")}
2086 description="Also include skills in the / command menu. Skills always appear when you type $."
2087 resetAction={
2088 settings.showSkillsInSlashMenu !== DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu ? (
2089 <SettingResetButton
2090 label="skills in slash menu"
2091 onClick={() =>
2092 updateSettings({
2093 showSkillsInSlashMenu: DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu,
2094 })
2095 }
2096 />
2097 ) : null
2098 }
2099 control={
2100 <Switch
2101 checked={settings.showSkillsInSlashMenu}
2102 onCheckedChange={(checked) =>
2103 updateSettings({ showSkillsInSlashMenu: Boolean(checked) })
2104 }
2105 aria-label="Show skills in slash menu"
2106 />
2107 }
2108 />
2109
20792110 <SettingsRow
20802111 {...searchableSetting("provider-update-checks")}
20812112 description="Check installed provider CLIs for newer available versions."
apps/web/src/components/settings/settingsSearch.ts+5
......@@ -131,6 +131,11 @@ export const SETTINGS_SEARCH_ITEMS = [
131131 title: "Hide whitespace changes",
132132 to: "/settings/general",
133133 },
134 {
135 id: "skills-in-slash-menu",
136 title: "Show skills in slash menu",
137 to: "/settings/general",
138 },
134139 {
135140 id: "provider-update-checks",
136141 title: "Provider update checks",
docs/user/composer.md+11
......@@ -4,6 +4,17 @@ Messages can contain up to 120,000 characters. If a draft is longer, T3 Code kee
44composer and shows how many characters need to be removed. Shorten the draft or split it into
55multiple messages, then send again in the same thread.
66
7## Commands and skills
8
9Type `/` to open the command menu. Type `$` to find and add a skill. Skill rows show their source,
10such as System, Personal, Project, or App.
11
12By default, the `/` menu includes skills. To keep this menu command-only, turn off **Show skills in
13slash menu** in **Settings → General**. Skill results use the `/skill:Skill Name` label and add the
14same `$name` skill token to your message. The original skill name remains searchable. If the provider
15also reports that skill as a native slash command, T3 Code hides the duplicate native entry and keeps
16the `/skill:Skill Name` label.
17
718On desktop, press `Cmd+Enter` on macOS or `Ctrl+Enter` on Windows and Linux from a new thread to
819start it in the background. T3 Code opens another new thread and shows an **Open** action for the
920thread that started. The new thread keeps the selected workspace mode and base branch. If **New
packages/client-runtime/src/providerSkills.test.ts+43
......@@ -2,6 +2,8 @@ import { describe, expect, it } from "vite-plus/test";
22
33import {
44 formatProviderSkillDisplayName,
5 getProviderSlashCommandsForSlashMenu,
6 getProviderSkillsForSlashMenu,
57 resolveProviderSkillSourceKind,
68} from "./providerSkills.ts";
79
......@@ -24,6 +26,47 @@ describe("formatProviderSkillDisplayName", () => {
2426 });
2527});
2628
29describe("getProviderSkillsForSlashMenu", () => {
30 it("keeps the skill alias when the provider also exposes it as a slash command", () => {
31 const askMatt = {
32 name: "ask-matt",
33 path: "/Users/matt/.agents/skills/ask-matt/SKILL.md",
34 enabled: true,
35 };
36 expect(getProviderSkillsForSlashMenu([askMatt], true).map((skill) => skill.name)).toEqual([
37 "ask-matt",
38 ]);
39 });
40});
41
42describe("getProviderSlashCommandsForSlashMenu", () => {
43 const commands = [
44 { name: "ask-matt", description: "Ask which skill fits your situation." },
45 { name: "compact", description: "Compact the conversation." },
46 ];
47 const skills = [
48 {
49 name: "ask-matt",
50 path: "/Users/matt/.agents/skills/ask-matt/SKILL.md",
51 enabled: true,
52 },
53 ];
54
55 it("lets the skill alias win when a provider command has the same name", () => {
56 expect(
57 getProviderSlashCommandsForSlashMenu(commands, skills).map((command) => command.name),
58 ).toEqual(["compact"]);
59 });
60
61 it("keeps the provider command when the matching skill alias is hidden", () => {
62 const visibleSkills = getProviderSkillsForSlashMenu(skills, false);
63
64 expect(
65 getProviderSlashCommandsForSlashMenu(commands, visibleSkills).map((command) => command.name),
66 ).toEqual(["ask-matt", "compact"]);
67 });
68});
69
2770describe("resolveProviderSkillSourceKind", () => {
2871 it("marks plugin-backed skills as app installs", () => {
2972 expect(
packages/client-runtime/src/providerSkills.ts+16-1
......@@ -1,4 +1,4 @@
1import type { ServerProviderSkill } from "@t3tools/contracts";
1import type { ServerProviderSkill, ServerProviderSlashCommand } from "@t3tools/contracts";
22
33export type ProviderSkillSourceKind = "app" | "repo" | "project" | "personal" | "system" | "other";
44
......@@ -25,6 +25,21 @@ export function formatProviderSkillDisplayName(
2525 return titleCaseWords(skill.name);
2626}
2727
28export function getProviderSkillsForSlashMenu(
29 skills: ReadonlyArray<ServerProviderSkill>,
30 showSkillsInSlashMenu: boolean,
31): ServerProviderSkill[] {
32 return showSkillsInSlashMenu ? skills.filter((skill) => skill.enabled) : [];
33}
34
35export function getProviderSlashCommandsForSlashMenu(
36 slashCommands: ReadonlyArray<ServerProviderSlashCommand>,
37 visibleSkills: ReadonlyArray<ServerProviderSkill>,
38): ServerProviderSlashCommand[] {
39 const skillNames = new Set(visibleSkills.map((skill) => skill.name.trim().toLowerCase()));
40 return slashCommands.filter((command) => !skillNames.has(command.name.trim().toLowerCase()));
41}
42
2843export function resolveProviderSkillSourceKind(
2944 skill: Pick<ServerProviderSkill, "path" | "scope">,
3045): ProviderSkillSourceKind {
packages/contracts/src/settings.ts+2
......@@ -225,6 +225,7 @@ export const ClientSettingsSchema = Schema.Struct({
225225 // default UI; this beta flag restores it (plus the /plan and /default slash
226226 // commands) for users who still rely on the old workflow.
227227 planModeEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
228 showSkillsInSlashMenu: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
228229 // Legacy sidebar (the original per-project tree). Deliberately a fresh key
229230 // (was `sidebarV2Enabled` + `sidebarV2ConfiguredByUser`): decoding drops the
230231 // old keys, so everyone, including prior beta opt-outs, resets to the new
......@@ -913,6 +914,7 @@ export const ClientSettingsPatch = Schema.Struct({
913914 ),
914915 ),
915916 planModeEnabled: Schema.optionalKey(Schema.Boolean),
917 showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean),
916918 legacySidebarEnabled: Schema.optionalKey(Schema.Boolean),
917919 sidebarAutoSettleAfterDays: Schema.optionalKey(Schema.NullOr(SidebarAutoSettleAfterDays)),
918920 sidebarAutoSettleOnMerge: Schema.optionalKey(Schema.Boolean),