@fonlan/dsh-task-kanban 0.1.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/LICENSE +21 -0
- package/README.md +45 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +1128 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +1514 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/server/git.d.ts +16 -0
- package/lib/types/server/models.d.ts +4 -0
- package/lib/types/server/prompts.d.ts +7 -0
- package/lib/types/server/rpc.d.ts +4 -0
- package/lib/types/server/runner.d.ts +84 -0
- package/lib/types/server/settings.d.ts +27 -0
- package/lib/types/server/task-store.d.ts +27 -0
- package/lib/types/server/tools.d.ts +11 -0
- package/lib/types/shared/card.d.ts +86 -0
- package/lib/types/shared/lanes.d.ts +9 -0
- package/lib/types/shared/plan.d.ts +12 -0
- package/package.json +105 -0
- package/src/client/api.ts +42 -0
- package/src/client/board.css +361 -0
- package/src/client/board.tsx +516 -0
- package/src/client/index.tsx +47 -0
- package/src/client/kanban-state.ts +104 -0
- package/src/client/locales.ts +127 -0
- package/src/client/sections.tsx +209 -0
- package/src/client/slot-augment.d.ts +15 -0
- package/src/index.ts +34 -0
- package/src/server/git.ts +68 -0
- package/src/server/models.ts +20 -0
- package/src/server/prompts.ts +54 -0
- package/src/server/rpc.ts +167 -0
- package/src/server/runner.ts +834 -0
- package/src/server/settings.ts +69 -0
- package/src/server/task-store.ts +128 -0
- package/src/server/tools.ts +131 -0
- package/src/shared/card.ts +115 -0
- package/src/shared/lanes.ts +43 -0
- package/src/shared/plan.ts +54 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@fonlan/dsh-task-kanban",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
|
+
//#region src/shared/lanes.ts
|
|
11
|
+
/** Which lane a card renders in, based on status + error stage. */
|
|
12
|
+
function laneOf(card) {
|
|
13
|
+
if (card.status === "error") {
|
|
14
|
+
if (card.error?.stage !== void 0) return card.error.stage;
|
|
15
|
+
const kind = card.error?.kind;
|
|
16
|
+
if (kind === "phase_failed" || kind === "no_base_branch" || kind === "worktree_failed") return "running";
|
|
17
|
+
if (kind === "merge_failed") return "completed";
|
|
18
|
+
return "demand";
|
|
19
|
+
}
|
|
20
|
+
switch (card.status) {
|
|
21
|
+
case "draft":
|
|
22
|
+
case "refining":
|
|
23
|
+
case "planned": return "demand";
|
|
24
|
+
case "queued": return "queue";
|
|
25
|
+
case "running": return "running";
|
|
26
|
+
case "completed":
|
|
27
|
+
case "merging": return "completed";
|
|
28
|
+
case "merged": return "merged";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Lanes a drop onto `to` may come from. */
|
|
32
|
+
const DROP_RULES = {
|
|
33
|
+
demand: ["queue"],
|
|
34
|
+
queue: ["demand", "running"],
|
|
35
|
+
running: [],
|
|
36
|
+
completed: [],
|
|
37
|
+
merged: []
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/client/api.ts
|
|
41
|
+
var KanbanApiError = class extends Error {
|
|
42
|
+
code;
|
|
43
|
+
constructor(code, message) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.code = code;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
async function call(method, payload = {}) {
|
|
49
|
+
let response;
|
|
50
|
+
try {
|
|
51
|
+
response = await fetch(`/plugins/@fonlan/dsh-task-kanban/api/${method}`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { "content-type": "application/json" },
|
|
54
|
+
body: JSON.stringify(payload)
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new KanbanApiError("network", error instanceof Error ? error.message : String(error));
|
|
58
|
+
}
|
|
59
|
+
const parsed = await response.json().catch(() => null);
|
|
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
|
+
return parsed.value;
|
|
62
|
+
}
|
|
63
|
+
const api = {
|
|
64
|
+
list: (workspacePath) => call("list", { workspacePath }),
|
|
65
|
+
create: (workspacePath, requirement, model, provider) => call("create", {
|
|
66
|
+
workspacePath,
|
|
67
|
+
requirement,
|
|
68
|
+
model,
|
|
69
|
+
provider
|
|
70
|
+
}),
|
|
71
|
+
move: (cardId, toLane) => call("move", {
|
|
72
|
+
cardId,
|
|
73
|
+
toLane
|
|
74
|
+
}),
|
|
75
|
+
stop: (cardId) => call("stop", { cardId }),
|
|
76
|
+
retry: (cardId) => call("retry", { cardId }),
|
|
77
|
+
remove: (cardId) => call("remove", { cardId }),
|
|
78
|
+
settingsGet: () => call("settings.get"),
|
|
79
|
+
settingsSet: (patch) => call("settings.set", patch),
|
|
80
|
+
models: () => call("models.list")
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/client/kanban-state.ts
|
|
84
|
+
let client = null;
|
|
85
|
+
function setClient(ctx) {
|
|
86
|
+
client = ctx;
|
|
87
|
+
}
|
|
88
|
+
function getClient() {
|
|
89
|
+
return client;
|
|
90
|
+
}
|
|
91
|
+
let boardRoot = null;
|
|
92
|
+
function setBoardRoot(component) {
|
|
93
|
+
boardRoot = component;
|
|
94
|
+
}
|
|
95
|
+
let boardDisposer = null;
|
|
96
|
+
let boardOpen = false;
|
|
97
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
98
|
+
function notify() {
|
|
99
|
+
for (const l of listeners) l();
|
|
100
|
+
}
|
|
101
|
+
function isBoardOpen() {
|
|
102
|
+
return boardOpen;
|
|
103
|
+
}
|
|
104
|
+
function subscribe(cb) {
|
|
105
|
+
listeners.add(cb);
|
|
106
|
+
return () => {
|
|
107
|
+
listeners.delete(cb);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function useBoardOpen() {
|
|
111
|
+
return (0, react.useSyncExternalStore)(subscribe, isBoardOpen);
|
|
112
|
+
}
|
|
113
|
+
function enterBoard() {
|
|
114
|
+
const ctx = client;
|
|
115
|
+
const root = boardRoot;
|
|
116
|
+
if (ctx === null || root === null || boardDisposer !== null) return;
|
|
117
|
+
try {
|
|
118
|
+
boardDisposer = ctx.slots.register({
|
|
119
|
+
name: "conversation",
|
|
120
|
+
priority: -1,
|
|
121
|
+
locale: "task-kanban"
|
|
122
|
+
}, root);
|
|
123
|
+
boardOpen = true;
|
|
124
|
+
notify();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error("[@fonlan/dsh-task-kanban] cannot open the board:", error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function exitBoard() {
|
|
130
|
+
if (boardDisposer !== null) {
|
|
131
|
+
boardDisposer();
|
|
132
|
+
boardDisposer = null;
|
|
133
|
+
}
|
|
134
|
+
if (boardOpen) {
|
|
135
|
+
boardOpen = false;
|
|
136
|
+
notify();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function toggleBoard() {
|
|
140
|
+
if (boardOpen) exitBoard();
|
|
141
|
+
else enterBoard();
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Route session navigation through the board: opening any session while the
|
|
145
|
+
* board is open leaves the board first, so the conversation view can render
|
|
146
|
+
* for the newly selected session.
|
|
147
|
+
*
|
|
148
|
+
* Every sidebar path that selects a session funnels through `sessions.open`
|
|
149
|
+
* (session rows, search results, fork results, New Session) or
|
|
150
|
+
* `sessions.openSubagent` (catalog children) — including re-clicking the
|
|
151
|
+
* already-current session, which never changes `list.current` and therefore
|
|
152
|
+
* cannot be caught by a list-store subscription. Wrapping the two entry
|
|
153
|
+
* points covers all of them in one place.
|
|
154
|
+
*
|
|
155
|
+
* The wrapper is idempotent: a `kbBound` marker on the wrapper prevents a
|
|
156
|
+
* second bind (plugin re-apply / HMR) from stacking another layer.
|
|
157
|
+
*/
|
|
158
|
+
function bindSessionNavigation(ctx) {
|
|
159
|
+
const sessions = ctx.sessions;
|
|
160
|
+
if (sessions === void 0) return;
|
|
161
|
+
const open = sessions.open;
|
|
162
|
+
if (typeof open === "function" && open.kbBound !== true) {
|
|
163
|
+
const bound = ((id) => {
|
|
164
|
+
exitBoard();
|
|
165
|
+
return open.call(sessions, id);
|
|
166
|
+
});
|
|
167
|
+
bound.kbBound = true;
|
|
168
|
+
sessions.open = bound;
|
|
169
|
+
}
|
|
170
|
+
const openSubagent = sessions.openSubagent;
|
|
171
|
+
if (typeof openSubagent === "function" && openSubagent.kbBound !== true) {
|
|
172
|
+
const bound = ((address) => {
|
|
173
|
+
exitBoard();
|
|
174
|
+
return openSubagent.call(sessions, address);
|
|
175
|
+
});
|
|
176
|
+
bound.kbBound = true;
|
|
177
|
+
sessions.openSubagent = bound;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/client/locales.ts
|
|
182
|
+
const LOCALE_NS = "task-kanban";
|
|
183
|
+
const zh = {
|
|
184
|
+
kanban: "任务看板",
|
|
185
|
+
openBoard: "打开任务看板",
|
|
186
|
+
closeBoard: "返回对话",
|
|
187
|
+
newTask: "新建任务",
|
|
188
|
+
requirement: "需求",
|
|
189
|
+
project: "所属项目",
|
|
190
|
+
model: "模型",
|
|
191
|
+
cancel: "取消",
|
|
192
|
+
close: "关闭",
|
|
193
|
+
addAndRefine: "添加并细化",
|
|
194
|
+
laneDemand: "需求",
|
|
195
|
+
laneQueue: "队列",
|
|
196
|
+
laneRunning: "运行中",
|
|
197
|
+
laneCompleted: "已完成",
|
|
198
|
+
laneMerged: "已合并",
|
|
199
|
+
statusDraft: "未细化",
|
|
200
|
+
statusRefining: "细化中",
|
|
201
|
+
statusPlanned: "已规划",
|
|
202
|
+
statusQueued: "排队中",
|
|
203
|
+
statusRunning: "实现中",
|
|
204
|
+
statusCompleted: "已完成",
|
|
205
|
+
statusMerging: "合并中",
|
|
206
|
+
statusMerged: "已合并",
|
|
207
|
+
statusError: "出错",
|
|
208
|
+
phaseProgress: "{current}/{total}",
|
|
209
|
+
retry: "重试",
|
|
210
|
+
stop: "停止",
|
|
211
|
+
delete: "删除",
|
|
212
|
+
deleteConfirm: "确定删除该任务?其 worktree 会被清理。",
|
|
213
|
+
openSession: "打开会话",
|
|
214
|
+
plan: "实现方案",
|
|
215
|
+
summary: "摘要",
|
|
216
|
+
phases: "阶段",
|
|
217
|
+
error: "错误",
|
|
218
|
+
requirementLabel: "需求文本",
|
|
219
|
+
requirementPlaceholder: "描述你要实现的需求……",
|
|
220
|
+
modelPlaceholder: "(使用默认模型)",
|
|
221
|
+
noWorkspace: "没有可用工作区,请先在侧边栏添加",
|
|
222
|
+
refresh: "刷新",
|
|
223
|
+
settingsTitle: "任务看板设置",
|
|
224
|
+
maxParallelWorkers: "全局并行 worker 数",
|
|
225
|
+
defaultModel: "默认模型",
|
|
226
|
+
saved: "已保存",
|
|
227
|
+
loadError: "加载任务失败",
|
|
228
|
+
noPlan: "该卡片还没有实现计划,请先点击\"添加并细化\"",
|
|
229
|
+
dragNoPlan: "还没有实现计划的卡片不能进入队列",
|
|
230
|
+
actionFailed: "操作失败:{message}",
|
|
231
|
+
sessionNotListed: "该会话不在当前列表,可能来自其他工作区",
|
|
232
|
+
interrupted: "已中断",
|
|
233
|
+
noBaseBranch: "项目没有 main/master 分支",
|
|
234
|
+
worktreeFailed: "worktree 创建失败",
|
|
235
|
+
refineFailed: "细化失败",
|
|
236
|
+
phaseFailed: "阶段执行失败",
|
|
237
|
+
mergeFailed: "合并失败",
|
|
238
|
+
createFailed: "创建失败",
|
|
239
|
+
refinementLabel: "需求细化",
|
|
240
|
+
mergeLabel: "合并",
|
|
241
|
+
commitLabel: "提交"
|
|
242
|
+
};
|
|
243
|
+
const en = {
|
|
244
|
+
kanban: "Task Kanban",
|
|
245
|
+
openBoard: "Open Task Kanban",
|
|
246
|
+
closeBoard: "Back to chat",
|
|
247
|
+
newTask: "New Task",
|
|
248
|
+
requirement: "Requirement",
|
|
249
|
+
project: "Project",
|
|
250
|
+
model: "Model",
|
|
251
|
+
cancel: "Cancel",
|
|
252
|
+
close: "Close",
|
|
253
|
+
addAndRefine: "Add & Refine",
|
|
254
|
+
laneDemand: "Demands",
|
|
255
|
+
laneQueue: "Queue",
|
|
256
|
+
laneRunning: "Running",
|
|
257
|
+
laneCompleted: "Completed",
|
|
258
|
+
laneMerged: "Merged",
|
|
259
|
+
statusDraft: "Unrefined",
|
|
260
|
+
statusRefining: "Refining",
|
|
261
|
+
statusPlanned: "Planned",
|
|
262
|
+
statusQueued: "Queued",
|
|
263
|
+
statusRunning: "Running",
|
|
264
|
+
statusCompleted: "Completed",
|
|
265
|
+
statusMerging: "Merging",
|
|
266
|
+
statusMerged: "Merged",
|
|
267
|
+
statusError: "Error",
|
|
268
|
+
phaseProgress: "{current}/{total}",
|
|
269
|
+
retry: "Retry",
|
|
270
|
+
stop: "Stop",
|
|
271
|
+
delete: "Delete",
|
|
272
|
+
deleteConfirm: "Delete this task? Its worktree will be cleaned up.",
|
|
273
|
+
openSession: "Open session",
|
|
274
|
+
plan: "Plan",
|
|
275
|
+
summary: "Summary",
|
|
276
|
+
phases: "Phases",
|
|
277
|
+
error: "Error",
|
|
278
|
+
requirementLabel: "Requirement",
|
|
279
|
+
requirementPlaceholder: "Describe what you want to build…",
|
|
280
|
+
modelPlaceholder: "(use default model)",
|
|
281
|
+
noWorkspace: "No workspaces available — add one in the sidebar first",
|
|
282
|
+
refresh: "Refresh",
|
|
283
|
+
settingsTitle: "Task Kanban Settings",
|
|
284
|
+
maxParallelWorkers: "Global parallel workers",
|
|
285
|
+
defaultModel: "Default model",
|
|
286
|
+
saved: "Saved",
|
|
287
|
+
loadError: "Failed to load tasks",
|
|
288
|
+
noPlan: "This card has no plan yet — click \"Add & Refine\" first",
|
|
289
|
+
dragNoPlan: "Cards without a plan cannot enter the queue",
|
|
290
|
+
actionFailed: "Action failed: {message}",
|
|
291
|
+
sessionNotListed: "Session is not listed here; it may belong to another workspace",
|
|
292
|
+
interrupted: "Interrupted",
|
|
293
|
+
noBaseBranch: "Project has no main/master branch",
|
|
294
|
+
worktreeFailed: "Worktree creation failed",
|
|
295
|
+
refineFailed: "Refinement failed",
|
|
296
|
+
phaseFailed: "Phase failed",
|
|
297
|
+
mergeFailed: "Merge failed",
|
|
298
|
+
createFailed: "Creation failed",
|
|
299
|
+
refinementLabel: "Refinement",
|
|
300
|
+
mergeLabel: "Merge",
|
|
301
|
+
commitLabel: "Commit"
|
|
302
|
+
};
|
|
303
|
+
//#endregion
|
|
304
|
+
//#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}";
|
|
306
|
+
const tagId = "@fonlan/dsh-task-kanban/board.css";
|
|
307
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
308
|
+
const tag = document.createElement("style");
|
|
309
|
+
tag.dataset.plugin = "@fonlan/dsh-task-kanban";
|
|
310
|
+
tag.dataset.pluginCss = tagId;
|
|
311
|
+
tag.textContent = css;
|
|
312
|
+
document.head.appendChild(tag);
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/client/board.tsx
|
|
316
|
+
function firstLine(text) {
|
|
317
|
+
return text.split("\n").find((l) => l.trim() !== "")?.trim() ?? "";
|
|
318
|
+
}
|
|
319
|
+
function statusLabel(card, t) {
|
|
320
|
+
if (card.status === "error") switch (card.error?.kind) {
|
|
321
|
+
case "interrupted": return t("interrupted");
|
|
322
|
+
case "no_base_branch": return t("noBaseBranch");
|
|
323
|
+
case "worktree_failed": return t("worktreeFailed");
|
|
324
|
+
case "refine_failed": return t("refineFailed");
|
|
325
|
+
case "phase_failed": return t("phaseFailed");
|
|
326
|
+
case "merge_failed": return t("mergeFailed");
|
|
327
|
+
default: return t("createFailed");
|
|
328
|
+
}
|
|
329
|
+
switch (card.status) {
|
|
330
|
+
case "draft": return t("statusDraft");
|
|
331
|
+
case "refining": return t("statusRefining");
|
|
332
|
+
case "planned": return t("statusPlanned");
|
|
333
|
+
case "queued": return t("statusQueued");
|
|
334
|
+
case "running": return t("statusRunning");
|
|
335
|
+
case "completed": return t("statusCompleted");
|
|
336
|
+
case "merging": return t("statusMerging");
|
|
337
|
+
case "merged": return t("statusMerged");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const LANE_ORDER = [
|
|
341
|
+
{
|
|
342
|
+
key: "demand",
|
|
343
|
+
labelKey: "laneDemand"
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
key: "queue",
|
|
347
|
+
labelKey: "laneQueue"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
key: "running",
|
|
351
|
+
labelKey: "laneRunning"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
key: "completed",
|
|
355
|
+
labelKey: "laneCompleted"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
key: "merged",
|
|
359
|
+
labelKey: "laneMerged"
|
|
360
|
+
}
|
|
361
|
+
];
|
|
362
|
+
const WS_STORAGE_KEY = "@fonlan/dsh-task-kanban:workspace";
|
|
363
|
+
function BoardRoot(props) {
|
|
364
|
+
const t = props.t ?? ((key, params) => {
|
|
365
|
+
const dict = zh[key] ?? en[key];
|
|
366
|
+
if (dict === void 0 || params === void 0) return dict ?? key;
|
|
367
|
+
return dict.replace(/\{(\w+)\}/g, (m, name) => params[name] ?? m);
|
|
368
|
+
});
|
|
369
|
+
const useWorkspaces = props.useWorkspaces;
|
|
370
|
+
const items = (useWorkspaces !== void 0 ? useWorkspaces((s) => s) : void 0)?.items ?? [];
|
|
371
|
+
const [selectedPath, setSelectedPath] = (0, react.useState)(() => {
|
|
372
|
+
try {
|
|
373
|
+
return localStorage.getItem(WS_STORAGE_KEY) ?? "";
|
|
374
|
+
} catch {
|
|
375
|
+
return "";
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
const [cards, setCards] = (0, react.useState)([]);
|
|
379
|
+
const [toast, setToast] = (0, react.useState)(null);
|
|
380
|
+
const [detailId, setDetailId] = (0, react.useState)(null);
|
|
381
|
+
const [newTaskOpen, setNewTaskOpen] = (0, react.useState)(false);
|
|
382
|
+
(0, react.useEffect)(() => {
|
|
383
|
+
if (selectedPath === "" && items.length > 0) setSelectedPath(items[0].path);
|
|
384
|
+
}, [selectedPath, items]);
|
|
385
|
+
const showToast = (0, react.useCallback)((message) => {
|
|
386
|
+
setToast(message);
|
|
387
|
+
window.setTimeout(() => setToast(null), 3500);
|
|
388
|
+
}, []);
|
|
389
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
390
|
+
if (selectedPath === "") return;
|
|
391
|
+
try {
|
|
392
|
+
setCards(await api.list(selectedPath));
|
|
393
|
+
} catch (error) {
|
|
394
|
+
showToast(error instanceof Error ? error.message : String(error));
|
|
395
|
+
}
|
|
396
|
+
}, [selectedPath, showToast]);
|
|
397
|
+
(0, react.useEffect)(() => {
|
|
398
|
+
refresh();
|
|
399
|
+
const timer = window.setInterval(() => {
|
|
400
|
+
refresh();
|
|
401
|
+
}, 2500);
|
|
402
|
+
return () => window.clearInterval(timer);
|
|
403
|
+
}, [refresh]);
|
|
404
|
+
const byLane = (0, react.useMemo)(() => {
|
|
405
|
+
const map = {
|
|
406
|
+
demand: [],
|
|
407
|
+
queue: [],
|
|
408
|
+
running: [],
|
|
409
|
+
completed: [],
|
|
410
|
+
merged: []
|
|
411
|
+
};
|
|
412
|
+
for (const card of cards) map[laneOf(card)].push(card);
|
|
413
|
+
for (const lane of LANE_ORDER) map[lane.key].sort((a, b) => a.createdAt - b.createdAt);
|
|
414
|
+
return map;
|
|
415
|
+
}, [cards]);
|
|
416
|
+
const handleDrop = (0, react.useCallback)(async (target, event) => {
|
|
417
|
+
event.preventDefault();
|
|
418
|
+
const cardId = event.dataTransfer.getData("text/plain");
|
|
419
|
+
if (cardId === "") return;
|
|
420
|
+
const card = cards.find((c) => c.id === cardId);
|
|
421
|
+
if (card === void 0) return;
|
|
422
|
+
if (target === "queue" && card.status !== "running" && card.plan === void 0) {
|
|
423
|
+
showToast(t("dragNoPlan"));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
const result = await api.move(cardId, target);
|
|
428
|
+
if (!result.ok) showToast(t("actionFailed", { message: result.message ?? "" }));
|
|
429
|
+
refresh();
|
|
430
|
+
} catch (error) {
|
|
431
|
+
showToast(error instanceof Error ? error.message : String(error));
|
|
432
|
+
}
|
|
433
|
+
}, [
|
|
434
|
+
cards,
|
|
435
|
+
refresh,
|
|
436
|
+
showToast,
|
|
437
|
+
t
|
|
438
|
+
]);
|
|
439
|
+
const openSession = (0, react.useCallback)((sessionId) => {
|
|
440
|
+
const ctx = getClient();
|
|
441
|
+
exitBoard();
|
|
442
|
+
if (ctx === null) return;
|
|
443
|
+
try {
|
|
444
|
+
ctx.sessions.open(sessionId);
|
|
445
|
+
} catch {
|
|
446
|
+
showToast(t("sessionNotListed"));
|
|
447
|
+
}
|
|
448
|
+
}, [showToast, t]);
|
|
449
|
+
if (items.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
450
|
+
className: "kb-board kb-empty",
|
|
451
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
452
|
+
className: "kb-empty-text",
|
|
453
|
+
children: t("noWorkspace")
|
|
454
|
+
})
|
|
455
|
+
});
|
|
456
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
457
|
+
className: "kb-board",
|
|
458
|
+
children: [
|
|
459
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
460
|
+
className: "kb-board-header",
|
|
461
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
462
|
+
className: "kb-board-title",
|
|
463
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
464
|
+
className: "kb-ws-select",
|
|
465
|
+
value: selectedPath,
|
|
466
|
+
onChange: (e) => {
|
|
467
|
+
setSelectedPath(e.target.value);
|
|
468
|
+
try {
|
|
469
|
+
localStorage.setItem(WS_STORAGE_KEY, e.target.value);
|
|
470
|
+
} catch {}
|
|
471
|
+
},
|
|
472
|
+
children: items.map((w) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
473
|
+
value: w.path,
|
|
474
|
+
children: w.title
|
|
475
|
+
}, 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", {
|
|
480
|
+
type: "button",
|
|
481
|
+
className: "kb-btn kb-btn-primary",
|
|
482
|
+
onClick: () => setNewTaskOpen(true),
|
|
483
|
+
children: t("newTask")
|
|
484
|
+
})
|
|
485
|
+
})]
|
|
486
|
+
}),
|
|
487
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
488
|
+
className: "kb-lanes",
|
|
489
|
+
children: LANE_ORDER.map((lane) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
490
|
+
className: "kb-lane" + (DROP_RULES[lane.key].length > 0 ? " kb-lane-drop" : ""),
|
|
491
|
+
onDragOver: (e) => {
|
|
492
|
+
if (DROP_RULES[lane.key].length > 0) e.preventDefault();
|
|
493
|
+
},
|
|
494
|
+
onDrop: (e) => {
|
|
495
|
+
handleDrop(lane.key, e);
|
|
496
|
+
},
|
|
497
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
498
|
+
className: "kb-lane-head",
|
|
499
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
500
|
+
className: "kb-lane-title",
|
|
501
|
+
children: t(lane.labelKey)
|
|
502
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
503
|
+
className: "kb-lane-count",
|
|
504
|
+
children: byLane[lane.key].length
|
|
505
|
+
})]
|
|
506
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
507
|
+
className: "kb-lane-body",
|
|
508
|
+
children: byLane[lane.key].map((card) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardView, {
|
|
509
|
+
card,
|
|
510
|
+
t,
|
|
511
|
+
onToggle: () => setDetailId(detailId === card.id ? null : card.id)
|
|
512
|
+
}, card.id))
|
|
513
|
+
})]
|
|
514
|
+
}, lane.key))
|
|
515
|
+
}),
|
|
516
|
+
toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
517
|
+
className: "kb-toast",
|
|
518
|
+
children: toast
|
|
519
|
+
}),
|
|
520
|
+
newTaskOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NewTaskModal, {
|
|
521
|
+
t,
|
|
522
|
+
workspaces: items,
|
|
523
|
+
selectedPath,
|
|
524
|
+
onClose: () => setNewTaskOpen(false),
|
|
525
|
+
onCreated: () => {
|
|
526
|
+
setNewTaskOpen(false);
|
|
527
|
+
refresh();
|
|
528
|
+
},
|
|
529
|
+
onError: (message) => showToast(message)
|
|
530
|
+
}),
|
|
531
|
+
detailId !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DetailPanel, {
|
|
532
|
+
card: cards.find((c) => c.id === detailId) ?? null,
|
|
533
|
+
t,
|
|
534
|
+
onClose: () => setDetailId(null),
|
|
535
|
+
onChanged: () => {
|
|
536
|
+
setDetailId(null);
|
|
537
|
+
refresh();
|
|
538
|
+
},
|
|
539
|
+
onToast: showToast,
|
|
540
|
+
onOpenSession: openSession
|
|
541
|
+
})
|
|
542
|
+
]
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
function CardView({ card, t, onToggle }) {
|
|
546
|
+
const draggable = card.status === "planned" || card.status === "queued" || card.status === "running";
|
|
547
|
+
const title = card.plan !== void 0 && card.plan.title !== "" ? card.plan.title : firstLine(card.requirement);
|
|
548
|
+
const badge = statusLabel(card, t);
|
|
549
|
+
const phaseInfo = card.status === "running" || card.status === "error" ? card.currentPhase < card.phaseCount ? t("phaseProgress", {
|
|
550
|
+
current: card.currentPhase + 1,
|
|
551
|
+
total: card.phaseCount
|
|
552
|
+
}) : "" : "";
|
|
553
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
554
|
+
className: "kb-card" + (card.status === "error" ? " kb-card-error" : "") + (card.status === "running" ? " kb-card-running" : ""),
|
|
555
|
+
draggable,
|
|
556
|
+
onDragStart: (e) => {
|
|
557
|
+
e.dataTransfer.setData("text/plain", card.id);
|
|
558
|
+
e.dataTransfer.effectAllowed = "move";
|
|
559
|
+
},
|
|
560
|
+
onClick: onToggle,
|
|
561
|
+
children: [
|
|
562
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
563
|
+
className: "kb-card-title",
|
|
564
|
+
children: title
|
|
565
|
+
}),
|
|
566
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
567
|
+
className: "kb-card-meta",
|
|
568
|
+
children: [
|
|
569
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
570
|
+
className: "kb-card-badge",
|
|
571
|
+
children: badge
|
|
572
|
+
}),
|
|
573
|
+
phaseInfo !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
574
|
+
className: "kb-card-phase",
|
|
575
|
+
children: phaseInfo
|
|
576
|
+
}),
|
|
577
|
+
card.model !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
578
|
+
className: "kb-card-model",
|
|
579
|
+
children: card.model
|
|
580
|
+
})
|
|
581
|
+
]
|
|
582
|
+
}),
|
|
583
|
+
card.status === "error" && card.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
584
|
+
className: "kb-card-error-text",
|
|
585
|
+
children: card.error.message
|
|
586
|
+
}),
|
|
587
|
+
card.status === "running" && card.gitMode === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
588
|
+
className: "kb-card-worktree",
|
|
589
|
+
children: card.worktreePath ?? ""
|
|
590
|
+
})
|
|
591
|
+
]
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
function NewTaskModal({ t, workspaces, selectedPath, onClose, onCreated, onError }) {
|
|
595
|
+
const [requirement, setRequirement] = (0, react.useState)("");
|
|
596
|
+
const [project, setProject] = (0, react.useState)(selectedPath);
|
|
597
|
+
const [model, setModel] = (0, react.useState)("");
|
|
598
|
+
const [modelProvider, setModelProvider] = (0, react.useState)("");
|
|
599
|
+
const [models, setModels] = (0, react.useState)([]);
|
|
600
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
601
|
+
(0, react.useEffect)(() => {
|
|
602
|
+
let alive = true;
|
|
603
|
+
api.models().then((list) => {
|
|
604
|
+
if (alive) setModels(list);
|
|
605
|
+
}).catch(() => {
|
|
606
|
+
if (alive) setModels([]);
|
|
607
|
+
});
|
|
608
|
+
api.settingsGet().then((s) => {
|
|
609
|
+
if (alive && s.defaultModel !== "") setModel(s.defaultModel);
|
|
610
|
+
}).catch(() => void 0);
|
|
611
|
+
return () => {
|
|
612
|
+
alive = false;
|
|
613
|
+
};
|
|
614
|
+
}, []);
|
|
615
|
+
const submit = async () => {
|
|
616
|
+
if (requirement.trim() === "") {
|
|
617
|
+
onError(t("requirementLabel") + " " + t("error"));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (project === "") {
|
|
621
|
+
onError(t("noWorkspace"));
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
setBusy(true);
|
|
625
|
+
try {
|
|
626
|
+
await api.create(project, requirement.trim(), model, modelProvider);
|
|
627
|
+
onCreated();
|
|
628
|
+
} catch (error) {
|
|
629
|
+
onError(error instanceof Error ? error.message : String(error));
|
|
630
|
+
setBusy(false);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
634
|
+
className: "kb-modal-backdrop",
|
|
635
|
+
onClick: busy ? void 0 : onClose,
|
|
636
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
637
|
+
className: "kb-modal",
|
|
638
|
+
onClick: (e) => e.stopPropagation(),
|
|
639
|
+
children: [
|
|
640
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
641
|
+
className: "kb-modal-title",
|
|
642
|
+
children: t("newTask")
|
|
643
|
+
}),
|
|
644
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
645
|
+
className: "kb-field",
|
|
646
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("requirementLabel") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
647
|
+
className: "kb-input kb-textarea",
|
|
648
|
+
value: requirement,
|
|
649
|
+
placeholder: t("requirementPlaceholder"),
|
|
650
|
+
onChange: (e) => setRequirement(e.target.value)
|
|
651
|
+
})]
|
|
652
|
+
}),
|
|
653
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
654
|
+
className: "kb-field",
|
|
655
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("project") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
656
|
+
className: "kb-input",
|
|
657
|
+
value: project,
|
|
658
|
+
onChange: (e) => setProject(e.target.value),
|
|
659
|
+
children: workspaces.map((w) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
660
|
+
value: w.path,
|
|
661
|
+
children: w.title
|
|
662
|
+
}, w.workspaceId))
|
|
663
|
+
})]
|
|
664
|
+
}),
|
|
665
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
666
|
+
className: "kb-field",
|
|
667
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("model") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
668
|
+
className: "kb-input",
|
|
669
|
+
value: model,
|
|
670
|
+
onChange: (e) => {
|
|
671
|
+
const id = e.target.value;
|
|
672
|
+
setModel(id);
|
|
673
|
+
const found = models.find((m) => m.id === id);
|
|
674
|
+
setModelProvider(found?.provider ?? "");
|
|
675
|
+
},
|
|
676
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
677
|
+
value: "",
|
|
678
|
+
children: t("modelPlaceholder")
|
|
679
|
+
}), models.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
680
|
+
value: m.id,
|
|
681
|
+
children: m.name ?? m.id
|
|
682
|
+
}, m.provider + "/" + m.id))]
|
|
683
|
+
})]
|
|
684
|
+
}),
|
|
685
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
686
|
+
className: "kb-modal-actions",
|
|
687
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
688
|
+
type: "button",
|
|
689
|
+
className: "kb-btn",
|
|
690
|
+
onClick: onClose,
|
|
691
|
+
disabled: busy,
|
|
692
|
+
children: t("cancel")
|
|
693
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
694
|
+
type: "button",
|
|
695
|
+
className: "kb-btn kb-btn-primary",
|
|
696
|
+
onClick: () => {
|
|
697
|
+
submit();
|
|
698
|
+
},
|
|
699
|
+
disabled: busy,
|
|
700
|
+
children: t("addAndRefine")
|
|
701
|
+
})]
|
|
702
|
+
})
|
|
703
|
+
]
|
|
704
|
+
})
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
function DetailPanel({ card, t, onClose, onChanged, onToast, onOpenSession }) {
|
|
708
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
709
|
+
if (card === null) return null;
|
|
710
|
+
const act = async (fn) => {
|
|
711
|
+
setBusy(true);
|
|
712
|
+
try {
|
|
713
|
+
await fn();
|
|
714
|
+
onChanged();
|
|
715
|
+
} catch (error) {
|
|
716
|
+
onToast(error instanceof Error ? error.message : String(error));
|
|
717
|
+
setBusy(false);
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
const remove = async () => {
|
|
721
|
+
if (!window.confirm(t("deleteConfirm"))) return;
|
|
722
|
+
await act(() => api.remove(card.id));
|
|
723
|
+
};
|
|
724
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
725
|
+
className: "kb-detail-panel",
|
|
726
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
727
|
+
className: "kb-detail-panel-head",
|
|
728
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
729
|
+
className: "kb-modal-title",
|
|
730
|
+
children: [card.plan !== void 0 && card.plan.title !== "" ? card.plan.title : firstLine(card.requirement), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
731
|
+
className: "kb-detail-status kb-status-" + card.status,
|
|
732
|
+
children: statusLabel(card, t)
|
|
733
|
+
})]
|
|
734
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
735
|
+
type: "button",
|
|
736
|
+
className: "kb-detail-close",
|
|
737
|
+
"aria-label": t("close"),
|
|
738
|
+
title: t("close"),
|
|
739
|
+
onClick: onClose,
|
|
740
|
+
children: "✕"
|
|
741
|
+
})]
|
|
742
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
743
|
+
className: "kb-detail-panel-body",
|
|
744
|
+
children: [
|
|
745
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
746
|
+
className: "kb-detail-section",
|
|
747
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
748
|
+
className: "kb-detail-label",
|
|
749
|
+
children: t("requirement")
|
|
750
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
751
|
+
className: "kb-detail-text",
|
|
752
|
+
children: card.requirement
|
|
753
|
+
})]
|
|
754
|
+
}),
|
|
755
|
+
card.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
756
|
+
className: "kb-detail-section",
|
|
757
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
758
|
+
className: "kb-detail-label",
|
|
759
|
+
children: t("plan")
|
|
760
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
761
|
+
className: "kb-detail-plan",
|
|
762
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
763
|
+
className: "kb-plan-summary",
|
|
764
|
+
children: card.plan.summary
|
|
765
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
|
|
766
|
+
className: "kb-plan-phases",
|
|
767
|
+
children: card.plan.phases.map((phase, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
768
|
+
className: "kb-plan-phase" + (i === card.currentPhase && card.status === "running" ? " kb-plan-phase-current" : ""),
|
|
769
|
+
children: [
|
|
770
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
771
|
+
className: "kb-plan-phase-head",
|
|
772
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
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
|
+
]
|
|
791
|
+
}, phase.id))
|
|
792
|
+
})]
|
|
793
|
+
})]
|
|
794
|
+
}),
|
|
795
|
+
card.sessions.refinement.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
796
|
+
className: "kb-detail-section",
|
|
797
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
798
|
+
className: "kb-detail-label",
|
|
799
|
+
children: t("refinementLabel")
|
|
800
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
801
|
+
className: "kb-session-links",
|
|
802
|
+
children: card.sessions.refinement.map((sid) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
803
|
+
type: "button",
|
|
804
|
+
className: "kb-btn kb-btn-small",
|
|
805
|
+
onClick: () => onOpenSession(sid),
|
|
806
|
+
children: [
|
|
807
|
+
t("openSession"),
|
|
808
|
+
" · ",
|
|
809
|
+
sid.slice(0, 13)
|
|
810
|
+
]
|
|
811
|
+
}, sid))
|
|
812
|
+
})]
|
|
813
|
+
}),
|
|
814
|
+
card.status === "error" && card.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
815
|
+
className: "kb-detail-section kb-detail-error",
|
|
816
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
817
|
+
className: "kb-detail-label",
|
|
818
|
+
children: t("error")
|
|
819
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
820
|
+
className: "kb-detail-text",
|
|
821
|
+
children: card.error.message
|
|
822
|
+
})]
|
|
823
|
+
}),
|
|
824
|
+
card.merge?.mergeCommit !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
825
|
+
className: "kb-detail-section",
|
|
826
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
827
|
+
className: "kb-detail-label",
|
|
828
|
+
children: t("mergeLabel")
|
|
829
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
830
|
+
className: "kb-detail-text",
|
|
831
|
+
children: [
|
|
832
|
+
t("commitLabel"),
|
|
833
|
+
" ",
|
|
834
|
+
card.merge.mergeCommit.slice(0, 12)
|
|
835
|
+
]
|
|
836
|
+
})]
|
|
837
|
+
}),
|
|
838
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
839
|
+
className: "kb-modal-actions",
|
|
840
|
+
children: [
|
|
841
|
+
card.status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
842
|
+
type: "button",
|
|
843
|
+
className: "kb-btn kb-btn-primary",
|
|
844
|
+
disabled: busy,
|
|
845
|
+
onClick: () => {
|
|
846
|
+
act(() => api.retry(card.id));
|
|
847
|
+
},
|
|
848
|
+
children: t("retry")
|
|
849
|
+
}),
|
|
850
|
+
(card.status === "running" || card.status === "merging") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
851
|
+
type: "button",
|
|
852
|
+
className: "kb-btn",
|
|
853
|
+
disabled: busy,
|
|
854
|
+
onClick: () => {
|
|
855
|
+
act(() => api.stop(card.id));
|
|
856
|
+
},
|
|
857
|
+
children: t("stop")
|
|
858
|
+
}),
|
|
859
|
+
card.status !== "running" && card.status !== "refining" && card.status !== "merging" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
860
|
+
type: "button",
|
|
861
|
+
className: "kb-btn kb-btn-danger",
|
|
862
|
+
disabled: busy,
|
|
863
|
+
onClick: () => {
|
|
864
|
+
remove();
|
|
865
|
+
},
|
|
866
|
+
children: t("delete")
|
|
867
|
+
}),
|
|
868
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
869
|
+
type: "button",
|
|
870
|
+
className: "kb-btn",
|
|
871
|
+
onClick: onClose,
|
|
872
|
+
children: t("cancel")
|
|
873
|
+
})
|
|
874
|
+
]
|
|
875
|
+
})
|
|
876
|
+
]
|
|
877
|
+
})]
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
function PhaseSessions({ phaseIndex, card, t, onOpenSession }) {
|
|
881
|
+
const attempts = card.sessions.phases.filter((a) => a.phaseIndex === phaseIndex);
|
|
882
|
+
if (attempts.length === 0) return null;
|
|
883
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
884
|
+
className: "kb-session-links",
|
|
885
|
+
children: attempts.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
886
|
+
className: "kb-session-attempt",
|
|
887
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
888
|
+
type: "button",
|
|
889
|
+
className: "kb-btn kb-btn-small",
|
|
890
|
+
onClick: () => onOpenSession(a.sessionId),
|
|
891
|
+
children: [
|
|
892
|
+
t("openSession"),
|
|
893
|
+
" · ",
|
|
894
|
+
a.sessionId.slice(0, 13)
|
|
895
|
+
]
|
|
896
|
+
}), a.summary !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
897
|
+
className: "kb-session-summary",
|
|
898
|
+
children: a.summary
|
|
899
|
+
})]
|
|
900
|
+
}, a.sessionId))
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
//#endregion
|
|
904
|
+
//#region src/client/sections.tsx
|
|
905
|
+
/** Sidebar footer entry: toggles the kanban board main view.
|
|
906
|
+
* The shell renders footer actions on their own row ABOVE the settings row,
|
|
907
|
+
* so the button is reparented into the settings area to sit on the same
|
|
908
|
+
* line, right of the Settings trigger (see board.css .kb-settings-row). */
|
|
909
|
+
function KanbanFooterButton({ wide, t }) {
|
|
910
|
+
const open = useBoardOpen();
|
|
911
|
+
const ref = (0, react.useRef)(null);
|
|
912
|
+
const label = t !== void 0 ? t(open ? "closeBoard" : "openBoard") : "Task Kanban";
|
|
913
|
+
(0, react.useEffect)(() => {
|
|
914
|
+
const btn = ref.current;
|
|
915
|
+
if (btn === null) return;
|
|
916
|
+
let foot = null;
|
|
917
|
+
let disposed = false;
|
|
918
|
+
const findFoot = () => {
|
|
919
|
+
let el = btn.parentElement;
|
|
920
|
+
while (el !== null && el !== document.body && el.parentElement !== null) {
|
|
921
|
+
const last = el.lastElementChild;
|
|
922
|
+
const hasTrigger = last !== null && (last.querySelector?.("button[aria-haspopup=\"dialog\"]") !== null || last.querySelector?.("button[aria-haspopup=\"menu\"]") !== null);
|
|
923
|
+
const isLastChild = el.parentElement.lastElementChild === el;
|
|
924
|
+
if (hasTrigger && isLastChild) return el;
|
|
925
|
+
el = el.parentElement;
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
};
|
|
929
|
+
const tryApply = () => {
|
|
930
|
+
if (foot === null) try {
|
|
931
|
+
foot = findFoot();
|
|
932
|
+
} catch {
|
|
933
|
+
foot = null;
|
|
934
|
+
}
|
|
935
|
+
if (foot === null) return false;
|
|
936
|
+
try {
|
|
937
|
+
foot.classList.add("kb-foot-row");
|
|
938
|
+
foot.classList.toggle("kb-foot-rail", wide !== true);
|
|
939
|
+
} catch {}
|
|
940
|
+
return true;
|
|
941
|
+
};
|
|
942
|
+
let applied = false;
|
|
943
|
+
try {
|
|
944
|
+
applied = tryApply();
|
|
945
|
+
} catch (error) {
|
|
946
|
+
console.error("[@fonlan/dsh-task-kanban] foot placement error:", error);
|
|
947
|
+
}
|
|
948
|
+
if (!applied) {
|
|
949
|
+
const observer = new MutationObserver(() => {
|
|
950
|
+
if (disposed) return;
|
|
951
|
+
if (tryApply()) observer.disconnect();
|
|
952
|
+
});
|
|
953
|
+
observer.observe(document.body, {
|
|
954
|
+
childList: true,
|
|
955
|
+
subtree: true
|
|
956
|
+
});
|
|
957
|
+
const timer = window.setInterval(() => {
|
|
958
|
+
if (disposed) {
|
|
959
|
+
window.clearInterval(timer);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (tryApply()) {
|
|
963
|
+
observer.disconnect();
|
|
964
|
+
window.clearInterval(timer);
|
|
965
|
+
}
|
|
966
|
+
}, 1e3);
|
|
967
|
+
window.setTimeout(() => {
|
|
968
|
+
observer.disconnect();
|
|
969
|
+
window.clearInterval(timer);
|
|
970
|
+
if (foot === null) {
|
|
971
|
+
const chain = [];
|
|
972
|
+
let el = btn.parentElement;
|
|
973
|
+
while (el !== null && chain.length < 8) {
|
|
974
|
+
chain.push(el.tagName + "." + String(el.className).slice(0, 60));
|
|
975
|
+
el = el.parentElement;
|
|
976
|
+
}
|
|
977
|
+
console.warn("[@fonlan/dsh-task-kanban] could not locate the sidebar foot area:", chain);
|
|
978
|
+
}
|
|
979
|
+
}, 15e3);
|
|
980
|
+
return () => {
|
|
981
|
+
disposed = true;
|
|
982
|
+
observer.disconnect();
|
|
983
|
+
window.clearInterval(timer);
|
|
984
|
+
try {
|
|
985
|
+
foot?.classList.remove("kb-foot-row");
|
|
986
|
+
foot?.classList.remove("kb-foot-rail");
|
|
987
|
+
} catch {}
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
return () => {
|
|
991
|
+
try {
|
|
992
|
+
foot?.classList.remove("kb-foot-row");
|
|
993
|
+
foot?.classList.remove("kb-foot-rail");
|
|
994
|
+
} catch {}
|
|
995
|
+
};
|
|
996
|
+
}, [wide]);
|
|
997
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
998
|
+
ref,
|
|
999
|
+
type: "button",
|
|
1000
|
+
className: "kb-footer-action" + (wide === true ? " kb-footer-action-wide" : "") + (open ? " kb-footer-action-active" : ""),
|
|
1001
|
+
"aria-label": label,
|
|
1002
|
+
title: label,
|
|
1003
|
+
onClick: () => toggleBoard(),
|
|
1004
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChecklistOutline14, { size: wide === true ? 16 : 18 }), wide === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1005
|
+
className: "kb-footer-label",
|
|
1006
|
+
children: t !== void 0 ? t("kanban") : "Kanban"
|
|
1007
|
+
})]
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
/** Plugin settings section: global parallel workers + default model. */
|
|
1011
|
+
function KanbanSettingsSection({ t }) {
|
|
1012
|
+
const tr = t ?? ((key) => key);
|
|
1013
|
+
const [settings, setSettings] = (0, react.useState)({
|
|
1014
|
+
maxParallelWorkers: 1,
|
|
1015
|
+
defaultModel: ""
|
|
1016
|
+
});
|
|
1017
|
+
const [models, setModels] = (0, react.useState)([]);
|
|
1018
|
+
const [saved, setSaved] = (0, react.useState)(false);
|
|
1019
|
+
(0, react.useEffect)(() => {
|
|
1020
|
+
let alive = true;
|
|
1021
|
+
api.settingsGet().then((s) => {
|
|
1022
|
+
if (alive) setSettings(s);
|
|
1023
|
+
}).catch(() => void 0);
|
|
1024
|
+
api.models().then((list) => {
|
|
1025
|
+
if (alive) setModels(list);
|
|
1026
|
+
}).catch(() => void 0);
|
|
1027
|
+
return () => {
|
|
1028
|
+
alive = false;
|
|
1029
|
+
};
|
|
1030
|
+
}, []);
|
|
1031
|
+
const commit = async (patch) => {
|
|
1032
|
+
try {
|
|
1033
|
+
const next = await api.settingsSet(patch);
|
|
1034
|
+
setSettings(next);
|
|
1035
|
+
setSaved(true);
|
|
1036
|
+
window.setTimeout(() => setSaved(false), 1500);
|
|
1037
|
+
} catch {}
|
|
1038
|
+
};
|
|
1039
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1040
|
+
className: "kb-settings",
|
|
1041
|
+
children: [
|
|
1042
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1043
|
+
className: "kb-field kb-field-row",
|
|
1044
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tr("maxParallelWorkers") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1045
|
+
className: "kb-input kb-input-number",
|
|
1046
|
+
type: "number",
|
|
1047
|
+
min: 1,
|
|
1048
|
+
step: 1,
|
|
1049
|
+
value: settings.maxParallelWorkers,
|
|
1050
|
+
onChange: (e) => {
|
|
1051
|
+
const value = Math.max(1, Math.floor(Number(e.target.value) || 1));
|
|
1052
|
+
setSettings((s) => ({
|
|
1053
|
+
...s,
|
|
1054
|
+
maxParallelWorkers: value
|
|
1055
|
+
}));
|
|
1056
|
+
commit({ maxParallelWorkers: value });
|
|
1057
|
+
}
|
|
1058
|
+
})]
|
|
1059
|
+
}),
|
|
1060
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1061
|
+
className: "kb-field kb-field-row",
|
|
1062
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tr("defaultModel") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1063
|
+
className: "kb-input",
|
|
1064
|
+
value: settings.defaultModel,
|
|
1065
|
+
onChange: (e) => {
|
|
1066
|
+
commit({ defaultModel: e.target.value });
|
|
1067
|
+
},
|
|
1068
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1069
|
+
value: "",
|
|
1070
|
+
children: "—"
|
|
1071
|
+
}), models.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1072
|
+
value: m.id,
|
|
1073
|
+
children: m.name ?? m.id
|
|
1074
|
+
}, m.provider + "/" + m.id))]
|
|
1075
|
+
})]
|
|
1076
|
+
}),
|
|
1077
|
+
saved && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1078
|
+
className: "kb-settings-saved",
|
|
1079
|
+
children: tr("saved")
|
|
1080
|
+
})
|
|
1081
|
+
]
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
//#endregion
|
|
1085
|
+
//#region src/client/index.tsx
|
|
1086
|
+
/** Services required before mounting (provided by the client runtime). */
|
|
1087
|
+
const inject = [
|
|
1088
|
+
"slots",
|
|
1089
|
+
"sessions",
|
|
1090
|
+
"workspaces",
|
|
1091
|
+
"locale"
|
|
1092
|
+
];
|
|
1093
|
+
/** Client plugin body. */
|
|
1094
|
+
function apply(ctx) {
|
|
1095
|
+
setClient(ctx);
|
|
1096
|
+
setBoardRoot(BoardRoot);
|
|
1097
|
+
bindSessionNavigation(ctx);
|
|
1098
|
+
const t = ctx.locale.bind(LOCALE_NS);
|
|
1099
|
+
ctx.effect(() => {
|
|
1100
|
+
const off = ctx.locale.register(LOCALE_NS, {
|
|
1101
|
+
zh,
|
|
1102
|
+
en
|
|
1103
|
+
});
|
|
1104
|
+
return () => off();
|
|
1105
|
+
}, "task-kanban: dictionaries");
|
|
1106
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
1107
|
+
name: "sidebar.footer.action",
|
|
1108
|
+
id: "task-kanban",
|
|
1109
|
+
order: 60,
|
|
1110
|
+
label: () => t("kanban"),
|
|
1111
|
+
locale: LOCALE_NS
|
|
1112
|
+
}, KanbanFooterButton));
|
|
1113
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1114
|
+
name: "settings.section",
|
|
1115
|
+
id: "task-kanban",
|
|
1116
|
+
order: 200,
|
|
1117
|
+
label: () => t("settingsTitle"),
|
|
1118
|
+
locale: LOCALE_NS
|
|
1119
|
+
}, KanbanSettingsSection));
|
|
1120
|
+
}
|
|
1121
|
+
//#endregion
|
|
1122
|
+
exports.apply = apply;
|
|
1123
|
+
exports.inject = inject;
|
|
1124
|
+
return module.exports;
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
//# sourceMappingURL=client.js.map
|