@fonlan/dsh-task-kanban 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,6 +11,18 @@
11
11
  - **自动合并**:全部 phase 完成后,插件自动将 plan 分支合入主分支(`main` / `master`),完成后清理 worktree 与分支。
12
12
  - **状态可视化**:卡片完整显示标题、需求摘要、模型、细化 Skill、plan 状态徽标、所属阶段与时间,支持停止 / 重试 / 删除。
13
13
 
14
+ ## 设置
15
+
16
+ 侧边栏设置中的「任务看板」分节(`settings.section` 插槽)支持:
17
+
18
+ - **全局并行 worker 数**:跨工作区同时执行的卡片数上限。
19
+ - **细化需求会话默认 / Phase 实现会话默认**:分别配置两类会话的默认值:
20
+ - **模型**:留空表示走默认链路(卡片模型 → 宿主默认模型)。
21
+ - **推理强度**:仅列出所选模型支持的推理等级;留空表示使用提供方默认。
22
+ - **Agent 预设**:留空表示使用部署默认预设;会话开始后固定,恢复时会话按记录重建同一预设。
23
+
24
+ 新建任务弹窗不再预填卡片模型,卡片模型保持为空即可让细化 / 实现会话各自套用上述默认值。
25
+
14
26
  ## 安装
15
27
 
16
28
  > 通过 `dsh` 的 profile 插件管理命令安装,目标 profile 为 `web`。运行命令的目录将作为默认 workspace 根目录。
