@fonlan/dsh-task-kanban 0.1.1 → 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 {
@@ -611,6 +650,11 @@ var KanbanRunner = class {
611
650
  merging = /* @__PURE__ */ new Set();
612
651
  /** Retained agent handles (kept so ended sessions stay in the store/UI). */
613
652
  handles = /* @__PURE__ */ new Map();
653
+ /** Whether startup recovery + session indexing ran (deferred until workspaces exist). */
654
+ recovered = false;
655
+ /** Agents that already got their scoped kanban tools (event idempotency). */
656
+ toolScopedAgents = /* @__PURE__ */ new WeakSet();
657
+ agentStartOff;
614
658
  constructor(ctx, store, settings) {
615
659
  this.ctx = ctx;
616
660
  this.store = store;
@@ -623,9 +667,17 @@ var KanbanRunner = class {
623
667
  this.pumpTimer = setInterval(() => {
624
668
  this.pump();
625
669
  }, 2500);
670
+ this.agentStartOff = this.ctx.on("agent/session-start", (payload) => {
671
+ this.onAgentSessionStart(payload.agent);
672
+ });
626
673
  }
627
674
  stop() {
628
675
  if (this.pumpTimer !== void 0) clearInterval(this.pumpTimer);
676
+ this.pumpTimer = void 0;
677
+ if (this.agentStartOff !== void 0) {
678
+ this.agentStartOff();
679
+ this.agentStartOff = void 0;
680
+ }
629
681
  }
630
682
  newSessionId() {
631
683
  return `session-${randomUUID()}`;
@@ -703,6 +755,52 @@ var KanbanRunner = class {
703
755
  }
704
756
  }
705
757
  }
758
+ /**
759
+ * Rebuild sessionId → cardId from the task store. Runs once the workspace
760
+ * registry is available (see pump), so kanban sessions resumed after a
761
+ * restart still resolve their card.
762
+ */
763
+ async indexSessions() {
764
+ for (const ws of await this.workspaceList()) {
765
+ const cards = await this.store.list(ws.path);
766
+ for (const card of cards) this.mapCardSessions(card.id, card);
767
+ }
768
+ }
769
+ mapCardSessions(cardId, card) {
770
+ for (const sid of card.sessions.refinement) this.sessionCards.set(sid, cardId);
771
+ for (const attempt of card.sessions.phases) this.sessionCards.set(attempt.sessionId, cardId);
772
+ for (const sid of card.sessions.merge) this.sessionCards.set(sid, cardId);
773
+ }
774
+ /** Fallback lookup when a session starts before indexSessions ran. */
775
+ async lookupCardOfSession(sessionId) {
776
+ for (const ws of await this.workspaceList()) {
777
+ const cards = await this.store.list(ws.path);
778
+ for (const card of cards) if (card.sessions.refinement.includes(sessionId) || card.sessions.phases.some((a) => a.sessionId === sessionId) || card.sessions.merge.includes(sessionId)) {
779
+ this.mapCardSessions(card.id, card);
780
+ return card.id;
781
+ }
782
+ }
783
+ }
784
+ /**
785
+ * The single place kanban tools get registered. `agent/session-start` fires
786
+ * for BOTH `agents.create` (startup) and `agents.resume` (host re-open after
787
+ * a restart), so a resumed refinement/phase/merge session keeps its scoped
788
+ * tools. Idempotent per agent object; non-kanban sessions are skipped.
789
+ */
790
+ async onAgentSessionStart(agent) {
791
+ if (agent === void 0 || agent.id === void 0 || agent.ctx === void 0) return;
792
+ if (this.toolScopedAgents.has(agent)) return;
793
+ let cardId = this.sessionCards.get(agent.id);
794
+ if (cardId === void 0) cardId = await this.lookupCardOfSession(agent.id);
795
+ if (cardId === void 0) return;
796
+ this.sessionCards.set(agent.id, cardId);
797
+ try {
798
+ registerKanbanTools(agent.ctx, this.toolResolver());
799
+ this.toolScopedAgents.add(agent);
800
+ } catch (error) {
801
+ console.error("[task-kanban] register kanban tools for " + agent.id + " failed:", String(error));
802
+ }
803
+ }
706
804
  async listCards(workspacePath) {
707
805
  return this.store.list(workspacePath);
708
806
  }
@@ -859,6 +957,21 @@ var KanbanRunner = class {
859
957
  return this.settings.get().maxParallelWorkers;
860
958
  }
861
959
  async pump() {
960
+ if (!this.recovered) {
961
+ if ((await this.workspaceList()).length > 0) {
962
+ this.recovered = true;
963
+ try {
964
+ await this.recoverInterrupted();
965
+ } catch (error) {
966
+ console.error("[task-kanban] recoverInterrupted failed:", String(error));
967
+ }
968
+ try {
969
+ await this.indexSessions();
970
+ } catch (error) {
971
+ console.error("[task-kanban] indexSessions failed:", String(error));
972
+ }
973
+ }
974
+ }
862
975
  if (this.slots >= this.maxParallel()) return;
863
976
  const workspaces = await this.workspaceList();
864
977
  for (const ws of workspaces) {
@@ -902,7 +1015,7 @@ var KanbanRunner = class {
902
1015
  async runRefinement(cardId, wsPath) {
903
1016
  const card = await this.store.get(wsPath, cardId);
904
1017
  if (card === void 0) return;
905
- const route = await this.modelRoute(cardId, wsPath);
1018
+ const route = await this.modelRoute(cardId, wsPath, "refine");
906
1019
  const sessionId = this.newSessionId();
907
1020
  this.sessionCards.set(sessionId, cardId);
908
1021
  await this.store.mutate(cardId, (c) => {
@@ -911,7 +1024,7 @@ var KanbanRunner = class {
911
1024
  c.sessions.refinement.push(sessionId);
912
1025
  }, wsPath);
913
1026
  try {
914
- const handle = await this.createAgent(sessionId, wsPath, route, this.toolResolver());
1027
+ const handle = await this.createAgent(sessionId, wsPath, route, "refine");
915
1028
  this.keepHandle(cardId, handle);
916
1029
  await this.attachRefinementSession(wsPath, sessionId);
917
1030
  const skill = card.skill !== void 0 ? await loadCardSkill(this.ctx, card.skill, wsPath, handle.agent) : void 0;
@@ -1015,20 +1128,47 @@ var KanbanRunner = class {
1015
1128
  dispose
1016
1129
  };
1017
1130
  }
1018
- async createAgent(sessionId, cwd, route, resolver) {
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) {
1019
1142
  const agentOptions = route.provider !== void 0 && route.model !== void 0 && route.model !== "" ? {
1020
1143
  provider: route.provider,
1021
1144
  model: route.model
1022
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
+ }
1023
1160
  return this.ctx.agents.create({
1024
1161
  sessionId,
1025
- meta: { cwd },
1162
+ meta: {
1163
+ cwd,
1164
+ ...presetId !== void 0 ? { agentPreset: presetId } : {}
1165
+ },
1026
1166
  ...agentOptions !== void 0 ? { agentOptions } : {},
1027
1167
  setup: async (agentCtx) => {
1028
1168
  const agentPresets = agentCtx.get("agentPresets");
1029
- if (agentPresets !== void 0) await agentPresets.mount(agentCtx, "standard");
1169
+ if (agentPresets !== void 0) await agentPresets.mount(agentCtx, presetId);
1030
1170
  else throw new Error("@fonlan/dsh-task-kanban: agent-presets service is not mounted");
1031
- registerKanbanTools(agentCtx, resolver);
1171
+ if (selectionRef.current !== void 0 && selectionRef.current.reasoningEffort !== void 0) installModelSelection(agentCtx, selectionRef);
1032
1172
  }
1033
1173
  });
1034
1174
  }
@@ -1060,8 +1200,15 @@ var KanbanRunner = class {
1060
1200
  if (candidate !== void 0 && typeof candidate.resolveByPath === "function") return candidate;
1061
1201
  }
1062
1202
  }
1063
- /** Resolve the provider+model route for an agent (both are required). */
1064
- 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) {
1065
1212
  const card = await this.store.get(wsPath, cardId);
1066
1213
  if (card === void 0) return {};
1067
1214
  if (card.model !== "" && card.provider !== void 0 && card.provider !== "") return {
@@ -1075,14 +1222,12 @@ var KanbanRunner = class {
1075
1222
  model: card.model
1076
1223
  } : { model: card.model };
1077
1224
  }
1078
- const userDefault = this.settings.get().defaultModel;
1079
- if (userDefault !== "") {
1080
- const fallback = this.settings.defaultModelRoute();
1081
- return fallback.provider !== void 0 ? {
1082
- provider: fallback.provider,
1083
- model: userDefault
1084
- } : { model: userDefault };
1085
- }
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
+ };
1086
1231
  return this.settings.defaultModelRoute();
1087
1232
  }
1088
1233
  async runCard(wsPath, card, entry) {
@@ -1179,9 +1324,9 @@ var KanbanRunner = class {
1179
1324
  startedAt: Date.now()
1180
1325
  });
1181
1326
  }, wsPath);
1182
- const route = await this.modelRoute(cardId, wsPath);
1327
+ const route = await this.modelRoute(cardId, wsPath, "phase");
1183
1328
  try {
1184
- const handle = await this.createAgent(sessionId, workdir, route, this.toolResolver());
1329
+ const handle = await this.createAgent(sessionId, workdir, route, "phase");
1185
1330
  this.keepHandle(cardId, handle);
1186
1331
  entry.cancel = () => handle.agent.cancel({ kind: "user" });
1187
1332
  handle.agent.followup(this.asUserMessage(phasePrompt({
@@ -1473,9 +1618,9 @@ var KanbanRunner = class {
1473
1618
  await this.store.mutate(cardId, (c) => {
1474
1619
  c.sessions.merge.push(sessionId);
1475
1620
  }, wsPath);
1476
- const route = await this.modelRoute(cardId, wsPath);
1621
+ const route = await this.modelRoute(cardId, wsPath, "merge");
1477
1622
  try {
1478
- const handle = await this.createAgent(sessionId, workdir, route, this.toolResolver());
1623
+ const handle = await this.createAgent(sessionId, workdir, route, "merge");
1479
1624
  this.keepHandle(cardId, handle);
1480
1625
  handle.agent.followup(this.asUserMessage(mergePrompt(workdir, conflicts)));
1481
1626
  await handle.agent.whenIdle();
@@ -1532,6 +1677,28 @@ async function listModels(ctx) {
1532
1677
  } catch {}
1533
1678
  return out;
1534
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
+ }
1535
1702
  //#endregion
1536
1703
  //#region src/server/rpc.ts
1537
1704
  const API_PREFIX = "/plugins/@fonlan/dsh-task-kanban/api";
@@ -1664,14 +1831,37 @@ function registerApiRoutes(ctx, runner, settings) {
1664
1831
  route("retry", async (p) => runner.retryTask(requireString(p.cardId, "cardId")));
1665
1832
  route("remove", async (p) => runner.deleteTask(requireString(p.cardId, "cardId")));
1666
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
+ ];
1667
1845
  route("settings.set", async (p) => {
1668
1846
  const patch = {};
1669
1847
  if (typeof p.maxParallelWorkers === "number") patch.maxParallelWorkers = p.maxParallelWorkers;
1670
- 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];
1671
1849
  await settings.update(patch);
1672
1850
  return settings.get();
1673
1851
  });
1674
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
+ });
1675
1865
  return () => {
1676
1866
  for (const d of disposers) d();
1677
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>;
@@ -17,6 +17,11 @@ export declare class KanbanRunner implements KanbanToolResolver {
17
17
  private merging;
18
18
  /** Retained agent handles (kept so ended sessions stay in the store/UI). */
19
19
  private handles;
20
+ /** Whether startup recovery + session indexing ran (deferred until workspaces exist). */
21
+ private recovered;
22
+ /** Agents that already got their scoped kanban tools (event idempotency). */
23
+ private toolScopedAgents;
24
+ private agentStartOff;
20
25
  constructor(ctx: Context, store: TaskStore, settings: KanbanSettingsFace);
21
26
  register(ctx: Context): void;
22
27
  start(): void;
@@ -30,6 +35,22 @@ export declare class KanbanRunner implements KanbanToolResolver {
30
35
  mergeResolved(sessionId: string): Promise<void>;
31
36
  private toolResolver;
32
37
  recoverInterrupted(): Promise<void>;
38
+ /**
39
+ * Rebuild sessionId → cardId from the task store. Runs once the workspace
40
+ * registry is available (see pump), so kanban sessions resumed after a
41
+ * restart still resolve their card.
42
+ */
43
+ private indexSessions;
44
+ private mapCardSessions;
45
+ /** Fallback lookup when a session starts before indexSessions ran. */
46
+ private lookupCardOfSession;
47
+ /**
48
+ * The single place kanban tools get registered. `agent/session-start` fires
49
+ * for BOTH `agents.create` (startup) and `agents.resume` (host re-open after
50
+ * a restart), so a resumed refinement/phase/merge session keeps its scoped
51
+ * tools. Idempotent per agent object; non-kanban sessions are skipped.
52
+ */
53
+ private onAgentSessionStart;
33
54
  listCards(workspacePath: string): Promise<KanbanCard[]>;
34
55
  createTask(input: {
35
56
  workspacePath: string;
@@ -82,6 +103,16 @@ export declare class KanbanRunner implements KanbanToolResolver {
82
103
  * resolve it — only real human replies drive the interactive refinement.
83
104
  */
84
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
+ */
85
116
  private createAgent;
86
117
  private keepHandle;
87
118
  /**
@@ -93,7 +124,14 @@ export declare class KanbanRunner implements KanbanToolResolver {
93
124
  private attachRefinementSession;
94
125
  /** The host workspace registry, whichever service name carries it. */
95
126
  private attachRegistry;
96
- /** 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
+ */
97
135
  private modelRoute;
98
136
  private runCard;
99
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.1.1",
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
+ }