@sqdr2/dsh-fusion 0.3.0 → 0.3.3
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/CHANGELOG.md +148 -0
- package/README.en.md +9 -4
- package/README.md +9 -4
- package/RELEASING.md +129 -0
- package/RELEASING.zh.md +120 -0
- package/dshx.yml +3 -0
- package/lib/client.js +293 -75
- package/lib/client.js.map +1 -1
- package/lib/dsh-fusion.js +609 -237
- package/lib/types/client/FusionPermission.d.ts +15 -0
- package/lib/types/client/FusionSettings.d.ts +9 -1
- package/lib/types/client/status-polling.d.ts +6 -2
- package/lib/types/contracts.d.ts +2 -0
- package/lib/types/host/cache-policy.d.ts +16 -6
- package/lib/types/host/native-role-sandbox.d.ts +10 -2
- package/lib/types/host/native-selection.d.ts +15 -5
- package/lib/types/host/pairs.d.ts +47 -0
- package/lib/types/host/settings.d.ts +7 -3
- package/lib/types/identity.d.ts +16 -14
- package/lib/types/status.d.ts +17 -1
- package/package.json +64 -60
- package/src/client/FusionHint.tsx +11 -4
- package/src/client/FusionPermission.tsx +52 -0
- package/src/client/FusionSettings.tsx +109 -30
- package/src/client/index.ts +10 -5
- package/src/client/status-polling.ts +9 -4
- package/src/contracts.ts +2 -0
- package/src/dsh-fusion.ts +130 -54
- package/src/host/cache-policy.ts +18 -6
- package/src/host/native-role-sandbox.ts +18 -6
- package/src/host/native-scopes.ts +2 -2
- package/src/host/native-selection.ts +65 -21
- package/src/host/native-worker-brief.ts +18 -5
- package/src/host/pairs.ts +178 -0
- package/src/host/settings.ts +9 -5
- package/src/host/status.ts +24 -1
- package/src/identity.ts +39 -16
- package/src/status.ts +17 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
|
|
2
|
+
import type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client';
|
|
3
|
+
export interface FusionPermissionProps {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
directory: ObservableSnapshot<ModelDirectoryState>;
|
|
6
|
+
load(): void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The permission of the model working now. The composer's native permission control
|
|
10
|
+
* belongs to the conversation (Lead) session, which Fusion confines to read-only, and
|
|
11
|
+
* that confinement matches no configured preset — so the control reads Custom while a
|
|
12
|
+
* task runs. This additive chip names the role and its mode, so the tool row never
|
|
13
|
+
* shows an unexplained Custom alone.
|
|
14
|
+
*/
|
|
15
|
+
export declare function FusionPermission({ sessionId, directory, load }: FusionPermissionProps): import("react").JSX.Element | null;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { PhysicalRoute } from '../contracts.js';
|
|
2
|
-
import type { SettingsCatalog } from '../host/settings.js';
|
|
2
|
+
import type { PairChoice, SettingsCatalog } from '../host/settings.js';
|
|
3
|
+
/** One saved combo as the page shows it: its id, its label and its pair. */
|
|
4
|
+
export interface SavedPairView {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
pair: PairChoice;
|
|
8
|
+
authorized: boolean;
|
|
9
|
+
reason?: string;
|
|
10
|
+
}
|
|
3
11
|
export declare function ModelChoice({ role, route, catalog, onChange }: {
|
|
4
12
|
role: 'Lead' | 'Sidekick' | 'compactor';
|
|
5
13
|
route?: PhysicalRoute;
|
|
@@ -4,6 +4,10 @@ interface Snapshot {
|
|
|
4
4
|
status?: FusionStatus;
|
|
5
5
|
failed: boolean;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* One in-flight read per mounted session; collapsed hints still discover new attention states.
|
|
9
|
+
* `live` asks for the fast cadence whenever the task is running, for a view whose whole
|
|
10
|
+
* purpose is the current stage (the role and permission of the model working now).
|
|
11
|
+
*/
|
|
12
|
+
export declare function useFusionStatus(sessionId: string, expandedTask: string | undefined, attentionOnly?: boolean, live?: boolean): Snapshot | undefined;
|
|
9
13
|
export {};
|
package/lib/types/contracts.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export type RunSelection = {
|
|
|
37
37
|
requestedVersion?: string;
|
|
38
38
|
};
|
|
39
39
|
export type Role = 'lead' | 'worker';
|
|
40
|
+
/** A Host sandbox mode, spelled as the `sandbox/mode` session event writes it. */
|
|
41
|
+
export type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
40
42
|
export type Phase = 'READY' | 'PLANNING' | 'DIRECT' | 'WORKER_RUNNING' | 'REVIEWING' | 'REWORK' | 'WAITING_APPROVAL' | 'WAITING_USER' | 'WAITING_BUDGET' | 'RECOVERING' | 'PAUSED' | 'STOPPING' | 'NEEDS_DECISION' | 'FAILED' | 'COMPLETED' | 'CANCELLED';
|
|
41
43
|
export type VerificationLevel = 'unverified' | 'partial' | 'verified';
|
|
42
44
|
export type ExecutionIntent = 'DIRECT' | 'DELEGATE';
|
|
@@ -93,11 +93,21 @@ type Route = {
|
|
|
93
93
|
provider: string;
|
|
94
94
|
model: string;
|
|
95
95
|
};
|
|
96
|
-
/**
|
|
97
|
-
export
|
|
98
|
-
lead: Route;
|
|
99
|
-
worker: Route;
|
|
100
|
-
|
|
96
|
+
/** A saved combo's routes, plus its optional per-role keepalive choice. */
|
|
97
|
+
export interface CachePairRoutes {
|
|
98
|
+
readonly lead: Route;
|
|
99
|
+
readonly worker: Route;
|
|
100
|
+
readonly cacheKeepalive?: {
|
|
101
|
+
lead?: boolean | 'auto';
|
|
102
|
+
worker?: boolean | 'auto';
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* One row of the settings page: what applies, why, what the defaults say, and
|
|
107
|
+
* what was observed. Every saved combo contributes its routes, so a second
|
|
108
|
+
* combo's models are listed and labelled even before a session uses them.
|
|
109
|
+
*/
|
|
110
|
+
export declare function cacheView(policy: CachePolicy, pair?: CachePairRoutes | readonly CachePairRoutes[], pairChoice?: {
|
|
101
111
|
lead?: boolean | 'auto';
|
|
102
112
|
worker?: boolean | 'auto';
|
|
103
113
|
}): {
|
|
@@ -106,7 +116,7 @@ export declare function cacheView(policy: CachePolicy, pair?: {
|
|
|
106
116
|
role: Role | undefined;
|
|
107
117
|
mode: CacheMode;
|
|
108
118
|
intervalSeconds: number;
|
|
109
|
-
modeSource: "user" | "role" | "route" | "
|
|
119
|
+
modeSource: "user" | "role" | "route" | "pair" | "family" | "generic";
|
|
110
120
|
intervalSource: "user" | "route" | "family" | "generic" | "learned";
|
|
111
121
|
setting: ModelCacheSetting;
|
|
112
122
|
defaults: {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
3
3
|
import type { ToolExecution } from '@deepseek-ai/dsh-tools';
|
|
4
|
-
import type { Role } from '../contracts.js';
|
|
4
|
+
import type { Role, SandboxMode } from '../contracts.js';
|
|
5
5
|
import type { SqliteFusionStore } from '../task/sqlite-store.js';
|
|
6
6
|
import type { SessionBinding } from './bindings.js';
|
|
7
|
-
type Mode =
|
|
7
|
+
type Mode = SandboxMode;
|
|
8
8
|
declare module '@deepseek-ai/dsh-session/types' {
|
|
9
9
|
interface SessionEventMap {
|
|
10
10
|
'sandbox/mode': {
|
|
@@ -14,6 +14,14 @@ declare module '@deepseek-ai/dsh-session/types' {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
export declare const roleSeparated: (binding: SessionBinding) => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The mode the user chose for a confined Fusion conversation, which is the mode
|
|
19
|
+
* the Sidekick runs under. Undefined until Fusion has confined the Lead, or after
|
|
20
|
+
* the conversation left Fusion and its own mode was restored.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readSandboxIntent(store: SqliteFusionStore, sessionId: string): {
|
|
23
|
+
mode: Mode;
|
|
24
|
+
} | undefined;
|
|
17
25
|
/**
|
|
18
26
|
* Role separation enforced below the prompt: the Lead's session runs under the
|
|
19
27
|
* Host's own read-only sandbox (bash and filesystem are refused by the OS
|
|
@@ -7,12 +7,19 @@ import { FUSION_MODEL, FUSION_PROVIDER, isFusionSelection } from '../identity.js
|
|
|
7
7
|
import type { ResolvedProfile } from '../profile/resolve.js';
|
|
8
8
|
import type { FusionCoordinator } from './coordinator.js';
|
|
9
9
|
export { FUSION_MODEL, FUSION_PROVIDER, isFusionSelection };
|
|
10
|
-
/**
|
|
11
|
-
export
|
|
10
|
+
/** One combo as the native menu needs it: its id, its label and its Lead's modalities. */
|
|
11
|
+
export interface FusionModelEntry {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly description?: string;
|
|
15
|
+
readonly inputModalities: readonly ModelModality[];
|
|
16
|
+
}
|
|
17
|
+
/** Read every combo this Host currently advertises; empty means unconfigured. */
|
|
18
|
+
export type FusionModels = () => Promise<readonly FusionModelEntry[]>;
|
|
12
19
|
/** Catalog-only local adapter; the native request is physically routed before dispatch. */
|
|
13
20
|
export declare class FusionCatalogAdapter extends LlmAdapter {
|
|
14
|
-
private readonly
|
|
15
|
-
constructor(
|
|
21
|
+
private readonly models?;
|
|
22
|
+
constructor(models?: FusionModels | undefined);
|
|
16
23
|
providerInfo(provider: string): {
|
|
17
24
|
id: string;
|
|
18
25
|
name: string;
|
|
@@ -24,6 +31,9 @@ export declare class FusionCatalogAdapter extends LlmAdapter {
|
|
|
24
31
|
/** Public native lifecycle integration. No model-menu replacement or Host patch. */
|
|
25
32
|
export declare function installNativeFusionSelection(ctx: Context, input: {
|
|
26
33
|
coordinator(): FusionCoordinator | undefined;
|
|
27
|
-
profile
|
|
34
|
+
/** The frozen profile of one combo, or undefined when it cannot be used yet. */
|
|
35
|
+
profile(model: string): ResolvedProfile | undefined;
|
|
36
|
+
/** Why one combo cannot run, for the message a blocked session shows. */
|
|
37
|
+
reason?(model: string): string | undefined;
|
|
28
38
|
selection(agent: Agent): PhysicalRoute | undefined;
|
|
29
39
|
}): () => void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SqliteFusionStore } from '../task/sqlite-store.js';
|
|
2
|
+
import type { PairChoice, SettingsCatalog } from './settings.js';
|
|
3
|
+
/**
|
|
4
|
+
* One saved combo: the model id the native menu advertises, the label beside
|
|
5
|
+
* it, and the pair that id freezes. The id is the durable identity — a session
|
|
6
|
+
* that selected it keeps its own frozen copy — so renaming never moves a combo.
|
|
7
|
+
*/
|
|
8
|
+
export interface PairEntry {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly pair: PairChoice;
|
|
12
|
+
}
|
|
13
|
+
/** The saved combos, in menu order. `auto` is the historical first combo. */
|
|
14
|
+
export interface PairsSnapshot {
|
|
15
|
+
readonly revision: number;
|
|
16
|
+
readonly pairs: readonly PairEntry[];
|
|
17
|
+
/** True when these came from a pre-combo `settings:profile` document. */
|
|
18
|
+
readonly migrated: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Where the combo list lives; `settings:profile` stays readable for migration. */
|
|
21
|
+
export declare const PAIRS_DOCUMENT = "settings:pairs";
|
|
22
|
+
/** Every menu entry costs one row in the native picker, so the list stays short. */
|
|
23
|
+
export declare const MAX_PAIRS = 6;
|
|
24
|
+
export declare const MAX_PAIR_NAME = 40;
|
|
25
|
+
/** The migrated first combo keeps the label the historical entry had. */
|
|
26
|
+
export declare const DEFAULT_PAIR_NAME = "\u81EA\u52A8";
|
|
27
|
+
export declare const profileIdOf: (model: string) => string;
|
|
28
|
+
/** The combo a frozen profile belongs to, or undefined for a non-combo profile. */
|
|
29
|
+
export declare function modelOfProfileId(id: string): string | undefined;
|
|
30
|
+
/** A pre-combo profile document as the choice it was saved from. */
|
|
31
|
+
export declare function choiceFromStoredProfile(value: unknown): PairChoice | undefined;
|
|
32
|
+
/** The saved combos; a pre-combo installation migrates to one `auto` combo on read. */
|
|
33
|
+
export declare function readPairs(store: SqliteFusionStore): PairsSnapshot;
|
|
34
|
+
export declare function pairName(value: unknown, fallback: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Validate the list a settings action proposes. Ids are assigned here, never by
|
|
37
|
+
* the client, so a stale tab cannot name a combo that is already frozen
|
|
38
|
+
* elsewhere; reordering only moves the menu rows.
|
|
39
|
+
*/
|
|
40
|
+
export declare function validatePairs(value: unknown, catalog: SettingsCatalog, existing: readonly PairEntry[]): PairEntry[];
|
|
41
|
+
export declare function pairsDocument(pairs: readonly PairEntry[]): Record<string, unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* Persist the list under one compare-and-swap revision. The first combo is
|
|
44
|
+
* mirrored into the pre-combo `settings:profile` document so an older tab (or a
|
|
45
|
+
* downgrade) still finds the pair it knows about.
|
|
46
|
+
*/
|
|
47
|
+
export declare function writePairs(store: SqliteFusionStore, expectedRevision: number, pairs: readonly PairEntry[]): number;
|
|
@@ -32,8 +32,12 @@ export declare function savedLeadRoute(value: unknown): {
|
|
|
32
32
|
export declare function validatePairChoice(value: unknown, catalog: SettingsCatalog): PairChoice;
|
|
33
33
|
/** New sessions can opt into this Worker cap. Frozen sessions keep their saved profile. */
|
|
34
34
|
export declare const WORKER_IMPLEMENTATION_OUTPUT_TOKENS = 128000;
|
|
35
|
-
export declare function profileFromChoice(pair: PairChoice): Record<string, unknown>;
|
|
36
|
-
/**
|
|
37
|
-
|
|
35
|
+
export declare function profileFromChoice(pair: PairChoice, id?: string): Record<string, unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Product defaults; explicit legacy/benchmark profiles retain their old contract.
|
|
38
|
+
* `id` names the combo; the first one keeps the historical `fusion-auto` so a
|
|
39
|
+
* profile frozen before combos existed keeps its identity.
|
|
40
|
+
*/
|
|
41
|
+
export declare function modelProfileFromChoice(pair: PairChoice, id?: string): Record<string, unknown>;
|
|
38
42
|
/** Keep existing default settings stable; expose explicit larger/smaller limits on reload. */
|
|
39
43
|
export declare function choiceFromProfile(profile: PairProfile): PairChoice;
|
package/lib/types/identity.d.ts
CHANGED
|
@@ -27,10 +27,18 @@ export declare const PACKAGE_NAME = "@sqdr2/dsh-fusion";
|
|
|
27
27
|
export declare const PLUGIN_NAME = "dsh-fusion";
|
|
28
28
|
/** The plugin id and package name this project used before the rename. */
|
|
29
29
|
export declare const LEGACY_PLUGIN_NAME = "dsh-model-fusion";
|
|
30
|
-
/** Registered provider route for the Fusion model
|
|
30
|
+
/** Registered provider route for the Fusion model entries. */
|
|
31
31
|
export declare const FUSION_PROVIDER = "dsh-fusion";
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* The model id of the combo every installation starts with.
|
|
34
|
+
*
|
|
35
|
+
* It is a pair id like any other; keeping this exact spelling for the first
|
|
36
|
+
* combo means a durable `model/selection` written before combos existed still
|
|
37
|
+
* names a combo that exists.
|
|
38
|
+
*/
|
|
33
39
|
export declare const FUSION_MODEL = "auto";
|
|
40
|
+
/** Whether a model id names a Fusion combo on this provider. */
|
|
41
|
+
export declare const isFusionModel: (model: string | undefined) => boolean;
|
|
34
42
|
/**
|
|
35
43
|
* The provider route this plugin registered before it was renamed from
|
|
36
44
|
* `dsh-model-fusion`. Sessions created then still name it in their durable
|
|
@@ -40,23 +48,17 @@ export declare const FUSION_MODEL = "auto";
|
|
|
40
48
|
export declare const LEGACY_FUSION_PROVIDER = "dsh-model-fusion";
|
|
41
49
|
/** Whether a provider route belongs to this plugin, under either name. */
|
|
42
50
|
export declare const isFusionProvider: (provider: string | undefined) => boolean;
|
|
43
|
-
/** Whether a route names
|
|
51
|
+
/** Whether a route names a Fusion combo, under either provider spelling. */
|
|
44
52
|
export declare const isFusionSelection: (route: {
|
|
45
53
|
provider?: string;
|
|
46
54
|
model?: string;
|
|
47
55
|
} | undefined) => boolean;
|
|
56
|
+
/** Whether a switch-notice summary describes a Fusion combo as either side. */
|
|
57
|
+
export declare const isFusionSelectionLabel: (summary: string | undefined) => boolean;
|
|
48
58
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* rename carries the old one in its durable log, and the notice is the same
|
|
53
|
-
* non-change on every Fusion step either way.
|
|
54
|
-
*/
|
|
55
|
-
export declare const FUSION_SELECTION_LABELS: readonly ["dsh-fusion/auto", "dsh-model-fusion/auto"];
|
|
56
|
-
/**
|
|
57
|
-
* Whether a route already names the entry under its current name. A durable
|
|
58
|
-
* intent that fails this is rewritten before the next request is assembled;
|
|
59
|
-
* the legacy spelling is a read-side alias, not a route that still dispatches.
|
|
59
|
+
* Whether a route already names a combo under its current provider spelling. A
|
|
60
|
+
* durable intent that fails this is rewritten before the next request is
|
|
61
|
+
* assembled; the legacy spelling is a read-side alias, not a dispatching route.
|
|
60
62
|
*/
|
|
61
63
|
export declare const isCurrentFusionSelection: (route: {
|
|
62
64
|
provider?: string;
|
package/lib/types/status.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ControlView } from './host/native-model-control.js';
|
|
2
|
-
import type { PhysicalRoute, Role, TokenMeasurement, VerificationLevel } from './contracts.js';
|
|
2
|
+
import type { PhysicalRoute, Role, SandboxMode, TokenMeasurement, VerificationLevel } from './contracts.js';
|
|
3
3
|
/** Bounded, read-only UI projection. No prompts, tool arguments or credentials. */
|
|
4
4
|
export interface FusionStatus {
|
|
5
5
|
schemaVersion: 1;
|
|
@@ -23,6 +23,22 @@ export interface FusionStatus {
|
|
|
23
23
|
worker: PhysicalRoute;
|
|
24
24
|
compactor: PhysicalRoute | null;
|
|
25
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Role-separated sandbox modes, present while Fusion confines the Lead. The
|
|
28
|
+
* composer's own permission control follows the conversation (Lead) session,
|
|
29
|
+
* so the UI shows these instead of the derived preset name.
|
|
30
|
+
*/
|
|
31
|
+
permissions?: {
|
|
32
|
+
/**
|
|
33
|
+
* What the Lead's own work is confined to. The session is widened only for the
|
|
34
|
+
* checks Fusion runs itself and for the Lead during a Host-unlocked takeover.
|
|
35
|
+
*/
|
|
36
|
+
lead: SandboxMode;
|
|
37
|
+
/** What the Sidekick runs under: the mode the user chose for this conversation. */
|
|
38
|
+
worker: SandboxMode;
|
|
39
|
+
/** The role working right now; null while the task waits. */
|
|
40
|
+
active: Role | null;
|
|
41
|
+
};
|
|
26
42
|
usage: {
|
|
27
43
|
calls: number;
|
|
28
44
|
finalCalls: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sqdr2/dsh-fusion",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -26,10 +26,13 @@
|
|
|
26
26
|
"profiles",
|
|
27
27
|
"dshx.yml",
|
|
28
28
|
"cordis.patch.yml",
|
|
29
|
+
"CHANGELOG.md",
|
|
29
30
|
"README.md",
|
|
30
31
|
"README.en.md",
|
|
31
32
|
"INSTALL.md",
|
|
32
33
|
"INSTALL.zh.md",
|
|
34
|
+
"RELEASING.md",
|
|
35
|
+
"RELEASING.zh.md",
|
|
33
36
|
"LICENSE",
|
|
34
37
|
"docs/COMPATIBILITY.md",
|
|
35
38
|
"docs/EVIDENCE.md"
|
|
@@ -63,75 +66,76 @@
|
|
|
63
66
|
}
|
|
64
67
|
},
|
|
65
68
|
"devDependencies": {
|
|
66
|
-
"@types/node": "^22.20.0",
|
|
67
|
-
"typescript": "^5.9.2",
|
|
68
|
-
"vitest": "^4.1.8",
|
|
69
|
-
"@types/react": "~18.3.1",
|
|
70
|
-
"@types/react-test-renderer": "18.3.1",
|
|
71
|
-
"react": "^18.3.1",
|
|
72
|
-
"react-test-renderer": "18.3.1",
|
|
73
|
-
"semver": "^7.7.2",
|
|
74
|
-
"yaml": "^2.8.0",
|
|
75
69
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
76
|
-
"@deepseek-ai/schemastery": "^3.18.4",
|
|
77
|
-
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
78
|
-
"@deepseek-ai/dsh-llm": "0.2.0-rc.2",
|
|
79
|
-
"@deepseek-ai/dsh-session": "0.2.0-rc.2",
|
|
80
|
-
"@deepseek-ai/dsh-session-query": "0.2.0-rc.2",
|
|
81
|
-
"@deepseek-ai/dsh-subagent": "0.2.0-rc.2",
|
|
82
|
-
"@deepseek-ai/dsh-tools": "0.2.0-rc.2",
|
|
83
|
-
"@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
|
|
84
|
-
"@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2",
|
|
85
|
-
"@deepseek-ai/dsh-commands": "0.2.0-rc.2",
|
|
86
|
-
"@deepseek-ai/dsh-host-webserver": "0.2.0-rc.2",
|
|
87
|
-
"@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
|
|
88
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2",
|
|
89
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
|
|
90
|
-
"@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.2",
|
|
91
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
|
|
92
|
-
"@deepseek-ai/dsh-client-ui-model-selection": "0.2.0-rc.2",
|
|
93
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2",
|
|
94
|
-
"@deepseek-ai/dsh-client-ui-tool": "0.2.0-rc.2",
|
|
95
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
|
|
96
|
-
"@deepseek-ai/dsh-client-ui-chat": "0.2.0-rc.2",
|
|
97
|
-
"@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
|
|
98
|
-
"@deepseek-ai/dsh-agent-default-model": "0.2.0-rc.2",
|
|
99
|
-
"@deepseek-ai/dsh-token-meter": "0.2.0-rc.2",
|
|
100
|
-
"@deepseek-ai/dsh-compaction": "0.2.0-rc.2",
|
|
101
|
-
"@deepseek-ai/dsh-user-approval": "0.2.0-rc.2",
|
|
102
|
-
"@deepseek-ai/dsh-jobs": "0.2.0-rc.2",
|
|
103
|
-
"@deepseek-ai/dsh-agent-preset-registry": "0.2.0-rc.2",
|
|
104
70
|
"@deepseek-ai/cordis-plugin-group": ">=1.0.4 <1.1.0-0",
|
|
105
71
|
"@deepseek-ai/cordis-plugin-include": ">=1.0.9 <1.1.0-0",
|
|
106
72
|
"@deepseek-ai/cordis-plugin-loader": ">=1.0.5 <1.1.0-0",
|
|
73
|
+
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-agent-default-model": "0.2.0-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-agent-preset-registry": "0.2.0-rc.2",
|
|
107
76
|
"@deepseek-ai/dsh-api-gateway": "0.2.0-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2",
|
|
108
78
|
"@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
|
|
109
79
|
"@deepseek-ai/dsh-client-connection": "0.2.0-rc.2",
|
|
110
80
|
"@deepseek-ai/dsh-client-file-upload": "0.2.0-rc.2",
|
|
81
|
+
"@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
|
|
82
|
+
"@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
|
|
83
|
+
"@deepseek-ai/dsh-client-ui-chat": "0.2.0-rc.2",
|
|
84
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2",
|
|
85
|
+
"@deepseek-ai/dsh-client-ui-model-selection": "0.2.0-rc.2",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
|
|
88
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.2",
|
|
89
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2",
|
|
90
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
|
|
91
|
+
"@deepseek-ai/dsh-client-ui-tool": "0.2.0-rc.2",
|
|
92
|
+
"@deepseek-ai/dsh-commands": "0.2.0-rc.2",
|
|
93
|
+
"@deepseek-ai/dsh-compaction": "0.2.0-rc.2",
|
|
111
94
|
"@deepseek-ai/dsh-credentials": "0.2.0-rc.2",
|
|
112
95
|
"@deepseek-ai/dsh-file-reference": "0.2.0-rc.2",
|
|
113
96
|
"@deepseek-ai/dsh-fs": "0.2.0-rc.2",
|
|
114
97
|
"@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
|
|
98
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.0-rc.2",
|
|
115
99
|
"@deepseek-ai/dsh-invariants": "0.2.0-rc.2",
|
|
100
|
+
"@deepseek-ai/dsh-jobs": "0.2.0-rc.2",
|
|
116
101
|
"@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2",
|
|
102
|
+
"@deepseek-ai/dsh-llm": "0.2.0-rc.2",
|
|
117
103
|
"@deepseek-ai/dsh-llm-retry": "0.2.0-rc.2",
|
|
118
104
|
"@deepseek-ai/dsh-native-command": "0.2.0-rc.2",
|
|
119
105
|
"@deepseek-ai/dsh-ptc-runtime": "0.2.0-rc.2",
|
|
120
106
|
"@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
|
|
121
107
|
"@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
|
|
122
108
|
"@deepseek-ai/dsh-scope": "0.2.0-rc.2",
|
|
109
|
+
"@deepseek-ai/dsh-session": "0.2.0-rc.2",
|
|
110
|
+
"@deepseek-ai/dsh-session-persistence": "0.2.0-rc.2",
|
|
123
111
|
"@deepseek-ai/dsh-session-projection": "0.2.0-rc.2",
|
|
112
|
+
"@deepseek-ai/dsh-session-query": "0.2.0-rc.2",
|
|
124
113
|
"@deepseek-ai/dsh-session-title": "0.2.0-rc.2",
|
|
125
114
|
"@deepseek-ai/dsh-settings": "0.2.0-rc.2",
|
|
126
115
|
"@deepseek-ai/dsh-skill": "0.2.0-rc.2",
|
|
116
|
+
"@deepseek-ai/dsh-storage": "0.2.0-rc.2",
|
|
117
|
+
"@deepseek-ai/dsh-storage-domain": "0.2.0-rc.2",
|
|
118
|
+
"@deepseek-ai/dsh-subagent": "0.2.0-rc.2",
|
|
119
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
|
|
120
|
+
"@deepseek-ai/dsh-token-meter": "0.2.0-rc.2",
|
|
127
121
|
"@deepseek-ai/dsh-tool-todo": "0.2.0-rc.2",
|
|
122
|
+
"@deepseek-ai/dsh-tools": "0.2.0-rc.2",
|
|
128
123
|
"@deepseek-ai/dsh-typert-registry": "0.2.0-rc.2",
|
|
124
|
+
"@deepseek-ai/dsh-user-approval": "0.2.0-rc.2",
|
|
129
125
|
"@deepseek-ai/dsh-util-time": "0.2.0-rc.2",
|
|
130
126
|
"@deepseek-ai/dsh-util-workspace-path": "0.2.0-rc.2",
|
|
131
127
|
"@deepseek-ai/dsh-workspace": "0.2.0-rc.2",
|
|
132
|
-
"@deepseek-ai/
|
|
133
|
-
"@
|
|
134
|
-
"@
|
|
128
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
129
|
+
"@types/node": "^22.20.0",
|
|
130
|
+
"@types/react": "~18.3.1",
|
|
131
|
+
"@types/react-test-renderer": "18.3.1",
|
|
132
|
+
"esbuild": "^0.28.2",
|
|
133
|
+
"react": "^18.3.1",
|
|
134
|
+
"react-test-renderer": "18.3.1",
|
|
135
|
+
"semver": "^7.7.2",
|
|
136
|
+
"typescript": "^5.9.2",
|
|
137
|
+
"vitest": "^4.1.8",
|
|
138
|
+
"yaml": "^2.8.0"
|
|
135
139
|
},
|
|
136
140
|
"engines": {
|
|
137
141
|
"node": ">=22.14.0",
|
|
@@ -140,33 +144,33 @@
|
|
|
140
144
|
"packageManager": "pnpm@9.15.9",
|
|
141
145
|
"peerDependencies": {
|
|
142
146
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
143
|
-
"@deepseek-ai/schemastery": "^3.18.4",
|
|
144
147
|
"@deepseek-ai/dsh-agent": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
145
|
-
"@deepseek-ai/dsh-
|
|
146
|
-
"@deepseek-ai/dsh-
|
|
147
|
-
"@deepseek-ai/dsh-session-query": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
148
|
-
"@deepseek-ai/dsh-subagent": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
149
|
-
"@deepseek-ai/dsh-tools": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
150
|
-
"@deepseek-ai/dsh-system-prompt": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
148
|
+
"@deepseek-ai/dsh-agent-default-model": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
149
|
+
"@deepseek-ai/dsh-agent-preset-registry": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
151
150
|
"@deepseek-ai/dsh-api-session-controller": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
152
|
-
"@deepseek-ai/dsh-commands": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
153
|
-
"@deepseek-ai/dsh-host-webserver": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
154
151
|
"@deepseek-ai/dsh-client-locale": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
152
|
+
"@deepseek-ai/dsh-client-store": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
153
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
154
|
+
"@deepseek-ai/dsh-client-ui-model-selection": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
155
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
156
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
155
157
|
"@deepseek-ai/dsh-client-ui-settings": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
156
158
|
"@deepseek-ai/dsh-client-ui-slots": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
157
|
-
"react": "^18.2.0",
|
|
158
|
-
"@deepseek-ai/dsh-client-ui-renderer": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
159
|
-
"@deepseek-ai/dsh-client-ui-model-selection": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
160
|
-
"@deepseek-ai/dsh-client-ui-conversation": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
161
159
|
"@deepseek-ai/dsh-client-ui-tool": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
162
|
-
"@deepseek-ai/dsh-
|
|
163
|
-
"@deepseek-ai/dsh-client-store": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
164
|
-
"@deepseek-ai/dsh-agent-default-model": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
165
|
-
"@deepseek-ai/dsh-token-meter": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
160
|
+
"@deepseek-ai/dsh-commands": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
166
161
|
"@deepseek-ai/dsh-compaction": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
167
|
-
"@deepseek-ai/dsh-
|
|
162
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
168
163
|
"@deepseek-ai/dsh-jobs": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
169
|
-
"@deepseek-ai/dsh-
|
|
164
|
+
"@deepseek-ai/dsh-llm": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
165
|
+
"@deepseek-ai/dsh-session": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
166
|
+
"@deepseek-ai/dsh-session-query": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
167
|
+
"@deepseek-ai/dsh-subagent": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
168
|
+
"@deepseek-ai/dsh-system-prompt": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
169
|
+
"@deepseek-ai/dsh-token-meter": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
170
|
+
"@deepseek-ai/dsh-tools": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
171
|
+
"@deepseek-ai/dsh-user-approval": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-rc.2 <0.3.0-0",
|
|
172
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
173
|
+
"react": "^18.2.0"
|
|
170
174
|
},
|
|
171
175
|
"scripts": {
|
|
172
176
|
"build": "node scripts/build.mjs",
|
|
@@ -22,15 +22,22 @@ export function FusionHint({ sessionId, directory, load }: FusionHintProps) {
|
|
|
22
22
|
const abort = new AbortController()
|
|
23
23
|
const refresh = () => {
|
|
24
24
|
void fetch(`${FUSION_API_PATH}?view=settings`, { credentials: 'same-origin', cache: 'no-store', signal: abort.signal })
|
|
25
|
-
.then(async response => { if (!response.ok) throw new Error('unavailable'); return response.json() as Promise<{
|
|
26
|
-
.then(state => {
|
|
27
|
-
|
|
25
|
+
.then(async response => { if (!response.ok) throw new Error('unavailable'); return response.json() as Promise<{ pairs: { id: string; authorized: boolean; reason?: string }[] }> })
|
|
26
|
+
.then(state => {
|
|
27
|
+
if (!active) return
|
|
28
|
+
// The hint speaks for this conversation's combo, not for the first one.
|
|
29
|
+
const combo = state.pairs.find(pair => pair.id === selected?.model)
|
|
30
|
+
if (!state.pairs.length) setMessage(tr(lang, 'Fusion 尚未配置模型,请前往设置 → Fusion 选择 Lead 和 Sidekick。', 'Fusion has no models yet: open Settings → Fusion and choose a Lead and a Sidekick.'))
|
|
31
|
+
else if (!combo) setMessage(tr(lang, '当前组合已被删除,请在模型菜单中选择其他组合。', 'This combo was removed; pick another one in the model menu.'))
|
|
32
|
+
else if (!combo.authorized) setMessage(tr(lang, 'Fusion 当前配置未就绪,请前往设置 → Fusion 查看。', 'The Fusion setup is not ready: see Settings → Fusion.'))
|
|
33
|
+
else setMessage(undefined)
|
|
34
|
+
})
|
|
28
35
|
.catch(() => { if (active) setMessage(tr(lang, '暂时无法确认 Fusion 配置,请前往设置 → Fusion 查看。', 'Cannot confirm the Fusion setup right now: see Settings → Fusion.')) })
|
|
29
36
|
}
|
|
30
37
|
refresh()
|
|
31
38
|
const off = onSettingsChanged(refresh)
|
|
32
39
|
return () => { active = false; abort.abort(); off() }
|
|
33
|
-
}, [fusion, lang])
|
|
40
|
+
}, [fusion, lang, selected?.model])
|
|
34
41
|
if (!fusion) return null
|
|
35
42
|
return <>
|
|
36
43
|
{message && <p role="status" style={{ margin: '8px 12px', color: 'var(--dsw-alias-label-secondary)', fontSize: 12, lineHeight: '18px' }}>{message}</p>}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useEffect, useSyncExternalStore } from 'react'
|
|
2
|
+
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
|
|
3
|
+
import type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client'
|
|
4
|
+
import { PermissionIconFullAccessRegular, PermissionIconReadOnlyRegular,
|
|
5
|
+
PermissionIconWorkspaceWriteRegular } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
6
|
+
import type { SandboxMode } from '../contracts.js'
|
|
7
|
+
import { isFusionSelection } from '../identity.js'
|
|
8
|
+
import { tr, useLang, type Lang } from './i18n.js'
|
|
9
|
+
import { useFusionStatus } from './status-polling.js'
|
|
10
|
+
|
|
11
|
+
const styles = `
|
|
12
|
+
.fusion-permission{display:inline-flex;align-items:center;gap:4px;height:28px;min-width:0;max-width:220px;padding:0 6px;border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;font-weight:500}
|
|
13
|
+
.fusion-permission svg{width:14px;height:14px;flex:none}
|
|
14
|
+
.fusion-permission .fusion-permission-role{flex:none;color:var(--dsw-alias-label-caption)}
|
|
15
|
+
.fusion-permission .fusion-permission-mode{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
16
|
+
`
|
|
17
|
+
|
|
18
|
+
const modeOf = (lang: Lang, mode: SandboxMode) => mode === 'read-only' ? tr(lang, '仅可查看', 'Read Only')
|
|
19
|
+
: mode === 'workspace-write' ? tr(lang, '工作区内修改', 'Workspace Write') : tr(lang, '完全权限', 'Full access')
|
|
20
|
+
const iconOf = (mode: SandboxMode) => mode === 'read-only' ? <PermissionIconReadOnlyRegular />
|
|
21
|
+
: mode === 'workspace-write' ? <PermissionIconWorkspaceWriteRegular /> : <PermissionIconFullAccessRegular />
|
|
22
|
+
|
|
23
|
+
export interface FusionPermissionProps { sessionId: string; directory: ObservableSnapshot<ModelDirectoryState>; load(): void }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The permission of the model working now. The composer's native permission control
|
|
27
|
+
* belongs to the conversation (Lead) session, which Fusion confines to read-only, and
|
|
28
|
+
* that confinement matches no configured preset — so the control reads Custom while a
|
|
29
|
+
* task runs. This additive chip names the role and its mode, so the tool row never
|
|
30
|
+
* shows an unexplained Custom alone.
|
|
31
|
+
*/
|
|
32
|
+
export function FusionPermission({ sessionId, directory, load }: FusionPermissionProps) {
|
|
33
|
+
const selected = useSyncExternalStore(directory.subscribe, directory.getSnapshot).current
|
|
34
|
+
const fusion = isFusionSelection(selected ?? undefined)
|
|
35
|
+
const lang = useLang()
|
|
36
|
+
// The acting role is the whole point of this chip, so it always wants fresh polls.
|
|
37
|
+
const snapshot = useFusionStatus(sessionId, undefined, false, true)
|
|
38
|
+
useEffect(() => { load() }, [load])
|
|
39
|
+
const permissions = fusion ? snapshot?.status?.task?.permissions : undefined
|
|
40
|
+
if (!permissions) return null
|
|
41
|
+
const role = permissions.active === 'worker' ? 'Sidekick' : 'Lead'
|
|
42
|
+
const mode = permissions.active === 'worker' ? permissions.worker : permissions.lead
|
|
43
|
+
const hint = tr(lang,
|
|
44
|
+
`Fusion 按角色分离权限:Lead ${modeOf(lang, permissions.lead)},Sidekick ${modeOf(lang, permissions.worker)}。输入框的权限控件属于对话会话(Lead),运行期间可能显示 Custom。`,
|
|
45
|
+
`Fusion separates permissions by role: Lead ${modeOf(lang, permissions.lead)}, Sidekick ${modeOf(lang, permissions.worker)}. The permission control belongs to the conversation (Lead) session, so it may read Custom while a task runs.`)
|
|
46
|
+
return <span className="fusion-permission" title={hint} aria-label={`${role} · ${modeOf(lang, mode)}`} data-ud-check="fusion-permission">
|
|
47
|
+
<style>{styles}</style>
|
|
48
|
+
{iconOf(mode)}
|
|
49
|
+
<span className="fusion-permission-role">{role}</span>
|
|
50
|
+
<span className="fusion-permission-mode">{modeOf(lang, mode)}</span>
|
|
51
|
+
</span>
|
|
52
|
+
}
|