dsh-sidebar-row-menu 1.9.3 → 1.10.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/CHANGELOG.md +30 -0
- package/README.en.md +40 -40
- package/README.md +31 -28
- package/lib/client.js +402 -56
- package/lib/host/permission-defaults.js +190 -0
- package/lib/host/route.js +106 -1
- package/lib/index.js +28 -3
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -94,6 +94,18 @@ window.__ModuleLoader__.load({
|
|
|
94
94
|
const DELETE_ENDPOINT = '/dsh-sidebar-row-menu/session/delete';
|
|
95
95
|
/** 删除「工作区 + 其下全部会话」的宿主端点。 */
|
|
96
96
|
const WORKSPACE_DELETE_ENDPOINT = '/dsh-sidebar-row-menu/workspace/delete';
|
|
97
|
+
/** 按工作区读写「新会话默认权限」的宿主端点。 */
|
|
98
|
+
const PERMISSION_DEFAULTS_ENDPOINT = '/dsh-sidebar-row-menu/permission-defaults';
|
|
99
|
+
/** 宿主原生预设的本地化名称;catalog 里的 name 是英文,中文界面用这张表。 */
|
|
100
|
+
const PRESET_LABEL_KEYS = {
|
|
101
|
+
'read-only': 'permission.readOnly',
|
|
102
|
+
'workspace-write': 'permission.workspaceWrite',
|
|
103
|
+
'danger-full-access': 'permission.fullAccess',
|
|
104
|
+
auto: 'permission.autoReview',
|
|
105
|
+
custom: 'permission.custom',
|
|
106
|
+
};
|
|
107
|
+
/** 宿主还没连上时先按内置预设渲染,catalog 到了再替换。 */
|
|
108
|
+
const FALLBACK_PRESETS = ['read-only', 'workspace-write', 'danger-full-access'];
|
|
97
109
|
/** 官方 open-in-app 的三条路由;用文档相对路径让浏览器自带认证。 */
|
|
98
110
|
const APPS_ROUTE = 'open-in-app/apps';
|
|
99
111
|
const OPEN_ROUTE = 'open-in-app/open';
|
|
@@ -121,10 +133,19 @@ window.__ModuleLoader__.load({
|
|
|
121
133
|
};
|
|
122
134
|
const ITEM_KEYS = Object.keys(ITEM_DEFAULTS);
|
|
123
135
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
136
|
+
* 「右键专属条目是否也放进行尾…菜单」的逐项开关(旧版数据)。
|
|
137
|
+
* 0.2.2 起改用下面的 PLACEMENT_KEY 三态;这里只保留迁移读取。
|
|
126
138
|
*/
|
|
127
139
|
const ELLIPSIS_KEY = 'dsh.sidebar-row-menu.ellipsis.v1';
|
|
140
|
+
/**
|
|
141
|
+
* 每个附加条目在两种入口里的显示位置:
|
|
142
|
+
* context 只在右键菜单
|
|
143
|
+
* ellipsis 只在「…」菜单
|
|
144
|
+
* both 两边都显示
|
|
145
|
+
* 旧版只有一个「同时放进…菜单」布尔值;读取时 true → both、false → context。
|
|
146
|
+
*/
|
|
147
|
+
const PLACEMENT_KEY = 'dsh.sidebar-row-menu.placement.v1';
|
|
148
|
+
const PLACEMENT_VALUES = ['context', 'ellipsis', 'both'];
|
|
128
149
|
/**
|
|
129
150
|
* 按工作区「已隐藏归档会话」的集合。宿主只有全局归档筛选(default/show/only),
|
|
130
151
|
* 没有「这个工作区显示归档、那个工作区不显示」的概念,所以这里记住要单独
|
|
@@ -134,6 +155,7 @@ window.__ModuleLoader__.load({
|
|
|
134
155
|
const ARCHIVED_HIDDEN_KEY = 'dsh.sidebar-row-menu.archived-hidden.v1';
|
|
135
156
|
const ELLIPSIS_DEFAULTS = {
|
|
136
157
|
newSession: false,
|
|
158
|
+
permissionDefault: false,
|
|
137
159
|
showArchived: false,
|
|
138
160
|
openWith: false,
|
|
139
161
|
copyPath: false,
|
|
@@ -194,6 +216,14 @@ window.__ModuleLoader__.load({
|
|
|
194
216
|
actionFailed: '操作失败,请稍后重试',
|
|
195
217
|
openWith: '打开方式',
|
|
196
218
|
openWithApp: '用 {name} 打开',
|
|
219
|
+
newSessionPermission: '新会话默认权限',
|
|
220
|
+
permissionFollowGlobal: '跟随全局默认',
|
|
221
|
+
'permission.readOnly': '仅可查看',
|
|
222
|
+
'permission.workspaceWrite': '工作区内修改',
|
|
223
|
+
'permission.fullAccess': '完全权限',
|
|
224
|
+
'permission.autoReview': '自动审查(实验)',
|
|
225
|
+
'permission.custom': '自定义',
|
|
226
|
+
'permission.unavailable': '宿主没有提供权限预设',
|
|
197
227
|
deleteSession: '永久删除会话',
|
|
198
228
|
deleteArchived: '删除所有已归档会话',
|
|
199
229
|
'settings.title': '行菜单',
|
|
@@ -220,12 +250,17 @@ window.__ModuleLoader__.load({
|
|
|
220
250
|
'settings.appsAllOff': '所有应用都被二级开关隐藏了',
|
|
221
251
|
'settings.expand': '展开应用列表',
|
|
222
252
|
'settings.collapse': '收起应用列表',
|
|
223
|
-
'settings.ellipsisTitle': '
|
|
224
|
-
'settings.ellipsisHint': '
|
|
253
|
+
'settings.ellipsisTitle': '菜单显示位置',
|
|
254
|
+
'settings.ellipsisHint': '每个条目都可以分别选择出现在右键菜单、「…」菜单,或两边都出现;至少保留一边。上面的单项开关仍然决定条目是否启用。',
|
|
255
|
+
'settings.placement.context': '右键菜单',
|
|
256
|
+
'settings.placement.ellipsis': '「…」菜单',
|
|
257
|
+
'settings.placement.locked': '至少要在一边显示',
|
|
225
258
|
'settings.ellipsisWorkspace': '工作区行',
|
|
226
259
|
'settings.ellipsisSession': '会话行',
|
|
227
260
|
'settings.ellipsis.newSession.label': '新建会话',
|
|
228
261
|
'settings.ellipsis.newSession.hint': '在「…」菜单里直接新建该工作区的会话。',
|
|
262
|
+
'settings.ellipsis.permissionDefault.label': '新会话默认权限',
|
|
263
|
+
'settings.ellipsis.permissionDefault.hint': '在「…」菜单里为该工作区设置新建会话的默认权限;只在带原生权限预设的宿主上出现。',
|
|
229
264
|
'settings.ellipsis.showArchived.label': '显示已归档会话',
|
|
230
265
|
'settings.ellipsis.showArchived.hint': '在「…」菜单里单独显示或隐藏该工作区的已归档会话。',
|
|
231
266
|
'settings.ellipsis.openWith.label': '打开方式',
|
|
@@ -292,6 +327,14 @@ window.__ModuleLoader__.load({
|
|
|
292
327
|
actionFailed: 'Action failed, please retry',
|
|
293
328
|
openWith: 'Open with',
|
|
294
329
|
openWithApp: 'Open with {name}',
|
|
330
|
+
newSessionPermission: 'Default permission for new sessions',
|
|
331
|
+
permissionFollowGlobal: 'Follow the global default',
|
|
332
|
+
'permission.readOnly': 'Read Only',
|
|
333
|
+
'permission.workspaceWrite': 'Workspace Write',
|
|
334
|
+
'permission.fullAccess': 'Full access',
|
|
335
|
+
'permission.autoReview': 'Auto review (experimental)',
|
|
336
|
+
'permission.custom': 'Custom',
|
|
337
|
+
'permission.unavailable': 'The host exposes no permission presets',
|
|
295
338
|
deleteSession: 'Delete session permanently',
|
|
296
339
|
deleteArchived: 'Delete all archived sessions',
|
|
297
340
|
'settings.title': 'Row menu',
|
|
@@ -318,12 +361,17 @@ window.__ModuleLoader__.load({
|
|
|
318
361
|
'settings.appsAllOff': 'Every application is hidden by the switches above',
|
|
319
362
|
'settings.expand': 'Show applications',
|
|
320
363
|
'settings.collapse': 'Hide applications',
|
|
321
|
-
'settings.ellipsisTitle': '
|
|
322
|
-
'settings.ellipsisHint': '
|
|
364
|
+
'settings.ellipsisTitle': 'Menu placement',
|
|
365
|
+
'settings.ellipsisHint': 'Each entry can appear in the right-click menu, the "…" menu, or both. At least one side stays enabled. The switches above still decide whether the entry itself is on.',
|
|
366
|
+
'settings.placement.context': 'Right-click',
|
|
367
|
+
'settings.placement.ellipsis': '"…" menu',
|
|
368
|
+
'settings.placement.locked': 'Keep at least one side',
|
|
323
369
|
'settings.ellipsisWorkspace': 'Workspace rows',
|
|
324
370
|
'settings.ellipsisSession': 'Session rows',
|
|
325
371
|
'settings.ellipsis.newSession.label': 'New session',
|
|
326
372
|
'settings.ellipsis.newSession.hint': 'Create a session for this workspace from the "…" menu.',
|
|
373
|
+
'settings.ellipsis.permissionDefault.label': 'Default permission for new sessions',
|
|
374
|
+
'settings.ellipsis.permissionDefault.hint': 'Set the default permission for new sessions of this workspace from the "…" menu; only shown when the host exposes native permission presets.',
|
|
327
375
|
'settings.ellipsis.showArchived.label': 'Show archived sessions',
|
|
328
376
|
'settings.ellipsis.showArchived.hint': 'Show or hide this workspace\'s archived sessions from the "…" menu.',
|
|
329
377
|
'settings.ellipsis.openWith.label': 'Open with',
|
|
@@ -701,6 +749,7 @@ window.__ModuleLoader__.load({
|
|
|
701
749
|
|
|
702
750
|
/* -------------------------------- 「…」菜单附加开关(localStorage) */
|
|
703
751
|
|
|
752
|
+
/** 读旧版「同时放进…菜单」布尔表,仅用于一次性迁移。 */
|
|
704
753
|
function readStoredEllipsis() {
|
|
705
754
|
const defaults = { ...ELLIPSIS_DEFAULTS };
|
|
706
755
|
try {
|
|
@@ -717,11 +766,32 @@ window.__ModuleLoader__.load({
|
|
|
717
766
|
return defaults;
|
|
718
767
|
}
|
|
719
768
|
|
|
720
|
-
|
|
721
|
-
|
|
769
|
+
function readStoredPlacements() {
|
|
770
|
+
const defaults = {};
|
|
771
|
+
for (const key of ELLIPSIS_KEYS) defaults[key] = 'context';
|
|
772
|
+
const legacy = readStoredEllipsis();
|
|
773
|
+
for (const key of ELLIPSIS_KEYS) {
|
|
774
|
+
if (legacy[key] === true) defaults[key] = 'both';
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
const raw = globalThis.localStorage?.getItem(PLACEMENT_KEY);
|
|
778
|
+
if (typeof raw !== 'string' || raw === '') return defaults;
|
|
779
|
+
const parsed = JSON.parse(raw);
|
|
780
|
+
if (parsed === null || typeof parsed !== 'object') return defaults;
|
|
781
|
+
for (const key of ELLIPSIS_KEYS) {
|
|
782
|
+
if (PLACEMENT_VALUES.includes(parsed[key])) defaults[key] = parsed[key];
|
|
783
|
+
}
|
|
784
|
+
} catch {
|
|
785
|
+
/* 坏数据回退默认 */
|
|
786
|
+
}
|
|
787
|
+
return defaults;
|
|
788
|
+
}
|
|
722
789
|
|
|
723
|
-
|
|
724
|
-
|
|
790
|
+
let placementSettings = readStoredPlacements();
|
|
791
|
+
const placementListeners = new Set();
|
|
792
|
+
|
|
793
|
+
function publishPlacements() {
|
|
794
|
+
for (const listener of placementListeners) {
|
|
725
795
|
try {
|
|
726
796
|
listener();
|
|
727
797
|
} catch {
|
|
@@ -730,43 +800,55 @@ window.__ModuleLoader__.load({
|
|
|
730
800
|
}
|
|
731
801
|
}
|
|
732
802
|
|
|
733
|
-
function
|
|
734
|
-
|
|
803
|
+
function subscribePlacements(listener) {
|
|
804
|
+
placementListeners.add(listener);
|
|
735
805
|
return () => {
|
|
736
|
-
|
|
806
|
+
placementListeners.delete(listener);
|
|
737
807
|
};
|
|
738
808
|
}
|
|
739
809
|
|
|
740
|
-
function
|
|
741
|
-
return
|
|
810
|
+
function placementsSnapshot() {
|
|
811
|
+
return placementSettings;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function usePlacements() {
|
|
815
|
+
return useSyncExternalStore(subscribePlacements, placementsSnapshot, placementsSnapshot);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function placementOf(key) {
|
|
819
|
+
const value = placementSettings[key];
|
|
820
|
+
return PLACEMENT_VALUES.includes(value) ? value : 'context';
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function showsInContext(key) {
|
|
824
|
+
return placementOf(key) !== 'ellipsis';
|
|
742
825
|
}
|
|
743
826
|
|
|
744
|
-
function
|
|
745
|
-
return
|
|
827
|
+
function showsInEllipsis(key) {
|
|
828
|
+
return placementOf(key) !== 'context';
|
|
746
829
|
}
|
|
747
830
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
831
|
+
/** 三态位置;不允许把条目同时从两边关掉,所以最小值是其中一边。 */
|
|
832
|
+
function setPlacement(key, value) {
|
|
833
|
+
if (!ELLIPSIS_KEYS.includes(key) || !PLACEMENT_VALUES.includes(value)) return;
|
|
834
|
+
placementSettings = { ...placementSettings, [key]: value };
|
|
751
835
|
try {
|
|
752
|
-
globalThis.localStorage?.setItem(
|
|
836
|
+
globalThis.localStorage?.setItem(PLACEMENT_KEY, JSON.stringify(placementSettings));
|
|
753
837
|
} catch {
|
|
754
838
|
/* 隐私模式写不进去也照样生效到本次会话 */
|
|
755
839
|
}
|
|
756
|
-
|
|
840
|
+
publishPlacements();
|
|
757
841
|
}
|
|
758
842
|
|
|
759
|
-
function
|
|
760
|
-
|
|
843
|
+
function placementOn(key, mode) {
|
|
844
|
+
const placement = placementOf(key);
|
|
845
|
+
return mode === 'context' ? placement !== 'ellipsis' : placement !== 'context';
|
|
761
846
|
}
|
|
762
847
|
|
|
763
848
|
/** 工作区行是否至少有一项要在「…」菜单里显示(否则完全不接管原生菜单)。 */
|
|
764
849
|
function workspaceEllipsisOn() {
|
|
765
|
-
return
|
|
766
|
-
|
|
767
|
-
|| ellipsisSettings.copyPath === true
|
|
768
|
-
|| ellipsisSettings.openInTab === true
|
|
769
|
-
|| ellipsisSettings.deleteSessions === true;
|
|
850
|
+
return ['newSession', 'permissionDefault', 'openWith', 'copyPath', 'openInTab', 'deleteSessions']
|
|
851
|
+
.some((key) => showsInEllipsis(key));
|
|
770
852
|
}
|
|
771
853
|
|
|
772
854
|
/* -------------------------------------- 会话空白处右键菜单(localStorage) */
|
|
@@ -1046,7 +1128,175 @@ window.__ModuleLoader__.load({
|
|
|
1046
1128
|
);
|
|
1047
1129
|
}
|
|
1048
1130
|
|
|
1049
|
-
/*
|
|
1131
|
+
/* ---------------------------------------- 工作区新会话默认权限(host 存储) */
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* 宿主 permissionPresets 的目录 + 插件自己的「工作区 → preset」映射。
|
|
1135
|
+
* 映射存在宿主侧($DSH_HOME/storages/...),浏览器这边只是缓存;
|
|
1136
|
+
* 选择后先乐观更新,写入失败再回滚并提示。
|
|
1137
|
+
*/
|
|
1138
|
+
function emptyPermissionState() {
|
|
1139
|
+
return {
|
|
1140
|
+
status: 'idle',
|
|
1141
|
+
options: FALLBACK_PRESETS.map((value) => ({ value })),
|
|
1142
|
+
defaultPreset: '',
|
|
1143
|
+
defaults: {},
|
|
1144
|
+
at: 0,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
let permissionState = emptyPermissionState();
|
|
1149
|
+
const permissionListeners = new Set();
|
|
1150
|
+
|
|
1151
|
+
function publishPermission(next) {
|
|
1152
|
+
permissionState = next;
|
|
1153
|
+
for (const listener of permissionListeners) {
|
|
1154
|
+
try {
|
|
1155
|
+
listener();
|
|
1156
|
+
} catch {
|
|
1157
|
+
/* ignore */
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function subscribePermission(listener) {
|
|
1163
|
+
permissionListeners.add(listener);
|
|
1164
|
+
return () => {
|
|
1165
|
+
permissionListeners.delete(listener);
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function permissionSnapshot() {
|
|
1170
|
+
return permissionState;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function usePermission() {
|
|
1174
|
+
return useSyncExternalStore(subscribePermission, permissionSnapshot, permissionSnapshot);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const permissionSource = {
|
|
1178
|
+
getSnapshot: permissionSnapshot,
|
|
1179
|
+
subscribe: subscribePermission,
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
function presetLabel(value, t) {
|
|
1183
|
+
const option = permissionState.options.find((candidate) => candidate?.value === value);
|
|
1184
|
+
const key = PRESET_LABEL_KEYS[value];
|
|
1185
|
+
if (typeof key === 'string') {
|
|
1186
|
+
const localized = t(key);
|
|
1187
|
+
if (localized !== key) return localized;
|
|
1188
|
+
}
|
|
1189
|
+
if (typeof option?.name === 'string' && option.name !== '') return option.name;
|
|
1190
|
+
return value;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function presetOptions() {
|
|
1194
|
+
return permissionState.options.filter((option) => (
|
|
1195
|
+
option !== null
|
|
1196
|
+
&& typeof option === 'object'
|
|
1197
|
+
&& typeof option.value === 'string'
|
|
1198
|
+
&& option.value !== ''
|
|
1199
|
+
&& option.value !== 'custom'
|
|
1200
|
+
));
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function workspacePreset(workspaceId) {
|
|
1204
|
+
const value = permissionState.defaults[workspaceId];
|
|
1205
|
+
return typeof value === 'string' ? value : '';
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function effectivePreset(workspaceId) {
|
|
1209
|
+
return workspacePreset(workspaceId) || permissionState.defaultPreset;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function normalizeDefaults(value) {
|
|
1213
|
+
const result = {};
|
|
1214
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return result;
|
|
1215
|
+
for (const [workspaceId, preset] of Object.entries(value)) {
|
|
1216
|
+
if (typeof workspaceId !== 'string' || workspaceId === '') continue;
|
|
1217
|
+
if (typeof preset !== 'string' || preset === '') continue;
|
|
1218
|
+
result[workspaceId] = preset;
|
|
1219
|
+
}
|
|
1220
|
+
return result;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function ensurePermissionData(ctx) {
|
|
1224
|
+
if (typeof fetch !== 'function') return;
|
|
1225
|
+
const now = Date.now();
|
|
1226
|
+
if (permissionState.status === 'loading') return;
|
|
1227
|
+
if (permissionState.status === 'ready' && now - permissionState.at < 30000) return;
|
|
1228
|
+
if (permissionState.status === 'error' && now - permissionState.at < 15000) return;
|
|
1229
|
+
publishPermission({ ...permissionState, status: 'loading', at: now });
|
|
1230
|
+
|
|
1231
|
+
fetch(PERMISSION_DEFAULTS_ENDPOINT, {
|
|
1232
|
+
headers: { accept: 'application/json' },
|
|
1233
|
+
}).then(
|
|
1234
|
+
(response) => (response.ok ? response.json() : null),
|
|
1235
|
+
() => null,
|
|
1236
|
+
).then((payload) => {
|
|
1237
|
+
const readDefaults = payload?.ok === true
|
|
1238
|
+
? normalizeDefaults(payload.defaults)
|
|
1239
|
+
: null;
|
|
1240
|
+
const catalog = payload?.catalog;
|
|
1241
|
+
const rawOptions = Array.isArray(catalog?.options) ? catalog.options : [];
|
|
1242
|
+
const options = rawOptions.filter((option) => (
|
|
1243
|
+
option !== null
|
|
1244
|
+
&& typeof option === 'object'
|
|
1245
|
+
&& typeof option.value === 'string'
|
|
1246
|
+
&& option.value !== ''
|
|
1247
|
+
&& option.value !== 'custom'
|
|
1248
|
+
));
|
|
1249
|
+
const usable = options.length > 0 ? options : permissionState.options;
|
|
1250
|
+
const defaultPreset = typeof catalog?.defaultPreset === 'string'
|
|
1251
|
+
? catalog.defaultPreset
|
|
1252
|
+
: permissionState.defaultPreset;
|
|
1253
|
+
publishPermission({
|
|
1254
|
+
status: readDefaults === null ? 'error' : 'ready',
|
|
1255
|
+
options: usable,
|
|
1256
|
+
defaultPreset,
|
|
1257
|
+
defaults: readDefaults ?? permissionState.defaults,
|
|
1258
|
+
at: Date.now(),
|
|
1259
|
+
});
|
|
1260
|
+
}, () => {
|
|
1261
|
+
publishPermission({ ...permissionState, status: 'error', at: Date.now() });
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function setWorkspacePreset(ctx, workspaceId, preset, t) {
|
|
1266
|
+
if (typeof workspaceId !== 'string' || workspaceId === '') return Promise.resolve(false);
|
|
1267
|
+
const previous = permissionState.defaults;
|
|
1268
|
+
const next = { ...previous };
|
|
1269
|
+
if (preset === null) delete next[workspaceId];
|
|
1270
|
+
else next[workspaceId] = preset;
|
|
1271
|
+
publishPermission({ ...permissionState, status: 'ready', defaults: next, at: Date.now() });
|
|
1272
|
+
if (typeof fetch !== 'function') {
|
|
1273
|
+
publishPermission({ ...permissionState, defaults: previous, at: Date.now() });
|
|
1274
|
+
return Promise.resolve(false);
|
|
1275
|
+
}
|
|
1276
|
+
return fetch(PERMISSION_DEFAULTS_ENDPOINT, {
|
|
1277
|
+
method: 'POST',
|
|
1278
|
+
headers: { 'content-type': 'application/json' },
|
|
1279
|
+
body: JSON.stringify({ workspaceId, preset }),
|
|
1280
|
+
}).then(
|
|
1281
|
+
(response) => response.json().catch(() => null).then((data) => {
|
|
1282
|
+
if (!response.ok || data?.ok !== true) throw new Error(`HTTP ${response.status}`);
|
|
1283
|
+
publishPermission({
|
|
1284
|
+
...permissionState,
|
|
1285
|
+
status: 'ready',
|
|
1286
|
+
defaults: normalizeDefaults(data.defaults),
|
|
1287
|
+
at: Date.now(),
|
|
1288
|
+
});
|
|
1289
|
+
return true;
|
|
1290
|
+
}),
|
|
1291
|
+
() => false,
|
|
1292
|
+
).catch(() => {
|
|
1293
|
+
publishPermission({ ...permissionState, defaults: previous, at: Date.now() });
|
|
1294
|
+
toast(t('actionFailed'));
|
|
1295
|
+
return false;
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
|
|
1050
1300
|
|
|
1051
1301
|
/** 读 slot 注册项 id(不同 DSH 版本的 register 返回形状略有差异)。 */
|
|
1052
1302
|
function slotItemId(entry) {
|
|
@@ -1586,8 +1836,7 @@ window.__ModuleLoader__.load({
|
|
|
1586
1836
|
*/
|
|
1587
1837
|
function SessionOpenWithMenuItem({ sessionId, useMenuOpenState, t }) {
|
|
1588
1838
|
const rightClick = useRightClickEntry();
|
|
1589
|
-
const
|
|
1590
|
-
const visible = rightClick || ellipsis.sessionOpenWith === true;
|
|
1839
|
+
const visible = placementOn('sessionOpenWith', rightClick ? 'context' : 'ellipsis');
|
|
1591
1840
|
const items = useItems();
|
|
1592
1841
|
const apps = useApps();
|
|
1593
1842
|
const setMenuOpen = menuCloser(useMenuOpenState);
|
|
@@ -1609,8 +1858,7 @@ window.__ModuleLoader__.load({
|
|
|
1609
1858
|
*/
|
|
1610
1859
|
function SessionOpenWithPinnedMenuItem({ sessionId, useMenuOpenState, t }) {
|
|
1611
1860
|
const rightClick = useRightClickEntry();
|
|
1612
|
-
const
|
|
1613
|
-
const visible = rightClick || ellipsis.sessionOpenWith === true;
|
|
1861
|
+
const visible = placementOn('sessionOpenWith', rightClick ? 'context' : 'ellipsis');
|
|
1614
1862
|
const items = useItems();
|
|
1615
1863
|
useChildren();
|
|
1616
1864
|
const apps = useApps();
|
|
@@ -1648,8 +1896,7 @@ window.__ModuleLoader__.load({
|
|
|
1648
1896
|
*/
|
|
1649
1897
|
function SessionDeleteMenuItem({ sessionId, displayTitle, useMenuOpenState, t }) {
|
|
1650
1898
|
const rightClick = useRightClickEntry();
|
|
1651
|
-
const
|
|
1652
|
-
const visible = rightClick || ellipsis.sessionDelete === true;
|
|
1899
|
+
const visible = placementOn('sessionDelete', rightClick ? 'context' : 'ellipsis');
|
|
1653
1900
|
const items = useItems();
|
|
1654
1901
|
const archived = useArchivedIds();
|
|
1655
1902
|
const setMenuOpen = menuCloser(useMenuOpenState);
|
|
@@ -1673,8 +1920,7 @@ window.__ModuleLoader__.load({
|
|
|
1673
1920
|
*/
|
|
1674
1921
|
function SessionDeleteArchivedMenuItem({ sessionId, displayTitle, useMenuOpenState, t }) {
|
|
1675
1922
|
const rightClick = useRightClickEntry();
|
|
1676
|
-
const
|
|
1677
|
-
const visible = rightClick || ellipsis.sessionDeleteArchived === true;
|
|
1923
|
+
const visible = placementOn('sessionDeleteArchived', rightClick ? 'context' : 'ellipsis');
|
|
1678
1924
|
const items = useItems();
|
|
1679
1925
|
const archived = useArchivedIds();
|
|
1680
1926
|
const setMenuOpen = menuCloser(useMenuOpenState);
|
|
@@ -1704,10 +1950,11 @@ window.__ModuleLoader__.load({
|
|
|
1704
1950
|
['archivedSessions', 'settings.archivedSessions.label', 'settings.archivedSessions.hint'],
|
|
1705
1951
|
];
|
|
1706
1952
|
|
|
1707
|
-
/**
|
|
1708
|
-
const
|
|
1953
|
+
/** 菜单显示位置;按行分组,避免用户分不清工作区项和会话项。 */
|
|
1954
|
+
const PLACEMENT_ROWS = [
|
|
1709
1955
|
['workspace', 'settings.ellipsisWorkspace', [
|
|
1710
1956
|
['newSession', 'settings.ellipsis.newSession.label', 'settings.ellipsis.newSession.hint'],
|
|
1957
|
+
['permissionDefault', 'settings.ellipsis.permissionDefault.label', 'settings.ellipsis.permissionDefault.hint'],
|
|
1711
1958
|
['openWith', 'settings.ellipsis.openWith.label', 'settings.ellipsis.openWith.hint'],
|
|
1712
1959
|
['copyPath', 'settings.ellipsis.copyPath.label', 'settings.ellipsis.copyPath.hint'],
|
|
1713
1960
|
['openInTab', 'settings.ellipsis.openInTab.label', 'settings.ellipsis.openInTab.hint'],
|
|
@@ -1757,7 +2004,7 @@ window.__ModuleLoader__.load({
|
|
|
1757
2004
|
|
|
1758
2005
|
function SettingsSection({ t }) {
|
|
1759
2006
|
const items = useItems();
|
|
1760
|
-
const
|
|
2007
|
+
const placements = usePlacements();
|
|
1761
2008
|
const blank = useBlankMenu();
|
|
1762
2009
|
const children = useChildren();
|
|
1763
2010
|
const apps = useApps();
|
|
@@ -1841,7 +2088,32 @@ window.__ModuleLoader__.load({
|
|
|
1841
2088
|
return key === 'openWith' && childRows ? [row, childRows] : [row];
|
|
1842
2089
|
});
|
|
1843
2090
|
|
|
1844
|
-
const
|
|
2091
|
+
const placementSwitch = (key, mode, labelKey) => {
|
|
2092
|
+
const inContext = showsInContext(key);
|
|
2093
|
+
const inEllipsis = showsInEllipsis(key);
|
|
2094
|
+
const checked = mode === 'context' ? inContext : inEllipsis;
|
|
2095
|
+
// 三态里只剩一边时,那个开关锁住:不允许把条目从两边都关掉。
|
|
2096
|
+
const locked = mode === 'context' ? !inEllipsis : !inContext;
|
|
2097
|
+
const onChange = (next) => {
|
|
2098
|
+
if (mode === 'context') {
|
|
2099
|
+
setPlacement(key, next ? (inEllipsis ? 'both' : 'context') : 'ellipsis');
|
|
2100
|
+
} else {
|
|
2101
|
+
setPlacement(key, next ? (inContext ? 'both' : 'ellipsis') : 'context');
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
return h('span', { className: 'dsrm-placement-choice' },
|
|
2105
|
+
h('span', { className: 'dsrm-placement-choice-label' }, t(labelKey)),
|
|
2106
|
+
jsx(Switch, {
|
|
2107
|
+
checked,
|
|
2108
|
+
disabled: locked,
|
|
2109
|
+
label: t(labelKey),
|
|
2110
|
+
title: locked ? t('settings.placement.locked') : undefined,
|
|
2111
|
+
onChange,
|
|
2112
|
+
}),
|
|
2113
|
+
);
|
|
2114
|
+
};
|
|
2115
|
+
|
|
2116
|
+
const placementGroups = PLACEMENT_ROWS.map(([groupKey, titleKey, rows]) => h('div', {
|
|
1845
2117
|
className: 'dsrm-settings-group',
|
|
1846
2118
|
key: groupKey,
|
|
1847
2119
|
},
|
|
@@ -1854,12 +2126,9 @@ window.__ModuleLoader__.load({
|
|
|
1854
2126
|
h('div', { className: 'dsrm-settings-label' }, t(labelKey)),
|
|
1855
2127
|
h('div', { className: 'dsrm-settings-hint' }, t(hintKey)),
|
|
1856
2128
|
),
|
|
1857
|
-
h('div', { className: 'dsrm-settings-controls' },
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
label: t(labelKey),
|
|
1861
|
-
onChange: (next) => setEllipsis(key, next),
|
|
1862
|
-
}),
|
|
2129
|
+
h('div', { className: 'dsrm-settings-controls dsrm-placement-controls' },
|
|
2130
|
+
placementSwitch(key, 'context', 'settings.placement.context'),
|
|
2131
|
+
placementSwitch(key, 'ellipsis', 'settings.placement.ellipsis'),
|
|
1863
2132
|
),
|
|
1864
2133
|
))),
|
|
1865
2134
|
));
|
|
@@ -1888,7 +2157,7 @@ window.__ModuleLoader__.load({
|
|
|
1888
2157
|
h('div', { className: 'dsrm-settings-list' }, rows),
|
|
1889
2158
|
h('h2', { className: 'dsrm-settings-title dsrm-settings-subtitle' }, t('settings.ellipsisTitle')),
|
|
1890
2159
|
h('div', { className: 'dsrm-settings-note' }, t('settings.ellipsisHint')),
|
|
1891
|
-
h('div', { className: 'dsrm-settings-groups' },
|
|
2160
|
+
h('div', { className: 'dsrm-settings-groups' }, placementGroups),
|
|
1892
2161
|
h('h2', { className: 'dsrm-settings-title dsrm-settings-subtitle' }, t('settings.blankMenuTitle')),
|
|
1893
2162
|
h('div', { className: 'dsrm-settings-note' }, t('settings.blankMenuNote')),
|
|
1894
2163
|
h('div', { className: 'dsrm-settings-list' }, blankRows),
|
|
@@ -1904,7 +2173,7 @@ window.__ModuleLoader__.load({
|
|
|
1904
2173
|
function WorkspaceRowMenu({ x, y, props, onClose, mode = 'context' }) {
|
|
1905
2174
|
const t = localeOf(rootCtx);
|
|
1906
2175
|
const items = useItems();
|
|
1907
|
-
|
|
2176
|
+
usePlacements();
|
|
1908
2177
|
const apps = useApps();
|
|
1909
2178
|
usePinned();
|
|
1910
2179
|
useChildren();
|
|
@@ -1912,8 +2181,8 @@ window.__ModuleLoader__.load({
|
|
|
1912
2181
|
const pos = useMenuPos(hostRef, x, y, [apps.status]);
|
|
1913
2182
|
|
|
1914
2183
|
const ellipsisMode = mode === 'ellipsis';
|
|
1915
|
-
/**
|
|
1916
|
-
const show = (key) =>
|
|
2184
|
+
/** 右键菜单永远显示;「…」菜单按设置页里选定的位置显示(内置项除外)。 */
|
|
2185
|
+
const show = (key) => placementOn(key, ellipsisMode ? 'ellipsis' : 'context');
|
|
1917
2186
|
const openWithOn = items.openWith !== false && show('openWith');
|
|
1918
2187
|
|
|
1919
2188
|
useEffect(() => {
|
|
@@ -3299,6 +3568,37 @@ window.__ModuleLoader__.load({
|
|
|
3299
3568
|
* 数据贡献的一批工作区动作;group 字段决定它挂到哪个 order 的 provider,
|
|
3300
3569
|
* 从而夹在原生 rename(100)/reveal(200)/delete(300) 之间。
|
|
3301
3570
|
*/
|
|
3571
|
+
/** 菜单项里的「当前默认」勾:宿主菜单没有 checked 字段,只能自绘。 */
|
|
3572
|
+
function permissionCheck() {
|
|
3573
|
+
return h('svg', {
|
|
3574
|
+
className: 'dsrm-check',
|
|
3575
|
+
width: 14,
|
|
3576
|
+
height: 14,
|
|
3577
|
+
viewBox: '0 0 16 16',
|
|
3578
|
+
'aria-hidden': 'true',
|
|
3579
|
+
}, h('path', {
|
|
3580
|
+
d: 'M3.2 8.4 6.3 11.5 12.8 5',
|
|
3581
|
+
fill: 'none',
|
|
3582
|
+
stroke: 'currentColor',
|
|
3583
|
+
'stroke-width': 1.7,
|
|
3584
|
+
'stroke-linecap': 'round',
|
|
3585
|
+
'stroke-linejoin': 'round',
|
|
3586
|
+
}));
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
/** 权限子菜单的图标:一把小锁,避免与「打开方式」的箭头混淆。 */
|
|
3590
|
+
const ContextMenuPermissionIcon = () => h('svg', {
|
|
3591
|
+
width: 16,
|
|
3592
|
+
height: 16,
|
|
3593
|
+
viewBox: '0 0 16 16',
|
|
3594
|
+
'aria-hidden': 'true',
|
|
3595
|
+
fill: 'none',
|
|
3596
|
+
stroke: 'currentColor',
|
|
3597
|
+
'stroke-width': 1.4,
|
|
3598
|
+
},
|
|
3599
|
+
h('rect', { x: 3.2, y: 7, width: 9.6, height: 6.6, rx: 1.6 }),
|
|
3600
|
+
h('path', { d: 'M5.4 7V5.4a2.6 2.6 0 0 1 5.2 0V7' }));
|
|
3601
|
+
|
|
3302
3602
|
function workspaceMenuActions(ctx, value, t) {
|
|
3303
3603
|
const workspaceId = typeof value?.workspaceId === 'string' ? value.workspaceId : '';
|
|
3304
3604
|
if (workspaceId === '') return [];
|
|
@@ -3409,6 +3709,44 @@ window.__ModuleLoader__.load({
|
|
|
3409
3709
|
});
|
|
3410
3710
|
}
|
|
3411
3711
|
|
|
3712
|
+
// 该工作区新建会话的默认权限:走宿主 permissionPresets,插件只记住
|
|
3713
|
+
// 「工作区 → preset」并在 agent/created 时用原生 set() 落盘。
|
|
3714
|
+
ensurePermissionData(ctx);
|
|
3715
|
+
const permissionOptions = presetOptions();
|
|
3716
|
+
if (permissionOptions.length > 0) {
|
|
3717
|
+
const override = workspacePreset(workspaceId);
|
|
3718
|
+
const effective = effectivePreset(workspaceId);
|
|
3719
|
+
const children = permissionOptions.map((option) => ({
|
|
3720
|
+
id: `preset-${option.value}`,
|
|
3721
|
+
label: h('span', { className: 'dsrm-host-label' },
|
|
3722
|
+
h('span', { className: 'dsrm-host-label-text' }, presetLabel(option.value, t)),
|
|
3723
|
+
option.value === effective ? permissionCheck() : null,
|
|
3724
|
+
),
|
|
3725
|
+
icon: { key: 'sidebar-row-menu.permission' },
|
|
3726
|
+
run: () => { setWorkspacePreset(rootCtx, workspaceId, option.value, t); },
|
|
3727
|
+
}));
|
|
3728
|
+
if (override !== '') {
|
|
3729
|
+
children.push({
|
|
3730
|
+
id: 'permission-follow-global',
|
|
3731
|
+
separatorBefore: true,
|
|
3732
|
+
label: t('permissionFollowGlobal'),
|
|
3733
|
+
icon: { key: 'sidebar-row-menu.permission' },
|
|
3734
|
+
run: () => { setWorkspacePreset(rootCtx, workspaceId, null, t); },
|
|
3735
|
+
});
|
|
3736
|
+
}
|
|
3737
|
+
items.push({
|
|
3738
|
+
id: 'new-session-permission',
|
|
3739
|
+
group: 'permission',
|
|
3740
|
+
separatorBefore: true,
|
|
3741
|
+
label: h('span', { className: 'dsrm-host-label' },
|
|
3742
|
+
h('span', { className: 'dsrm-host-label-text' }, t('newSessionPermission')),
|
|
3743
|
+
jsx(IconChevronRightOutlineRegular, { size: 12, className: 'dsrm-submenu-chevron' }),
|
|
3744
|
+
),
|
|
3745
|
+
icon: { key: 'sidebar-row-menu.permission' },
|
|
3746
|
+
children,
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3412
3750
|
items.push({
|
|
3413
3751
|
id: 'delete-sessions',
|
|
3414
3752
|
group: 'delete',
|
|
@@ -3438,12 +3776,13 @@ window.__ModuleLoader__.load({
|
|
|
3438
3776
|
{ key: 'copy', order: 150 },
|
|
3439
3777
|
{ key: 'pinned', order: 210 },
|
|
3440
3778
|
{ key: 'open-with', order: 220 },
|
|
3779
|
+
{ key: 'permission', order: 225 },
|
|
3441
3780
|
{ key: 'delete', order: 310 },
|
|
3442
3781
|
];
|
|
3443
3782
|
|
|
3444
3783
|
/**
|
|
3445
3784
|
* 0.2.2 的「…」按钮与右键共用同一份宿主菜单,条目本身无法感知手势,
|
|
3446
|
-
*
|
|
3785
|
+
* 所以按右键标记决定是否进入这份菜单;设置页可按条目选择只出现在一边。
|
|
3447
3786
|
*/
|
|
3448
3787
|
const WORKSPACE_GROUP_ELLIPSIS = {
|
|
3449
3788
|
new: 'newSession',
|
|
@@ -3452,13 +3791,14 @@ window.__ModuleLoader__.load({
|
|
|
3452
3791
|
copy: 'copyPath',
|
|
3453
3792
|
tab: 'openInTab',
|
|
3454
3793
|
archived: 'showArchived',
|
|
3794
|
+
permission: 'permissionDefault',
|
|
3455
3795
|
delete: 'deleteSessions',
|
|
3456
3796
|
};
|
|
3457
3797
|
|
|
3458
3798
|
function workspaceGroupVisible(groupKey) {
|
|
3459
|
-
if (rightClickMark) return true;
|
|
3460
3799
|
const key = WORKSPACE_GROUP_ELLIPSIS[groupKey];
|
|
3461
|
-
|
|
3800
|
+
if (key === undefined) return rightClickMark;
|
|
3801
|
+
return placementOn(key, rightClickMark ? 'context' : 'ellipsis');
|
|
3462
3802
|
}
|
|
3463
3803
|
|
|
3464
3804
|
/** 注册工作区动作;服务可能晚于插件 apply 出现,带有限重试。 */
|
|
@@ -3477,6 +3817,7 @@ window.__ModuleLoader__.load({
|
|
|
3477
3817
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.archive' }, IconArchiveOutlineRegular);
|
|
3478
3818
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.archive-off' }, IconArchiveOffOutlineRegular);
|
|
3479
3819
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.open-with' }, IconRightUpOutlineRegular);
|
|
3820
|
+
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.permission' }, ContextMenuPermissionIcon);
|
|
3480
3821
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.delete' }, IconTrashOutlineRegular);
|
|
3481
3822
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.app' }, ContextMenuAppIcon);
|
|
3482
3823
|
});
|
|
@@ -3493,6 +3834,7 @@ window.__ModuleLoader__.load({
|
|
|
3493
3834
|
return;
|
|
3494
3835
|
}
|
|
3495
3836
|
registerIcons();
|
|
3837
|
+
ensurePermissionData(ctx);
|
|
3496
3838
|
const registered = [];
|
|
3497
3839
|
for (const group of WORKSPACE_MENU_GROUPS) {
|
|
3498
3840
|
try {
|
|
@@ -3500,7 +3842,7 @@ window.__ModuleLoader__.load({
|
|
|
3500
3842
|
id: `sidebar-row-menu.workspace.${group.key}`,
|
|
3501
3843
|
target: 'workspace',
|
|
3502
3844
|
order: group.order,
|
|
3503
|
-
observe: () => [appsSource],
|
|
3845
|
+
observe: () => [appsSource, permissionSource],
|
|
3504
3846
|
resolve: (target) => {
|
|
3505
3847
|
if (!workspaceGroupVisible(group.key)) return [];
|
|
3506
3848
|
return workspaceMenuActions(ctx, target, localeOf(ctx))
|
|
@@ -3757,6 +4099,7 @@ window.__ModuleLoader__.load({
|
|
|
3757
4099
|
.dsrm-submenu-chevron-flipped{transform:scaleX(-1)}
|
|
3758
4100
|
.dsrm-host-label{display:flex;align-items:center;gap:6px;width:100%}
|
|
3759
4101
|
.dsrm-host-label-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
4102
|
+
.dsrm-check{margin-left:auto;flex:none;color:var(--dsw-alias-label-primary)}
|
|
3760
4103
|
.dsrm-submenu{position:fixed;z-index:1101;min-width:180px;max-width:300px;max-height:min(380px,64vh);overflow:auto;padding:4px}
|
|
3761
4104
|
.dsrm-submenu-empty{padding:6px 8px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
3762
4105
|
.dsrm-menu{position:fixed;z-index:2147483647}
|
|
@@ -3783,6 +4126,9 @@ window.__ModuleLoader__.load({
|
|
|
3783
4126
|
.dsrm-settings-label{font-size:14px;line-height:20px}
|
|
3784
4127
|
.dsrm-settings-hint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
3785
4128
|
.dsrm-settings-controls{display:flex;align-items:center;gap:10px}
|
|
4129
|
+
.dsrm-placement-controls{gap:16px}
|
|
4130
|
+
.dsrm-placement-choice{display:flex;align-items:center;gap:6px;white-space:nowrap}
|
|
4131
|
+
.dsrm-placement-choice-label{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
3786
4132
|
.dsrm-settings-children{margin-left:12px;padding:0 0 12px 18px;border-left:1px solid var(--dsw-alias-border-l2)}
|
|
3787
4133
|
.dsrm-settings-child{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:6px 14px 6px 0}
|
|
3788
4134
|
.dsrm-settings-child-label{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:18px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|