@fonlan/dsh-task-kanban 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  ![设置界面](docs/screenshots/kanban-settings.png)
12
12
 
13
- 侧边栏设置 → 插件配置 →「任务看板」卡片可配置全局并行 worker 数,以及细化需求会话与 Phase 实现会话各自的默认模型、推理强度与 Agent 预设。
13
+ 打开看板:侧边栏「新建会话」下方的「任务看板」面板入口(与「插件」并列)。设置页:设置 → 侧边栏「任务看板」页,可配置全局并行 worker 数,以及细化需求会话与 Phase 实现会话各自的默认模型、推理强度与 Agent 预设。
14
14
 
15
15
  ## 功能
16
16
 
@@ -23,7 +23,7 @@
23
23
 
24
24
  ## 设置
25
25
 
26
- 侧边栏设置 → 插件配置中的「任务看板」可展开卡片(`settings.plugin.item` 插槽)支持:
26
+ 设置 → 侧边栏「任务看板」页(`settings.section` 插槽)支持:
27
27
 
28
28
  - **全局并行 worker 数**:跨工作区同时执行的卡片数上限。
29
29
  - **细化需求会话默认 / Phase 实现会话默认**:分别配置两类会话的默认值:
@@ -39,13 +39,13 @@
39
39
 
40
40
  ### 方式一:从 npm 发布包安装
41
41
 
42
- 当前发布版本:`0.2.4`(适配 DSH `0.1.5-rc.2`)。
42
+ 当前发布版本:`0.2.6`(适配 DSH `0.1.7-alpha.1`)。
43
43
 
44
44
  ```sh
45
45
  dsh plugin --profile web add @fonlan/dsh-task-kanban
46
46
  ```
47
47
 
48
- > 如需固定版本,可显式指定版本号:`dsh plugin --profile web add @fonlan/dsh-task-kanban@0.2.4`。
48
+ > 如需固定版本,可显式指定版本号:`dsh plugin --profile web add @fonlan/dsh-task-kanban@0.2.6`。
49
49
 
50
50
  ### 方式二:本地源码链接安装
51
51
 
@@ -56,7 +56,9 @@ pnpm build
56
56
  dsh plugin --profile web add .
