dsh-brief-sidebar 0.3.0

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.
Files changed (40) hide show
  1. package/README.md +178 -0
  2. package/cordis.patch.yml +18 -0
  3. package/dsh.plugin.json +16 -0
  4. package/lib/client.js +960 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/index.mjs +5620 -0
  7. package/lib/index.mjs.map +1 -0
  8. package/lib/types/client/BriefIcon.d.ts +13 -0
  9. package/lib/types/client/SummaryTab.d.ts +18 -0
  10. package/lib/types/client/TodoBoardTab.d.ts +31 -0
  11. package/lib/types/client/brief/board.d.ts +50 -0
  12. package/lib/types/client/brief/dock-shadow.d.ts +49 -0
  13. package/lib/types/client/brief/use-todos.d.ts +13 -0
  14. package/lib/types/client/file-open.d.ts +50 -0
  15. package/lib/types/client/index.d.ts +23 -0
  16. package/lib/types/client/locales.d.ts +53 -0
  17. package/lib/types/client/summary/DeliverablesSection.d.ts +18 -0
  18. package/lib/types/client/summary/deliverables.d.ts +40 -0
  19. package/lib/types/client/use-projection.d.ts +54 -0
  20. package/lib/types/index.d.ts +27 -0
  21. package/lib/types/projection/deliverables-fold.d.ts +85 -0
  22. package/lib/types/projection/keys.d.ts +25 -0
  23. package/lib/types/projection/register.d.ts +9 -0
  24. package/package.json +94 -0
  25. package/src/client/BriefIcon.tsx +56 -0
  26. package/src/client/SummaryTab.tsx +73 -0
  27. package/src/client/TodoBoardTab.tsx +188 -0
  28. package/src/client/brief/board.ts +104 -0
  29. package/src/client/brief/dock-shadow.tsx +74 -0
  30. package/src/client/brief/use-todos.ts +27 -0
  31. package/src/client/file-open.ts +108 -0
  32. package/src/client/index.tsx +144 -0
  33. package/src/client/locales.ts +78 -0
  34. package/src/client/summary/DeliverablesSection.tsx +192 -0
  35. package/src/client/summary/deliverables.ts +93 -0
  36. package/src/client/use-projection.ts +117 -0
  37. package/src/index.ts +32 -0
  38. package/src/projection/deliverables-fold.ts +206 -0
  39. package/src/projection/keys.ts +27 -0
  40. package/src/projection/register.ts +99 -0
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "dsh-brief-sidebar",
3
+ "version": "0.3.0",
4
+ "description": "DSH web plugin (dsh-better-sidebar consumer): render the session brief — the todo list (the `todos` projection board) plus the artifact list (a live produced-files feed, codeplan artifacts marked) — as a summary tab in the right sidebar, and shadow the official composer-band todo panel so the board is the single visible carrier. Read-only.",
5
+ "type": "module",
6
+ "main": "lib/index.mjs",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/index.mjs"
12
+ },
13
+ "./client": {
14
+ "types": "./lib/types/client/index.d.ts",
15
+ "default": "./lib/client.js"
16
+ },
17
+ "./cordis.patch.yml": "./cordis.patch.yml",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "lib",
22
+ "src",
23
+ "cordis.patch.yml",
24
+ "dsh.plugin.json",
25
+ "README.md"
26
+ ],
27
+ "dsh": {
28
+ "bundle": {
29
+ "patch": "./cordis.patch.yml"
30
+ },
31
+ "client": {
32
+ "platform": "web",
33
+ "inject": [
34
+ "@deepseek-ai/dsh-client-locale"
35
+ ]
36
+ }
37
+ },
38
+ "keywords": [
39
+ "dsh",
40
+ "deepseek-harness",
41
+ "plugin",
42
+ "better-sidebar",
43
+ "brief",
44
+ "todo",
45
+ "artifact-list",
46
+ "task-board",
47
+ "summary",
48
+ "deliverables"
49
+ ],
50
+ "license": "MIT",
51
+ "publishConfig": {
52
+ "registry": "https://registry.npmjs.org",
53
+ "access": "public"
54
+ },
55
+ "engines": {
56
+ "node": ">=20",
57
+ "dsh": ">=0.1.5-rc.1 <0.2.0-0"
58
+ },
59
+ "peerDependencies": {
60
+ "@deepseek-ai/cordis": "^4.0.2",
61
+ "dsh-better-sidebar": ">=0.18.1",
62
+ "react": "^18.2.0"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "dsh-better-sidebar": {
66
+ "optional": true
67
+ },
68
+ "react": {
69
+ "optional": true
70
+ }
71
+ },
72
+ "scripts": {
73
+ "build": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && tsdown --config-loader tsx",
74
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
75
+ "test": "vitest run",
76
+ "clean": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\"",
77
+ "prepublishOnly": "npm run build"
78
+ },
79
+ "devDependencies": {
80
+ "@deepseek-ai/cordis": "^4.0.2",
81
+ "@types/node": "^22.0.0",
82
+ "@types/react": "~18.3.1",
83
+ "@types/react-dom": "~18.3.7",
84
+ "dsh-better-sidebar": "^0.19.1",
85
+ "jsdom": "^26.0.0",
86
+ "react": "^18.2.0",
87
+ "react-dom": "^18.2.0",
88
+ "tsdown": "^0.22.2",
89
+ "tsx": "^4.23.12",
90
+ "typescript": "^5.6.0",
91
+ "vitest": "^3.0.0",
92
+ "zod": "^4.4.3"
93
+ }
94
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Tab glyph for the task board.
3
+ *
4
+ * Skin contract: plugin-drawn glyphs take their colour from the `--dsw-alias-*`
5
+ * tokens — no colour literals anywhere in this file.
6
+ */
7
+ import type { ReactNode } from 'react'
8
+
9
+ /**
10
+ * A checklist mark: three rows with ticks and text rails — the same status
11
+ * vocabulary the board paints.
12
+ * @param size - square edge in px (the host passes its own tab-icon size).
13
+ */
14
+ export function BriefIcon(size: number): ReactNode {
15
+ return (
16
+ <svg
17
+ width={size}
18
+ height={size}
19
+ viewBox="0 0 16 16"
20
+ fill="none"
21
+ aria-hidden="true"
22
+ focusable="false"
23
+ style={{ display: 'block', flex: 'none' }}
24
+ >
25
+ <rect
26
+ x={1.6}
27
+ y={2.4}
28
+ width={12.8}
29
+ height={11.2}
30
+ rx={2}
31
+ stroke="var(--dsw-alias-label-tertiary)"
32
+ strokeWidth={1.2}
33
+ />
34
+ <path
35
+ d="M4.2 6.1l1 1 1.8-2"
36
+ stroke="var(--dsw-alias-state-success-primary)"
37
+ strokeWidth={1.3}
38
+ strokeLinecap="round"
39
+ strokeLinejoin="round"
40
+ />
41
+ <path
42
+ d="M4.2 10.6l1 1 1.8-2"
43
+ stroke="var(--dsw-alias-label-tertiary)"
44
+ strokeWidth={1.3}
45
+ strokeLinecap="round"
46
+ strokeLinejoin="round"
47
+ />
48
+ <path
49
+ d="M8.4 6.4h4M8.4 10.9h4"
50
+ stroke="var(--dsw-alias-label-tertiary)"
51
+ strokeWidth={1.2}
52
+ strokeLinecap="round"
53
+ />
54
+ </svg>
55
+ )
56
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The summary tab body: the session's at-a-glance state, composed of sections.
3
+ *
4
+ * Section order: 进展 (the todos board) → 产物 (produced files, codeplan
5
+ * artifacts marked inline) → the reserved memory-recall slot (see below).
6
+ * Each section owns its own data chain and its own three-state contract; this
7
+ * component owns only the shell.
8
+ *
9
+ * Two better-sidebar contracts are honoured:
10
+ *
11
+ * 1. **Height contract.** The tab body mounts inside a full-height column flex
12
+ * host whose `.paneBody` is a definite-height BLOCK scroll container. The
13
+ * root declares `height: 100%` + `min-height: 0`, and the scrolling element
14
+ * is an inner div — not the root. Sections are non-scrolling slices with a
15
+ * bottom border; the inner div scrolls them all.
16
+ * 2. **`visible` pause.** Nothing is drawn while the tab is not the active
17
+ * one, so a hidden tab does not re-render on every projection frame.
18
+ */
19
+ import { createElement } from 'react'
20
+ import type { CSSProperties, ReactNode } from 'react'
21
+ import type { Context } from '@deepseek-ai/cordis'
22
+ import { TodoSection } from './TodoBoardTab'
23
+ import type { TodoScope } from './TodoBoardTab'
24
+ import { DeliverablesSection } from './summary/DeliverablesSection'
25
+
26
+ export interface SummaryTabProps {
27
+ /** The DSH locale lookup, passed down from `apply`. */
28
+ t: (key: string) => string
29
+ /** The client root context (the tab body's only way to reach services). */
30
+ ctx?: Context
31
+ /** The session this tab is scoped to. */
32
+ scope?: TodoScope
33
+ /** Whether the tab is the active one AND the panel is open. */
34
+ visible?: boolean
35
+ }
36
+
37
+ const ROOT_STYLE: CSSProperties = {
38
+ display: 'flex',
39
+ flexDirection: 'column',
40
+ height: '100%',
41
+ minHeight: 0,
42
+ background: 'var(--dsw-alias-bg-layer-1)',
43
+ color: 'var(--dsw-alias-label-primary)',
44
+ font: 'inherit',
45
+ fontSize: 13,
46
+ }
47
+
48
+ const SCROLL_STYLE: CSSProperties = { flex: 1, minHeight: 0, overflow: 'auto' }
49
+
50
+ /**
51
+ * The summary tab.
52
+ * @param props - translation, client context, session scope, visibility.
53
+ */
54
+ export function SummaryTab(props: SummaryTabProps): ReactNode {
55
+ const { t, ctx, scope, visible = true } = props
56
+
57
+ // A hidden tab renders NOTHING — not even the shell — so it never
58
+ // re-renders on a projection frame while it is off screen.
59
+ if (!visible) return null
60
+
61
+ return createElement(
62
+ 'div',
63
+ { style: ROOT_STYLE, 'data-dsh-brief-sidebar': 'board' },
64
+ createElement('div', { style: SCROLL_STYLE },
65
+ createElement(TodoSection, { key: 'progress', t, ctx, scope }),
66
+ createElement(DeliverablesSection, { key: 'deliverables', t, ctx, scope }),
67
+ // 记忆召回插槽(远期,固定最底):概要从两区扩为三区时,记忆召回分区
68
+ // 排在这里。系统中没有默认记忆,数据链等记忆类插件注册自己的投影 key
69
+ // 后经同一个 `faceOf` 通道接入(`useProjectionValue`),本期不渲染。
70
+ null,
71
+ ),
72
+ )
73
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * The progress section: the `todos` projection drawn as one part of the
3
+ * summary tab.
4
+ *
5
+ * The official composer-band panel is shadowed by this plugin (see
6
+ * `brief/dock-shadow.tsx`), so the projection is read here and rendered as an
7
+ * ordinary React view — no Canvas, no cross-package dependency. The full-height
8
+ * shell and the scroll container live on the summary tab (`../SummaryTab`);
9
+ * this component renders only its own slice and is the one section whose
10
+ * absent state must stay VISIBLE: the official dock is hidden while this
11
+ * plugin is mounted, so "unavailable" is a statement the reader needs.
12
+ *
13
+ * Colours come from `--dsw-alias-*` tokens only (skin contract), and the status
14
+ * palette uses tokens that actually exist in the shipping theme:
15
+ * `state-success-primary` / `state-business-primary` / `label-tertiary`.
16
+ */
17
+ import { createElement } from 'react'
18
+ import type { CSSProperties, ReactNode } from 'react'
19
+ import type { Context } from '@deepseek-ai/cordis'
20
+ import { countTodos, progressLine, statusKey } from './brief/board'
21
+ import type { TodoItem, TodoStatus } from './brief/board'
22
+ import { useTodos } from './brief/use-todos'
23
+
24
+ /** The session scope better-sidebar hands a tab body. */
25
+ export interface TodoScope {
26
+ readonly sessionId?: string
27
+ }
28
+
29
+ export interface TodoSectionProps {
30
+ /** The DSH locale lookup, passed down from `apply`. */
31
+ t: (key: string) => string
32
+ /** The client root context (the tab body's only way to reach services). */
33
+ ctx?: Context
34
+ /** The session this tab is scoped to. */
35
+ scope?: TodoScope
36
+ }
37
+
38
+ /** Status → token colour for the pill's text and border. */
39
+ const STATUS_COLOR: Record<TodoStatus, string> = {
40
+ completed: 'var(--dsw-alias-state-success-primary)',
41
+ in_progress: 'var(--dsw-alias-state-business-primary)',
42
+ pending: 'var(--dsw-alias-label-tertiary)',
43
+ }
44
+
45
+ const SECTION_STYLE: CSSProperties = {
46
+ flexShrink: 0,
47
+ borderBottom: '1px solid var(--dsw-alias-border-secondary)',
48
+ }
49
+
50
+ const HEADER_STYLE: CSSProperties = {
51
+ padding: '10px 12px 0',
52
+ color: 'var(--dsw-alias-label-secondary)',
53
+ fontSize: 12,
54
+ fontWeight: 600,
55
+ }
56
+
57
+ const PROGRESS_STYLE: CSSProperties = {
58
+ padding: '6px 12px 10px',
59
+ color: 'var(--dsw-alias-label-secondary)',
60
+ fontSize: 12,
61
+ }
62
+
63
+ const LIST_STYLE: CSSProperties = {
64
+ listStyle: 'none',
65
+ margin: 0,
66
+ padding: '6px 0',
67
+ display: 'flex',
68
+ flexDirection: 'column',
69
+ }
70
+
71
+ const ROW_STYLE: CSSProperties = {
72
+ display: 'flex',
73
+ alignItems: 'flex-start',
74
+ gap: 8,
75
+ padding: '6px 12px',
76
+ }
77
+
78
+ /** Pill style, coloured per status. */
79
+ function pillStyle(status: TodoStatus): CSSProperties {
80
+ return {
81
+ flexShrink: 0,
82
+ color: STATUS_COLOR[status],
83
+ border: `1px solid ${STATUS_COLOR[status]}`,
84
+ borderRadius: 999,
85
+ padding: '0 6px',
86
+ fontSize: 11,
87
+ lineHeight: '16px',
88
+ whiteSpace: 'nowrap',
89
+ }
90
+ }
91
+
92
+ /** Task text: finished entries read as done, the working one as active. */
93
+ function contentStyle(status: TodoStatus): CSSProperties {
94
+ return {
95
+ minWidth: 0,
96
+ overflowWrap: 'anywhere',
97
+ color:
98
+ status === 'completed'
99
+ ? 'var(--dsw-alias-label-tertiary)'
100
+ : status === 'in_progress'
101
+ ? 'var(--dsw-alias-label-primary)'
102
+ : 'var(--dsw-alias-label-secondary)',
103
+ textDecoration: status === 'completed' ? 'line-through' : 'none',
104
+ }
105
+ }
106
+
107
+ /** Centred message block used by every non-list state. */
108
+ export function Notice(props: { title: string; detail?: string }): ReactNode {
109
+ return (
110
+ <div
111
+ style={{
112
+ margin: '0 auto',
113
+ padding: 24,
114
+ maxWidth: 460,
115
+ textAlign: 'center',
116
+ color: 'var(--dsw-alias-label-tertiary)',
117
+ lineHeight: 1.7,
118
+ fontSize: 12,
119
+ }}
120
+ >
121
+ <div style={{ marginBottom: 6, color: 'var(--dsw-alias-label-secondary)' }}>{props.title}</div>
122
+ {props.detail === undefined ? null : <div>{props.detail}</div>}
123
+ </div>
124
+ )
125
+ }
126
+
127
+ /** One task row. */
128
+ function TodoRow(props: { item: TodoItem; t: (key: string) => string }): ReactNode {
129
+ const { item, t } = props
130
+ return createElement(
131
+ 'li',
132
+ { style: ROW_STYLE },
133
+ createElement('span', { style: pillStyle(item.status) }, t(statusKey(item.status))),
134
+ createElement('span', { style: contentStyle(item.status) }, item.content),
135
+ )
136
+ }
137
+
138
+ /**
139
+ * The progress section. The subscription is unconditional (a plain listener,
140
+ * not IO); the three-state contract of the projection is preserved verbatim.
141
+ * @param props - translation, client context, session scope.
142
+ */
143
+ export function TodoSection(props: TodoSectionProps): ReactNode {
144
+ const { t, ctx, scope } = props
145
+
146
+ const todos = useTodos(ctx, scope?.sessionId)
147
+
148
+ if (todos === undefined) {
149
+ // `undefined` is the store's "capability absent": no session, or the host's
150
+ // todo unit is unmounted. Say so rather than showing an empty board.
151
+ return createElement(
152
+ 'div',
153
+ { style: SECTION_STYLE, 'data-dsh-brief-sidebar': 'progress-section' },
154
+ createElement('div', { style: HEADER_STYLE }, t('section.progress')),
155
+ createElement(Notice, { title: t('board.unavailable'), detail: t('board.unavailableHint') }),
156
+ )
157
+ }
158
+
159
+ if (todos.length === 0) {
160
+ return createElement(
161
+ 'div',
162
+ { style: SECTION_STYLE, 'data-dsh-brief-sidebar': 'progress-section' },
163
+ createElement('div', { style: HEADER_STYLE }, t('section.progress')),
164
+ createElement(Notice, { title: t('board.empty'), detail: t('board.emptyHint') }),
165
+ )
166
+ }
167
+
168
+ const counts = countTodos(todos)
169
+
170
+ return createElement(
171
+ 'div',
172
+ { style: SECTION_STYLE, 'data-dsh-brief-sidebar': 'progress-section' },
173
+ createElement('div', { style: HEADER_STYLE }, t('section.progress')),
174
+ createElement('div', { style: PROGRESS_STYLE }, progressLine(counts, t)),
175
+ createElement(
176
+ 'ul',
177
+ { style: LIST_STYLE },
178
+ todos.map((item, index) => createElement(TodoRow, { key: `${index}:${item.content}`, item, t })),
179
+ ),
180
+ )
181
+ }
182
+
183
+ /**
184
+ * Backwards-compatible alias for the pre-summary export name. The component
185
+ * no longer owns the full-height shell — that moved to `SummaryTab` — so the
186
+ * alias exists only to keep import sites honest during the transition.
187
+ */
188
+ export const TodoBoardTab = TodoSection
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The `todos` projection — reading and classifying it.
3
+ *
4
+ * The Host computes this projection (`dsh-tool-todo` registers the key) and
5
+ * pushes finished whole values; the client never folds it. This module is the
6
+ * read side of that contract and is deliberately PURE — no React, no context,
7
+ * no IO — so the mapping can be tested directly.
8
+ */
9
+ /** The projection's three-state lifecycle (`TodoItem` in `dsh-tool-todo`). */
10
+ export type TodoStatus = 'pending' | 'in_progress' | 'completed'
11
+
12
+ /** One entry of the `todos` projection. */
13
+ export interface TodoItem {
14
+ readonly content: string
15
+ readonly status: TodoStatus
16
+ }
17
+
18
+ const STATUSES: readonly TodoStatus[] = ['pending', 'in_progress', 'completed']
19
+
20
+ /** Locale key per status. Kept here so this module owns the vocabulary. */
21
+ const STATUS_KEY: Record<TodoStatus, string> = {
22
+ pending: 'status.pending',
23
+ in_progress: 'status.inProgress',
24
+ completed: 'status.completed',
25
+ }
26
+
27
+ /** Locale key for one status label. */
28
+ export function statusKey(status: TodoStatus): string {
29
+ return STATUS_KEY[status]
30
+ }
31
+
32
+ /**
33
+ * Narrow a raw projection value into a todo list.
34
+ *
35
+ * Three outcomes are kept apart on purpose, mirroring the store's own contract:
36
+ * - `undefined` — the capability is ABSENT (no session, the host unit is
37
+ * unmounted, or no baseline/frame has carried the key yet);
38
+ * - `[]` — the projection is present and the list is empty (`null` on the wire);
39
+ * - a list — real entries.
40
+ *
41
+ * A malformed entry is dropped rather than thrown on: the projection is a
42
+ * trusted Host value, but a shape drift must not blank the whole board.
43
+ *
44
+ * @param value - the raw `unknown` snapshot of the `todos` face.
45
+ * @returns the validated list, or `undefined` when the capability is absent.
46
+ */
47
+ export function readTodos(value: unknown): readonly TodoItem[] | undefined {
48
+ if (value === null) return []
49
+ if (!Array.isArray(value)) return undefined
50
+
51
+ const items: TodoItem[] = []
52
+ for (const raw of value) {
53
+ if (raw === null || typeof raw !== 'object') continue
54
+ const { content, status } = raw as { content?: unknown; status?: unknown }
55
+ if (typeof content !== 'string') continue
56
+ if (typeof status !== 'string' || !isStatus(status)) continue
57
+ items.push({ content, status })
58
+ }
59
+ return items
60
+ }
61
+
62
+ /** Whether a string is one of the three lifecycle values. */
63
+ function isStatus(value: string): value is TodoStatus {
64
+ return (STATUSES as readonly string[]).includes(value)
65
+ }
66
+
67
+ /** Per-status counts over one list. */
68
+ export interface TodoCounts {
69
+ readonly total: number
70
+ readonly completed: number
71
+ readonly inProgress: number
72
+ readonly pending: number
73
+ }
74
+
75
+ /** Count a todo list by status. */
76
+ export function countTodos(todos: readonly TodoItem[]): TodoCounts {
77
+ let completed = 0
78
+ let inProgress = 0
79
+ let pending = 0
80
+ for (const item of todos) {
81
+ if (item.status === 'completed') completed += 1
82
+ else if (item.status === 'in_progress') inProgress += 1
83
+ else pending += 1
84
+ }
85
+ return { total: todos.length, completed, inProgress, pending }
86
+ }
87
+
88
+ /** How many entries are not finished — the tab badge. */
89
+ export function unfinishedCount(todos: readonly TodoItem[]): number {
90
+ const counts = countTodos(todos)
91
+ return counts.inProgress + counts.pending
92
+ }
93
+
94
+ /**
95
+ * The one-line progress summary, composed from parts so no interpolation
96
+ * machinery is needed (the locale binder is a plain key → string lookup).
97
+ * Mirrors the official dock's own label.
98
+ */
99
+ export function progressLine(counts: TodoCounts, t: (key: string) => string): string {
100
+ const parts = [`${t('status.completed')} ${counts.completed}/${counts.total}`]
101
+ if (counts.inProgress > 0) parts.push(`${t('status.inProgress')} ${counts.inProgress}`)
102
+ if (counts.pending > 0) parts.push(`${t('status.pending')} ${counts.pending}`)
103
+ return parts.join(' · ')
104
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Hiding the official todo dock.
3
+ *
4
+ * `conversation.input.dock` is a LIST slot, and its cells are keyed by each
5
+ * entry's `id`. The slot core keeps one live entry per cell — entries are
6
+ * sorted by `priority` ascending (then by `order`) and the first live entry of
7
+ * a cell is the one the outlet renders, so a SECOND registration with the same
8
+ * `id` at a LOWER priority shadows the first. The official todo entry
9
+ * (`conversation-todo-dock` in `dsh-client-ui-conversation`) registers
10
+ * `{ id: 'todo', order: 0 }` with the default priority 0, so
11
+ * `{ id: 'todo', priority: -1 }` wins the cell and the official panel never
12
+ * renders while this plugin is mounted.
13
+ *
14
+ * This is the same mechanism `dsh-input-traffic` uses on the sibling `queue`
15
+ * cell (`{ id: 'queue', priority: -1 }`), so it is a proven seam on this host
16
+ * generation, not a guess.
17
+ *
18
+ * Binding to a built-in id is a real coupling: if upstream renames the cell,
19
+ * the shadow silently stops winning and the official dock simply reappears
20
+ * (fail-open — the data is still shown, just twice). `slots.inject` is used
21
+ * rather than a bare `register` because the slot is declared by a built-in
22
+ * entry whose lifetime we do not control: `inject` waits for the declaration
23
+ * and re-installs the entry if the declaration is ever recreated.
24
+ *
25
+ * Renders nothing on purpose: the todo UI now lives in this plugin's sidebar
26
+ * tab. Return a summary chip here instead to keep a one-line entry point in the
27
+ * composer band.
28
+ */
29
+ import type { ReactNode } from 'react'
30
+ import type { Context } from '@deepseek-ai/cordis'
31
+
32
+ /** The built-in cell id of the official todo dock. */
33
+ export const TODO_CELL_ID = 'todo'
34
+
35
+ /** Lower than the built-in's default 0 — the winner of the `todo` cell. */
36
+ export const TODO_SHADOW_PRIORITY = -1
37
+
38
+ /** Structural view of the client slots service (avoids a second type graph). */
39
+ export interface SlotsLike {
40
+ inject(key: string, callback: () => (() => void) | undefined): () => void
41
+ register(options: Record<string, unknown>, component: (props: unknown) => ReactNode): () => void
42
+ }
43
+
44
+ /** The replacement entry: deliberately empty. */
45
+ function TodoDockShadow(): ReactNode {
46
+ return null
47
+ }
48
+
49
+ /**
50
+ * Register the shadowing entry.
51
+ *
52
+ * The caller wraps this in `ctx.effect(...)` so the returned disposer rides the
53
+ * plugin fiber: unloading the plugin restores the official dock.
54
+ *
55
+ * @param ctx - the client root context.
56
+ * @returns the disposer, or undefined when the slots service is unreachable.
57
+ */
58
+ export function registerTodoDockShadow(ctx: Context): (() => void) | undefined {
59
+ const slots = ctx.get('slots') as SlotsLike | undefined
60
+ if (slots === undefined || typeof slots.inject !== 'function') return undefined
61
+
62
+ return slots.inject('conversation.input.dock', () =>
63
+ slots.register(
64
+ {
65
+ name: 'conversation.input.dock',
66
+ id: TODO_CELL_ID,
67
+ order: 0,
68
+ priority: TODO_SHADOW_PRIORITY,
69
+ registrant: 'dsh-brief-sidebar',
70
+ },
71
+ TodoDockShadow,
72
+ ),
73
+ )
74
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Reading the live `todos` projection — the thin, todo-specific wrapper around
3
+ * the generic face resolver (`../use-projection`). The projection key is
4
+ * registered by `dsh-tool-todo`; the host pushes finished whole values and the
5
+ * client never folds it. Narrowing lives in `./board` (`readTodos`).
6
+ */
7
+ import { useMemo } from 'react'
8
+ import type { Context } from '@deepseek-ai/cordis'
9
+ import { readTodos } from './board'
10
+ import type { TodoItem } from './board'
11
+ import { useProjectionValue } from '../use-projection'
12
+
13
+ /** The projection key registered by `dsh-tool-todo`. */
14
+ export const TODOS_KEY = 'todos'
15
+
16
+ /**
17
+ * Subscribe to one session's todo list.
18
+ *
19
+ * @param ctx - the client root context.
20
+ * @param sessionId - the session whose projection is shown.
21
+ * @returns the list, `[]` for a present-but-empty projection, or `undefined`
22
+ * while the capability is absent.
23
+ */
24
+ export function useTodos(ctx: Context | undefined, sessionId: string | undefined): readonly TodoItem[] | undefined {
25
+ const raw = useProjectionValue(ctx, sessionId, TODOS_KEY)
26
+ return useMemo(() => readTodos(raw), [raw])
27
+ }