@t4r71/dsh-dual-axis-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,13 @@
1
+ //#region lib/types/index.js
2
+ /**
3
+ * 本包的宿主半边。这个包的实质内容全在浏览器半边,但 0.1.7 的客户端扫描只处理
4
+ * 「有活动 Loader 行」的包:包名没有行,`dsh.client` 就不会被采集成客户端行,
5
+ * 浏览器半边永远不会下发。这个空插件就是那一行的落点。
6
+ * @module @t4r71/dsh-dual-axis-ui
7
+ */
8
+ /** 插件名,loader 诊断用。 */
9
+ const name = "dual-axis-ui";
10
+ /** 不做任何事:两个界面面都由 ./client 那半边承担。 */
11
+ async function apply() {}
12
+ //#endregion
13
+ export { apply as default, name };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 双轴访问模式组合包的浏览器半边:把这一条能力的两个界面面合成一个客户端插件。
3
+ *
4
+ * 为什么是一个插件而不是两个:0.1.7 的客户端模块系统里,客户端行 id 就是包名
5
+ * (client/modules/src/client/manifest.ts 的 WebBootEntry.id),`dsh.client` 是单个对象,
6
+ * `stripClientSuffix` 只剥掉结尾的 /client —— 一个 npm 包因此只能带一个客户端半边。
7
+ * 两个界面面(设置页那一行、输入框上方的两个下拉)于是共用这一次 apply。
8
+ *
9
+ * @module @t4r71/dsh-dual-axis/client
10
+ */
11
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
12
+ export declare const inject: string[];
13
+ /**
14
+ * 挂载两个界面面:下拉那面先挂(它注册会话输入区的 slot 与词典),配置行那面随后。
15
+ * @param ctx - 浏览器插件上下文。
16
+ */
17
+ export declare function apply(ctx: ClientContext): void;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * 一条轴的 `custom` 路径编辑器:底座下拉加两个绝对路径列表。
3
+ *
4
+ * 这个编辑器是让 `custom` 成为一条完整策略语句的东西。另外三个取值本身各自就是
5
+ * 完整语句 —— 选中即提交 —— 而 `custom` 必须带底座,宿主的解析器宁可拒绝一条裸的
6
+ * `write:custom` 也不肯替用户猜一个。所以下拉里选中 `custom` 时不直接提交,而是展开
7
+ * 这个编辑器,由它提交完整的 `axis:custom:base=…,allow=…,deny=…` 参数。
8
+ *
9
+ * 两个路径框都是一行一条绝对路径。全局设置文档里存的是列表,而这个界面没有路径
10
+ * 选择器,所以文本是诚实的载体;一条相对路径会挡住提交并说明原因,而不是被解析到
11
+ * 某个用户从没指定过的目录上去。
12
+ *
13
+ * 提交要过一次风险确认:`base=all` 或带额外放行路径的 `custom` 都会放大该轴的
14
+ * 访问范围,所以「应用」先落到确认框,只有勾选并确认才真的写下去。不放大权限的
15
+ * 草稿(例如在 `workspace` 底座上只加排除路径)不过闸,免得确认变成噪音。
16
+ *
17
+ * @module @deepseek-ai/dsh-client-ui-permission-presets/client/PermissionCustomEditor
18
+ */
19
+ import type { ReactNode } from 'react';
20
+ import type { NarrowingNotice, PermissionAxis } from './presentation.ts';
21
+ import type { PermissionSelectProps } from './PermissionSelect.tsx';
22
+ import { type AxisEditorScope, type AxisEditorValue, type RuleGroupOption } from './axis-editor.ts';
23
+ export type { AxisEditorValue } from './axis-editor.ts';
24
+ /** 一条轴的路径编辑器的 props。 */
25
+ export interface PermissionCustomEditorProps {
26
+ /** 这条编辑器写哪条轴;两个下拉没有可见标题,所以文案自己说明轴名。 */
27
+ axis: PermissionAxis;
28
+ /** 该轴在当前会话里的取值,编辑器以它为初值;会话还没有这条轴时为 undefined。 */
29
+ current: AxisEditorValue | undefined;
30
+ /** 该轴在部署设置里的取值;会话没有这条轴({@link current} 缺席)时用它。 */
31
+ global: AxisEditorValue | undefined;
32
+ /**
33
+ * 写轴被读轴收窄的结论,由宿主解算后发布(见 {@link NarrowingNotice})。
34
+ * 只有写轴有:不变量讲的就是写范围。undefined 表示这条会话当刻没有被收窄。
35
+ */
36
+ notice: NarrowingNotice | undefined;
37
+ /** 设置页当前定义的规则组;编辑器在渲染时调它,取的就是当刻那一份。 */
38
+ groups: () => readonly RuleGroupOption[];
39
+ /** 输入框是否被锁住(只读会话,或没有活绑定)。 */
40
+ locked: boolean;
41
+ /** 该轴是否已有一次提交在飞。 */
42
+ busy: boolean;
43
+ /**
44
+ * 提交完整的轴参数;宿主结清**并且文档里真的出现这条取值**之后兑现。
45
+ *
46
+ * 第二个参数是这份草稿落成记录之后的形状({@link draftScope})。它不是给宿主看的
47
+ * —— 宿主只收那一个参数串 —— 而是给提交路径核对用的:宿主对自己没写的改动也会回
48
+ * 「命令执行了」,只有拿写入后的记录比对才能分辨「写进去了」与「被拒了没写」。
49
+ */
50
+ onSubmit: (args: string, expected: AxisEditorScope) => Promise<boolean>;
51
+ /** 不提交就关掉编辑器。 */
52
+ onDismiss: () => void;
53
+ t: PermissionSelectProps['t'];
54
+ }
55
+ /**
56
+ * 渲染一条轴的 `custom` 路径编辑器。
57
+ *
58
+ * 草稿从会话当前值起步,**任何取值都算**:这条会话的轴是这个界面表达的对象,
59
+ * 编辑器打开时显示别的值(例如部署默认)就会与它旁边那个下拉自相矛盾。只有
60
+ * 会话值缺席时才退到该轴的部署默认值。一次失败的提交保留草稿并显示一句可读的话,
61
+ * 而不是抛出去,所以被拒的审批留下的是一个还能用的编辑器。
62
+ * @param props - 轴、起始值、写入口与词典座位。
63
+ * @returns 底座下拉、两个路径框与提交行。
64
+ */
65
+ export declare function PermissionCustomEditor({ axis, current, global, notice, groups, locked, busy, onSubmit, onDismiss, t, }: PermissionCustomEditorProps): ReactNode;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Permission preference row: the default preset for subsequently created
3
+ * sessions. Current-session switches remain on the composer's read/write
4
+ * axis dropdowns.
5
+ */
6
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
7
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
8
+ import type { PermissionSettingsState } from './settings-store.ts';
9
+ import type { PermissionSettingsKey } from './locales.ts';
10
+ /** Registration-side business face for the host-backed preference. */
11
+ export interface PermissionRowInjected {
12
+ hooks: {
13
+ /** Permission settings snapshot bound by the renderer as usePermission. */
14
+ permission: SnapshotStore<PermissionSettingsState>;
15
+ };
16
+ /** Load the descriptor when the row first renders. */
17
+ load: () => Promise<void>;
18
+ /** Persist one advertised preset. */
19
+ select: (preset: string) => Promise<void>;
20
+ }
21
+ /** Full component props. */
22
+ export type PermissionRowProps = PropsRuntime<'settings.general.item'> & PropsLocale<'settings.permission'> & InjectFace<PermissionRowInjected>;
23
+ /**
24
+ * Render the new-session Permission default selector. The row keeps the host's
25
+ * single preset enum on purpose: its options come from the Host settings
26
+ * schema (which still publishes one bundled preset per mode), not from the
27
+ * current-session two-axis catalog, so splitting it here would invent a second
28
+ * source of truth for the same durable value. A visible pick applies directly —
29
+ * the full-access acknowledgement step was removed with the composer's.
30
+ * @param props - composed slot props.
31
+ * @returns the row, or null when the host does not expose permission settings.
32
+ */
33
+ export declare function PermissionRow({ load, select, usePermission, t }: PermissionRowProps): import("react").JSX.Element | null;
34
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
35
+ interface LocaleNamespaceMap {
36
+ /** Permission row copy. */
37
+ 'settings.permission': PermissionSettingsKey;
38
+ }
39
+ }
@@ -0,0 +1,89 @@
1
+ import type { HostObservable, InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { PermissionAxis } from './presentation.ts';
3
+ import type { PermissionCatalogState } from './catalog.ts';
4
+ import { PERMISSION_ACCESS_NS } from './locales.ts';
5
+ import type { DualAxisAxisWire } from './presentation.ts';
6
+ import type { SessionAxesView } from './session-axes-data.ts';
7
+ import { type RuleGroupOption } from './axis-editor.ts';
8
+ import { type AxisEditorValue } from './PermissionCustomEditor.tsx';
9
+ /** Business face injected by the permission package's slot registration. */
10
+ export interface PermissionSelectInjected {
11
+ hooks: {
12
+ /** One process catalog shared with the slash popup (axis values + defaults). */
13
+ permissionCatalog: HostObservable<PermissionCatalogState>;
14
+ };
15
+ /**
16
+ * Submit one axis switch for the current session. Each dropdown writes only
17
+ * its own axis. The three closed values are complete statements and go
18
+ * straight through; `custom` is not one — it is assembled by the path editor,
19
+ * which is why this surface never sends a bare `<axis>:custom`.
20
+ *
21
+ * `expected` is the value the submission MEANS, and it is what makes a refused
22
+ * write visible: the host's `Session.command` answers whether the line resolved
23
+ * to a registered command, not whether that command succeeded
24
+ * (`packages/api/session-controller/src/client/sessions/session.ts:387-391` —
25
+ * `matched: result.value !== undefined`), so a `/axis` entry the host refuses
26
+ * settles as a perfectly ordinary admission. The implementation therefore reads
27
+ * the stored pair back out of the settings document and answers `false` when it
28
+ * never becomes `expected`.
29
+ * @param axis - which axis the entry writes.
30
+ * @param args - the complete entry string after the command name.
31
+ * @param expected - the axis value this entry is meant to store.
32
+ * @returns whether the stored pair now carries `expected` on `axis`.
33
+ */
34
+ select: (axis: PermissionAxis, args: string, expected: DualAxisAxisWire) => Promise<boolean>;
35
+ /**
36
+ * The deployment default for one axis, read from the dual-axis settings
37
+ * section — the pair a NEW session starts from. Only a conversation that has
38
+ * no value of its own for that axis opens the editor on it; a live session's
39
+ * own axes are what the editor opens on. Undefined when the deployment serves
40
+ * no such section or keeps preferences process-local; the editor's own
41
+ * deployment fallback (read `all`, write `workspace`) covers that case
42
+ * without an error and without widening the write axis.
43
+ * @param axis - which axis's deployment default to read.
44
+ * @returns the deployment value, or undefined when there is none.
45
+ */
46
+ global: (axis: PermissionAxis) => PermissionAxisEditorSource | undefined;
47
+ /**
48
+ * 设置页当前定义的规则组,编辑器多选列表的来源。
49
+ *
50
+ * 与 {@link global} 不同的是这里每次都取**当刻**的库:编辑器只把库读成一份勾选
51
+ * 列表,用户勾了什么存在它自己的草稿里,所以设置页新增或改名一个组只是多出/改掉
52
+ * 一行,不会替掉已经勾好的清单。
53
+ * @returns 每个可用的组一项;设置页没有定义任何组时是空列表。
54
+ */
55
+ groupLibrary: () => readonly RuleGroupOption[];
56
+ /**
57
+ * 这条会话当刻的那一对轴,从配置文档镜像里按会话 id 读。
58
+ *
59
+ * 与 {@link groupLibrary} 同一条读路径(共享的 describe 镜像),但它是**订阅式**的:
60
+ * 宿主改完轴会发 `settings/document-updated`,镜像重读,组件跟着重渲染。
61
+ */
62
+ sessionAxes: SessionAxesInjected;
63
+ }
64
+ /** 会话轴取值面:一个订阅式 hook 加一个同步取值函数。 */
65
+ export interface SessionAxesInjected {
66
+ /**
67
+ * React hook:当前会话该显示的那一对轴,以及那份取值的来源。
68
+ *
69
+ * 返回的永远是三态。没有记录时**不是** undefined 而是 `{ state: 'default' }`:
70
+ * 那种情形下宿主执行的正是这条会话的**种子**(设置页那一行,子代理子会话则是父会话
71
+ * 当刻的记录),界面按同一份计算显示同一个值 —— 显示内置默认对就会「界面显示 A、
72
+ * 宿主执行 B」,设置页默认是「禁止读」时那一轮仍会被当成「全盘读」。只有连文档都读
73
+ * 不到时才是 `{ state: 'unknown' }`,由两个下拉如实显示「未设置」。
74
+ * @param sessionId - 当前会话 id。
75
+ * @returns 三态取值。
76
+ */
77
+ useAxes: (sessionId: string | undefined) => SessionAxesView;
78
+ /**
79
+ * 同一个值的同步读取,供非 React 调用方(编辑器初值、提交后的核对)使用。
80
+ * @param sessionId - 当前会话 id。
81
+ * @returns 三态取值。
82
+ */
83
+ axesOf: (sessionId: string | undefined) => SessionAxesView;
84
+ }
85
+ /** One axis's value as the editor reads it: the kind plus the custom detail. */
86
+ export type PermissionAxisEditorSource = AxisEditorValue;
87
+ /** Complete props derived from the conversation slot, injected hooks, and locale. */
88
+ export type PermissionSelectProps = PropsRuntime<'conversation.input.permission'> & InjectFace<PermissionSelectInjected> & PropsLocale<typeof PERMISSION_ACCESS_NS>;
89
+ export declare function PermissionSelect({ locked, select, global, groupLibrary, sessionAxes, usePermissionCatalog, t, sessionId, }: PermissionSelectProps): import("react").JSX.Element | null;
@@ -0,0 +1,263 @@
1
+ /**
2
+ * 输入框上方两个访问轴下拉的 `custom` 路径编辑器:把一条轴收成可编辑的草稿、
3
+ * 把草稿收成宿主 `/permission` 命令接受的参数串,并给出全局初值与兜底默认值。
4
+ *
5
+ * 这里不碰 React、不碰 DOM、也不碰宿主 —— 界面半边每次展开编辑器与每次提交都走
6
+ * 这几个函数,因此"界面上能编的形状"与"宿主解析器接受的形状"只有这一份定义。
7
+ * 宿主的语法在 `dsh-dual-axis` 的 `parseAxisEntry` / `parseCustomFragment`:
8
+ * `<axis>:<kind>[:base=<deny|workspace|all>,groups=<id>|<id>,allow=<绝对路径>,deny=<绝对路径>]`,
9
+ * `allow` / `deny` 重复 key 表示多条、逗号分隔,路径必须是绝对路径;`groups` 只写
10
+ * **组 id**,组体由宿主在判定时按 id 现取。
11
+ *
12
+ * @module @deepseek-ai/dsh-client-ui-permission-presets/client/axis-editor
13
+ */
14
+ import type { PermissionAxis, PermissionAxisValue } from './presentation.ts';
15
+ /** `custom` 可叠加的三个底座,与宿主 `AxisBase` 同一套字面量。 */
16
+ export declare const AXIS_BASES: readonly ["deny", "workspace", "all"];
17
+ /** 一个底座取值。 */
18
+ export type AxisBase = typeof AXIS_BASES[number];
19
+ /**
20
+ * 一条轴在编辑器里的草稿。四个取值共用同一种形状:切到 `custom` 再切回来时,
21
+ * 已经打好的路径不会丢,只是不再参与提交。
22
+ */
23
+ export interface AxisDraft {
24
+ /** 当前选中的轴取值。 */
25
+ kind: PermissionAxisValue;
26
+ /** `custom` 的底座;非 `custom` 时保留上一次的选择。 */
27
+ base: AxisBase;
28
+ /**
29
+ * 这条对话预设里引用的规则组 id。
30
+ *
31
+ * 存的是 **id 而不是组体**:组的内容永远只有设置页那一份,判定时按 id 现取
32
+ * (规格 §「为什么对话里绝不能存组体」)。顺序即提交顺序。
33
+ */
34
+ groups: readonly string[];
35
+ /** 额外放行的绝对路径,每行一条。 */
36
+ allow: string;
37
+ /** 额外禁止的绝对路径,每行一条;与底座取并后再减去它们,禁止优先。 */
38
+ deny: string;
39
+ }
40
+ /**
41
+ * 设置页定义的一个规则组,编辑器多选用的那一份。
42
+ *
43
+ * `read` / `write` 是这个组覆盖了哪几根轴:写侧组出现在读轴的选择器里不会报错,
44
+ * 但它对这条读轴什么也不做,所以界面要如实标出来。
45
+ */
46
+ export interface RuleGroupOption {
47
+ /** 会话引用的稳定标识。 */
48
+ readonly id: string;
49
+ /** 人读的名字;设置页留空时宿主用 id 顶上。 */
50
+ readonly name: string;
51
+ /** 这个组是否带读侧规则。 */
52
+ readonly read: boolean;
53
+ /** 这个组是否带写侧规则。 */
54
+ readonly write: boolean;
55
+ }
56
+ /** 多选列表里的一行:设置页定义的组,或一个已经引用不到定义的 id。 */
57
+ export interface GroupChoice {
58
+ /** 组 id。 */
59
+ readonly id: string;
60
+ /** 显示名;引用不到的 id 用 id 本身。 */
61
+ readonly name: string;
62
+ /** 这个组覆盖哪些轴;引用不到的 id 是 `none`。 */
63
+ readonly coverage: 'both' | 'read' | 'write' | 'none';
64
+ /** 设置页当前是否定义了这个 id。 */
65
+ readonly missing: boolean;
66
+ /** 这一行是否被勾选。 */
67
+ readonly checked: boolean;
68
+ }
69
+ /**
70
+ * 把设置节的 `groups` 读成多选用的组清单。
71
+ *
72
+ * 读取是宽容的:读不懂的成员跳过,不抛 —— 这个清单只是给人选的东西,坏取值由宿主
73
+ * 的 `parseLibrary` 在判定时响亮失败,界面不该因为一个坏成员就整块打不开。
74
+ * @param value - 设置节里的 `groups`,未信任。
75
+ * @returns 每个可用的组一项,顺序与文档一致,id 去重。
76
+ */
77
+ export declare function groupOptionsOf(value: unknown): readonly RuleGroupOption[];
78
+ /**
79
+ * 多选列表的完整内容:设置页定义的组,加上这条对话已经引用、但设置页不再定义的 id。
80
+ *
81
+ * 后一类必须留在列表里并保持勾选:它们缺席就等于「打开编辑器再应用」会**静默**把
82
+ * 引用删掉,而宿主正是靠这些引用响亮失败的(规格 §「引用不到的组 id → 响亮失败」)。
83
+ * 取消勾选仍然允许 —— 移除一个引用属于收窄,永远允许。
84
+ * @param library - 设置页当前定义的组。
85
+ * @param selected - 这条对话预设里引用的 id。
86
+ * @returns 列表的每一行。
87
+ */
88
+ export declare function groupChoicesOf(library: readonly RuleGroupOption[], selected: readonly string[]): readonly GroupChoice[];
89
+ /**
90
+ * 编辑器里一条轴的起始取值:会话当前值,或部署设置值。
91
+ *
92
+ * 两条都是**未信任输入**:会话那条来自宿主投影,部署那条来自设置文档。读不懂的
93
+ * 成员由 {@link initialDraft} 逐项兜底,界面不会因为一个坏取值打不开。
94
+ */
95
+ export interface AxisEditorValue {
96
+ /** 轴取值。 */
97
+ kind: string;
98
+ /** `custom` 时的底座。 */
99
+ base?: string;
100
+ /** `custom` 时这条预设引用的规则组 id。 */
101
+ groups?: readonly string[];
102
+ /** `custom` 时额外放行的绝对路径。 */
103
+ allow?: readonly string[];
104
+ /** `custom` 时额外禁止的绝对路径。 */
105
+ deny?: readonly string[];
106
+ }
107
+ /**
108
+ * 某一轴拿不到任何配置时使用的兜底轴:读轴 `all`、写轴 `workspace`,
109
+ * 即 `dsh-sandbox` 的 `DEFAULT_READ_SCOPE` / `DEFAULT_WRITE_SCOPE`。
110
+ *
111
+ * 这两条是**部署默认值**,不是"兜底给全盘权限":读轴 `all`、写轴 `workspace`
112
+ * 正是 `dsh-sandbox` 里那两条默认轴,所以拿不到全局配置时也不会把写权限放大。
113
+ * @param axis - 轴名。
114
+ * @returns 该轴的兜底取值。
115
+ */
116
+ export declare function fallbackAxis(axis: PermissionAxis): Exclude<PermissionAxisValue, 'custom'>;
117
+ /**
118
+ * 一条空白草稿:以给定取值起步,并以该取值本身为底座。
119
+ *
120
+ * 底座取"这条轴当前的取值"而不是写死的 `workspace`:拿不到任何全局配置时,读轴的
121
+ * 兜底是 `all`,于是编辑器的底座也必须是 `all` —— 若写死 `workspace`,兜底打开的
122
+ * 编辑器会静默把读轴收窄成会话工作区,与那条兜底值本身矛盾。
123
+ * @param kind - 起步取值,同时用作底座。
124
+ * @returns 一份可编辑的草稿。
125
+ */
126
+ export declare function emptyDraft(kind: PermissionAxisValue): AxisDraft;
127
+ /** 一条 `groups` 项里分隔组 id 的字符;与宿主 `GROUP_ID_SEPARATOR` 同一个字符。 */
128
+ export declare const GROUP_ID_SEPARATOR = "|";
129
+ /**
130
+ * 一条轴的起始草稿:**这条会话自己的轴**优先,它缺席时才退到部署设置值,再没有
131
+ * 才用该轴的部署默认值。它永远不会空着打开,也永远不抛 —— 兜底就是这个函数的
132
+ * 全部意义。
133
+ *
134
+ * 「会话优先」不是顺序偏好而是这个界面的定义:编辑器表达的就是这条会话,打开时
135
+ * 显示别的值会与它旁边那个下拉自相矛盾。会话读轴是 `deny` 时初值必须是 `deny`,
136
+ * 不是部署默认的 `all`。
137
+ * @param axis - 正在编辑的轴。
138
+ * @param current - 这条会话在该轴上的取值,宿主投影还没有这条轴时为 undefined。
139
+ * @param global - 部署设置在该轴上的取值,只在会话没有取值时兜底。
140
+ * @returns 一份不用重打任何东西就能提交的草稿。
141
+ */
142
+ export declare function initialDraft(axis: PermissionAxis, current: AxisEditorValue | undefined, global: AxisEditorValue | undefined): AxisDraft;
143
+ /**
144
+ * 一个写法是否为绝对路径。相对路径只能相对宿主进程的 cwd 解析,永远不是写这份配置
145
+ * 的人想说的位置,所以与宿主一样在这里就拒绝,而不是留给宿主报一句难读的错。
146
+ * @param value - 待检查的路径写法。
147
+ * @returns 该写法是否为绝对路径。
148
+ */
149
+ export declare function isAbsolutePath(value: string): boolean;
150
+ /**
151
+ * 把一个多行文本框读成路径列表:去掉空行与首尾空白。
152
+ * @param text - 文本框里的原文。
153
+ * @returns 其中每一行作为一条路径。
154
+ */
155
+ export declare function pathLines(text: string): string[];
156
+ /** 一份草稿收成参数串的结果:成功给出片段,失败给出一句可读的说明。 */
157
+ export type DraftParse = {
158
+ ok: true;
159
+ fragment: string;
160
+ } | {
161
+ ok: false;
162
+ problem: string;
163
+ };
164
+ /**
165
+ * 把一份 `custom` 草稿收成宿主解析器接受的参数片段
166
+ * (`base=<…>,groups=<id>|<id>,allow=<…>,deny=<…>`,`allow` / `deny` 每条重复一次 key)。
167
+ *
168
+ * 路径里的逗号、以及组 id 里的三类分隔符都会被宿主的解析器当成别的东西,因此它们
169
+ * 在这里就拒绝,而不是拼出一条宿主读成另一个意思的命令行。
170
+ * @param draft - 编辑器里的草稿。
171
+ * @param problemOf - 生成本地化说明的回调。
172
+ * @returns 参数片段,或一句说明这份草稿为什么还不能提交的话。
173
+ */
174
+ export declare function customFragment(draft: AxisDraft, problemOf: (key: 'baseUnknown' | 'notAbsolute' | 'commaUnsupported' | 'groupUnsupported') => string): DraftParse;
175
+ /**
176
+ * 一份 `custom` 草稿提交出去之后,宿主文档里应当出现的那条轴值。
177
+ *
178
+ * 与 {@link customFragment} 同一份草稿、同一套取值:片段是发给宿主的那句话,这里是
179
+ * 那句话落成记录之后的形状。两者一起用才有意义 —— 宿主对自己没写的改动同样会回一句
180
+ * 「命令执行了」(`Session.command` 的 `matched` 只说明命令被解析到,见
181
+ * `packages/api/session-controller/src/client/sessions/session.ts:387-391`),所以界面
182
+ * 只能拿写入之后的记录来核对这次提交到底有没有生效。
183
+ *
184
+ * 组 id 去重,因为宿主 `normalizeScope` 的 `groupIds` 也去重;路径列表保持顺序,
185
+ * 因为宿主的 `pathList` 保序。
186
+ * @param draft - 编辑器里的草稿。
187
+ * @returns 宿主文档里那条轴的形状。
188
+ */
189
+ export declare function draftScope(draft: AxisDraft): AxisEditorScope;
190
+ /**
191
+ * 宿主文档里一条轴值的形状(`dual-axis-sessions` 的记录成员)。
192
+ *
193
+ * 只有这里真的会读的成员:`groups` 是这条轴引用的组 id,`allow` / `deny` 是它自己
194
+ * 追加与排除的绝对路径。
195
+ */
196
+ export interface AxisEditorScope {
197
+ /** 轴取值。 */
198
+ readonly kind: string;
199
+ /** `custom` 的底座。 */
200
+ readonly base?: string;
201
+ /** `custom` 引用的规则组 id。 */
202
+ readonly groups?: readonly string[];
203
+ /** `custom` 额外放行的绝对路径。 */
204
+ readonly allow?: readonly string[];
205
+ /** `custom` 额外排除的绝对路径。 */
206
+ readonly deny?: readonly string[];
207
+ }
208
+ /**
209
+ * 两条轴值是不是同一件事 —— 按值比,不按引用比。
210
+ *
211
+ * 比较的是宿主写进文档之后的那份形状:`groups` / `allow` / `deny` 只有一条轴真的是
212
+ * `custom` 时才参与比较(`deny` / `workspace` / `all` 都不带这些成员),列表按顺序比,
213
+ * 因为顺序是取值的一部分(提交顺序就是用户勾选的顺序)。
214
+ * @param left - 一条轴值。
215
+ * @param right - 另一条轴值。
216
+ * @returns 两者是否描述同一条轴。
217
+ */
218
+ export declare function sameStoredAxis(left: AxisEditorScope | undefined, right: AxisEditorScope | undefined): boolean;
219
+ /**
220
+ * 把一个 `custom` 取值收成编辑器草稿(当前值显示用)。
221
+ * @param axis - 该取值所属的轴。
222
+ * @param value - 轴值,可能是 `custom` 并带底座与两个列表。
223
+ * @returns 一份可编辑的草稿。
224
+ */
225
+ export declare function draftOfAxisState(axis: PermissionAxis, value: {
226
+ kind: PermissionAxisValue;
227
+ base?: string;
228
+ groups?: readonly string[];
229
+ allow?: readonly string[];
230
+ deny?: readonly string[];
231
+ }): AxisDraft;
232
+ /**
233
+ * 一条轴取值是否会**放开**权限:只有 `all`(整台主机)与带额外放行路径的 `custom`
234
+ * 会,`deny` / `workspace` 和空清单的 `custom` 不会。
235
+ *
236
+ * 这个判定就是发布版二次确认闸的开关。它按**取值本身**算,不按界面动作算:同一条
237
+ * `write:all`,无论从下拉选出还是从编辑器拼出,都要过一次同样的确认。
238
+ * @param value - 轴取值。
239
+ * @returns 该取值是否会放大访问范围。
240
+ */
241
+ export declare function widensAccess(value: PermissionAxisValue): boolean;
242
+ /**
243
+ * 一条 `custom` 提交片段是否会放开权限:底座是 `all`,或带了任何额外放行路径。
244
+ * 排除清单不收窄判定 —— `base=all,deny=/x` 仍然是整台主机减去一条路径。
245
+ * @param fragment - `customFragment` 产出的片段。
246
+ * @returns 该片段是否会放大访问范围。
247
+ */
248
+ export declare function fragmentWidensAccess(fragment: string): boolean;
249
+ /**
250
+ * 把一条轴值读成一行摘要(当前值显示用),例如 `自定义:基于 工作区,+3 −1`。
251
+ * @param value - 轴值。
252
+ * @param text - 文案表。
253
+ * @returns 一行说明。
254
+ */
255
+ export declare function axisSummary(value: {
256
+ kind: PermissionAxisValue;
257
+ base?: string;
258
+ allow?: readonly string[];
259
+ deny?: readonly string[];
260
+ }, text: {
261
+ custom: (base: string, allow: number, deny: number) => string;
262
+ base: (base: string) => string;
263
+ }): string | undefined;
@@ -0,0 +1,62 @@
1
+ /** Identity-stable process permission catalog shared by both selection surfaces. */
2
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
3
+ import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
4
+ import type { PermissionCatalog } from '@deepseek-ai/dsh-permission-presets/client';
5
+ /** Observable complete catalog for the current Host generation. */
6
+ export interface PermissionCatalogState {
7
+ /** Last complete catalog for this generation, or null before one succeeds. */
8
+ value: PermissionCatalog | null;
9
+ }
10
+ /** One latest-result-wins catalog reader for the whole browser process. */
11
+ export declare class PermissionCatalogDirectory {
12
+ private readonly ctx;
13
+ /** Complete snapshot consumed by both the slash popup and composer seat. */
14
+ readonly store: SnapshotStore<PermissionCatalogState>;
15
+ /**
16
+ * One tick per invalidation (a catalog notification or a connection-generation
17
+ * change), published before the replacement read starts. Consumers that must
18
+ * drop displayed options subscribe here instead of to {@link store}, whose
19
+ * publications also settle a read a displayed surface is waiting for.
20
+ */
21
+ readonly invalidations: SnapshotStore<{
22
+ count: number;
23
+ }>;
24
+ private readonly connection;
25
+ private readonly stopCatalog;
26
+ private readonly stopGeneration;
27
+ private generationId;
28
+ private initialized;
29
+ private epoch;
30
+ private pending;
31
+ private failure;
32
+ private disposed;
33
+ /**
34
+ * Subscribe to both invalidation sources before the first read, closing the
35
+ * install/read race.
36
+ * @param ctx - root Client context carrying Remote and Connection.
37
+ */
38
+ constructor(ctx: ClientContext);
39
+ /**
40
+ * Publish one invalidation tick for consumers holding displayed options.
41
+ * Neither caller can run after disposal: `dispose()` unsubscribes the
42
+ * catalog-changed listener, and `syncGeneration()` returns early when the
43
+ * directory is disposed.
44
+ */
45
+ private invalidate;
46
+ /** Force a fresh complete read for the active connection generation. */
47
+ refresh(): void;
48
+ /**
49
+ * Resolve a complete current-generation catalog for an imperative popup
50
+ * open. An active refresh settles before a retained value can be reused.
51
+ * @returns The active Host generation's complete permission catalog.
52
+ */
53
+ load(): Promise<PermissionCatalog>;
54
+ /** Stop subscriptions and revoke every late settlement's write access. */
55
+ dispose(): void;
56
+ /** Observe generation loss/replacement and hard-clear the old Host value. */
57
+ private syncGeneration;
58
+ /** Start one independent read; the newest epoch in the same generation wins. */
59
+ private startRead;
60
+ /** Fence by disposal, refresh epoch, and the actual Connection generation. */
61
+ private accepts;
62
+ }
@@ -0,0 +1,54 @@
1
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
2
+ import { accessEn } from './locales.ts';
3
+ export type { AxisLabelKey, PermissionPresetLabelKey } from './presentation.ts';
4
+ export type { PermissionRowInjected, PermissionRowProps } from './PermissionRow.tsx';
5
+ export type { PermissionCatalogState } from './catalog.ts';
6
+ export type { PermissionSelectInjected, PermissionSelectProps, PermissionAxisEditorSource, } from './PermissionSelect.tsx';
7
+ export type { PermissionDefaultOption, PermissionSettingsState, } from './settings-store.ts';
8
+ /**
9
+ * 双轴组合包在宿主上注册的设置节名(`@t4r71/dsh-dual-axis`)。
10
+ *
11
+ * 0.1.7 的设置命名空间就是 Loader entry id(`packages/settings/settings/src/index.ts`
12
+ * 的 `describe()` 按已装载 entry 投影),而宿主那一行在 `cordis.patch.yml` 里就叫
13
+ * `dual-axis`(`packages/bundle/dual-axis/src/config.ts` 的 `DUAL_AXIS_ROW_ID`)。
14
+ * 0.1.6 的 `sandbox-axis` 已经退役:写它取不到任何值,于是这张表单永远返回
15
+ * undefined,编辑器的部署兜底静默退化成编译期默认。
16
+ *
17
+ * 这一节持有「新会话从哪两条轴开始」。两个下拉的路径编辑器只在**会话还没有这条轴**
18
+ * 时以它为初值;对话级的选择永远优先。
19
+ */
20
+ export declare const DUAL_AXIS_SETTINGS_NAMESPACE = "dual-axis";
21
+ /** 提交之后最多等多久,等这次取值出现在设置文档里。 */
22
+ export declare const AXIS_CONFIRM_TIMEOUT_MS = 3000;
23
+ /** 上面那段等待的轮询间隔。 */
24
+ export declare const AXIS_CONFIRM_POLL_MS = 100;
25
+ /**
26
+ * 双轴设置节的取值形状。两条轴都当作未信任输入读:文档可能被人手改过,也可能由
27
+ * 别的写入方改过,所以这里只声明"是个对象",真正的收窄在
28
+ * {@link PermissionSelect} 的 `globalOf` 里按 `kind` 判定;读不懂就当作没有部署值,
29
+ * 由编辑器用该轴的部署默认值兜底。
30
+ */
31
+ export interface DualAxisSection {
32
+ /** 新会话起步的读轴。 */
33
+ read?: unknown;
34
+ /** 新会话起步的写轴。 */
35
+ write?: unknown;
36
+ /** 组库:会话按 id 引用的具名规则片段。多选列表只读它。 */
37
+ groups?: unknown;
38
+ /** 新会话默认加载哪些组;判定路径不读它,界面这边也不读。 */
39
+ defaultGroups?: unknown;
40
+ }
41
+ /** Required services (cordis fiber inject). */
42
+ export declare const inject: string[];
43
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
44
+ interface LocaleNamespaceMap {
45
+ /** Current-session preset picker and read/write axis copy. */
46
+ 'permission.access': keyof typeof accessEn;
47
+ }
48
+ }
49
+ /**
50
+ * Client plugin body: register the /permission popup picker over the
51
+ * permissions projection.
52
+ * @param ctx - client root context.
53
+ */
54
+ export declare function apply(ctx: ClientContext): void;