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.
- 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 +18 -18
- 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,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
|
+
}
|
package/src/client/file-open.ts
CHANGED
|
@@ -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
|
+
}
|