@fonlan/dsh-task-kanban 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,10 +5,11 @@
5
5
  ## 功能
6
6
 
7
7
  - **需求 → 计划**:新建任务后自动进入细化会话,把需求拆解为串行执行的 phase 计划(plan)。
8
+ - **Skill 驱动的需求细化**:在新建任务的需求输入框开头输入 `/` 会弹出可用的 Skill 列表(与聊天输入框一致,来自宿主 skill 目录;支持方向键 / Enter / Tab 选择),选中或手动输入 `/skill-name`(如 `/grill-me 帮我规划…`)即可指定细化时使用的 Skill。插件会把该 Skill 的完整指令注入细化会话,并切换到交互式细化模式——细化会话是显式会话,agent 会按 Skill 的指引(如 grill-me 的深度追问)与你在会话中交流打磨需求,直到需求明确后再写回计划。未知/不可用的 Skill 会给出明确报错并可重试。
8
9
  - **队列**:已规划的卡片拖入队列,按 FIFO 领取执行。
9
10
  - **串行实现**:每个 phase 一个独立会话,在同一 git worktree 中严格串行推进(同一工作区单并发,跨工作区可按设置并行)。
10
11
  - **自动合并**:全部 phase 完成后,插件自动将 plan 分支合入主分支(`main` / `master`),完成后清理 worktree 与分支。
11
- - **状态可视化**:卡片完整显示标题、需求摘要、模型、plan 状态徽标、所属阶段与时间,支持停止 / 重试 / 删除。
12
+ - **状态可视化**:卡片完整显示标题、需求摘要、模型、细化 Skill、plan 状态徽标、所属阶段与时间,支持停止 / 重试 / 删除。
12
13
 
13
14
  ## 安装
14
15
 
package/lib/client.js CHANGED
@@ -60,13 +60,40 @@ window.__ModuleLoader__.load({
60
60
  if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === void 0) throw new KanbanApiError(parsed?.error?.code ?? "http", parsed?.error?.message ?? `HTTP ${response.status}`);
61
61
  return parsed.value;
62
62
  }
