@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/README.md +12 -0
- package/lib/client.js +371 -122
- package/lib/client.js.map +1 -1
- package/lib/index.js +214 -24
- package/lib/types/server/models.d.ts +16 -0
- package/lib/types/server/runner.d.ts +39 -1
- package/lib/types/server/settings.d.ts +35 -7
- package/lib/types/shared/card.d.ts +12 -1
- package/package.json +5 -1
- package/src/client/api.ts +19 -1
- package/src/client/board.css +107 -16
- package/src/client/board.tsx +65 -29
- package/src/client/index.tsx +11 -9
- package/src/client/locales.ts +24 -4
- package/src/client/sections.tsx +244 -48
- package/src/server/models.ts +40 -0
- package/src/server/rpc.ts +33 -4
- package/src/server/runner.ts +168 -20
- package/src/server/settings.ts +52 -7
- package/src/shared/card.ts +13 -1
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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: {
|
|
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,
|
|
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
|
-
|
|
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
|
-
/**
|
|
1064
|
-
|
|
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
|
|
1079
|
-
if (
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
/**
|
|
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,
|
|
3
|
-
* persisted through the settings service
|
|
4
|
-
* settings service requires a lowercase
|
|
5
|
-
* (/^[a-z][a-z0-9-]*$/), so the scoped package name
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|