@deepseek-ai/dsh-client-ui-permission-presets 0.0.1-rc.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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DeepSeek
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/client/ui-permission-presets/README.md
5
+ README.md: 20529e6ce8a187254774fc85cfd981e4a9115937
6
+ README.zh.md: afe7ff36e52511a182a783153879125d48cccca5
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @deepseek-ai/dsh-client-ui-permission-presets
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ Permission browser surfaces for two different lifetimes. The General-settings row reads the explicitly exposed `permission` Settings descriptor, derives its options from the host's dynamic `defaultPreset` enum, and writes one `settings.mutate` path operation with the descriptor revision. Its observable rides the slot system's `hooks` compartment, so the renderer owns React hook binding; a push invalidation refetches the descriptor. This value applies only when a later session is created; changing it does not switch the current session. Choosing Full access requires an explicit risk acknowledgement before the row writes it.
6
+
7
+ The current-session surface remains a popupSelect DECORATION hung on the host `/permission` command (`ctx.commandUi.decorate`). A decoration is not a second command — the host command keeps its slash-menu row, the argued path (`/permission <preset>` switches directly), and the durable lifecycle logging; the decoration replaces only the bare invocation with the picker: one flat preset list with the current value marked active and kebab-case preset names rendered as title-case labels (`workspace-write` → `Workspace Write`, the composer chip's display transform twin), where a pick submits the `/permission <preset>` command line. Options and the active mark read the session's `permissions` projection (the same host-computed select the composer chip renders), so both current-session surfaces share one read source and one write path, and the pushed projection frame is the single confirmation both follow. The decoration is available exactly while the projection key is present; a permission-less composition shows neither picker nor Settings row.
8
+
9
+ The `/client` exports are the plugin body (`apply`/`inject`).
10
+
11
+ ## Model Experience
12
+
13
+ Indirectly, through the permission facts written by its two surfaces: the Settings row causes a future session to start with whole-value knob events (`permission/preset`, `sandbox/mode`, `approval/policy`), while the `/permission` picker appends the same facts when it switches the current session; those events select the sandbox mode and approval policy later tool calls resolve, and picker interaction adds no prompt content.
14
+
15
+ #### KV Cache effect
16
+
17
+ No direct invalidation; the knob consumers own any request-prefix changes.
18
+
19
+ ## Known Limitations and Deferred Work
20
+
21
+ - **The Settings row is Web-only** — non-Web clients may still switch the current session through `/permission`, but do not receive this browser contribution.
package/README.zh.md ADDED
@@ -0,0 +1,21 @@
1
+ # @deepseek-ai/dsh-client-ui-permission-presets
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 面向两种不同生命周期的浏览器权限界面。「通用」设置行读取显式暴露的 `permission` Settings 描述符,从 host 的动态 `defaultPreset` enum 中推导选项,并携带描述符的 revision 写入一条 `settings.mutate` 路径操作。它的 observable 经 slot 系统的 `hooks` 格传递,因此 React 钩子由渲染器绑定;推送的失效通知会重新获取描述符。这个值仅在后续会话创建时生效;改变它不会切换当前会话。选择 Full access 时必须先显式确认风险,该行随后才会写入。
6
+
7
+ 当前会话界面仍是挂在 host `/permission` 命令上的 popupSelect **装饰**(`ctx.commandUi.decorate`)。装饰不是第二条命令——host 命令保留斜杠菜单行、带参路径(`/permission <preset>` 直接切换)与持久生命周期记账;装饰只把裸调用替换为选择框:一张扁平预设列表,当前值标记为 active,kebab-case 预设名渲染为 Title Case 标签(`workspace-write` → `Workspace Write`,与 composer chip 的显示变换孪生),选中即提交 `/permission <preset>` 命令行。选项与 active 标记读取会话的 `permissions` 投影(与 composer chip 渲染的同一份 host 计算 select),因此两个当前会话界面共享同一读源与同一写路径,推送的投影帧是两者共同跟随的唯一确认。装饰恰在投影 key 存在时可用;无权限组合既不显示选择框,也不显示 Settings 行。
8
+
9
+ `/client` 导出面为插件本体(`apply`/`inject`)。
10
+
11
+ ## 模型体验
12
+
13
+ 通过两个界面写入的权限事实间接影响:Settings 行使未来会话带着全量值旋钮事件(`permission/preset`、`sandbox/mode`、`approval/policy`)启动,而 `/permission` 选择框切换当前会话时会追加相同的事实;这些事件决定后续工具调用解析到的沙箱模式与审批策略,选择框交互本身不添加任何提示词内容。
14
+
15
+ #### KV Cache 影响
16
+
17
+ 无直接失效;请求前缀的变化由旋钮消费方自行承担。
18
+
19
+ ## 已知限制与暂缓事项
20
+
21
+ - **Settings 行仅在 Web 中可用**:非 Web 客户端仍可通过 `/permission` 切换当前会话,但不会获得这项浏览器贡献。
package/lib/client.js ADDED
@@ -0,0 +1,461 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@deepseek-ai/dsh-client-ui-permission-presets",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react_jsx_runtime = require("react/jsx-runtime");
8
+ let react = require("react");
9
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
11
+ let _deepseek_ai_dsh_client_schema_form = require("@deepseek-ai/dsh-client-schema-form");
12
+ //#region lib/types/client/presentation.js
13
+ /** Machine value of the preset that requires an explicit GUI risk gate. */
14
+ const FULL_ACCESS_PRESET = "danger-full-access";
15
+ /**
16
+ * Convert conventional kebab-case preset names into user-facing title case.
17
+ * @param name - host-supplied preset label or key.
18
+ * @returns the title-cased conventional key, or a non-kebab label unchanged.
19
+ */
20
+ function displayPresetName(name) {
21
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) return name;
22
+ return name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
23
+ }
24
+ /**
25
+ * Render a permission preset under its product label.
26
+ * @param value - preset machine value.
27
+ * @param name - host-supplied preset name.
28
+ * @returns the Full access product label or the conventional display name.
29
+ */
30
+ function displayPermissionPreset(value, name) {
31
+ return value === "danger-full-access" ? "Full access" : displayPresetName(name);
32
+ }
33
+ //#endregion
34
+ //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-permission-presets/src/client/PermissionRow.module.css.mjs
35
+ const css = ".oY77xG_row{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:16px 0;display:flex}.oY77xG_rowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}.oY77xG_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.oY77xG_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.oY77xG_selector{background:var(--dsw-alias-bg-module-platform);height:36px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:18px;align-items:center;gap:12px;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.oY77xG_selector:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.oY77xG_selector:disabled{cursor:default}.oY77xG_chevron{flex:none}";
36
+ const tagId = "@deepseek-ai/dsh-client-ui-permission-presets/PermissionRow.module.css";
37
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
38
+ const tag = document.createElement("style");
39
+ tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-permission-presets";
40
+ tag.dataset.pluginCss = tagId;
41
+ tag.textContent = css;
42
+ document.head.appendChild(tag);
43
+ }
44
+ var PermissionRow_module_css_default = {
45
+ "desc": "oY77xG_desc",
46
+ "title": "oY77xG_title",
47
+ "selector": "oY77xG_selector",
48
+ "rowText": "oY77xG_rowText",
49
+ "row": "oY77xG_row",
50
+ "chevron": "oY77xG_chevron"
51
+ };
52
+ //#endregion
53
+ //#region lib/types/client/PermissionRow.js
54
+ /**
55
+ * Permission preference row: the default preset for subsequently created
56
+ * sessions. Current-session switches remain on the composer `/permission`
57
+ * control.
58
+ */
59
+ /**
60
+ * Render the new-session Permission default selector.
61
+ * @param props - composed slot props.
62
+ * @returns the row, or null when the host does not expose permission settings.
63
+ */
64
+ function PermissionRow({ load, select, usePermission, t }) {
65
+ const state = usePermission((snapshot) => snapshot);
66
+ const [open, setOpen] = (0, react.useState)(false);
67
+ const [confirmingFullAccess, setConfirmingFullAccess] = (0, react.useState)(false);
68
+ const [acknowledged, setAcknowledged] = (0, react.useState)(false);
69
+ (0, react.useEffect)(() => {
70
+ load();
71
+ }, [load]);
72
+ (0, react.useEffect)(() => {
73
+ if (state.writable && state.status !== "unavailable") return;
74
+ setOpen(false);
75
+ setAcknowledged(false);
76
+ setConfirmingFullAccess(false);
77
+ }, [state.status, state.writable]);
78
+ if (state.status === "unavailable") return null;
79
+ const selected = state.options.find((option) => option.id === state.currentValue);
80
+ const busy = state.status === "loading" || state.status === "saving" || confirmingFullAccess;
81
+ const label = selected?.label ?? (busy ? t("loading") : t("unavailable"));
82
+ const description = state.error ?? t("description");
83
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
84
+ className: PermissionRow_module_css_default.row,
85
+ children: [(0, react_jsx_runtime.jsxs)("div", {
86
+ className: PermissionRow_module_css_default.rowText,
87
+ children: [(0, react_jsx_runtime.jsx)("div", {
88
+ className: PermissionRow_module_css_default.title,
89
+ children: t("title")
90
+ }), (0, react_jsx_runtime.jsx)("div", {
91
+ className: PermissionRow_module_css_default.desc,
92
+ role: state.error === null ? void 0 : "alert",
93
+ children: description
94
+ })]
95
+ }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
96
+ open,
97
+ onClose: () => {
98
+ setOpen(false);
99
+ },
100
+ items: state.options.map((option) => ({
101
+ id: option.id,
102
+ label: option.label
103
+ })),
104
+ selectedId: state.currentValue,
105
+ onSelect: (id) => {
106
+ setOpen(false);
107
+ if (id === state.currentValue) return;
108
+ if (id === "danger-full-access") {
109
+ setAcknowledged(false);
110
+ setConfirmingFullAccess(true);
111
+ return;
112
+ }
113
+ select(id);
114
+ },
115
+ align: "end",
116
+ portal: true,
117
+ anchor: (0, react_jsx_runtime.jsxs)("button", {
118
+ type: "button",
119
+ className: PermissionRow_module_css_default.selector,
120
+ "aria-haspopup": "menu",
121
+ "aria-expanded": open,
122
+ disabled: busy || !state.writable || state.options.length === 0,
123
+ onClick: () => {
124
+ setOpen((value) => !value);
125
+ },
126
+ children: [label, (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: PermissionRow_module_css_default.chevron })]
127
+ })
128
+ })]
129
+ }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.RiskConfirmation, {
130
+ open: confirmingFullAccess,
131
+ title: t("confirm.title"),
132
+ description: t("confirm.description"),
133
+ acknowledgeLabel: t("confirm.acknowledge"),
134
+ cancelLabel: t("confirm.cancel"),
135
+ confirmLabel: t("confirm.enable"),
136
+ acknowledged,
137
+ disabled: !state.writable || state.status === "saving",
138
+ onAcknowledgedChange: setAcknowledged,
139
+ onCancel: () => {
140
+ setAcknowledged(false);
141
+ setConfirmingFullAccess(false);
142
+ },
143
+ onConfirm: () => {
144
+ setAcknowledged(false);
145
+ setConfirmingFullAccess(false);
146
+ select(FULL_ACCESS_PRESET);
147
+ }
148
+ })] });
149
+ }
150
+ //#endregion
151
+ //#region lib/types/client/locales.js
152
+ /** `settings.permission` namespace dictionaries (the Permission row's copy). */
153
+ /** Simplified Chinese dictionary (the key-set source of truth). */
154
+ const zh = {
155
+ "title": "权限",
156
+ "description": "选择新会话的默认权限模式",
157
+ "loading": "加载中",
158
+ "unavailable": "不可用",
159
+ "confirm.title": "确认启用 Full access?",
160
+ "confirm.description": "启用 Full access 后,新会话将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任后续任务时使用。",
161
+ "confirm.acknowledge": "我已了解风险,并愿意继续",
162
+ "confirm.cancel": "取消",
163
+ "confirm.enable": "启用 Full access"
164
+ };
165
+ /** English dictionary, checked complete against the zh key set. */
166
+ const en = {
167
+ "title": "Permission",
168
+ "description": "Choose the default permission mode for new sessions",
169
+ "loading": "Loading",
170
+ "unavailable": "Unavailable",
171
+ "confirm.title": "Enable Full access?",
172
+ "confirm.description": "Full access lets new sessions reduce confirmation steps and perform more actions directly, including sensitive operations, file changes, or external commands. Only use it when you trust subsequent tasks.",
173
+ "confirm.acknowledge": "I understand the risks and want to continue",
174
+ "confirm.cancel": "Cancel",
175
+ "confirm.enable": "Enable Full access"
176
+ };
177
+ /** Simplified Chinese dictionary for the current-session popup gate. */
178
+ const accessZh = {
179
+ "confirm.title": "确认启用 Full access?",
180
+ "confirm.description": "启用 Full access 后,agent 将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。",
181
+ "confirm.acknowledge": "我已了解风险,并愿意继续",
182
+ "confirm.cancel": "取消",
183
+ "confirm.enable": "启用 Full access"
184
+ };
185
+ /** English dictionary for the current-session popup gate. */
186
+ const accessEn = {
187
+ "confirm.title": "Enable Full access?",
188
+ "confirm.description": "Full access reduces confirmation steps and lets the agent perform more actions directly, including sensitive operations, file changes, or external commands. Only use it when you trust the current task.",
189
+ "confirm.acknowledge": "I understand the risks and want to continue",
190
+ "confirm.cancel": "Cancel",
191
+ "confirm.enable": "Enable Full access"
192
+ };
193
+ //#endregion
194
+ //#region lib/types/client/settings-store.js
195
+ /**
196
+ * Permission default-settings controller. The host descriptor supplies the
197
+ * current value and the dynamic preset enum; writes target only
198
+ * `defaultPreset` and carry the descriptor revision.
199
+ */
200
+ /** Permission's settings namespace on the host wire. */
201
+ const PERMISSION_SETTINGS_NS = "permission";
202
+ /**
203
+ * Read the dynamic preset enum encoded by the host's `defaultPreset` schema.
204
+ * @param view - permission namespace descriptor.
205
+ * @returns current value and selectable options.
206
+ */
207
+ function permissionDefaultOf(view) {
208
+ const value = view.value?.defaultPreset;
209
+ if (typeof value !== "string") throw new Error("permission settings has no defaultPreset value");
210
+ const node = (0, _deepseek_ai_dsh_client_schema_form.nodeAtPath)((0, _deepseek_ai_dsh_client_schema_form.rehydrateSchema)(view.schema), ["defaultPreset"]);
211
+ if (node === void 0) throw new Error("permission settings schema has no defaultPreset field");
212
+ const options = (node.type === "union" ? node.list ?? [] : [node]).flatMap((candidate) => {
213
+ const choice = candidate;
214
+ if (choice.type !== "const" || typeof choice.value !== "string") return [];
215
+ const described = choice.meta?.description;
216
+ return [{
217
+ id: choice.value,
218
+ label: typeof described === "string" && described.length > 0 ? displayPermissionPreset(choice.value, described) : displayPermissionPreset(choice.value, choice.value)
219
+ }];
220
+ });
221
+ if (options.length === 0 || !options.some((option) => option.id === value)) throw new Error("permission settings schema does not advertise its current preset");
222
+ return {
223
+ currentValue: value,
224
+ options
225
+ };
226
+ }
227
+ /** Controller joining Settings reads, writes, and pushed invalidations. */
228
+ var PermissionPresetSettingsController = class {
229
+ api;
230
+ /** Row snapshot consumed through a bound selector hook. */
231
+ store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)({
232
+ status: "idle",
233
+ error: null,
234
+ writable: false,
235
+ currentValue: "",
236
+ options: [],
237
+ revision: 0
238
+ });
239
+ generation = 0;
240
+ view;
241
+ /** @param api - Settings wire face. */
242
+ constructor(api) {
243
+ this.api = api;
244
+ }
245
+ /**
246
+ * Refresh the permission descriptor. Latest request wins.
247
+ * @returns nothing; {@link store} carries success or failure.
248
+ */
249
+ async load() {
250
+ const generation = ++this.generation;
251
+ this.store.update((state) => {
252
+ state.status = "loading";
253
+ state.error = null;
254
+ });
255
+ try {
256
+ const response = await this.api.settings.describe({});
257
+ if (!response.result.ok) throw new Error(response.result.error.message);
258
+ if (generation !== this.generation) return;
259
+ const view = response.result.value.namespaces.find((entry) => entry.ns === PERMISSION_SETTINGS_NS);
260
+ if (view === void 0) {
261
+ this.view = void 0;
262
+ this.store.update((state) => {
263
+ state.status = "unavailable";
264
+ state.writable = false;
265
+ state.currentValue = "";
266
+ state.options = [];
267
+ });
268
+ return;
269
+ }
270
+ this.accept(view, response.result.value.writable);
271
+ } catch (error) {
272
+ if (generation !== this.generation) return;
273
+ this.fail(error);
274
+ }
275
+ }
276
+ /**
277
+ * Persist one preset as the default for subsequently created sessions.
278
+ * @param preset - advertised preset key.
279
+ * @returns nothing; {@link store} carries success or failure.
280
+ */
281
+ async select(preset) {
282
+ const view = this.view;
283
+ const state = this.store.getSnapshot();
284
+ if (view === void 0 || !state.writable) return;
285
+ const generation = ++this.generation;
286
+ this.store.update((draft) => {
287
+ draft.status = "saving";
288
+ draft.error = null;
289
+ });
290
+ try {
291
+ const response = await this.api.settings.mutate({
292
+ ns: PERMISSION_SETTINGS_NS,
293
+ ops: [{
294
+ op: "set",
295
+ path: ["defaultPreset"],
296
+ value: preset
297
+ }],
298
+ expectedRevision: view.revision
299
+ });
300
+ if (generation !== this.generation) return;
301
+ if (!response.result.ok) throw new Error(response.result.error.message);
302
+ this.accept(response.result.value, true);
303
+ } catch (error) {
304
+ if (generation !== this.generation) return;
305
+ this.fail(error);
306
+ }
307
+ }
308
+ /** Stop in-flight responses from publishing after plugin disposal. */
309
+ dispose() {
310
+ this.generation += 1;
311
+ this.view = void 0;
312
+ }
313
+ accept(view, writable) {
314
+ const resolved = permissionDefaultOf(view);
315
+ this.view = view;
316
+ this.store.update((state) => {
317
+ state.status = "ready";
318
+ state.error = null;
319
+ state.writable = writable;
320
+ state.currentValue = resolved.currentValue;
321
+ state.options = resolved.options;
322
+ state.revision = view.revision;
323
+ });
324
+ }
325
+ fail(error) {
326
+ this.store.update((state) => {
327
+ state.status = "error";
328
+ state.error = error instanceof Error ? error.message : String(error);
329
+ });
330
+ }
331
+ };
332
+ /**
333
+ * Refetch only after the row has opened once.
334
+ * @param controller - permission settings controller.
335
+ */
336
+ function refreshPermissionIfLoaded(controller) {
337
+ if (controller.store.getSnapshot().status === "idle") return;
338
+ controller.load();
339
+ }
340
+ //#endregion
341
+ //#region lib/types/client/index.js
342
+ /** Required services (cordis fiber inject). */
343
+ const inject = [
344
+ "commandUi",
345
+ "sessions",
346
+ "slots",
347
+ "locale",
348
+ "connection",
349
+ "remote"
350
+ ];
351
+ const ACCESS_NS = "permission.access";
352
+ /** Read one session's current permissions projection value (undefined = capability absent). */
353
+ function selectOf(session) {
354
+ return session?.projections.faceOf("permissions").getSnapshot();
355
+ }
356
+ /** Flatten the projection select into popup rows; `custom` is display state, never a target. */
357
+ function optionsOf(value, t) {
358
+ return value.options.filter((option) => option.value !== "custom").map((option) => ({
359
+ id: option.value,
360
+ label: displayPermissionPreset(option.value, option.name),
361
+ ...option.description !== void 0 ? { detail: option.description } : {},
362
+ ...option.value === value.currentValue ? { active: true } : {},
363
+ ...option.value === "danger-full-access" ? { confirmation: {
364
+ title: t("confirm.title"),
365
+ description: t("confirm.description"),
366
+ acknowledgeLabel: t("confirm.acknowledge"),
367
+ cancelLabel: t("confirm.cancel"),
368
+ confirmLabel: t("confirm.enable")
369
+ } } : {}
370
+ }));
371
+ }
372
+ /**
373
+ * Client plugin body: register the /permission popup picker over the
374
+ * permissions projection.
375
+ * @param ctx - client root context.
376
+ */
377
+ function apply(ctx) {
378
+ const command = ctx.get("commandUi");
379
+ const sessions = ctx.sessions;
380
+ ctx.effect(() => {
381
+ const disposers = [ctx.locale.register(ACCESS_NS, "zh", {
382
+ "confirm.title": accessZh["confirm.title"],
383
+ "confirm.description": accessZh["confirm.description"],
384
+ "confirm.acknowledge": accessZh["confirm.acknowledge"],
385
+ "confirm.cancel": accessZh["confirm.cancel"],
386
+ "confirm.enable": accessZh["confirm.enable"]
387
+ }), ctx.locale.register(ACCESS_NS, "en", {
388
+ "confirm.title": accessEn["confirm.title"],
389
+ "confirm.description": accessEn["confirm.description"],
390
+ "confirm.acknowledge": accessEn["confirm.acknowledge"],
391
+ "confirm.cancel": accessEn["confirm.cancel"],
392
+ "confirm.enable": accessEn["confirm.enable"]
393
+ })];
394
+ return () => {
395
+ for (const dispose of disposers) dispose();
396
+ };
397
+ }, "ui-permission: Full access confirmation dictionaries");
398
+ const t = ctx.locale.bind(ACCESS_NS);
399
+ const sessionFor = (session) => sessions.binding(session.sessionId)?.session;
400
+ ctx.effect(() => ctx.locale.register("settings.permission", {
401
+ zh,
402
+ en
403
+ }), "ui-permission: settings row dictionaries");
404
+ const controller = new PermissionPresetSettingsController(ctx.get("connection").api);
405
+ const load = () => controller.load();
406
+ const select = (preset) => controller.select(preset);
407
+ const injected = () => ({
408
+ hooks: { permission: controller.store },
409
+ load,
410
+ select
411
+ });
412
+ ctx.effect(() => {
413
+ const refresh = () => {
414
+ refreshPermissionIfLoaded(controller);
415
+ };
416
+ const disposers = [ctx.remote.$on("settings/document-updated", (ns) => {
417
+ if (ns !== "permission") return;
418
+ refresh();
419
+ }), ctx.on("connection/reset", () => {
420
+ refresh();
421
+ })];
422
+ return () => {
423
+ controller.dispose();
424
+ for (const dispose of disposers) dispose();
425
+ };
426
+ }, "ui-permission: settings invalidations");
427
+ ctx.slots.inject("settings.general.item", () => ctx.slots.register({
428
+ name: "settings.general.item",
429
+ id: "permission",
430
+ order: -20,
431
+ locale: "settings.permission",
432
+ inject: injected
433
+ }, PermissionRow));
434
+ ctx.effect(() => command.decorate({
435
+ name: "permission",
436
+ available: (session) => selectOf(sessionFor(session)) !== void 0,
437
+ ui: {
438
+ kind: "popupSelect",
439
+ options: (session) => {
440
+ const value = selectOf(sessionFor(session));
441
+ if (value === void 0) throw new Error("permission presets are not available on this host");
442
+ return Promise.resolve(optionsOf(value, t));
443
+ },
444
+ onSelect: async (option, session) => {
445
+ const live = sessionFor(session);
446
+ if (live === void 0) throw new Error("this session is not materialized yet");
447
+ const result = await live.command(`/permission ${option.id}`);
448
+ if (!result.ok) throw new Error(`permission switch failed: ${result.error.code}: ${result.error.message}`);
449
+ if (!result.value.matched) throw new Error("the host offers no /permission command");
450
+ }
451
+ }
452
+ }), "ui-permission: /permission decoration");
453
+ }
454
+ //#endregion
455
+ exports.apply = apply;
456
+ exports.inject = inject;
457
+ return module.exports;
458
+ }
459
+ });
460
+
461
+ //# sourceMappingURL=client.js.map
package/lib/index.js ADDED
@@ -0,0 +1,11 @@
1
+ //#region lib/types/index.js
2
+ /**
3
+ * Permission surfaces plugin, node half. The empty apply exists so the plugin
4
+ * appears in the host cordis.yml / Loader; the browser half ships the
5
+ * new-session Settings row and current-session command picker through
6
+ * exports["./client"], discovered from the package.json dsh.client declaration.
7
+ */
8
+ /** Host plugin body — no host-side behavior for this surface plugin. */
9
+ function apply() {}
10
+ //#endregion
11
+ export { apply };
@@ -0,0 +1,24 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-permission-presets`.
4
+ * @module @deepseek-ai/dsh-client-ui-permission-presets/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-client-ui-permission-presets";
7
+ /** Cordis companion plugin name. */
8
+ const name = "client-ui-permission-presets-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: the command and slot contribution lifecycles are
13
+ * proven by the HMR-safety spec, while the browser-only Settings controller
14
+ * owns no host events or cross-plugin mutable state.
15
+ */
16
+ const install = () => {};
17
+ /**
18
+ * Register this package's invariant companion.
19
+ * @param ctx - Cordis context carrying the invariant service.
20
+ * @returns the installed registration's disposer after setup succeeds.
21
+ */
22
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
23
+ //#endregion
24
+ export { apply, inject, name };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Permission preference row: the default preset for subsequently created
3
+ * sessions. Current-session switches remain on the composer `/permission`
4
+ * control.
5
+ */
6
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
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.
25
+ * @param props - composed slot props.
26
+ * @returns the row, or null when the host does not expose permission settings.
27
+ */
28
+ export declare function PermissionRow({ load, select, usePermission, t }: PermissionRowProps): import("react").JSX.Element | null;
29
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
30
+ interface LocaleNamespaceMap {
31
+ /** Permission row copy. */
32
+ 'settings.permission': PermissionSettingsKey;
33
+ }
34
+ }
35
+ //# sourceMappingURL=PermissionRow.d.ts.map
@@ -0,0 +1,12 @@
1
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
2
+ export type { PermissionRowInjected, PermissionRowProps } from './PermissionRow.tsx';
3
+ export type { PermissionDefaultOption, PermissionSettingsState, } from './settings-store.ts';
4
+ /** Required services (cordis fiber inject). */
5
+ export declare const inject: string[];
6
+ /**
7
+ * Client plugin body: register the /permission popup picker over the
8
+ * permissions projection.
9
+ * @param ctx - client root context.
10
+ */
11
+ export declare function apply(ctx: ClientContext): void;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,46 @@
1
+ /** `settings.permission` namespace dictionaries (the Permission row's copy). */
2
+ /** Simplified Chinese dictionary (the key-set source of truth). */
3
+ export declare const zh: {
4
+ title: string;
5
+ description: string;
6
+ loading: string;
7
+ unavailable: string;
8
+ 'confirm.title': string;
9
+ 'confirm.description': string;
10
+ 'confirm.acknowledge': string;
11
+ 'confirm.cancel': string;
12
+ 'confirm.enable': string;
13
+ };
14
+ /** The settings.permission namespace key union. */
15
+ export type PermissionSettingsKey = keyof typeof zh;
16
+ /** English dictionary, checked complete against the zh key set. */
17
+ export declare const en: {
18
+ title: string;
19
+ description: string;
20
+ loading: string;
21
+ unavailable: string;
22
+ 'confirm.title': string;
23
+ 'confirm.description': string;
24
+ 'confirm.acknowledge': string;
25
+ 'confirm.cancel': string;
26
+ 'confirm.enable': string;
27
+ };
28
+ /** Simplified Chinese dictionary for the current-session popup gate. */
29
+ export declare const accessZh: {
30
+ 'confirm.title': string;
31
+ 'confirm.description': string;
32
+ 'confirm.acknowledge': string;
33
+ 'confirm.cancel': string;
34
+ 'confirm.enable': string;
35
+ };
36
+ /** Current-session popup-gate key union. */
37
+ export type PermissionAccessKey = keyof typeof accessZh;
38
+ /** English dictionary for the current-session popup gate. */
39
+ export declare const accessEn: {
40
+ 'confirm.title': string;
41
+ 'confirm.description': string;
42
+ 'confirm.acknowledge': string;
43
+ 'confirm.cancel': string;
44
+ 'confirm.enable': string;
45
+ };
46
+ //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1,16 @@
1
+ /** Machine value of the preset that requires an explicit GUI risk gate. */
2
+ export declare const FULL_ACCESS_PRESET = "danger-full-access";
3
+ /**
4
+ * Convert conventional kebab-case preset names into user-facing title case.
5
+ * @param name - host-supplied preset label or key.
6
+ * @returns the title-cased conventional key, or a non-kebab label unchanged.
7
+ */
8
+ export declare function displayPresetName(name: string): string;
9
+ /**
10
+ * Render a permission preset under its product label.
11
+ * @param value - preset machine value.
12
+ * @param name - host-supplied preset name.
13
+ * @returns the Full access product label or the conventional display name.
14
+ */
15
+ export declare function displayPermissionPreset(value: string, name: string): string;
16
+ //# sourceMappingURL=presentation.d.ts.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Permission default-settings controller. The host descriptor supplies the
3
+ * current value and the dynamic preset enum; writes target only
4
+ * `defaultPreset` and carry the descriptor revision.
5
+ */
6
+ import type { IApiClient, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client';
7
+ import { type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
8
+ /** Permission's settings namespace on the host wire. */
9
+ export declare const PERMISSION_SETTINGS_NS = "permission";
10
+ /** One selectable new-session default. */
11
+ export interface PermissionDefaultOption {
12
+ /** Preset key written to Settings. */
13
+ id: string;
14
+ /** Host-supplied label or a title-cased preset key. */
15
+ label: string;
16
+ }
17
+ /** Permission settings-row snapshot. */
18
+ export interface PermissionSettingsState {
19
+ status: 'idle' | 'loading' | 'ready' | 'saving' | 'unavailable' | 'error';
20
+ error: string | null;
21
+ writable: boolean;
22
+ currentValue: string;
23
+ options: readonly PermissionDefaultOption[];
24
+ revision: number;
25
+ }
26
+ /**
27
+ * Read the dynamic preset enum encoded by the host's `defaultPreset` schema.
28
+ * @param view - permission namespace descriptor.
29
+ * @returns current value and selectable options.
30
+ */
31
+ export declare function permissionDefaultOf(view: SettingsNamespaceView): {
32
+ currentValue: string;
33
+ options: PermissionDefaultOption[];
34
+ };
35
+ /** Controller joining Settings reads, writes, and pushed invalidations. */
36
+ export declare class PermissionPresetSettingsController {
37
+ private readonly api;
38
+ /** Row snapshot consumed through a bound selector hook. */
39
+ readonly store: SnapshotStore<PermissionSettingsState>;
40
+ private generation;
41
+ private view;
42
+ /** @param api - Settings wire face. */
43
+ constructor(api: Pick<IApiClient, 'settings'>);
44
+ /**
45
+ * Refresh the permission descriptor. Latest request wins.
46
+ * @returns nothing; {@link store} carries success or failure.
47
+ */
48
+ load(): Promise<void>;
49
+ /**
50
+ * Persist one preset as the default for subsequently created sessions.
51
+ * @param preset - advertised preset key.
52
+ * @returns nothing; {@link store} carries success or failure.
53
+ */
54
+ select(preset: string): Promise<void>;
55
+ /** Stop in-flight responses from publishing after plugin disposal. */
56
+ dispose(): void;
57
+ private accept;
58
+ private fail;
59
+ }
60
+ /**
61
+ * Refetch only after the row has opened once.
62
+ * @param controller - permission settings controller.
63
+ */
64
+ export declare function refreshPermissionIfLoaded(controller: PermissionPresetSettingsController): void;
65
+ //# sourceMappingURL=settings-store.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Permission surfaces plugin, node half. The empty apply exists so the plugin
3
+ * appears in the host cordis.yml / Loader; the browser half ships the
4
+ * new-session Settings row and current-session command picker through
5
+ * exports["./client"], discovered from the package.json dsh.client declaration.
6
+ */
7
+ /** Host plugin body — no host-side behavior for this surface plugin. */
8
+ export declare function apply(): void;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-permission-presets`.
3
+ * @module @deepseek-ai/dsh-client-ui-permission-presets/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "client-ui-permission-presets-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-client-ui-permission-presets",
3
+ "description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection",
4
+ "version": "0.0.1-rc.3",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/client/ui-permission-presets"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./client": {
26
+ "types": "./lib/types/client/index.d.ts",
27
+ "default": "./lib/client.js"
28
+ },
29
+ "./src/*": "./src/*",
30
+ "./package.json": "./package.json"
31
+ },
32
+ "dsh": {
33
+ "client": {
34
+ "inject": [
35
+ "@deepseek-ai/dsh-client-connection",
36
+ "@deepseek-ai/dsh-client-locale",
37
+ "@deepseek-ai/dsh-client-runtime",
38
+ "@deepseek-ai/dsh-client-ui-commands",
39
+ "@deepseek-ai/dsh-api-remotes",
40
+ "@deepseek-ai/dsh-client-ui-settings"
41
+ ],
42
+ "platform": "web"
43
+ }
44
+ },
45
+ "license": "BSD-3-Clause",
46
+ "peerDependencies": {
47
+ "react": "^18.2.0",
48
+ "@deepseek-ai/cordis": "^4.0.1-rc.1",
49
+ "@deepseek-ai/dsh-client-connection": "^0.0.1-rc.3",
50
+ "@deepseek-ai/dsh-client-locale": "^0.0.1-rc.3",
51
+ "@deepseek-ai/dsh-client-runtime": "^0.0.1-rc.3",
52
+ "@deepseek-ai/dsh-client-schema-form": "^0.0.1-rc.3",
53
+ "@deepseek-ai/dsh-client-ui-commands": "^0.0.1-rc.3",
54
+ "@deepseek-ai/dsh-client-ui-settings": "^0.0.1-rc.3",
55
+ "@deepseek-ai/dsh-api-remotes": "^0.0.1-rc.3",
56
+ "@deepseek-ai/dsh-client-ui-input-trigger": "^0.0.1-rc.3",
57
+ "@deepseek-ai/dsh-client-ui-slots": "^0.0.1-rc.3",
58
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.3",
59
+ "@deepseek-ai/dsh-permission-presets": "^0.0.1-rc.3",
60
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.0.1-rc.3"
61
+ },
62
+ "devDependencies": {
63
+ "@types/react": "~18.3.1",
64
+ "react": "^18.2.0",
65
+ "@deepseek-ai/cordis": "^4.0.1-rc.1",
66
+ "@deepseek-ai/dsh-api-remotes": "^0.0.1-rc.3",
67
+ "@deepseek-ai/dsh-client-connection": "^0.0.1-rc.3",
68
+ "@deepseek-ai/dsh-client-locale": "^0.0.1-rc.3",
69
+ "@deepseek-ai/dsh-client-runtime": "^0.0.1-rc.3",
70
+ "@deepseek-ai/dsh-client-schema-form": "^0.0.1-rc.3",
71
+ "@deepseek-ai/dsh-client-test-runtime": "^0.0.1-rc.3",
72
+ "@deepseek-ai/dsh-client-ui-commands": "^0.0.1-rc.3",
73
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.0.1-rc.3",
74
+ "@deepseek-ai/dsh-client-ui-settings": "^0.0.1-rc.3",
75
+ "@deepseek-ai/dsh-client-ui-input-trigger": "^0.0.1-rc.3",
76
+ "@deepseek-ai/dsh-client-ui-slots": "^0.0.1-rc.3",
77
+ "@deepseek-ai/dsh-client-web-react": "^0.0.1-rc.3",
78
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.3",
79
+ "@deepseek-ai/dsh-permission-presets": "^0.0.1-rc.3"
80
+ },
81
+ "files": [
82
+ "lib/index.js",
83
+ "lib/invariant.js",
84
+ "lib/client.js",
85
+ "lib/types/**/*.d.ts"
86
+ ],
87
+ "scripts": {
88
+ "bundle": "tsdown",
89
+ "watch": "tsdown --watch"
90
+ }
91
+ }