@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/lib/index.js CHANGED
@@ -3,7 +3,8 @@ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { appendFile, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
5
5
  import { join } from "node:path";
6
- import { createUserMessage } from "@deepseek-ai/dsh-llm";
6
+ import { ReasoningEffortId, createUserMessage } from "@deepseek-ai/dsh-llm";
7
+ import { installModelSelection } from "@deepseek-ai/dsh-agent";
7
8
  import { execFile } from "node:child_process";
8
9
  import { promisify } from "node:util";
9
10
  import { isSkillName, isUserInvocable, renderSkillContent } from "@deepseek-ai/dsh-skill";
@@ -12,12 +13,32 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
12
13
  const KANBAN_SETTINGS_NS = settingsNamespace("task-kanban");
13
14
  const KanbanSettingsSchema = z.object({
14
15
  maxParallelWorkers: z.natural().min(1).default(1),
15
- defaultModel: z.string().default("")
16
+ refinementModel: z.string().default(""),
17
+ refinementProvider: z.string().default(""),
18
+ refinementReasoningEffort: z.string().default(""),
19
+ refinementPreset: z.string().default(""),
20
+ phaseModel: z.string().default(""),
21
+ phaseProvider: z.string().default(""),
22
+ phaseReasoningEffort: z.string().default(""),
23
+ phasePreset: z.string().default("")
16
24
  });
25
+ const EMPTY_DEFAULTS = {
26
+ model: "",
27
+ provider: "",
28
+ reasoningEffort: "",
29
+ preset: ""
30
+ };
17
31
  function registerSettings(ctx) {
18
32
  let current = {
19
33
  maxParallelWorkers: 1,
20
- defaultModel: ""
34
+ refinementModel: "",
35
+ refinementProvider: "",
36
+ refinementReasoningEffort: "",
37
+ refinementPreset: "",
38
+ phaseModel: "",
39
+ phaseProvider: "",
40
+ phaseReasoningEffort: "",
41
+ phasePreset: ""
21
42
  };
22
43
  let service;
23
44
  ctx.inject(["settings"], (sctx) => {
@@ -30,6 +51,24 @@ function registerSettings(ctx) {
30
51
  });
31
52
  return {
32
53
  get: () => current,
54
+ sessionDefaults: (kind) => {
55
+ const s = current;
56
+ switch (kind) {
57
+ case "refine": return {
58
+ model: s.refinementModel,
59
+ provider: s.refinementProvider,
60
+ reasoningEffort: s.refinementReasoningEffort,
61
+ preset: s.refinementPreset
62
+ };
63
+ case "phase": return {
64
+ model: s.phaseModel,
65
+ provider: s.phaseProvider,
66
+ reasoningEffort: s.phaseReasoningEffort,
67
+ preset: s.phasePreset
68
+ };
69
+ case "merge": return EMPTY_DEFAULTS;
70
+ }
71
+ },
33
72
  defaultModelRoute: () => {
34
73
  if (service === void 0) return {};
35
74
  try {
@@ -976,7 +1015,7 @@ var KanbanRunner = class {
976
1015
  async runRefinement(cardId, wsPath) {
977
1016
  const card = await this.store.get(wsPath, cardId);
978
1017
  if (card === void 0) return;
979
- const route = await this.modelRoute(cardId, wsPath);
1018
+ const route = await this.modelRoute(cardId, wsPath, "refine");
980
1019
  const sessionId = this.newSessionId();
981
1020
  this.sessionCards.set(sessionId, cardId);
982
1021
  await this.store.mutate(cardId, (c) => {
@@ -985,7 +1024,7 @@ var KanbanRunner = class {
985
1024
  c.sessions.refinement.push(sessionId);
986
1025
  }, wsPath);
987
1026
  try {
988
- const handle = await this.createAgent(sessionId, wsPath, route);
1027
+ const handle = await this.createAgent(sessionId, wsPath, route, "refine");
989
1028
  this.keepHandle(cardId, handle);
990
1029
  await this.attachRefinementSession(wsPath, sessionId);
991
1030
  const skill = card.skill !== void 0 ? await loadCardSkill(this.ctx, card.skill, wsPath, handle.agent) : void 0;
@@ -1089,19 +1128,47 @@ var KanbanRunner = class {
1089
1128
  dispose
1090
1129
  };
1091
1130
  }
1092
- async createAgent(sessionId, cwd, route) {
1131
+ /**
1132
+ * Create one kanban session agent. `kind` decides the per-session preset:
1133
+ * refine/phase use their settings default (falling back to the roster's
1134
+ * default preset when unset), merge keeps the historical 'standard' preset.
1135
+ * When the kind defaults configure a reasoning effort, an
1136
+ * `installModelSelection` ref couples the provider/model + effort into every
1137
+ * prompt assembly and model request. Without a configured effort no
1138
+ * selection is installed: `agentOptions` carries the route and the loop
1139
+ * materializes the model's own default effort — the historical behavior.
1140
+ */
1141
+ async createAgent(sessionId, cwd, route, kind) {
1093
1142
  const agentOptions = route.provider !== void 0 && route.model !== void 0 && route.model !== "" ? {
1094
1143
  provider: route.provider,
1095
1144
  model: route.model
1096
1145
  } : void 0;
1146
+ const selectionRef = {
1147
+ current: route.provider !== void 0 && route.model !== void 0 && route.model !== "" ? {
1148
+ provider: route.provider,
1149
+ model: route.model,
1150
+ ...route.reasoningEffort !== void 0 && route.reasoningEffort !== "" ? { reasoningEffort: ReasoningEffortId(route.reasoningEffort) } : {}
1151
+ } : void 0,
1152
+ assembled: void 0
1153
+ };
1154
+ let presetId;
1155
+ const presets = this.ctx.get("agentPresets");
1156
+ if (presets !== void 0) {
1157
+ const requested = kind === "merge" ? "standard" : this.settings.sessionDefaults(kind).preset;
1158
+ presetId = (await presets.resolve(requested === "" ? void 0 : requested)).id;
1159
+ }
1097
1160
  return this.ctx.agents.create({
1098
1161
  sessionId,
1099
- meta: { cwd },
1162
+ meta: {
1163
+ cwd,
1164
+ ...presetId !== void 0 ? { agentPreset: presetId } : {}
1165
+ },
1100
1166
  ...agentOptions !== void 0 ? { agentOptions } : {},
1101
1167
  setup: async (agentCtx) => {
1102
1168
  const agentPresets = agentCtx.get("agentPresets");
1103
- if (agentPresets !== void 0) await agentPresets.mount(agentCtx, "standard");
1169
+ if (agentPresets !== void 0) await agentPresets.mount(agentCtx, presetId);
1104
1170
  else throw new Error("@fonlan/dsh-task-kanban: agent-presets service is not mounted");
1171
+ if (selectionRef.current !== void 0 && selectionRef.current.reasoningEffort !== void 0) installModelSelection(agentCtx, selectionRef);
1105
1172
  }
1106
1173
  });
1107
1174
  }
@@ -1133,8 +1200,15 @@ var KanbanRunner = class {
1133
1200
  if (candidate !== void 0 && typeof candidate.resolveByPath === "function") return candidate;
1134
1201
  }
1135
1202
  }
1136
- /** Resolve the provider+model route for an agent (both are required). */
1137
- async modelRoute(cardId, wsPath) {
1203
+ /**
1204
+ * Resolve the provider+model route for an agent (both are required), plus
1205
+ * the per-kind reasoning effort. Precedence per kind:
1206
+ * 1. the card's explicit model (chosen at creation) — wins for every kind;
1207
+ * 2. the session-kind defaults (refine/phase settings);
1208
+ * 3. the host `agent-default-model` route.
1209
+ * Merge sessions skip step 2 (no per-type defaults for them).
1210
+ */
1211
+ async modelRoute(cardId, wsPath, kind) {
1138
1212
  const card = await this.store.get(wsPath, cardId);
1139
1213
  if (card === void 0) return {};
1140
1214
  if (card.model !== "" && card.provider !== void 0 && card.provider !== "") return {
@@ -1148,14 +1222,12 @@ var KanbanRunner = class {
1148
1222
  model: card.model
1149
1223
  } : { model: card.model };
1150
1224
  }
1151
- const userDefault = this.settings.get().defaultModel;
1152
- if (userDefault !== "") {
1153
- const fallback = this.settings.defaultModelRoute();
1154
- return fallback.provider !== void 0 ? {
1155
- provider: fallback.provider,
1156
- model: userDefault
1157
- } : { model: userDefault };
1158
- }
1225
+ const kindDefaults = this.settings.sessionDefaults(kind);
1226
+ if (kindDefaults.model !== "") return {
1227
+ ...kindDefaults.provider !== "" ? { provider: kindDefaults.provider } : {},
1228
+ model: kindDefaults.model,
1229
+ ...kindDefaults.reasoningEffort !== "" ? { reasoningEffort: kindDefaults.reasoningEffort } : {}
1230
+ };
1159
1231
  return this.settings.defaultModelRoute();
1160
1232
  }
1161
1233
  async runCard(wsPath, card, entry) {
@@ -1252,9 +1324,9 @@ var KanbanRunner = class {
1252
1324
  startedAt: Date.now()
1253
1325
  });
1254
1326
  }, wsPath);
1255
- const route = await this.modelRoute(cardId, wsPath);
1327
+ const route = await this.modelRoute(cardId, wsPath, "phase");
1256
1328
  try {
1257
- const handle = await this.createAgent(sessionId, workdir, route);
1329
+ const handle = await this.createAgent(sessionId, workdir, route, "phase");
1258
1330
  this.keepHandle(cardId, handle);
1259
1331
  entry.cancel = () => handle.agent.cancel({ kind: "user" });
1260
1332
  handle.agent.followup(this.asUserMessage(phasePrompt({
@@ -1546,9 +1618,9 @@ var KanbanRunner = class {
1546
1618
  await this.store.mutate(cardId, (c) => {
1547
1619
  c.sessions.merge.push(sessionId);
1548
1620
  }, wsPath);
1549
- const route = await this.modelRoute(cardId, wsPath);
1621
+ const route = await this.modelRoute(cardId, wsPath, "merge");
1550
1622
  try {
1551
- const handle = await this.createAgent(sessionId, workdir, route);
1623
+ const handle = await this.createAgent(sessionId, workdir, route, "merge");
1552
1624
  this.keepHandle(cardId, handle);
1553
1625
  handle.agent.followup(this.asUserMessage(mergePrompt(workdir, conflicts)));
1554
1626
  await handle.agent.whenIdle();
@@ -1605,6 +1677,28 @@ async function listModels(ctx) {
1605
1677
  } catch {}
1606
1678
  return out;
1607
1679
  }
1680
+ /**
1681
+ * Resolve the adapter-owned reasoning efforts of one exact provider/model
1682
+ * route. On-demand (one model at a time) so the settings picker can offer the
1683
+ * levels the selected model actually supports; failures yield an empty list.
1684
+ */
1685
+ async function reasoningOptions(ctx, provider, model) {
1686
+ const llm = ctx.get("llm");
1687
+ if (llm === void 0) return { efforts: [] };
1688
+ try {
1689
+ const info = await llm.resolveModelInfo(provider, model);
1690
+ return {
1691
+ efforts: (info.reasoning?.efforts ?? []).map((e) => ({
1692
+ id: String(e.id),
1693
+ name: e.name,
1694
+ ...e.description !== void 0 ? { description: e.description } : {}
1695
+ })),
1696
+ ...info.reasoning?.defaultEffort !== void 0 ? { defaultEffort: String(info.reasoning.defaultEffort) } : {}
1697
+ };
1698
+ } catch {
1699
+ return { efforts: [] };
1700
+ }
1701
+ }
1608
1702
  //#endregion
1609
1703
  //#region src/server/rpc.ts
1610
1704
  const API_PREFIX = "/plugins/@fonlan/dsh-task-kanban/api";
@@ -1737,14 +1831,37 @@ function registerApiRoutes(ctx, runner, settings) {
1737
1831
  route("retry", async (p) => runner.retryTask(requireString(p.cardId, "cardId")));
1738
1832
  route("remove", async (p) => runner.deleteTask(requireString(p.cardId, "cardId")));
1739
1833
  route("settings.get", async () => settings.get());
1834
+ /** Every settings field the client may write (per-session-type defaults included). */
1835
+ const SETTING_STRING_FIELDS = [
1836
+ "refinementModel",
1837
+ "refinementProvider",
1838
+ "refinementReasoningEffort",
1839
+ "refinementPreset",
1840
+ "phaseModel",
1841
+ "phaseProvider",
1842
+ "phaseReasoningEffort",
1843
+ "phasePreset"
1844
+ ];
1740
1845
  route("settings.set", async (p) => {
1741
1846
  const patch = {};
1742
1847
  if (typeof p.maxParallelWorkers === "number") patch.maxParallelWorkers = p.maxParallelWorkers;
1743
- if (typeof p.defaultModel === "string") patch.defaultModel = p.defaultModel;
1848
+ for (const key of SETTING_STRING_FIELDS) if (typeof p[key] === "string") patch[key] = p[key];
1744
1849
  await settings.update(patch);
1745
1850
  return settings.get();
1746
1851
  });
1747
1852
  route("models.list", async () => listModels(ctx));
1853
+ route("presets.list", async () => {
1854
+ const presets = ctx.get("agentPresets");
1855
+ if (presets === void 0) return [];
1856
+ return (await presets.list()).map((r) => ({
1857
+ id: r.id,
1858
+ name: r.name,
1859
+ description: r.description
1860
+ }));
1861
+ });
1862
+ route("reasoning.options", async (p) => {
1863
+ return reasoningOptions(ctx, requireString(p.provider, "provider"), requireString(p.model, "model"));
1864
+ });
1748
1865
  return () => {
1749
1866
  for (const d of disposers) d();
1750
1867
  };
@@ -2,3 +2,19 @@ import type { Context } from '@deepseek-ai/cordis';
2
2
  import type { ModelOption } from '../shared/card.js';
3
3
  /** Available models from every registered LLM provider (advisory catalog). */
4
4
  export declare function listModels(ctx: Context): Promise<ModelOption[]>;
5
+ /** One selectable reasoning effort of a model (display vocabulary for the settings picker). */
6
+ export interface ReasoningEffortOption {
7
+ id: string;
8
+ name: string;
9
+ description?: string;
10
+ }
11
+ export interface ReasoningOptions {
12
+ efforts: ReasoningEffortOption[];
13
+ defaultEffort?: string;
14
+ }
15
+ /**
16
+ * Resolve the adapter-owned reasoning efforts of one exact provider/model
17
+ * route. On-demand (one model at a time) so the settings picker can offer the
18
+ * levels the selected model actually supports; failures yield an empty list.
19
+ */
20
+ export declare function reasoningOptions(ctx: Context, provider: string, model: string): Promise<ReasoningOptions>;
@@ -103,6 +103,16 @@ export declare class KanbanRunner implements KanbanToolResolver {
103
103
  * resolve it — only real human replies drive the interactive refinement.
104
104
  */
105
105
  private waitForUserReply;
106
+ /**
107
+ * Create one kanban session agent. `kind` decides the per-session preset:
108
+ * refine/phase use their settings default (falling back to the roster's
109
+ * default preset when unset), merge keeps the historical 'standard' preset.
110
+ * When the kind defaults configure a reasoning effort, an
111
+ * `installModelSelection` ref couples the provider/model + effort into every
112
+ * prompt assembly and model request. Without a configured effort no
113
+ * selection is installed: `agentOptions` carries the route and the loop
114
+ * materializes the model's own default effort — the historical behavior.
115
+ */
106
116
  private createAgent;
107
117
  private keepHandle;
108
118
  /**
@@ -114,7 +124,14 @@ export declare class KanbanRunner implements KanbanToolResolver {
114
124
  private attachRefinementSession;
115
125
  /** The host workspace registry, whichever service name carries it. */
116
126
  private attachRegistry;
117
- /** Resolve the provider+model route for an agent (both are required). */
127
+ /**
128
+ * Resolve the provider+model route for an agent (both are required), plus
129
+ * the per-kind reasoning effort. Precedence per kind:
130
+ * 1. the card's explicit model (chosen at creation) — wins for every kind;
131
+ * 2. the session-kind defaults (refine/phase settings);
132
+ * 3. the host `agent-default-model` route.
133
+ * Merge sessions skip step 2 (no per-type defaults for them).
134
+ */
118
135
  private modelRoute;
119
136
  private runCard;
120
137
  private runPhase;
@@ -1,20 +1,42 @@
1
1
  /**
2
- * User-facing plugin settings (global parallel worker count, default model),
3
- * persisted through the settings service namespace `task-kanban`. The DSH
4
- * settings service requires a lowercase kebab-case namespace
5
- * (/^[a-z][a-z0-9-]*$/), so the scoped package name cannot be used here.
2
+ * User-facing plugin settings (global parallel worker count, per-session-type
3
+ * model/effort/preset defaults), persisted through the settings service
4
+ * namespace `task-kanban`. The DSH settings service requires a lowercase
5
+ * kebab-case namespace (/^[a-z][a-z0-9-]*$/), so the scoped package name
6
+ * cannot be used here.
6
7
  */
7
8
  import type { Context } from '@deepseek-ai/cordis';
8
9
  import z from '@deepseek-ai/schemastery';
9
- import type { KanbanSettingsShape } from '../shared/card.js';
10
+ import type { KanbanSessionKind, KanbanSettingsShape } from '../shared/card.js';
10
11
  export declare const KANBAN_SETTINGS_NS: import("@deepseek-ai/dsh-settings").SettingsNamespace;
11
12
  export declare const KanbanSettingsSchema: z<Schemastery.ObjectS<{
12
13
  maxParallelWorkers: z<number, number>;
13
- defaultModel: z<string, string>;
14
+ refinementModel: z<string, string>;
15
+ refinementProvider: z<string, string>;
16
+ refinementReasoningEffort: z<string, string>;
17
+ refinementPreset: z<string, string>;
18
+ phaseModel: z<string, string>;
19
+ phaseProvider: z<string, string>;
20
+ phaseReasoningEffort: z<string, string>;
21
+ phasePreset: z<string, string>;
14
22
  }>, Schemastery.ObjectT<{
15
23
  maxParallelWorkers: z<number, number>;
16
- defaultModel: z<string, string>;
24
+ refinementModel: z<string, string>;
25
+ refinementProvider: z<string, string>;
26
+ refinementReasoningEffort: z<string, string>;
27
+ refinementPreset: z<string, string>;
28
+ phaseModel: z<string, string>;
29
+ phaseProvider: z<string, string>;
30
+ phaseReasoningEffort: z<string, string>;
31
+ phasePreset: z<string, string>;
17
32
  }>>;
33
+ /** The per-session-type default slice a session kind resolves. */
34
+ export interface KanbanSessionDefaults {
35
+ model: string;
36
+ provider: string;
37
+ reasoningEffort: string;
38
+ preset: string;
39
+ }
18
40
  export interface KanbanSettingsFace {
19
41
  get(): KanbanSettingsShape;
20
42
  update(patch: Partial<KanbanSettingsShape>): Promise<void>;
@@ -23,5 +45,11 @@ export interface KanbanSettingsFace {
23
45
  provider?: string;
24
46
  model?: string;
25
47
  };
48
+ /**
49
+ * The configured model/effort/preset defaults for one session kind.
50
+ * `merge` sessions deliberately carry no per-type defaults: they keep the
51
+ * legacy resolution chain (card model → host default).
52
+ */
53
+ sessionDefaults(kind: KanbanSessionKind): KanbanSessionDefaults;
26
54
  }
27
55
  export declare function registerSettings(ctx: Context): KanbanSettingsFace;
@@ -82,7 +82,18 @@ export interface ModelOption {
82
82
  id: string;
83
83
  name?: string;
84
84
  }
85
+ /** Which kanban session kind a per-type default applies to. */
86
+ export type KanbanSessionKind = 'refine' | 'phase' | 'merge';
85
87
  export interface KanbanSettingsShape {
86
88
  maxParallelWorkers: number;
87
- defaultModel: string;
89
+ /** Requirement-refinement session defaults. */
90
+ refinementModel: string;
91
+ refinementProvider: string;
92
+ refinementReasoningEffort: string;
93
+ refinementPreset: string;
94
+ /** Phase implementation session defaults. */
95
+ phaseModel: string;
96
+ phaseProvider: string;
97
+ phaseReasoningEffort: string;
98
+ phasePreset: string;
88
99
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonlan/dsh-task-kanban",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "DSH web plugin: a task kanban that refines requirements into phased plans, auto-executes them in git worktrees (one session per phase), and auto-merges into the main branch on completion.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,6 +44,7 @@
44
44
  },
45
45
  "scripts": {
46
46
  "build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown",
47
+ "prepare": "npm run build",
47
48
  "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json",
48
49
  "bundle": "tsdown",
49
50
  "watch": "tsdown --watch",
@@ -64,6 +65,7 @@
64
65
  "@deepseek-ai/dsh-client-ui-layout",
65
66
  "@deepseek-ai/dsh-client-ui-conversation",
66
67
  "@deepseek-ai/dsh-client-ui-settings",
68
+ "@deepseek-ai/dsh-client-ui-settings-plugins",
67
69
  "@deepseek-ai/dsh-client-web-react"
68
70
  ]
69
71
  }
@@ -82,6 +84,7 @@
82
84
  "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
83
85
  "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
84
86
  "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
87
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.6",
85
88
  "react": "^18.2.0",
86
89
  "react-dom": "^18.2.0"
87
90
  },
@@ -104,6 +107,7 @@
104
107
  "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.6",
105
108
  "@deepseek-ai/dsh-client-ui-layout": "^0.1.0-rc.6",
106
109
  "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
110
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.6",
107
111
  "vitest": "^3.2.0"
108
112
  },
109
113
  "packageManager": "pnpm@11.21.0"
package/src/client/api.ts CHANGED
@@ -69,6 +69,21 @@ export async function gatewaySkillList(sessionId: string): Promise<SkillOption[]
69
69
  return parsed.result.value?.skills ?? []
70
70
  }
71
71
 
72
+ /** One row of the host `agentPresets.list()` roster. */
73
+ export interface PresetOption {
74
+ id: string
75
+ name?: string
76
+ description?: string
77
+ }
78
+
79
+ /** Reasoning efforts of one provider/model route (`reasoning.options`). */
80
+ export interface ReasoningOptions {
81
+ efforts: Array<{ id: string; name: string; description?: string }>
82
+ defaultEffort?: string
83
+ /** Client-side marker: the fetch failed; the stored effort must be kept. */
84
+ failed?: boolean
85
+ }
86
+
72
87
  export const api = {
73
88
  list: (workspacePath: string) => call<KanbanCard[]>('list', { workspacePath }),
74
89
  create: (workspacePath: string, requirement: string, model: string, provider?: string, skill?: string) =>
@@ -80,4 +95,7 @@ export const api = {
80
95
  settingsGet: () => call<KanbanSettingsShape>('settings.get'),
81
96
  settingsSet: (patch: Partial<KanbanSettingsShape>) => call<KanbanSettingsShape>('settings.set', patch),
82
97
  models: () => call<ModelOption[]>('models.list'),
83
- }
98
+ presets: () => call<PresetOption[]>('presets.list'),
99
+ reasoningOptions: (provider: string, model: string) =>
100
+ call<ReasoningOptions>('reasoning.options', { provider, model }),
101
+ }
@@ -41,9 +41,9 @@
41
41
  flex: 0 1 auto;
42
42
  padding: 6px 10px;
43
43
  border-radius: 6px;
44
- border: 1px solid rgba(128, 128, 128, 0.35);
45
- background: rgba(128, 128, 128, 0.12);
46
- color: inherit;
44
+ border: 1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35)));
45
+ background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-1, rgba(128, 128, 128, 0.12)));
46
+ color: var(--dsw-alias-label-primary, inherit);
47
47
  font-size: 14px;
48
48
  }
49
49
  .kb-btn {
@@ -272,17 +272,23 @@
272
272
  text-overflow: ellipsis;
273
273
  }
274
274
  .kb-field-row { display: flex; align-items: center; gap: 10px; justify-content: space-between; }
275
+ /* Field labels in a row never wrap; selects get the remaining fixed width. */
276
+ .kb-field-row > span { white-space: nowrap; flex-shrink: 0; margin-bottom: 0; }
275
277
  .kb-input {
276
278
  width: 100%;
277
279
  box-sizing: border-box;
278
280
  padding: 7px 10px;
279
281
  border-radius: 6px;
280
- border: 1px solid rgba(128, 128, 128, 0.35);
281
- background: rgba(128, 128, 128, 0.1);
282
- color: inherit;
282
+ /* Form controls follow the shell theme: DSH's input surface token plus a
283
+ theme-tuned border; the fallbacks cover shells without these tokens. */
284
+ border: 1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35)));
285
+ background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-1, rgba(128, 128, 128, 0.1)));
286
+ color: var(--dsw-alias-label-primary, inherit);
283
287
  font-size: 13px;
284
288
  }
285
289
  .kb-input-number { width: 90px; }
290
+ /* All settings selects share one width so the column looks even. */
291
+ .kb-settings select.kb-input { width: 200px; flex: 0 0 200px; }
286
292
  .kb-textarea { min-height: 110px; resize: vertical; font-family: inherit; }
287
293
  .kb-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
288
294
  .kb-detail-section { margin-bottom: 12px; }
@@ -350,6 +356,55 @@
350
356
  .kb-session-links { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
351
357
  .kb-settings { display: flex; flex-direction: column; gap: 12px; max-width: 420px; }
352
358
  .kb-settings-saved { font-size: 12px; color: #4ade80; }
359
+ .kb-settings-group { border-top: 1px solid rgba(128, 128, 128, 0.22); padding-top: 12px; }
360
+ .kb-settings-group-title { font-size: 12px; font-weight: 600; opacity: 0.9; margin-bottom: 10px; }
361
+ /* Settings card (设置 → 插件配置): mirrors the built-in plugin cards
362
+ (dsh-client-ui-settings-plugins PluginCard) — same surface, border, head
363
+ layout, and disclosure chevron, spelled with kb-settings-card prefixed
364
+ classes. The prefix deliberately does NOT reuse the board's `.kb-card*`
365
+ classes: the board card carries its own padding/cursor/user-select, and
366
+ sharing the root class would leak those onto the settings card. */
367
+ .kb-settings-card {
368
+ border: 1px solid var(--dsw-alias-border-l2);
369
+ background: var(--dsw-alias-bg-layer-3);
370
+ border-radius: 12px;
371
+ list-style: none;
372
+ transition: border-color .16s, background .16s;
373
+ }
374
+ .kb-settings-card:hover { border-color: var(--dsw-alias-label-dimmed); }
375
+ .kb-settings-card[data-open] {
376
+ background: var(--dsw-alias-bg-layer-2);
377
+ border-color: var(--dsw-alias-label-dimmed);
378
+ }
379
+ .kb-settings-card-head {
380
+ appearance: none;
381
+ width: 100%;
382
+ font: inherit;
383
+ color: inherit;
384
+ text-align: left;
385
+ cursor: pointer;
386
+ background: transparent;
387
+ border: 0;
388
+ border-radius: 12px;
389
+ align-items: center;
390
+ gap: 12px;
391
+ padding: 14px 16px;
392
+ display: flex;
393
+ }
394
+ .kb-settings-card-head:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -2px; }
395
+ .kb-settings-card-headText { flex-direction: column; flex: 1; gap: 4px; min-width: 0; display: flex; }
396
+ .kb-settings-card-title { color: var(--dsw-alias-label-primary); font-size: 15px; font-weight: 600; line-height: 1.4; }
397
+ .kb-settings-card-sub { color: var(--dsw-alias-label-tertiary); font-size: 13px; line-height: 1.5; }
398
+ .kb-settings-card-chevron {
399
+ color: var(--dsw-alias-label-tertiary);
400
+ flex: none;
401
+ transition: transform .16s;
402
+ display: flex;
403
+ align-items: center;
404
+ justify-content: center;
405
+ }
406
+ .kb-settings-card-chevron[data-open] { transform: rotate(180deg); }
407
+ .kb-settings-card-body { border-top: 1px solid var(--dsw-alias-border-l2); margin: 0 16px; padding: 12px 0 8px; }
353
408
  .kb-footer-action {
354
409
  display: inline-flex;
355
410
  align-items: center;
@@ -326,13 +326,6 @@ function NewTaskModal({ t, workspaces, selectedPath, sessionId, onClose, onCreat
326
326
  api.models()
327
327
  .then((list) => { if (alive) setModels(list) })
328
328
  .catch(() => { if (alive) setModels([]) })
329
- api.settingsGet()
330
- .then((s) => {
331
- if (alive && s.defaultModel !== '') {
332
- setModel(s.defaultModel)
333
- }
334
- })
335
- .catch(() => undefined)
336
329
  return () => { alive = false }
337
330
  }, [])
338
331
 
@@ -2,9 +2,9 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
2
2
  import type {} from '@deepseek-ai/dsh-client-locale/client'
3
3
  import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
4
4
  import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
5
- import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
5
+ import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
6
6
  import { BoardRoot } from './board'
7
- import { KanbanFooterButton, KanbanSettingsSection } from './sections'
7
+ import { KanbanFooterButton, KanbanSettingsCard } from './sections'
8
8
  import { LOCALE_NS, zh, en } from './locales'
9
9
  import { setClient, setBoardRoot, bindSessionNavigation } from './kanban-state'
10
10
 
@@ -35,13 +35,15 @@ export function apply(ctx: ClientContext): void {
35
35
  }, KanbanFooterButton as never),
36
36
  )
37
37
 
38
- ctx.slots.inject('settings.section', () =>
38
+ // The plugin's own Settings Card (设置 → 插件配置) rides the `task-kanban`
39
+ // settings namespace: registering into the keyed `settings.plugin.item` slot
40
+ // with the namespace string makes the configurable-plugins tab dispatch the
41
+ // card next to the built-in ones (bash / agent loop / web search).
42
+ ctx.slots.inject('settings.plugin.item', () =>
39
43
  ctx.slots.register({
40
- name: 'settings.section',
41
- id: 'task-kanban',
42
- order: 200,
43
- label: () => t('settingsTitle'),
44
+ name: 'settings.plugin.item',
45
+ key: 'task-kanban',
44
46
  locale: LOCALE_NS,
45
- }, KanbanSettingsSection as never),
47
+ }, KanbanSettingsCard as never),
46
48
  )
