@dsh-workbench/shared 0.1.12 → 0.1.14
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/dist/components/ChoiceGroup.css +49 -0
- package/dist/components/ChoiceGroup.d.ts +13 -3
- package/dist/components/ChoiceGroup.js +38 -5
- package/dist/components/StateDot.d.ts +5 -0
- package/dist/components/StateDot.js +10 -0
- package/dist/host/__tests__/fakes.d.ts +41 -0
- package/dist/host/__tests__/fakes.js +76 -0
- package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
- package/dist/host/__tests__/preflight-fixtures.js +47 -0
- package/dist/host/context-inject.d.ts +24 -0
- package/dist/host/context-inject.js +74 -0
- package/dist/host/events.d.ts +9 -0
- package/dist/host/events.js +70 -0
- package/dist/host/mount-units.d.ts +52 -0
- package/dist/host/mount-units.js +186 -0
- package/dist/host/preflight.d.ts +58 -0
- package/dist/host/preflight.js +233 -0
- package/dist/host/probe-cache.d.ts +6 -0
- package/dist/host/probe-cache.js +19 -0
- package/dist/host/probe-libs.d.ts +45 -0
- package/dist/host/probe-libs.js +61 -0
- package/dist/host/probe-units.d.ts +17 -0
- package/dist/host/probe-units.js +102 -0
- package/dist/host/probe-verdict.d.ts +7 -0
- package/dist/host/probe-verdict.js +143 -0
- package/dist/host/tool-scope.d.ts +32 -0
- package/dist/host/tool-scope.js +84 -0
- package/dist/host/unit-readiness.d.ts +7 -0
- package/dist/host/unit-readiness.js +5 -0
- package/dist/host/unit-registry.d.ts +9 -0
- package/dist/host/unit-registry.js +33 -0
- package/dist/host/version-range.d.ts +2 -0
- package/dist/host/version-range.js +73 -0
- package/dist/lib/ui.js +8 -1
- package/package.json +1 -1
- package/src/components/ChoiceGroup.css +49 -0
- package/src/components/ChoiceGroup.test.tsx +201 -0
- package/src/components/ChoiceGroup.tsx +112 -19
- package/src/lib/ui.test.ts +8 -0
- package/src/lib/ui.ts +8 -1
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.wb-choiceOption {
|
|
13
|
+
position: relative;
|
|
13
14
|
display: flex;
|
|
14
15
|
align-items: center;
|
|
15
16
|
gap: var(--wb-len-6);
|
|
@@ -22,6 +23,54 @@
|
|
|
22
23
|
color: var(--dsw-alias-label-primary);
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
.wb-choiceOptionOn {
|
|
27
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.wb-checkbox {
|
|
31
|
+
position: absolute;
|
|
32
|
+
width: var(--wb-len-16);
|
|
33
|
+
height: var(--wb-len-16);
|
|
34
|
+
margin: 0;
|
|
35
|
+
opacity: 0;
|
|
36
|
+
pointer-events: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.wb-checkboxBox {
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
flex: none;
|
|
45
|
+
width: var(--wb-len-16);
|
|
46
|
+
height: var(--wb-len-16);
|
|
47
|
+
border: 1px solid var(--dsw-alias-border-l3);
|
|
48
|
+
border-radius: var(--dsw-radius-xs);
|
|
49
|
+
corner-shape: round;
|
|
50
|
+
color: var(--dsw-alias-label-primary-foreground);
|
|
51
|
+
transition:
|
|
52
|
+
background-color var(--wb-dur-0-12) ease, border-color var(--wb-dur-0-12) ease;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.wb-choiceOption:hover .wb-checkboxBox {
|
|
56
|
+
border-color: var(--dsw-alias-label-tertiary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.wb-checkbox:checked + .wb-checkboxBox {
|
|
60
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
61
|
+
background: var(--dsw-alias-brand-primary);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.wb-checkbox:disabled + .wb-checkboxBox {
|
|
65
|
+
opacity: 0.5;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.wb-checkbox:focus-visible + .wb-checkboxBox {
|
|
69
|
+
outline: var(--dsw-focus-ring-width, var(--wb-len-2)) solid
|
|
70
|
+
var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
71
|
+
outline-offset: 2px;
|
|
72
|
+
}
|
|
73
|
+
|
|
25
74
|
.wb-choiceOption:hover {
|
|
26
75
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
27
76
|
}
|
|
@@ -4,12 +4,22 @@ export interface ChoiceGroupOption {
|
|
|
4
4
|
readonly value: string;
|
|
5
5
|
readonly label: ReactNode;
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
interface ChoiceGroupCommon {
|
|
8
8
|
readonly label: string;
|
|
9
9
|
readonly name: string;
|
|
10
|
-
readonly value: string;
|
|
11
10
|
readonly options: readonly ChoiceGroupOption[];
|
|
12
11
|
readonly disabled?: boolean | undefined;
|
|
12
|
+
}
|
|
13
|
+
export interface ChoiceGroupSingleProps extends ChoiceGroupCommon {
|
|
14
|
+
readonly multi?: false | undefined;
|
|
15
|
+
readonly value: string;
|
|
13
16
|
readonly onChange: (next: string) => void;
|
|
14
17
|
}
|
|
15
|
-
export
|
|
18
|
+
export interface ChoiceGroupMultiProps extends ChoiceGroupCommon {
|
|
19
|
+
readonly multi: true;
|
|
20
|
+
readonly values: readonly string[];
|
|
21
|
+
readonly onChange: (next: readonly string[]) => void;
|
|
22
|
+
}
|
|
23
|
+
export type ChoiceGroupProps = ChoiceGroupSingleProps | ChoiceGroupMultiProps;
|
|
24
|
+
export declare function ChoiceGroup(props: ChoiceGroupProps): JSX.Element;
|
|
25
|
+
export {};
|
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
import { Fragment, h } from '../lib/react.js';
|
|
2
|
+
import { IconCheckOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
2
3
|
import './ChoiceGroup.css';
|
|
3
|
-
|
|
4
|
-
return (h("div", { className: "wb-choiceMenu", role:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
h("
|
|
4
|
+
function ChoiceMenu(input) {
|
|
5
|
+
return (h("div", { className: "wb-choiceMenu", role: input.multi ? 'group' : 'radiogroup', "aria-label": input.label }, input.options.map((option) => {
|
|
6
|
+
const row = input.rowOf(option.value);
|
|
7
|
+
return (h(Fragment, { key: option.value },
|
|
8
|
+
h("label", { className: `wb-choiceOption${row.checked ? ' wb-choiceOptionOn' : ''}` },
|
|
9
|
+
h("input", { className: input.multi ? 'wb-checkbox' : 'wb-radio', type: input.multi ? 'checkbox' : 'radio', name: input.name, checked: row.checked, disabled: input.disabled, onChange: row.onSelect }),
|
|
10
|
+
input.multi ? (h("span", { className: "wb-checkboxBox", "aria-hidden": "true" }, row.checked ? h(IconCheckOutlineRegular, { size: 12 }) : null)) : null,
|
|
11
|
+
h("span", { className: "wb-choiceLabel" }, option.label))));
|
|
12
|
+
})));
|
|
13
|
+
}
|
|
14
|
+
export function ChoiceGroup(props) {
|
|
15
|
+
const common = {
|
|
16
|
+
label: props.label,
|
|
17
|
+
name: props.name,
|
|
18
|
+
options: props.options,
|
|
19
|
+
disabled: props.disabled === true,
|
|
20
|
+
};
|
|
21
|
+
if (props.multi === true) {
|
|
22
|
+
const values = props.values;
|
|
23
|
+
const onChange = props.onChange;
|
|
24
|
+
return (h(ChoiceMenu, { label: common.label, name: common.name, options: common.options, disabled: common.disabled, multi: true, rowOf: (value) => ({
|
|
25
|
+
checked: values.includes(value),
|
|
26
|
+
onSelect: () => {
|
|
27
|
+
onChange(values.includes(value)
|
|
28
|
+
? values.filter((one) => one !== value)
|
|
29
|
+
: [...values, value]);
|
|
30
|
+
},
|
|
31
|
+
}) }));
|
|
32
|
+
}
|
|
33
|
+
const value = props.value;
|
|
34
|
+
const onChange = props.onChange;
|
|
35
|
+
return (h(ChoiceMenu, { label: common.label, name: common.name, options: common.options, disabled: common.disabled, multi: false, rowOf: (option) => ({
|
|
36
|
+
checked: option === value,
|
|
37
|
+
onSelect: () => {
|
|
38
|
+
onChange(option);
|
|
39
|
+
},
|
|
40
|
+
}) }));
|
|
8
41
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { h } from '../lib/react.js';
|
|
2
|
+
export function StateDot({ state, size }) {
|
|
3
|
+
if (state === 'ongoing') {
|
|
4
|
+
return (h("svg", { className: "wb-loading", viewBox: "0 0 24 24", width: size, height: size, "aria-hidden": "true", style: { display: 'block' } },
|
|
5
|
+
h("g", { className: "wb-loadingMotion" },
|
|
6
|
+
h("circle", { className: "wb-loadingTrack", cx: 12, cy: 12, r: 9.5 }),
|
|
7
|
+
h("circle", { className: "wb-loadingArc", cx: 12, cy: 12, r: 9.5 }))));
|
|
8
|
+
}
|
|
9
|
+
return (h("span", { className: "wb-stateDot", "data-state": state, style: { width: size, height: size }, "aria-hidden": "true" }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { DshShell } from '../../api/dsh.js';
|
|
2
|
+
import type { Opencli } from '../../host/opencli.js';
|
|
3
|
+
export interface ShellCall {
|
|
4
|
+
command: string;
|
|
5
|
+
workdir?: string;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
onExpiry?: string;
|
|
8
|
+
stdoutMaxBytes?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ShellReply {
|
|
11
|
+
stdout?: string;
|
|
12
|
+
stderr?: string;
|
|
13
|
+
exitCode?: number | null;
|
|
14
|
+
timedOut?: boolean;
|
|
15
|
+
aborted?: boolean;
|
|
16
|
+
failWith?: Error;
|
|
17
|
+
}
|
|
18
|
+
export declare function createFakeShell(replies?: ShellReply[] | ((call: ShellCall, index: number) => ShellReply)): {
|
|
19
|
+
shell: DshShell;
|
|
20
|
+
calls: ShellCall[];
|
|
21
|
+
peak: () => number;
|
|
22
|
+
commands: () => string[];
|
|
23
|
+
};
|
|
24
|
+
export interface OpencliCall {
|
|
25
|
+
args: readonly string[];
|
|
26
|
+
key?: string;
|
|
27
|
+
workdir?: string;
|
|
28
|
+
refresh?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function createFakeOpencli(replies?: Array<unknown | Error> | ((call: OpencliCall, index: number) => unknown | Error)): {
|
|
31
|
+
opencli: Opencli;
|
|
32
|
+
calls: OpencliCall[];
|
|
33
|
+
keys: () => (string | undefined)[];
|
|
34
|
+
};
|
|
35
|
+
export declare function createFakeLogger(): {
|
|
36
|
+
logger: {
|
|
37
|
+
info: () => void;
|
|
38
|
+
warn: (message: string) => void;
|
|
39
|
+
};
|
|
40
|
+
warnings: string[];
|
|
41
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
const tick = () => delay(0);
|
|
3
|
+
export function createFakeShell(replies = []) {
|
|
4
|
+
const calls = [];
|
|
5
|
+
let running = 0;
|
|
6
|
+
let peak = 0;
|
|
7
|
+
const replyAt = (index) => typeof replies === 'function' ? replies(calls[index], index) : (replies[index] ?? {});
|
|
8
|
+
const shell = {
|
|
9
|
+
resolve(request) {
|
|
10
|
+
calls.push(request);
|
|
11
|
+
return request;
|
|
12
|
+
},
|
|
13
|
+
async execute() {
|
|
14
|
+
const index = calls.length - 1;
|
|
15
|
+
const reply = replyAt(index);
|
|
16
|
+
if (reply.failWith !== undefined)
|
|
17
|
+
throw reply.failWith;
|
|
18
|
+
running += 1;
|
|
19
|
+
peak = Math.max(peak, running);
|
|
20
|
+
return {
|
|
21
|
+
async result() {
|
|
22
|
+
await tick();
|
|
23
|
+
running -= 1;
|
|
24
|
+
return {
|
|
25
|
+
exitCode: reply.exitCode ?? 0,
|
|
26
|
+
timedOut: reply.timedOut ?? false,
|
|
27
|
+
aborted: reply.aborted ?? false,
|
|
28
|
+
stdout: { text: reply.stdout ?? '', truncated: false },
|
|
29
|
+
stderr: { text: reply.stderr ?? '', truncated: false },
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
shell,
|
|
37
|
+
calls,
|
|
38
|
+
peak: () => peak,
|
|
39
|
+
commands: () => calls.map((call) => call.command),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function createFakeOpencli(replies = []) {
|
|
43
|
+
const calls = [];
|
|
44
|
+
const opencli = {
|
|
45
|
+
async json(args, options = {}) {
|
|
46
|
+
const call = {
|
|
47
|
+
args,
|
|
48
|
+
...(options.key === undefined ? {} : { key: options.key }),
|
|
49
|
+
...(options.workdir === undefined ? {} : { workdir: options.workdir }),
|
|
50
|
+
};
|
|
51
|
+
calls.push(call);
|
|
52
|
+
const index = calls.length - 1;
|
|
53
|
+
const reply = typeof replies === 'function' ? replies(call, index) : (replies[index] ?? []);
|
|
54
|
+
if (reply instanceof Error)
|
|
55
|
+
throw reply;
|
|
56
|
+
return { value: reply, at: Date.now() };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
opencli,
|
|
61
|
+
calls,
|
|
62
|
+
keys: () => calls.map((call) => call.key),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function createFakeLogger() {
|
|
66
|
+
const warnings = [];
|
|
67
|
+
return {
|
|
68
|
+
logger: {
|
|
69
|
+
info: () => { },
|
|
70
|
+
warn: (message) => {
|
|
71
|
+
warnings.push(message);
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
warnings,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createFakeShell } from './fakes';
|
|
2
|
+
export declare const GOOD_BRIDGE_REPORT: string;
|
|
3
|
+
export declare const ALL_OK: {
|
|
4
|
+
stdout: string;
|
|
5
|
+
}[];
|
|
6
|
+
export declare const STALE_DAEMON_BRIDGE: string;
|
|
7
|
+
export declare const BROKEN_BRIDGE: string;
|
|
8
|
+
export declare function build(replies: Parameters<typeof createFakeShell>[0], install?: () => Promise<{
|
|
9
|
+
ok: boolean;
|
|
10
|
+
detail: string;
|
|
11
|
+
}>, warmReads?: () => readonly (readonly string[])[]): {
|
|
12
|
+
fake: {
|
|
13
|
+
shell: import("../..").DshShell;
|
|
14
|
+
calls: import("./fakes").ShellCall[];
|
|
15
|
+
peak: () => number;
|
|
16
|
+
commands: () => string[];
|
|
17
|
+
};
|
|
18
|
+
preflight: import("../preflight").Preflight;
|
|
19
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createPreflight } from '../preflight';
|
|
2
|
+
import { createFakeShell } from './fakes';
|
|
3
|
+
export const GOOD_BRIDGE_REPORT = [
|
|
4
|
+
'opencli v1.8.7 doctor (node v24.15.0)',
|
|
5
|
+
'',
|
|
6
|
+
'[OK] Daemon: running on port 19825 (v1.8.7)',
|
|
7
|
+
'[OK] Extension: connected (v1.0.24)',
|
|
8
|
+
'[OK] Connectivity: connected in 0.0s',
|
|
9
|
+
'',
|
|
10
|
+
'Everything looks good!',
|
|
11
|
+
].join('\n');
|
|
12
|
+
export const ALL_OK = [{ stdout: '1.8.8\n' }, { stdout: GOOD_BRIDGE_REPORT }];
|
|
13
|
+
export const STALE_DAEMON_BRIDGE = [
|
|
14
|
+
'opencli v1.8.7 doctor (node v24.15.0)',
|
|
15
|
+
'',
|
|
16
|
+
'[STALE] Daemon: running on port 19825 (v1.8.8 != CLI v1.8.7)',
|
|
17
|
+
'[OK] Extension: connected (v1.0.24)',
|
|
18
|
+
'[OK] Connectivity: connected in 0.0s',
|
|
19
|
+
'',
|
|
20
|
+
'Issues:',
|
|
21
|
+
' • Stale daemon detected: daemon v1.8.8 != CLI v1.8.7.',
|
|
22
|
+
' Run: opencli daemon restart',
|
|
23
|
+
].join('\n');
|
|
24
|
+
export const BROKEN_BRIDGE = [
|
|
25
|
+
'opencli v1.8.7 doctor (node v24.15.0)',
|
|
26
|
+
'',
|
|
27
|
+
'[MISSING] Daemon: not running',
|
|
28
|
+
'[MISSING] Extension: not connected',
|
|
29
|
+
'[FAIL] Connectivity: failed (daemon not reachable)',
|
|
30
|
+
'',
|
|
31
|
+
'Issues:',
|
|
32
|
+
" • Daemon is not running. It should start automatically when you run an opencli browser command.",
|
|
33
|
+
' • Daemon is running but the Chrome/Chromium extension is not connected.',
|
|
34
|
+
'If the extension is not installed:',
|
|
35
|
+
' 1. Download from https://github.com/jackwener/opencli/releases',
|
|
36
|
+
' 2. Open chrome://extensions/ → Enable Developer Mode',
|
|
37
|
+
' 3. Click "Load unpacked" → select the extension folder',
|
|
38
|
+
].join('\n');
|
|
39
|
+
export function build(replies, install, warmReads) {
|
|
40
|
+
const fake = createFakeShell(replies);
|
|
41
|
+
const preflight = createPreflight({
|
|
42
|
+
shell: fake.shell,
|
|
43
|
+
...(install === undefined ? {} : { install }),
|
|
44
|
+
...(warmReads === undefined ? {} : { warmReads }),
|
|
45
|
+
});
|
|
46
|
+
return { fake, preflight };
|
|
47
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ContextRef } from '@dsh-workbench/shared';
|
|
2
|
+
import type { ContextRenderer } from './unit-face';
|
|
3
|
+
export declare const CONTEXT_SOURCE_KIND = "workbench";
|
|
4
|
+
export interface ScannableMessage {
|
|
5
|
+
readonly source?: {
|
|
6
|
+
readonly kind?: string;
|
|
7
|
+
};
|
|
8
|
+
readonly content?: readonly {
|
|
9
|
+
readonly type?: string;
|
|
10
|
+
readonly text?: string;
|
|
11
|
+
}[];
|
|
12
|
+
}
|
|
13
|
+
export type { ContextRenderer };
|
|
14
|
+
export declare function collectContextRefs(messages: readonly ScannableMessage[]): ContextRef[];
|
|
15
|
+
export declare function buildContextMessage(text: string, summary: string): unknown;
|
|
16
|
+
export interface ContextInjector {
|
|
17
|
+
inject(messages: readonly ScannableMessage[], agent: unknown): Promise<unknown | undefined>;
|
|
18
|
+
}
|
|
19
|
+
export declare function createContextInjector(options: {
|
|
20
|
+
renderers: ReadonlyMap<string, ContextRenderer>;
|
|
21
|
+
logger?: {
|
|
22
|
+
warn?(message: string): void;
|
|
23
|
+
} | undefined;
|
|
24
|
+
}): ContextInjector;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { encodeRef, parseMarkers } from '@dsh-workbench/shared';
|
|
3
|
+
export const CONTEXT_SOURCE_KIND = 'workbench';
|
|
4
|
+
export function collectContextRefs(messages) {
|
|
5
|
+
const seen = new Set();
|
|
6
|
+
const refs = [];
|
|
7
|
+
for (const message of messages) {
|
|
8
|
+
if (message.source?.kind !== 'user')
|
|
9
|
+
continue;
|
|
10
|
+
for (const block of message.content ?? []) {
|
|
11
|
+
if (block.type !== 'text' || typeof block.text !== 'string')
|
|
12
|
+
continue;
|
|
13
|
+
for (const ref of parseMarkers(block.text)) {
|
|
14
|
+
const key = encodeRef(ref);
|
|
15
|
+
if (seen.has(key))
|
|
16
|
+
continue;
|
|
17
|
+
seen.add(key);
|
|
18
|
+
refs.push(ref);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return refs;
|
|
23
|
+
}
|
|
24
|
+
export function buildContextMessage(text, summary) {
|
|
25
|
+
return {
|
|
26
|
+
role: 'user',
|
|
27
|
+
id: randomUUID(),
|
|
28
|
+
content: [{ type: 'text', text }],
|
|
29
|
+
source: { kind: CONTEXT_SOURCE_KIND, form: 'notice', summary },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function createContextInjector(options) {
|
|
33
|
+
const done = new WeakMap();
|
|
34
|
+
return {
|
|
35
|
+
async inject(messages, agent) {
|
|
36
|
+
const refs = collectContextRefs(messages);
|
|
37
|
+
if (refs.length === 0)
|
|
38
|
+
return undefined;
|
|
39
|
+
const key = typeof agent === 'object' && agent !== null ? agent : undefined;
|
|
40
|
+
const seen = key === undefined ? undefined : (done.get(key) ?? new Set());
|
|
41
|
+
if (key !== undefined && seen !== undefined)
|
|
42
|
+
done.set(key, seen);
|
|
43
|
+
const fresh = [];
|
|
44
|
+
for (const ref of refs) {
|
|
45
|
+
const encoded = encodeRef(ref);
|
|
46
|
+
if (seen?.has(encoded) === true)
|
|
47
|
+
continue;
|
|
48
|
+
seen?.add(encoded);
|
|
49
|
+
fresh.push(ref);
|
|
50
|
+
}
|
|
51
|
+
if (fresh.length === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
const texts = [];
|
|
54
|
+
for (const ref of fresh) {
|
|
55
|
+
const render = options.renderers.get(ref.kind);
|
|
56
|
+
if (render === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
let text;
|
|
59
|
+
try {
|
|
60
|
+
text = await render(ref);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
options.logger?.warn?.(`[dsh-workbench] 渲染对话引用失败:${String(error)}`);
|
|
64
|
+
text = `工作台面板引用了一条内容(${encodeRef(ref)}),但当前渲染不出来(项目未选中或数据源不可用)。`;
|
|
65
|
+
}
|
|
66
|
+
if (text !== '')
|
|
67
|
+
texts.push(text);
|
|
68
|
+
}
|
|
69
|
+
if (texts.length === 0)
|
|
70
|
+
return undefined;
|
|
71
|
+
return buildContextMessage(texts.join('\n\n'), `工作台引用:${fresh.length} 条`);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DshHostContext, DshRequest, DshResponse } from '@dsh-workbench/shared';
|
|
2
|
+
import type { FactLog } from './facts';
|
|
3
|
+
export interface FactStream {
|
|
4
|
+
handler(req: DshRequest, res: DshResponse): void;
|
|
5
|
+
closeAll(): void;
|
|
6
|
+
size(): number;
|
|
7
|
+
}
|
|
8
|
+
export declare function createFactStream(log: FactLog): FactStream;
|
|
9
|
+
export declare function registerFactStream(ctx: DshHostContext, log: FactLog): () => void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { EVENTS_PATH } from '@dsh-workbench/shared';
|
|
2
|
+
const HEARTBEAT_MS = 25_000;
|
|
3
|
+
function frame(payload) {
|
|
4
|
+
return `data: ${JSON.stringify(payload)}\n\n`;
|
|
5
|
+
}
|
|
6
|
+
export function createFactStream(log) {
|
|
7
|
+
const connections = new Set();
|
|
8
|
+
const cleanups = new Map();
|
|
9
|
+
const drop = (res) => {
|
|
10
|
+
cleanups.get(res)?.();
|
|
11
|
+
cleanups.delete(res);
|
|
12
|
+
connections.delete(res);
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
size: () => connections.size,
|
|
16
|
+
closeAll() {
|
|
17
|
+
for (const res of [...connections]) {
|
|
18
|
+
drop(res);
|
|
19
|
+
res.end?.();
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
handler(req, res) {
|
|
23
|
+
if (req.method !== undefined && req.method !== 'GET') {
|
|
24
|
+
res.statusCode = 405;
|
|
25
|
+
res.end();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const url = req.url ?? '/';
|
|
29
|
+
const cut = url.indexOf('?');
|
|
30
|
+
const since = Number(new URLSearchParams(cut === -1 ? '' : url.slice(cut + 1)).get('since') ?? 0) || 0;
|
|
31
|
+
const opening = log.since(since);
|
|
32
|
+
if (typeof res.write !== 'function') {
|
|
33
|
+
res.statusCode = 200;
|
|
34
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
35
|
+
res.end(JSON.stringify({ ok: true, ...opening }));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const headers = {
|
|
39
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
40
|
+
'cache-control': 'no-store, no-transform',
|
|
41
|
+
connection: 'keep-alive',
|
|
42
|
+
};
|
|
43
|
+
if (typeof res.writeHead === 'function')
|
|
44
|
+
res.writeHead(200, headers);
|
|
45
|
+
else {
|
|
46
|
+
res.statusCode = 200;
|
|
47
|
+
for (const [name, value] of Object.entries(headers))
|
|
48
|
+
res.setHeader(name, value);
|
|
49
|
+
}
|
|
50
|
+
res.write(': connected\n\n');
|
|
51
|
+
res.write(frame({ type: 'sync', ...opening }));
|
|
52
|
+
connections.add(res);
|
|
53
|
+
const timer = setInterval(() => res.write?.(': ping\n\n'), HEARTBEAT_MS);
|
|
54
|
+
const unsubscribe = log.subscribe((change) => res.write?.(frame({ type: 'bump', ...change })));
|
|
55
|
+
cleanups.set(res, () => {
|
|
56
|
+
clearInterval(timer);
|
|
57
|
+
unsubscribe();
|
|
58
|
+
});
|
|
59
|
+
res.on?.('close', () => drop(res));
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function registerFactStream(ctx, log) {
|
|
64
|
+
const stream = createFactStream(log);
|
|
65
|
+
const unregister = ctx.webServer.register({ kind: 'exact', path: EVENTS_PATH, handler: stream.handler });
|
|
66
|
+
return () => {
|
|
67
|
+
stream.closeAll();
|
|
68
|
+
unregister();
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { DshHostContext } from '@dsh-workbench/shared';
|
|
2
|
+
import type { UnitManifest } from '@dsh-workbench/shared';
|
|
3
|
+
import { type RouteTable } from './http';
|
|
4
|
+
import type { ContextRenderer, UnitHostFace, UnitMountDeps, UnitMountResult, UnitCall } from './unit-face';
|
|
5
|
+
import { type UnitReadiness } from './unit-readiness';
|
|
6
|
+
export interface MountedUnit {
|
|
7
|
+
readonly face: UnitHostFace;
|
|
8
|
+
readonly result: UnitMountResult;
|
|
9
|
+
readonly readiness: UnitReadiness;
|
|
10
|
+
}
|
|
11
|
+
export interface UnitsPlan {
|
|
12
|
+
readonly mounted: readonly MountedUnit[];
|
|
13
|
+
readonly routes: RouteTable;
|
|
14
|
+
readonly gate: UnitMountResult['gate'];
|
|
15
|
+
readonly prefs: ReadonlyArray<{
|
|
16
|
+
readonly resource: string;
|
|
17
|
+
readonly unit: string;
|
|
18
|
+
handle(payload: unknown): unknown;
|
|
19
|
+
}>;
|
|
20
|
+
readonly actions: ReadonlyArray<{
|
|
21
|
+
readonly resource: string;
|
|
22
|
+
readonly unit: string;
|
|
23
|
+
handle(payload: unknown): unknown;
|
|
24
|
+
}>;
|
|
25
|
+
readonly provision: UnitMountResult['provision'];
|
|
26
|
+
readonly contextRenderers: ReadonlyMap<string, ContextRenderer>;
|
|
27
|
+
readonly owners: ReadonlyMap<string, string>;
|
|
28
|
+
readonly routesOfUnit: ReadonlyMap<string, readonly string[]>;
|
|
29
|
+
readonly factsOf: ReadonlyMap<string, (call: UnitCall) => readonly string[] | undefined>;
|
|
30
|
+
readonly skipped: ReadonlyArray<{
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly missing: readonly string[];
|
|
33
|
+
}>;
|
|
34
|
+
readonly warmReads: () => readonly (readonly string[])[];
|
|
35
|
+
readonly disposeLongTasks: () => void;
|
|
36
|
+
}
|
|
37
|
+
export declare function mountUnits(faces: readonly UnitHostFace[], deps: {
|
|
38
|
+
ctx: DshHostContext;
|
|
39
|
+
logger?: {
|
|
40
|
+
info?(m: string): void;
|
|
41
|
+
warn?(m: string): void;
|
|
42
|
+
} | undefined;
|
|
43
|
+
manifests: () => readonly UnitManifest[];
|
|
44
|
+
opencliCommand?: string;
|
|
45
|
+
probe?: UnitMountDeps['probe'];
|
|
46
|
+
store?: UnitMountDeps['store'];
|
|
47
|
+
values?: UnitMountDeps['values'];
|
|
48
|
+
longTasks?: {
|
|
49
|
+
readonly scope: string;
|
|
50
|
+
readonly bump: (facts: readonly string[]) => void;
|
|
51
|
+
} | undefined;
|
|
52
|
+
}): UnitsPlan;
|