@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 +2 -1
- package/lib/client.js +241 -40
- package/lib/client.js.map +1 -1
- package/lib/index.js +193 -8
- package/lib/types/server/prompts.d.ts +7 -0
- package/lib/types/server/runner.d.ts +23 -0
- package/lib/types/server/skills.d.ts +71 -0
- package/lib/types/server/task-store.d.ts +1 -0
- package/lib/types/shared/card.d.ts +2 -0
- package/package.json +6 -1
- package/src/client/api.ts +43 -2
- package/src/client/board.css +80 -18
- package/src/client/board.tsx +145 -10
- package/src/client/kanban-state.ts +65 -23
- package/src/client/locales.ts +8 -0
- package/src/index.ts +1 -1
- package/src/server/prompts.ts +24 -0
- package/src/server/rpc.ts +2 -1
- package/src/server/runner.ts +114 -9
- package/src/server/skills.ts +105 -0
- package/src/server/task-store.ts +2 -0
- package/src/shared/card.ts +2 -0
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
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
|
115
|
-
const
|
|
116
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
boardDisposer
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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: [
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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: [
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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",
|