@fonlan/dsh-task-kanban 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -10,48 +10,67 @@ import { isSkillName, isUserInvocable, renderSkillContent } from "@deepseek-ai/d
10
10
  import { defineTool } from "@deepseek-ai/dsh-tools";
11
11
  //#region src/server/settings.ts
12
12
  const KANBAN_SETTINGS_NS = "task-kanban";
13
+ /**
14
+ * The entry config schema. Deliberately UNANNOTATED: schemastery >= 3.18.3
15
+ * added a third `Mode` type parameter, so `.default().volatile()` fields have
16
+ * output type `Volatile<T>` and an explicit `z<KanbanSettingsShape>` no longer
17
+ * compiles (the schema's `meta.default` then carries `Volatile<number>`, which
18
+ * is not a `number`). The plain `KanbanSettingsShape` interface stays the
19
+ * plugin-facing shape: `registerSettings` unwraps the live cells below.
20
+ */
13
21
  const KanbanSettingsSchema = z.object({
14
- maxParallelWorkers: z.natural().min(1).default(1),
15
- refinementModel: z.string().default(""),
16
- refinementProvider: z.string().default(""),
17
- refinementReasoningEffort: z.string().default(""),
18
- refinementPreset: z.string().default(""),
19
- phaseModel: z.string().default(""),
20
- phaseProvider: z.string().default(""),
21
- phaseReasoningEffort: z.string().default(""),
22
- phasePreset: z.string().default("")
22
+ maxParallelWorkers: z.natural().min(1).default(1).volatile(),
23
+ refinementModel: z.string().default("").volatile(),
24
+ refinementProvider: z.string().default("").volatile(),
25
+ refinementReasoningEffort: z.string().default("").volatile(),
26
+ refinementPreset: z.string().default("").volatile(),
27
+ phaseModel: z.string().default("").volatile(),
28
+ phaseProvider: z.string().default("").volatile(),
29
+ phaseReasoningEffort: z.string().default("").volatile(),
30
+ phasePreset: z.string().default("").volatile()
23
31
  });
32
+ /**
33
+ * Dereference one config field. `Config` fields marked `.volatile()` are handed
34
+ * to the plugin as references (`{ get() }`) instead of plain values, so reading
35
+ * them raw yields the reference object — which serializes to `{}` and makes
36
+ * every setting look empty. Non-volatile (plain) configs keep working as-is.
37
+ */
38
+ function fieldValue(value, fallback) {
39
+ if (value !== null && typeof value === "object" && typeof value.get === "function") return value.get() ?? fallback;
40
+ return value ?? fallback;
41
+ }
24
42
  const EMPTY_DEFAULTS = {
25
43
  model: "",
26
44
  provider: "",
27
45
  reasoningEffort: "",
28
46
  preset: ""
29
47
  };
