@khorsheed/dsh-local-agent-dsh 0.1.0-rc.6
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.en.md +150 -0
- package/README.i18n.yaml +6 -0
- package/README.md +151 -0
- package/cordis.patch.yml +24 -0
- package/lib/client.js +1115 -0
- package/lib/dsh-cli-provider-UhWvivte.js +1701 -0
- package/lib/dsh-cli-provider.js +3 -0
- package/lib/index.js +624 -0
- package/lib/invariant.js +33 -0
- package/lib/provision.js +602 -0
- package/lib/records.js +109 -0
- package/lib/session-log-DOsSz56-.js +88 -0
- package/lib/types/client/SettingsCard.d.ts +78 -0
- package/lib/types/client/SettingsCard.js +200 -0
- package/lib/types/client/icons.d.ts +18 -0
- package/lib/types/client/icons.js +2 -0
- package/lib/types/client/index.d.ts +45 -0
- package/lib/types/client/index.js +88 -0
- package/lib/types/client/locales.d.ts +76 -0
- package/lib/types/client/locales.js +67 -0
- package/lib/types/dsh-cli-provider.d.ts +213 -0
- package/lib/types/dsh-cli-provider.js +733 -0
- package/lib/types/index.d.ts +124 -0
- package/lib/types/index.js +347 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +37 -0
- package/lib/types/live-driver.d.ts +180 -0
- package/lib/types/live-driver.js +887 -0
- package/lib/types/live-switch.d.ts +76 -0
- package/lib/types/live-switch.js +100 -0
- package/lib/types/model-broker.d.ts +86 -0
- package/lib/types/model-broker.js +149 -0
- package/lib/types/model-catalog.d.ts +27 -0
- package/lib/types/model-catalog.js +71 -0
- package/lib/types/provision.d.ts +382 -0
- package/lib/types/provision.js +656 -0
- package/lib/types/records.d.ts +41 -0
- package/lib/types/records.js +125 -0
- package/lib/types/session-log.d.ts +58 -0
- package/lib/types/session-log.js +83 -0
- package/lib/types/session-mirror.d.ts +155 -0
- package/lib/types/session-mirror.js +451 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +121 -0
package/lib/records.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { t as resolveDshSessionLog } from "./session-log-DOsSz56-.js";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
4
|
+
import { zstdDecompressSync } from "node:zlib";
|
|
5
|
+
//#region lib/types/records.js
|
|
6
|
+
/**
|
|
7
|
+
* Session records adapter for the `dsh` harness: list the sub-dsh's own
|
|
8
|
+
* sessions from its scoped-home store. The sub-dsh persists JSONL logs under
|
|
9
|
+
* `<scoped home>/sessions/<project>/<session-id>/` (the same physical layout
|
|
10
|
+
* the official JSONL persistence backend writes), so the adapter reads headers
|
|
11
|
+
* directly instead of asking the parent's persistence service, which is bound
|
|
12
|
+
* to the parent's own store root. WHICH file in that directory holds the log
|
|
13
|
+
* is the host's generation choice, resolved through the same
|
|
14
|
+
* {@link resolveDshSessionLog} the session mirror uses — the two readers must
|
|
15
|
+
* never disagree about where a session's history lives.
|
|
16
|
+
* @module @khorsheed/dsh-local-agent-dsh/records
|
|
17
|
+
*/
|
|
18
|
+
/** Parse the header line of a session log; undefined when it is not one. */
|
|
19
|
+
function parseDshSessionHeader(firstLine) {
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(firstLine);
|
|
23
|
+
} catch {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
27
|
+
const header = parsed;
|
|
28
|
+
if (header.type !== "session" || typeof header.id !== "string") return void 0;
|
|
29
|
+
return {
|
|
30
|
+
id: header.id,
|
|
31
|
+
...typeof header.cwd === "string" ? { cwd: header.cwd } : {},
|
|
32
|
+
...typeof header.createdAt === "number" ? { createdAt: header.createdAt } : {}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Whether an fs error is a missing entry. */
|
|
36
|
+
function isENOENT(error) {
|
|
37
|
+
return error.code === "ENOENT";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read a session log's first line (its header). The JSONL backend stores one
|
|
41
|
+
* zstd frame per append batch; node's zstdDecompress decodes the first frame,
|
|
42
|
+
* which holds the header line written at session creation.
|
|
43
|
+
* @param logPath - the session log file to read (any generation, compressed
|
|
44
|
+
* or raw — the caller resolved which one).
|
|
45
|
+
* @param compressed - whether the file is zstd-compressed; a raw log is read
|
|
46
|
+
* as text. Defaults to compressed, the backend's own default.
|
|
47
|
+
* @returns the first line, or undefined when the file is empty/half-written.
|
|
48
|
+
*/
|
|
49
|
+
async function readDshSessionHeaderLine(logPath, compressed = true) {
|
|
50
|
+
let buffer;
|
|
51
|
+
try {
|
|
52
|
+
buffer = await readFile(logPath);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (isENOENT(error)) return void 0;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
if (buffer.length === 0) return void 0;
|
|
58
|
+
let text;
|
|
59
|
+
try {
|
|
60
|
+
text = compressed ? zstdDecompressSync(buffer).toString("utf8") : buffer.toString("utf8");
|
|
61
|
+
} catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const firstLine = text.split("\n", 1)[0];
|
|
65
|
+
return firstLine === void 0 ? void 0 : firstLine;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* List the sessions recorded in the harness scoped home, in the store's own
|
|
69
|
+
* order (project directories first, then session directories).
|
|
70
|
+
* @param homeDir - the `dsh` harness's scoped home (its `$DSH_HOME` root).
|
|
71
|
+
* @returns the session records; an absent store yields an empty list.
|
|
72
|
+
*/
|
|
73
|
+
async function listDshSessions(homeDir) {
|
|
74
|
+
const root = join(homeDir, "sessions");
|
|
75
|
+
let projects;
|
|
76
|
+
try {
|
|
77
|
+
projects = await readdir(root);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (isENOENT(error)) return [];
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
const records = [];
|
|
83
|
+
for (const project of projects) {
|
|
84
|
+
if (!project.startsWith("--")) continue;
|
|
85
|
+
let sessionIds;
|
|
86
|
+
try {
|
|
87
|
+
sessionIds = await readdir(join(root, project));
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (isENOENT(error)) continue;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
for (const sessionId of sessionIds) {
|
|
93
|
+
const log = await resolveDshSessionLog(join(root, project, sessionId));
|
|
94
|
+
if (log === void 0) continue;
|
|
95
|
+
const headerLine = await readDshSessionHeaderLine(log.path, log.compressed);
|
|
96
|
+
if (headerLine === void 0) continue;
|
|
97
|
+
const header = parseDshSessionHeader(headerLine);
|
|
98
|
+
if (header === void 0) continue;
|
|
99
|
+
records.push({
|
|
100
|
+
id: header.id,
|
|
101
|
+
...header.cwd !== void 0 ? { workDir: header.cwd } : { workDir: project },
|
|
102
|
+
...header.createdAt !== void 0 ? { startedAt: header.createdAt } : {}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return records;
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { listDshSessions, parseDshSessionHeader, readDshSessionHeaderLine };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
//#region lib/types/session-log.js
|
|
4
|
+
/**
|
|
5
|
+
* Locating one sub-dsh session's log file inside its session directory.
|
|
6
|
+
*
|
|
7
|
+
* The host's JSONL persistence backend addresses its logs by SESSION FORMAT
|
|
8
|
+
* GENERATION: version zero keeps the original `session.jsonl`, and every later
|
|
9
|
+
* generation carries a lowercase `vN` component — `session.v3.jsonl` on host
|
|
10
|
+
* 0.1.5. Either basename may additionally carry `.zstd` for the compressed
|
|
11
|
+
* default. The backend itself "selects the numerically highest canonical
|
|
12
|
+
* generation", and so does this module: a reader that hardcodes one filename
|
|
13
|
+
* reads nothing the moment the host advances a generation, which is exactly
|
|
14
|
+
* how a 0.1.5 sub-dsh round lost its observed model, its usage and its tool
|
|
15
|
+
* calls at once while still settling `completed`.
|
|
16
|
+
*
|
|
17
|
+
* Both readers in this package — the session mirror's read-back and the
|
|
18
|
+
* `/dsh sessions` records adapter — resolve through here, so the two can never
|
|
19
|
+
* disagree about which file a session's history lives in.
|
|
20
|
+
* @module @khorsheed/dsh-local-agent-dsh/session-log
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The canonical raw-log basename, matched AFTER any compression suffix is
|
|
24
|
+
* removed. Deliberately the host's own rule, character for character
|
|
25
|
+
* (`@deepseek-ai/dsh-session-format`): `.v0`, leading zeros, uppercase and
|
|
26
|
+
* temporary names are NOT canonical generations, and a reader that accepted
|
|
27
|
+
* them would read a file the writer never published.
|
|
28
|
+
*/
|
|
29
|
+
const CANONICAL_LOG_FILENAME = /^session(?:\.v([1-9][0-9]*))?\.jsonl$/u;
|
|
30
|
+
/**
|
|
31
|
+
* Read the generation one session-directory entry names.
|
|
32
|
+
* @param filename - one basename from a session directory.
|
|
33
|
+
* @returns its generation and encoding, or undefined when the name is not a
|
|
34
|
+
* canonical log (`session.lock`, a temporary file, `session.v0.jsonl`, …).
|
|
35
|
+
*/
|
|
36
|
+
function parseDshSessionLogFilename(filename) {
|
|
37
|
+
const compressed = filename.endsWith(".zstd");
|
|
38
|
+
const raw = compressed ? filename.slice(0, -5) : filename;
|
|
39
|
+
const match = CANONICAL_LOG_FILENAME.exec(raw);
|
|
40
|
+
if (match === null) return void 0;
|
|
41
|
+
if (match[1] === void 0) return {
|
|
42
|
+
version: 0,
|
|
43
|
+
compressed
|
|
44
|
+
};
|
|
45
|
+
const version = Number(match[1]);
|
|
46
|
+
return Number.isSafeInteger(version) ? {
|
|
47
|
+
version,
|
|
48
|
+
compressed
|
|
49
|
+
} : void 0;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Choose the log file of one session directory: the numerically HIGHEST
|
|
53
|
+
* canonical generation present, preferring the compressed artifact when a
|
|
54
|
+
* generation somehow has both (the backend writes one per its configured
|
|
55
|
+
* encoding, so the pair only appears when a store changed settings).
|
|
56
|
+
*
|
|
57
|
+
* A directory with no canonical log — including one that holds only a
|
|
58
|
+
* `session.lock` — yields undefined. Absence is reported, never guessed: a
|
|
59
|
+
* caller that cannot find a log records nothing rather than inventing an empty
|
|
60
|
+
* history.
|
|
61
|
+
* @param dir - the session-owned directory.
|
|
62
|
+
* @returns the chosen log, or undefined when the directory holds none.
|
|
63
|
+
*/
|
|
64
|
+
async function resolveDshSessionLog(dir) {
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = await readdir(dir);
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
let best;
|
|
72
|
+
for (const filename of entries) {
|
|
73
|
+
const parsed = parseDshSessionLogFilename(filename);
|
|
74
|
+
if (parsed === void 0) continue;
|
|
75
|
+
if (best !== void 0) {
|
|
76
|
+
if (parsed.version < best.version) continue;
|
|
77
|
+
if (parsed.version === best.version && !(parsed.compressed && !best.compressed)) continue;
|
|
78
|
+
}
|
|
79
|
+
best = {
|
|
80
|
+
path: join(dir, filename),
|
|
81
|
+
filename,
|
|
82
|
+
...parsed
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return best;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
export { resolveDshSessionLog as t };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { HarnessModelPickerInput } from '@khorsheed/dsh-local-agent/client';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type { SettingsScope } from '@khorsheed/dsh-local-agent/src/client/settings-scope.ts';
|
|
4
|
+
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
|
+
import { type ProviderAuthBlockProps, type ProviderAuthInjected } from '@khorsheed/dsh-local-agent/src/client/ProviderAuthBlock.tsx';
|
|
6
|
+
import type { LocalAgentModelInfo } from '@khorsheed/dsh-local-agent/types';
|
|
7
|
+
import { NS } from './locales.ts';
|
|
8
|
+
/** The `local-agent-dsh` settings namespace section the card edits. */
|
|
9
|
+
export interface DshCardSettings {
|
|
10
|
+
/** Whether the dsh harness, provider, and delegation tool are registered. */
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
/** Resident (live) mode: one resident sub-dsh serve process per member. */
|
|
13
|
+
live?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The model every delegation round starts the sub-dsh with, spelled
|
|
16
|
+
* `provider/model`. Absent (the field cleared) means the plugin passes no
|
|
17
|
+
* `--model` at all — the host instance's own default selection decides.
|
|
18
|
+
*/
|
|
19
|
+
model?: string;
|
|
20
|
+
/** Model identifiers saved before; the input's suggestions, capped at five. */
|
|
21
|
+
recentModels?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
/** Injected face of the dsh settings card. */
|
|
24
|
+
export interface DshSettingsCardInjected {
|
|
25
|
+
renderModelPicker?: ((props: HarnessModelPickerInput) => ReactNode) | undefined;
|
|
26
|
+
/** Bound settings scope for the local-agent-dsh namespace. */
|
|
27
|
+
scope: SettingsScope<DshCardSettings>;
|
|
28
|
+
/** The auth block's query/command faces, backed by the family core Remote. */
|
|
29
|
+
auth: ProviderAuthInjected;
|
|
30
|
+
/** Translate bound to the family core's `local-agent` namespace (the auth block's copy lives there). */
|
|
31
|
+
authT: ProviderAuthBlockProps['t'];
|
|
32
|
+
/**
|
|
33
|
+
* The harness's memberless model surface (the core gateway's `harnessModel`
|
|
34
|
+
* Remote): the inherited default an unset field displays and the menu's
|
|
35
|
+
* choice vocabulary. Undefined or null when the gateway or the broker is
|
|
36
|
+
* absent — the card then keeps the bare input exactly as before brokers
|
|
37
|
+
* existed.
|
|
38
|
+
*/
|
|
39
|
+
harnessModel: () => Promise<LocalAgentModelInfo | null | undefined>;
|
|
40
|
+
hooks: {
|
|
41
|
+
/** The same scope as a hooks source, so the card reacts to external writes. */
|
|
42
|
+
settings: SettingsScope<DshCardSettings>;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Full props of the 0.1.5 settings.plugin.item card entry. The removed slot's
|
|
47
|
+
* owner share was intentionally empty and its root scope delivered the global
|
|
48
|
+
* seats; the structural type below is exact about the one seat the auth block
|
|
49
|
+
* consumes (the slot name is gone from the alpha.2 SlotMap, so the
|
|
50
|
+
* registration goes through a duck-typed narrow — see client/index.ts).
|
|
51
|
+
*/
|
|
52
|
+
export type DshSettingsCardProps = InjectFace<DshSettingsCardInjected> & PropsLocale<typeof NS> & {
|
|
53
|
+
/** The standing root-scope global seat (ui-session's merge), delivered to the 0.1.5 card by the renderer. */
|
|
54
|
+
useSessions: ProviderAuthBlockProps['useSessions'];
|
|
55
|
+
};
|
|
56
|
+
/** Full props of the alpha.2 plugins.bundle.config entry (owner prop `view`). */
|
|
57
|
+
export type DshBundleConfigProps = PropsRuntime<'plugins.bundle.config'> & InjectFace<DshSettingsCardInjected> & PropsLocale<typeof NS>;
|
|
58
|
+
/**
|
|
59
|
+
* The dsh harness settings card in the 0.1.5 plugin configuration tab: the
|
|
60
|
+
* same collapsible chrome the official plugin cards use, re-implemented
|
|
61
|
+
* locally (the bundle-purity gate forbids value-importing the official chrome
|
|
62
|
+
* — the context-guard pattern), with the at-a-glance credential dot in the
|
|
63
|
+
* header. Explanation copy lives in hover/focus bubbles behind ⓘ anchors —
|
|
64
|
+
* the card's rows stay one line each.
|
|
65
|
+
* @param props - the injected scope/auth faces and copy.
|
|
66
|
+
* @returns the card.
|
|
67
|
+
*/
|
|
68
|
+
export declare function DshSettingsCard(props: DshSettingsCardProps): import("react").JSX.Element;
|
|
69
|
+
/**
|
|
70
|
+
* The bundle's configuration on the alpha.2 Plugins page
|
|
71
|
+
* (`plugins.bundle.config`, keyed by package name): the page draws the title,
|
|
72
|
+
* the icon, and the crumb itself, so the entry renders the one-liner for the
|
|
73
|
+
* `summary` view and the bare form — with its own save control — for `page`.
|
|
74
|
+
* @param props - the owner view, the injected scope/auth faces, and copy.
|
|
75
|
+
* @returns the entry.
|
|
76
|
+
*/
|
|
77
|
+
export declare function DshBundleConfig({ view, useSettings, scope, auth, authT, renderModelPicker, harnessModel, useSessions, t }: DshBundleConfigProps): import("react").JSX.Element;
|
|
78
|
+
//# sourceMappingURL=SettingsCard.d.ts.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
|
+
import { IconChevronDownOutlineMedium } from './icons.js';
|
|
5
|
+
// The shared auth block is bundled from the family core's source — the
|
|
6
|
+
// sanctioned core/companion edge (the host half already depends on the core);
|
|
7
|
+
// the block carries no runtime identity to share.
|
|
8
|
+
import { ProviderAuthBlock, } from '@khorsheed/dsh-local-agent/src/client/ProviderAuthBlock.tsx';
|
|
9
|
+
import { AuthStatusDot } from '@khorsheed/dsh-local-agent/src/client/AuthStatusDot.tsx';
|
|
10
|
+
import { useHarnessAuthStatus } from '@khorsheed/dsh-local-agent/src/client/auth-status.ts';
|
|
11
|
+
import css from './SettingsCard.module.css';
|
|
12
|
+
/** How many previously-saved model identifiers the input suggests. */
|
|
13
|
+
const RECENT_MODEL_LIMIT = 5;
|
|
14
|
+
/**
|
|
15
|
+
* The staged card state both settings surfaces share: the delegation,
|
|
16
|
+
* resident-mode, and model edits write through the bound settingsScope
|
|
17
|
+
* immediately (revision-fenced), so a flip takes effect on the next
|
|
18
|
+
* delegation round without a reload; the harness's model surface is fetched
|
|
19
|
+
* while the surface is active (the 0.1.5 card opened, the alpha.2 page
|
|
20
|
+
* mounted) and re-fetched after every save.
|
|
21
|
+
* @param active - whether the surface currently shows the body (gates the model-surface fetch).
|
|
22
|
+
* @param useSettings - the bound settings hook from the inject hooks compartment.
|
|
23
|
+
* @param scope - the bound settings scope the writes go through.
|
|
24
|
+
* @param harnessModel - the harness's memberless model surface read.
|
|
25
|
+
* @param t - copy.
|
|
26
|
+
*/
|
|
27
|
+
function useCardState(active, useSettings, scope, harnessModel, t) {
|
|
28
|
+
const snapshot = useSettings(value => value);
|
|
29
|
+
const [saved, setSaved] = useState(false);
|
|
30
|
+
const [error, setError] = useState(false);
|
|
31
|
+
const ready = snapshot.status === 'ready' && snapshot.writable;
|
|
32
|
+
const enabled = snapshot.value?.enabled ?? false;
|
|
33
|
+
const live = snapshot.value?.live ?? false;
|
|
34
|
+
// The model input is a DRAFT until saved: `null` means "showing whatever the
|
|
35
|
+
// scope holds", so an external write (another tab, a YAML reload) still
|
|
36
|
+
// reaches the field while the user is not typing in it.
|
|
37
|
+
const storedModel = snapshot.value?.model ?? '';
|
|
38
|
+
const recentModels = snapshot.value?.recentModels ?? [];
|
|
39
|
+
const [modelDraft, setModelDraft] = useState(null);
|
|
40
|
+
const [modelSaved, setModelSaved] = useState(false);
|
|
41
|
+
const [modelError, setModelError] = useState(false);
|
|
42
|
+
const modelValue = modelDraft ?? storedModel;
|
|
43
|
+
// The harness's model surface, fetched while the surface is active (and
|
|
44
|
+
// re-fetched after a save): undefined until the first answer, null when the
|
|
45
|
+
// gateway or the broker is absent — the card then keeps the bare input with
|
|
46
|
+
// its recent-models suggestions, exactly as before brokers existed.
|
|
47
|
+
const [modelInfo, setModelInfo] = useState(undefined);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!active)
|
|
50
|
+
return;
|
|
51
|
+
let stale = false;
|
|
52
|
+
void harnessModel().then((info) => {
|
|
53
|
+
if (!stale)
|
|
54
|
+
setModelInfo(info ?? null);
|
|
55
|
+
});
|
|
56
|
+
return () => { stale = true; };
|
|
57
|
+
}, [active, harnessModel, modelSaved]);
|
|
58
|
+
/**
|
|
59
|
+
* Commit the model field. A blank value UNSETS the key rather than storing
|
|
60
|
+
* an empty string, so the field re-inherits the YAML composition base — and
|
|
61
|
+
* with no base, the harness is back to passing no model at all.
|
|
62
|
+
*/
|
|
63
|
+
const saveModel = () => {
|
|
64
|
+
const next = modelValue.trim();
|
|
65
|
+
setModelSaved(false);
|
|
66
|
+
setModelError(false);
|
|
67
|
+
const write = next === '' ? scope.unset('model') : scope.set('model', next);
|
|
68
|
+
void write.then(async () => {
|
|
69
|
+
// Most-recent-first, deduplicated, capped: the suggestion list is a
|
|
70
|
+
// memory of what this instance has actually run, never a catalog.
|
|
71
|
+
if (next !== '') {
|
|
72
|
+
await scope.set('recentModels', [next, ...recentModels.filter(value => value !== next)].slice(0, RECENT_MODEL_LIMIT));
|
|
73
|
+
}
|
|
74
|
+
setModelDraft(null);
|
|
75
|
+
setModelSaved(true);
|
|
76
|
+
}, () => { setModelError(true); });
|
|
77
|
+
};
|
|
78
|
+
const write = (field, value) => {
|
|
79
|
+
setSaved(false);
|
|
80
|
+
setError(false);
|
|
81
|
+
void scope.set(field, value).then(() => { setSaved(true); }, () => { setError(true); });
|
|
82
|
+
};
|
|
83
|
+
// The suggestion vocabulary: the broker's deduped union (settings + host
|
|
84
|
+
// default selection + recent) when the surface answered, the card's own
|
|
85
|
+
// recent-models memory when it did not. Never a hardcoded catalog. A
|
|
86
|
+
// non-empty list turns the field into a select-like control: the
|
|
87
|
+
// always-visible chevron opens the same vocabulary as a menu.
|
|
88
|
+
const choices = modelInfo?.choices ?? recentModels;
|
|
89
|
+
const [modelMenuOpen, setModelMenuOpen] = useState(false);
|
|
90
|
+
const [modelMenuUp, setModelMenuUp] = useState(false);
|
|
91
|
+
const modelFieldRef = useRef(null);
|
|
92
|
+
// Click-outside and Esc close the model menu (the member composer's picker
|
|
93
|
+
// pattern); the upward flip is measured when the menu opens.
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!modelMenuOpen)
|
|
96
|
+
return;
|
|
97
|
+
const onPointerDown = (event) => {
|
|
98
|
+
if (modelFieldRef.current?.contains(event.target) === true)
|
|
99
|
+
return;
|
|
100
|
+
setModelMenuOpen(false);
|
|
101
|
+
};
|
|
102
|
+
const onKeyDown = (event) => {
|
|
103
|
+
if (event.key === 'Escape')
|
|
104
|
+
setModelMenuOpen(false);
|
|
105
|
+
};
|
|
106
|
+
document.addEventListener('mousedown', onPointerDown);
|
|
107
|
+
document.addEventListener('keydown', onKeyDown);
|
|
108
|
+
return () => {
|
|
109
|
+
document.removeEventListener('mousedown', onPointerDown);
|
|
110
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
111
|
+
};
|
|
112
|
+
}, [modelMenuOpen]);
|
|
113
|
+
const toggleModelMenu = () => {
|
|
114
|
+
if (!modelMenuOpen) {
|
|
115
|
+
const rect = modelFieldRef.current?.getBoundingClientRect();
|
|
116
|
+
if (rect !== undefined) {
|
|
117
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
118
|
+
setModelMenuUp(spaceBelow < 260 && rect.top > spaceBelow);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
setModelMenuOpen(open => !open);
|
|
122
|
+
};
|
|
123
|
+
// An unset field DISPLAYS the default it follows (dimmed, as the input's
|
|
124
|
+
// placeholder — inherited, never pinned), down the chain the broker
|
|
125
|
+
// reports: the host's default-model selection, then a cli-builtin source
|
|
126
|
+
// that still names its default (a harness catalog's isDefault entry), then
|
|
127
|
+
// the last observed model the records know, and last the generic copy.
|
|
128
|
+
const inheritedModel = modelInfo != null && modelInfo.cliDefault !== undefined && modelInfo.cliDefault !== ''
|
|
129
|
+
? modelInfo.cliDefault
|
|
130
|
+
: modelInfo != null && modelInfo.source === 'cli-builtin' && modelInfo.effective !== undefined && modelInfo.effective !== ''
|
|
131
|
+
? modelInfo.effective
|
|
132
|
+
: modelInfo != null && modelInfo.lastObserved !== undefined && modelInfo.lastObserved !== ''
|
|
133
|
+
? modelInfo.lastObserved
|
|
134
|
+
: undefined;
|
|
135
|
+
const modelPlaceholder = inheritedModel ?? t('model.placeholder');
|
|
136
|
+
// The menu's leading item: the same chain, spelled as the follow-default
|
|
137
|
+
// choice. It reads checked while the field is unset, and picking it clears
|
|
138
|
+
// the draft back to follow-default (a save then unsets the key).
|
|
139
|
+
const modelDefaultItem = modelInfo != null && modelInfo.cliDefault !== undefined && modelInfo.cliDefault !== ''
|
|
140
|
+
? t('model.menuDefault.cli-config', { model: modelInfo.cliDefault })
|
|
141
|
+
: modelInfo != null && modelInfo.source === 'cli-builtin' && modelInfo.effective !== undefined && modelInfo.effective !== ''
|
|
142
|
+
? t('model.menuDefault.cli-builtin', { model: modelInfo.effective })
|
|
143
|
+
: modelInfo != null && modelInfo.lastObserved !== undefined && modelInfo.lastObserved !== ''
|
|
144
|
+
? t('model.menuDefault.last-observed', { model: modelInfo.lastObserved })
|
|
145
|
+
: t('model.menuDefault');
|
|
146
|
+
return {
|
|
147
|
+
snapshot, saved, error, ready, enabled, live, storedModel, modelValue,
|
|
148
|
+
modelSaved, modelError, choices, modelMenuOpen, modelMenuUp, modelFieldRef,
|
|
149
|
+
modelPlaceholder, modelDefaultItem, saveModel, write, toggleModelMenu,
|
|
150
|
+
setModelDraft, setModelMenuOpen,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** The card body both settings surfaces share: the auth block, the delegation switch, the default-model block, and the resident-mode block. */
|
|
154
|
+
function CardBody({ state, auth, authT, renderModelPicker, useSessions, t }) {
|
|
155
|
+
const { snapshot, saved, error, ready, enabled, live, storedModel, modelValue, modelSaved, modelError, choices, modelMenuOpen, modelMenuUp, modelFieldRef, modelPlaceholder, modelDefaultItem, saveModel, write, toggleModelMenu, setModelDraft, setModelMenuOpen, } = state;
|
|
156
|
+
// The free-text input, single-sourced for both presentations: bare when no
|
|
157
|
+
// vocabulary exists (a fresh install degrades to exactly the pre-picker
|
|
158
|
+
// field), or inside the select-like field next to its chevron. No datalist:
|
|
159
|
+
// the chevron menu is the single choice list.
|
|
160
|
+
const modelInputElement = (_jsx("input", { type: "text", className: css.modelInput, value: modelValue, placeholder: modelPlaceholder, "aria-label": t('model.title'), disabled: !ready, onChange: (event) => { setModelDraft(event.target.value); } }));
|
|
161
|
+
return (_jsxs("div", { className: css.body, children: [_jsxs("section", { className: css.block, children: [_jsx("h3", { className: css.blockTitle, children: t('auth.title') }), _jsx(ProviderAuthBlock, { harness: { id: 'dsh', label: 'dsh' }, useSessions: useSessions, status: auth.status, runCommand: auth.runCommand, t: authT }), _jsx("span", { className: css.hint, children: t('auth.host') })] }), _jsxs("section", { className: css.block, children: [_jsx("h3", { className: css.blockTitle, children: t('enable.title') }), _jsxs("div", { className: css.row, children: [_jsx("span", { className: css.rowLabel, children: t('enable.switch') }), _jsx("button", { type: "button", role: "switch", "aria-checked": enabled, "aria-label": t('enable.switch.aria'), className: enabled ? `${css.switch} ${css.switchOn}` : css.switch, disabled: !ready, onClick: () => { write('enabled', !enabled); }, children: _jsx("span", { className: css.knob }) })] }), _jsx("span", { className: css.hint, children: t(enabled ? 'enable.on' : 'enable.off') })] }), _jsxs("section", { className: css.block, children: [_jsxs("h3", { className: css.blockTitle, children: [t('model.title'), _jsx(Tooltip, { label: t('model.info'), side: "bottom", maxWidth: 360, children: _jsx("button", { type: "button", className: css.info, "aria-label": t('model.info.aria'), children: "\u24D8" }) })] }), _jsxs("div", { className: css.row, children: [renderModelPicker?.({ value: modelValue, onChange: setModelDraft, disabled: !ready, defaultLabel: modelDefaultItem }) ?? (choices.length > 0 ? (_jsxs("div", { className: css.modelField, ref: modelFieldRef, children: [modelInputElement, _jsx("button", { type: "button", className: css.modelMenuButton, "aria-label": t('model.menu'), "aria-haspopup": "menu", "aria-expanded": modelMenuOpen, disabled: !ready, onClick: () => { toggleModelMenu(); }, children: _jsx(IconChevronDownOutlineMedium, { className: modelMenuOpen ? `${css.modelMenuChevron} ${css.modelMenuChevronOpen}` : css.modelMenuChevron }) }), modelMenuOpen && (_jsxs("div", { className: modelMenuUp ? `${css.modelMenu} ${css.modelMenuUp}` : css.modelMenu, role: "menu", "aria-label": t('model.menu'), children: [_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": modelValue.trim() === '', className: modelValue.trim() === '' ? `${css.modelItemDefault} ${css.modelItemCurrent}` : css.modelItemDefault, onClick: () => { setModelDraft(''); setModelMenuOpen(false); }, children: [_jsx("span", { className: css.modelItemLabel, children: modelDefaultItem }), _jsx("span", { className: css.modelItemCheck, "aria-hidden": true, children: modelValue.trim() === '' ? '✓' : '' })] }), choices.map(choice => (_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": choice === modelValue, className: choice === modelValue ? css.modelItemCurrent : css.modelItem, onClick: () => { setModelDraft(choice); setModelMenuOpen(false); }, children: [_jsx("span", { className: css.modelItemLabel, children: choice }), _jsx("span", { className: css.modelItemCheck, "aria-hidden": true, children: choice === modelValue ? '✓' : '' })] }, choice)))] }))] })) : modelInputElement), _jsx("button", { type: "button", className: css.modelSave, disabled: !ready || modelValue.trim() === storedModel, onClick: () => { saveModel(); }, children: t('model.save') })] }), modelSaved && _jsx("span", { className: css.saved, children: t('model.applied') }), modelError && _jsx("span", { className: css.errorText, children: t('model.error') })] }), _jsxs("section", { className: css.block, children: [_jsxs("div", { className: css.row, children: [_jsxs("span", { className: css.rowLabel, children: [t('live.title'), _jsx(Tooltip, { label: t('live.info'), side: "bottom", maxWidth: 360, children: _jsx("button", { type: "button", className: css.info, "aria-label": t('live.info.aria'), children: "\u24D8" }) })] }), _jsx("button", { type: "button", role: "switch", "aria-checked": live, "aria-label": t('live.title'), className: live ? `${css.switch} ${css.switchOn}` : css.switch, disabled: !ready, onClick: () => { write('live', !live); }, children: _jsx("span", { className: css.knob }) })] }), snapshot.status === 'unavailable' && (_jsx("span", { className: css.hint, children: t('live.unavailable') }))] }), saved && _jsx("span", { className: css.saved, children: t('live.applied') }), error && _jsx("span", { className: css.errorText, children: t('live.error') })] }));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The dsh harness settings card in the 0.1.5 plugin configuration tab: the
|
|
165
|
+
* same collapsible chrome the official plugin cards use, re-implemented
|
|
166
|
+
* locally (the bundle-purity gate forbids value-importing the official chrome
|
|
167
|
+
* — the context-guard pattern), with the at-a-glance credential dot in the
|
|
168
|
+
* header. Explanation copy lives in hover/focus bubbles behind ⓘ anchors —
|
|
169
|
+
* the card's rows stay one line each.
|
|
170
|
+
* @param props - the injected scope/auth faces and copy.
|
|
171
|
+
* @returns the card.
|
|
172
|
+
*/
|
|
173
|
+
export function DshSettingsCard(props) {
|
|
174
|
+
const { useSettings, scope, auth, authT, renderModelPicker, harnessModel, useSessions, t } = props;
|
|
175
|
+
const [open, setOpen] = useState(false);
|
|
176
|
+
const state = useCardState(open, useSettings, scope, harnessModel, t);
|
|
177
|
+
// The at-a-glance credential dot in the collapsed header: every mount
|
|
178
|
+
// re-probes (a credential expiring mid-session must flip the dot at the
|
|
179
|
+
// next view); the bus dedupes identical results, so no re-render storm.
|
|
180
|
+
const authStatus = useHarnessAuthStatus('dsh', auth.status);
|
|
181
|
+
const dotLabel = authT(authStatus === 'authenticated' ? 'settings.authenticated'
|
|
182
|
+
: authStatus === 'anonymous' ? 'settings.notAuthenticated'
|
|
183
|
+
: authStatus === 'checking' ? 'loading' : 'error');
|
|
184
|
+
const title = t('card.title');
|
|
185
|
+
return (_jsxs("li", { className: open ? `${css.card} ${css.cardOpen}` : css.card, children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${t(open ? 'card.collapse' : 'card.expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [_jsxs("span", { className: css.headText, children: [_jsxs("span", { className: css.nameRow, children: [_jsx("span", { className: css.name, children: title }), _jsx(AuthStatusDot, { status: authStatus, label: dotLabel })] }), _jsx("span", { className: css.description, children: t('card.description') })] }), _jsx(IconChevronDownOutlineMedium, { className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron })] }), open && _jsx(CardBody, { state: state, auth: auth, authT: authT, renderModelPicker: renderModelPicker, useSessions: useSessions, t: t })] }));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The bundle's configuration on the alpha.2 Plugins page
|
|
189
|
+
* (`plugins.bundle.config`, keyed by package name): the page draws the title,
|
|
190
|
+
* the icon, and the crumb itself, so the entry renders the one-liner for the
|
|
191
|
+
* `summary` view and the bare form — with its own save control — for `page`.
|
|
192
|
+
* @param props - the owner view, the injected scope/auth faces, and copy.
|
|
193
|
+
* @returns the entry.
|
|
194
|
+
*/
|
|
195
|
+
export function DshBundleConfig({ view, useSettings, scope, auth, authT, renderModelPicker, harnessModel, useSessions, t }) {
|
|
196
|
+
const state = useCardState(view === 'page', useSettings, scope, harnessModel, t);
|
|
197
|
+
if (view === 'summary')
|
|
198
|
+
return _jsx("span", { className: css.description, children: t('card.description') });
|
|
199
|
+
return _jsx(CardBody, { state: state, auth: auth, authT: authT, renderModelPicker: renderModelPicker, useSessions: useSessions, t: t });
|
|
200
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated by scripts/sync-icon-artwork.mts from @deepseek-ai/dsh-client-ui-primitives
|
|
3
|
+
* (v0.1.7-rc.1, harness 46a7f68b09) — do not edit by hand. The 0.1.5 and rc.1 host lines
|
|
4
|
+
* share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
|
|
5
|
+
* no host runtime identity, identical on every line. Re-run the generator
|
|
6
|
+
* after adding an icon import or bumping the harness checkout.
|
|
7
|
+
*
|
|
8
|
+
* @module @khorsheed/dsh-local-agent-dsh/client
|
|
9
|
+
*/
|
|
10
|
+
/** Shared props for every product icon component (mirrors the upstream IconProps). */
|
|
11
|
+
export interface IconProps {
|
|
12
|
+
/** Square edge in px; defaults to the glyph's own drawn size. */
|
|
13
|
+
size?: number | undefined;
|
|
14
|
+
/** Extra class for layout placement; color rides currentColor. */
|
|
15
|
+
className?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
export declare const IconChevronDownOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=icons.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export const IconChevronDownOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6", stroke: "currentColor" }) }));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-agent-dsh plugin, browser half: the dsh settings surface, one face
|
|
3
|
+
* per install shape and host line — a standalone install renders the
|
|
4
|
+
* bundle's own configuration on its Plugins-page detail view
|
|
5
|
+
* (`plugins.bundle.config`, keyed by package name), a family-bundle install
|
|
6
|
+
* renders the same card through the row-level configure entry on the
|
|
7
|
+
* BUNDLE's detail view (`plugins.row.config`, keyed
|
|
8
|
+
* `@khorsheed/dsh-bundle-local-agent#local-agent-dsh`), and 0.1.5 renders
|
|
9
|
+
* the `settings.plugin.item` card (keyed to the `local-agent-dsh`
|
|
10
|
+
* settings namespace the host half registers) in the official
|
|
11
|
+
* Plugins → 可配置插件 tab. The card carries the family core's shared
|
|
12
|
+
* ProviderAuthBlock (dsh authenticates through the host credentials, so the
|
|
13
|
+
* block renders status only), the DeepSeek delegation switch — migrated from
|
|
14
|
+
* the core section's `local-agent.settings.row-action` seat, which this
|
|
15
|
+
* plugin no longer contributes to — and the resident-mode block (live switch)
|
|
16
|
+
* plus the default-model block (free-text input, the broker's effective-model
|
|
17
|
+
* line and suggestion vocabulary), whose writes ride the bound settingsScope:
|
|
18
|
+
* the host watcher hot-applies them, no reload. Every read degrades: an
|
|
19
|
+
* absent gateway renders the auth block's 'unavailable' state and drops the
|
|
20
|
+
* model surface to the bare input, an unregistered namespace disables the
|
|
21
|
+
* controls.
|
|
22
|
+
* @module @khorsheed/dsh-local-agent-dsh/client
|
|
23
|
+
*/
|
|
24
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
25
|
+
import { en, NS, zh, type LocalAgentDshKey } from './locales.ts';
|
|
26
|
+
export { DshBundleConfig, DshSettingsCard } from './SettingsCard.tsx';
|
|
27
|
+
export type { DshCardSettings, DshBundleConfigProps, DshSettingsCardInjected, DshSettingsCardProps, } from './SettingsCard.tsx';
|
|
28
|
+
export { en, NS, zh };
|
|
29
|
+
export type { LocalAgentDshKey };
|
|
30
|
+
/**
|
|
31
|
+
* Required services: slot registry, command Remote, and locale registry. The
|
|
32
|
+
* settings scope is NOT a static inject — its service name differs per host
|
|
33
|
+
* line (`settingsScope` on 0.1.5, `configForms` on rc.1), so a static entry
|
|
34
|
+
* would pend the whole client plugin on the other line; bindSettingsScope
|
|
35
|
+
* probes the serving one instead (the settings client package is a declared
|
|
36
|
+
* client inject, so one of them is always already up).
|
|
37
|
+
*/
|
|
38
|
+
export declare const inject: string[];
|
|
39
|
+
/**
|
|
40
|
+
* Client plugin body: register the dictionaries and the dsh settings surfaces
|
|
41
|
+
* bound to the local-agent-dsh namespace.
|
|
42
|
+
* @param ctx - client root context.
|
|
43
|
+
*/
|
|
44
|
+
export declare function apply(ctx: Context): void;
|
|
45
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { bindSettingsScope } from '@khorsheed/dsh-local-agent/src/client/settings-scope.ts';
|
|
2
|
+
import { en, NS, zh } from './locales.js';
|
|
3
|
+
import { DshBundleConfig, DshSettingsCard, } from './SettingsCard.js';
|
|
4
|
+
export { DshBundleConfig, DshSettingsCard } from './SettingsCard.js';
|
|
5
|
+
export { en, NS, zh };
|
|
6
|
+
/**
|
|
7
|
+
* This bundle's package name — the key the Plugins page dispatches
|
|
8
|
+
* `plugins.bundle.config` entries on (identity triangle: cordis.patch.yml,
|
|
9
|
+
* tsdown.config.ts, invariant.ts).
|
|
10
|
+
*/
|
|
11
|
+
const PACKAGE_NAME = '@khorsheed/dsh-local-agent-dsh';
|
|
12
|
+
/**
|
|
13
|
+
* Required services: slot registry, command Remote, and locale registry. The
|
|
14
|
+
* settings scope is NOT a static inject — its service name differs per host
|
|
15
|
+
* line (`settingsScope` on 0.1.5, `configForms` on rc.1), so a static entry
|
|
16
|
+
* would pend the whole client plugin on the other line; bindSettingsScope
|
|
17
|
+
* probes the serving one instead (the settings client package is a declared
|
|
18
|
+
* client inject, so one of them is always already up).
|
|
19
|
+
*/
|
|
20
|
+
export const inject = ['slots', 'remote', 'remote.commands', 'locale'];
|
|
21
|
+
/**
|
|
22
|
+
* Client plugin body: register the dictionaries and the dsh settings surfaces
|
|
23
|
+
* bound to the local-agent-dsh namespace.
|
|
24
|
+
* @param ctx - client root context.
|
|
25
|
+
*/
|
|
26
|
+
export function apply(ctx) {
|
|
27
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'local-agent-dsh: dictionaries');
|
|
28
|
+
const scope = bindSettingsScope(ctx, 'local-agent-dsh');
|
|
29
|
+
// The auth block's copy lives in the family core's dictionary; binding is
|
|
30
|
+
// stable per namespace and late dictionary registration still resolves.
|
|
31
|
+
const authT = ctx.locale.bind('local-agent');
|
|
32
|
+
// The gateway namespace is mounted by the family core's client half; read it
|
|
33
|
+
// lazily from the global store so boot order between the two client plugins
|
|
34
|
+
// never matters, and degrade to the auth block's 'unavailable' state when
|
|
35
|
+
// the core client is absent.
|
|
36
|
+
const gateway = () => ctx.get('remote.localAgentGateway');
|
|
37
|
+
// Both settings surfaces inject the same face.
|
|
38
|
+
const cardInject = () => ({
|
|
39
|
+
scope,
|
|
40
|
+
hooks: { settings: scope },
|
|
41
|
+
authT,
|
|
42
|
+
renderModelPicker: props => ctx.get('localAgentUi')?.renderHarnessModelPicker('dsh', props),
|
|
43
|
+
// The harness's memberless model surface; absent gateway/broker
|
|
44
|
+
// degrades to the bare input with its recent-models suggestions.
|
|
45
|
+
harnessModel: () => gateway()?.harnessModel('dsh').then(result => (result.ok ? result.value : undefined))
|
|
46
|
+
?? Promise.resolve(undefined),
|
|
47
|
+
auth: {
|
|
48
|
+
status: name =>
|
|
49
|
+
// The Remote declares (name, scope?) and the client enforces exact arity:
|
|
50
|
+
// pass the default scope explicitly (undefined reads as omitted host-side).
|
|
51
|
+
gateway()?.status(name, undefined).then(result => (result.ok ? result.value : undefined))
|
|
52
|
+
?? Promise.resolve(undefined),
|
|
53
|
+
runCommand: (sessionId, line) => ctx.remote.commands.execute(sessionId, line, [])
|
|
54
|
+
.then(result => (result.ok ? result.value?.result.text : undefined)),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
// The settings surface follows the host line AND the install shape — three
|
|
58
|
+
// tracks, all riding slots.inject so each fires only where its declaration
|
|
59
|
+
// exists. Standalone install: the package's own Plugins-page detail view
|
|
60
|
+
// (`plugins.bundle.config`, keyed by package name). Installed as a family
|
|
61
|
+
// member: the member is no longer the profile's direct dependency, so only
|
|
62
|
+
// the bundle gets a detail view — the card rides the row-level slot
|
|
63
|
+
// (`plugins.row.config`, keyed `<bundle package>#<row id>` with the row id
|
|
64
|
+
// the bundle's patch declares) and the bundle's page gains a configure
|
|
65
|
+
// entry opening it. 0.1.5: the legacy configurable-plugins tab card
|
|
66
|
+
// (`settings.plugin.item`, keyed by the settings namespace) through a
|
|
67
|
+
// string-keyed duck narrow of the same service (the slot name is gone from
|
|
68
|
+
// the alpha.2 SlotMap).
|
|
69
|
+
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
70
|
+
name: 'plugins.bundle.config',
|
|
71
|
+
key: PACKAGE_NAME,
|
|
72
|
+
locale: NS,
|
|
73
|
+
inject: cardInject,
|
|
74
|
+
}, DshBundleConfig));
|
|
75
|
+
ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
|
|
76
|
+
name: 'plugins.row.config',
|
|
77
|
+
key: '@khorsheed/dsh-bundle-local-agent#local-agent-dsh',
|
|
78
|
+
locale: NS,
|
|
79
|
+
inject: cardInject,
|
|
80
|
+
}, DshBundleConfig));
|
|
81
|
+
const legacy = ctx.slots;
|
|
82
|
+
legacy.inject('settings.plugin.item', () => legacy.register({
|
|
83
|
+
name: 'settings.plugin.item',
|
|
84
|
+
key: 'local-agent-dsh',
|
|
85
|
+
locale: NS,
|
|
86
|
+
inject: cardInject,
|
|
87
|
+
}, DshSettingsCard));
|
|
88
|
+
}
|