dsh-brief-sidebar 0.3.0 → 0.3.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.
@@ -1,74 +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
- }
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
+ }
@@ -1,27 +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
- }
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
+ }
@@ -1,108 +1,108 @@
1
- /**
2
- * Opening a produced file's preview in the right Sidebar — the same channel
3
- * the conversation flow uses for its produced-file chips and inline mentions
4
- * (ui-chat `openFile` → `ctx.sidebarRight.openResource` with a
5
- * `dsh-resource://file/…` address).
6
- *
7
- * The address grammar is ported verbatim from the harness's browser-safe
8
- * `@deepseek-ai/dsh-util-workspace-path` (`file-address.ts` / `index.ts`),
9
- * because the plugin does not compile against that package: a relative path,
10
- * or an absolute path inside the session's workspace, is addressed under the
11
- * session's scope (`dsh-resource://file/session/<id>/<path>`); an absolute
12
- * path elsewhere keeps its absolute spelling in the same session's address.
13
- * Which tab type claims the address is the Sidebar's decision, not ours.
14
- *
15
- * The `sidebarRight` service is consumed structurally and treated as OPTIONAL:
16
- * where it is absent the section renders plain rows instead of links, the same
17
- * graceful degradation as the deliverables projection itself.
18
- */
19
- import type { Context } from '@deepseek-ai/cordis'
20
-
21
- /** Structural view of the right-Sidebar service this plugin needs. */
22
- export interface SidebarRightLike {
23
- openResource(url: string, options?: { params?: { line?: number } }): void
24
- }
25
-
26
- /** Structural view of the sessions service slice the opener reads. */
27
- interface SessionsListLike {
28
- list?: { getSnapshot(): { byId?: Record<string, { cwd?: string }> } }
29
- }
30
-
31
- /** The address scheme every file resource opens with. */
32
- const FILE_ADDRESS_PREFIX = 'dsh-resource://file/'
33
-
34
- /** Whether a path uses a Windows drive or UNC prefix. */
35
- function isWindowsStylePath(value: string): boolean {
36
- return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith('\\\\')
37
- }
38
-
39
- /**
40
- * Whether a path is absolute in either spelling the Host accepts: POSIX (`/a/b`)
41
- * or Windows drive or UNC.
42
- */
43
- export function isAbsoluteWorkspacePath(path: string): boolean {
44
- return path.startsWith('/') || isWindowsStylePath(path)
45
- }
46
-
47
- /** Component-encode one id or path segment, keeping `:` literal for drive letters. */
48
- function encodeSegment(segment: string): string {
49
- return encodeURIComponent(segment).replace(/%3A/gi, ':')
50
- }
51
-
52
- /** Encode a `/`-separated path segment by segment. */
53
- function encodePath(path: string): string {
54
- return path.split('/').map(encodeSegment).join('/')
55
- }
56
-
57
- /**
58
- * Build the address of a file read through one Session.
59
- * @param sessionId - the Session whose Host workspace resolves the path.
60
- * @param path - absolute or workspace-relative path; backslashes are normalized
61
- * to `/`, and leading `./` prefixes are dropped.
62
- */
63
- function sessionFileAddress(sessionId: string, path: string): string {
64
- const normalized = path.replace(/\\/g, '/').replace(/^(?:\.\/)+/, '')
65
- return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(normalized)}`
66
- }
67
-
68
- /**
69
- * The address for a path as a caller holds it.
70
- * @param sessionId - the Session the path is read in.
71
- * @param cwd - that Session's workspace root, when known.
72
- * @param path - absolute or workspace-relative path, in either separator spelling.
73
- */
74
- export function fileAddressFor(sessionId: string, cwd: string | undefined, path: string): string {
75
- const normalized = path.replace(/\\/g, '/')
76
- if (!isAbsoluteWorkspacePath(normalized)) return sessionFileAddress(sessionId, normalized)
77
- const root = cwd === undefined ? '' : cwd.replace(/\\/g, '/').replace(/\/+$/, '')
78
- if (root !== '' && normalized === root) return sessionFileAddress(sessionId, '')
79
- if (root !== '' && normalized.startsWith(`${root}/`)) {
80
- return sessionFileAddress(sessionId, normalized.slice(root.length + 1))
81
- }
82
- return sessionFileAddress(sessionId, normalized)
83
- }
84
-
85
- /**
86
- * Build a sidebar file opener for one session, or `undefined` when the
87
- * environment cannot open previews (no `sidebarRight` service, no session).
88
- * The cwd is re-read per call inside the opener, so a workspace switch between
89
- * renders is picked up on the next click.
90
- *
91
- * @param ctx - the client root context.
92
- * @param sessionId - the session whose workspace resolves paths.
93
- * @returns an opener that routes a produced path to the Sidebar preview.
94
- */
95
- export function sidebarFileOpener(
96
- ctx: Context | undefined,
97
- sessionId: string | undefined,
98
- ): ((path: string) => void) | undefined {
99
- if (ctx === undefined || sessionId === undefined) return undefined
100
- const bar = ctx.get('sidebarRight') as SidebarRightLike | undefined
101
- if (bar === undefined || typeof bar.openResource !== 'function') return undefined
102
-
103
- return (path: string) => {
104
- const sessions = ctx.get('sessions') as SessionsListLike | undefined
105
- const cwd = sessions?.list?.getSnapshot?.().byId?.[sessionId]?.cwd
106
- bar.openResource(fileAddressFor(sessionId, cwd, path))
107
- }
108
- }
1
+ /**
2
+ * Opening a produced file's preview in the right Sidebar — the same channel
3
+ * the conversation flow uses for its produced-file chips and inline mentions
4
+ * (ui-chat `openFile` → `ctx.sidebarRight.openResource` with a
5
+ * `dsh-resource://file/…` address).
6
+ *
7
+ * The address grammar is ported verbatim from the harness's browser-safe
8
+ * `@deepseek-ai/dsh-util-workspace-path` (`file-address.ts` / `index.ts`),
9
+ * because the plugin does not compile against that package: a relative path,
10
+ * or an absolute path inside the session's workspace, is addressed under the
11
+ * session's scope (`dsh-resource://file/session/<id>/<path>`); an absolute
12
+ * path elsewhere keeps its absolute spelling in the same session's address.
13
+ * Which tab type claims the address is the Sidebar's decision, not ours.
14
+ *
15
+ * The `sidebarRight` service is consumed structurally and treated as OPTIONAL:
16
+ * where it is absent the section renders plain rows instead of links, the same
17
+ * graceful degradation as the deliverables projection itself.
18
+ */
19
+ import type { Context } from '@deepseek-ai/cordis'
20
+
21
+ /** Structural view of the right-Sidebar service this plugin needs. */
22
+ export interface SidebarRightLike {
23
+ openResource(url: string, options?: { params?: { line?: number } }): void
24
+ }
25
+
26
+ /** Structural view of the sessions service slice the opener reads. */
27
+ interface SessionsListLike {
28
+ list?: { getSnapshot(): { byId?: Record<string, { cwd?: string }> } }
29
+ }
30
+
31
+ /** The address scheme every file resource opens with. */
32
+ const FILE_ADDRESS_PREFIX = 'dsh-resource://file/'
33
+
34
+ /** Whether a path uses a Windows drive or UNC prefix. */
35
+ function isWindowsStylePath(value: string): boolean {
36
+ return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith('\\\\')
37
+ }
38
+
39
+ /**
40
+ * Whether a path is absolute in either spelling the Host accepts: POSIX (`/a/b`)
41
+ * or Windows drive or UNC.
42
+ */
43
+ export function isAbsoluteWorkspacePath(path: string): boolean {
44
+ return path.startsWith('/') || isWindowsStylePath(path)
45
+ }
46
+
47
+ /** Component-encode one id or path segment, keeping `:` literal for drive letters. */
48
+ function encodeSegment(segment: string): string {
49
+ return encodeURIComponent(segment).replace(/%3A/gi, ':')
50
+ }
51
+
52
+ /** Encode a `/`-separated path segment by segment. */
53
+ function encodePath(path: string): string {
54
+ return path.split('/').map(encodeSegment).join('/')
55
+ }
56
+
57
+ /**
58
+ * Build the address of a file read through one Session.
59
+ * @param sessionId - the Session whose Host workspace resolves the path.
60
+ * @param path - absolute or workspace-relative path; backslashes are normalized
61
+ * to `/`, and leading `./` prefixes are dropped.
62
+ */
63
+ function sessionFileAddress(sessionId: string, path: string): string {
64
+ const normalized = path.replace(/\\/g, '/').replace(/^(?:\.\/)+/, '')
65
+ return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(normalized)}`
66
+ }
67
+
68
+ /**
69
+ * The address for a path as a caller holds it.
70
+ * @param sessionId - the Session the path is read in.
71
+ * @param cwd - that Session's workspace root, when known.
72
+ * @param path - absolute or workspace-relative path, in either separator spelling.
73
+ */
74
+ export function fileAddressFor(sessionId: string, cwd: string | undefined, path: string): string {
75
+ const normalized = path.replace(/\\/g, '/')
76
+ if (!isAbsoluteWorkspacePath(normalized)) return sessionFileAddress(sessionId, normalized)
77
+ const root = cwd === undefined ? '' : cwd.replace(/\\/g, '/').replace(/\/+$/, '')
78
+ if (root !== '' && normalized === root) return sessionFileAddress(sessionId, '')
79
+ if (root !== '' && normalized.startsWith(`${root}/`)) {
80
+ return sessionFileAddress(sessionId, normalized.slice(root.length + 1))
81
+ }
82
+ return sessionFileAddress(sessionId, normalized)
83
+ }
84
+
85
+ /**
86
+ * Build a sidebar file opener for one session, or `undefined` when the
87
+ * environment cannot open previews (no `sidebarRight` service, no session).
88
+ * The cwd is re-read per call inside the opener, so a workspace switch between
89
+ * renders is picked up on the next click.
90
+ *
91
+ * @param ctx - the client root context.
92
+ * @param sessionId - the session whose workspace resolves paths.
93
+ * @returns an opener that routes a produced path to the Sidebar preview.
94
+ */
95
+ export function sidebarFileOpener(
96
+ ctx: Context | undefined,
97
+ sessionId: string | undefined,
98
+ ): ((path: string) => void) | undefined {
99
+ if (ctx === undefined || sessionId === undefined) return undefined
100
+ const bar = ctx.get('sidebarRight') as SidebarRightLike | undefined
101
+ if (bar === undefined || typeof bar.openResource !== 'function') return undefined
102
+
103
+ return (path: string) => {
104
+ const sessions = ctx.get('sessions') as SessionsListLike | undefined
105
+ const cwd = sessions?.list?.getSnapshot?.().byId?.[sessionId]?.cwd
106
+ bar.openResource(fileAddressFor(sessionId, cwd, path))
107
+ }
108
+ }