47
- }
49
+ }
@@ -52,8 +52,15 @@ export const zh = {
52
52
  noWorkspace: '没有可用工作区,请先在侧边栏添加',
53
53
  refresh: '刷新',
54
54
  settingsTitle: '任务看板',
55
+ settingsDescription: '全局并行 worker 数与各会话类型的默认模型、推理强度、Agent 预设',
56
+ settingsExpand: '展开设置',
57
+ settingsCollapse: '收起设置',
55
58
  maxParallelWorkers: '全局并行 worker 数',
56
- defaultModel: '默认模型',
59
+ settingsRefineGroup: '细化需求会话默认',
60
+ settingsPhaseGroup: 'Phase 实现会话默认',
61
+ reasoningEffort: '推理强度',
62
+ agentPreset: 'Agent 预设',
63
+ defaultOption: '(默认)',
57
64
  saved: '已保存',
58
65
  loadError: '加载任务失败',
59
66
  noPlan: '该卡片还没有实现计划,请先点击"添加并细化"',
@@ -120,8 +127,15 @@ export const en = {
120
127
  noWorkspace: 'No workspaces available — add one in the sidebar first',
121
128
  refresh: 'Refresh',
122
129
  settingsTitle: 'Task Kanban',
130
+ settingsDescription: 'Global parallel workers and per-session-type defaults for model, reasoning effort, and agent preset',
131
+ settingsExpand: 'Show settings',
132
+ settingsCollapse: 'Hide settings',
123
133
  maxParallelWorkers: 'Global parallel workers',
124
- defaultModel: 'Default model',
134
+ settingsRefineGroup: 'Refinement session defaults',
135
+ settingsPhaseGroup: 'Phase implementation defaults',
136
+ reasoningEffort: 'Reasoning effort',
137
+ agentPreset: 'Agent preset',
138
+ defaultOption: '(default)',
125
139
  saved: 'Saved',
126
140
  loadError: 'Failed to load tasks',
127
141
  noPlan: 'This card has no plan yet — click "Add & Refine" first',