package/lib/client.js CHANGED
@@ -104,7 +104,12 @@ window.__ModuleLoader__.load({
104
104
  remove: (cardId) => call("remove", { cardId }),
105
105
  settingsGet: () => call("settings.get"),
106
106
  settingsSet: (patch) => call("settings.set", patch),
107
- models: () => call("models.list")
107
+ models: () => call("models.list"),
108
+ presets: () => call("presets.list"),
109
+ reasoningOptions: (provider, model) => call("reasoning.options", {
110
+ provider,
111
+ model
112
+ })
108
113
  };
109
114
  //#endregion
110
115
  //#region src/client/kanban-state.ts
@@ -287,8 +292,15 @@ window.__ModuleLoader__.load({
287
292
  noWorkspace: "没有可用工作区,请先在侧边栏添加",
288
293
  refresh: "刷新",
289
294
  settingsTitle: "任务看板",
295
+ settingsDescription: "全局并行 worker 数与各会话类型的默认模型、推理强度、Agent 预设",
296
+ settingsExpand: "展开设置",
297
+ settingsCollapse: "收起设置",
290
298
  maxParallelWorkers: "全局并行 worker 数",
291
- defaultModel: "默认模型",
299
+ settingsRefineGroup: "细化需求会话默认",
300
+ settingsPhaseGroup: "Phase 实现会话默认",
301
+ reasoningEffort: "推理强度",
302
+ agentPreset: "Agent 预设",
303
+ defaultOption: "(默认)",
292
304
  saved: "已保存",
293
305
  loadError: "加载任务失败",
294
306
  noPlan: "该卡片还没有实现计划,请先点击\"添加并细化\"",
@@ -354,8 +366,15 @@ window.__ModuleLoader__.load({
354
366
  noWorkspace: "No workspaces available — add one in the sidebar first",
355
367
  refresh: "Refresh",
356
368
  settingsTitle: "Task Kanban",
369
+ settingsDescription: "Global parallel workers and per-session-type defaults for model, reasoning effort, and agent preset",
370
+ settingsExpand: "Show settings",
371
+ settingsCollapse: "Hide settings",
357
372
  maxParallelWorkers: "Global parallel workers",
358
- defaultModel: "Default model",
373
+ settingsRefineGroup: "Refinement session defaults",
374
+ settingsPhaseGroup: "Phase implementation defaults",
375
+ reasoningEffort: "Reasoning effort",
376
+ agentPreset: "Agent preset",
377
+ defaultOption: "(default)",
359
378
  saved: "Saved",
360
379
  loadError: "Failed to load tasks",
361
380
  noPlan: "This card has no plan yet — click \"Add & Refine\" first",
@@ -375,8 +394,8 @@ window.__ModuleLoader__.load({
375
394
  };
376
395
  //#endregion
377
396
  //#region \0dsh-css:/Users/fonlan/Repos/dsh-task-kanban/src/client/board.css.mjs
378
- const css = "/* @fonlan/dsh-task-kanban board styles (kb- prefixed, plain CSS).\n Surfaces use the DSH theme tokens (--dsw-alias-*) so they follow the\n shell's selected light/dark appearance; the var() fallbacks cover shells\n that do not define them. */\n.kb-board {\n height: 100%;\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n padding: 12px 16px;\n overflow: hidden;\n color: var(--dsw-alias-label-primary, var(--dsh-fg, #e6e6e6));\n background: var(--dsw-alias-bg-base, var(--dsh-bg, #141418));\n font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;\n}\n.kb-empty {\n align-items: center;\n justify-content: center;\n}\n.kb-empty-text { opacity: 0.6; }\n.kb-board-header {\n display: flex;\n align-items: center;\n gap: 12px;\n padding-bottom: 10px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.25);\n flex: 0 0 auto;\n}\n/* Left-aligned group: workspace selector + new-task button sit adjacent. */\n.kb-board-title {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n flex: 0 1 auto;\n}\n.kb-board-title .kb-btn { flex: none; }\n.kb-ws-select {\n max-width: 320px;\n min-width: 0;\n flex: 0 1 auto;\n padding: 6px 10px;\n border-radius: 6px;\n border: 1px solid rgba(128, 128, 128, 0.35);\n background: rgba(128, 128, 128, 0.12);\n color: inherit;\n font-size: 14px;\n}\n.kb-btn {\n padding: 6px 14px;\n border-radius: 6px;\n border: 1px solid rgba(128, 128, 128, 0.35);\n background: rgba(128, 128, 128, 0.12);\n color: inherit;\n font-size: 13px;\n white-space: nowrap;\n cursor: pointer;\n}\n.kb-btn:hover { background: rgba(128, 128, 128, 0.22); }\n.kb-btn:disabled { opacity: 0.5; cursor: default; }\n.kb-btn-primary { background: #3b82f6; border-color: #3b82f6; color: #fff; }\n.kb-btn-primary:hover { background: #2f6fe0; }\n.kb-btn-danger { background: rgba(239, 68, 68, 0.15); border-color: rgba(239, 68, 68, 0.5); color: #f87171; }\n.kb-btn-small { padding: 2px 8px; font-size: 12px; }\n.kb-lanes {\n flex: 1 1 auto;\n display: flex;\n gap: 12px;\n overflow-x: auto;\n overflow-y: hidden;\n padding-top: 10px;\n min-height: 0;\n}\n.kb-lane {\n flex: 1 1 0;\n min-width: 220px;\n max-width: 320px;\n display: flex;\n flex-direction: column;\n background: rgba(128, 128, 128, 0.07);\n border-radius: 8px;\n border: 1px solid rgba(128, 128, 128, 0.12);\n overflow: hidden;\n}\n.kb-lane-drop { border-style: dashed; border-color: rgba(59, 130, 246, 0.45); }\n.kb-lane-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 10px;\n font-weight: 600;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary, var(--dsh-fg-soft, #b8b8c0));\n border-bottom: 1px solid rgba(128, 128, 128, 0.12);\n flex: 0 0 auto;\n}\n.kb-lane-count {\n background: rgba(128, 128, 128, 0.18);\n border-radius: 10px;\n padding: 0 8px;\n font-size: 12px;\n}\n.kb-lane-body {\n flex: 1 1 auto;\n overflow-y: auto;\n padding: 8px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.kb-card {\n background: rgba(128, 128, 128, 0.1);\n border: 1px solid rgba(128, 128, 128, 0.2);\n border-radius: 8px;\n padding: 8px 10px;\n cursor: pointer;\n user-select: none;\n}\n.kb-card:hover { border-color: rgba(128, 128, 128, 0.45); }\n.kb-card[draggable='true'] { cursor: grab; }\n.kb-card-running { border-color: rgba(59, 130, 246, 0.6); }\n.kb-card-error { border-color: rgba(239, 68, 68, 0.6); }\n.kb-card-title { font-weight: 600; font-size: 13px; margin-bottom: 6px; word-break: break-all; }\n.kb-card-meta { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; font-size: 12px; }\n.kb-card-badge {\n background: rgba(128, 128, 128, 0.2);\n border-radius: 4px;\n padding: 1px 6px;\n}\n.kb-card-running .kb-card-badge { background: rgba(59, 130, 246, 0.3); }\n.kb-card-error .kb-card-badge { background: rgba(239, 68, 68, 0.3); }\n.kb-card-phase { opacity: 0.8; }\n.kb-card-skill {\n background: rgba(139, 92, 246, 0.22);\n border-radius: 4px;\n padding: 1px 6px;\n}\n.kb-card-model { opacity: 0.55; }\n.kb-card-error-text { margin-top: 6px; font-size: 12px; color: #f87171; word-break: break-all; }\n.kb-card-worktree { margin-top: 4px; font-size: 11px; opacity: 0.5; word-break: break-all; }\n.kb-toast {\n position: fixed;\n left: 50%;\n bottom: 24px;\n transform: translateX(-50%);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-primary);\n padding: 8px 16px;\n border-radius: 8px;\n font-size: 13px;\n z-index: 2147483000;\n max-width: 70vw;\n}\n.kb-modal-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 2147482900;\n}\n.kb-modal {\n width: 460px;\n max-width: 90vw;\n max-height: 85vh;\n overflow-y: auto;\n background: var(--dsw-alias-bg-layer-1);\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n padding: 18px;\n color: var(--dsw-alias-label-primary);\n}\n.kb-modal-wide { width: 640px; }\n/* Task-detail bottom panel: docks at the bottom of the board page, fills the\n full board width and exactly half of the board height (the lanes column\n above flexes to the remaining space). */\n.kb-detail-panel {\n flex: 0 0 50%;\n min-height: 0;\n display: flex;\n flex-direction: column;\n margin-top: 10px;\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n overflow: hidden;\n color: var(--dsw-alias-label-primary);\n}\n.kb-detail-panel-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 10px 14px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.2);\n flex: 0 0 auto;\n}\n.kb-detail-panel-head .kb-modal-title { margin-bottom: 0; }\n.kb-detail-panel-body {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n padding: 14px;\n}\n.kb-detail-close {\n flex: none;\n width: 28px;\n height: 28px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 1px solid transparent;\n border-radius: 6px;\n background: transparent;\n color: inherit;\n font-size: 15px;\n line-height: 1;\n opacity: 0.75;\n cursor: pointer;\n}\n.kb-detail-close:hover { background: rgba(128, 128, 128, 0.18); opacity: 1; }\n.kb-modal-title { font-size: 16px; font-weight: 700; margin-bottom: 14px; display: flex; align-items: center; gap: 10px; }\n.kb-field { display: block; margin-bottom: 12px; }\n.kb-field > span { display: block; font-size: 12px; opacity: 0.75; margin-bottom: 4px; }\n.kb-field-hint {\n display: block;\n font-size: 11px;\n opacity: 0.55;\n margin-top: 4px;\n}\n.kb-field-hint-skill {\n opacity: 0.9;\n color: #a78bfa;\n}\n.kb-skill-menu-wrap { position: relative; }\n.kb-skill-menu {\n position: absolute;\n top: 100%;\n left: 0;\n right: 0;\n z-index: 20;\n margin: 2px 0 0;\n padding: 4px;\n list-style: none;\n max-height: 180px;\n overflow-y: auto;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-overlay);\n color: var(--dsw-alias-label-primary);\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);\n}\n.kb-skill-menu-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 6px 8px;\n border-radius: 6px;\n cursor: pointer;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n}\n.kb-skill-menu-item-active { background: var(--dsw-alias-interactive-bg-hover); }\n.kb-skill-menu-name { font-weight: 600; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\n.kb-skill-menu-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.kb-field-row { display: flex; align-items: center; gap: 10px; justify-content: space-between; }\n.kb-input {\n width: 100%;\n box-sizing: border-box;\n padding: 7px 10px;\n border-radius: 6px;\n border: 1px solid rgba(128, 128, 128, 0.35);\n background: rgba(128, 128, 128, 0.1);\n color: inherit;\n font-size: 13px;\n}\n.kb-input-number { width: 90px; }\n.kb-textarea { min-height: 110px; resize: vertical; font-family: inherit; }\n.kb-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }\n.kb-detail-section { margin-bottom: 12px; }\n.kb-detail-label { font-size: 12px; opacity: 0.65; margin-bottom: 4px; }\n.kb-detail-skill {\n display: inline-block;\n font-size: 11px;\n background: rgba(139, 92, 246, 0.22);\n border-radius: 4px;\n padding: 1px 6px;\n margin-bottom: 4px;\n}\n.kb-detail-text { white-space: pre-wrap; word-break: break-all; font-size: 13px; }\n.kb-detail-error .kb-detail-text { color: #f87171; }\n.kb-detail-status {\n font-size: 12px;\n font-weight: 600;\n border-radius: 4px;\n padding: 1px 8px;\n background: rgba(128, 128, 128, 0.2);\n}\n.kb-status-running, .kb-status-merging { background: rgba(59, 130, 246, 0.3) !important; }\n.kb-status-error { background: rgba(239, 68, 68, 0.3) !important; color: #f87171; }\n.kb-status-merged { background: rgba(34, 197, 94, 0.3) !important; }\n.kb-plan-summary { font-size: 13px; margin-bottom: 10px; }\n/* Phase cards: each plan phase is a card with an ordered number (kept from the\n parent <ol>) and three labeled sections (goal / sessions / conclusion) split\n by section titles and thin divider lines. */\n.kb-plan-phases { margin: 0; padding-left: 24px; }\n.kb-plan-phase {\n margin-bottom: 10px;\n padding: 8px 10px;\n background: rgba(128, 128, 128, 0.08);\n border: 1px solid rgba(128, 128, 128, 0.2);\n border-radius: 8px;\n}\n.kb-plan-phase-head {\n display: flex;\n gap: 8px;\n align-items: baseline;\n padding-bottom: 6px;\n margin-bottom: 6px;\n border-bottom: 1px solid rgba(128, 128, 128, 0.16);\n}\n.kb-plan-phase-title { font-weight: 600; font-size: 13px; }\n.kb-plan-phase-id { font-size: 11px; opacity: 0.5; }\n.kb-plan-phase-body { display: flex; flex-direction: column; }\n/* Section isolation: label on top, thin divider between adjacent sections. */\n.kb-plan-phase-section { display: flex; flex-direction: column; }\n.kb-plan-phase-section + .kb-plan-phase-section {\n margin-top: 6px;\n padding-top: 6px;\n border-top: 1px solid rgba(128, 128, 128, 0.14);\n}\n.kb-plan-phase-section-title { font-size: 11px; opacity: 0.55; margin-bottom: 3px; }\n.kb-plan-phase-goal { font-size: 12px; opacity: 0.85; white-space: pre-wrap; word-break: break-all; }\n.kb-plan-phase-sessions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\n.kb-plan-phase-conclusion { font-size: 12px; opacity: 0.85; white-space: pre-wrap; word-break: break-all; }\n.kb-plan-phase-conclusion-item + .kb-plan-phase-conclusion-item { margin-top: 6px; }\n/* Running phase keeps the blue highlight; completed phases get success color. */\n.kb-plan-phase-current { border-color: rgba(59, 130, 246, 0.6); background: rgba(59, 130, 246, 0.08); }\n.kb-plan-phase-current .kb-plan-phase-title { color: #60a5fa; }\n.kb-plan-phase-completed { border-color: rgba(34, 197, 94, 0.45); background: rgba(34, 197, 94, 0.06); }\n.kb-plan-phase-completed .kb-plan-phase-title { color: #4ade80; }\n.kb-session-links { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\n.kb-settings { display: flex; flex-direction: column; gap: 12px; max-width: 420px; }\n.kb-settings-saved { font-size: 12px; color: #4ade80; }\n.kb-footer-action {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n width: 32px;\n height: 32px;\n border-radius: 6px;\n border: none;\n background: transparent;\n /* same foreground as the settings trigger (its trigger css uses this var) */\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n}\n.kb-footer-action:hover { background: rgba(128, 128, 128, 0.18); }\n.kb-footer-action-active { background: rgba(59, 130, 246, 0.25); }\n/* Wide mode borrows the settings trigger's metrics (34px tall, 14px label,\n 18px icon) so both controls read as siblings on the same line. */\n.kb-footer-action-wide {\n width: auto;\n height: 34px;\n padding: 0 8px;\n border-radius: 12px;\n font-size: 14px;\n}\n.kb-footer-action-wide .kb-footer-label {\n font-size: 14px;\n line-height: 22px;\n}\n/* Theme following is handled by the --dsw-alias-* tokens above: they change\n value with the shell's selected light/dark appearance, so no OS-level\n prefers-color-scheme override is needed (and one would fight the app\n theme when the two disagree). */\n/* Same-line placement WITHOUT moving any DOM: the sidebar foot area becomes\n a reversed flex row, so its LAST child (the settings area) renders on the\n LEFT and the FIRST child (footer actions = the kanban button) on the RIGHT\n — exactly \"settings, then kanban\". The settings trigger flexes to fill the\n remaining width; the button keeps its content width. */\n.kb-foot-row {\n display: flex;\n flex-direction: row-reverse;\n align-items: stretch;\n gap: 4px;\n}\n.kb-foot-row > div:first-child {\n flex: none;\n width: auto;\n min-width: 0;\n display: flex;\n align-items: center;\n}\n.kb-foot-row > div:last-child {\n flex: 1;\n min-width: 0;\n}\n/* Rail (collapsed sidebar): column-reverse stacks the settings circle on top\n and the kanban icon below it, both centered; the kanban button borrows the\n settings trigger's rail chrome (36px circle) so the two match. */\n.kb-foot-rail {\n flex-direction: column-reverse;\n align-items: center;\n}\n.kb-foot-rail .kb-footer-action {\n width: 36px;\n height: 36px;\n border-radius: 50%;\n margin: 8px 0 10px;\n}\n.kb-footer-label {\n max-width: 110px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n/* Narrow screens (mobile drawer / auto-collapse breakpoint): the drawer's\n foot area is ~256-280px but the footer actions row carries the kanban\n button PLUS mobile-nav's drawer actions (Files / session log), which\n together exceed the row and squeeze the settings trigger to zero width\n (row-reverse overflows left, clipping the button). Below the shell's\n auto-collapse breakpoint, stack the settings row above the actions and\n let the actions wrap so nothing is squeezed or clipped. */\n@media (max-width: 1023px) {\n .kb-foot-row {\n flex-direction: column-reverse;\n align-items: stretch;\n }\n .kb-foot-row > div:first-child {\n flex: none;\n width: 100%;\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n gap: 4px;\n padding-top: 4px;\n }\n .kb-foot-row > div:last-child {\n flex: none;\n width: 100%;\n }\n /* The footer-actions entries are display:contents wrappers, so the kanban\n button itself is a flex item of the wrapping actions row; pull it to the\n first wrap position so it sits directly below the settings row (the\n mobile-nav drawer actions follow on their own wrapped line). */\n .kb-foot-row .kb-footer-action {\n order: -1;\n }\n}";
379
- const tagId = "@fonlan/dsh-task-kanban/board.css";
397
+ 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 card (设置 → 插件配置): mirrors the built-in plugin cards\n (dsh-client-ui-settings-plugins PluginCard) — same surface, border, head\n layout, and disclosure chevron, spelled with kb-settings-card prefixed\n classes. The prefix deliberately does NOT reuse the board's `.kb-card*`\n classes: the board card carries its own padding/cursor/user-select, and\n sharing the root class would leak those onto the settings card. */\n.kb-settings-card {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-3);\n border-radius: 12px;\n list-style: none;\n transition: border-color .16s, background .16s;\n}\n.kb-settings-card:hover { border-color: var(--dsw-alias-label-dimmed); }\n.kb-settings-card[data-open] {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.kb-settings-card-head {\n appearance: none;\n width: 100%;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n background: transparent;\n border: 0;\n border-radius: 12px;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n display: flex;\n}\n.kb-settings-card-head:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -2px; }\n.kb-settings-card-headText { flex-direction: column; flex: 1; gap: 4px; min-width: 0; display: flex; }\n.kb-settings-card-title { color: var(--dsw-alias-label-primary); font-size: 15px; font-weight: 600; line-height: 1.4; }\n.kb-settings-card-sub { color: var(--dsw-alias-label-tertiary); font-size: 13px; line-height: 1.5; }\n.kb-settings-card-chevron {\n color: var(--dsw-alias-label-tertiary);\n flex: none;\n transition: transform .16s;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.kb-settings-card-chevron[data-open] { transform: rotate(180deg); }\n.kb-settings-card-body { border-top: 1px solid var(--dsw-alias-border-l2); margin: 0 16px; padding: 12px 0 8px; }\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}";
398
+ const tagId = "@fonlan/dsh-task-kanban/board.css#bcd6e906";
380
399
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
381
400
  const tag = document.createElement("style");
382
401
  tag.dataset.plugin = "@fonlan/dsh-task-kanban";
@@ -699,9 +718,6 @@ window.__ModuleLoader__.load({
699
718
  }).catch(() => {
700
719
  if (alive) setModels([]);
701
720
  });
702
- api.settingsGet().then((s) => {
703
- if (alive && s.defaultModel !== "") setModel(s.defaultModel);
704
- }).catch(() => void 0);
705
721
  return () => {
706
722
  alive = false;
707
723
  };
@@ -1244,80 +1260,279 @@ window.__ModuleLoader__.load({
1244
1260
  })]
1245
1261
  });
1246
1262
  }
1247
- /** Plugin settings section: global parallel workers + default model. */
1248
- function KanbanSettingsSection({ t }) {
1249
- const tr = t ?? ((key) => key);
1250
- const [settings, setSettings] = (0, react.useState)({
1251
- maxParallelWorkers: 1,
1252
- defaultModel: ""
1253
- });
1254
- const [models, setModels] = (0, react.useState)([]);
1255
- const [saved, setSaved] = (0, react.useState)(false);
1256
- (0, react.useEffect)(() => {
1257
- let alive = true;
1258
- api.settingsGet().then((s) => {
1259
- if (alive) setSettings(s);
1260
- }).catch(() => void 0);
1261
- api.models().then((list) => {
1262
- if (alive) setModels(list);
1263
- }).catch(() => void 0);
1264
- return () => {
1265
- alive = false;
1266
- };
1267
- }, []);
1268
- const commit = async (patch) => {
1269
- try {
1270
- const next = await api.settingsSet(patch);
1271
- setSettings(next);
1272
- setSaved(true);
1273
- window.setTimeout(() => setSaved(false), 1500);
1274
- } catch {}
1263
+ /** Fallback shape while settings load (server always resolves the full set). */
1264
+ const EMPTY_SETTINGS = {
1265
+ maxParallelWorkers: 1,
1266
+ refinementModel: "",
1267
+ refinementProvider: "",
1268
+ refinementReasoningEffort: "",
1269
+ refinementPreset: "",
1270
+ phaseModel: "",
1271
+ phaseProvider: "",
1272
+ phaseReasoningEffort: "",
1273
+ phasePreset: ""
1274
+ };
1275
+ /** The setting field names of one session-type group. */
1276
+ const GROUP_FIELDS = {
1277
+ refinement: {
1278
+ model: "refinementModel",
1279
+ provider: "refinementProvider",
1280
+ effort: "refinementReasoningEffort",
1281
+ preset: "refinementPreset"
1282
+ },
1283
+ phase: {
1284
+ model: "phaseModel",
1285
+ provider: "phaseProvider",
1286
+ effort: "phaseReasoningEffort",
1287
+ preset: "phasePreset"
1288
+ }
1289
+ };
1290
+ /** Model / reasoning effort / agent preset defaults for one session type. */
1291
+ function SessionDefaultsGroup({ kind, settings, models, presets, efforts, tr, onCommit, loadEfforts }) {
1292
+ const fields = GROUP_FIELDS[kind];
1293
+ const model = settings[fields.model];
1294
+ const provider = settings[fields.provider];
1295
+ const effort = settings[fields.effort];
1296
+ const preset = settings[fields.preset];
1297
+ const effortKey = model !== "" && provider !== "" ? `${provider}/${model}` : "";
1298
+ const effortOptions = effortKey !== "" ? efforts[effortKey]?.efforts ?? [] : [];
1299
+ const handleModel = (id) => {
1300
+ const nextProvider = models.find((m) => m.id === id)?.provider ?? "";
1301
+ onCommit({
1302
+ [fields.model]: id,
1303
+ [fields.provider]: nextProvider
1304
+ });
1305
+ if (id !== "" && nextProvider !== "") loadEfforts(nextProvider, id);
1275
1306
  };
1276
1307
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1277
- className: "kb-settings",
1308
+ className: "kb-settings-group",
1278
1309
  children: [
1279
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1280
- className: "kb-field kb-field-row",
1281
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tr("maxParallelWorkers") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1282
- className: "kb-input kb-input-number",
1283
- type: "number",
1284
- min: 1,
1285
- step: 1,
1286
- value: settings.maxParallelWorkers,
1287
- onChange: (e) => {
1288
- const value = Math.max(1, Math.floor(Number(e.target.value) || 1));
1289
- setSettings((s) => ({
1290
- ...s,
1291
- maxParallelWorkers: value
1292
- }));
1293
- commit({ maxParallelWorkers: value });
1294
- }
1295
- })]
1310
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1311
+ className: "kb-settings-group-title",
1312
+ children: tr(kind === "refinement" ? "settingsRefineGroup" : "settingsPhaseGroup")
1296
1313
  }),
1297
1314
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1298
1315
  className: "kb-field kb-field-row",
1299
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tr("defaultModel") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1316
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tr("model") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1300
1317
  className: "kb-input",
1301
- value: settings.defaultModel,
1302
- onChange: (e) => {
1303
- commit({ defaultModel: e.target.value });
1304
- },
1318
+ value: model,
1319
+ onChange: (e) => handleModel(e.target.value),
1305
1320
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1306
1321
  value: "",
1307
- children: "—"
1322
+ children: tr("defaultOption")
1308
1323
  }), models.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1309
