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/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
- * 全部默认关闭:不勾选时点「…」看到的仍是 DSH 原生菜单。
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': 'DSH 0.2.2 起「…」按钮与右键共用同一份菜单:勾选后对应条目也会出现在「…」菜单里,并沿用上面的单项开关;不勾选则只在右键菜单出现。',
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': 'Also show in the "…" menu',
322
- 'settings.ellipsisHint': 'Since DSH 0.2.2 the "…" button opens the same menu as right-click: checked entries also appear there, still subject to the switches above. Unchecked entries stay right-click only.',
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
- let ellipsisSettings = readStoredEllipsis();
721
- const ellipsisListeners = new Set();
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
- function publishEllipsis() {
724
- for (const listener of ellipsisListeners) {
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 subscribeEllipsis(listener) {
734
- ellipsisListeners.add(listener);
803
+ function subscribePlacements(listener) {
804
+ placementListeners.add(listener);
735
805
  return () => {
736
- ellipsisListeners.delete(listener);
806
+ placementListeners.delete(listener);
737
807
  };
738
808
  }
739
809
 
740
- function ellipsisSnapshot() {
741
- return ellipsisSettings;
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 useEllipsis() {
745
- return useSyncExternalStore(subscribeEllipsis, ellipsisSnapshot, ellipsisSnapshot);
827
+ function showsInEllipsis(key) {
828
+ return placementOf(key) !== 'context';
746
829
  }
747
830
 
748
- function setEllipsis(key, enabled) {
749
- if (!ELLIPSIS_KEYS.includes(key)) return;
750
- ellipsisSettings = { ...ellipsisSettings, [key]: enabled === true };
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(ELLIPSIS_KEY, JSON.stringify(ellipsisSettings));
836
+ globalThis.localStorage?.setItem(PLACEMENT_KEY, JSON.stringify(placementSettings));
753
837
  } catch {
754
838
  /* 隐私模式写不进去也照样生效到本次会话 */
755
839
  }
756
- publishEllipsis();
840
+ publishPlacements();
757
841
  }
758
842
 
759
- function ellipsisOn(key) {
760
- return ellipsisSettings[key] === true;
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 ellipsisSettings.newSession === true
766
- || ellipsisSettings.openWith === true
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
- /* ------------------------------------------------ 与 dsh-session-menu 共存 */
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 ellipsis = useEllipsis();
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 ellipsis = useEllipsis();
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 ellipsis = useEllipsis();
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 ellipsis = useEllipsis();
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 ELLIPSIS_ROWS = [
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 ellipsis = useEllipsis();
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 ellipsisGroups = ELLIPSIS_ROWS.map(([groupKey, titleKey, rows]) => h('div', {
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
- jsx(Switch, {
1859
- checked: ellipsis[key] === true,
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' }, ellipsisGroups),
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
- const ellipsis = useEllipsis();
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) => !ellipsisMode || ellipsis[key] === true;
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
- return key !== undefined && ellipsisSettings[key] === true;
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}