dsh-sidebar-row-menu 1.9.2 → 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 +52 -0
- package/README.en.md +40 -40
- package/README.md +31 -28
- package/lib/client.js +540 -134
- 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
|
+
}
|
|
789
|
+
|
|
790
|
+
let placementSettings = readStoredPlacements();
|
|
791
|
+
const placementListeners = new Set();
|
|
722
792
|
|
|
723
|
-
function
|
|
724
|
-
for (const listener of
|
|
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) {
|
|
@@ -1296,18 +1546,135 @@ window.__ModuleLoader__.load({
|
|
|
1296
1546
|
|
|
1297
1547
|
/* ---------------------------------------------------------- 打开方式子菜单 */
|
|
1298
1548
|
|
|
1299
|
-
/**
|
|
1300
|
-
function
|
|
1549
|
+
/** 视口可用尺寸:滚动条会占掉 innerWidth/innerHeight 的一部分,贴边要贴着可视区算。 */
|
|
1550
|
+
function viewportBounds() {
|
|
1551
|
+
const root = typeof document === 'undefined' ? null : document.documentElement;
|
|
1552
|
+
const width = root !== null && root !== undefined && root.clientWidth > 0 ? root.clientWidth : window.innerWidth;
|
|
1553
|
+
const height = root !== null && root !== undefined && root.clientHeight > 0 ? root.clientHeight : window.innerHeight;
|
|
1554
|
+
return { width: Number(width) || 0, height: Number(height) || 0 };
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* 元素真实占位尺寸。必须用 offsetWidth/offsetHeight:面板入场动画带 transform 缩放,
|
|
1559
|
+
* 动画期间 getBoundingClientRect 量出来偏小(实测 181 vs 198),按它夹取会溢出视口。
|
|
1560
|
+
*/
|
|
1561
|
+
function layoutSize(node) {
|
|
1562
|
+
if (node === null || node === undefined) return null;
|
|
1563
|
+
const width = Number(node.offsetWidth) || 0;
|
|
1564
|
+
const height = Number(node.offsetHeight) || 0;
|
|
1565
|
+
if (width > 0 && height > 0) return { width, height };
|
|
1566
|
+
if (typeof node.getBoundingClientRect !== 'function') return null;
|
|
1567
|
+
const rect = node.getBoundingClientRect();
|
|
1568
|
+
if (!(rect.width > 0) || !(rect.height > 0)) return null;
|
|
1569
|
+
return { width: rect.width, height: rect.height };
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/** 菜单面板定位:按布局尺寸夹进视口;尺寸后续变化(异步条目、字体)时再夹一次。 */
|
|
1573
|
+
function useMenuPos(hostRef, x, y, deps) {
|
|
1574
|
+
const [pos, setPos] = useState(null);
|
|
1575
|
+
const place = () => {
|
|
1576
|
+
const host = hostRef.current;
|
|
1577
|
+
if (host === null || host === undefined) return;
|
|
1578
|
+
const size = layoutSize(host);
|
|
1579
|
+
if (size === null) return;
|
|
1580
|
+
const view = viewportBounds();
|
|
1581
|
+
const margin = 8;
|
|
1582
|
+
const left = Math.max(margin, Math.min(x, view.width - size.width - margin));
|
|
1583
|
+
const top = Math.max(margin, Math.min(y, view.height - size.height - margin));
|
|
1584
|
+
setPos((prev) => (prev !== null && prev.left === left && prev.top === top ? prev : { left, top }));
|
|
1585
|
+
};
|
|
1586
|
+
useLayoutEffect(place, [x, y, ...deps]);
|
|
1587
|
+
useEffect(() => {
|
|
1588
|
+
if (typeof ResizeObserver !== 'function') return undefined;
|
|
1589
|
+
const host = hostRef.current;
|
|
1590
|
+
if (host === null || host === undefined) return undefined;
|
|
1591
|
+
const observer = new ResizeObserver(place);
|
|
1592
|
+
observer.observe(host);
|
|
1593
|
+
return () => observer.disconnect();
|
|
1594
|
+
}, [x, y]);
|
|
1595
|
+
return pos;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* 二级菜单位置:横向贴住菜单面板外沿,纵向和触发它的那一行对齐;
|
|
1600
|
+
* 右侧放不下就翻到左边,仍然紧贴面板,再整体夹进视口。
|
|
1601
|
+
*/
|
|
1602
|
+
function submenuGeometry(anchor, size, row = anchor) {
|
|
1301
1603
|
const margin = 12;
|
|
1302
1604
|
const gap = 6;
|
|
1605
|
+
const view = viewportBounds();
|
|
1303
1606
|
const width = Math.max(160, size?.width ?? 180);
|
|
1304
1607
|
const height = Math.max(0, size?.height ?? 0);
|
|
1608
|
+
const rightRoom = view.width - margin - (anchor.right + gap);
|
|
1609
|
+
const leftRoom = anchor.left - gap - margin;
|
|
1610
|
+
let side = 'right';
|
|
1305
1611
|
let left = anchor.right + gap;
|
|
1306
|
-
if (
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1612
|
+
if (rightRoom < width && leftRoom > rightRoom) {
|
|
1613
|
+
side = 'left';
|
|
1614
|
+
left = anchor.left - width - gap;
|
|
1615
|
+
}
|
|
1616
|
+
left = Math.max(margin, Math.min(left, view.width - width - margin));
|
|
1617
|
+
let top = row.top;
|
|
1618
|
+
top = Math.max(margin, Math.min(top, view.height - height - margin));
|
|
1619
|
+
return { left, top, side };
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* 二级菜单定位:面板先以隐藏状态挂载,量到真实尺寸后再贴边,
|
|
1624
|
+
* 因此顶到屏幕右侧时会精确翻到另一侧并紧贴菜单,不再按估算宽度留出空隙。
|
|
1625
|
+
*/
|
|
1626
|
+
function useSubmenuPos(open, hostRef, deps) {
|
|
1627
|
+
const [box, setBox] = useState(null);
|
|
1628
|
+
const place = () => {
|
|
1629
|
+
const host = hostRef.current;
|
|
1630
|
+
if (host === null || host === undefined) return;
|
|
1631
|
+
const panel = typeof host.querySelector === 'function' ? host.querySelector('.dsrm-submenu') : null;
|
|
1632
|
+
const size = layoutSize(panel);
|
|
1633
|
+
if (size === null) return;
|
|
1634
|
+
const row = host.getBoundingClientRect();
|
|
1635
|
+
// 横向以菜单面板外沿为锚:自绘菜单是 .dsrm-menu,会话 / 工作区行注入到宿主菜单里,
|
|
1636
|
+
// 那时要找宿主的 [role="menu"] 面板(行本身比面板内缩 4px,贴它会偏进面板里)。
|
|
1637
|
+
const menu = typeof host.closest === 'function'
|
|
1638
|
+
? (host.closest('.dsrm-menu') ?? host.closest('[role="menu"]'))
|
|
1639
|
+
: null;
|
|
1640
|
+
const anchor = menu !== null && menu !== undefined && menu !== host ? menu.getBoundingClientRect() : row;
|
|
1641
|
+
const next = submenuGeometry(anchor, size, row);
|
|
1642
|
+
setBox((prev) => (prev !== null
|
|
1643
|
+
&& prev.left === next.left
|
|
1644
|
+
&& prev.top === next.top
|
|
1645
|
+
&& prev.side === next.side ? prev : next));
|
|
1646
|
+
};
|
|
1647
|
+
useLayoutEffect(() => {
|
|
1648
|
+
if (!open) {
|
|
1649
|
+
setBox(null);
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
place();
|
|
1653
|
+
}, [open, ...deps]);
|
|
1654
|
+
useEffect(() => {
|
|
1655
|
+
if (!open) return undefined;
|
|
1656
|
+
window.addEventListener('resize', place);
|
|
1657
|
+
const host = hostRef.current;
|
|
1658
|
+
const panel = host !== null && host !== undefined && typeof host.querySelector === 'function'
|
|
1659
|
+
? host.querySelector('.dsrm-submenu')
|
|
1660
|
+
: null;
|
|
1661
|
+
const observer = typeof ResizeObserver === 'function' && panel !== null && panel !== undefined
|
|
1662
|
+
? new ResizeObserver(place)
|
|
1663
|
+
: null;
|
|
1664
|
+
if (observer !== null) observer.observe(panel);
|
|
1665
|
+
return () => {
|
|
1666
|
+
window.removeEventListener('resize', place);
|
|
1667
|
+
if (observer !== null) observer.disconnect();
|
|
1668
|
+
};
|
|
1669
|
+
}, [open, ...deps]);
|
|
1670
|
+
return box;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/** 面板翻到左侧时箭头跟着翻转,指向菜单所在方向。 */
|
|
1674
|
+
function chevronClass(box) {
|
|
1675
|
+
return box?.side === 'left'
|
|
1676
|
+
? 'dsrm-submenu-chevron dsrm-submenu-chevron-flipped'
|
|
1677
|
+
: 'dsrm-submenu-chevron';
|
|
1311
1678
|
}
|
|
1312
1679
|
|
|
1313
1680
|
/** slot 注入的 useMenuOpenState 在不同版本里可能不是数组,宽松取关闭函数。 */
|
|
@@ -1326,7 +1693,6 @@ window.__ModuleLoader__.load({
|
|
|
1326
1693
|
const apps = useApps();
|
|
1327
1694
|
usePinned();
|
|
1328
1695
|
const [open, setOpen] = useState(false);
|
|
1329
|
-
const [box, setBox] = useState(null);
|
|
1330
1696
|
const hostRef = useRef(null);
|
|
1331
1697
|
const closeTimer = useRef(0);
|
|
1332
1698
|
|
|
@@ -1352,17 +1718,8 @@ window.__ModuleLoader__.load({
|
|
|
1352
1718
|
};
|
|
1353
1719
|
}, [open]);
|
|
1354
1720
|
|
|
1355
|
-
//
|
|
1356
|
-
|
|
1357
|
-
useEffect(() => {
|
|
1358
|
-
if (!open) return;
|
|
1359
|
-
const host = hostRef.current;
|
|
1360
|
-
if (host === null) return;
|
|
1361
|
-
const pinnedIds = new Set(effectivePinned(apps.ids));
|
|
1362
|
-
const count = apps.ids.filter((id) => childOn(id) && !pinnedIds.has(id)).length;
|
|
1363
|
-
const height = Math.min(380, Math.max(40, count * 34 + 8));
|
|
1364
|
-
setBox(submenuGeometry(host.getBoundingClientRect(), { width: 220, height }));
|
|
1365
|
-
}, [open, apps.status, apps.ids]);
|
|
1721
|
+
// 面板先隐藏挂载再量真实尺寸,所以这里不再按条目数估算宽高。
|
|
1722
|
+
const box = useSubmenuPos(open, hostRef, [apps.status, apps.ids]);
|
|
1366
1723
|
|
|
1367
1724
|
// 会话行菜单的 pointerover/out 在 document 捕获阶段被「移出关闭」守卫截断,
|
|
1368
1725
|
// React 的 onPointerEnter 收不到。把悬浮展开挂到 window 捕获阶段,抢在守卫前。
|
|
@@ -1388,12 +1745,6 @@ window.__ModuleLoader__.load({
|
|
|
1388
1745
|
|
|
1389
1746
|
const openNow = () => {
|
|
1390
1747
|
window.clearTimeout(closeTimer.current);
|
|
1391
|
-
if (!open && hostRef.current !== null) {
|
|
1392
|
-
const pinnedIds = new Set(effectivePinned(apps.ids));
|
|
1393
|
-
const count = apps.ids.filter((id) => childOn(id) && !pinnedIds.has(id)).length;
|
|
1394
|
-
const height = Math.min(380, Math.max(40, count * 34 + 8));
|
|
1395
|
-
setBox(submenuGeometry(hostRef.current.getBoundingClientRect(), { width: 220, height }));
|
|
1396
|
-
}
|
|
1397
1748
|
setOpen(true);
|
|
1398
1749
|
ensureApps();
|
|
1399
1750
|
};
|
|
@@ -1459,7 +1810,7 @@ window.__ModuleLoader__.load({
|
|
|
1459
1810
|
},
|
|
1460
1811
|
jsx(IconRightUpOutlineRegular, { size: 14 }),
|
|
1461
1812
|
h('span', { className: 'dsrm-submenu-label' }, t('openWith')),
|
|
1462
|
-
jsx(IconChevronRightOutlineRegular, { size: 12, className:
|
|
1813
|
+
jsx(IconChevronRightOutlineRegular, { size: 12, className: chevronClass(box) }),
|
|
1463
1814
|
),
|
|
1464
1815
|
open ? h(MenuSurface, {
|
|
1465
1816
|
role: 'group',
|
|
@@ -1485,8 +1836,7 @@ window.__ModuleLoader__.load({
|
|
|
1485
1836
|
*/
|
|
1486
1837
|
function SessionOpenWithMenuItem({ sessionId, useMenuOpenState, t }) {
|
|
1487
1838
|
const rightClick = useRightClickEntry();
|
|
1488
|
-
const
|
|
1489
|
-
const visible = rightClick || ellipsis.sessionOpenWith === true;
|
|
1839
|
+
const visible = placementOn('sessionOpenWith', rightClick ? 'context' : 'ellipsis');
|
|
1490
1840
|
const items = useItems();
|
|
1491
1841
|
const apps = useApps();
|
|
1492
1842
|
const setMenuOpen = menuCloser(useMenuOpenState);
|
|
@@ -1508,8 +1858,7 @@ window.__ModuleLoader__.load({
|
|
|
1508
1858
|
*/
|
|
1509
1859
|
function SessionOpenWithPinnedMenuItem({ sessionId, useMenuOpenState, t }) {
|
|
1510
1860
|
const rightClick = useRightClickEntry();
|
|
1511
|
-
const
|
|
1512
|
-
const visible = rightClick || ellipsis.sessionOpenWith === true;
|
|
1861
|
+
const visible = placementOn('sessionOpenWith', rightClick ? 'context' : 'ellipsis');
|
|
1513
1862
|
const items = useItems();
|
|
1514
1863
|
useChildren();
|
|
1515
1864
|
const apps = useApps();
|
|
@@ -1547,8 +1896,7 @@ window.__ModuleLoader__.load({
|
|
|
1547
1896
|
*/
|
|
1548
1897
|
function SessionDeleteMenuItem({ sessionId, displayTitle, useMenuOpenState, t }) {
|
|
1549
1898
|
const rightClick = useRightClickEntry();
|
|
1550
|
-
const
|
|
1551
|
-
const visible = rightClick || ellipsis.sessionDelete === true;
|
|
1899
|
+
const visible = placementOn('sessionDelete', rightClick ? 'context' : 'ellipsis');
|
|
1552
1900
|
const items = useItems();
|
|
1553
1901
|
const archived = useArchivedIds();
|
|
1554
1902
|
const setMenuOpen = menuCloser(useMenuOpenState);
|
|
@@ -1572,8 +1920,7 @@ window.__ModuleLoader__.load({
|
|
|
1572
1920
|
*/
|
|
1573
1921
|
function SessionDeleteArchivedMenuItem({ sessionId, displayTitle, useMenuOpenState, t }) {
|
|
1574
1922
|
const rightClick = useRightClickEntry();
|
|
1575
|
-
const
|
|
1576
|
-
const visible = rightClick || ellipsis.sessionDeleteArchived === true;
|
|
1923
|
+
const visible = placementOn('sessionDeleteArchived', rightClick ? 'context' : 'ellipsis');
|
|
1577
1924
|
const items = useItems();
|
|
1578
1925
|
const archived = useArchivedIds();
|
|
1579
1926
|
const setMenuOpen = menuCloser(useMenuOpenState);
|
|
@@ -1603,10 +1950,11 @@ window.__ModuleLoader__.load({
|
|
|
1603
1950
|
['archivedSessions', 'settings.archivedSessions.label', 'settings.archivedSessions.hint'],
|
|
1604
1951
|
];
|
|
1605
1952
|
|
|
1606
|
-
/**
|
|
1607
|
-
const
|
|
1953
|
+
/** 菜单显示位置;按行分组,避免用户分不清工作区项和会话项。 */
|
|
1954
|
+
const PLACEMENT_ROWS = [
|
|
1608
1955
|
['workspace', 'settings.ellipsisWorkspace', [
|
|
1609
1956
|
['newSession', 'settings.ellipsis.newSession.label', 'settings.ellipsis.newSession.hint'],
|
|
1957
|
+
['permissionDefault', 'settings.ellipsis.permissionDefault.label', 'settings.ellipsis.permissionDefault.hint'],
|
|
1610
1958
|
['openWith', 'settings.ellipsis.openWith.label', 'settings.ellipsis.openWith.hint'],
|
|
1611
1959
|
['copyPath', 'settings.ellipsis.copyPath.label', 'settings.ellipsis.copyPath.hint'],
|
|
1612
1960
|
['openInTab', 'settings.ellipsis.openInTab.label', 'settings.ellipsis.openInTab.hint'],
|
|
@@ -1656,7 +2004,7 @@ window.__ModuleLoader__.load({
|
|
|
1656
2004
|
|
|
1657
2005
|
function SettingsSection({ t }) {
|
|
1658
2006
|
const items = useItems();
|
|
1659
|
-
const
|
|
2007
|
+
const placements = usePlacements();
|
|
1660
2008
|
const blank = useBlankMenu();
|
|
1661
2009
|
const children = useChildren();
|
|
1662
2010
|
const apps = useApps();
|
|
@@ -1740,7 +2088,32 @@ window.__ModuleLoader__.load({
|
|
|
1740
2088
|
return key === 'openWith' && childRows ? [row, childRows] : [row];
|
|
1741
2089
|
});
|
|
1742
2090
|
|
|
1743
|
-
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', {
|
|
1744
2117
|
className: 'dsrm-settings-group',
|
|
1745
2118
|
key: groupKey,
|
|
1746
2119
|
},
|
|
@@ -1753,12 +2126,9 @@ window.__ModuleLoader__.load({
|
|
|
1753
2126
|
h('div', { className: 'dsrm-settings-label' }, t(labelKey)),
|
|
1754
2127
|
h('div', { className: 'dsrm-settings-hint' }, t(hintKey)),
|
|
1755
2128
|
),
|
|
1756
|
-
h('div', { className: 'dsrm-settings-controls' },
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
label: t(labelKey),
|
|
1760
|
-
onChange: (next) => setEllipsis(key, next),
|
|
1761
|
-
}),
|
|
2129
|
+
h('div', { className: 'dsrm-settings-controls dsrm-placement-controls' },
|
|
2130
|
+
placementSwitch(key, 'context', 'settings.placement.context'),
|
|
2131
|
+
placementSwitch(key, 'ellipsis', 'settings.placement.ellipsis'),
|
|
1762
2132
|
),
|
|
1763
2133
|
))),
|
|
1764
2134
|
));
|
|
@@ -1787,7 +2157,7 @@ window.__ModuleLoader__.load({
|
|
|
1787
2157
|
h('div', { className: 'dsrm-settings-list' }, rows),
|
|
1788
2158
|
h('h2', { className: 'dsrm-settings-title dsrm-settings-subtitle' }, t('settings.ellipsisTitle')),
|
|
1789
2159
|
h('div', { className: 'dsrm-settings-note' }, t('settings.ellipsisHint')),
|
|
1790
|
-
h('div', { className: 'dsrm-settings-groups' },
|
|
2160
|
+
h('div', { className: 'dsrm-settings-groups' }, placementGroups),
|
|
1791
2161
|
h('h2', { className: 'dsrm-settings-title dsrm-settings-subtitle' }, t('settings.blankMenuTitle')),
|
|
1792
2162
|
h('div', { className: 'dsrm-settings-note' }, t('settings.blankMenuNote')),
|
|
1793
2163
|
h('div', { className: 'dsrm-settings-list' }, blankRows),
|
|
@@ -1803,16 +2173,16 @@ window.__ModuleLoader__.load({
|
|
|
1803
2173
|
function WorkspaceRowMenu({ x, y, props, onClose, mode = 'context' }) {
|
|
1804
2174
|
const t = localeOf(rootCtx);
|
|
1805
2175
|
const items = useItems();
|
|
1806
|
-
|
|
2176
|
+
usePlacements();
|
|
1807
2177
|
const apps = useApps();
|
|
1808
2178
|
usePinned();
|
|
1809
2179
|
useChildren();
|
|
1810
2180
|
const hostRef = useRef(null);
|
|
1811
|
-
const
|
|
2181
|
+
const pos = useMenuPos(hostRef, x, y, [apps.status]);
|
|
1812
2182
|
|
|
1813
2183
|
const ellipsisMode = mode === 'ellipsis';
|
|
1814
|
-
/**
|
|
1815
|
-
const show = (key) =>
|
|
2184
|
+
/** 右键菜单永远显示;「…」菜单按设置页里选定的位置显示(内置项除外)。 */
|
|
2185
|
+
const show = (key) => placementOn(key, ellipsisMode ? 'ellipsis' : 'context');
|
|
1816
2186
|
const openWithOn = items.openWith !== false && show('openWith');
|
|
1817
2187
|
|
|
1818
2188
|
useEffect(() => {
|
|
@@ -1836,16 +2206,7 @@ window.__ModuleLoader__.load({
|
|
|
1836
2206
|
};
|
|
1837
2207
|
}, [onClose]);
|
|
1838
2208
|
|
|
1839
|
-
//
|
|
1840
|
-
useLayoutEffect(() => {
|
|
1841
|
-
const host = hostRef.current;
|
|
1842
|
-
if (host === null) return;
|
|
1843
|
-
const rect = host.getBoundingClientRect();
|
|
1844
|
-
const margin = 8;
|
|
1845
|
-
const left = Math.max(margin, Math.min(x, window.innerWidth - rect.width - margin));
|
|
1846
|
-
const top = Math.max(margin, Math.min(y, window.innerHeight - rect.height - margin));
|
|
1847
|
-
setPos((prev) => (prev !== null && prev.left === left && prev.top === top ? prev : { left, top }));
|
|
1848
|
-
}, [x, y, apps.status]);
|
|
2209
|
+
// 置顶应用是异步取到的,行数会变;尺寸变化由 useMenuPos 里的 ResizeObserver 兜住。
|
|
1849
2210
|
|
|
1850
2211
|
const group = props.group ?? {};
|
|
1851
2212
|
const workspaceId = typeof group.workspaceId === 'string' ? group.workspaceId : '';
|
|
@@ -2357,7 +2718,6 @@ window.__ModuleLoader__.load({
|
|
|
2357
2718
|
useChildren();
|
|
2358
2719
|
usePinned();
|
|
2359
2720
|
const [open, setOpen] = useState(false);
|
|
2360
|
-
const [box, setBox] = useState(null);
|
|
2361
2721
|
const hostRef = useRef(null);
|
|
2362
2722
|
const closeTimer = useRef(0);
|
|
2363
2723
|
|
|
@@ -2379,27 +2739,13 @@ window.__ModuleLoader__.load({
|
|
|
2379
2739
|
};
|
|
2380
2740
|
}, [open]);
|
|
2381
2741
|
|
|
2382
|
-
//
|
|
2383
|
-
|
|
2384
|
-
if (!open) return;
|
|
2385
|
-
const host = hostRef.current;
|
|
2386
|
-
if (host === null) return;
|
|
2387
|
-
const pinnedIds = new Set(effectivePinned(apps.ids).filter((id) => childOn(id)));
|
|
2388
|
-
const count = apps.ids.filter((id) => childOn(id) && !pinnedIds.has(id)).length;
|
|
2389
|
-
const height = Math.min(380, Math.max(40, count * 34 + 8));
|
|
2390
|
-
setBox(submenuGeometry(host.getBoundingClientRect(), { width: 220, height }));
|
|
2391
|
-
}, [open, apps.status, apps.ids]);
|
|
2742
|
+
// 面板先隐藏挂载再量真实尺寸,位置按真实宽高算。
|
|
2743
|
+
const box = useSubmenuPos(open, hostRef, [apps.status, apps.ids]);
|
|
2392
2744
|
|
|
2393
2745
|
useEffect(() => () => window.clearTimeout(closeTimer.current), []);
|
|
2394
2746
|
|
|
2395
2747
|
const openNow = () => {
|
|
2396
2748
|
window.clearTimeout(closeTimer.current);
|
|
2397
|
-
if (!open && hostRef.current !== null) {
|
|
2398
|
-
const pinnedIds = new Set(effectivePinned(apps.ids).filter((id) => childOn(id)));
|
|
2399
|
-
const count = apps.ids.filter((id) => childOn(id) && !pinnedIds.has(id)).length;
|
|
2400
|
-
const height = Math.min(380, Math.max(40, count * 34 + 8));
|
|
2401
|
-
setBox(submenuGeometry(hostRef.current.getBoundingClientRect(), { width: 220, height }));
|
|
2402
|
-
}
|
|
2403
2749
|
setOpen(true);
|
|
2404
2750
|
ensureApps();
|
|
2405
2751
|
};
|
|
@@ -2458,7 +2804,7 @@ window.__ModuleLoader__.load({
|
|
|
2458
2804
|
icon: jsx(IconRightUpOutlineRegular, { size: 14 }),
|
|
2459
2805
|
children: h('span', { className: 'dsrm-host-label' },
|
|
2460
2806
|
h('span', { className: 'dsrm-host-label-text' }, t('openWith')),
|
|
2461
|
-
jsx(IconChevronRightOutlineRegular, { size: 12, className:
|
|
2807
|
+
jsx(IconChevronRightOutlineRegular, { size: 12, className: chevronClass(box) }),
|
|
2462
2808
|
),
|
|
2463
2809
|
}),
|
|
2464
2810
|
open ? h(MenuSurface, {
|
|
@@ -2506,7 +2852,6 @@ window.__ModuleLoader__.load({
|
|
|
2506
2852
|
*/
|
|
2507
2853
|
function BlankMoreRows({ label, icon, controller, separatorBefore, onClose, children }) {
|
|
2508
2854
|
const [open, setOpen] = useState(false);
|
|
2509
|
-
const [box, setBox] = useState(null);
|
|
2510
2855
|
const hostRef = useRef(null);
|
|
2511
2856
|
const closeTimer = useRef(0);
|
|
2512
2857
|
const entries = orderMoreItems(controller.items);
|
|
@@ -2529,22 +2874,13 @@ window.__ModuleLoader__.load({
|
|
|
2529
2874
|
};
|
|
2530
2875
|
}, [open]);
|
|
2531
2876
|
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
const host = hostRef.current;
|
|
2535
|
-
if (host === null) return;
|
|
2536
|
-
const height = Math.min(320, Math.max(40, entries.length * 34 + 8));
|
|
2537
|
-
setBox(submenuGeometry(host.getBoundingClientRect(), { width: 220, height }));
|
|
2538
|
-
}, [open, controller, entries.length]);
|
|
2877
|
+
// 面板先隐藏挂载再量真实尺寸,位置按真实宽高算。
|
|
2878
|
+
const box = useSubmenuPos(open, hostRef, [entries.length]);
|
|
2539
2879
|
|
|
2540
2880
|
useEffect(() => () => window.clearTimeout(closeTimer.current), []);
|
|
2541
2881
|
|
|
2542
2882
|
const openNow = () => {
|
|
2543
2883
|
window.clearTimeout(closeTimer.current);
|
|
2544
|
-
if (!open && hostRef.current !== null) {
|
|
2545
|
-
const height = Math.min(320, Math.max(40, entries.length * 34 + 8));
|
|
2546
|
-
setBox(submenuGeometry(hostRef.current.getBoundingClientRect(), { width: 220, height }));
|
|
2547
|
-
}
|
|
2548
2884
|
setOpen(true);
|
|
2549
2885
|
};
|
|
2550
2886
|
const closeSoon = () => {
|
|
@@ -2578,7 +2914,7 @@ window.__ModuleLoader__.load({
|
|
|
2578
2914
|
icon: headerIconNode(icon),
|
|
2579
2915
|
children: h('span', { className: 'dsrm-host-label' },
|
|
2580
2916
|
h('span', { className: 'dsrm-host-label-text' }, children ?? label),
|
|
2581
|
-
jsx(IconChevronRightOutlineRegular, { size: 12, className:
|
|
2917
|
+
jsx(IconChevronRightOutlineRegular, { size: 12, className: chevronClass(box) }),
|
|
2582
2918
|
),
|
|
2583
2919
|
}),
|
|
2584
2920
|
open ? h(MenuSurface, {
|
|
@@ -2610,7 +2946,7 @@ window.__ModuleLoader__.load({
|
|
|
2610
2946
|
*/
|
|
2611
2947
|
function HeaderToolbarMenu({ x, y, items, t, sessionId, onClose }) {
|
|
2612
2948
|
const hostRef = useRef(null);
|
|
2613
|
-
const
|
|
2949
|
+
const pos = useMenuPos(hostRef, x, y, []);
|
|
2614
2950
|
const itemsConfig = useItems();
|
|
2615
2951
|
const blankMenu = useBlankMenu();
|
|
2616
2952
|
const apps = useApps();
|
|
@@ -2634,15 +2970,7 @@ window.__ModuleLoader__.load({
|
|
|
2634
2970
|
};
|
|
2635
2971
|
}, [onClose]);
|
|
2636
2972
|
|
|
2637
|
-
|
|
2638
|
-
const host = hostRef.current;
|
|
2639
|
-
if (host === null) return;
|
|
2640
|
-
const rect = host.getBoundingClientRect();
|
|
2641
|
-
const margin = 8;
|
|
2642
|
-
const left = Math.max(margin, Math.min(x, window.innerWidth - rect.width - margin));
|
|
2643
|
-
const top = Math.max(margin, Math.min(y, window.innerHeight - rect.height - margin));
|
|
2644
|
-
setPos((prev) => (prev !== null && prev.left === left && prev.top === top ? prev : { left, top }));
|
|
2645
|
-
}, [x, y, items]);
|
|
2973
|
+
// 位置由 useMenuPos 统一夹取:菜单行数随工具条内容变化,尺寸变了再夹一次。
|
|
2646
2974
|
|
|
2647
2975
|
const cwd = typeof sessionId === 'string' && sessionId !== '' ? sessionCwd(rootCtx, sessionId) : '';
|
|
2648
2976
|
const enabledApps = apps.ids.filter((id) => childOn(id));
|
|
@@ -3240,6 +3568,37 @@ window.__ModuleLoader__.load({
|
|
|
3240
3568
|
* 数据贡献的一批工作区动作;group 字段决定它挂到哪个 order 的 provider,
|
|
3241
3569
|
* 从而夹在原生 rename(100)/reveal(200)/delete(300) 之间。
|
|
3242
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
|
+
|
|
3243
3602
|
function workspaceMenuActions(ctx, value, t) {
|
|
3244
3603
|
const workspaceId = typeof value?.workspaceId === 'string' ? value.workspaceId : '';
|
|
3245
3604
|
if (workspaceId === '') return [];
|
|
@@ -3350,6 +3709,44 @@ window.__ModuleLoader__.load({
|
|
|
3350
3709
|
});
|
|
3351
3710
|
}
|
|
3352
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
|
+
|
|
3353
3750
|
items.push({
|
|
3354
3751
|
id: 'delete-sessions',
|
|
3355
3752
|
group: 'delete',
|
|
@@ -3379,12 +3776,13 @@ window.__ModuleLoader__.load({
|
|
|
3379
3776
|
{ key: 'copy', order: 150 },
|
|
3380
3777
|
{ key: 'pinned', order: 210 },
|
|
3381
3778
|
{ key: 'open-with', order: 220 },
|
|
3779
|
+
{ key: 'permission', order: 225 },
|
|
3382
3780
|
{ key: 'delete', order: 310 },
|
|
3383
3781
|
];
|
|
3384
3782
|
|
|
3385
3783
|
/**
|
|
3386
3784
|
* 0.2.2 的「…」按钮与右键共用同一份宿主菜单,条目本身无法感知手势,
|
|
3387
|
-
*
|
|
3785
|
+
* 所以按右键标记决定是否进入这份菜单;设置页可按条目选择只出现在一边。
|
|
3388
3786
|
*/
|
|
3389
3787
|
const WORKSPACE_GROUP_ELLIPSIS = {
|
|
3390
3788
|
new: 'newSession',
|
|
@@ -3393,13 +3791,14 @@ window.__ModuleLoader__.load({
|
|
|
3393
3791
|
copy: 'copyPath',
|
|
3394
3792
|
tab: 'openInTab',
|
|
3395
3793
|
archived: 'showArchived',
|
|
3794
|
+
permission: 'permissionDefault',
|
|
3396
3795
|
delete: 'deleteSessions',
|
|
3397
3796
|
};
|
|
3398
3797
|
|
|
3399
3798
|
function workspaceGroupVisible(groupKey) {
|
|
3400
|
-
if (rightClickMark) return true;
|
|
3401
3799
|
const key = WORKSPACE_GROUP_ELLIPSIS[groupKey];
|
|
3402
|
-
|
|
3800
|
+
if (key === undefined) return rightClickMark;
|
|
3801
|
+
return placementOn(key, rightClickMark ? 'context' : 'ellipsis');
|
|
3403
3802
|
}
|
|
3404
3803
|
|
|
3405
3804
|
/** 注册工作区动作;服务可能晚于插件 apply 出现,带有限重试。 */
|
|
@@ -3418,6 +3817,7 @@ window.__ModuleLoader__.load({
|
|
|
3418
3817
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.archive' }, IconArchiveOutlineRegular);
|
|
3419
3818
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.archive-off' }, IconArchiveOffOutlineRegular);
|
|
3420
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);
|
|
3421
3821
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.delete' }, IconTrashOutlineRegular);
|
|
3422
3822
|
yield ctx.slots.register({ name: 'contextMenu.icon', key: 'sidebar-row-menu.app' }, ContextMenuAppIcon);
|
|
3423
3823
|
});
|
|
@@ -3434,6 +3834,7 @@ window.__ModuleLoader__.load({
|
|
|
3434
3834
|
return;
|
|
3435
3835
|
}
|
|
3436
3836
|
registerIcons();
|
|
3837
|
+
ensurePermissionData(ctx);
|
|
3437
3838
|
const registered = [];
|
|
3438
3839
|
for (const group of WORKSPACE_MENU_GROUPS) {
|
|
3439
3840
|
try {
|
|
@@ -3441,7 +3842,7 @@ window.__ModuleLoader__.load({
|
|
|
3441
3842
|
id: `sidebar-row-menu.workspace.${group.key}`,
|
|
3442
3843
|
target: 'workspace',
|
|
3443
3844
|
order: group.order,
|
|
3444
|
-
observe: () => [appsSource],
|
|
3845
|
+
observe: () => [appsSource, permissionSource],
|
|
3445
3846
|
resolve: (target) => {
|
|
3446
3847
|
if (!workspaceGroupVisible(group.key)) return [];
|
|
3447
3848
|
return workspaceMenuActions(ctx, target, localeOf(ctx))
|
|
@@ -3695,8 +4096,10 @@ window.__ModuleLoader__.load({
|
|
|
3695
4096
|
.dsrm-submenu-trigger:hover:not(:disabled),.dsrm-submenu-trigger:focus-visible{background:var(--dsw-alias-interactive-bg-hover);outline:none}
|
|
3696
4097
|
.dsrm-submenu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3697
4098
|
.dsrm-submenu-chevron{color:var(--dsw-alias-label-caption)}
|
|
4099
|
+
.dsrm-submenu-chevron-flipped{transform:scaleX(-1)}
|
|
3698
4100
|
.dsrm-host-label{display:flex;align-items:center;gap:6px;width:100%}
|
|
3699
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)}
|
|
3700
4103
|
.dsrm-submenu{position:fixed;z-index:1101;min-width:180px;max-width:300px;max-height:min(380px,64vh);overflow:auto;padding:4px}
|
|
3701
4104
|
.dsrm-submenu-empty{padding:6px 8px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
3702
4105
|
.dsrm-menu{position:fixed;z-index:2147483647}
|
|
@@ -3723,6 +4126,9 @@ window.__ModuleLoader__.load({
|
|
|
3723
4126
|
.dsrm-settings-label{font-size:14px;line-height:20px}
|
|
3724
4127
|
.dsrm-settings-hint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
3725
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}
|
|
3726
4132
|
.dsrm-settings-children{margin-left:12px;padding:0 0 12px 18px;border-left:1px solid var(--dsw-alias-border-l2)}
|
|
3727
4133
|
.dsrm-settings-child{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:6px 14px 6px 0}
|
|
3728
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}
|