30
- function registerSettings(ctx) {
31
- let current = {
32
- maxParallelWorkers: 1,
33
- refinementModel: "",
34
- refinementProvider: "",
35
- refinementReasoningEffort: "",
36
- refinementPreset: "",
37
- phaseModel: "",
38
- phaseProvider: "",
39
- phaseReasoningEffort: "",
40
- phasePreset: ""
41
- };
48
+ function registerSettings(ctx, config) {
49
+ const fields = config ?? {};
50
+ const snapshot = () => ({
51
+ maxParallelWorkers: fieldValue(fields.maxParallelWorkers, 1),
52
+ refinementModel: fieldValue(fields.refinementModel, ""),
53
+ refinementProvider: fieldValue(fields.refinementProvider, ""),
54
+ refinementReasoningEffort: fieldValue(fields.refinementReasoningEffort, ""),
55
+ refinementPreset: fieldValue(fields.refinementPreset, ""),
56
+ phaseModel: fieldValue(fields.phaseModel, ""),
57
+ phaseProvider: fieldValue(fields.phaseProvider, ""),
58
+ phaseReasoningEffort: fieldValue(fields.phaseReasoningEffort, ""),
59
+ phasePreset: fieldValue(fields.phasePreset, "")
60
+ });
61
+ let pending = {};
62
+ const current = () => ({
63
+ ...snapshot(),
64
+ ...pending
65
+ });
42
66
  let service;
43
67
  ctx.inject(["settings"], (sctx) => {
44
68
  service = sctx.settings;
45
- const scope = service.register(KANBAN_SETTINGS_NS, KanbanSettingsSchema);
46
- current = scope.get();
47
- scope.watch((next) => {
48
- current = next;
49
- });
50
69
  });
51
70
  return {
52
- get: () => current,
71
+ get: () => current(),
53
72
  sessionDefaults: (kind) => {
54
- const s = current;
73
+ const s = current();
55
74
  switch (kind) {
56
75
  case "refine": return {
57
76
  model: s.refinementModel,
@@ -85,6 +104,10 @@ function registerSettings(ctx) {
85
104
  update: async (patch) => {
86
105
  if (service === void 0) throw new Error("@fonlan/dsh-task-kanban: settings service is not available in this profile");
87
106
  await service.update(KANBAN_SETTINGS_NS, patch);
107
+ pending = {
108
+ ...pending,
109
+ ...patch
110
+ };
88
111
  }
89
112
  };
90
113
  }
@@ -653,7 +676,7 @@ var KanbanRunner = class {
653
676
  recovered = false;
654
677
  /** Agents that already got their scoped kanban tools (event idempotency). */
655
678
  toolScopedAgents = /* @__PURE__ */ new WeakSet();
656
- agentStartOff;
679
+ agentCreatedOff;
657
680
  constructor(ctx, store, settings) {
658
681
  this.ctx = ctx;
659
682
  this.store = store;
@@ -666,16 +689,16 @@ var KanbanRunner = class {
666
689
  this.pumpTimer = setInterval(() => {
667
690
  this.pump();
668
691
  }, 2500);
669
- this.agentStartOff = this.ctx.on("agent/session-start", (payload) => {
670
- this.onAgentSessionStart(payload.agent);
692
+ this.agentCreatedOff = this.ctx.on("agent/created", (payload) => {
693
+ this.onAgentCreated(payload.agent);
671
694
  });
672
695
  }
673
696
  stop() {
674
697
  if (this.pumpTimer !== void 0) clearInterval(this.pumpTimer);
675
698
  this.pumpTimer = void 0;
676
- if (this.agentStartOff !== void 0) {
677
- this.agentStartOff();
678
- this.agentStartOff = void 0;
699
+ if (this.agentCreatedOff !== void 0) {
700
+ this.agentCreatedOff();
701
+ this.agentCreatedOff = void 0;
679
702
  }
680
703
  }
681
704
  newSessionId() {
@@ -781,12 +804,13 @@ var KanbanRunner = class {
781
804
  }
782
805
  }
783
806
  /**
784
- * The single place kanban tools get registered. `agent/session-start` fires
785
- * for BOTH `agents.create` (startup) and `agents.resume` (host re-open after
786
- * a restart), so a resumed refinement/phase/merge session keeps its scoped
807
+ * The single place kanban tools get registered. Subscribed to
808
+ * `agent/created`, which fires for BOTH `agents.create` (startup) and
809
+ * `agents.resume` (host re-open after a restart) and carries the path in its
810
+ * `source`, so a resumed refinement/phase/merge session keeps its scoped
787
811
  * tools. Idempotent per agent object; non-kanban sessions are skipped.
788
812
  */
789
- async onAgentSessionStart(agent) {
813
+ async onAgentCreated(agent) {
790
814
  if (agent === void 0 || agent.id === void 0 || agent.ctx === void 0) return;
791
815
  if (this.toolScopedAgents.has(agent)) return;
792
816
  let cardId = this.sessionCards.get(agent.id);
@@ -1901,9 +1925,9 @@ const inject = [
1901
1925
  "agents",
1902
1926
  "skills"
1903
1927
  ];
1904
- const Config = z.object({});
1905
- function apply(ctx, _config) {
1906
- const settings = registerSettings(ctx);
1928
+ const Config = KanbanSettingsSchema;
1929
+ function apply(ctx, config) {
1930
+ const settings = registerSettings(ctx, config);
1907
1931
  const runner = new KanbanRunner(ctx, new TaskStore(), settings);
1908
1932
  ctx.effect(() => {
1909
1933
  runner.recoverInterrupted();
@@ -7,8 +7,28 @@
7
7
  * serves the fenced JSON API the board UI calls.
8
8
  */
9
9
  import type { Context } from '@deepseek-ai/cordis';
10
- import z from '@deepseek-ai/schemastery';
10
+ import type { KanbanSettingsShape } from './shared/card.js';
11
11
  export declare const name = "@fonlan/dsh-task-kanban";
12
12
  export declare const inject: string[];
13
- export declare const Config: z<Schemastery.ObjectS<{}>, Schemastery.ObjectT<{}>>;
14
- export declare function apply(ctx: Context, _config: unknown): void;
13
+ export declare const Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
14
+ maxParallelWorkers: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
15
+ refinementModel: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
16
+ refinementProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
17
+ refinementReasoningEffort: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
18
+ refinementPreset: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
19
+ phaseModel: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
20
+ phaseProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
21
+ phaseReasoningEffort: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
22
+ phasePreset: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
23
+ }>>, Schemastery.ObjectT<NoInfer<{
24
+ maxParallelWorkers: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
25
+ refinementModel: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
26
+ refinementProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
27
+ refinementReasoningEffort: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
28
+ refinementPreset: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
29
+ phaseModel: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
30
+ phaseProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
31
+ phaseReasoningEffort: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
32
+ phasePreset: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
33
+ }>>, "plain">;
34
+ export declare function apply(ctx: Context, config: KanbanSettingsShape): void;
@@ -21,7 +21,7 @@ export declare class KanbanRunner implements KanbanToolResolver {
21
21
  private recovered;
22
22
  /** Agents that already got their scoped kanban tools (event idempotency). */
23
23
  private toolScopedAgents;
24
- private agentStartOff;
24
+ private agentCreatedOff;
25
25
  constructor(ctx: Context, store: TaskStore, settings: KanbanSettingsFace);
26
26
  register(ctx: Context): void;
27
27
  start(): void;
@@ -45,12 +45,13 @@ export declare class KanbanRunner implements KanbanToolResolver {
45
45
  /** Fallback lookup when a session starts before indexSessions ran. */
46
46
  private lookupCardOfSession;
47
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
48
+ * The single place kanban tools get registered. Subscribed to
49
+ * `agent/created`, which fires for BOTH `agents.create` (startup) and
50
+ * `agents.resume` (host re-open after a restart) and carries the path in its
51
+ * `source`, so a resumed refinement/phase/merge session keeps its scoped
51
52
  * tools. Idempotent per agent object; non-kanban sessions are skipped.
52
53
  */
53
- private onAgentSessionStart;
54
+ private onAgentCreated;
54
55
  listCards(workspacePath: string): Promise<KanbanCard[]>;
55
56
  createTask(input: {
56
57
  workspacePath: string;
@@ -1,35 +1,43 @@
1
1
  /**
2
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.
3
+ * model/effort/preset defaults), carried by this bundle's entry config: dsh
4
+ * >= 0.1.7 owns plugin settings as entry config (the plugin's `Config`
5
+ * schema), so the face reads the config it was applied with and persists
6
+ * updates through `settings.update`.
7
7
  */
8
8
  import type { Context } from '@deepseek-ai/cordis';
9
9
  import z from '@deepseek-ai/schemastery';
10
10
  import type { KanbanSessionKind, KanbanSettingsShape } from '../shared/card.js';
11
11
  export declare const KANBAN_SETTINGS_NS = "task-kanban";
12
- export declare const KanbanSettingsSchema: z<Schemastery.ObjectS<{
13
- maxParallelWorkers: z<number, number>;
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>;
22
- }>, Schemastery.ObjectT<{
23
- maxParallelWorkers: z<number, number>;
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>;
32
- }>>;
12
+ /**
13
+ * The entry config schema. Deliberately UNANNOTATED: schemastery >= 3.18.3
14
+ * added a third `Mode` type parameter, so `.default().volatile()` fields have
15
+ * output type `Volatile<T>` and an explicit `z<KanbanSettingsShape>` no longer
16
+ * compiles (the schema's `meta.default` then carries `Volatile<number>`, which
17
+ * is not a `number`). The plain `KanbanSettingsShape` interface stays the
18
+ * plugin-facing shape: `registerSettings` unwraps the live cells below.
19
+ */
20
+ export declare const KanbanSettingsSchema: z<Schemastery.ObjectS<NoInfer<{
21
+ maxParallelWorkers: z<number, number, "volatile-defined">;
22
+ refinementModel: z<string, string, "volatile-defined">;
23
+ refinementProvider: z<string, string, "volatile-defined">;
24
+ refinementReasoningEffort: z<string, string, "volatile-defined">;
25
+ refinementPreset: z<string, string, "volatile-defined">;
26
+ phaseModel: z<string, string, "volatile-defined">;
27
+ phaseProvider: z<string, string, "volatile-defined">;
28
+ phaseReasoningEffort: z<string, string, "volatile-defined">;
29
+ phasePreset: z<string, string, "volatile-defined">;
30
+ }>>, Schemastery.ObjectT<NoInfer<{
31
+ maxParallelWorkers: z<number, number, "volatile-defined">;
32
+ refinementModel: z<string, string, "volatile-defined">;
33
+ refinementProvider: z<string, string, "volatile-defined">;
34
+ refinementReasoningEffort: z<string, string, "volatile-defined">;
35
+ refinementPreset: z<string, string, "volatile-defined">;
36
+ phaseModel: z<string, string, "volatile-defined">;
37
+ phaseProvider: z<string, string, "volatile-defined">;
38
+ phaseReasoningEffort: z<string, string, "volatile-defined">;
39
+ phasePreset: z<string, string, "volatile-defined">;
40
+ }>>, "plain">;
33
41
  /** The per-session-type default slice a session kind resolves. */
34
42
  export interface KanbanSessionDefaults {
35
43
  model: string;
@@ -52,4 +60,4 @@ export interface KanbanSettingsFace {
52
60
  */
53
61
  sessionDefaults(kind: KanbanSessionKind): KanbanSessionDefaults;
54
62
  }
55
- export declare function registerSettings(ctx: Context): KanbanSettingsFace;
63
+ export declare function registerSettings(ctx: Context, config: KanbanSettingsShape): KanbanSettingsFace;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonlan/dsh-task-kanban",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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": {
@@ -78,54 +78,54 @@
78
78
  },
79
79
  "peerDependencies": {
80
80
  "@deepseek-ai/cordis": "^4.0.2",
81
- "@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
82
- "@deepseek-ai/dsh-agent-presets": "^0.1.5-rc.2",
83
- "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
84
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
85
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
86
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
87
- "@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
81
+ "@deepseek-ai/dsh-agent": "^0.1.5-rc.2 || ^0.2.0-rc.1",
82
+ "@deepseek-ai/dsh-agent-preset-registry": "^0.1.7-rc.1 || ^0.2.0-rc.1",
83
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.2 || ^0.2.0-rc.1",
84
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2 || ^0.2.0-rc.1",
85
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2 || ^0.2.0-rc.1",
86
+ "@deepseek-ai/dsh-client-ui-layout": "^0.1.5-rc.2 || ^0.2.0-rc.1",
87
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2 || ^0.2.0-rc.1",
88
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2 || ^0.2.0-rc.1",
89
+ "@deepseek-ai/dsh-client-ui-session": "^0.1.5-rc.2 || ^0.2.0-rc.1",
90
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.5-rc.2 || ^0.2.0-rc.1",
91
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2 || ^0.2.0-rc.1",
92
+ "@deepseek-ai/dsh-client-ui-workspace": "^0.1.5-rc.2 || ^0.2.0-rc.1",
93
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.2 || ^0.2.0-rc.1",
94
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.2 || ^0.2.0-rc.1",
95
+ "@deepseek-ai/dsh-settings": "^0.1.5-rc.2 || ^0.2.0-rc.1",
96
+ "@deepseek-ai/dsh-skill": "^0.1.5-rc.2 || ^0.2.0-rc.1",
97
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.2 || ^0.2.0-rc.1",
88
98
  "@deepseek-ai/schemastery": "^3.18.2",
89
- "@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.2",
90
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
91
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
92
- "@deepseek-ai/dsh-client-ui-layout": "^0.1.5-rc.2",
93
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
94
- "@deepseek-ai/dsh-client-ui-session": "^0.1.5-rc.2",
95
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
96
- "@deepseek-ai/dsh-client-ui-workspace": "^0.1.5-rc.2",
97
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
98
- "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.5-rc.2",
99
99
  "react": "^18.2.0",
100
100
  "react-dom": "^18.2.0"
101
101
  },
102
102
  "devDependencies": {
103
+ "@deepseek-ai/dsh-agent": "0.2.0-rc.1",
104
+ "@deepseek-ai/dsh-agent-preset": "0.2.0-rc.1",
105
+ "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.1",
106
+ "@deepseek-ai/dsh-client-locale": "0.2.0-rc.1",
107
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.1",
108
+ "@deepseek-ai/dsh-client-ui-layout": "0.2.0-rc.1",
109
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.1",
110
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.1",
111
+ "@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.1",
112
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.1",
113
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "0.2.0-rc.1",
114
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.2.0-rc.1",
115
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.1",
116
+ "@deepseek-ai/dsh-client-ui-workspace": "0.2.0-rc.1",
117
+ "@deepseek-ai/dsh-host-webserver": "0.2.0-rc.1",
118
+ "@deepseek-ai/dsh-llm": "0.2.0-rc.1",
119
+ "@deepseek-ai/dsh-session": "0.2.0-rc.1",
120
+ "@deepseek-ai/dsh-settings": "0.2.0-rc.1",
121
+ "@deepseek-ai/dsh-skill": "0.2.0-rc.1",
122
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.1",
123
+ "@deepseek-ai/schemastery": "^3.18.4",
103
124
  "@types/node": "^22.0.0",
104
125
  "@types/react": "~18.3.1",
105
126
  "@types/react-dom": "~18.3.0",
106
127
  "tsdown": "^0.22.14",
107
128
  "typescript": "^5.9.0",
108
- "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
109
- "@deepseek-ai/dsh-agent-presets": "^0.1.5-rc.2",
110
- "@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
111
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
112
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
113
- "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
114
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
115
- "@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
116
- "@deepseek-ai/schemastery": "^3.18.2",
117
- "@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.2",
118
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
119
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
120
- "@deepseek-ai/dsh-client-ui-layout": "^0.1.5-rc.2",
121
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
122
- "@deepseek-ai/dsh-client-ui-session": "^0.1.5-rc.2",
123
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.5-rc.2",
124
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
125
- "@deepseek-ai/dsh-client-ui-workspace": "^0.1.5-rc.2",
126
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
127
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
128
- "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.5-rc.2",
129
129
  "vitest": "^3.2.0"
130
130
  },
131
131
  "packageManager": "pnpm@11.21.0"
@@ -386,110 +386,3 @@
386
386
  font-size: 13px;
387
387
  line-height: 1.5;
388
388
  }
389
- .kb-footer-action {
390
- display: inline-flex;
391
- align-items: center;
392
- justify-content: center;
393
- gap: 6px;
394
- width: 32px;
395
- height: 32px;
396
- border-radius: 6px;
397
- border: none;
398
- background: transparent;
399
- /* same foreground as the settings trigger (its trigger css uses this var) */
400
- color: var(--dsw-alias-label-primary, inherit);
401
- cursor: pointer;
402
- }
403
- .kb-footer-action:hover { background: rgba(128, 128, 128, 0.18); }
404
- .kb-footer-action-active { background: rgba(59, 130, 246, 0.25); }
405
- /* Wide mode borrows the settings trigger's metrics (34px tall, 14px label,
406
- 18px icon) so both controls read as siblings on the same line. */
407
- .kb-footer-action-wide {
408
- width: auto;
409
- height: 34px;
410
- padding: 0 8px;
411
- border-radius: 12px;
412
- font-size: 14px;
413
- }
414
- .kb-footer-action-wide .kb-footer-label {
415
- font-size: 14px;
416
- line-height: 22px;
417
- }
418
- /* Theme following is handled by the --dsw-alias-* tokens above: they change
419
- value with the shell's selected light/dark appearance, so no OS-level
420
- prefers-color-scheme override is needed (and one would fight the app
421
- theme when the two disagree). */
422
- /* Same-line placement WITHOUT moving any DOM: the sidebar foot area becomes
423
- a reversed flex row, so its LAST child (the settings area) renders on the
424
- LEFT and the FIRST child (footer actions = the kanban button) on the RIGHT
425
- — exactly "settings, then kanban". The settings trigger flexes to fill the
426
- remaining width; the button keeps its content width. */
427
- .kb-foot-row {
428
- display: flex;
429
- flex-direction: row-reverse;
430
- align-items: stretch;
431
- gap: 4px;
432
- }
433
- .kb-foot-row > div:first-child {
434
- flex: none;
435
- width: auto;
436
- min-width: 0;
437
- display: flex;
438
- align-items: center;
439
- }
440
- .kb-foot-row > div:last-child {
441
- flex: 1;
442
- min-width: 0;
443
- }
444
- /* Rail (collapsed sidebar): column-reverse stacks the settings circle on top
445
- and the kanban icon below it, both centered; the kanban button borrows the
446
- settings trigger's rail chrome (36px circle) so the two match. */
447
- .kb-foot-rail {
448
- flex-direction: column-reverse;
449
- align-items: center;
450
- }
451
- .kb-foot-rail .kb-footer-action {
452
- width: 36px;
453
- height: 36px;
454
- border-radius: 50%;
455
- margin: 8px 0 10px;
456
- }
457
- .kb-footer-label {
458
- max-width: 110px;
459
- overflow: hidden;
460
- text-overflow: ellipsis;
461
- white-space: nowrap;
462
- }
463
- /* Narrow screens (mobile drawer / auto-collapse breakpoint): the drawer's
464
- foot area is ~256-280px but the footer actions row carries the kanban
465
- button PLUS mobile-nav's drawer actions (Files / session log), which
466
- together exceed the row and squeeze the settings trigger to zero width
467
- (row-reverse overflows left, clipping the button). Below the shell's
468
- auto-collapse breakpoint, stack the settings row above the actions and
469
- let the actions wrap so nothing is squeezed or clipped. */
470
- @media (max-width: 1023px) {
471
- .kb-foot-row {
472
- flex-direction: column-reverse;
473
- align-items: stretch;
474
- }
475
- .kb-foot-row > div:first-child {
476
- flex: none;
477
- width: 100%;
478
- display: flex;
479
- flex-wrap: wrap;
480
- justify-content: flex-start;
481
- gap: 4px;
482
- padding-top: 4px;
483
- }
484
- .kb-foot-row > div:last-child {
485
- flex: none;
486
- width: 100%;
487
- }
488
- /* The footer-actions entries are display:contents wrappers, so the kanban
489
- button itself is a flex item of the wrapping actions row; pull it to the
490
- first wrap position so it sits directly below the settings row (the
491
- mobile-nav drawer actions follow on their own wrapped line). */
492
- .kb-foot-row .kb-footer-action {
493
- order: -1;
494
- }
495
- }
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
2
  import type { KanbanCard, Lane, PhaseAttempt, PlanPhase } from '../shared/card'
3
3
  import { DROP_RULES, laneOf } from '../shared/lanes'
4
4
  import { api, gatewaySkillList, type SkillOption } from './api'
5
- import { exitBoard, getClient } from './kanban-state'
5
+ import { openSessionView } from './kanban-state'
6
6
  import { en, zh } from './locales'
7
7
  import './board.css'
8
8
 
@@ -157,14 +157,10 @@ export function BoardRoot(props: BoardProps): JSX.Element {
157
157
  }, [cards, refresh, showToast, t])
158
158
 
159
159
  const openSession = useCallback((sessionId: string) => {
160
- const ctx = getClient()
161
- exitBoard()
162
- if (ctx === null) return
163
- try {
164
- ;(ctx.sessions as { open(id: string): void }).open(sessionId)
165
- } catch {
166
- showToast(t('sessionNotListed'))
167
- }
160
+ // The shell's view owner retains the session and reveals the Conversation
161
+ // in one action, so the board itself needs no panel teardown; a runtime
162
+ // without that face cannot show the session at all.
163
+ if (!openSessionView(sessionId)) showToast(t('sessionNotListed'))
168
164
  }, [showToast, t])
169
165
 
170
166
  if (items.length === 0) {
@@ -0,0 +1,74 @@
1
+ import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
2
+
3
+ /** Props every shell icon accepts: square edge in px, color rides currentColor. */
4
+ export interface IconProps {
5
+ size?: number
6
+ className?: string
7
+ }
8
+
9
+ /** One shell icon component. */
10
+ export type IconComponent = (props: IconProps) => JSX.Element
11
+
12
+ /** The runtime export table, read by capability instead of by one hard-coded name. */
13
+ const table = primitives as unknown as Record<string, IconComponent | undefined>
14
+
15
+ /** First name the runtime actually provides (a missing export is `undefined`). */
16
+ function firstDefined(...names: string[]): IconComponent | undefined {
17
+ for (const name of names) {
18
+ let candidate: IconComponent | undefined
19
+ try {
20
+ candidate = table[name]
21
+ } catch {
22
+ // Some module namespaces reject unknown names instead of yielding
23
+ // `undefined`; a rename must never throw on its way to the fallback.
24
+ continue
25
+ }
26
+ if (typeof candidate === 'function') return candidate
27
+ }
28
+ return undefined
29
+ }
30
+
31
+ /**
32
+ * Self-contained 16×16 checklist glyph (the 0.1.7 artwork) used when the shell
33
+ * primitives expose none of the names above: the footer entry must render
34
+ * something even on a runtime whose icon exports we do not know.
35
+ */
36
+ function ChecklistFallback({ size = 14, className }: IconProps): JSX.Element {
37
+ return (
38
+ <svg
39
+ width={size}
40
+ height={size}
41
+ className={className}
42
+ viewBox="0 0 16 16"
43
+ fill="none"
44
+ xmlns="http://www.w3.org/2000/svg"
45
+ aria-hidden="true"
46
+ strokeWidth={1}
47
+ >
48
+ <path d="M3.75 6.25C4.7165 6.25 5.5 5.4665 5.5 4.5C5.5 3.5335 4.7165 2.75 3.75 2.75C2.7835 2.75 2 3.5335 2 4.5C2 5.4665 2.7835 6.25 3.75 6.25Z" stroke="currentColor" />
49
+ <path d="M7.5 4.5H13.5" stroke="currentColor" />
50
+ <path d="M3.75 13.25C4.7165 13.25 5.5 12.4665 5.5 11.5C5.5 10.5335 4.7165 9.75 3.75 9.75C2.7835 9.75 2 10.5335 2 11.5C2 12.4665 2.7835 13.25 3.75 13.25Z" stroke="currentColor" />
51
+ <path d="M7.5 11.5H13.5" stroke="currentColor" />
52
+ </svg>
53
+ )
54
+ }
55
+
56
+ /**
57
+ * The checklist icon used by the sidebar panel row entry.
58
+ *
59
+ * DSH 0.1.7 renamed every size-suffixed shell icon (`IconChecklistOutline14`)
60
+ * into stroke-weight variants (`…Regular` 1px / `…Medium` 1.3px) inside
61
+ * `@deepseek-ai/dsh-client-ui-primitives`. A stale named import resolves to
62
+ * `undefined` and React then throws "Element type is invalid" while rendering
63
+ * the slot entry — the entry is dropped and the button silently disappears
64
+ * (exactly what upgrading to 0.1.7 did to this plugin). Resolve the name at
65
+ * runtime, newest first, so the next rename degrades to another shipped
66
+ * variant instead of taking the button down with it.
67
+ */
68
+ export const ChecklistIcon: IconComponent =
69
+ firstDefined(
70
+ 'IconChecklistOutlineRegular',
71
+ 'IconChecklistOutlineMedium',
72
+ 'IconChecklistOutline',
73
+ 'IconChecklistOutline14',
74
+ ) ?? ChecklistFallback