1324
  value: m.id,
1310
1325
  children: m.name ?? m.id
1311
1326
  }, m.provider + "/" + m.id))]
1312
1327
  })]
1313
1328
  }),
1314
- saved && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1315
- className: "kb-settings-saved",
1316
- children: tr("saved")
1329
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1330
+ className: "kb-field kb-field-row",
1331
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tr("reasoningEffort") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1332
+ className: "kb-input",
1333
+ value: effort,
1334
+ disabled: model === "",
1335
+ onChange: (e) => void onCommit({ [fields.effort]: e.target.value }),
1336
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1337
+ value: "",
1338
+ children: tr("defaultOption")
1339
+ }), effortOptions.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1340
+ value: o.id,
1341
+ children: o.name
1342
+ }, o.id))]
1343
+ })]
1344
+ }),
1345
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1346
+ className: "kb-field kb-field-row",
1347
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tr("agentPreset") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1348
+ className: "kb-input",
1349
+ value: preset,
1350
+ onChange: (e) => void onCommit({ [fields.preset]: e.target.value }),
1351
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1352
+ value: "",
1353
+ children: tr("defaultOption")
1354
+ }), presets.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1355
+ value: p.id,
1356
+ children: p.name ?? p.id
1357
+ }, p.id))]
1358
+ })]
1317
1359
  })
