@tivility/dsh-session-share 0.1.0 → 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.
- package/README.md +51 -1
- package/lib/client/ShareLink.js +48 -0
- package/lib/client/ShareLink.js.map +1 -0
- package/lib/client/index.js +29 -10
- package/lib/client/index.js.map +1 -1
- package/lib/client/locales.js +14 -0
- package/lib/client/locales.js.map +1 -0
- package/lib/client/namespace.js +12 -0
- package/lib/client/namespace.js.map +1 -0
- package/lib/client.js +117 -9
- package/lib/client.js.map +1 -1
- package/lib/index.js +34 -13
- package/lib/index.js.map +1 -1
- package/lib/types/client/ShareLink.d.ts +26 -0
- package/lib/types/client/ShareLink.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +23 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/namespace.d.ts +19 -0
- package/lib/types/client/namespace.d.ts.map +1 -0
- package/lib/types/index.d.ts +14 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +14 -3
- package/src/client/ShareLink.tsx +71 -0
- package/src/client/index.ts +44 -10
- package/src/client/locales.ts +18 -0
- package/src/client/namespace.ts +21 -0
- package/src/index.ts +40 -13
package/README.md
CHANGED
|
@@ -36,12 +36,33 @@ Restricting what a visitor can reach is
|
|
|
36
36
|
[`@tivility/dsh-readonly-auth`](../readonly-auth)'s job, and it does it at the
|
|
37
37
|
API rather than in the DOM.
|
|
38
38
|
|
|
39
|
+
## Getting a link
|
|
40
|
+
|
|
41
|
+
A **Copy link** control in the session header, beside the session log. Its
|
|
42
|
+
hover text is the URL itself — which is also how someone discovers the format
|
|
43
|
+
exists.
|
|
44
|
+
|
|
45
|
+
That control is the difference between a feature and a convention. Without it
|
|
46
|
+
the link format reaches only the *model*, through the system prompt, and a
|
|
47
|
+
person looking at the GUI is left to assemble a URL from a session id the
|
|
48
|
+
interface never shows them.
|
|
49
|
+
|
|
50
|
+
The origin comes from `window.location`, so the link is always the address this
|
|
51
|
+
browser reached the harness at — loopback when that is how you opened it, the
|
|
52
|
+
LAN address when it is not. Nothing to configure, and nothing that can drift
|
|
53
|
+
from where the server actually answers.
|
|
54
|
+
|
|
55
|
+
A refused clipboard write (denied permission, insecure context) leaves the
|
|
56
|
+
button idle rather than claiming a copy that did not happen. The URL is in the
|
|
57
|
+
hover text either way.
|
|
58
|
+
|
|
39
59
|
## How it works
|
|
40
60
|
|
|
41
61
|
Two halves, which is what the harness's client-module system is for.
|
|
42
62
|
|
|
43
63
|
**The browser half** reads `?session=`, waits for the session list, calls
|
|
44
|
-
`sessions.open(id)`, then strips the parameter from the address bar.
|
|
64
|
+
`sessions.open(id)`, then strips the parameter from the address bar. It also
|
|
65
|
+
registers the header control into `conversation.session.header.utilities`.
|
|
45
66
|
|
|
46
67
|
Two details carry their weight:
|
|
47
68
|
|
|
@@ -82,6 +103,35 @@ is repository-bound (it locates packages by globbing inside the harness
|
|
|
82
103
|
checkout), so the four rules are written out instead. They are short, and they
|
|
83
104
|
are the whole of what makes a bundle loadable.
|
|
84
105
|
|
|
106
|
+
## Profile scope, and links behind a proxy
|
|
107
|
+
|
|
108
|
+
**This row is safe in any profile.** It activates without `webServer` and
|
|
109
|
+
simply serves nothing, so a suite installed once in `$DSH_HOME/cordis.patch.yml`
|
|
110
|
+
does not turn a Web feature into a headless startup failure.
|
|
111
|
+
|
|
112
|
+
**`publicBaseUrl` is the origin browsers actually reach.** The local bind is
|
|
113
|
+
not the public origin behind a reverse proxy, tunnel, port forwarder, or TLS
|
|
114
|
+
terminator, and no inference recovers one — so a deployment that has one says
|
|
115
|
+
so:
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
publicBaseUrl: https://dsh.example.com
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Origin only: scheme, host, optional port. Credentials, queries, fragments, and
|
|
122
|
+
path prefixes are refused at activation rather than half-supported — every
|
|
123
|
+
route is appended whole, so a prefix would vanish from the middle of each link.
|
|
124
|
+
`DSH_PUBLIC_BASE_URL` sets it process-wide; explicit configuration outranks it.
|
|
125
|
+
|
|
126
|
+
Forwarded headers are deliberately never consulted: `Host` and `X-Forwarded-*`
|
|
127
|
+
are attacker-controlled on any request that reaches the server, so trusting
|
|
128
|
+
them would let a visitor choose the origin the model puts in front of a user.
|
|
129
|
+
|
|
130
|
+
With no `webServer` **and** no `publicBaseUrl`, the prompt section renders
|
|
131
|
+
nothing rather than naming a loopback address — in a headless profile that
|
|
132
|
+
would point at a server which is not running. Configure the origin of the Web
|
|
133
|
+
profile serving the same data and headless agents can link into it.
|
|
134
|
+
|
|
85
135
|
## Configuration
|
|
86
136
|
|
|
87
137
|
```yaml
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The session header's "copy link" control.
|
|
4
|
+
*
|
|
5
|
+
* Without it the feature is unreachable by a person: the link format lives in
|
|
6
|
+
* the system prompt, which teaches the *model* to hand out links, and leaves
|
|
7
|
+
* whoever is looking at the GUI to construct one from a session id they cannot
|
|
8
|
+
* see. This is the one affordance that makes it a feature rather than a
|
|
9
|
+
* convention.
|
|
10
|
+
*
|
|
11
|
+
* The origin comes from `window.location`, so the link is always the address
|
|
12
|
+
* this browser reached the harness at — the loopback URL when that is how you
|
|
13
|
+
* opened it, the LAN address when it is not. Nothing to configure, and nothing
|
|
14
|
+
* that can drift from where the server actually answers.
|
|
15
|
+
* @module @tivility/dsh-session-share/client/ShareLink
|
|
16
|
+
*/
|
|
17
|
+
import { useCallback, useState } from 'react';
|
|
18
|
+
import { Button, IconCheckOutline16, IconLinkOutline16, writeClipboard, } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
19
|
+
import { shareQuery } from '../param.js';
|
|
20
|
+
import { SHARE_NS } from './namespace.js';
|
|
21
|
+
/** How long the copied state shows, in ms — matched to the harness's own copy controls. */
|
|
22
|
+
const COPIED_MS = 1000;
|
|
23
|
+
/**
|
|
24
|
+
* Render the copy-link control for the current session.
|
|
25
|
+
* @param props - composed slot props.
|
|
26
|
+
* @returns the control.
|
|
27
|
+
*/
|
|
28
|
+
export function ShareLink({ sessionId, t }) {
|
|
29
|
+
const [copied, setCopied] = useState(false);
|
|
30
|
+
const url = `${window.location.origin}/${shareQuery(String(sessionId))}`;
|
|
31
|
+
const onCopy = useCallback(() => {
|
|
32
|
+
if (copied)
|
|
33
|
+
return;
|
|
34
|
+
void writeClipboard(url).then((accepted) => {
|
|
35
|
+
// A refused write (denied permission, insecure context) leaves the flag
|
|
36
|
+
// alone: the control must never claim a copy the host declined.
|
|
37
|
+
if (!accepted)
|
|
38
|
+
return;
|
|
39
|
+
setCopied(true);
|
|
40
|
+
window.setTimeout(() => { setCopied(false); }, COPIED_MS);
|
|
41
|
+
});
|
|
42
|
+
}, [copied, url]);
|
|
43
|
+
return (_jsx(Button, { variant: "ghost", size: "sm", onClick: onCopy,
|
|
44
|
+
// The URL itself is the most useful hover text: it is what gets copied,
|
|
45
|
+
// and seeing it is how someone learns the format exists.
|
|
46
|
+
title: copied ? t('copied') : `${t('hint')}\n${url}`, icon: copied ? _jsx(IconCheckOutline16, { size: 14 }) : _jsx(IconLinkOutline16, { size: 14 }), children: copied ? t('copied') : t('copy') }));
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=ShareLink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShareLink.js","sourceRoot":"","sources":["../../src/client/ShareLink.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAC7C,OAAO,EACL,MAAM,EACN,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,GACf,MAAM,uCAAuC,CAAA;AAI9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,2FAA2F;AAC3F,MAAM,SAAS,GAAG,IAAI,CAAA;AAOtB;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,EAAE,SAAS,EAAE,CAAC,EAAkB;IACxD,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC3C,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,IAAI,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,CAAA;IAExE,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE;QAC9B,IAAI,MAAM;YAAE,OAAM;QAClB,KAAK,cAAc,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;YACzC,wEAAwE;YACxE,gEAAgE;YAChE,IAAI,CAAC,QAAQ;gBAAE,OAAM;YACrB,SAAS,CAAC,IAAI,CAAC,CAAA;YACf,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,CAAA,CAAC,CAAC,EAAE,SAAS,CAAC,CAAA;QAC1D,CAAC,CAAC,CAAA;IACJ,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;IAEjB,OAAO,CACL,KAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,MAAM;QACf,wEAAwE;QACxE,yDAAyD;QACzD,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,GAAG,EAAE,EACpD,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,KAAC,kBAAkB,IAAC,IAAI,EAAE,EAAE,GAAI,CAAC,CAAC,CAAC,KAAC,iBAAiB,IAAC,IAAI,EAAE,EAAE,GAAI,YAEhF,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAC1B,CACV,CAAA;AACH,CAAC"}
|
package/lib/client/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The browser half: open the session a link names.
|
|
2
|
+
* The browser half: open the session a link names, and offer a way to get one.
|
|
3
3
|
*
|
|
4
4
|
* The harness's GUI has no routing — the whole client is searched in vain for
|
|
5
5
|
* `pushState`, `location.hash`, or `searchParams`. The selected session lives
|
|
6
6
|
* in the sessions manager and is restored from persisted state on load, so a
|
|
7
|
-
* URL naming one has nowhere to be read. This adds that
|
|
8
|
-
*
|
|
7
|
+
* URL naming one has nowhere to be read. This adds that step, and the control
|
|
8
|
+
* that makes it reachable by a person rather than only by the model.
|
|
9
9
|
*
|
|
10
10
|
* It deliberately hides nothing. The sidebar, the other sessions, and the
|
|
11
11
|
* composer are all exactly as they would be — the link decides where you land,
|
|
@@ -17,14 +17,18 @@
|
|
|
17
17
|
* node half pulls in the harness's HOST augmentations of `Context` — where
|
|
18
18
|
* `sessions` is a different service with the same name. Importing
|
|
19
19
|
* `ClientContext` would therefore resolve `ctx.sessions` to the host's shape.
|
|
20
|
-
* The harness avoids this with separate client and host programs;
|
|
21
|
-
*
|
|
22
|
-
* half with no harness type dependency at all.
|
|
20
|
+
* The harness avoids this with separate client and host programs; this package
|
|
21
|
+
* states the members it uses instead.
|
|
23
22
|
* @module @tivility/dsh-session-share/client
|
|
24
23
|
*/
|
|
25
24
|
import { SESSION_PARAM } from '../param.js';
|
|
25
|
+
import { en, zh } from './locales.js';
|
|
26
|
+
import { SHARE_NS } from './namespace.js';
|
|
27
|
+
import { ShareLink } from './ShareLink.js';
|
|
28
|
+
export { ShareLink } from './ShareLink.js';
|
|
29
|
+
export { SHARE_NS } from './namespace.js';
|
|
26
30
|
/** Client-side services this plugin drives. */
|
|
27
|
-
export const inject = ['sessions'];
|
|
31
|
+
export const inject = ['sessions', 'slots', 'locale'];
|
|
28
32
|
/**
|
|
29
33
|
* Remove the parameter from the address bar once it has been acted on.
|
|
30
34
|
*
|
|
@@ -41,13 +45,13 @@ function stripParam() {
|
|
|
41
45
|
window.history.replaceState(window.history.state, '', `${url.pathname}${url.search}${url.hash}`);
|
|
42
46
|
}
|
|
43
47
|
/**
|
|
44
|
-
*
|
|
48
|
+
* Open the session a `?session=` parameter names, once the list can carry it.
|
|
45
49
|
* @param ctx - the client root context.
|
|
46
50
|
*/
|
|
47
|
-
|
|
51
|
+
function openRequestedSession(ctx) {
|
|
48
52
|
const requested = new URL(window.location.href).searchParams.get(SESSION_PARAM);
|
|
49
53
|
// No parameter is the overwhelmingly common case — every ordinary visit — so
|
|
50
|
-
// this
|
|
54
|
+
// this costs nothing at all when it has nothing to do.
|
|
51
55
|
if (requested === null || requested === '')
|
|
52
56
|
return;
|
|
53
57
|
ctx.effect(() => {
|
|
@@ -78,4 +82,19 @@ export function apply(ctx) {
|
|
|
78
82
|
return unsubscribe;
|
|
79
83
|
});
|
|
80
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Read the link, and offer one.
|
|
87
|
+
* @param ctx - the client root context.
|
|
88
|
+
*/
|
|
89
|
+
export function apply(ctx) {
|
|
90
|
+
openRequestedSession(ctx);
|
|
91
|
+
ctx.effect(() => ctx.locale.register(SHARE_NS, { zh, en }));
|
|
92
|
+
ctx.effect(() => ctx.slots.register({
|
|
93
|
+
// The header's utility band, not the title row: this is a control, and the
|
|
94
|
+
// band beside the breadcrumb is where static session context belongs.
|
|
95
|
+
name: 'conversation.session.header.utilities',
|
|
96
|
+
id: 'session-share',
|
|
97
|
+
locale: SHARE_NS,
|
|
98
|
+
}, ShareLink));
|
|
99
|
+
}
|
|
81
100
|
//# sourceMappingURL=index.js.map
|
package/lib/client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Copy for the session-header share control. */
|
|
2
|
+
/** English copy. */
|
|
3
|
+
export const en = {
|
|
4
|
+
copy: 'Copy link',
|
|
5
|
+
copied: 'Copied',
|
|
6
|
+
hint: 'Copy a link that opens this session',
|
|
7
|
+
};
|
|
8
|
+
/** Chinese copy. */
|
|
9
|
+
export const zh = {
|
|
10
|
+
copy: '复制链接',
|
|
11
|
+
copied: '已复制',
|
|
12
|
+
hint: '复制一个直接打开这条会话的链接',
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=locales.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locales.js","sourceRoot":"","sources":["../../src/client/locales.ts"],"names":[],"mappings":"AAAA,iDAAiD;AAKjD,oBAAoB;AACpB,MAAM,CAAC,MAAM,EAAE,GAA6B;IAC1C,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,QAAQ;IAChB,IAAI,EAAE,qCAAqC;CAC5C,CAAA;AAED,oBAAoB;AACpB,MAAM,CAAC,MAAM,EAAE,GAA6B;IAC1C,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,iBAAiB;CACxB,CAAA"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The locale namespace this bundle registers, and the key union its copy
|
|
3
|
+
* satisfies.
|
|
4
|
+
*
|
|
5
|
+
* Kept in its own module because the declaration merge below has to be visible
|
|
6
|
+
* to both the registration site and the component, and importing the component
|
|
7
|
+
* from the registration site (or the reverse) would make that a cycle.
|
|
8
|
+
* @module @tivility/dsh-session-share/client/namespace
|
|
9
|
+
*/
|
|
10
|
+
/** Locale namespace for the session-header share control. */
|
|
11
|
+
export const SHARE_NS = 'session.share';
|
|
12
|
+
//# sourceMappingURL=namespace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"namespace.js","sourceRoot":"","sources":["../../src/client/namespace.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,6DAA6D;AAC7D,MAAM,CAAC,MAAM,QAAQ,GAAG,eAAe,CAAA"}
|
package/lib/client.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
window.__ModuleLoader__.load({ id: "@tivility/dsh-session-share", factory: (require) => {
|
|
2
2
|
var module = { exports: {} }; var exports = module.exports;
|
|
3
3
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
4
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
5
|
+
let react = require("react");
|
|
6
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
4
7
|
|
|
5
8
|
//#region lib/param.js
|
|
6
9
|
/**
|
|
@@ -16,17 +19,101 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
16
19
|
*/
|
|
17
20
|
/** Query parameter naming the session to open on load. */
|
|
18
21
|
const SESSION_PARAM = "session";
|
|
22
|
+
/**
|
|
23
|
+
* Build the query a share link carries.
|
|
24
|
+
* @param sessionId - the session to open.
|
|
25
|
+
* @returns the query string, question mark included.
|
|
26
|
+
*/
|
|
27
|
+
function shareQuery(sessionId) {
|
|
28
|
+
return `?${SESSION_PARAM}=${encodeURIComponent(sessionId)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region lib/client/locales.js
|
|
33
|
+
/** Copy for the session-header share control. */
|
|
34
|
+
/** English copy. */
|
|
35
|
+
const en = {
|
|
36
|
+
copy: "Copy link",
|
|
37
|
+
copied: "Copied",
|
|
38
|
+
hint: "Copy a link that opens this session"
|
|
39
|
+
};
|
|
40
|
+
/** Chinese copy. */
|
|
41
|
+
const zh = {
|
|
42
|
+
copy: "复制链接",
|
|
43
|
+
copied: "已复制",
|
|
44
|
+
hint: "复制一个直接打开这条会话的链接"
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region lib/client/namespace.js
|
|
49
|
+
/**
|
|
50
|
+
* The locale namespace this bundle registers, and the key union its copy
|
|
51
|
+
* satisfies.
|
|
52
|
+
*
|
|
53
|
+
* Kept in its own module because the declaration merge below has to be visible
|
|
54
|
+
* to both the registration site and the component, and importing the component
|
|
55
|
+
* from the registration site (or the reverse) would make that a cycle.
|
|
56
|
+
* @module @tivility/dsh-session-share/client/namespace
|
|
57
|
+
*/
|
|
58
|
+
/** Locale namespace for the session-header share control. */
|
|
59
|
+
const SHARE_NS = "session.share";
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region lib/client/ShareLink.js
|
|
63
|
+
/**
|
|
64
|
+
* The session header's "copy link" control.
|
|
65
|
+
*
|
|
66
|
+
* Without it the feature is unreachable by a person: the link format lives in
|
|
67
|
+
* the system prompt, which teaches the *model* to hand out links, and leaves
|
|
68
|
+
* whoever is looking at the GUI to construct one from a session id they cannot
|
|
69
|
+
* see. This is the one affordance that makes it a feature rather than a
|
|
70
|
+
* convention.
|
|
71
|
+
*
|
|
72
|
+
* The origin comes from `window.location`, so the link is always the address
|
|
73
|
+
* this browser reached the harness at — the loopback URL when that is how you
|
|
74
|
+
* opened it, the LAN address when it is not. Nothing to configure, and nothing
|
|
75
|
+
* that can drift from where the server actually answers.
|
|
76
|
+
* @module @tivility/dsh-session-share/client/ShareLink
|
|
77
|
+
*/
|
|
78
|
+
/** How long the copied state shows, in ms — matched to the harness's own copy controls. */
|
|
79
|
+
const COPIED_MS = 1e3;
|
|
80
|
+
/**
|
|
81
|
+
* Render the copy-link control for the current session.
|
|
82
|
+
* @param props - composed slot props.
|
|
83
|
+
* @returns the control.
|
|
84
|
+
*/
|
|
85
|
+
function ShareLink({ sessionId, t }) {
|
|
86
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
87
|
+
const url = `${window.location.origin}/${shareQuery(String(sessionId))}`;
|
|
88
|
+
return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
89
|
+
variant: "ghost",
|
|
90
|
+
size: "sm",
|
|
91
|
+
onClick: (0, react.useCallback)(() => {
|
|
92
|
+
if (copied) return;
|
|
93
|
+
(0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(url).then((accepted) => {
|
|
94
|
+
if (!accepted) return;
|
|
95
|
+
setCopied(true);
|
|
96
|
+
window.setTimeout(() => {
|
|
97
|
+
setCopied(false);
|
|
98
|
+
}, COPIED_MS);
|
|
99
|
+
});
|
|
100
|
+
}, [copied, url]),
|
|
101
|
+
title: copied ? t("copied") : `${t("hint")}\n${url}`,
|
|
102
|
+
icon: copied ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 14 }) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLinkOutline16, { size: 14 }),
|
|
103
|
+
children: copied ? t("copied") : t("copy")
|
|
104
|
+
});
|
|
105
|
+
}
|
|
19
106
|
|
|
20
107
|
//#endregion
|
|
21
108
|
//#region lib/client/index.js
|
|
22
109
|
/**
|
|
23
|
-
* The browser half: open the session a link names.
|
|
110
|
+
* The browser half: open the session a link names, and offer a way to get one.
|
|
24
111
|
*
|
|
25
112
|
* The harness's GUI has no routing — the whole client is searched in vain for
|
|
26
113
|
* `pushState`, `location.hash`, or `searchParams`. The selected session lives
|
|
27
114
|
* in the sessions manager and is restored from persisted state on load, so a
|
|
28
|
-
* URL naming one has nowhere to be read. This adds that
|
|
29
|
-
*
|
|
115
|
+
* URL naming one has nowhere to be read. This adds that step, and the control
|
|
116
|
+
* that makes it reachable by a person rather than only by the model.
|
|
30
117
|
*
|
|
31
118
|
* It deliberately hides nothing. The sidebar, the other sessions, and the
|
|
32
119
|
* composer are all exactly as they would be — the link decides where you land,
|
|
@@ -38,13 +125,16 @@ const SESSION_PARAM = "session";
|
|
|
38
125
|
* node half pulls in the harness's HOST augmentations of `Context` — where
|
|
39
126
|
* `sessions` is a different service with the same name. Importing
|
|
40
127
|
* `ClientContext` would therefore resolve `ctx.sessions` to the host's shape.
|
|
41
|
-
* The harness avoids this with separate client and host programs;
|
|
42
|
-
*
|
|
43
|
-
* half with no harness type dependency at all.
|
|
128
|
+
* The harness avoids this with separate client and host programs; this package
|
|
129
|
+
* states the members it uses instead.
|
|
44
130
|
* @module @tivility/dsh-session-share/client
|
|
45
131
|
*/
|
|
46
132
|
/** Client-side services this plugin drives. */
|
|
47
|
-
const inject = [
|
|
133
|
+
const inject = [
|
|
134
|
+
"sessions",
|
|
135
|
+
"slots",
|
|
136
|
+
"locale"
|
|
137
|
+
];
|
|
48
138
|
/**
|
|
49
139
|
* Remove the parameter from the address bar once it has been acted on.
|
|
50
140
|
*
|
|
@@ -60,10 +150,10 @@ function stripParam() {
|
|
|
60
150
|
window.history.replaceState(window.history.state, "", `${url.pathname}${url.search}${url.hash}`);
|
|
61
151
|
}
|
|
62
152
|
/**
|
|
63
|
-
*
|
|
153
|
+
* Open the session a `?session=` parameter names, once the list can carry it.
|
|
64
154
|
* @param ctx - the client root context.
|
|
65
155
|
*/
|
|
66
|
-
function
|
|
156
|
+
function openRequestedSession(ctx) {
|
|
67
157
|
const requested = new URL(window.location.href).searchParams.get(SESSION_PARAM);
|
|
68
158
|
if (requested === null || requested === "") return;
|
|
69
159
|
ctx.effect(() => {
|
|
@@ -88,8 +178,26 @@ function apply(ctx) {
|
|
|
88
178
|
return unsubscribe;
|
|
89
179
|
});
|
|
90
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Read the link, and offer one.
|
|
183
|
+
* @param ctx - the client root context.
|
|
184
|
+
*/
|
|
185
|
+
function apply(ctx) {
|
|
186
|
+
openRequestedSession(ctx);
|
|
187
|
+
ctx.effect(() => ctx.locale.register(SHARE_NS, {
|
|
188
|
+
zh,
|
|
189
|
+
en
|
|
190
|
+
}));
|
|
191
|
+
ctx.effect(() => ctx.slots.register({
|
|
192
|
+
name: "conversation.session.header.utilities",
|
|
193
|
+
id: "session-share",
|
|
194
|
+
locale: SHARE_NS
|
|
195
|
+
}, ShareLink));
|
|
196
|
+
}
|
|
91
197
|
|
|
92
198
|
//#endregion
|
|
199
|
+
exports.SHARE_NS = SHARE_NS;
|
|
200
|
+
exports.ShareLink = ShareLink;
|
|
93
201
|
exports.apply = apply;
|
|
94
202
|
exports.inject = inject;
|
|
95
203
|
return module.exports; } });
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["param.js","client/index.js"],"sourcesContent":["/**\n * The one fact both halves of this plugin have to agree on.\n *\n * The query parameter is a constant rather than configuration because the two\n * halves cannot share a configuration value: the browser bundle is fetched\n * outside the loader's config tree and receives only what its own `apply`\n * is handed, while the link the host writes into the system prompt is built\n * on the other side of the wire. A constant is the honest form of a value\n * that must match in two places.\n * @module @tivility/dsh-session-share/param\n */\n/** Query parameter naming the session to open on load. */\nexport const SESSION_PARAM = 'session';\n/**\n * Build the query a share link carries.\n * @param sessionId - the session to open.\n * @returns the query string, question mark included.\n */\nexport function shareQuery(sessionId) {\n return `?${SESSION_PARAM}=${encodeURIComponent(sessionId)}`;\n}\n//# sourceMappingURL=param.js.map","/**\n * The browser half: open the session a link names.\n *\n * The harness's GUI has no routing — the whole client is searched in vain for\n * `pushState`, `location.hash`, or `searchParams`. The selected session lives\n * in the sessions manager and is restored from persisted state on load, so a\n * URL naming one has nowhere to be read. This adds that one step and nothing\n * else: read the parameter, select the session, get out of the way.\n *\n * It deliberately hides nothing. The sidebar, the other sessions, and the\n * composer are all exactly as they would be — the link decides where you land,\n * not what you can see.\n *\n * ## Why the context is typed here\n *\n * The two halves of this package compile in one TypeScript program, and the\n * node half pulls in the harness's HOST augmentations of `Context` — where\n * `sessions` is a different service with the same name. Importing\n * `ClientContext` would therefore resolve `ctx.sessions` to the host's shape.\n * The harness avoids this with separate client and host programs; a two-file\n * package states the three members it uses instead, which also leaves this\n * half with no harness type dependency at all.\n * @module @tivility/dsh-session-share/client\n */\nimport { SESSION_PARAM } from '../param.js';\n/** Client-side services this plugin drives. */\nexport const inject = ['sessions'];\n/**\n * Remove the parameter from the address bar once it has been acted on.\n *\n * The selection is persisted by the runtime, so leaving the parameter in place\n * would make every later reload jump back to the linked session — fighting the\n * user each time they navigate away and refresh. Consuming it once makes the\n * link mean \"start here\", which is what a link means.\n */\nfunction stripParam() {\n const url = new URL(window.location.href);\n if (!url.searchParams.has(SESSION_PARAM))\n return;\n url.searchParams.delete(SESSION_PARAM);\n window.history.replaceState(window.history.state, '', `${url.pathname}${url.search}${url.hash}`);\n}\n/**\n * Watch the session list until the named session can be opened.\n * @param ctx - the client root context.\n */\nexport function apply(ctx) {\n const requested = new URL(window.location.href).searchParams.get(SESSION_PARAM);\n // No parameter is the overwhelmingly common case — every ordinary visit — so\n // this plugin costs nothing at all when it has nothing to do.\n if (requested === null || requested === '')\n return;\n ctx.effect(() => {\n let done = false;\n const attempt = () => {\n if (done)\n return;\n const state = ctx.sessions.list.getSnapshot();\n if (state.byId[requested] !== undefined) {\n done = true;\n ctx.sessions.open(requested);\n stripParam();\n return;\n }\n // The list arrives asynchronously, so an id that is absent right now may\n // simply not have loaded. The phase is what separates \"not yet\" from \"no\n // such session\" — a timeout would guess at it.\n if (state.phase === 'ready') {\n done = true;\n stripParam();\n ctx.logger?.warn?.(`session-share: no session ${requested} in this harness`);\n }\n };\n const unsubscribe = ctx.sessions.list.subscribe(attempt);\n // The list may already be ready before this plugin activates, in which case\n // no change is ever published and the subscription alone would wait forever.\n attempt();\n return unsubscribe;\n });\n}\n//# sourceMappingURL=index.js.map"],"mappings":";;;;;;;;;;;;;;;;;AAYA,MAAa,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACc7B,MAAa,SAAS,CAAC,WAAW;;;;;;;;;AASlC,SAAS,aAAa;CAClB,MAAM,MAAM,IAAI,IAAI,OAAO,SAAS,KAAK;AACzC,KAAI,CAAC,IAAI,aAAa,IAAI,cAAc,CACpC;AACJ,KAAI,aAAa,OAAO,cAAc;AACtC,QAAO,QAAQ,aAAa,OAAO,QAAQ,OAAO,IAAI,GAAG,IAAI,WAAW,IAAI,SAAS,IAAI,OAAO;;;;;;AAMpG,SAAgB,MAAM,KAAK;CACvB,MAAM,YAAY,IAAI,IAAI,OAAO,SAAS,KAAK,CAAC,aAAa,IAAI,cAAc;AAG/E,KAAI,cAAc,QAAQ,cAAc,GACpC;AACJ,KAAI,aAAa;EACb,IAAI,OAAO;EACX,MAAM,gBAAgB;AAClB,OAAI,KACA;GACJ,MAAM,QAAQ,IAAI,SAAS,KAAK,aAAa;AAC7C,OAAI,MAAM,KAAK,eAAe,QAAW;AACrC,WAAO;AACP,QAAI,SAAS,KAAK,UAAU;AAC5B,gBAAY;AACZ;;AAKJ,OAAI,MAAM,UAAU,SAAS;AACzB,WAAO;AACP,gBAAY;AACZ,QAAI,QAAQ,OAAO,6BAA6B,UAAU,kBAAkB;;;EAGpF,MAAM,cAAc,IAAI,SAAS,KAAK,UAAU,QAAQ;AAGxD,WAAS;AACT,SAAO;GACT"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["Button","IconCheckOutline16","IconLinkOutline16"],"sources":["param.js","client/locales.js","client/namespace.js","client/ShareLink.js","client/index.js"],"sourcesContent":["/**\n * The one fact both halves of this plugin have to agree on.\n *\n * The query parameter is a constant rather than configuration because the two\n * halves cannot share a configuration value: the browser bundle is fetched\n * outside the loader's config tree and receives only what its own `apply`\n * is handed, while the link the host writes into the system prompt is built\n * on the other side of the wire. A constant is the honest form of a value\n * that must match in two places.\n * @module @tivility/dsh-session-share/param\n */\n/** Query parameter naming the session to open on load. */\nexport const SESSION_PARAM = 'session';\n/**\n * Build the query a share link carries.\n * @param sessionId - the session to open.\n * @returns the query string, question mark included.\n */\nexport function shareQuery(sessionId) {\n return `?${SESSION_PARAM}=${encodeURIComponent(sessionId)}`;\n}\n//# sourceMappingURL=param.js.map","/** Copy for the session-header share control. */\n/** English copy. */\nexport const en = {\n copy: 'Copy link',\n copied: 'Copied',\n hint: 'Copy a link that opens this session',\n};\n/** Chinese copy. */\nexport const zh = {\n copy: '复制链接',\n copied: '已复制',\n hint: '复制一个直接打开这条会话的链接',\n};\n//# sourceMappingURL=locales.js.map","/**\n * The locale namespace this bundle registers, and the key union its copy\n * satisfies.\n *\n * Kept in its own module because the declaration merge below has to be visible\n * to both the registration site and the component, and importing the component\n * from the registration site (or the reverse) would make that a cycle.\n * @module @tivility/dsh-session-share/client/namespace\n */\n/** Locale namespace for the session-header share control. */\nexport const SHARE_NS = 'session.share';\n//# sourceMappingURL=namespace.js.map","import { jsx as _jsx } from \"react/jsx-runtime\";\n/**\n * The session header's \"copy link\" control.\n *\n * Without it the feature is unreachable by a person: the link format lives in\n * the system prompt, which teaches the *model* to hand out links, and leaves\n * whoever is looking at the GUI to construct one from a session id they cannot\n * see. This is the one affordance that makes it a feature rather than a\n * convention.\n *\n * The origin comes from `window.location`, so the link is always the address\n * this browser reached the harness at — the loopback URL when that is how you\n * opened it, the LAN address when it is not. Nothing to configure, and nothing\n * that can drift from where the server actually answers.\n * @module @tivility/dsh-session-share/client/ShareLink\n */\nimport { useCallback, useState } from 'react';\nimport { Button, IconCheckOutline16, IconLinkOutline16, writeClipboard, } from '@deepseek-ai/dsh-client-ui-primitives';\nimport { shareQuery } from '../param.js';\nimport { SHARE_NS } from './namespace.js';\n/** How long the copied state shows, in ms — matched to the harness's own copy controls. */\nconst COPIED_MS = 1000;\n/**\n * Render the copy-link control for the current session.\n * @param props - composed slot props.\n * @returns the control.\n */\nexport function ShareLink({ sessionId, t }) {\n const [copied, setCopied] = useState(false);\n const url = `${window.location.origin}/${shareQuery(String(sessionId))}`;\n const onCopy = useCallback(() => {\n if (copied)\n return;\n void writeClipboard(url).then((accepted) => {\n // A refused write (denied permission, insecure context) leaves the flag\n // alone: the control must never claim a copy the host declined.\n if (!accepted)\n return;\n setCopied(true);\n window.setTimeout(() => { setCopied(false); }, COPIED_MS);\n });\n }, [copied, url]);\n return (_jsx(Button, { variant: \"ghost\", size: \"sm\", onClick: onCopy, \n // The URL itself is the most useful hover text: it is what gets copied,\n // and seeing it is how someone learns the format exists.\n title: copied ? t('copied') : `${t('hint')}\\n${url}`, icon: copied ? _jsx(IconCheckOutline16, { size: 14 }) : _jsx(IconLinkOutline16, { size: 14 }), children: copied ? t('copied') : t('copy') }));\n}\n//# sourceMappingURL=ShareLink.js.map","/**\n * The browser half: open the session a link names, and offer a way to get one.\n *\n * The harness's GUI has no routing — the whole client is searched in vain for\n * `pushState`, `location.hash`, or `searchParams`. The selected session lives\n * in the sessions manager and is restored from persisted state on load, so a\n * URL naming one has nowhere to be read. This adds that step, and the control\n * that makes it reachable by a person rather than only by the model.\n *\n * It deliberately hides nothing. The sidebar, the other sessions, and the\n * composer are all exactly as they would be — the link decides where you land,\n * not what you can see.\n *\n * ## Why the context is typed here\n *\n * The two halves of this package compile in one TypeScript program, and the\n * node half pulls in the harness's HOST augmentations of `Context` — where\n * `sessions` is a different service with the same name. Importing\n * `ClientContext` would therefore resolve `ctx.sessions` to the host's shape.\n * The harness avoids this with separate client and host programs; this package\n * states the members it uses instead.\n * @module @tivility/dsh-session-share/client\n */\nimport { SESSION_PARAM } from '../param.js';\nimport { en, zh } from './locales.js';\nimport { SHARE_NS } from './namespace.js';\nimport { ShareLink } from './ShareLink.js';\nexport { ShareLink } from './ShareLink.js';\nexport { SHARE_NS } from './namespace.js';\n/** Client-side services this plugin drives. */\nexport const inject = ['sessions', 'slots', 'locale'];\n/**\n * Remove the parameter from the address bar once it has been acted on.\n *\n * The selection is persisted by the runtime, so leaving the parameter in place\n * would make every later reload jump back to the linked session — fighting the\n * user each time they navigate away and refresh. Consuming it once makes the\n * link mean \"start here\", which is what a link means.\n */\nfunction stripParam() {\n const url = new URL(window.location.href);\n if (!url.searchParams.has(SESSION_PARAM))\n return;\n url.searchParams.delete(SESSION_PARAM);\n window.history.replaceState(window.history.state, '', `${url.pathname}${url.search}${url.hash}`);\n}\n/**\n * Open the session a `?session=` parameter names, once the list can carry it.\n * @param ctx - the client root context.\n */\nfunction openRequestedSession(ctx) {\n const requested = new URL(window.location.href).searchParams.get(SESSION_PARAM);\n // No parameter is the overwhelmingly common case — every ordinary visit — so\n // this costs nothing at all when it has nothing to do.\n if (requested === null || requested === '')\n return;\n ctx.effect(() => {\n let done = false;\n const attempt = () => {\n if (done)\n return;\n const state = ctx.sessions.list.getSnapshot();\n if (state.byId[requested] !== undefined) {\n done = true;\n ctx.sessions.open(requested);\n stripParam();\n return;\n }\n // The list arrives asynchronously, so an id that is absent right now may\n // simply not have loaded. The phase is what separates \"not yet\" from \"no\n // such session\" — a timeout would guess at it.\n if (state.phase === 'ready') {\n done = true;\n stripParam();\n ctx.logger?.warn?.(`session-share: no session ${requested} in this harness`);\n }\n };\n const unsubscribe = ctx.sessions.list.subscribe(attempt);\n // The list may already be ready before this plugin activates, in which case\n // no change is ever published and the subscription alone would wait forever.\n attempt();\n return unsubscribe;\n });\n}\n/**\n * Read the link, and offer one.\n * @param ctx - the client root context.\n */\nexport function apply(ctx) {\n openRequestedSession(ctx);\n ctx.effect(() => ctx.locale.register(SHARE_NS, { zh, en }));\n ctx.effect(() => ctx.slots.register({\n // The header's utility band, not the title row: this is a control, and the\n // band beside the breadcrumb is where static session context belongs.\n name: 'conversation.session.header.utilities',\n id: 'session-share',\n locale: SHARE_NS,\n }, ShareLink));\n}\n//# sourceMappingURL=index.js.map"],"mappings":";;;;;;;;;;;;;;;;;;;;AAYA,MAAa,gBAAgB;;;;;;AAM7B,SAAgB,WAAW,WAAW;AAClC,QAAO,IAAI,cAAc,GAAG,mBAAmB,UAAU;;;;;;;ACjB7D,MAAa,KAAK;CACd,MAAM;CACN,QAAQ;CACR,MAAM;CACT;;AAED,MAAa,KAAK;CACd,MAAM;CACN,QAAQ;CACR,MAAM;CACT;;;;;;;;;;;;;;ACFD,MAAa,WAAW;;;;;;;;;;;;;;;;;;;;ACWxB,MAAM,YAAY;;;;;;AAMlB,SAAgB,UAAU,EAAE,WAAW,KAAK;CACxC,MAAM,CAAC,QAAQ,iCAAsB,MAAM;CAC3C,MAAM,MAAM,GAAG,OAAO,SAAS,OAAO,GAAG,WAAW,OAAO,UAAU,CAAC;AAatE,mCAAaA,8CAAQ;EAAE,SAAS;EAAS,MAAM;EAAM,sCAZpB;AAC7B,OAAI,OACA;AACJ,6DAAoB,IAAI,CAAC,MAAM,aAAa;AAGxC,QAAI,CAAC,SACD;AACJ,cAAU,KAAK;AACf,WAAO,iBAAiB;AAAE,eAAU,MAAM;OAAK,UAAU;KAC3D;KACH,CAAC,QAAQ,IAAI,CAAC;EAIb,OAAO,SAAS,EAAE,SAAS,GAAG,GAAG,EAAE,OAAO,CAAC,IAAI;EAAO,MAAM,oCAAcC,0DAAoB,EAAE,MAAM,IAAI,CAAC,8BAAQC,yDAAmB,EAAE,MAAM,IAAI,CAAC;EAAE,UAAU,SAAS,EAAE,SAAS,GAAG,EAAE,OAAO;EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACf1M,MAAa,SAAS;CAAC;CAAY;CAAS;CAAS;;;;;;;;;AASrD,SAAS,aAAa;CAClB,MAAM,MAAM,IAAI,IAAI,OAAO,SAAS,KAAK;AACzC,KAAI,CAAC,IAAI,aAAa,IAAI,cAAc,CACpC;AACJ,KAAI,aAAa,OAAO,cAAc;AACtC,QAAO,QAAQ,aAAa,OAAO,QAAQ,OAAO,IAAI,GAAG,IAAI,WAAW,IAAI,SAAS,IAAI,OAAO;;;;;;AAMpG,SAAS,qBAAqB,KAAK;CAC/B,MAAM,YAAY,IAAI,IAAI,OAAO,SAAS,KAAK,CAAC,aAAa,IAAI,cAAc;AAG/E,KAAI,cAAc,QAAQ,cAAc,GACpC;AACJ,KAAI,aAAa;EACb,IAAI,OAAO;EACX,MAAM,gBAAgB;AAClB,OAAI,KACA;GACJ,MAAM,QAAQ,IAAI,SAAS,KAAK,aAAa;AAC7C,OAAI,MAAM,KAAK,eAAe,QAAW;AACrC,WAAO;AACP,QAAI,SAAS,KAAK,UAAU;AAC5B,gBAAY;AACZ;;AAKJ,OAAI,MAAM,UAAU,SAAS;AACzB,WAAO;AACP,gBAAY;AACZ,QAAI,QAAQ,OAAO,6BAA6B,UAAU,kBAAkB;;;EAGpF,MAAM,cAAc,IAAI,SAAS,KAAK,UAAU,QAAQ;AAGxD,WAAS;AACT,SAAO;GACT;;;;;;AAMN,SAAgB,MAAM,KAAK;AACvB,sBAAqB,IAAI;AACzB,KAAI,aAAa,IAAI,OAAO,SAAS,UAAU;EAAE;EAAI;EAAI,CAAC,CAAC;AAC3D,KAAI,aAAa,IAAI,MAAM,SAAS;EAGhC,MAAM;EACN,IAAI;EACJ,QAAQ;EACX,EAAE,UAAU,CAAC"}
|
package/lib/index.js
CHANGED
|
@@ -21,17 +21,24 @@
|
|
|
21
21
|
* @module @tivility/dsh-session-share
|
|
22
22
|
*/
|
|
23
23
|
import z from '@deepseek-ai/schemastery';
|
|
24
|
-
import {
|
|
24
|
+
import { isTrustedRequest, normalizePublicBaseUrl, resolvePublicBaseUrl, sendStatus } from '@tivility/dsh-web-kit';
|
|
25
25
|
import { SESSION_PARAM, shareQuery } from './param.js';
|
|
26
26
|
export { SESSION_PARAM, shareQuery } from './param.js';
|
|
27
27
|
/** Stable Cordis plugin name. */
|
|
28
28
|
export const name = 'session-share';
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Nothing hard. The redirect needs `webServer`, but requiring it here would
|
|
31
|
+
* make this row a startup failure in every profile that serves no HTTP — and a
|
|
32
|
+
* plugin suite installed once in the home overlay is exactly how that happens.
|
|
33
|
+
* The route mounts through a nested injection instead, so a headless profile
|
|
34
|
+
* activates the row and still teaches its agents the link format.
|
|
35
|
+
*/
|
|
36
|
+
export const inject = [];
|
|
31
37
|
export const Config = z.object({
|
|
32
38
|
route: z.string().default('/s'),
|
|
33
39
|
prompt: z.boolean().default(true),
|
|
34
40
|
trustedHosts: z.array(String).default([]),
|
|
41
|
+
publicBaseUrl: z.string().default(''),
|
|
35
42
|
});
|
|
36
43
|
/** A session id as it may appear in a URL: the harness mints uuids. */
|
|
37
44
|
const SESSION_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
@@ -84,12 +91,17 @@ function createHandler(route, trustedHosts) {
|
|
|
84
91
|
* @param sessionId - the session the calling agent belongs to.
|
|
85
92
|
* @returns the prompt text.
|
|
86
93
|
*/
|
|
87
|
-
function promptSection(ctx, route, sessionId) {
|
|
88
|
-
const
|
|
89
|
-
|
|
94
|
+
function promptSection(ctx, route, sessionId, publicBaseUrl) {
|
|
95
|
+
const origin = resolvePublicBaseUrl(ctx, publicBaseUrl);
|
|
96
|
+
// A headless profile with no configured origin has nowhere to point: the
|
|
97
|
+
// sessions may well be served by a Web profile elsewhere, but only the
|
|
98
|
+
// operator knows where, and a guessed loopback link points at nothing.
|
|
99
|
+
if (origin === undefined)
|
|
100
|
+
return '';
|
|
101
|
+
const link = `${origin}/${shareQuery(sessionId)}`;
|
|
90
102
|
const shortForm = route === ''
|
|
91
103
|
? ''
|
|
92
|
-
: ` The shorter ${
|
|
104
|
+
: ` The shorter ${origin}${route}/${sessionId} redirects to the same place.`;
|
|
93
105
|
return [
|
|
94
106
|
`This conversation has a direct link: ${link}`,
|
|
95
107
|
`Opening it in a browser selects this session instead of whatever was last open.${shortForm}`,
|
|
@@ -108,15 +120,24 @@ function promptSection(ctx, route, sessionId) {
|
|
|
108
120
|
export function apply(ctx, config = {}) {
|
|
109
121
|
const route = config.route ?? '/s';
|
|
110
122
|
const trustedHosts = config.trustedHosts ?? [];
|
|
123
|
+
const publicBaseUrl = config.publicBaseUrl ?? '';
|
|
124
|
+
// Validate a configured origin at activation: a typo should stop the load,
|
|
125
|
+
// not quietly put broken links in front of the model.
|
|
126
|
+
if (publicBaseUrl !== '')
|
|
127
|
+
normalizePublicBaseUrl(publicBaseUrl);
|
|
111
128
|
if (route !== '') {
|
|
112
129
|
if (!route.startsWith('/') || route.endsWith('/')) {
|
|
113
130
|
throw new Error(`session-share: route ${JSON.stringify(route)} must be an absolute path without a trailing slash`);
|
|
114
131
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
132
|
+
// Nested, so a profile with no HTTP server activates this row and simply
|
|
133
|
+
// serves no redirect.
|
|
134
|
+
ctx.inject(['webServer'], (web) => {
|
|
135
|
+
web.effect(() => web.webServer.register({
|
|
136
|
+
kind: 'prefix',
|
|
137
|
+
path: route,
|
|
138
|
+
handler: createHandler(route, trustedHosts),
|
|
139
|
+
}), `session-share: ${route} route`);
|
|
140
|
+
});
|
|
120
141
|
}
|
|
121
142
|
if (config.prompt === false)
|
|
122
143
|
return;
|
|
@@ -128,7 +149,7 @@ export function apply(ctx, config = {}) {
|
|
|
128
149
|
// Resolved per assembly: the port is known only after the server binds,
|
|
129
150
|
// and the session id is whichever agent this prompt is being built for.
|
|
130
151
|
const sessionId = context.agent?.session.id;
|
|
131
|
-
return sessionId === undefined ? '' : promptSection(scope, route, String(sessionId));
|
|
152
|
+
return sessionId === undefined ? '' : promptSection(scope, route, String(sessionId), publicBaseUrl);
|
|
132
153
|
},
|
|
133
154
|
});
|
|
134
155
|
});
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAKxC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session header's "copy link" control.
|
|
3
|
+
*
|
|
4
|
+
* Without it the feature is unreachable by a person: the link format lives in
|
|
5
|
+
* the system prompt, which teaches the *model* to hand out links, and leaves
|
|
6
|
+
* whoever is looking at the GUI to construct one from a session id they cannot
|
|
7
|
+
* see. This is the one affordance that makes it a feature rather than a
|
|
8
|
+
* convention.
|
|
9
|
+
*
|
|
10
|
+
* The origin comes from `window.location`, so the link is always the address
|
|
11
|
+
* this browser reached the harness at — the loopback URL when that is how you
|
|
12
|
+
* opened it, the LAN address when it is not. Nothing to configure, and nothing
|
|
13
|
+
* that can drift from where the server actually answers.
|
|
14
|
+
* @module @tivility/dsh-session-share/client/ShareLink
|
|
15
|
+
*/
|
|
16
|
+
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
17
|
+
import { SHARE_NS } from './namespace.js';
|
|
18
|
+
/** Full component props: the framework's session context plus this bundle's copy. */
|
|
19
|
+
export type ShareLinkProps = PropsRuntime<'conversation.session.header.utilities'> & PropsLocale<typeof SHARE_NS>;
|
|
20
|
+
/**
|
|
21
|
+
* Render the copy-link control for the current session.
|
|
22
|
+
* @param props - composed slot props.
|
|
23
|
+
* @returns the control.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ShareLink({ sessionId, t }: ShareLinkProps): import("react").JSX.Element;
|
|
26
|
+
//# sourceMappingURL=ShareLink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ShareLink.d.ts","sourceRoot":"","sources":["../../../src/client/ShareLink.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAIjF,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAKzC,qFAAqF;AACrF,MAAM,MAAM,cAAc,GACxB,YAAY,CAAC,uCAAuC,CAAC,GACnD,WAAW,CAAC,OAAO,QAAQ,CAAC,CAAA;AAEhC;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE,cAAc,+BA4BzD"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The browser half: open the session a link names.
|
|
2
|
+
* The browser half: open the session a link names, and offer a way to get one.
|
|
3
3
|
*
|
|
4
4
|
* The harness's GUI has no routing — the whole client is searched in vain for
|
|
5
5
|
* `pushState`, `location.hash`, or `searchParams`. The selected session lives
|
|
6
6
|
* in the sessions manager and is restored from persisted state on load, so a
|
|
7
|
-
* URL naming one has nowhere to be read. This adds that
|
|
8
|
-
*
|
|
7
|
+
* URL naming one has nowhere to be read. This adds that step, and the control
|
|
8
|
+
* that makes it reachable by a person rather than only by the model.
|
|
9
9
|
*
|
|
10
10
|
* It deliberately hides nothing. The sidebar, the other sessions, and the
|
|
11
11
|
* composer are all exactly as they would be — the link decides where you land,
|
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
* node half pulls in the harness's HOST augmentations of `Context` — where
|
|
18
18
|
* `sessions` is a different service with the same name. Importing
|
|
19
19
|
* `ClientContext` would therefore resolve `ctx.sessions` to the host's shape.
|
|
20
|
-
* The harness avoids this with separate client and host programs;
|
|
21
|
-
*
|
|
22
|
-
* half with no harness type dependency at all.
|
|
20
|
+
* The harness avoids this with separate client and host programs; this package
|
|
21
|
+
* states the members it uses instead.
|
|
23
22
|
* @module @tivility/dsh-session-share/client
|
|
24
23
|
*/
|
|
24
|
+
export { ShareLink } from './ShareLink.js';
|
|
25
|
+
export { SHARE_NS } from './namespace.js';
|
|
25
26
|
/** The session-list snapshot this plugin reads. */
|
|
26
27
|
interface SessionListSnapshot {
|
|
27
28
|
/** Every listed session, keyed by id. */
|
|
@@ -41,9 +42,24 @@ interface SessionsFace {
|
|
|
41
42
|
*/
|
|
42
43
|
open(id: string): void;
|
|
43
44
|
}
|
|
45
|
+
/** The slot registry, narrowed to the one registration this plugin makes. */
|
|
46
|
+
interface SlotsFace {
|
|
47
|
+
register(spec: {
|
|
48
|
+
name: string;
|
|
49
|
+
id?: string;
|
|
50
|
+
order?: number;
|
|
51
|
+
locale?: string;
|
|
52
|
+
}, component: unknown): () => void;
|
|
53
|
+
}
|
|
54
|
+
/** The locale registry, narrowed to the one bundle this plugin adds. */
|
|
55
|
+
interface LocaleFace {
|
|
56
|
+
register(namespace: string, dictionaries: Record<string, unknown>): () => void;
|
|
57
|
+
}
|
|
44
58
|
/** The client context members this plugin uses. */
|
|
45
59
|
interface ShareContext {
|
|
46
60
|
readonly sessions: SessionsFace;
|
|
61
|
+
readonly slots: SlotsFace;
|
|
62
|
+
readonly locale: LocaleFace;
|
|
47
63
|
effect(run: () => () => void): unknown;
|
|
48
64
|
readonly logger?: {
|
|
49
65
|
warn?(message: string): void;
|
|
@@ -52,9 +68,8 @@ interface ShareContext {
|
|
|
52
68
|
/** Client-side services this plugin drives. */
|
|
53
69
|
export declare const inject: string[];
|
|
54
70
|
/**
|
|
55
|
-
*
|
|
71
|
+
* Read the link, and offer one.
|
|
56
72
|
* @param ctx - the client root context.
|
|
57
73
|
*/
|
|
58
74
|
export declare function apply(ctx: ShareContext): void;
|
|
59
|
-
export {};
|
|
60
75
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,mDAAmD;AACnD,UAAU,mBAAmB;IAC3B,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAChD,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAA;CACpC;AAED,kEAAkE;AAClE,UAAU,YAAY;IACpB,QAAQ,CAAC,IAAI,EAAE;QACb,WAAW,IAAI,mBAAmB,CAAA;QAClC,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;KAC5C,CAAA;IACD;;;OAGG;IACH,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,6EAA6E;AAC7E,UAAU,SAAS;IACjB,QAAQ,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,IAAI,CAAA;CAC/G;AAED,wEAAwE;AACxE,UAAU,UAAU;IAClB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,IAAI,CAAA;CAC/E;AAED,mDAAmD;AACnD,UAAU,YAAY;IACpB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;IAC3B,MAAM,CAAC,GAAG,EAAE,MAAM,MAAM,IAAI,GAAG,OAAO,CAAA;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CACnD;AAED,+CAA+C;AAC/C,eAAO,MAAM,MAAM,UAAkC,CAAA;AAuDrD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAW7C"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Copy for the session-header share control. */
|
|
2
|
+
/** Locale keys this surface renders. */
|
|
3
|
+
export type ShareKey = 'copy' | 'copied' | 'hint';
|
|
4
|
+
/** English copy. */
|
|
5
|
+
export declare const en: Record<ShareKey, string>;
|
|
6
|
+
/** Chinese copy. */
|
|
7
|
+
export declare const zh: Record<ShareKey, string>;
|
|
8
|
+
//# sourceMappingURL=locales.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,iDAAiD;AAEjD,wCAAwC;AACxC,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEjD,oBAAoB;AACpB,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAIvC,CAAA;AAED,oBAAoB;AACpB,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAIvC,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The locale namespace this bundle registers, and the key union its copy
|
|
3
|
+
* satisfies.
|
|
4
|
+
*
|
|
5
|
+
* Kept in its own module because the declaration merge below has to be visible
|
|
6
|
+
* to both the registration site and the component, and importing the component
|
|
7
|
+
* from the registration site (or the reverse) would make that a cycle.
|
|
8
|
+
* @module @tivility/dsh-session-share/client/namespace
|
|
9
|
+
*/
|
|
10
|
+
import type { ShareKey } from './locales.js';
|
|
11
|
+
/** Locale namespace for the session-header share control. */
|
|
12
|
+
export declare const SHARE_NS = "session.share";
|
|
13
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
14
|
+
interface LocaleNamespaceMap {
|
|
15
|
+
/** The share control's copy. */
|
|
16
|
+
'session.share': ShareKey;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=namespace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"namespace.d.ts","sourceRoot":"","sources":["../../../src/client/namespace.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAE5C,6DAA6D;AAC7D,eAAO,MAAM,QAAQ,kBAAkB,CAAA;AAEvC,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,gCAAgC;QAChC,eAAe,EAAE,QAAQ,CAAA;KAC1B;CACF"}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -25,7 +25,13 @@ import z from '@deepseek-ai/schemastery';
|
|
|
25
25
|
export { SESSION_PARAM, shareQuery } from './param.js';
|
|
26
26
|
/** Stable Cordis plugin name. */
|
|
27
27
|
export declare const name = "session-share";
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Nothing hard. The redirect needs `webServer`, but requiring it here would
|
|
30
|
+
* make this row a startup failure in every profile that serves no HTTP — and a
|
|
31
|
+
* plugin suite installed once in the home overlay is exactly how that happens.
|
|
32
|
+
* The route mounts through a nested injection instead, so a headless profile
|
|
33
|
+
* activates the row and still teaches its agents the link format.
|
|
34
|
+
*/
|
|
29
35
|
export declare const inject: string[];
|
|
30
36
|
/** Plugin configuration. */
|
|
31
37
|
export interface Config {
|
|
@@ -38,6 +44,13 @@ export interface Config {
|
|
|
38
44
|
prompt?: boolean;
|
|
39
45
|
/** Non-loopback authorities this deployment serves, as `host` or `host:port`. */
|
|
40
46
|
trustedHosts?: string[];
|
|
47
|
+
/**
|
|
48
|
+
* The origin browsers actually reach this deployment at, when that is not
|
|
49
|
+
* the local bind — behind a reverse proxy, tunnel, or port forwarder. Also
|
|
50
|
+
* what lets a headless profile link into a Web profile serving the same
|
|
51
|
+
* sessions. Origin only: scheme, host, optional port.
|
|
52
|
+
*/
|
|
53
|
+
publicBaseUrl?: string;
|
|
41
54
|
}
|
|
42
55
|
export declare const Config: z<Config>;
|
|
43
56
|
/**
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAQxC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEtD,iCAAiC;AACjC,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAQxC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEtD,iCAAiC;AACjC,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC;;;;;;GAMG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,EAAO,CAAA;AAElC,4BAA4B;AAC5B,MAAM,WAAW,MAAM;IACrB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oFAAoF;IACpF,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,iFAAiF;IACjF,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAK3B,CAAA;AA8EF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,CAoC7D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tivility/dsh-session-share",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Link straight to one DeepSeek Harness session: the GUI opens on it instead of wherever it was left.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,12 +37,14 @@
|
|
|
37
37
|
"client": {
|
|
38
38
|
"platform": "web",
|
|
39
39
|
"inject": [
|
|
40
|
-
"@deepseek-ai/dsh-client-runtime"
|
|
40
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
42
|
+
"@deepseek-ai/dsh-client-locale"
|
|
41
43
|
]
|
|
42
44
|
}
|
|
43
45
|
},
|
|
44
46
|
"dependencies": {
|
|
45
|
-
"@tivility/dsh-web-kit": "^0.
|
|
47
|
+
"@tivility/dsh-web-kit": "^0.2.0"
|
|
46
48
|
},
|
|
47
49
|
"peerDependencies": {
|
|
48
50
|
"@deepseek-ai/cordis": "*",
|
|
@@ -61,10 +63,19 @@
|
|
|
61
63
|
"devDependencies": {
|
|
62
64
|
"@deepseek-ai/cordis": "*",
|
|
63
65
|
"@deepseek-ai/dsh-agent": "0.1.1-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-client-locale": "0.1.1-rc.2",
|
|
64
67
|
"@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.1-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.1-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.1-rc.2",
|
|
65
71
|
"@deepseek-ai/dsh-host-webserver": "0.1.1-rc.2",
|
|
66
72
|
"@deepseek-ai/dsh-system-prompt": "0.1.1-rc.2",
|
|
67
73
|
"@deepseek-ai/schemastery": "*",
|
|
74
|
+
"@testing-library/react": "^16.1.0",
|
|
75
|
+
"@types/react": "~18.3.1",
|
|
76
|
+
"jsdom": "^26.0.0",
|
|
77
|
+
"react": "^18.2.0",
|
|
78
|
+
"react-dom": "^18.2.0",
|
|
68
79
|
"tsdown": "^0.20.0"
|
|
69
80
|
},
|
|
70
81
|
"publishConfig": {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session header's "copy link" control.
|
|
3
|
+
*
|
|
4
|
+
* Without it the feature is unreachable by a person: the link format lives in
|
|
5
|
+
* the system prompt, which teaches the *model* to hand out links, and leaves
|
|
6
|
+
* whoever is looking at the GUI to construct one from a session id they cannot
|
|
7
|
+
* see. This is the one affordance that makes it a feature rather than a
|
|
8
|
+
* convention.
|
|
9
|
+
*
|
|
10
|
+
* The origin comes from `window.location`, so the link is always the address
|
|
11
|
+
* this browser reached the harness at — the loopback URL when that is how you
|
|
12
|
+
* opened it, the LAN address when it is not. Nothing to configure, and nothing
|
|
13
|
+
* that can drift from where the server actually answers.
|
|
14
|
+
* @module @tivility/dsh-session-share/client/ShareLink
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useCallback, useState } from 'react'
|
|
18
|
+
import {
|
|
19
|
+
Button,
|
|
20
|
+
IconCheckOutline16,
|
|
21
|
+
IconLinkOutline16,
|
|
22
|
+
writeClipboard,
|
|
23
|
+
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
24
|
+
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
25
|
+
// Type-only: pulls the ui-conversation SlotMap merge that declares this seat.
|
|
26
|
+
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
27
|
+
import { shareQuery } from '../param.js'
|
|
28
|
+
import { SHARE_NS } from './namespace.js'
|
|
29
|
+
|
|
30
|
+
/** How long the copied state shows, in ms — matched to the harness's own copy controls. */
|
|
31
|
+
const COPIED_MS = 1000
|
|
32
|
+
|
|
33
|
+
/** Full component props: the framework's session context plus this bundle's copy. */
|
|
34
|
+
export type ShareLinkProps =
|
|
35
|
+
PropsRuntime<'conversation.session.header.utilities'>
|
|
36
|
+
& PropsLocale<typeof SHARE_NS>
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Render the copy-link control for the current session.
|
|
40
|
+
* @param props - composed slot props.
|
|
41
|
+
* @returns the control.
|
|
42
|
+
*/
|
|
43
|
+
export function ShareLink({ sessionId, t }: ShareLinkProps) {
|
|
44
|
+
const [copied, setCopied] = useState(false)
|
|
45
|
+
const url = `${window.location.origin}/${shareQuery(String(sessionId))}`
|
|
46
|
+
|
|
47
|
+
const onCopy = useCallback(() => {
|
|
48
|
+
if (copied) return
|
|
49
|
+
void writeClipboard(url).then((accepted) => {
|
|
50
|
+
// A refused write (denied permission, insecure context) leaves the flag
|
|
51
|
+
// alone: the control must never claim a copy the host declined.
|
|
52
|
+
if (!accepted) return
|
|
53
|
+
setCopied(true)
|
|
54
|
+
window.setTimeout(() => { setCopied(false) }, COPIED_MS)
|
|
55
|
+
})
|
|
56
|
+
}, [copied, url])
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Button
|
|
60
|
+
variant="ghost"
|
|
61
|
+
size="sm"
|
|
62
|
+
onClick={onCopy}
|
|
63
|
+
// The URL itself is the most useful hover text: it is what gets copied,
|
|
64
|
+
// and seeing it is how someone learns the format exists.
|
|
65
|
+
title={copied ? t('copied') : `${t('hint')}\n${url}`}
|
|
66
|
+
icon={copied ? <IconCheckOutline16 size={14} /> : <IconLinkOutline16 size={14} />}
|
|
67
|
+
>
|
|
68
|
+
{copied ? t('copied') : t('copy')}
|
|
69
|
+
</Button>
|
|
70
|
+
)
|
|
71
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The browser half: open the session a link names.
|
|
2
|
+
* The browser half: open the session a link names, and offer a way to get one.
|
|
3
3
|
*
|
|
4
4
|
* The harness's GUI has no routing — the whole client is searched in vain for
|
|
5
5
|
* `pushState`, `location.hash`, or `searchParams`. The selected session lives
|
|
6
6
|
* in the sessions manager and is restored from persisted state on load, so a
|
|
7
|
-
* URL naming one has nowhere to be read. This adds that
|
|
8
|
-
*
|
|
7
|
+
* URL naming one has nowhere to be read. This adds that step, and the control
|
|
8
|
+
* that makes it reachable by a person rather than only by the model.
|
|
9
9
|
*
|
|
10
10
|
* It deliberately hides nothing. The sidebar, the other sessions, and the
|
|
11
11
|
* composer are all exactly as they would be — the link decides where you land,
|
|
@@ -17,13 +17,18 @@
|
|
|
17
17
|
* node half pulls in the harness's HOST augmentations of `Context` — where
|
|
18
18
|
* `sessions` is a different service with the same name. Importing
|
|
19
19
|
* `ClientContext` would therefore resolve `ctx.sessions` to the host's shape.
|
|
20
|
-
* The harness avoids this with separate client and host programs;
|
|
21
|
-
*
|
|
22
|
-
* half with no harness type dependency at all.
|
|
20
|
+
* The harness avoids this with separate client and host programs; this package
|
|
21
|
+
* states the members it uses instead.
|
|
23
22
|
* @module @tivility/dsh-session-share/client
|
|
24
23
|
*/
|
|
25
24
|
|
|
26
25
|
import { SESSION_PARAM } from '../param.js'
|
|
26
|
+
import { en, zh } from './locales.js'
|
|
27
|
+
import { SHARE_NS } from './namespace.js'
|
|
28
|
+
import { ShareLink } from './ShareLink.js'
|
|
29
|
+
|
|
30
|
+
export { ShareLink } from './ShareLink.js'
|
|
31
|
+
export { SHARE_NS } from './namespace.js'
|
|
27
32
|
|
|
28
33
|
/** The session-list snapshot this plugin reads. */
|
|
29
34
|
interface SessionListSnapshot {
|
|
@@ -46,15 +51,27 @@ interface SessionsFace {
|
|
|
46
51
|
open(id: string): void
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
/** The slot registry, narrowed to the one registration this plugin makes. */
|
|
55
|
+
interface SlotsFace {
|
|
56
|
+
register(spec: { name: string, id?: string, order?: number, locale?: string }, component: unknown): () => void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The locale registry, narrowed to the one bundle this plugin adds. */
|
|
60
|
+
interface LocaleFace {
|
|
61
|
+
register(namespace: string, dictionaries: Record<string, unknown>): () => void
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
/** The client context members this plugin uses. */
|
|
50
65
|
interface ShareContext {
|
|
51
66
|
readonly sessions: SessionsFace
|
|
67
|
+
readonly slots: SlotsFace
|
|
68
|
+
readonly locale: LocaleFace
|
|
52
69
|
effect(run: () => () => void): unknown
|
|
53
70
|
readonly logger?: { warn?(message: string): void }
|
|
54
71
|
}
|
|
55
72
|
|
|
56
73
|
/** Client-side services this plugin drives. */
|
|
57
|
-
export const inject = ['sessions']
|
|
74
|
+
export const inject = ['sessions', 'slots', 'locale']
|
|
58
75
|
|
|
59
76
|
/**
|
|
60
77
|
* Remove the parameter from the address bar once it has been acted on.
|
|
@@ -72,13 +89,13 @@ function stripParam(): void {
|
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
/**
|
|
75
|
-
*
|
|
92
|
+
* Open the session a `?session=` parameter names, once the list can carry it.
|
|
76
93
|
* @param ctx - the client root context.
|
|
77
94
|
*/
|
|
78
|
-
|
|
95
|
+
function openRequestedSession(ctx: ShareContext): void {
|
|
79
96
|
const requested = new URL(window.location.href).searchParams.get(SESSION_PARAM)
|
|
80
97
|
// No parameter is the overwhelmingly common case — every ordinary visit — so
|
|
81
|
-
// this
|
|
98
|
+
// this costs nothing at all when it has nothing to do.
|
|
82
99
|
if (requested === null || requested === '') return
|
|
83
100
|
|
|
84
101
|
ctx.effect(() => {
|
|
@@ -108,3 +125,20 @@ export function apply(ctx: ShareContext): void {
|
|
|
108
125
|
return unsubscribe
|
|
109
126
|
})
|
|
110
127
|
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Read the link, and offer one.
|
|
131
|
+
* @param ctx - the client root context.
|
|
132
|
+
*/
|
|
133
|
+
export function apply(ctx: ShareContext): void {
|
|
134
|
+
openRequestedSession(ctx)
|
|
135
|
+
|
|
136
|
+
ctx.effect(() => ctx.locale.register(SHARE_NS, { zh, en }))
|
|
137
|
+
ctx.effect(() => ctx.slots.register({
|
|
138
|
+
// The header's utility band, not the title row: this is a control, and the
|
|
139
|
+
// band beside the breadcrumb is where static session context belongs.
|
|
140
|
+
name: 'conversation.session.header.utilities',
|
|
141
|
+
id: 'session-share',
|
|
142
|
+
locale: SHARE_NS,
|
|
143
|
+
}, ShareLink))
|
|
144
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Copy for the session-header share control. */
|
|
2
|
+
|
|
3
|
+
/** Locale keys this surface renders. */
|
|
4
|
+
export type ShareKey = 'copy' | 'copied' | 'hint'
|
|
5
|
+
|
|
6
|
+
/** English copy. */
|
|
7
|
+
export const en: Record<ShareKey, string> = {
|
|
8
|
+
copy: 'Copy link',
|
|
9
|
+
copied: 'Copied',
|
|
10
|
+
hint: 'Copy a link that opens this session',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Chinese copy. */
|
|
14
|
+
export const zh: Record<ShareKey, string> = {
|
|
15
|
+
copy: '复制链接',
|
|
16
|
+
copied: '已复制',
|
|
17
|
+
hint: '复制一个直接打开这条会话的链接',
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The locale namespace this bundle registers, and the key union its copy
|
|
3
|
+
* satisfies.
|
|
4
|
+
*
|
|
5
|
+
* Kept in its own module because the declaration merge below has to be visible
|
|
6
|
+
* to both the registration site and the component, and importing the component
|
|
7
|
+
* from the registration site (or the reverse) would make that a cycle.
|
|
8
|
+
* @module @tivility/dsh-session-share/client/namespace
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ShareKey } from './locales.js'
|
|
12
|
+
|
|
13
|
+
/** Locale namespace for the session-header share control. */
|
|
14
|
+
export const SHARE_NS = 'session.share'
|
|
15
|
+
|
|
16
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
17
|
+
interface LocaleNamespaceMap {
|
|
18
|
+
/** The share control's copy. */
|
|
19
|
+
'session.share': ShareKey
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -28,7 +28,7 @@ import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
|
28
28
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
29
29
|
// Type-only: contributes the `agent` field on the prompt assembly context.
|
|
30
30
|
import type {} from '@deepseek-ai/dsh-agent'
|
|
31
|
-
import {
|
|
31
|
+
import { isTrustedRequest, normalizePublicBaseUrl, resolvePublicBaseUrl, sendStatus } from '@tivility/dsh-web-kit'
|
|
32
32
|
import { SESSION_PARAM, shareQuery } from './param.js'
|
|
33
33
|
|
|
34
34
|
export { SESSION_PARAM, shareQuery } from './param.js'
|
|
@@ -36,8 +36,14 @@ export { SESSION_PARAM, shareQuery } from './param.js'
|
|
|
36
36
|
/** Stable Cordis plugin name. */
|
|
37
37
|
export const name = 'session-share'
|
|
38
38
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Nothing hard. The redirect needs `webServer`, but requiring it here would
|
|
41
|
+
* make this row a startup failure in every profile that serves no HTTP — and a
|
|
42
|
+
* plugin suite installed once in the home overlay is exactly how that happens.
|
|
43
|
+
* The route mounts through a nested injection instead, so a headless profile
|
|
44
|
+
* activates the row and still teaches its agents the link format.
|
|
45
|
+
*/
|
|
46
|
+
export const inject: string[] = []
|
|
41
47
|
|
|
42
48
|
/** Plugin configuration. */
|
|
43
49
|
export interface Config {
|
|
@@ -50,12 +56,20 @@ export interface Config {
|
|
|
50
56
|
prompt?: boolean
|
|
51
57
|
/** Non-loopback authorities this deployment serves, as `host` or `host:port`. */
|
|
52
58
|
trustedHosts?: string[]
|
|
59
|
+
/**
|
|
60
|
+
* The origin browsers actually reach this deployment at, when that is not
|
|
61
|
+
* the local bind — behind a reverse proxy, tunnel, or port forwarder. Also
|
|
62
|
+
* what lets a headless profile link into a Web profile serving the same
|
|
63
|
+
* sessions. Origin only: scheme, host, optional port.
|
|
64
|
+
*/
|
|
65
|
+
publicBaseUrl?: string
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
export const Config: z<Config> = z.object({
|
|
56
69
|
route: z.string().default('/s'),
|
|
57
70
|
prompt: z.boolean().default(true),
|
|
58
71
|
trustedHosts: z.array(String).default([]),
|
|
72
|
+
publicBaseUrl: z.string().default(''),
|
|
59
73
|
})
|
|
60
74
|
|
|
61
75
|
/** A session id as it may appear in a URL: the harness mints uuids. */
|
|
@@ -113,12 +127,16 @@ function createHandler(
|
|
|
113
127
|
* @param sessionId - the session the calling agent belongs to.
|
|
114
128
|
* @returns the prompt text.
|
|
115
129
|
*/
|
|
116
|
-
function promptSection(ctx: Context, route: string, sessionId: string): string {
|
|
117
|
-
const
|
|
118
|
-
|
|
130
|
+
function promptSection(ctx: Context, route: string, sessionId: string, publicBaseUrl: string): string {
|
|
131
|
+
const origin = resolvePublicBaseUrl(ctx, publicBaseUrl)
|
|
132
|
+
// A headless profile with no configured origin has nowhere to point: the
|
|
133
|
+
// sessions may well be served by a Web profile elsewhere, but only the
|
|
134
|
+
// operator knows where, and a guessed loopback link points at nothing.
|
|
135
|
+
if (origin === undefined) return ''
|
|
136
|
+
const link = `${origin}/${shareQuery(sessionId)}`
|
|
119
137
|
const shortForm = route === ''
|
|
120
138
|
? ''
|
|
121
|
-
: ` The shorter ${
|
|
139
|
+
: ` The shorter ${origin}${route}/${sessionId} redirects to the same place.`
|
|
122
140
|
return [
|
|
123
141
|
`This conversation has a direct link: ${link}`,
|
|
124
142
|
`Opening it in a browser selects this session instead of whatever was last open.${shortForm}`,
|
|
@@ -138,15 +156,24 @@ function promptSection(ctx: Context, route: string, sessionId: string): string {
|
|
|
138
156
|
export function apply(ctx: Context, config: Config = {}): void {
|
|
139
157
|
const route = config.route ?? '/s'
|
|
140
158
|
const trustedHosts = config.trustedHosts ?? []
|
|
159
|
+
const publicBaseUrl = config.publicBaseUrl ?? ''
|
|
160
|
+
// Validate a configured origin at activation: a typo should stop the load,
|
|
161
|
+
// not quietly put broken links in front of the model.
|
|
162
|
+
if (publicBaseUrl !== '') normalizePublicBaseUrl(publicBaseUrl)
|
|
163
|
+
|
|
141
164
|
if (route !== '') {
|
|
142
165
|
if (!route.startsWith('/') || route.endsWith('/')) {
|
|
143
166
|
throw new Error(`session-share: route ${JSON.stringify(route)} must be an absolute path without a trailing slash`)
|
|
144
167
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
168
|
+
// Nested, so a profile with no HTTP server activates this row and simply
|
|
169
|
+
// serves no redirect.
|
|
170
|
+
ctx.inject(['webServer'], (web) => {
|
|
171
|
+
web.effect(() => web.webServer.register({
|
|
172
|
+
kind: 'prefix',
|
|
173
|
+
path: route,
|
|
174
|
+
handler: createHandler(route, trustedHosts),
|
|
175
|
+
}), `session-share: ${route} route`)
|
|
176
|
+
})
|
|
150
177
|
}
|
|
151
178
|
|
|
152
179
|
if (config.prompt === false) return
|
|
@@ -158,7 +185,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
158
185
|
// Resolved per assembly: the port is known only after the server binds,
|
|
159
186
|
// and the session id is whichever agent this prompt is being built for.
|
|
160
187
|
const sessionId = context.agent?.session.id
|
|
161
|
-
return sessionId === undefined ? '' : promptSection(scope, route, String(sessionId))
|
|
188
|
+
return sessionId === undefined ? '' : promptSection(scope, route, String(sessionId), publicBaseUrl)
|
|
162
189
|
},
|
|
163
190
|
})
|
|
164
191
|
})
|