57
57
  ```
58
58
 
59
- 安装后插件即通过 `cordis.patch.yml` 自动挂载到 web profile。web profile 会读取该 patch 并把插件注册进 `sidebar.footer.action`(看板入口)、`main.conversation`(看板视图)与 `settings.plugin.item`(插件设置)等插槽,通常重启 web profile 后即可在侧边栏底部看到"任务看板"入口。
59
+ 安装后插件即通过 `cordis.patch.yml` 自动挂载到 web profile。web profile 会读取该 patch,并把看板注册为**全局主面板**:侧边栏「新建会话」下方的面板行出现「任务看板」入口(`sidebar.panellist`,与「插件」并列),点击即把看板切到中央区域(`main` keyed 槽位,key = `task-kanban`);设置页入口走 `settings.section`。点侧栏会话会先把中央区域交还给会话视图。
60
+
61
+ > 入口位置在 DSH `0.1.7` 起改为面板行(旧版是 `sidebar.footer.action` 的侧栏底部按钮):底部那一行归 Settings 独占,插件按钮挤在同一行会把设置触发按钮压窄。
60
62
 
61
63
  ## 开发
62
64
 
package/lib/client.js CHANGED
@@ -4,9 +4,32 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region \0rolldown/runtime.js
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
+ key = keys[i];
17
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
18
+ get: ((k) => from[k]).bind(null, key),
19
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
+ });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
25
+ value: mod,
26
+ enumerable: true
27
+ }) : target, mod));
28
+ //#endregion
7
29
  let react = require("react");
8
30
  let react_jsx_runtime = require("react/jsx-runtime");
9
31
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
32
+ _deepseek_ai_dsh_client_ui_primitives = __toESM(_deepseek_ai_dsh_client_ui_primitives, 1);
10
33
  //#region src/shared/lanes.ts
11
34
  /** Which lane a card renders in, based on status + error stage. */
12
35
  function laneOf(card) {
@@ -122,33 +145,25 @@ window.__ModuleLoader__.load({
122
145
  //#endregion
123
146
  //#region src/client/kanban-state.ts
124
147
  /**
125
- * HMR-safe shared state store keyed on the global symbol registry.
148
+ * Panel id shared by this plugin's main-panel key (`main`) and its sidebar
149
+ * entry (`sidebar.panellist`): the shell selects the panel by this id, so the
150
+ * two registrations must agree.
151
+ */
152
+ const KANBAN_PANEL_ID = "task-kanban";
153
+ /**
154
+ * HMR-safe owner context, keyed on the global symbol registry.
126
155
  *
127
156
  * The client-hmr hot swap re-evaluates this module (a fresh `lib/client.js`
128
- * bundle) WITHOUT reloading the page. A module-level singleton would then be
129
- * duplicated per evaluation: the session-navigation wrapper installed by the
130
- * OLD module copy keeps calling the OLD copy's `exitBoard`, which reads the
131
- * OLD copy's `boardOpen`/`boardDisposer` — while a board opened after the swap
132
- * writes the NEW copy's state cell. The wrapper would then no-op and clicking
133
- * a sidebar session would never leave the board.
134
- *
135
- * Keeping every field on one `globalThis`-backed record (via `Symbol.for`, so
136
- * all bundle copies resolve the same key) makes every copy read/write the same
137
- * state cell, so exit works regardless of which module copy performed the
138
- * enter.
157
+ * bundle) WITHOUT reloading the page; a module-level cell would then be
158
+ * duplicated per evaluation and an old copy's helper would read a stale owner.
159
+ * `Symbol.for` makes every bundle copy share one cell.
139
160
  */
140
161
  const STATE_KEY = Symbol.for("@fonlan/dsh-task-kanban/state");
141
162
  function getState() {
142
163
  const g = globalThis;
143
164
  let state = g[STATE_KEY];
144
165
  if (state === void 0) {
145
- state = {
146
- client: null,
147
- boardRoot: null,
148
- boardDisposer: null,
149
- boardOpen: false,
150
- listeners: /* @__PURE__ */ new Set()
151
- };
166
+ state = { client: null };
152
167
  g[STATE_KEY] = state;
153
168
  }
154
169
  return state;
@@ -159,94 +174,39 @@ window.__ModuleLoader__.load({
159
174
  function getClient() {
160
175
  return getState().client;
161
176
  }
162
- function setBoardRoot(component) {
163
- getState().boardRoot = component;
164
- }
165
- function notify() {
166
- for (const listener of getState().listeners) listener();
167
- }
168
- function isBoardOpen() {
169
- return getState().boardOpen;
170
- }
171
- function subscribe(cb) {
172
- const listeners = getState().listeners;
173
- listeners.add(cb);
174
- return () => {
175
- listeners.delete(cb);
176
- };
177
- }
178
- function useBoardOpen() {
179
- return (0, react.useSyncExternalStore)(subscribe, isBoardOpen);
180
- }
181
- function enterBoard() {
182
- const state = getState();
183
- const ctx = state.client;
184
- const root = state.boardRoot;
185
- if (ctx === null || root === null || state.boardDisposer !== null) return;
186
- try {
187
- state.boardDisposer = ctx.slots.inject("main.conversation", () => ctx.slots.register({
188
- name: "main.conversation",
189
- priority: -1,
190
- locale: "task-kanban"
191
- }, root));
192
- state.boardOpen = true;
193
- notify();
194
- } catch (error) {
195
- console.error("[@fonlan/dsh-task-kanban] cannot open the board:", error);
196
- }
197
- }
198
- function exitBoard() {
199
- const state = getState();
200
- if (state.boardDisposer !== null) {
201
- state.boardDisposer();
202
- state.boardDisposer = null;
203
- }
204
- if (state.boardOpen) {
205
- state.boardOpen = false;
206
- notify();
207
- }
208
- }
209
- function toggleBoard() {
210
- if (isBoardOpen()) exitBoard();
211
- else enterBoard();
212
- }
213
177
  /**
214
- * Route session navigation through the board: opening any session while the
215
- * board is open leaves the board first, so the conversation view can render
216
- * for the newly selected session.
178
+ * Open one session — or a durable direct-parent subagent address — in the
179
+ * center column, leaving the kanban panel.
217
180
  *
218
- * Every sidebar path that selects a session funnels through `sessions.open`
219
- * (session rows, search results, fork results, New Session) or
220
- * `sessions.openSubagent` (catalog children) — including re-clicking the
221
- * already-current session, which never changes `list.current` and therefore
222
- * cannot be caught by a list-store subscription. Wrapping the two entry
223
- * points covers all of them in one place.
181
+ * Navigation is a view-owner concern, not a session-controller one: `ctx.sessions`
182
+ * (`ISessions`) exposes retention (`retain`/`using`/`retainInfo`), creation,
183
+ * fork, search and catalog reads, and has no `open`/`openSubagent` at all
184
+ * ("navigation belongs to view owners"). The view owner the shell itself uses is
185
+ * `ctx.uiWorkspace.openSession(target)` (dsh-client-ui-workspace): it retains the
186
+ * session AND reveals the Conversation panel as ONE navigation action. Routing
187
+ * every open through it therefore replaces the old wrapper around
188
+ * `sessions.open`/`openSubagent`, which existed only to hand the center column
189
+ * back when a sidebar click selected a session while the board was showing.
224
190
  *
225
- * The wrapper is idempotent: a `kbBound` marker on the wrapper prevents a
226
- * second bind (plugin re-apply / HMR) from stacking another layer. Because the
227
- * wrapper's exit goes through the shared state store, a wrapper left over from
228
- * a previous (HMR-replaced) module copy still closes the board correctly.
191
+ * @param target - session id (or subagent address) to show.
192
+ * @returns whether the runtime provided the navigation face; `false` means the
193
+ * session could not be shown (a client without ui-workspace).
229
194
  */
230
- function bindSessionNavigation(ctx) {
231
- const sessions = ctx.sessions;
232
- if (sessions === void 0) return;
233
- const open = sessions.open;
234
- if (typeof open === "function" && open.kbBound !== true) {
235
- const bound = ((id) => {
236
- exitBoard();
237
- return open.call(sessions, id);
238
- });
239
- bound.kbBound = true;
240
- sessions.open = bound;
195
+ function openSessionView(target, ctx = getClient()) {
196
+ if (ctx === null) return false;
197
+ let uiWorkspace;
198
+ try {
199
+ uiWorkspace = ctx.uiWorkspace;
200
+ } catch {
201
+ return false;
241
202
  }
242
- const openSubagent = sessions.openSubagent;
243
- if (typeof openSubagent === "function" && openSubagent.kbBound !== true) {
244
- const bound = ((address) => {
245
- exitBoard();
246
- return openSubagent.call(sessions, address);
247
- });
248
- bound.kbBound = true;
249
- sessions.openSubagent = bound;
203
+ if (typeof uiWorkspace?.openSession !== "function") return false;
204
+ try {
205
+ uiWorkspace.openSession(target);
206
+ return true;
207
+ } catch (error) {
208
+ console.error("[@fonlan/dsh-task-kanban] cannot open the session view:", error);
209
+ return false;
250
210
  }
251
211
  }
252
212
  //#endregion
@@ -400,8 +360,8 @@ window.__ModuleLoader__.load({
400
360
  };
401
361
  //#endregion
402
362
  //#region \0dsh-css:/home/runner/work/dsh-task-kanban/dsh-task-kanban/src/client/board.css.mjs
403
- const css = "/* @fonlan/dsh-task-kanban board styles (kb- prefixed, plain CSS).\n Surfaces use the DSH theme tokens (--dsw-alias-*) so they follow the\n shell's selected light/dark appearance; the var() fallbacks cover shells\n that do not define them. */\n.kb-board {\n height: 100%;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n padding: 12px 16px;\n overflow: hidden;\n color: var(--dsw-alias-label-primary, var(--dsh-fg, #e6e6e6));\n background: var(--dsw-alias-bg-base, var(--dsh-bg, #141418));\n font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;\n}\n.kb-empty {\n align-items: center;\n justify-content: center;\n}\n.kb-empty-text { opacity: 0.6; }\n.kb-board-header {\n display: flex;\n align-items: center;\n gap: 12px;\n padding-bottom: 10px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.25);\n flex: 0 0 auto;\n}\n/* Left-aligned group: workspace selector + new-task button sit adjacent. */\n.kb-board-title {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n flex: 0 1 auto;\n}\n.kb-board-title .kb-btn { flex: none; }\n.kb-ws-select {\n max-width: 320px;\n min-width: 0;\n flex: 0 1 auto;\n padding: 6px 10px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35)));\n background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-1, rgba(128, 128, 128, 0.12)));\n color: var(--dsw-alias-label-primary, inherit);\n font-size: 14px;\n}\n.kb-btn {\n padding: 6px 14px;\n border-radius: 6px;\n border: 1px solid rgba(128, 128, 128, 0.35);\n background: rgba(128, 128, 128, 0.12);\n color: inherit;\n font-size: 13px;\n white-space: nowrap;\n cursor: pointer;\n}\n.kb-btn:hover { background: rgba(128, 128, 128, 0.22); }\n.kb-btn:disabled { opacity: 0.5; cursor: default; }\n.kb-btn-primary { background: #3b82f6; border-color: #3b82f6; color: #fff; }\n.kb-btn-primary:hover { background: #2f6fe0; }\n.kb-btn-danger { background: rgba(239, 68, 68, 0.15); border-color: rgba(239, 68, 68, 0.5); color: #f87171; }\n.kb-btn-small { padding: 2px 8px; font-size: 12px; }\n.kb-lanes {\n flex: 1 1 auto;\n display: flex;\n gap: 12px;\n overflow-x: auto;\n overflow-y: hidden;\n padding-top: 10px;\n min-height: 0;\n}\n.kb-lane {\n flex: 1 1 0;\n min-width: 220px;\n max-width: 320px;\n display: flex;\n flex-direction: column;\n background: rgba(128, 128, 128, 0.07);\n border-radius: 8px;\n border: 1px solid rgba(128, 128, 128, 0.12);\n overflow: hidden;\n}\n.kb-lane-drop { border-style: dashed; border-color: rgba(59, 130, 246, 0.45); }\n.kb-lane-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 10px;\n font-weight: 600;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary, var(--dsh-fg-soft, #b8b8c0));\n border-bottom: 1px solid rgba(128, 128, 128, 0.12);\n flex: 0 0 auto;\n}\n.kb-lane-count {\n background: rgba(128, 128, 128, 0.18);\n border-radius: 10px;\n padding: 0 8px;\n font-size: 12px;\n}\n.kb-lane-body {\n flex: 1 1 auto;\n overflow-y: auto;\n padding: 8px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.kb-card {\n background: rgba(128, 128, 128, 0.1);\n border: 1px solid rgba(128, 128, 128, 0.2);\n border-radius: 8px;\n padding: 8px 10px;\n cursor: pointer;\n user-select: none;\n}\n.kb-card:hover { border-color: rgba(128, 128, 128, 0.45); }\n.kb-card[draggable='true'] { cursor: grab; }\n.kb-card-running { border-color: rgba(59, 130, 246, 0.6); }\n.kb-card-error { border-color: rgba(239, 68, 68, 0.6); }\n.kb-card-title { font-weight: 600; font-size: 13px; margin-bottom: 6px; word-break: break-all; }\n.kb-card-meta { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; font-size: 12px; }\n.kb-card-badge {\n background: rgba(128, 128, 128, 0.2);\n border-radius: 4px;\n padding: 1px 6px;\n}\n.kb-card-running .kb-card-badge { background: rgba(59, 130, 246, 0.3); }\n.kb-card-error .kb-card-badge { background: rgba(239, 68, 68, 0.3); }\n.kb-card-phase { opacity: 0.8; }\n.kb-card-skill {\n background: rgba(139, 92, 246, 0.22);\n border-radius: 4px;\n padding: 1px 6px;\n}\n.kb-card-model { opacity: 0.55; }\n.kb-card-error-text { margin-top: 6px; font-size: 12px; color: #f87171; word-break: break-all; }\n.kb-card-worktree { margin-top: 4px; font-size: 11px; opacity: 0.5; word-break: break-all; }\n.kb-toast {\n position: fixed;\n left: 50%;\n bottom: 24px;\n transform: translateX(-50%);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-primary);\n padding: 8px 16px;\n border-radius: 8px;\n font-size: 13px;\n z-index: 2147483000;\n max-width: 70vw;\n}\n.kb-modal-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 2147482900;\n}\n.kb-modal {\n width: 460px;\n max-width: 90vw;\n max-height: 85vh;\n overflow-y: auto;\n background: var(--dsw-alias-bg-layer-1);\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n padding: 18px;\n color: var(--dsw-alias-label-primary);\n}\n.kb-modal-wide { width: 640px; }\n/* Task-detail bottom panel: docks at the bottom of the board page, fills the\n full board width and exactly half of the board height (the lanes column\n above flexes to the remaining space). */\n.kb-detail-panel {\n flex: 0 0 50%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n margin-top: 10px;\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n overflow: hidden;\n color: var(--dsw-alias-label-primary);\n}\n.kb-detail-panel-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 10px 14px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.2);\n flex: 0 0 auto;\n}\n.kb-detail-panel-head .kb-modal-title { margin-bottom: 0; }\n.kb-detail-panel-body {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n padding: 14px;\n}\n.kb-detail-close {\n flex: none;\n width: 28px;\n height: 28px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 1px solid transparent;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font-size: 15px;\n line-height: 1;\n opacity: 0.75;\n cursor: pointer;\n}\n.kb-detail-close:hover { background: rgba(128, 128, 128, 0.18); opacity: 1; }\n.kb-modal-title { font-size: 16px; font-weight: 700; margin-bottom: 14px; display: flex; align-items: center; gap: 10px; }\n.kb-field { display: block; margin-bottom: 12px; }\n.kb-field > span { display: block; font-size: 12px; opacity: 0.75; margin-bottom: 4px; }\n.kb-field-hint {\n display: block;\n font-size: 11px;\n opacity: 0.55;\n margin-top: 4px;\n}\n.kb-field-hint-skill {\n opacity: 0.9;\n color: #a78bfa;\n}\n.kb-skill-menu-wrap { position: relative; }\n.kb-skill-menu {\n position: absolute;\n top: 100%;\n left: 0;\n right: 0;\n z-index: 20;\n margin: 2px 0 0;\n padding: 4px;\n list-style: none;\n max-height: 180px;\n overflow-y: auto;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-overlay);\n color: var(--dsw-alias-label-primary);\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);\n}\n.kb-skill-menu-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px 8px;\n border-radius: 6px;\n cursor: pointer;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n}\n.kb-skill-menu-item-active { background: var(--dsw-alias-interactive-bg-hover); }\n.kb-skill-menu-name { font-weight: 600; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n.kb-skill-menu-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.kb-field-row { display: flex; align-items: center; gap: 10px; justify-content: space-between; }\n/* Field labels in a row never wrap; selects get the remaining fixed width. */\n.kb-field-row > span { white-space: nowrap; flex-shrink: 0; margin-bottom: 0; }\n.kb-input {\n width: 100%;\n box-sizing: border-box;\n padding: 7px 10px;\n border-radius: 6px;\n /* Form controls follow the shell theme: DSH's input surface token plus a\n theme-tuned border; the fallbacks cover shells without these tokens. */\n border: 1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35)));\n background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-1, rgba(128, 128, 128, 0.1)));\n color: var(--dsw-alias-label-primary, inherit);\n font-size: 13px;\n}\n.kb-input-number { width: 90px; }\n/* All settings selects share one width so the column looks even. */\n.kb-settings select.kb-input { width: 200px; flex: 0 0 200px; }\n.kb-textarea { min-height: 110px; resize: vertical; font-family: inherit; }\n.kb-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }\n.kb-detail-section { margin-bottom: 12px; }\n.kb-detail-label { font-size: 12px; opacity: 0.65; margin-bottom: 4px; }\n.kb-detail-skill {\n display: inline-block;\n font-size: 11px;\n background: rgba(139, 92, 246, 0.22);\n border-radius: 4px;\n padding: 1px 6px;\n margin-bottom: 4px;\n}\n.kb-detail-text { white-space: pre-wrap; word-break: break-all; font-size: 13px; }\n.kb-detail-error .kb-detail-text { color: #f87171; }\n.kb-detail-status {\n font-size: 12px;\n font-weight: 600;\n border-radius: 4px;\n padding: 1px 8px;\n background: rgba(128, 128, 128, 0.2);\n}\n.kb-status-running, .kb-status-merging { background: rgba(59, 130, 246, 0.3) !important; }\n.kb-status-error { background: rgba(239, 68, 68, 0.3) !important; color: #f87171; }\n.kb-status-merged { background: rgba(34, 197, 94, 0.3) !important; }\n.kb-plan-summary { font-size: 13px; margin-bottom: 10px; }\n/* Phase cards: each plan phase is a card with an ordered number (kept from the\n parent <ol>) and three labeled sections (goal / sessions / conclusion) split\n by section titles and thin divider lines. */\n.kb-plan-phases { margin: 0; padding-left: 24px; }\n.kb-plan-phase {\n margin-bottom: 10px;\n padding: 8px 10px;\n background: rgba(128, 128, 128, 0.08);\n border: 1px solid rgba(128, 128, 128, 0.2);\n border-radius: 8px;\n}\n.kb-plan-phase-head {\n display: flex;\n gap: 8px;\n align-items: baseline;\n padding-bottom: 6px;\n margin-bottom: 6px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.16);\n}\n.kb-plan-phase-title { font-weight: 600; font-size: 13px; }\n.kb-plan-phase-id { font-size: 11px; opacity: 0.5; }\n.kb-plan-phase-body { display: flex; flex-direction: column; }\n/* Section isolation: label on top, thin divider between adjacent sections. */\n.kb-plan-phase-section { display: flex; flex-direction: column; }\n.kb-plan-phase-section + .kb-plan-phase-section {\n margin-top: 6px;\n padding-top: 6px;\n border-top: 1px solid rgba(128, 128, 128, 0.14);\n}\n.kb-plan-phase-section-title { font-size: 11px; opacity: 0.55; margin-bottom: 3px; }\n.kb-plan-phase-goal { font-size: 12px; opacity: 0.85; white-space: pre-wrap; word-break: break-all; }\n.kb-plan-phase-sessions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\n.kb-plan-phase-conclusion { font-size: 12px; opacity: 0.85; white-space: pre-wrap; word-break: break-all; }\n.kb-plan-phase-conclusion-item + .kb-plan-phase-conclusion-item { margin-top: 6px; }\n/* Running phase keeps the blue highlight; completed phases get success color. */\n.kb-plan-phase-current { border-color: rgba(59, 130, 246, 0.6); background: rgba(59, 130, 246, 0.08); }\n.kb-plan-phase-current .kb-plan-phase-title { color: #60a5fa; }\n.kb-plan-phase-completed { border-color: rgba(34, 197, 94, 0.45); background: rgba(34, 197, 94, 0.06); }\n.kb-plan-phase-completed .kb-plan-phase-title { color: #4ade80; }\n.kb-session-links { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\n.kb-settings { display: flex; flex-direction: column; gap: 12px; max-width: 420px; }\n.kb-settings-saved { font-size: 12px; color: #4ade80; }\n.kb-settings-group { border-top: 1px solid rgba(128, 128, 128, 0.22); padding-top: 12px; }\n.kb-settings-group-title { font-size: 12px; font-weight: 600; opacity: 0.9; margin-bottom: 10px; }\n/* Settings page (设置 -> 侧栏「任务看板」): the page owns one entry in the\n settings sidebar and renders into the panel's content column, so it draws\n its own header (title plus a one-line summary) above the always-visible\n body. The body keeps the board's kb-settings control classes. */\n.kb-page {\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-width: 720px;\n}\n.kb-page-head {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.kb-page-title {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n}\n.kb-page-sub {\n margin: 0;\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n line-height: 1.5;\n}\n.kb-footer-action {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n width: 32px;\n height: 32px;\n border-radius: 6px;\n border: none;\n background: transparent;\n /* same foreground as the settings trigger (its trigger css uses this var) */\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n}\n.kb-footer-action:hover { background: rgba(128, 128, 128, 0.18); }\n.kb-footer-action-active { background: rgba(59, 130, 246, 0.25); }\n/* Wide mode borrows the settings trigger's metrics (34px tall, 14px label,\n 18px icon) so both controls read as siblings on the same line. */\n.kb-footer-action-wide {\n width: auto;\n height: 34px;\n padding: 0 8px;\n border-radius: 12px;\n font-size: 14px;\n}\n.kb-footer-action-wide .kb-footer-label {\n font-size: 14px;\n line-height: 22px;\n}\n/* Theme following is handled by the --dsw-alias-* tokens above: they change\n value with the shell's selected light/dark appearance, so no OS-level\n prefers-color-scheme override is needed (and one would fight the app\n theme when the two disagree). */\n/* Same-line placement WITHOUT moving any DOM: the sidebar foot area becomes\n a reversed flex row, so its LAST child (the settings area) renders on the\n LEFT and the FIRST child (footer actions = the kanban button) on the RIGHT\n — exactly \"settings, then kanban\". The settings trigger flexes to fill the\n remaining width; the button keeps its content width. */\n.kb-foot-row {\n display: flex;\n flex-direction: row-reverse;\n align-items: stretch;\n gap: 4px;\n}\n.kb-foot-row > div:first-child {\n flex: none;\n width: auto;\n min-width: 0;\n display: flex;\n align-items: center;\n}\n.kb-foot-row > div:last-child {\n flex: 1;\n min-width: 0;\n}\n/* Rail (collapsed sidebar): column-reverse stacks the settings circle on top\n and the kanban icon below it, both centered; the kanban button borrows the\n settings trigger's rail chrome (36px circle) so the two match. */\n.kb-foot-rail {\n flex-direction: column-reverse;\n align-items: center;\n}\n.kb-foot-rail .kb-footer-action {\n width: 36px;\n height: 36px;\n border-radius: 50%;\n margin: 8px 0 10px;\n}\n.kb-footer-label {\n max-width: 110px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n/* Narrow screens (mobile drawer / auto-collapse breakpoint): the drawer's\n foot area is ~256-280px but the footer actions row carries the kanban\n button PLUS mobile-nav's drawer actions (Files / session log), which\n together exceed the row and squeeze the settings trigger to zero width\n (row-reverse overflows left, clipping the button). Below the shell's\n auto-collapse breakpoint, stack the settings row above the actions and\n let the actions wrap so nothing is squeezed or clipped. */\n@media (max-width: 1023px) {\n .kb-foot-row {\n flex-direction: column-reverse;\n align-items: stretch;\n }\n .kb-foot-row > div:first-child {\n flex: none;\n width: 100%;\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n gap: 4px;\n padding-top: 4px;\n }\n .kb-foot-row > div:last-child {\n flex: none;\n width: 100%;\n }\n /* The footer-actions entries are display:contents wrappers, so the kanban\n button itself is a flex item of the wrapping actions row; pull it to the\n first wrap position so it sits directly below the settings row (the\n mobile-nav drawer actions follow on their own wrapped line). */\n .kb-foot-row .kb-footer-action {\n order: -1;\n }\n}";
404
- const tagId = "@fonlan/dsh-task-kanban/board.css#455e1b16";
363
+ const css = "/* @fonlan/dsh-task-kanban board styles (kb- prefixed, plain CSS).\n Surfaces use the DSH theme tokens (--dsw-alias-*) so they follow the\n shell's selected light/dark appearance; the var() fallbacks cover shells\n that do not define them. */\n.kb-board {\n height: 100%;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n padding: 12px 16px;\n overflow: hidden;\n color: var(--dsw-alias-label-primary, var(--dsh-fg, #e6e6e6));\n background: var(--dsw-alias-bg-base, var(--dsh-bg, #141418));\n font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;\n}\n.kb-empty {\n align-items: center;\n justify-content: center;\n}\n.kb-empty-text { opacity: 0.6; }\n.kb-board-header {\n display: flex;\n align-items: center;\n gap: 12px;\n padding-bottom: 10px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.25);\n flex: 0 0 auto;\n}\n/* Left-aligned group: workspace selector + new-task button sit adjacent. */\n.kb-board-title {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n flex: 0 1 auto;\n}\n.kb-board-title .kb-btn { flex: none; }\n.kb-ws-select {\n max-width: 320px;\n min-width: 0;\n flex: 0 1 auto;\n padding: 6px 10px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35)));\n background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-1, rgba(128, 128, 128, 0.12)));\n color: var(--dsw-alias-label-primary, inherit);\n font-size: 14px;\n}\n.kb-btn {\n padding: 6px 14px;\n border-radius: 6px;\n border: 1px solid rgba(128, 128, 128, 0.35);\n background: rgba(128, 128, 128, 0.12);\n color: inherit;\n font-size: 13px;\n white-space: nowrap;\n cursor: pointer;\n}\n.kb-btn:hover { background: rgba(128, 128, 128, 0.22); }\n.kb-btn:disabled { opacity: 0.5; cursor: default; }\n.kb-btn-primary { background: #3b82f6; border-color: #3b82f6; color: #fff; }\n.kb-btn-primary:hover { background: #2f6fe0; }\n.kb-btn-danger { background: rgba(239, 68, 68, 0.15); border-color: rgba(239, 68, 68, 0.5); color: #f87171; }\n.kb-btn-small { padding: 2px 8px; font-size: 12px; }\n.kb-lanes {\n flex: 1 1 auto;\n display: flex;\n gap: 12px;\n overflow-x: auto;\n overflow-y: hidden;\n padding-top: 10px;\n min-height: 0;\n}\n.kb-lane {\n flex: 1 1 0;\n min-width: 220px;\n max-width: 320px;\n display: flex;\n flex-direction: column;\n background: rgba(128, 128, 128, 0.07);\n border-radius: 8px;\n border: 1px solid rgba(128, 128, 128, 0.12);\n overflow: hidden;\n}\n.kb-lane-drop { border-style: dashed; border-color: rgba(59, 130, 246, 0.45); }\n.kb-lane-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 10px;\n font-weight: 600;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary, var(--dsh-fg-soft, #b8b8c0));\n border-bottom: 1px solid rgba(128, 128, 128, 0.12);\n flex: 0 0 auto;\n}\n.kb-lane-count {\n background: rgba(128, 128, 128, 0.18);\n border-radius: 10px;\n padding: 0 8px;\n font-size: 12px;\n}\n.kb-lane-body {\n flex: 1 1 auto;\n overflow-y: auto;\n padding: 8px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.kb-card {\n background: rgba(128, 128, 128, 0.1);\n border: 1px solid rgba(128, 128, 128, 0.2);\n border-radius: 8px;\n padding: 8px 10px;\n cursor: pointer;\n user-select: none;\n}\n.kb-card:hover { border-color: rgba(128, 128, 128, 0.45); }\n.kb-card[draggable='true'] { cursor: grab; }\n.kb-card-running { border-color: rgba(59, 130, 246, 0.6); }\n.kb-card-error { border-color: rgba(239, 68, 68, 0.6); }\n.kb-card-title { font-weight: 600; font-size: 13px; margin-bottom: 6px; word-break: break-all; }\n.kb-card-meta { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; font-size: 12px; }\n.kb-card-badge {\n background: rgba(128, 128, 128, 0.2);\n border-radius: 4px;\n padding: 1px 6px;\n}\n.kb-card-running .kb-card-badge { background: rgba(59, 130, 246, 0.3); }\n.kb-card-error .kb-card-badge { background: rgba(239, 68, 68, 0.3); }\n.kb-card-phase { opacity: 0.8; }\n.kb-card-skill {\n background: rgba(139, 92, 246, 0.22);\n border-radius: 4px;\n padding: 1px 6px;\n}\n.kb-card-model { opacity: 0.55; }\n.kb-card-error-text { margin-top: 6px; font-size: 12px; color: #f87171; word-break: break-all; }\n.kb-card-worktree { margin-top: 4px; font-size: 11px; opacity: 0.5; word-break: break-all; }\n.kb-toast {\n position: fixed;\n left: 50%;\n bottom: 24px;\n transform: translateX(-50%);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-primary);\n padding: 8px 16px;\n border-radius: 8px;\n font-size: 13px;\n z-index: 2147483000;\n max-width: 70vw;\n}\n.kb-modal-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 2147482900;\n}\n.kb-modal {\n width: 460px;\n max-width: 90vw;\n max-height: 85vh;\n overflow-y: auto;\n background: var(--dsw-alias-bg-layer-1);\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n padding: 18px;\n color: var(--dsw-alias-label-primary);\n}\n.kb-modal-wide { width: 640px; }\n/* Task-detail bottom panel: docks at the bottom of the board page, fills the\n full board width and exactly half of the board height (the lanes column\n above flexes to the remaining space). */\n.kb-detail-panel {\n flex: 0 0 50%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n margin-top: 10px;\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n overflow: hidden;\n color: var(--dsw-alias-label-primary);\n}\n.kb-detail-panel-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 10px 14px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.2);\n flex: 0 0 auto;\n}\n.kb-detail-panel-head .kb-modal-title { margin-bottom: 0; }\n.kb-detail-panel-body {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n padding: 14px;\n}\n.kb-detail-close {\n flex: none;\n width: 28px;\n height: 28px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 1px solid transparent;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font-size: 15px;\n line-height: 1;\n opacity: 0.75;\n cursor: pointer;\n}\n.kb-detail-close:hover { background: rgba(128, 128, 128, 0.18); opacity: 1; }\n.kb-modal-title { font-size: 16px; font-weight: 700; margin-bottom: 14px; display: flex; align-items: center; gap: 10px; }\n.kb-field { display: block; margin-bottom: 12px; }\n.kb-field > span { display: block; font-size: 12px; opacity: 0.75; margin-bottom: 4px; }\n.kb-field-hint {\n display: block;\n font-size: 11px;\n opacity: 0.55;\n margin-top: 4px;\n}\n.kb-field-hint-skill {\n opacity: 0.9;\n color: #a78bfa;\n}\n.kb-skill-menu-wrap { position: relative; }\n.kb-skill-menu {\n position: absolute;\n top: 100%;\n left: 0;\n right: 0;\n z-index: 20;\n margin: 2px 0 0;\n padding: 4px;\n list-style: none;\n max-height: 180px;\n overflow-y: auto;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-overlay);\n color: var(--dsw-alias-label-primary);\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);\n}\n.kb-skill-menu-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px 8px;\n border-radius: 6px;\n cursor: pointer;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n}\n.kb-skill-menu-item-active { background: var(--dsw-alias-interactive-bg-hover); }\n.kb-skill-menu-name { font-weight: 600; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n.kb-skill-menu-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.kb-field-row { display: flex; align-items: center; gap: 10px; justify-content: space-between; }\n/* Field labels in a row never wrap; selects get the remaining fixed width. */\n.kb-field-row > span { white-space: nowrap; flex-shrink: 0; margin-bottom: 0; }\n.kb-input {\n width: 100%;\n box-sizing: border-box;\n padding: 7px 10px;\n border-radius: 6px;\n /* Form controls follow the shell theme: DSH's input surface token plus a\n theme-tuned border; the fallbacks cover shells without these tokens. */\n border: 1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35)));\n background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-1, rgba(128, 128, 128, 0.1)));\n color: var(--dsw-alias-label-primary, inherit);\n font-size: 13px;\n}\n.kb-input-number { width: 90px; }\n/* All settings selects share one width so the column looks even. */\n.kb-settings select.kb-input { width: 200px; flex: 0 0 200px; }\n.kb-textarea { min-height: 110px; resize: vertical; font-family: inherit; }\n.kb-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }\n.kb-detail-section { margin-bottom: 12px; }\n.kb-detail-label { font-size: 12px; opacity: 0.65; margin-bottom: 4px; }\n.kb-detail-skill {\n display: inline-block;\n font-size: 11px;\n background: rgba(139, 92, 246, 0.22);\n border-radius: 4px;\n padding: 1px 6px;\n margin-bottom: 4px;\n}\n.kb-detail-text { white-space: pre-wrap; word-break: break-all; font-size: 13px; }\n.kb-detail-error .kb-detail-text { color: #f87171; }\n.kb-detail-status {\n font-size: 12px;\n font-weight: 600;\n border-radius: 4px;\n padding: 1px 8px;\n background: rgba(128, 128, 128, 0.2);\n}\n.kb-status-running, .kb-status-merging { background: rgba(59, 130, 246, 0.3) !important; }\n.kb-status-error { background: rgba(239, 68, 68, 0.3) !important; color: #f87171; }\n.kb-status-merged { background: rgba(34, 197, 94, 0.3) !important; }\n.kb-plan-summary { font-size: 13px; margin-bottom: 10px; }\n/* Phase cards: each plan phase is a card with an ordered number (kept from the\n parent <ol>) and three labeled sections (goal / sessions / conclusion) split\n by section titles and thin divider lines. */\n.kb-plan-phases { margin: 0; padding-left: 24px; }\n.kb-plan-phase {\n margin-bottom: 10px;\n padding: 8px 10px;\n background: rgba(128, 128, 128, 0.08);\n border: 1px solid rgba(128, 128, 128, 0.2);\n border-radius: 8px;\n}\n.kb-plan-phase-head {\n display: flex;\n gap: 8px;\n align-items: baseline;\n padding-bottom: 6px;\n margin-bottom: 6px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.16);\n}\n.kb-plan-phase-title { font-weight: 600; font-size: 13px; }\n.kb-plan-phase-id { font-size: 11px; opacity: 0.5; }\n.kb-plan-phase-body { display: flex; flex-direction: column; }\n/* Section isolation: label on top, thin divider between adjacent sections. */\n.kb-plan-phase-section { display: flex; flex-direction: column; }\n.kb-plan-phase-section + .kb-plan-phase-section {\n margin-top: 6px;\n padding-top: 6px;\n border-top: 1px solid rgba(128, 128, 128, 0.14);\n}\n.kb-plan-phase-section-title { font-size: 11px; opacity: 0.55; margin-bottom: 3px; }\n.kb-plan-phase-goal { font-size: 12px; opacity: 0.85; white-space: pre-wrap; word-break: break-all; }\n.kb-plan-phase-sessions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\n.kb-plan-phase-conclusion { font-size: 12px; opacity: 0.85; white-space: pre-wrap; word-break: break-all; }\n.kb-plan-phase-conclusion-item + .kb-plan-phase-conclusion-item { margin-top: 6px; }\n/* Running phase keeps the blue highlight; completed phases get success color. */\n.kb-plan-phase-current { border-color: rgba(59, 130, 246, 0.6); background: rgba(59, 130, 246, 0.08); }\n.kb-plan-phase-current .kb-plan-phase-title { color: #60a5fa; }\n.kb-plan-phase-completed { border-color: rgba(34, 197, 94, 0.45); background: rgba(34, 197, 94, 0.06); }\n.kb-plan-phase-completed .kb-plan-phase-title { color: #4ade80; }\n.kb-session-links { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\n.kb-settings { display: flex; flex-direction: column; gap: 12px; max-width: 420px; }\n.kb-settings-saved { font-size: 12px; color: #4ade80; }\n.kb-settings-group { border-top: 1px solid rgba(128, 128, 128, 0.22); padding-top: 12px; }\n.kb-settings-group-title { font-size: 12px; font-weight: 600; opacity: 0.9; margin-bottom: 10px; }\n/* Settings page (设置 -> 侧栏「任务看板」): the page owns one entry in the\n settings sidebar and renders into the panel's content column, so it draws\n its own header (title plus a one-line summary) above the always-visible\n body. The body keeps the board's kb-settings control classes. */\n.kb-page {\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-width: 720px;\n}\n.kb-page-head {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.kb-page-title {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n}\n.kb-page-sub {\n margin: 0;\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n line-height: 1.5;\n}\n";
364
+ const tagId = "@fonlan/dsh-task-kanban/board.css#3d30e9c5";
405
365
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
406
366
  const tag = document.createElement("style");
407
367
  tag.dataset.plugin = "@fonlan/dsh-task-kanban";
@@ -549,14 +509,7 @@ window.__ModuleLoader__.load({
549
509
  t
550
510
  ]);
551
511
  const openSession = (0, react.useCallback)((sessionId) => {
552
- const ctx = getClient();
553
- exitBoard();
554
- if (ctx === null) return;
555
- try {
556
- ctx.sessions.open(sessionId);
557
- } catch {
558
- showToast(t("sessionNotListed"));
559
- }
512
+ if (!openSessionView(sessionId)) showToast(t("sessionNotListed"));
560
513
  }, [showToast, t]);
561
514
  if (items.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
562
515
  className: "kb-board kb-empty",
@@ -1171,112 +1124,81 @@ window.__ModuleLoader__.load({
1171
1124
  });
1172
1125
  }
1173
1126
  //#endregion
1174
- //#region src/client/sections.tsx
1175
- /** Sidebar footer entry: toggles the kanban board main view.
1176
- * The shell renders footer actions on their own row ABOVE the settings row,
1177
- * so the button is reparented into the settings area to sit on the same
1178
- * line, right of the Settings trigger (see board.css .kb-settings-row). */
1179
- function KanbanFooterButton({ wide, t }) {
1180
- const open = useBoardOpen();
1181
- const ref = (0, react.useRef)(null);
1182
- const label = t !== void 0 ? t(open ? "closeBoard" : "openBoard") : "Task Kanban";
1183
- (0, react.useEffect)(() => {
1184
- const btn = ref.current;
1185
- if (btn === null) return;
1186
- let foot = null;
1187
- let disposed = false;
1188
- const findFoot = () => {
1189
- let el = btn.parentElement;
1190
- while (el !== null && el !== document.body && el.parentElement !== null) {
1191
- const last = el.lastElementChild;
1192
- const hasTrigger = last !== null && (last.querySelector?.("button[aria-haspopup=\"dialog\"]") !== null || last.querySelector?.("button[aria-haspopup=\"menu\"]") !== null);
1193
- const isLastChild = el.parentElement.lastElementChild === el;
1194
- if (hasTrigger && isLastChild) return el;
1195
- el = el.parentElement;
1196
- }
1197
- return null;
1198
- };
1199
- const tryApply = () => {
1200
- if (foot === null) try {
1201
- foot = findFoot();
1202
- } catch {
1203
- foot = null;
1204
- }
1205
- if (foot === null) return false;
1206
- try {
1207
- foot.classList.add("kb-foot-row");
1208
- foot.classList.toggle("kb-foot-rail", wide !== true);
1209
- } catch {}
1210
- return true;
1211
- };
1212
- let applied = false;
1127
+ //#region src/client/icons.tsx
1128
+ /** The runtime export table, read by capability instead of by one hard-coded name. */
1129
+ const table = _deepseek_ai_dsh_client_ui_primitives;
1130
+ /** First name the runtime actually provides (a missing export is `undefined`). */
1131
+ function firstDefined(...names) {
1132
+ for (const name of names) {
1133
+ let candidate;
1213
1134
  try {
1214
- applied = tryApply();
1215
- } catch (error) {
1216
- console.error("[@fonlan/dsh-task-kanban] foot placement error:", error);
1217
- }
1218
- if (!applied) {
1219
- const observer = new MutationObserver(() => {
1220
- if (disposed) return;
1221
- if (tryApply()) observer.disconnect();
1222
- });
1223
- observer.observe(document.body, {
1224
- childList: true,
1225
- subtree: true
1226
- });
1227
- const timer = window.setInterval(() => {
1228
- if (disposed) {
1229
- window.clearInterval(timer);
1230
- return;
1231
- }
1232
- if (tryApply()) {
1233
- observer.disconnect();
1234
- window.clearInterval(timer);
1235
- }
1236
- }, 1e3);
1237
- window.setTimeout(() => {
1238
- observer.disconnect();
1239
- window.clearInterval(timer);
1240
- if (foot === null) {
1241
- const chain = [];
1242
- let el = btn.parentElement;
1243
- while (el !== null && chain.length < 8) {
1244
- chain.push(el.tagName + "." + String(el.className).slice(0, 60));
1245
- el = el.parentElement;
1246
- }
1247
- console.warn("[@fonlan/dsh-task-kanban] could not locate the sidebar foot area:", chain);
1248
- }
1249
- }, 15e3);
1250
- return () => {
1251
- disposed = true;
1252
- observer.disconnect();
1253
- window.clearInterval(timer);
1254
- try {
1255
- foot?.classList.remove("kb-foot-row");
1256
- foot?.classList.remove("kb-foot-rail");
1257
- } catch {}
1258
- };
1135
+ candidate = table[name];
1136
+ } catch {
1137
+ continue;
1259
1138
  }
1260
- return () => {
1261
- try {
1262
- foot?.classList.remove("kb-foot-row");
1263
- foot?.classList.remove("kb-foot-rail");
1264
- } catch {}
1265
- };
1266
- }, [wide]);
1267
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1268
- ref,
1269
- type: "button",
1270
- className: "kb-footer-action" + (wide === true ? " kb-footer-action-wide" : "") + (open ? " kb-footer-action-active" : ""),
1271
- "aria-label": label,
1272
- title: label,
1273
- onClick: () => toggleBoard(),
1274
- 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", {
1275
- className: "kb-footer-label",
1276
- children: t !== void 0 ? t("kanban") : "Kanban"
1277
- })]
1139
+ if (typeof candidate === "function") return candidate;
1140
+ }
1141
+ }
1142
+ /**
1143
+ * Self-contained 16×16 checklist glyph (the 0.1.7 artwork) used when the shell
1144
+ * primitives expose none of the names above: the footer entry must render
1145
+ * something even on a runtime whose icon exports we do not know.
1146
+ */
1147
+ function ChecklistFallback({ size = 14, className }) {
1148
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1149
+ width: size,
1150
+ height: size,
1151
+ className,
1152
+ viewBox: "0 0 16 16",
1153
+ fill: "none",
1154
+ xmlns: "http://www.w3.org/2000/svg",
1155
+ "aria-hidden": "true",
1156
+ strokeWidth: 1,
1157
+ children: [
1158
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1159
+ d: "M3.75 6.25C4.7165 6.25 5.5 5.4665 5.5 4.5C5.5 3.5335 4.7165 2.75 3.75 2.75C2.7835 2.75 2 3.5335 2 4.5C2 5.4665 2.7835 6.25 3.75 6.25Z",
1160
+ stroke: "currentColor"
1161
+ }),
1162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1163
+ d: "M7.5 4.5H13.5",
1164
+ stroke: "currentColor"
1165
+ }),
1166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1167
+ d: "M3.75 13.25C4.7165 13.25 5.5 12.4665 5.5 11.5C5.5 10.5335 4.7165 9.75 3.75 9.75C2.7835 9.75 2 10.5335 2 11.5C2 12.4665 2.7835 13.25 3.75 13.25Z",
1168
+ stroke: "currentColor"
1169
+ }),
1170
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1171
+ d: "M7.5 11.5H13.5",
1172
+ stroke: "currentColor"
1173
+ })
1174
+ ]
1278
1175
  });
1279
1176
  }
1177
+ /**
1178
+ * The checklist icon used by the sidebar panel row entry.
1179
+ *
1180
+ * DSH 0.1.7 renamed every size-suffixed shell icon (`IconChecklistOutline14`)
1181
+ * into stroke-weight variants (`…Regular` 1px / `…Medium` 1.3px) inside
1182
+ * `@deepseek-ai/dsh-client-ui-primitives`. A stale named import resolves to
1183
+ * `undefined` and React then throws "Element type is invalid" while rendering
1184
+ * the slot entry — the entry is dropped and the button silently disappears
1185
+ * (exactly what upgrading to 0.1.7 did to this plugin). Resolve the name at
1186
+ * runtime, newest first, so the next rename degrades to another shipped
1187
+ * variant instead of taking the button down with it.
1188
+ */
1189
+ const ChecklistIcon = firstDefined("IconChecklistOutlineRegular", "IconChecklistOutlineMedium", "IconChecklistOutline", "IconChecklistOutline14") ?? ChecklistFallback;
1190
+ //#endregion
1191
+ //#region src/client/sections.tsx
1192
+ /**
1193
+ * Sidebar panel-row glyph for the board (`sidebar.panellist`).
1194
+ *
1195
+ * The shell owns the row: it renders the button, its label, tooltip and active
1196
+ * state, and calls `layout.selectPanel(id)` on click. The entry contributes
1197
+ * only the glyph, so this component must stay purely presentational.
1198
+ */
1199
+ function KanbanPanelIcon({ size }) {
1200
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChecklistIcon, { size });
1201
+ }
1280
1202
  /** Fallback shape while settings load (server always resolves the full set). */
1281
1203
  const EMPTY_SETTINGS = {
1282
1204
  maxParallelWorkers: 1,
@@ -1535,8 +1457,6 @@ window.__ModuleLoader__.load({
1535
1457
  /** Client plugin body. */
1536
1458
  function apply(ctx) {
1537
1459
  setClient(ctx);
1538
- setBoardRoot(BoardRoot);
1539
- bindSessionNavigation(ctx);
1540
1460
  const t = ctx.locale.bind(LOCALE_NS);
1541
1461
  ctx.effect(() => {
1542
1462
  const off = ctx.locale.register(LOCALE_NS, {
@@ -1545,13 +1465,18 @@ window.__ModuleLoader__.load({
1545
1465
  });
1546
1466
  return () => off();
1547
1467
  }, "task-kanban: dictionaries");
1548
- ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
1549
- name: "sidebar.footer.action",
1550
- id: "task-kanban",
1551
- order: 60,
1468
+ ctx.slots.inject("main", () => ctx.slots.register({
1469
+ name: "main",
1470
+ key: KANBAN_PANEL_ID,
1471
+ locale: LOCALE_NS
1472
+ }, BoardRoot));
1473
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
1474
+ name: "sidebar.panellist",
1475
+ id: KANBAN_PANEL_ID,
1476
+ order: 20,
1552
1477
  label: () => t("kanban"),
1553
1478
  locale: LOCALE_NS
1554
- }, KanbanFooterButton));
1479
+ }, KanbanPanelIcon));
1555
1480
  ctx.slots.inject("settings.section", () => ctx.slots.register({
1556
1481
  name: "settings.section",
1557
1482
  id: SETTINGS_SECTION_ID,