1318
1360
  ]
1319
1361
  });
1320
1362
  }
1363
+ /** Plugin settings card (设置 → 插件配置): workers + default model + per-session-type defaults. */
1364
+ function KanbanSettingsCard({ t }) {
1365
+ const tr = t ?? ((key) => key);
1366
+ const [open, setOpen] = (0, react.useState)(false);
1367
+ const [settings, setSettings] = (0, react.useState)(EMPTY_SETTINGS);
1368
+ const [models, setModels] = (0, react.useState)([]);
1369
+ const [presets, setPresets] = (0, react.useState)([]);
1370
+ const [efforts, setEfforts] = (0, react.useState)({});
1371
+ const [saved, setSaved] = (0, react.useState)(false);
1372
+ const commit = (0, react.useCallback)(async (patch) => {
1373
+ try {
1374
+ const next = await api.settingsSet(patch);
1375
+ setSettings(next);
1376
+ setSaved(true);
1377
+ window.setTimeout(() => setSaved(false), 1500);
1378
+ } catch {}
1379
+ }, []);
1380
+ const settingsRef = (0, react.useRef)(settings);
1381
+ settingsRef.current = settings;
1382
+ const commitRef = (0, react.useRef)(commit);
1383
+ commitRef.current = commit;
1384
+ (0, react.useEffect)(() => {
1385
+ let alive = true;
1386
+ api.settingsGet().then((s) => {
1387
+ if (alive) setSettings(s);
1388
+ }).catch(() => void 0);
1389
+ api.models().then((list) => {
1390
+ if (alive) setModels(list);
1391
+ }).catch(() => void 0);
1392
+ api.presets().then((list) => {
1393
+ if (alive) setPresets(list);
1394
+ }).catch(() => void 0);
1395
+ return () => {
1396
+ alive = false;
1397
+ };
1398
+ }, []);
1399
+ /** Reasoning routes already requested (dedupe across mount + model changes). */
1400
+ const effortFetched = (0, react.useRef)(/* @__PURE__ */ new Set());
1401
+ /**
1402
+ * Fetch the reasoning efforts of one model route (once per route). A failed
1403
+ * fetch is marked `failed` so the stored effort is preserved (see the reset
1404
+ * effect below) instead of being wiped by a transient error.
1405
+ */
1406
+ const loadEfforts = (0, react.useCallback)((provider, model) => {
1407
+ const key = `${provider}/${model}`;
1408
+ if (effortFetched.current.has(key)) return;
1409
+ effortFetched.current.add(key);
1410
+ api.reasoningOptions(provider, model).then((opts) => setEfforts((prev) => ({
1411
+ ...prev,
1412
+ [key]: opts
1413
+ }))).catch(() => setEfforts((prev) => ({
1414
+ ...prev,
1415
+ [key]: {
1416
+ efforts: [],
1417
+ failed: true
1418
+ }
1419
+ })));
1420
+ }, []);
1421
+ (0, react.useEffect)(() => {
1422
+ for (const kind of ["refinement", "phase"]) {
1423
+ const fields = GROUP_FIELDS[kind];
1424
+ const model = settings[fields.model];
1425
+ const provider = settings[fields.provider];
1426
+ if (model !== "" && provider !== "") loadEfforts(provider, model);
1427
+ }
1428
+ }, [
1429
+ settings.refinementModel,
1430
+ settings.refinementProvider,
1431
+ settings.phaseModel,
1432
+ settings.phaseProvider,
1433
+ loadEfforts
1434
+ ]);
1435
+ (0, react.useEffect)(() => {
1436
+ const s = settingsRef.current;
1437
+ for (const kind of ["refinement", "phase"]) {
1438
+ const fields = GROUP_FIELDS[kind];
1439
+ const model = s[fields.model];
1440
+ const provider = s[fields.provider];
1441
+ const effort = s[fields.effort];
1442
+ if (effort === "" || model === "" || provider === "") continue;
1443
+ const opts = efforts[`${provider}/${model}`];
1444
+ if (opts === void 0 || opts.failed === true) continue;
1445
+ if (!opts.efforts.some((o) => o.id === effort)) commitRef.current({ [fields.effort]: "" });
1446
+ }
1447
+ }, [efforts]);
1448
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1449
+ className: "kb-settings-card",
1450
+ "data-open": open ? "" : void 0,
1451
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1452
+ type: "button",
1453
+ className: "kb-settings-card-head",
1454
+ "aria-expanded": open,
1455
+ "aria-label": (open ? tr("settingsCollapse") : tr("settingsExpand")) + ":" + tr("settingsTitle"),
1456
+ onClick: () => setOpen(!open),
1457
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1458
+ className: "kb-settings-card-headText",
1459
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1460
+ className: "kb-settings-card-title",
1461
+ children: tr("settingsTitle")
1462
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1463
+ className: "kb-settings-card-sub",
1464
+ children: tr("settingsDescription")
1465
+ })]
1466
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1467
+ className: "kb-settings-card-chevron",
1468
+ "data-open": open ? "" : void 0,
1469
+ "aria-hidden": "true",
1470
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1471
+ width: "14",
1472
+ height: "14",
1473
+ viewBox: "0 0 14 14",
1474
+ fill: "none",
1475
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1476
+ d: "M3.5 5.25L7 8.75L10.5 5.25",
1477
+ stroke: "currentColor",
1478
+ strokeWidth: "1.5",
1479
+ strokeLinecap: "round",
1480
+ strokeLinejoin: "round"
1481
+ })
1482
+ })
1483
+ })]
1484
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1485
+ className: "kb-settings-card-body",
1486
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1487
+ className: "kb-settings",
1488
+ children: [
1489
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1490
+ className: "kb-field kb-field-row",
1491
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tr("maxParallelWorkers") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1492
+ className: "kb-input kb-input-number",
1493
+ type: "number",
1494
+ min: 1,
1495
+ step: 1,
1496
+ value: settings.maxParallelWorkers,
1497
+ onChange: (e) => {
1498
+ const value = Math.max(1, Math.floor(Number(e.target.value) || 1));
1499
+ setSettings((s) => ({
1500
+ ...s,
1501
+ maxParallelWorkers: value
1502
+ }));
1503
+ commit({ maxParallelWorkers: value });
1504
+ }
1505
+ })]
1506
+ }),
1507
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionDefaultsGroup, {
1508
+ kind: "refinement",
1509
+ settings,
1510
+ models,
1511
+ presets,
1512
+ efforts,
1513
+ tr,
1514
+ onCommit: commit,
1515
+ loadEfforts
1516
+ }),
1517
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionDefaultsGroup, {
1518
+ kind: "phase",
1519
+ settings,
1520
+ models,
1521
+ presets,
1522
+ efforts,
1523
+ tr,
1524
+ onCommit: commit,
1525
+ loadEfforts
1526
+ }),
1527
+ saved && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1528
+ className: "kb-settings-saved",
1529
+ children: tr("saved")
1530
+ })
1531
+ ]
1532
+ })
1533
+ })]
1534
+ });
1535
+ }
1321
1536
  //#endregion
1322
1537
  //#region src/client/index.tsx
1323
1538
  /** Services required before mounting (provided by the client runtime). */
@@ -1347,13 +1562,11 @@ window.__ModuleLoader__.load({
1347
1562
  label: () => t("kanban"),
1348
1563
  locale: LOCALE_NS
1349
1564
  }, KanbanFooterButton));
1350
- ctx.slots.inject("settings.section", () => ctx.slots.register({
1351
- name: "settings.section",
1352
- id: "task-kanban",
1353
- order: 200,
1354
- label: () => t("settingsTitle"),
1565
+ ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
1566
+ name: "settings.plugin.item",
1567
+ key: "task-kanban",
1355
1568
  locale: LOCALE_NS
1356
- }, KanbanSettingsSection));
1569
+ }, KanbanSettingsCard));
1357
1570
  }
1358
1571
  //#endregion
1359
1572
  exports.apply = apply;