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.
- package/README.md +188 -178
- package/assets/.gitkeep +0 -0
- package/assets/brief.png +0 -0
- package/cordis.patch.yml +18 -18
- package/dsh.plugin.json +16 -16
- package/lib/client.js.map +1 -1
- package/lib/index.mjs.map +1 -1
- package/package.json +106 -94
- package/screenshots.json +5 -0
- package/src/client/BriefIcon.tsx +56 -56
- package/src/client/SummaryTab.tsx +73 -73
- package/src/client/TodoBoardTab.tsx +188 -188
- package/src/client/brief/board.ts +104 -104
- package/src/client/brief/dock-shadow.tsx +74 -74
- package/src/client/brief/use-todos.ts +27 -27
- package/src/client/file-open.ts +108 -108
- package/src/client/index.tsx +144 -144
- package/src/client/locales.ts +78 -78
- package/src/client/summary/DeliverablesSection.tsx +192 -192
- package/src/client/summary/deliverables.ts +93 -93
- package/src/client/use-projection.ts +117 -117
- package/src/index.ts +32 -32
- package/src/projection/deliverables-fold.ts +206 -206
- package/src/projection/keys.ts +27 -27
- package/src/projection/register.ts +99 -99
|
@@ -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
|
+
}
|