@fonlan/dsh-task-kanban 0.1.0 → 0.2.0
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 +330 -93
- package/lib/client.js.map +1 -1
- package/lib/index.js +271 -13
- package/lib/types/server/prompts.d.ts +7 -0
- package/lib/types/server/runner.d.ts +44 -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 +126 -28
- package/src/client/board.tsx +210 -32
- package/src/client/kanban-state.ts +65 -23
- package/src/client/locales.ts +16 -2
- 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 +214 -14
- package/src/server/skills.ts +105 -0
- package/src/server/task-store.ts +2 -0
- package/src/shared/card.ts +2 -0
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;
|
|
@@ -214,13 +273,20 @@ window.__ModuleLoader__.load({
|
|
|
214
273
|
plan: "实现方案",
|
|
215
274
|
summary: "摘要",
|
|
216
275
|
phases: "阶段",
|
|
276
|
+
phaseGoal: "目标",
|
|
277
|
+
phaseSessions: "会话",
|
|
278
|
+
phaseConclusion: "结论",
|
|
217
279
|
error: "错误",
|
|
218
280
|
requirementLabel: "需求文本",
|
|
219
281
|
requirementPlaceholder: "描述你要实现的需求……",
|
|
282
|
+
skillGestureHint: "开头输入 /skill-name 可指定细化时使用的 Skill(如 /grill-me)",
|
|
283
|
+
skillHint: "细化时将使用 Skill「{skill}」,并可与该会话交流打磨需求",
|
|
284
|
+
skillMenuUserOnly: "仅用户可调",
|
|
285
|
+
skillBadge: "细化 Skill:/{skill}",
|
|
220
286
|
modelPlaceholder: "(使用默认模型)",
|
|
221
287
|
noWorkspace: "没有可用工作区,请先在侧边栏添加",
|
|
222
288
|
refresh: "刷新",
|
|
223
|
-
settingsTitle: "
|
|
289
|
+
settingsTitle: "任务看板",
|
|
224
290
|
maxParallelWorkers: "全局并行 worker 数",
|
|
225
291
|
defaultModel: "默认模型",
|
|
226
292
|
saved: "已保存",
|
|
@@ -274,13 +340,20 @@ window.__ModuleLoader__.load({
|
|
|
274
340
|
plan: "Plan",
|
|
275
341
|
summary: "Summary",
|
|
276
342
|
phases: "Phases",
|
|
343
|
+
phaseGoal: "Goal",
|
|
344
|
+
phaseSessions: "Sessions",
|
|
345
|
+
phaseConclusion: "Conclusion",
|
|
277
346
|
error: "Error",
|
|
278
347
|
requirementLabel: "Requirement",
|
|
279
348
|
requirementPlaceholder: "Describe what you want to build…",
|
|
349
|
+
skillGestureHint: "Start with /skill-name to choose the refinement skill (e.g. /grill-me)",
|
|
350
|
+
skillHint: "Refinement will use Skill “{skill}” — talk to this session to refine the requirement",
|
|
351
|
+
skillMenuUserOnly: "User-only",
|
|
352
|
+
skillBadge: "Refine skill: /{skill}",
|
|
280
353
|
modelPlaceholder: "(use default model)",
|
|
281
354
|
noWorkspace: "No workspaces available — add one in the sidebar first",
|
|
282
355
|
refresh: "Refresh",
|
|
283
|
-
settingsTitle: "Task Kanban
|
|
356
|
+
settingsTitle: "Task Kanban",
|
|
284
357
|
maxParallelWorkers: "Global parallel workers",
|
|
285
358
|
defaultModel: "Default model",
|
|
286
359
|
saved: "Saved",
|
|
@@ -302,7 +375,7 @@ window.__ModuleLoader__.load({
|
|
|
302
375
|
};
|
|
303
376
|
//#endregion
|
|
304
377
|
//#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}";
|
|
378
|
+
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 gap: 12px;\n padding-bottom: 10px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.25);\n flex: 0 0 auto;\n}\n/* Left-aligned group: workspace selector + new-task button sit adjacent. */\n.kb-board-title {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n flex: 0 1 auto;\n}\n.kb-board-title .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/* Phase cards: each plan phase is a card with an ordered number (kept from the\n parent <ol>) and three labeled sections (goal / sessions / conclusion) split\n by section titles and thin divider lines. */\n.kb-plan-phases { margin: 0; padding-left: 24px; }\n.kb-plan-phase {\n margin-bottom: 10px;\n padding: 8px 10px;\n background: rgba(128, 128, 128, 0.08);\n border: 1px solid rgba(128, 128, 128, 0.2);\n border-radius: 8px;\n}\n.kb-plan-phase-head {\n display: flex;\n gap: 8px;\n align-items: baseline;\n padding-bottom: 6px;\n margin-bottom: 6px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.16);\n}\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-body { display: flex; flex-direction: column; }\n/* Section isolation: label on top, thin divider between adjacent sections. */\n.kb-plan-phase-section { display: flex; flex-direction: column; }\n.kb-plan-phase-section + .kb-plan-phase-section {\n margin-top: 6px;\n padding-top: 6px;\n border-top: 1px solid rgba(128, 128, 128, 0.14);\n}\n.kb-plan-phase-section-title { font-size: 11px; opacity: 0.55; margin-bottom: 3px; }\n.kb-plan-phase-goal { font-size: 12px; opacity: 0.85; white-space: pre-wrap; word-break: break-all; }\n.kb-plan-phase-sessions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\n.kb-plan-phase-conclusion { font-size: 12px; opacity: 0.85; white-space: pre-wrap; word-break: break-all; }\n.kb-plan-phase-conclusion-item + .kb-plan-phase-conclusion-item { margin-top: 6px; }\n/* Running phase keeps the blue highlight; completed phases get success color. */\n.kb-plan-phase-current { border-color: rgba(59, 130, 246, 0.6); background: rgba(59, 130, 246, 0.08); }\n.kb-plan-phase-current .kb-plan-phase-title { color: #60a5fa; }\n.kb-plan-phase-completed { border-color: rgba(34, 197, 94, 0.45); background: rgba(34, 197, 94, 0.06); }\n.kb-plan-phase-completed .kb-plan-phase-title { color: #4ade80; }\n.kb-session-links { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\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
379
|
const tagId = "@fonlan/dsh-task-kanban/board.css";
|
|
307
380
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
308
381
|
const tag = document.createElement("style");
|
|
@@ -316,6 +389,17 @@ window.__ModuleLoader__.load({
|
|
|
316
389
|
function firstLine(text) {
|
|
317
390
|
return text.split("\n").find((l) => l.trim() !== "")?.trim() ?? "";
|
|
318
391
|
}
|
|
392
|
+
/** The leading `/skill-name` gesture at the start of the requirement input. */
|
|
393
|
+
const SKILL_GESTURE = /^\s*\/([a-z0-9]+(?:-[a-z0-9]+)*)(?=\s|$)/;
|
|
394
|
+
/** Extract a leading `/skill-name` token plus the cleaned remainder. */
|
|
395
|
+
function parseSkillGesture(raw) {
|
|
396
|
+
const m = SKILL_GESTURE.exec(raw);
|
|
397
|
+
if (m === null) return { requirement: raw.trim() };
|
|
398
|
+
return {
|
|
399
|
+
skill: m[1],
|
|
400
|
+
requirement: raw.slice(m[0].length).trim()
|
|
401
|
+
};
|
|
402
|
+
}
|
|
319
403
|
function statusLabel(card, t) {
|
|
320
404
|
if (card.status === "error") switch (card.error?.kind) {
|
|
321
405
|
case "interrupted": return t("interrupted");
|
|
@@ -368,6 +452,8 @@ window.__ModuleLoader__.load({
|
|
|
368
452
|
});
|
|
369
453
|
const useWorkspaces = props.useWorkspaces;
|
|
370
454
|
const items = (useWorkspaces !== void 0 ? useWorkspaces((s) => s) : void 0)?.items ?? [];
|
|
455
|
+
const useSessions = props.useSessions;
|
|
456
|
+
const currentSessionId = (useSessions !== void 0 ? useSessions((s) => s) : void 0)?.current;
|
|
371
457
|
const [selectedPath, setSelectedPath] = (0, react.useState)(() => {
|
|
372
458
|
try {
|
|
373
459
|
return localStorage.getItem(WS_STORAGE_KEY) ?? "";
|
|
@@ -456,11 +542,11 @@ window.__ModuleLoader__.load({
|
|
|
456
542
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
457
543
|
className: "kb-board",
|
|
458
544
|
children: [
|
|
459
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
545
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
460
546
|
className: "kb-board-header",
|
|
461
|
-
children:
|
|
547
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
462
548
|
className: "kb-board-title",
|
|
463
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
549
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
464
550
|
className: "kb-ws-select",
|
|
465
551
|
value: selectedPath,
|
|
466
552
|
onChange: (e) => {
|
|
@@ -473,16 +559,13 @@ window.__ModuleLoader__.load({
|
|
|
473
559
|
value: w.path,
|
|
474
560
|
children: w.title
|
|
475
561
|
}, w.workspaceId))
|
|
476
|
-
})
|
|
477
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
478
|
-
className: "kb-board-actions",
|
|
479
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
562
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
480
563
|
type: "button",
|
|
481
564
|
className: "kb-btn kb-btn-primary",
|
|
482
565
|
onClick: () => setNewTaskOpen(true),
|
|
483
566
|
children: t("newTask")
|
|
484
|
-
})
|
|
485
|
-
})
|
|
567
|
+
})]
|
|
568
|
+
})
|
|
486
569
|
}),
|
|
487
570
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
488
571
|
className: "kb-lanes",
|
|
@@ -521,6 +604,7 @@ window.__ModuleLoader__.load({
|
|
|
521
604
|
t,
|
|
522
605
|
workspaces: items,
|
|
523
606
|
selectedPath,
|
|
607
|
+
sessionId: currentSessionId,
|
|
524
608
|
onClose: () => setNewTaskOpen(false),
|
|
525
609
|
onCreated: () => {
|
|
526
610
|
setNewTaskOpen(false);
|
|
@@ -574,6 +658,10 @@ window.__ModuleLoader__.load({
|
|
|
574
658
|
className: "kb-card-phase",
|
|
575
659
|
children: phaseInfo
|
|
576
660
|
}),
|
|
661
|
+
card.skill !== void 0 && card.skill !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
662
|
+
className: "kb-card-skill",
|
|
663
|
+
children: ["/ ", card.skill]
|
|
664
|
+
}),
|
|
577
665
|
card.model !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
578
666
|
className: "kb-card-model",
|
|
579
667
|
children: card.model
|
|
@@ -591,13 +679,19 @@ window.__ModuleLoader__.load({
|
|
|
591
679
|
]
|
|
592
680
|
});
|
|
593
681
|
}
|
|
594
|
-
|
|
682
|
+
/** An in-progress leading `/query` token (menu-open state). */
|
|
683
|
+
const SKILL_MENU_RE = /^\s*\/([a-z0-9-]*)$/;
|
|
684
|
+
function NewTaskModal({ t, workspaces, selectedPath, sessionId, onClose, onCreated, onError }) {
|
|
595
685
|
const [requirement, setRequirement] = (0, react.useState)("");
|
|
596
686
|
const [project, setProject] = (0, react.useState)(selectedPath);
|
|
597
687
|
const [model, setModel] = (0, react.useState)("");
|
|
598
688
|
const [modelProvider, setModelProvider] = (0, react.useState)("");
|
|
599
689
|
const [models, setModels] = (0, react.useState)([]);
|
|
690
|
+
const [skills, setSkills] = (0, react.useState)([]);
|
|
691
|
+
const [menuQuery, setMenuQuery] = (0, react.useState)(null);
|
|
692
|
+
const [menuIndex, setMenuIndex] = (0, react.useState)(0);
|
|
600
693
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
694
|
+
const textareaRef = (0, react.useRef)(null);
|
|
601
695
|
(0, react.useEffect)(() => {
|
|
602
696
|
let alive = true;
|
|
603
697
|
api.models().then((list) => {
|
|
@@ -612,6 +706,68 @@ window.__ModuleLoader__.load({
|
|
|
612
706
|
alive = false;
|
|
613
707
|
};
|
|
614
708
|
}, []);
|
|
709
|
+
(0, react.useEffect)(() => {
|
|
710
|
+
if (sessionId === void 0) return;
|
|
711
|
+
let alive = true;
|
|
712
|
+
gatewaySkillList(sessionId).then((list) => {
|
|
713
|
+
if (alive) setSkills(list);
|
|
714
|
+
}).catch(() => {
|
|
715
|
+
if (alive) setSkills([]);
|
|
716
|
+
});
|
|
717
|
+
return () => {
|
|
718
|
+
alive = false;
|
|
719
|
+
};
|
|
720
|
+
}, [sessionId]);
|
|
721
|
+
const matches = skills.filter((s) => s.name.startsWith(menuQuery ?? ""));
|
|
722
|
+
const menuVisible = menuQuery !== null && matches.length > 0;
|
|
723
|
+
const selected = menuVisible ? matches[Math.min(menuIndex, matches.length - 1)] : void 0;
|
|
724
|
+
/** Re-evaluate the leading-token menu state from the current value + caret. */
|
|
725
|
+
const syncMenu = (value, caret) => {
|
|
726
|
+
const head = value.slice(0, caret);
|
|
727
|
+
const m = SKILL_MENU_RE.exec(head);
|
|
728
|
+
if (m !== null) {
|
|
729
|
+
setMenuQuery(m[1]);
|
|
730
|
+
setMenuIndex(0);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
setMenuQuery(null);
|
|
734
|
+
};
|
|
735
|
+
const selectSkill = (name) => {
|
|
736
|
+
const caret = textareaRef.current?.selectionStart ?? requirement.length;
|
|
737
|
+
const head = requirement.slice(0, caret);
|
|
738
|
+
const m = SKILL_MENU_RE.exec(head);
|
|
739
|
+
const inserted = `/${name} `;
|
|
740
|
+
const newValue = m !== null ? inserted + requirement.slice(caret) : inserted + requirement;
|
|
741
|
+
setRequirement(newValue);
|
|
742
|
+
setMenuQuery(null);
|
|
743
|
+
requestAnimationFrame(() => {
|
|
744
|
+
const next = textareaRef.current;
|
|
745
|
+
if (next !== null) {
|
|
746
|
+
next.focus();
|
|
747
|
+
const pos = inserted.length;
|
|
748
|
+
next.setSelectionRange(pos, pos);
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
};
|
|
752
|
+
const onKeyDown = (e) => {
|
|
753
|
+
if (!menuVisible) return;
|
|
754
|
+
if (e.key === "ArrowDown") {
|
|
755
|
+
e.preventDefault();
|
|
756
|
+
setMenuIndex((i) => (i + 1) % matches.length);
|
|
757
|
+
} else if (e.key === "ArrowUp") {
|
|
758
|
+
e.preventDefault();
|
|
759
|
+
setMenuIndex((i) => (i - 1 + matches.length) % matches.length);
|
|
760
|
+
} else if (e.key === "Enter") {
|
|
761
|
+
e.preventDefault();
|
|
762
|
+
if (selected !== void 0) selectSkill(selected.name);
|
|
763
|
+
} else if (e.key === "Escape") {
|
|
764
|
+
e.preventDefault();
|
|
765
|
+
setMenuQuery(null);
|
|
766
|
+
} else if (e.key === "Tab") {
|
|
767
|
+
e.preventDefault();
|
|
768
|
+
if (selected !== void 0) selectSkill(selected.name);
|
|
769
|
+
}
|
|
770
|
+
};
|
|
615
771
|
const submit = async () => {
|
|
616
772
|
if (requirement.trim() === "") {
|
|
617
773
|
onError(t("requirementLabel") + " " + t("error"));
|
|
@@ -621,15 +777,17 @@ window.__ModuleLoader__.load({
|
|
|
621
777
|
onError(t("noWorkspace"));
|
|
622
778
|
return;
|
|
623
779
|
}
|
|
780
|
+
const parsed = parseSkillGesture(requirement);
|
|
624
781
|
setBusy(true);
|
|
625
782
|
try {
|
|
626
|
-
await api.create(project, requirement
|
|
783
|
+
await api.create(project, parsed.requirement, model, modelProvider, parsed.skill);
|
|
627
784
|
onCreated();
|
|
628
785
|
} catch (error) {
|
|
629
786
|
onError(error instanceof Error ? error.message : String(error));
|
|
630
787
|
setBusy(false);
|
|
631
788
|
}
|
|
632
789
|
};
|
|
790
|
+
const activeSkill = parseSkillGesture(requirement).skill;
|
|
633
791
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
634
792
|
className: "kb-modal-backdrop",
|
|
635
793
|
onClick: busy ? void 0 : onClose,
|
|
@@ -643,12 +801,51 @@ window.__ModuleLoader__.load({
|
|
|
643
801
|
}),
|
|
644
802
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
645
803
|
className: "kb-field",
|
|
646
|
-
children: [
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
804
|
+
children: [
|
|
805
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("requirementLabel") }),
|
|
806
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
807
|
+
className: "kb-skill-menu-wrap",
|
|
808
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
809
|
+
ref: textareaRef,
|
|
810
|
+
className: "kb-input kb-textarea",
|
|
811
|
+
value: requirement,
|
|
812
|
+
placeholder: t("requirementPlaceholder"),
|
|
813
|
+
onChange: (e) => {
|
|
814
|
+
const value = e.target.value;
|
|
815
|
+
setRequirement(value);
|
|
816
|
+
syncMenu(value, e.target.selectionStart ?? value.length);
|
|
817
|
+
},
|
|
818
|
+
onKeyDown
|
|
819
|
+
}), menuVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
820
|
+
className: "kb-skill-menu",
|
|
821
|
+
role: "listbox",
|
|
822
|
+
children: matches.map((s, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
823
|
+
role: "option",
|
|
824
|
+
"aria-selected": i === menuIndex,
|
|
825
|
+
className: "kb-skill-menu-item" + (i === menuIndex ? " kb-skill-menu-item-active" : ""),
|
|
826
|
+
onMouseDown: (e) => {
|
|
827
|
+
e.preventDefault();
|
|
828
|
+
selectSkill(s.name);
|
|
829
|
+
},
|
|
830
|
+
onMouseEnter: () => setMenuIndex(i),
|
|
831
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
832
|
+
className: "kb-skill-menu-name",
|
|
833
|
+
children: ["/", s.name]
|
|
834
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
835
|
+
className: "kb-skill-menu-desc",
|
|
836
|
+
children: s.modelInvocable ? s.description : `${t("skillMenuUserOnly")} · ${s.description}`
|
|
837
|
+
})]
|
|
838
|
+
}, s.name))
|
|
839
|
+
})]
|
|
840
|
+
}),
|
|
841
|
+
activeSkill !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
842
|
+
className: "kb-field-hint kb-field-hint-skill",
|
|
843
|
+
children: t("skillHint", { skill: activeSkill })
|
|
844
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
845
|
+
className: "kb-field-hint",
|
|
846
|
+
children: t("skillGestureHint")
|
|
847
|
+
})
|
|
848
|
+
]
|
|
652
849
|
}),
|
|
653
850
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
654
851
|
className: "kb-field",
|
|
@@ -744,13 +941,20 @@ window.__ModuleLoader__.load({
|
|
|
744
941
|
children: [
|
|
745
942
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
746
943
|
className: "kb-detail-section",
|
|
747
|
-
children: [
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
944
|
+
children: [
|
|
945
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
946
|
+
className: "kb-detail-label",
|
|
947
|
+
children: t("requirement")
|
|
948
|
+
}),
|
|
949
|
+
card.skill !== void 0 && card.skill !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
950
|
+
className: "kb-detail-skill",
|
|
951
|
+
children: t("skillBadge", { skill: card.skill })
|
|
952
|
+
}),
|
|
953
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
954
|
+
className: "kb-detail-text",
|
|
955
|
+
children: card.requirement
|
|
956
|
+
})
|
|
957
|
+
]
|
|
754
958
|
}),
|
|
755
959
|
card.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
756
960
|
className: "kb-detail-section",
|
|
@@ -764,30 +968,12 @@ window.__ModuleLoader__.load({
|
|
|
764
968
|
children: card.plan.summary
|
|
765
969
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
|
|
766
970
|
className: "kb-plan-phases",
|
|
767
|
-
children: card.plan.phases.map((phase, i) => /* @__PURE__ */ (0, react_jsx_runtime.
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
className: "kb-plan-phase-title",
|
|
774
|
-
children: phase.title
|
|
775
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
776
|
-
className: "kb-plan-phase-id",
|
|
777
|
-
children: phase.id
|
|
778
|
-
})]
|
|
779
|
-
}),
|
|
780
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
781
|
-
className: "kb-plan-phase-goal",
|
|
782
|
-
children: phase.goal
|
|
783
|
-
}),
|
|
784
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhaseSessions, {
|
|
785
|
-
phaseIndex: i,
|
|
786
|
-
card,
|
|
787
|
-
t,
|
|
788
|
-
onOpenSession
|
|
789
|
-
})
|
|
790
|
-
]
|
|
971
|
+
children: card.plan.phases.map((phase, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhaseCard, {
|
|
972
|
+
phase,
|
|
973
|
+
phaseIndex: i,
|
|
974
|
+
card,
|
|
975
|
+
t,
|
|
976
|
+
onOpenSession
|
|
791
977
|
}, phase.id))
|
|
792
978
|
})]
|
|
793
979
|
})]
|
|
@@ -877,26 +1063,77 @@ window.__ModuleLoader__.load({
|
|
|
877
1063
|
})]
|
|
878
1064
|
});
|
|
879
1065
|
}
|
|
880
|
-
|
|
1066
|
+
/** One plan phase rendered as a card with goal / sessions / conclusion sections. */
|
|
1067
|
+
function PhaseCard({ phase, phaseIndex, card, t, onOpenSession }) {
|
|
881
1068
|
const attempts = card.sessions.phases.filter((a) => a.phaseIndex === phaseIndex);
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
children:
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
]
|
|
896
|
-
}), a.summary !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
897
|
-
className: "kb-session-summary",
|
|
898
|
-
children: a.summary
|
|
1069
|
+
const summaries = attempts.filter((a) => a.summary !== void 0 && a.summary.trim() !== "").map((a) => a.summary);
|
|
1070
|
+
const isCurrent = phaseIndex === card.currentPhase && card.status === "running";
|
|
1071
|
+
const isCompleted = summaries.length > 0;
|
|
1072
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1073
|
+
className: "kb-plan-phase" + (isCurrent ? " kb-plan-phase-current" : "") + (isCompleted ? " kb-plan-phase-completed" : ""),
|
|
1074
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1075
|
+
className: "kb-plan-phase-head",
|
|
1076
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1077
|
+
className: "kb-plan-phase-title",
|
|
1078
|
+
children: phase.title
|
|
1079
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1080
|
+
className: "kb-plan-phase-id",
|
|
1081
|
+
children: phase.id
|
|
899
1082
|
})]
|
|
1083
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1084
|
+
className: "kb-plan-phase-body",
|
|
1085
|
+
children: [
|
|
1086
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1087
|
+
className: "kb-plan-phase-section",
|
|
1088
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1089
|
+
className: "kb-plan-phase-section-title",
|
|
1090
|
+
children: t("phaseGoal")
|
|
1091
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1092
|
+
className: "kb-plan-phase-goal",
|
|
1093
|
+
children: phase.goal
|
|
1094
|
+
})]
|
|
1095
|
+
}),
|
|
1096
|
+
attempts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1097
|
+
className: "kb-plan-phase-section",
|
|
1098
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1099
|
+
className: "kb-plan-phase-section-title",
|
|
1100
|
+
children: t("phaseSessions")
|
|
1101
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PhaseSessions, {
|
|
1102
|
+
attempts,
|
|
1103
|
+
t,
|
|
1104
|
+
onOpenSession
|
|
1105
|
+
})]
|
|
1106
|
+
}),
|
|
1107
|
+
summaries.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1108
|
+
className: "kb-plan-phase-section",
|
|
1109
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1110
|
+
className: "kb-plan-phase-section-title",
|
|
1111
|
+
children: t("phaseConclusion")
|
|
1112
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1113
|
+
className: "kb-plan-phase-conclusion",
|
|
1114
|
+
children: summaries.map((s, idx) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1115
|
+
className: "kb-plan-phase-conclusion-item",
|
|
1116
|
+
children: s
|
|
1117
|
+
}, idx))
|
|
1118
|
+
})]
|
|
1119
|
+
})
|
|
1120
|
+
]
|
|
1121
|
+
})]
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
/** Open-session buttons for one phase's attempts (retries append more). */
|
|
1125
|
+
function PhaseSessions({ attempts, t, onOpenSession }) {
|
|
1126
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1127
|
+
className: "kb-plan-phase-sessions",
|
|
1128
|
+
children: attempts.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1129
|
+
type: "button",
|
|
1130
|
+
className: "kb-btn kb-btn-small",
|
|
1131
|
+
onClick: () => onOpenSession(a.sessionId),
|
|
1132
|
+
children: [
|
|
1133
|
+
t("openSession"),
|
|
1134
|
+
" · ",
|
|
1135
|
+
a.sessionId.slice(0, 13)
|
|
1136
|
+
]
|
|
900
1137
|
}, a.sessionId))
|
|
901
1138
|
});
|
|
902
1139
|
}
|