63
+ /**
64
+ * Call the host gateway's `skill.list` unary RPC over the same-origin HTTP
65
+ * bridge (the exact wire shape DSH's chat input uses for its /-autocomplete).
66
+ * The requested `sessionId` scopes the catalog to that session's agent
67
+ * preset, which is where the filesystem skill roots are registered.
68
+ */
69
+ async function gatewaySkillList(sessionId) {
70
+ let response;
71
+ try {
72
+ response = await fetch("/api/skill.list", {
73
+ method: "POST",
74
+ headers: { "content-type": "application/json" },
75
+ body: JSON.stringify({
76
+ type: "client-request",
77
+ rpcId: `task-kanban-${Math.random().toString(36).slice(2)}`,
78
+ method: "skill.list",
79
+ payload: { sessionId }
80
+ })
81
+ });
82
+ } catch (error) {
83
+ throw new KanbanApiError("network", error instanceof Error ? error.message : String(error));
84
+ }
85
+ const parsed = await response.json().catch(() => null);
86
+ if (!response.ok || parsed === null || parsed.type !== "server-response" || parsed.result?.ok !== true) throw new KanbanApiError(parsed?.result?.error?.code ?? "http", parsed?.result?.error?.message ?? `HTTP ${response.status}`);
87
+ return parsed.result.value?.skills ?? [];
88
+ }
63
89
  const api = {
64
90
  list: (workspacePath) => call("list", { workspacePath }),
65
- create: (workspacePath, requirement, model, provider) => call("create", {
91
+ create: (workspacePath, requirement, model, provider, skill) => call("create", {
66
92
  workspacePath,
67
93
  requirement,
68
94
  model,
69
- provider
95
+ provider,
96
+ ...skill !== void 0 && skill !== "" ? { skill } : {}
70
97
  }),
71
98
  move: (cardId, toLane) => call("move", {
72
99
  cardId,
@@ -81,27 +108,55 @@ window.__ModuleLoader__.load({
81
108
  };
82
109
  //#endregion
83
110
  //#region src/client/kanban-state.ts
84
- let client = null;
111
+ /**
112
+ * HMR-safe shared state store keyed on the global symbol registry.
113
+ *
114
+ * The client-hmr hot swap re-evaluates this module (a fresh `lib/client.js`
115
+ * bundle) WITHOUT reloading the page. A module-level singleton would then be
116
+ * duplicated per evaluation: the session-navigation wrapper installed by the
117
+ * OLD module copy keeps calling the OLD copy's `exitBoard`, which reads the
118
+ * OLD copy's `boardOpen`/`boardDisposer` — while a board opened after the swap
119
+ * writes the NEW copy's state cell. The wrapper would then no-op and clicking
120
+ * a sidebar session would never leave the board.
121
+ *
122
+ * Keeping every field on one `globalThis`-backed record (via `Symbol.for`, so
123
+ * all bundle copies resolve the same key) makes every copy read/write the same
124
+ * state cell, so exit works regardless of which module copy performed the
125
+ * enter.
126
+ */
127
+ const STATE_KEY = Symbol.for("@fonlan/dsh-task-kanban/state");
128
+ function getState() {
129
+ const g = globalThis;
130
+ let state = g[STATE_KEY];
131
+ if (state === void 0) {
132
+ state = {
133
+ client: null,
134
+ boardRoot: null,
135
+ boardDisposer: null,
136
+ boardOpen: false,
137
+ listeners: /* @__PURE__ */ new Set()
138
+ };
139
+ g[STATE_KEY] = state;
140
+ }
141
+ return state;
142
+ }
85
143
  function setClient(ctx) {
86
- client = ctx;
144
+ getState().client = ctx;
87
145
  }
88
146
  function getClient() {
89
- return client;
147
+ return getState().client;
90
148
  }
91
- let boardRoot = null;
92
149
  function setBoardRoot(component) {
93
- boardRoot = component;
150
+ getState().boardRoot = component;
94
151
  }
95
- let boardDisposer = null;
96
- let boardOpen = false;
97
- const listeners = /* @__PURE__ */ new Set();
98
152
  function notify() {
99
- for (const l of listeners) l();
153
+ for (const listener of getState().listeners) listener();
100
154
  }
101
155
  function isBoardOpen() {
102
- return boardOpen;
156
+ return getState().boardOpen;
103
157
  }
104
158
  function subscribe(cb) {
159
+ const listeners = getState().listeners;
105
160
  listeners.add(cb);
106
161
  return () => {
107
162
  listeners.delete(cb);
@@ -111,33 +166,35 @@ window.__ModuleLoader__.load({
111
166
  return (0, react.useSyncExternalStore)(subscribe, isBoardOpen);
112
167
  }
113
168
  function enterBoard() {
114
- const ctx = client;
115
- const root = boardRoot;
116
- if (ctx === null || root === null || boardDisposer !== null) return;
169
+ const state = getState();
170
+ const ctx = state.client;
171
+ const root = state.boardRoot;
172
+ if (ctx === null || root === null || state.boardDisposer !== null) return;
117
173
  try {
118
- boardDisposer = ctx.slots.register({
174
+ state.boardDisposer = ctx.slots.register({
119
175
  name: "conversation",
120
176
  priority: -1,
121
177
  locale: "task-kanban"
122
178
  }, root);
123
- boardOpen = true;
179
+ state.boardOpen = true;
124
180
  notify();
125
181
  } catch (error) {
126
182
  console.error("[@fonlan/dsh-task-kanban] cannot open the board:", error);
127
183
  }
128
184
  }
129
185
  function exitBoard() {
130
- if (boardDisposer !== null) {
131
- boardDisposer();
132
- boardDisposer = null;
186
+ const state = getState();
187
+ if (state.boardDisposer !== null) {
188
+ state.boardDisposer();
189
+ state.boardDisposer = null;
133
190
  }
134
- if (boardOpen) {
135
- boardOpen = false;
191
+ if (state.boardOpen) {
192
+ state.boardOpen = false;
136
193
  notify();
137
194
  }
138
195
  }
139
196
  function toggleBoard() {
140
- if (boardOpen) exitBoard();
197
+ if (isBoardOpen()) exitBoard();
141
198
  else enterBoard();
142
199
  }
143
200
  /**
@@ -153,7 +210,9 @@ window.__ModuleLoader__.load({
153
210
  * points covers all of them in one place.
154
211
  *
155
212
  * The wrapper is idempotent: a `kbBound` marker on the wrapper prevents a
156
- * second bind (plugin re-apply / HMR) from stacking another layer.
213
+ * second bind (plugin re-apply / HMR) from stacking another layer. Because the
214
+ * wrapper's exit goes through the shared state store, a wrapper left over from
215
+ * a previous (HMR-replaced) module copy still closes the board correctly.
157
216
  */
158
217
  function bindSessionNavigation(ctx) {
159
218
  const sessions = ctx.sessions;
@@ -217,6 +276,10 @@ window.__ModuleLoader__.load({
217
276
  error: "错误",
218
277
  requirementLabel: "需求文本",
219
278
  requirementPlaceholder: "描述你要实现的需求……",
279
+ skillGestureHint: "开头输入 /skill-name 可指定细化时使用的 Skill(如 /grill-me)",
280
+ skillHint: "细化时将使用 Skill「{skill}」,并可与该会话交流打磨需求",
281
+ skillMenuUserOnly: "仅用户可调",
282
+ skillBadge: "细化 Skill:/{skill}",
220
283
  modelPlaceholder: "(使用默认模型)",
221
284
  noWorkspace: "没有可用工作区,请先在侧边栏添加",
222
285
  refresh: "刷新",
@@ -277,6 +340,10 @@ window.__ModuleLoader__.load({
277
340
  error: "Error",
278
341
  requirementLabel: "Requirement",
279
342
  requirementPlaceholder: "Describe what you want to build…",
343
+ skillGestureHint: "Start with /skill-name to choose the refinement skill (e.g. /grill-me)",
344
+ skillHint: "Refinement will use Skill “{skill}” — talk to this session to refine the requirement",
345
+ skillMenuUserOnly: "User-only",
346
+ skillBadge: "Refine skill: /{skill}",
280
347
  modelPlaceholder: "(use default model)",
281
348
  noWorkspace: "No workspaces available — add one in the sidebar first",
282
349
  refresh: "Refresh",
@@ -302,7 +369,7 @@ window.__ModuleLoader__.load({
302
369
  };
303
370
  //#endregion
304
371
  //#region \0dsh-css:/Users/fonlan/Repos/dsh-task-kanban/src/client/board.css.mjs
305
- const css = "/* @fonlan/dsh-task-kanban board styles (kb- prefixed, plain CSS) */\n.kb-board {\n height: 100%;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n padding: 12px 16px;\n overflow: hidden;\n color: var(--dsh-fg, #e6e6e6);\n background: var(--dsh-bg, #141418);\n font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;\n}\n.kb-empty {\n align-items: center;\n justify-content: center;\n}\n.kb-empty-text { opacity: 0.6; }\n.kb-board-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding-bottom: 10px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.25);\n flex: 0 0 auto;\n}\n.kb-board-actions { display: flex; gap: 8px; align-items: center; flex: none; }\n.kb-board-actions .kb-btn { flex: none; }\n.kb-ws-select {\n max-width: 320px;\n min-width: 0;\n flex: 0 1 auto;\n padding: 6px 10px;\n border-radius: 6px;\n border: 1px solid rgba(128, 128, 128, 0.35);\n background: rgba(128, 128, 128, 0.12);\n color: inherit;\n font-size: 14px;\n}\n.kb-btn {\n padding: 6px 14px;\n border-radius: 6px;\n border: 1px solid rgba(128, 128, 128, 0.35);\n background: rgba(128, 128, 128, 0.12);\n color: inherit;\n font-size: 13px;\n white-space: nowrap;\n cursor: pointer;\n}\n.kb-btn:hover { background: rgba(128, 128, 128, 0.22); }\n.kb-btn:disabled { opacity: 0.5; cursor: default; }\n.kb-btn-primary { background: #3b82f6; border-color: #3b82f6; color: #fff; }\n.kb-btn-primary:hover { background: #2f6fe0; }\n.kb-btn-danger { background: rgba(239, 68, 68, 0.15); border-color: rgba(239, 68, 68, 0.5); color: #f87171; }\n.kb-btn-small { padding: 2px 8px; font-size: 12px; }\n.kb-lanes {\n flex: 1 1 auto;\n display: flex;\n gap: 12px;\n overflow-x: auto;\n overflow-y: hidden;\n padding-top: 10px;\n min-height: 0;\n}\n.kb-lane {\n flex: 1 1 0;\n min-width: 220px;\n max-width: 320px;\n display: flex;\n flex-direction: column;\n background: rgba(128, 128, 128, 0.07);\n border-radius: 8px;\n border: 1px solid rgba(128, 128, 128, 0.12);\n overflow: hidden;\n}\n.kb-lane-drop { border-style: dashed; border-color: rgba(59, 130, 246, 0.45); }\n.kb-lane-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 10px;\n font-weight: 600;\n font-size: 13px;\n color: var(--dsh-fg-soft, #b8b8c0);\n border-bottom: 1px solid rgba(128, 128, 128, 0.12);\n flex: 0 0 auto;\n}\n.kb-lane-count {\n background: rgba(128, 128, 128, 0.18);\n border-radius: 10px;\n padding: 0 8px;\n font-size: 12px;\n}\n.kb-lane-body {\n flex: 1 1 auto;\n overflow-y: auto;\n padding: 8px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.kb-card {\n background: rgba(128, 128, 128, 0.1);\n border: 1px solid rgba(128, 128, 128, 0.2);\n border-radius: 8px;\n padding: 8px 10px;\n cursor: pointer;\n user-select: none;\n}\n.kb-card:hover { border-color: rgba(128, 128, 128, 0.45); }\n.kb-card[draggable='true'] { cursor: grab; }\n.kb-card-running { border-color: rgba(59, 130, 246, 0.6); }\n.kb-card-error { border-color: rgba(239, 68, 68, 0.6); }\n.kb-card-title { font-weight: 600; font-size: 13px; margin-bottom: 6px; word-break: break-all; }\n.kb-card-meta { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; font-size: 12px; }\n.kb-card-badge {\n background: rgba(128, 128, 128, 0.2);\n border-radius: 4px;\n padding: 1px 6px;\n}\n.kb-card-running .kb-card-badge { background: rgba(59, 130, 246, 0.3); }\n.kb-card-error .kb-card-badge { background: rgba(239, 68, 68, 0.3); }\n.kb-card-phase { opacity: 0.8; }\n.kb-card-model { opacity: 0.55; }\n.kb-card-error-text { margin-top: 6px; font-size: 12px; color: #f87171; word-break: break-all; }\n.kb-card-worktree { margin-top: 4px; font-size: 11px; opacity: 0.5; word-break: break-all; }\n.kb-toast {\n position: fixed;\n left: 50%;\n bottom: 24px;\n transform: translateX(-50%);\n background: rgba(30, 30, 36, 0.95);\n border: 1px solid rgba(128, 128, 128, 0.4);\n color: #eee;\n padding: 8px 16px;\n border-radius: 8px;\n font-size: 13px;\n z-index: 2147483000;\n max-width: 70vw;\n}\n.kb-modal-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 2147482900;\n}\n.kb-modal {\n width: 460px;\n max-width: 90vw;\n max-height: 85vh;\n overflow-y: auto;\n background: #1c1c22;\n border: 1px solid rgba(128, 128, 128, 0.35);\n border-radius: 12px;\n padding: 18px;\n color: #e6e6e6;\n}\n.kb-modal-wide { width: 640px; }\n/* Task-detail bottom panel: docks at the bottom of the board page, fills the\n full board width and exactly half of the board height (the lanes column\n above flexes to the remaining space). */\n.kb-detail-panel {\n flex: 0 0 50%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n margin-top: 10px;\n background: #1c1c22;\n border: 1px solid rgba(128, 128, 128, 0.35);\n border-radius: 12px;\n overflow: hidden;\n color: #e6e6e6;\n}\n.kb-detail-panel-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 10px 14px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.2);\n flex: 0 0 auto;\n}\n.kb-detail-panel-head .kb-modal-title { margin-bottom: 0; }\n.kb-detail-panel-body {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n padding: 14px;\n}\n.kb-detail-close {\n flex: none;\n width: 28px;\n height: 28px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 1px solid transparent;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font-size: 15px;\n line-height: 1;\n opacity: 0.75;\n cursor: pointer;\n}\n.kb-detail-close:hover { background: rgba(128, 128, 128, 0.18); opacity: 1; }\n.kb-modal-title { font-size: 16px; font-weight: 700; margin-bottom: 14px; display: flex; align-items: center; gap: 10px; }\n.kb-field { display: block; margin-bottom: 12px; }\n.kb-field > span { display: block; font-size: 12px; opacity: 0.75; margin-bottom: 4px; }\n.kb-field-row { display: flex; align-items: center; gap: 10px; justify-content: space-between; }\n.kb-input {\n width: 100%;\n box-sizing: border-box;\n padding: 7px 10px;\n border-radius: 6px;\n border: 1px solid rgba(128, 128, 128, 0.35);\n background: rgba(128, 128, 128, 0.1);\n color: inherit;\n font-size: 13px;\n}\n.kb-input-number { width: 90px; }\n.kb-textarea { min-height: 110px; resize: vertical; font-family: inherit; }\n.kb-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }\n.kb-detail-section { margin-bottom: 12px; }\n.kb-detail-label { font-size: 12px; opacity: 0.65; margin-bottom: 4px; }\n.kb-detail-text { white-space: pre-wrap; word-break: break-all; font-size: 13px; }\n.kb-detail-error .kb-detail-text { color: #f87171; }\n.kb-detail-status {\n font-size: 12px;\n font-weight: 600;\n border-radius: 4px;\n padding: 1px 8px;\n background: rgba(128, 128, 128, 0.2);\n}\n.kb-status-running, .kb-status-merging { background: rgba(59, 130, 246, 0.3) !important; }\n.kb-status-error { background: rgba(239, 68, 68, 0.3) !important; color: #f87171; }\n.kb-status-merged { background: rgba(34, 197, 94, 0.3) !important; }\n.kb-plan-summary { font-size: 13px; margin-bottom: 10px; }\n.kb-plan-phases { margin: 0; padding-left: 20px; }\n.kb-plan-phase { margin-bottom: 10px; }\n.kb-plan-phase-current .kb-plan-phase-title { color: #60a5fa; }\n.kb-plan-phase-head { display: flex; gap: 8px; align-items: baseline; }\n.kb-plan-phase-title { font-weight: 600; font-size: 13px; }\n.kb-plan-phase-id { font-size: 11px; opacity: 0.5; }\n.kb-plan-phase-goal { font-size: 12px; opacity: 0.85; margin: 2px 0 6px; white-space: pre-wrap; }\n.kb-session-links { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\n.kb-session-attempt { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }\n.kb-session-summary { font-size: 12px; opacity: 0.75; }\n.kb-settings { display: flex; flex-direction: column; gap: 12px; max-width: 420px; }\n.kb-settings-saved { font-size: 12px; color: #4ade80; }\n.kb-footer-action {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n width: 32px;\n height: 32px;\n border-radius: 6px;\n border: none;\n background: transparent;\n /* same foreground as the settings trigger (its trigger css uses this var) */\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n}\n.kb-footer-action:hover { background: rgba(128, 128, 128, 0.18); }\n.kb-footer-action-active { background: rgba(59, 130, 246, 0.25); }\n/* Wide mode borrows the settings trigger's metrics (34px tall, 14px label,\n 18px icon) so both controls read as siblings on the same line. */\n.kb-footer-action-wide {\n width: auto;\n height: 34px;\n padding: 0 8px;\n border-radius: 12px;\n font-size: 14px;\n}\n.kb-footer-action-wide .kb-footer-label {\n font-size: 14px;\n line-height: 22px;\n}\n@media (prefers-color-scheme: light) {\n .kb-board { color: #1c1c22; background: #f5f5f7; }\n .kb-modal { background: #ffffff; color: #1c1c22; }\n .kb-detail-panel { background: #ffffff; color: #1c1c22; }\n}\n/* Same-line placement WITHOUT moving any DOM: the sidebar foot area becomes\n a reversed flex row, so its LAST child (the settings area) renders on the\n LEFT and the FIRST child (footer actions = the kanban button) on the RIGHT\n — exactly \"settings, then kanban\". The settings trigger flexes to fill the\n remaining width; the button keeps its content width. */\n.kb-foot-row {\n display: flex;\n flex-direction: row-reverse;\n align-items: stretch;\n gap: 4px;\n}\n.kb-foot-row > div:first-child {\n flex: none;\n width: auto;\n min-width: 0;\n display: flex;\n align-items: center;\n}\n.kb-foot-row > div:last-child {\n flex: 1;\n min-width: 0;\n}\n/* Rail (collapsed sidebar): column-reverse stacks the settings circle on top\n and the kanban icon below it, both centered; the kanban button borrows the\n settings trigger's rail chrome (36px circle) so the two match. */\n.kb-foot-rail {\n flex-direction: column-reverse;\n align-items: center;\n}\n.kb-foot-rail .kb-footer-action {\n width: 36px;\n height: 36px;\n border-radius: 50%;\n margin: 8px 0 10px;\n}\n.kb-footer-label {\n max-width: 110px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n/* Narrow screens (mobile drawer / auto-collapse breakpoint): the drawer's\n foot area is ~256-280px but the footer actions row carries the kanban\n button PLUS mobile-nav's drawer actions (Files / session log), which\n together exceed the row and squeeze the settings trigger to zero width\n (row-reverse overflows left, clipping the button). Below the shell's\n auto-collapse breakpoint, stack the settings row above the actions and\n let the actions wrap so nothing is squeezed or clipped. */\n@media (max-width: 1023px) {\n .kb-foot-row {\n flex-direction: column-reverse;\n align-items: stretch;\n }\n .kb-foot-row > div:first-child {\n flex: none;\n width: 100%;\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n gap: 4px;\n padding-top: 4px;\n }\n .kb-foot-row > div:last-child {\n flex: none;\n width: 100%;\n }\n /* The footer-actions entries are display:contents wrappers, so the kanban\n button itself is a flex item of the wrapping actions row; pull it to the\n first wrap position so it sits directly below the settings row (the\n mobile-nav drawer actions follow on their own wrapped line). */\n .kb-foot-row .kb-footer-action {\n order: -1;\n }\n}";
372
+ const css = "/* @fonlan/dsh-task-kanban board styles (kb- prefixed, plain CSS).\n Surfaces use the DSH theme tokens (--dsw-alias-*) so they follow the\n shell's selected light/dark appearance; the var() fallbacks cover shells\n that do not define them. */\n.kb-board {\n height: 100%;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n padding: 12px 16px;\n overflow: hidden;\n color: var(--dsw-alias-label-primary, var(--dsh-fg, #e6e6e6));\n background: var(--dsw-alias-bg-base, var(--dsh-bg, #141418));\n font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;\n}\n.kb-empty {\n align-items: center;\n justify-content: center;\n}\n.kb-empty-text { opacity: 0.6; }\n.kb-board-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding-bottom: 10px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.25);\n flex: 0 0 auto;\n}\n.kb-board-actions { display: flex; gap: 8px; align-items: center; flex: none; }\n.kb-board-actions .kb-btn { flex: none; }\n.kb-ws-select {\n max-width: 320px;\n min-width: 0;\n flex: 0 1 auto;\n padding: 6px 10px;\n border-radius: 6px;\n border: 1px solid rgba(128, 128, 128, 0.35);\n background: rgba(128, 128, 128, 0.12);\n color: inherit;\n font-size: 14px;\n}\n.kb-btn {\n padding: 6px 14px;\n border-radius: 6px;\n border: 1px solid rgba(128, 128, 128, 0.35);\n background: rgba(128, 128, 128, 0.12);\n color: inherit;\n font-size: 13px;\n white-space: nowrap;\n cursor: pointer;\n}\n.kb-btn:hover { background: rgba(128, 128, 128, 0.22); }\n.kb-btn:disabled { opacity: 0.5; cursor: default; }\n.kb-btn-primary { background: #3b82f6; border-color: #3b82f6; color: #fff; }\n.kb-btn-primary:hover { background: #2f6fe0; }\n.kb-btn-danger { background: rgba(239, 68, 68, 0.15); border-color: rgba(239, 68, 68, 0.5); color: #f87171; }\n.kb-btn-small { padding: 2px 8px; font-size: 12px; }\n.kb-lanes {\n flex: 1 1 auto;\n display: flex;\n gap: 12px;\n overflow-x: auto;\n overflow-y: hidden;\n padding-top: 10px;\n min-height: 0;\n}\n.kb-lane {\n flex: 1 1 0;\n min-width: 220px;\n max-width: 320px;\n display: flex;\n flex-direction: column;\n background: rgba(128, 128, 128, 0.07);\n border-radius: 8px;\n border: 1px solid rgba(128, 128, 128, 0.12);\n overflow: hidden;\n}\n.kb-lane-drop { border-style: dashed; border-color: rgba(59, 130, 246, 0.45); }\n.kb-lane-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 10px;\n font-weight: 600;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary, var(--dsh-fg-soft, #b8b8c0));\n border-bottom: 1px solid rgba(128, 128, 128, 0.12);\n flex: 0 0 auto;\n}\n.kb-lane-count {\n background: rgba(128, 128, 128, 0.18);\n border-radius: 10px;\n padding: 0 8px;\n font-size: 12px;\n}\n.kb-lane-body {\n flex: 1 1 auto;\n overflow-y: auto;\n padding: 8px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.kb-card {\n background: rgba(128, 128, 128, 0.1);\n border: 1px solid rgba(128, 128, 128, 0.2);\n border-radius: 8px;\n padding: 8px 10px;\n cursor: pointer;\n user-select: none;\n}\n.kb-card:hover { border-color: rgba(128, 128, 128, 0.45); }\n.kb-card[draggable='true'] { cursor: grab; }\n.kb-card-running { border-color: rgba(59, 130, 246, 0.6); }\n.kb-card-error { border-color: rgba(239, 68, 68, 0.6); }\n.kb-card-title { font-weight: 600; font-size: 13px; margin-bottom: 6px; word-break: break-all; }\n.kb-card-meta { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; font-size: 12px; }\n.kb-card-badge {\n background: rgba(128, 128, 128, 0.2);\n border-radius: 4px;\n padding: 1px 6px;\n}\n.kb-card-running .kb-card-badge { background: rgba(59, 130, 246, 0.3); }\n.kb-card-error .kb-card-badge { background: rgba(239, 68, 68, 0.3); }\n.kb-card-phase { opacity: 0.8; }\n.kb-card-skill {\n background: rgba(139, 92, 246, 0.22);\n border-radius: 4px;\n padding: 1px 6px;\n}\n.kb-card-model { opacity: 0.55; }\n.kb-card-error-text { margin-top: 6px; font-size: 12px; color: #f87171; word-break: break-all; }\n.kb-card-worktree { margin-top: 4px; font-size: 11px; opacity: 0.5; word-break: break-all; }\n.kb-toast {\n position: fixed;\n left: 50%;\n bottom: 24px;\n transform: translateX(-50%);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-primary);\n padding: 8px 16px;\n border-radius: 8px;\n font-size: 13px;\n z-index: 2147483000;\n max-width: 70vw;\n}\n.kb-modal-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 2147482900;\n}\n.kb-modal {\n width: 460px;\n max-width: 90vw;\n max-height: 85vh;\n overflow-y: auto;\n background: var(--dsw-alias-bg-layer-1);\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n padding: 18px;\n color: var(--dsw-alias-label-primary);\n}\n.kb-modal-wide { width: 640px; }\n/* Task-detail bottom panel: docks at the bottom of the board page, fills the\n full board width and exactly half of the board height (the lanes column\n above flexes to the remaining space). */\n.kb-detail-panel {\n flex: 0 0 50%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n margin-top: 10px;\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n overflow: hidden;\n color: var(--dsw-alias-label-primary);\n}\n.kb-detail-panel-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 10px 14px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.2);\n flex: 0 0 auto;\n}\n.kb-detail-panel-head .kb-modal-title { margin-bottom: 0; }\n.kb-detail-panel-body {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n padding: 14px;\n}\n.kb-detail-close {\n flex: none;\n width: 28px;\n height: 28px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 1px solid transparent;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font-size: 15px;\n line-height: 1;\n opacity: 0.75;\n cursor: pointer;\n}\n.kb-detail-close:hover { background: rgba(128, 128, 128, 0.18); opacity: 1; }\n.kb-modal-title { font-size: 16px; font-weight: 700; margin-bottom: 14px; display: flex; align-items: center; gap: 10px; }\n.kb-field { display: block; margin-bottom: 12px; }\n.kb-field > span { display: block; font-size: 12px; opacity: 0.75; margin-bottom: 4px; }\n.kb-field-hint {\n display: block;\n font-size: 11px;\n opacity: 0.55;\n margin-top: 4px;\n}\n.kb-field-hint-skill {\n opacity: 0.9;\n color: #a78bfa;\n}\n.kb-skill-menu-wrap { position: relative; }\n.kb-skill-menu {\n position: absolute;\n top: 100%;\n left: 0;\n right: 0;\n z-index: 20;\n margin: 2px 0 0;\n padding: 4px;\n list-style: none;\n max-height: 180px;\n overflow-y: auto;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-overlay);\n color: var(--dsw-alias-label-primary);\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);\n}\n.kb-skill-menu-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px 8px;\n border-radius: 6px;\n cursor: pointer;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n}\n.kb-skill-menu-item-active { background: var(--dsw-alias-interactive-bg-hover); }\n.kb-skill-menu-name { font-weight: 600; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n.kb-skill-menu-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.kb-field-row { display: flex; align-items: center; gap: 10px; justify-content: space-between; }\n.kb-input {\n width: 100%;\n box-sizing: border-box;\n padding: 7px 10px;\n border-radius: 6px;\n border: 1px solid rgba(128, 128, 128, 0.35);\n background: rgba(128, 128, 128, 0.1);\n color: inherit;\n font-size: 13px;\n}\n.kb-input-number { width: 90px; }\n.kb-textarea { min-height: 110px; resize: vertical; font-family: inherit; }\n.kb-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }\n.kb-detail-section { margin-bottom: 12px; }\n.kb-detail-label { font-size: 12px; opacity: 0.65; margin-bottom: 4px; }\n.kb-detail-skill {\n display: inline-block;\n font-size: 11px;\n background: rgba(139, 92, 246, 0.22);\n border-radius: 4px;\n padding: 1px 6px;\n margin-bottom: 4px;\n}\n.kb-detail-text { white-space: pre-wrap; word-break: break-all; font-size: 13px; }\n.kb-detail-error .kb-detail-text { color: #f87171; }\n.kb-detail-status {\n font-size: 12px;\n font-weight: 600;\n border-radius: 4px;\n padding: 1px 8px;\n background: rgba(128, 128, 128, 0.2);\n}\n.kb-status-running, .kb-status-merging { background: rgba(59, 130, 246, 0.3) !important; }\n.kb-status-error { background: rgba(239, 68, 68, 0.3) !important; color: #f87171; }\n.kb-status-merged { background: rgba(34, 197, 94, 0.3) !important; }\n.kb-plan-summary { font-size: 13px; margin-bottom: 10px; }\n.kb-plan-phases { margin: 0; padding-left: 20px; }\n.kb-plan-phase { margin-bottom: 10px; }\n.kb-plan-phase-current .kb-plan-phase-title { color: #60a5fa; }\n.kb-plan-phase-head { display: flex; gap: 8px; align-items: baseline; }\n.kb-plan-phase-title { font-weight: 600; font-size: 13px; }\n.kb-plan-phase-id { font-size: 11px; opacity: 0.5; }\n.kb-plan-phase-goal { font-size: 12px; opacity: 0.85; margin: 2px 0 6px; white-space: pre-wrap; }\n.kb-session-links { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\n.kb-session-attempt { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }\n.kb-session-summary { font-size: 12px; opacity: 0.75; }\n.kb-settings { display: flex; flex-direction: column; gap: 12px; max-width: 420px; }\n.kb-settings-saved { font-size: 12px; color: #4ade80; }\n.kb-footer-action {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n width: 32px;\n height: 32px;\n border-radius: 6px;\n border: none;\n background: transparent;\n /* same foreground as the settings trigger (its trigger css uses this var) */\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n}\n.kb-footer-action:hover { background: rgba(128, 128, 128, 0.18); }\n.kb-footer-action-active { background: rgba(59, 130, 246, 0.25); }\n/* Wide mode borrows the settings trigger's metrics (34px tall, 14px label,\n 18px icon) so both controls read as siblings on the same line. */\n.kb-footer-action-wide {\n width: auto;\n height: 34px;\n padding: 0 8px;\n border-radius: 12px;\n font-size: 14px;\n}\n.kb-footer-action-wide .kb-footer-label {\n font-size: 14px;\n line-height: 22px;\n}\n/* Theme following is handled by the --dsw-alias-* tokens above: they change\n value with the shell's selected light/dark appearance, so no OS-level\n prefers-color-scheme override is needed (and one would fight the app\n theme when the two disagree). */\n/* Same-line placement WITHOUT moving any DOM: the sidebar foot area becomes\n a reversed flex row, so its LAST child (the settings area) renders on the\n LEFT and the FIRST child (footer actions = the kanban button) on the RIGHT\n — exactly \"settings, then kanban\". The settings trigger flexes to fill the\n remaining width; the button keeps its content width. */\n.kb-foot-row {\n display: flex;\n flex-direction: row-reverse;\n align-items: stretch;\n gap: 4px;\n}\n.kb-foot-row > div:first-child {\n flex: none;\n width: auto;\n min-width: 0;\n display: flex;\n align-items: center;\n}\n.kb-foot-row > div:last-child {\n flex: 1;\n min-width: 0;\n}\n/* Rail (collapsed sidebar): column-reverse stacks the settings circle on top\n and the kanban icon below it, both centered; the kanban button borrows the\n settings trigger's rail chrome (36px circle) so the two match. */\n.kb-foot-rail {\n flex-direction: column-reverse;\n align-items: center;\n}\n.kb-foot-rail .kb-footer-action {\n width: 36px;\n height: 36px;\n border-radius: 50%;\n margin: 8px 0 10px;\n}\n.kb-footer-label {\n max-width: 110px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n/* Narrow screens (mobile drawer / auto-collapse breakpoint): the drawer's\n foot area is ~256-280px but the footer actions row carries the kanban\n button PLUS mobile-nav's drawer actions (Files / session log), which\n together exceed the row and squeeze the settings trigger to zero width\n (row-reverse overflows left, clipping the button). Below the shell's\n auto-collapse breakpoint, stack the settings row above the actions and\n let the actions wrap so nothing is squeezed or clipped. */\n@media (max-width: 1023px) {\n .kb-foot-row {\n flex-direction: column-reverse;\n align-items: stretch;\n }\n .kb-foot-row > div:first-child {\n flex: none;\n width: 100%;\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n gap: 4px;\n padding-top: 4px;\n }\n .kb-foot-row > div:last-child {\n flex: none;\n width: 100%;\n }\n /* The footer-actions entries are display:contents wrappers, so the kanban\n button itself is a flex item of the wrapping actions row; pull it to the\n first wrap position so it sits directly below the settings row (the\n mobile-nav drawer actions follow on their own wrapped line). */\n .kb-foot-row .kb-footer-action {\n order: -1;\n }\n}";
306
373
  const tagId = "@fonlan/dsh-task-kanban/board.css";
307
374
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
308
375
  const tag = document.createElement("style");
@@ -316,6 +383,17 @@ window.__ModuleLoader__.load({
316
383
  function firstLine(text) {
317
384
  return text.split("\n").find((l) => l.trim() !== "")?.trim() ?? "";
318
385
  }
386
+ /** The leading `/skill-name` gesture at the start of the requirement input. */
387
+ const SKILL_GESTURE = /^\s*\/([a-z0-9]+(?:-[a-z0-9]+)*)(?=\s|$)/;
388
+ /** Extract a leading `/skill-name` token plus the cleaned remainder. */
389
+ function parseSkillGesture(raw) {
390
+ const m = SKILL_GESTURE.exec(raw);
391
+ if (m === null) return { requirement: raw.trim() };
392
+ return {
393
+ skill: m[1],
394
+ requirement: raw.slice(m[0].length).trim()
395
+ };
396
+ }
319
397
  function statusLabel(card, t) {
320
398
  if (card.status === "error") switch (card.error?.kind) {
321
399
  case "interrupted": return t("interrupted");
@@ -368,6 +446,8 @@ window.__ModuleLoader__.load({
368
446
  });
369
447
  const useWorkspaces = props.useWorkspaces;
370
448
  const items = (useWorkspaces !== void 0 ? useWorkspaces((s) => s) : void 0)?.items ?? [];
449
+ const useSessions = props.useSessions;
450
+ const currentSessionId = (useSessions !== void 0 ? useSessions((s) => s) : void 0)?.current;
371
451
  const [selectedPath, setSelectedPath] = (0, react.useState)(() => {
372
452
  try {
373
453
  return localStorage.getItem(WS_STORAGE_KEY) ?? "";
@@ -521,6 +601,7 @@ window.__ModuleLoader__.load({
521
601
  t,
522
602
  workspaces: items,
523
603
  selectedPath,
604
+ sessionId: currentSessionId,
524
605
  onClose: () => setNewTaskOpen(false),
525
606
  onCreated: () => {
526
607
  setNewTaskOpen(false);
@@ -574,6 +655,10 @@ window.__ModuleLoader__.load({
574
655
  className: "kb-card-phase",
575
656
  children: phaseInfo
576
657
  }),
658
+ card.skill !== void 0 && card.skill !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
659
+ className: "kb-card-skill",
660
+ children: ["/ ", card.skill]
661
+ }),
577
662
  card.model !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
578
663
  className: "kb-card-model",
579
664
  children: card.model
@@ -591,13 +676,19 @@ window.__ModuleLoader__.load({
591
676
  ]
592
677
  });
593
678
  }
594
- function NewTaskModal({ t, workspaces, selectedPath, onClose, onCreated, onError }) {
679
+ /** An in-progress leading `/query` token (menu-open state). */
680
+ const SKILL_MENU_RE = /^\s*\/([a-z0-9-]*)$/;
681
+ function NewTaskModal({ t, workspaces, selectedPath, sessionId, onClose, onCreated, onError }) {
595
682
  const [requirement, setRequirement] = (0, react.useState)("");
596
683
  const [project, setProject] = (0, react.useState)(selectedPath);
597
684
  const [model, setModel] = (0, react.useState)("");
598
685
  const [modelProvider, setModelProvider] = (0, react.useState)("");
599
686
  const [models, setModels] = (0, react.useState)([]);
687
+ const [skills, setSkills] = (0, react.useState)([]);
688
+ const [menuQuery, setMenuQuery] = (0, react.useState)(null);
689
+ const [menuIndex, setMenuIndex] = (0, react.useState)(0);
600
690
  const [busy, setBusy] = (0, react.useState)(false);
691
+ const textareaRef = (0, react.useRef)(null);
601
692
  (0, react.useEffect)(() => {
602
693
  let alive = true;
603
694
  api.models().then((list) => {
@@ -612,6 +703,68 @@ window.__ModuleLoader__.load({
612
703
  alive = false;
613
704
  };
614
705
  }, []);
706
+ (0, react.useEffect)(() => {
707
+ if (sessionId === void 0) return;
708
+ let alive = true;
709
+ gatewaySkillList(sessionId).then((list) => {
710
+ if (alive) setSkills(list);
711
+ }).catch(() => {
712
+ if (alive) setSkills([]);
713
+ });
714
+ return () => {
715
+ alive = false;
716
+ };
717
+ }, [sessionId]);
718
+ const matches = skills.filter((s) => s.name.startsWith(menuQuery ?? ""));
719
+ const menuVisible = menuQuery !== null && matches.length > 0;
720
+ const selected = menuVisible ? matches[Math.min(menuIndex, matches.length - 1)] : void 0;
721
+ /** Re-evaluate the leading-token menu state from the current value + caret. */
722
+ const syncMenu = (value, caret) => {
723
+ const head = value.slice(0, caret);
724
+ const m = SKILL_MENU_RE.exec(head);
725
+ if (m !== null) {
726
+ setMenuQuery(m[1]);
727
+ setMenuIndex(0);
728
+ return;
729
+ }
730
+ setMenuQuery(null);
731
+ };
732
+ const selectSkill = (name) => {
733
+ const caret = textareaRef.current?.selectionStart ?? requirement.length;
734
+ const head = requirement.slice(0, caret);
735
+ const m = SKILL_MENU_RE.exec(head);
736
+ const inserted = `/${name} `;
737
+ const newValue = m !== null ? inserted + requirement.slice(caret) : inserted + requirement;
738
+ setRequirement(newValue);
739
+ setMenuQuery(null);
740
+ requestAnimationFrame(() => {
741
+ const next = textareaRef.current;
742
+ if (next !== null) {
743
+ next.focus();
744
+ const pos = inserted.length;
745
+ next.setSelectionRange(pos, pos);
746
+ }
747
+ });
748
+ };
749
+ const onKeyDown = (e) => {
750
+ if (!menuVisible) return;
751
+ if (e.key === "ArrowDown") {
752
+ e.preventDefault();
753
+ setMenuIndex((i) => (i + 1) % matches.length);
754
+ } else if (e.key === "ArrowUp") {
755
+ e.preventDefault();
756
+ setMenuIndex((i) => (i - 1 + matches.length) % matches.length);
757
+ } else if (e.key === "Enter") {
758
+ e.preventDefault();
759
+ if (selected !== void 0) selectSkill(selected.name);
760
+ } else if (e.key === "Escape") {
761
+ e.preventDefault();
762
+ setMenuQuery(null);
763
+ } else if (e.key === "Tab") {
764
+ e.preventDefault();
765
+ if (selected !== void 0) selectSkill(selected.name);
766
+ }
767
+ };
615
768
  const submit = async () => {
616
769
  if (requirement.trim() === "") {
617
770
  onError(t("requirementLabel") + " " + t("error"));
@@ -621,15 +774,17 @@ window.__ModuleLoader__.load({
621
774
  onError(t("noWorkspace"));
622
775
  return;
623
776
  }
777
+ const parsed = parseSkillGesture(requirement);
624
778
  setBusy(true);
625
779
  try {
626
- await api.create(project, requirement.trim(), model, modelProvider);
780
+ await api.create(project, parsed.requirement, model, modelProvider, parsed.skill);
627
781
  onCreated();
628
782
  } catch (error) {
629
783
  onError(error instanceof Error ? error.message : String(error));
630
784
  setBusy(false);
631
785
  }
632
786
  };
787
+ const activeSkill = parseSkillGesture(requirement).skill;
633
788
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
634
789
  className: "kb-modal-backdrop",
635
790
  onClick: busy ? void 0 : onClose,
@@ -643,12 +798,51 @@ window.__ModuleLoader__.load({
643
798
  }),
644
799
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
645
800
  className: "kb-field",
646
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("requirementLabel") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
647
- className: "kb-input kb-textarea",
648
- value: requirement,
649
- placeholder: t("requirementPlaceholder"),
650
- onChange: (e) => setRequirement(e.target.value)
651
- })]
801
+ children: [
802
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("requirementLabel") }),
803
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
804
+ className: "kb-skill-menu-wrap",
805
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
806
+ ref: textareaRef,
807
+ className: "kb-input kb-textarea",
808
+ value: requirement,
809
+ placeholder: t("requirementPlaceholder"),
810
+ onChange: (e) => {
811
+ const value = e.target.value;
812
+ setRequirement(value);
813
+ syncMenu(value, e.target.selectionStart ?? value.length);
814
+ },
815
+ onKeyDown
816
+ }), menuVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
817
+ className: "kb-skill-menu",
818
+ role: "listbox",
819
+ children: matches.map((s, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
820
+ role: "option",
821
+ "aria-selected": i === menuIndex,
822
+ className: "kb-skill-menu-item" + (i === menuIndex ? " kb-skill-menu-item-active" : ""),
823
+ onMouseDown: (e) => {
824
+ e.preventDefault();
825
+ selectSkill(s.name);
826
+ },
827
+ onMouseEnter: () => setMenuIndex(i),
828
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
829
+ className: "kb-skill-menu-name",
830
+ children: ["/", s.name]
831
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
832
+ className: "kb-skill-menu-desc",
833
+ children: s.modelInvocable ? s.description : `${t("skillMenuUserOnly")} · ${s.description}`
834
+ })]
835
+ }, s.name))
836
+ })]
837
+ }),
838
+ activeSkill !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
839
+ className: "kb-field-hint kb-field-hint-skill",
840
+ children: t("skillHint", { skill: activeSkill })
841
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
842
+ className: "kb-field-hint",
843
+ children: t("skillGestureHint")
844
+ })
845
+ ]
652
846
  }),
653
847
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
654
848
  className: "kb-field",
@@ -744,13 +938,20 @@ window.__ModuleLoader__.load({
744
938
  children: [
745
939
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
746
940
  className: "kb-detail-section",
747
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
748
- className: "kb-detail-label",
749
- children: t("requirement")
750
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
751
- className: "kb-detail-text",
752
- children: card.requirement
753
- })]
941
+ children: [
942
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
943
+ className: "kb-detail-label",
944
+ children: t("requirement")
945
+ }),
946
+ card.skill !== void 0 && card.skill !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
947
+ className: "kb-detail-skill",
948
+ children: t("skillBadge", { skill: card.skill })
949
+ }),
950
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
951
+ className: "kb-detail-text",
952
+ children: card.requirement
953
+ })
954
+ ]
754
955
  }),
755
956
  card.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
756
957
  className: "kb-detail-section",