dsh-brief-sidebar 0.3.0 → 0.4.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.
@@ -1,117 +1,117 @@
1
- /**
2
- * Reading a live session projection from the client Session Controller.
3
- *
4
- * better-sidebar tab bodies are NOT rendered inside a DSH slot, so the
5
- * framework's `useProjection` standard prop is not available to them. The
6
- * equivalent face is reachable directly: the client Session Controller is a
7
- * cordis service on the root context, and each session binding exposes its
8
- * projections store (`SessionFace.projections.faceOf(key)`), which is the very
9
- * observable `useProjection` resolves. Since it is a bare
10
- * `{ getSnapshot, subscribe }` source, `useSyncExternalStore` binds it as-is —
11
- * no local mirror, no folding, and the value reference only changes when the
12
- * Host pushes a frame.
13
- *
14
- * Everything here is resolved defensively: a host without the Session
15
- * Controller, or a session that is not open any more, degrades to "capability
16
- * absent" (undefined) rather than throwing inside a render.
17
- *
18
- * This module is key-agnostic; the per-projection knowledge (which key, how to
19
- * narrow the raw value) lives with the feature modules that call it.
20
- */
21
- import { useCallback, useMemo, useSyncExternalStore } from 'react'
22
- import type { Context } from '@deepseek-ai/cordis'
23
-
24
- /** Structural view of a projection face (`ObservableSnapshot<unknown>`). */
25
- export interface ProjectionFace {
26
- getSnapshot(): unknown
27
- subscribe(listener: () => void): () => void
28
- }
29
-
30
- /** Structural view of one session binding's projection store. */
31
- export interface ProjectionsLike {
32
- faceOf(key: string): ProjectionFace
33
- }
34
-
35
- /** Structural view of the client Session Controller face this plugin uses. */
36
- export interface SessionsLike {
37
- binding(id: string): { readonly session: { readonly projections: ProjectionsLike } } | undefined
38
- }
39
-
40
- /** A no-op unsubscribe, kept as one identity so effects never thrash. */
41
- const NO_SUBSCRIBE = (): void => {}
42
-
43
- /** A source that never has a value — the absent-capability face. */
44
- const ABSENT: ProjectionFace = { getSnapshot: () => undefined, subscribe: () => NO_SUBSCRIBE }
45
-
46
- /**
47
- * Whether a value really is a projection face.
48
- *
49
- * The store contract promises an identity-stable face for every key, so this
50
- * never fires against a healthy host. It exists because the fallback is worse
51
- * than the check: `useSyncExternalStore` would throw on a face-less key, and a
52
- * thrown render is a blank sidebar rather than a diagnosable "unavailable".
53
- */
54
- function isFace(value: unknown): value is ProjectionFace {
55
- if (value === null || typeof value !== 'object') return false
56
- const face = value as Partial<ProjectionFace>
57
- return typeof face.getSnapshot === 'function' && typeof face.subscribe === 'function'
58
- }
59
-
60
- /**
61
- * Resolve one projection face for one session.
62
- *
63
- * `ctx.get('sessions')` is the safe accessor (a direct `ctx.sessions` read can
64
- * throw on a context that does not carry the service yet).
65
- *
66
- * @param ctx - the client root context handed to the tab body.
67
- * @param sessionId - the tab's session scope; undefined means "no session".
68
- * @param key - the projection key to resolve.
69
- * @returns the projection face, or {@link ABSENT} when unreachable.
70
- */
71
- export function resolveFace(ctx: Context | undefined, sessionId: string | undefined, key: string): ProjectionFace {
72
- if (ctx === undefined || sessionId === undefined) return ABSENT
73
- const sessions = ctx.get('sessions') as SessionsLike | undefined
74
- if (sessions === undefined || typeof sessions.binding !== 'function') return ABSENT
75
-
76
- const binding = sessions.binding(sessionId)
77
- const projections = binding?.session?.projections
78
- if (projections === undefined || typeof projections.faceOf !== 'function') return ABSENT
79
-
80
- const face: unknown = projections.faceOf(key)
81
- return isFace(face) ? face : ABSENT
82
- }
83
-
84
- /**
85
- * Read one frame of a projection without subscribing.
86
- *
87
- * The one-shot sibling of {@link useProjectionValue}, for consumers that are
88
- * not React components — the tab badge is a plain function called on every
89
- * tab-bar render, so it must neither hold a subscription nor allocate one.
90
- *
91
- * @param ctx - the client root context.
92
- * @param sessionId - the session whose projection is read.
93
- * @param key - the projection key to read.
94
- * @returns the raw snapshot: `undefined` while the capability is absent; the
95
- * semantics of any other value belong to the projection's own contract.
96
- */
97
- export function projectionSnapshot(ctx: Context | undefined, sessionId: string | undefined, key: string): unknown {
98
- return resolveFace(ctx, sessionId, key).getSnapshot()
99
- }
100
-
101
- /**
102
- * Subscribe to one session projection by key.
103
- *
104
- * @param ctx - the client root context.
105
- * @param sessionId - the session whose projection is shown.
106
- * @param key - the projection key to subscribe to.
107
- * @returns the raw snapshot value, identity-stable across frames the host did
108
- * not change. Narrowing is the caller's job.
109
- */
110
- export function useProjectionValue(ctx: Context | undefined, sessionId: string | undefined, key: string): unknown {
111
- const face = useMemo(() => resolveFace(ctx, sessionId, key), [ctx, sessionId, key])
112
-
113
- const subscribe = useCallback((listener: () => void) => face.subscribe(listener), [face])
114
- const getSnapshot = useCallback(() => face.getSnapshot(), [face])
115
-
116
- return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
117
- }
1
+ /**
2
+ * Reading a live session projection from the client Session Controller.
3
+ *
4
+ * better-sidebar tab bodies are NOT rendered inside a DSH slot, so the
5
+ * framework's `useProjection` standard prop is not available to them. The
6
+ * equivalent face is reachable directly: the client Session Controller is a
7
+ * cordis service on the root context, and each session binding exposes its
8
+ * projections store (`SessionFace.projections.faceOf(key)`), which is the very
9
+ * observable `useProjection` resolves. Since it is a bare
10
+ * `{ getSnapshot, subscribe }` source, `useSyncExternalStore` binds it as-is —
11
+ * no local mirror, no folding, and the value reference only changes when the
12
+ * Host pushes a frame.
13
+ *
14
+ * Everything here is resolved defensively: a host without the Session
15
+ * Controller, or a session that is not open any more, degrades to "capability
16
+ * absent" (undefined) rather than throwing inside a render.
17
+ *
18
+ * This module is key-agnostic; the per-projection knowledge (which key, how to
19
+ * narrow the raw value) lives with the feature modules that call it.
20
+ */
21
+ import { useCallback, useMemo, useSyncExternalStore } from 'react'
22
+ import type { Context } from '@deepseek-ai/cordis'
23
+
24
+ /** Structural view of a projection face (`ObservableSnapshot<unknown>`). */
25
+ export interface ProjectionFace {
26
+ getSnapshot(): unknown
27
+ subscribe(listener: () => void): () => void
28
+ }
29
+
30
+ /** Structural view of one session binding's projection store. */
31
+ export interface ProjectionsLike {
32
+ faceOf(key: string): ProjectionFace
33
+ }
34
+
35
+ /** Structural view of the client Session Controller face this plugin uses. */
36
+ export interface SessionsLike {
37
+ binding(id: string): { readonly session: { readonly projections: ProjectionsLike } } | undefined
38
+ }
39
+
40
+ /** A no-op unsubscribe, kept as one identity so effects never thrash. */
41
+ const NO_SUBSCRIBE = (): void => {}
42
+
43
+ /** A source that never has a value — the absent-capability face. */
44
+ const ABSENT: ProjectionFace = { getSnapshot: () => undefined, subscribe: () => NO_SUBSCRIBE }
45
+
46
+ /**
47
+ * Whether a value really is a projection face.
48
+ *
49
+ * The store contract promises an identity-stable face for every key, so this
50
+ * never fires against a healthy host. It exists because the fallback is worse
51
+ * than the check: `useSyncExternalStore` would throw on a face-less key, and a
52
+ * thrown render is a blank sidebar rather than a diagnosable "unavailable".
53
+ */
54
+ function isFace(value: unknown): value is ProjectionFace {
55
+ if (value === null || typeof value !== 'object') return false
56
+ const face = value as Partial<ProjectionFace>
57
+ return typeof face.getSnapshot === 'function' && typeof face.subscribe === 'function'
58
+ }
59
+
60
+ /**
61
+ * Resolve one projection face for one session.
62
+ *
63
+ * `ctx.get('sessions')` is the safe accessor (a direct `ctx.sessions` read can
64
+ * throw on a context that does not carry the service yet).
65
+ *
66
+ * @param ctx - the client root context handed to the tab body.
67
+ * @param sessionId - the tab's session scope; undefined means "no session".
68
+ * @param key - the projection key to resolve.
69
+ * @returns the projection face, or {@link ABSENT} when unreachable.
70
+ */
71
+ export function resolveFace(ctx: Context | undefined, sessionId: string | undefined, key: string): ProjectionFace {
72
+ if (ctx === undefined || sessionId === undefined) return ABSENT
73
+ const sessions = ctx.get('sessions') as SessionsLike | undefined
74
+ if (sessions === undefined || typeof sessions.binding !== 'function') return ABSENT
75
+
76
+ const binding = sessions.binding(sessionId)
77
+ const projections = binding?.session?.projections
78
+ if (projections === undefined || typeof projections.faceOf !== 'function') return ABSENT
79
+
80
+ const face: unknown = projections.faceOf(key)
81
+ return isFace(face) ? face : ABSENT
82
+ }
83
+
84
+ /**
85
+ * Read one frame of a projection without subscribing.
86
+ *
87
+ * The one-shot sibling of {@link useProjectionValue}, for consumers that are
88
+ * not React components — the tab badge is a plain function called on every
89
+ * tab-bar render, so it must neither hold a subscription nor allocate one.
90
+ *
91
+ * @param ctx - the client root context.
92
+ * @param sessionId - the session whose projection is read.
93
+ * @param key - the projection key to read.
94
+ * @returns the raw snapshot: `undefined` while the capability is absent; the
95
+ * semantics of any other value belong to the projection's own contract.
96
+ */
97
+ export function projectionSnapshot(ctx: Context | undefined, sessionId: string | undefined, key: string): unknown {
98
+ return resolveFace(ctx, sessionId, key).getSnapshot()
99
+ }
100
+
101
+ /**
102
+ * Subscribe to one session projection by key.
103
+ *
104
+ * @param ctx - the client root context.
105
+ * @param sessionId - the session whose projection is shown.
106
+ * @param key - the projection key to subscribe to.
107
+ * @returns the raw snapshot value, identity-stable across frames the host did
108
+ * not change. Narrowing is the caller's job.
109
+ */
110
+ export function useProjectionValue(ctx: Context | undefined, sessionId: string | undefined, key: string): unknown {
111
+ const face = useMemo(() => resolveFace(ctx, sessionId, key), [ctx, sessionId, key])
112
+
113
+ const subscribe = useCallback((listener: () => void) => face.subscribe(listener), [face])
114
+ const getSnapshot = useCallback(() => face.getSnapshot(), [face])
115
+
116
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
117
+ }
package/src/index.ts CHANGED
@@ -1,32 +1,32 @@
1
- /**
2
- * Node half of the brief sidebar plugin.
3
- *
4
- * The Cordis loader needs an entry point for the profile row; the browser half
5
- * (`./client`, served by `dsh-client-modules`) renders the summary tab.
6
- *
7
- * Since the summary upgrade, this half owns ONE behaviour: contributing the
8
- * `dshSummaryDeliverables` projection unit (see `./projection/register`), which
9
- * folds successful mutation calls into a live produced-file feed. The
10
- * contribution is optional — `registerDeliverablesProjection` waits for the
11
- * `sessionProjections` service via `ctx.inject`, so a composition without the
12
- * registry still loads this profile row and simply hides the deliverables
13
- * section client-side.
14
- *
15
- * In particular this half still never touches the filesystem at load time: a
16
- * profile row is instantiated on the boot path, and a synchronous read there
17
- * would delay the host's startup. The fold is event-driven and stores paths
18
- * only.
19
- */
20
- import type { Context } from '@deepseek-ai/cordis'
21
- import { registerDeliverablesProjection } from './projection/register'
22
-
23
- /** Profile row identity. */
24
- export const name = 'dsh-brief-sidebar'
25
-
26
- /**
27
- * Node-face apply.
28
- * @param ctx - the host context the loader hands this profile row.
29
- */
30
- export function apply(ctx: Context): void {
31
- registerDeliverablesProjection(ctx)
32
- }
1
+ /**
2
+ * Node half of the brief sidebar plugin.
3
+ *
4
+ * The Cordis loader needs an entry point for the profile row; the browser half
5
+ * (`./client`, served by `dsh-client-modules`) renders the summary tab.
6
+ *
7
+ * Since the summary upgrade, this half owns ONE behaviour: contributing the
8
+ * `dshSummaryDeliverables` projection unit (see `./projection/register`), which
9
+ * folds successful mutation calls into a live produced-file feed. The
10
+ * contribution is optional — `registerDeliverablesProjection` waits for the
11
+ * `sessionProjections` service via `ctx.inject`, so a composition without the
12
+ * registry still loads this profile row and simply hides the deliverables
13
+ * section client-side.
14
+ *
15
+ * In particular this half still never touches the filesystem at load time: a
16
+ * profile row is instantiated on the boot path, and a synchronous read there
17
+ * would delay the host's startup. The fold is event-driven and stores paths
18
+ * only.
19
+ */
20
+ import type { Context } from '@deepseek-ai/cordis'
21
+ import { registerDeliverablesProjection } from './projection/register'
22
+
23
+ /** Profile row identity. */
24
+ export const name = 'dsh-brief-sidebar'
25
+
26
+ /**
27
+ * Node-face apply.
28
+ * @param ctx - the host context the loader hands this profile row.
29
+ */
30
+ export function apply(ctx: Context): void {
31
+ registerDeliverablesProjection(ctx)
32
+ }