@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/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