@mrweicodes/dsh-permgate 1.3.10 → 1.5.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/README.md +211 -211
- package/client.js +247 -36
- package/index.js +1485 -394
- package/package.json +12 -2
package/client.js
CHANGED
|
@@ -261,6 +261,7 @@ window.__ModuleLoader__.load({
|
|
|
261
261
|
'permgate:reload': ['POST', '/permgate/reload'],
|
|
262
262
|
'permgate:open-config': ['POST', '/permgate/open-config'],
|
|
263
263
|
'permgate:open-file': ['POST', '/permgate/open-file'],
|
|
264
|
+
'permgate:set-fallback': ['POST', '/permgate/set-fallback'],
|
|
264
265
|
};
|
|
265
266
|
function call(method, args) {
|
|
266
267
|
const entry = ROUTES[method] || ['GET', '/permgate/status'];
|
|
@@ -659,17 +660,31 @@ window.__ModuleLoader__.load({
|
|
|
659
660
|
};
|
|
660
661
|
}
|
|
661
662
|
|
|
662
|
-
|
|
663
|
-
|
|
663
|
+
// 分类清单默认值:以宿主下发为准(见 applyStatusLists),避免双端各存一份而漂移
|
|
664
|
+
let CATS = ['directory', 'command', 'read', 'image', 'edit', 'undo', 'subagent', 'doomloop'];
|
|
665
|
+
let EXC_CATS = ['directory', 'command', 'read', 'image', 'edit', 'undo'];
|
|
666
|
+
function applyStatusLists(s) {
|
|
667
|
+
if (s && Array.isArray(s.cats) && s.cats.length) CATS = s.cats;
|
|
668
|
+
if (s && Array.isArray(s.excCats) && s.excCats.length) EXC_CATS = s.excCats;
|
|
669
|
+
if (s && Array.isArray(s.modes) && s.modes.length) MODES = s.modes;
|
|
670
|
+
if (s && Array.isArray(s.allModes) && s.allModes.length) ALL_MODES = s.allModes;
|
|
671
|
+
if (s && Array.isArray(s.quickPreset) && s.quickPreset.length) QUICK_PRESET = s.quickPreset;
|
|
672
|
+
if (s && s.quickDefaults && typeof s.quickDefaults === 'object') QUICK_DEFAULTS = s.quickDefaults;
|
|
673
|
+
}
|
|
664
674
|
// 例外列表超过该数量默认折叠(展开/折叠按钮在标题行右侧)
|
|
665
675
|
const EXC_COLLAPSE_THRESHOLD = 6;
|
|
666
676
|
// 文件对比缓存:按审批 id 缓存 /permgate/file-diff 结果(弹窗详情与右侧抽屉共用)
|
|
667
677
|
const diffCache = new Map();
|
|
668
678
|
const diffFetching = new Set();
|
|
669
|
-
|
|
670
|
-
|
|
679
|
+
let ALL_MODES = ['ask', 'allow', 'deny', 'inherit'];
|
|
680
|
+
let MODES = ['ask', 'allow', 'deny'];
|
|
671
681
|
const MODE_COLORS = { ask: '#e65100', allow: '#2e7d32', deny: '#c62828', inherit: '#888' };
|
|
672
|
-
|
|
682
|
+
// 快捷工具预设清单与默认动作都由宿主下发(permgate:status 的 quickPreset/quickDefaults),
|
|
683
|
+
// 与 CATS/MODES 同口径,避免宿主与浏览器各存一份清单、新增工具时漂移;
|
|
684
|
+
// 未显式配置的工具按「项目键 → 全局键 → 预设默认 → 会话兜底」显示,与服务端 quickAction 同链。
|
|
685
|
+
let QUICK_PRESET = [];
|
|
686
|
+
let QUICK_DEFAULTS = {};
|
|
687
|
+
let QUICK_FALLBACK = 'ask';
|
|
673
688
|
|
|
674
689
|
// ── 中英文适配:字典 + locale 绑定(跟随 dsh 语言设置自动切换)──────────
|
|
675
690
|
let LC = null; // apply 时注入的 ctx
|
|
@@ -694,6 +709,10 @@ window.__ModuleLoader__.load({
|
|
|
694
709
|
'app.diffMore': '其余 {n} 行',
|
|
695
710
|
'app.diffLoading': '加载中…',
|
|
696
711
|
'app.diffErr': '无法生成对比',
|
|
712
|
+
'app.imageTag': '图片',
|
|
713
|
+
'app.imageTooLarge': '图片过大,未生成缩略图',
|
|
714
|
+
'app.imageSizeUnknown': '无法确认图片尺寸,未生成缩略图',
|
|
715
|
+
'app.imageNoPreview': '暂无可用的缩略图',
|
|
697
716
|
'app.uiErr': '界面渲染出错',
|
|
698
717
|
'app.eolNote': '行尾已按 LF 归一化匹配(磁盘为 CRLF/CR 而参数为 LF):预览仅为意图展示,实际编辑可能因行尾不一致失败',
|
|
699
718
|
'app.diffGapShow': '显示 {n} 行未更改',
|
|
@@ -718,13 +737,17 @@ window.__ModuleLoader__.load({
|
|
|
718
737
|
'cat.directory': '目录访问(工作区外)',
|
|
719
738
|
'cat.command': '执行命令',
|
|
720
739
|
'cat.read': '读取文件',
|
|
740
|
+
'cat.image': '读取图片',
|
|
721
741
|
'cat.edit': '编辑文件',
|
|
742
|
+
'cat.undo': '撤销操作(恢复上次编辑前的内容)',
|
|
722
743
|
'cat.subagent': '启动子代理',
|
|
723
744
|
'cat.doomloop': '重复操作(Doom Loop)',
|
|
724
745
|
'catS.directory': '目录',
|
|
725
746
|
'catS.command': '命令',
|
|
726
747
|
'catS.read': '读取',
|
|
748
|
+
'catS.image': '图片',
|
|
727
749
|
'catS.edit': '编辑',
|
|
750
|
+
'catS.undo': '撤销',
|
|
728
751
|
'catS.subagent': '子代理',
|
|
729
752
|
'catS.doomloop': '循环',
|
|
730
753
|
'mode.ask': '询问',
|
|
@@ -758,11 +781,10 @@ window.__ModuleLoader__.load({
|
|
|
758
781
|
'sandbox.desc.fa': '文件操作不受工作区限制',
|
|
759
782
|
'sandbox.desc.inherit': '跟随全局设置(当前全局:{g})',
|
|
760
783
|
'panel.quick': '快捷工具(其他工具快速设置)',
|
|
761
|
-
'panel.quickHint': '
|
|
784
|
+
'panel.quickHint': '无文件/命令语义的工具,按工具名设定默认动作;不在清单里的工具走兜底策略,改动即时生效。',
|
|
762
785
|
'panel.quickAdd': '新增工具名(如 todo_write,支持通配)',
|
|
763
|
-
'panel.quickAddBtn': '
|
|
786
|
+
'panel.quickAddBtn': '添加',
|
|
764
787
|
'panel.rules': '自定义规则(通用匹配)',
|
|
765
|
-
'panel.rulesHint': '优先级:自定义规则 > 分类例外 > 分类默认/快捷工具。',
|
|
766
788
|
'panel.actionDeny': 'deny 拒绝',
|
|
767
789
|
'panel.actionAsk': 'ask 审批',
|
|
768
790
|
'panel.actionAllow': 'allow 放行',
|
|
@@ -784,12 +806,39 @@ window.__ModuleLoader__.load({
|
|
|
784
806
|
'panel.savedToProject': '已保存到当前项目',
|
|
785
807
|
'panel.all': '(全部)',
|
|
786
808
|
'panel.sandboxSaved': '底层沙箱已保存({t}):{v}',
|
|
809
|
+
'panel.fallbackSaved': '兜底策略已保存({t}):{v}',
|
|
810
|
+
'panel.fallback': '兜底策略(未匹配任何规则)',
|
|
811
|
+
'panel.fallbackHint': '以上各分类之外、且不在快捷工具清单里的调用(如 mcp__*、cordis_run)如何处理。默认「询问」:每个未匹配的调用都会弹出审批。',
|
|
787
812
|
'panel.needValue': '请先输入例外匹配',
|
|
813
|
+
'panel.excRemaining': '该路径仍有 {n} 条同值例外,可继续逐条删除',
|
|
814
|
+
'panel.delFailed': '删除失败',
|
|
788
815
|
'panel.needTool': '请输入工具名',
|
|
789
816
|
'panel.excCount': '例外({n})',
|
|
790
817
|
'panel.excCmdHint': ':匹配命令子串/glob,优先于分类默认',
|
|
791
818
|
'panel.excPathHint': ':路径 glob,优先于分类默认',
|
|
792
819
|
'panel.excNone': '无例外',
|
|
820
|
+
'quick.web_search': '网页搜索',
|
|
821
|
+
'quick.skill': '加载技能',
|
|
822
|
+
'quick.grep': '内容检索',
|
|
823
|
+
'quick.glob': '按名查找文件',
|
|
824
|
+
'quick.web_fetch': '抓取网页内容',
|
|
825
|
+
'quick.ask_user_question': '向你提问',
|
|
826
|
+
'quick.todo_write': '任务清单',
|
|
827
|
+
'quick.list_agents': '列出子代理',
|
|
828
|
+
'quick.job_list': '后台任务列表',
|
|
829
|
+
'quick.job_output': '读取任务输出',
|
|
830
|
+
'quick.job_kill': '终止后台任务',
|
|
831
|
+
'quick.get_goal': '查看当前目标',
|
|
832
|
+
'quick.create_goal': '创建目标',
|
|
833
|
+
'quick.update_goal': '更新目标状态',
|
|
834
|
+
'quick.send_message': '给子代理发消息',
|
|
835
|
+
'quick.interrupt_agent': '打断子代理',
|
|
836
|
+
'quick.present': '声明交付文件',
|
|
837
|
+
'quick.exit_plan_mode': '退出计划模式',
|
|
838
|
+
'quick.cordis_define': '定义动态插件',
|
|
839
|
+
'quick.cordis_inspect_list': '检视能力清单',
|
|
840
|
+
'quick.cordis_inspect_query': '执行只读检视',
|
|
841
|
+
'quick.cordis_inspect_self': '检视本会话插件',
|
|
793
842
|
'panel.excExpand': '展开',
|
|
794
843
|
'panel.excCollapse': '折叠',
|
|
795
844
|
'panel.excListLink': '例外列表',
|
|
@@ -825,6 +874,10 @@ window.__ModuleLoader__.load({
|
|
|
825
874
|
'app.diffMore': '{n} more lines',
|
|
826
875
|
'app.diffLoading': 'Loading…',
|
|
827
876
|
'app.diffErr': 'Cannot build comparison',
|
|
877
|
+
'app.imageTag': 'Image',
|
|
878
|
+
'app.imageTooLarge': 'Image too large; thumbnail not generated',
|
|
879
|
+
'app.imageSizeUnknown': 'Image size could not be determined; thumbnail not generated',
|
|
880
|
+
'app.imageNoPreview': 'No thumbnail available',
|
|
828
881
|
'app.uiErr': 'UI render error',
|
|
829
882
|
'app.eolNote': 'Line endings normalized to LF for matching (CRLF/CR file with LF args): preview is indicative only; the actual edit may fail on line-ending mismatch',
|
|
830
883
|
'app.diffGapShow': 'Show {n} unchanged lines',
|
|
@@ -849,13 +902,17 @@ window.__ModuleLoader__.load({
|
|
|
849
902
|
'cat.directory': 'Directory access (outside workspace)',
|
|
850
903
|
'cat.command': 'Run command',
|
|
851
904
|
'cat.read': 'Read file',
|
|
905
|
+
'cat.image': 'Read image',
|
|
852
906
|
'cat.edit': 'Edit file',
|
|
907
|
+
'cat.undo': 'Undo edit (revert last edit)',
|
|
853
908
|
'cat.subagent': 'Spawn subagent',
|
|
854
909
|
'cat.doomloop': 'Doom Loop',
|
|
855
910
|
'catS.directory': 'Dir',
|
|
856
911
|
'catS.command': 'Cmd',
|
|
857
912
|
'catS.read': 'Read',
|
|
913
|
+
'catS.image': 'Image',
|
|
858
914
|
'catS.edit': 'Edit',
|
|
915
|
+
'catS.undo': 'Undo',
|
|
859
916
|
'catS.subagent': 'Sub',
|
|
860
917
|
'catS.doomloop': 'Loop',
|
|
861
918
|
'mode.ask': 'Ask',
|
|
@@ -889,11 +946,10 @@ window.__ModuleLoader__.load({
|
|
|
889
946
|
'sandbox.desc.fa': 'File operations are not workspace-restricted',
|
|
890
947
|
'sandbox.desc.inherit': 'Follows global (currently: {g})',
|
|
891
948
|
'panel.quick': 'Quick tools (other tools)',
|
|
892
|
-
'panel.quickHint': '
|
|
949
|
+
'panel.quickHint': 'Tools without file/command semantics: set a default per tool name; anything outside this list follows the fallback policy. Changes apply immediately.',
|
|
893
950
|
'panel.quickAdd': 'New tool name (e.g. todo_write, wildcards allowed)',
|
|
894
|
-
'panel.quickAddBtn': 'Add
|
|
951
|
+
'panel.quickAddBtn': 'Add',
|
|
895
952
|
'panel.rules': 'Custom rules (generic matching)',
|
|
896
|
-
'panel.rulesHint': 'Priority: custom rules > category exceptions > category default / quick tools.',
|
|
897
953
|
'panel.actionDeny': 'deny Deny',
|
|
898
954
|
'panel.actionAsk': 'ask Ask',
|
|
899
955
|
'panel.actionAllow': 'allow Allow',
|
|
@@ -912,12 +968,39 @@ window.__ModuleLoader__.load({
|
|
|
912
968
|
'panel.openConfigDone': 'Opened with default editor',
|
|
913
969
|
'panel.saved': 'Saved',
|
|
914
970
|
'panel.sandboxSaved': 'Sandbox saved ({t}): {v}',
|
|
971
|
+
'panel.fallbackSaved': 'Fallback policy saved ({t}): {v}',
|
|
972
|
+
'panel.fallback': 'Fallback policy (no rule matched)',
|
|
973
|
+
'panel.fallbackHint': 'How calls outside the categories above and not listed as quick tools (e.g. mcp__*, cordis_run) are handled. Default Ask: every unmatched call prompts for approval.',
|
|
915
974
|
'panel.needValue': 'Enter a match value first',
|
|
975
|
+
'panel.excRemaining': '{n} exception(s) with the same value remain on this path; delete them individually if not wanted',
|
|
976
|
+
'panel.delFailed': 'Delete failed',
|
|
916
977
|
'panel.needTool': 'Enter a tool name',
|
|
917
978
|
'panel.excCount': 'Exceptions ({n})',
|
|
918
979
|
'panel.excCmdHint': ': command substring/glob, overrides category default',
|
|
919
980
|
'panel.excPathHint': ': path glob, overrides category default',
|
|
920
981
|
'panel.excNone': 'No exceptions',
|
|
982
|
+
'quick.web_search': 'web search',
|
|
983
|
+
'quick.skill': 'load skill',
|
|
984
|
+
'quick.grep': 'content search',
|
|
985
|
+
'quick.glob': 'find files by name',
|
|
986
|
+
'quick.web_fetch': 'fetch page content',
|
|
987
|
+
'quick.ask_user_question': 'ask you a question',
|
|
988
|
+
'quick.todo_write': 'todo list',
|
|
989
|
+
'quick.list_agents': 'list subagents',
|
|
990
|
+
'quick.job_list': 'list background jobs',
|
|
991
|
+
'quick.job_output': 'read job output',
|
|
992
|
+
'quick.job_kill': 'kill a background job',
|
|
993
|
+
'quick.get_goal': 'view current goal',
|
|
994
|
+
'quick.create_goal': 'create a goal',
|
|
995
|
+
'quick.update_goal': 'update goal state',
|
|
996
|
+
'quick.send_message': 'message a subagent',
|
|
997
|
+
'quick.interrupt_agent': 'interrupt a subagent',
|
|
998
|
+
'quick.present': 'present deliverable files',
|
|
999
|
+
'quick.exit_plan_mode': 'exit plan mode',
|
|
1000
|
+
'quick.cordis_define': 'define a dynamic plugin',
|
|
1001
|
+
'quick.cordis_inspect_list': 'list inspect capabilities',
|
|
1002
|
+
'quick.cordis_inspect_query': 'run a read-only inspect query',
|
|
1003
|
+
'quick.cordis_inspect_self': 'inspect session plugins',
|
|
921
1004
|
'panel.excExpand': 'Expand',
|
|
922
1005
|
'panel.excCollapse': 'Collapse',
|
|
923
1006
|
'panel.excListLink': 'Exception list',
|
|
@@ -946,6 +1029,23 @@ window.__ModuleLoader__.load({
|
|
|
946
1029
|
function catLabel(c) { return T('cat.' + c); }
|
|
947
1030
|
function catShort(c) { return T('catS.' + c); }
|
|
948
1031
|
function modeLabel(m) { return T('mode.' + m); }
|
|
1032
|
+
// 快捷工具的用途说明:仅当 i18n 有对应文案时显示(用户自加的工具名不显示说明)
|
|
1033
|
+
function quickDesc(t) {
|
|
1034
|
+
const k = 'quick.' + t;
|
|
1035
|
+
const d = T(k);
|
|
1036
|
+
return d && d !== k ? d : '';
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// 只认「配置里自己写入的键」:不能用 o[k] !== undefined 判断,那会把 Object.prototype 的成员
|
|
1040
|
+
// (constructor/toString 等)当成「该层已配置」,与服务端 hasOwnProperty 的口径分叉。
|
|
1041
|
+
function hasOwnKey(o, k) { return !!(o && Object.prototype.hasOwnProperty.call(o, k)); }
|
|
1042
|
+
|
|
1043
|
+
// 未显式配置工具的全局列显示值:与服务端 quickAction 同一条链(全局键 → 预设默认 → 会话兜底)
|
|
1044
|
+
function quickGlobalValue(t, gq) {
|
|
1045
|
+
if (hasOwnKey(gq, t)) return gq[t];
|
|
1046
|
+
if (hasOwnKey(QUICK_DEFAULTS, t)) return QUICK_DEFAULTS[t];
|
|
1047
|
+
return QUICK_FALLBACK;
|
|
1048
|
+
}
|
|
949
1049
|
const FONT = 'system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif';
|
|
950
1050
|
const card = { border: '1px solid rgba(128,128,128,0.35)', borderRadius: 8, padding: 12, marginBottom: 12 };
|
|
951
1051
|
const rowStyle = { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 0', borderBottom: '1px solid rgba(128,128,128,0.15)', flexWrap: 'wrap' };
|
|
@@ -981,8 +1081,11 @@ window.__ModuleLoader__.load({
|
|
|
981
1081
|
for (const key of diffFetching) if (!aliveIds.has(key)) diffFetching.delete(key);
|
|
982
1082
|
for (const p of pending) {
|
|
983
1083
|
if (!p.hasDiff || diffCache.has(p.id) || diffFetching.has(p.id)) continue
|
|
984
|
-
const
|
|
1084
|
+
const userOpened = openDetail[p.id] === true
|
|
1085
|
+
const open = openDetail[p.id] === undefined ? true : userOpened
|
|
985
1086
|
if (!open) continue
|
|
1087
|
+
// 图片详情是整图 data URL(体积大):不做自动预取,只有用户主动展开该条时才拉取
|
|
1088
|
+
if (p.imagePreview && !userOpened) continue
|
|
986
1089
|
diffFetching.add(p.id)
|
|
987
1090
|
schedulePrismIdle()
|
|
988
1091
|
call('permgate:file-diff', { id: p.id }).then((r) => {
|
|
@@ -1007,24 +1110,36 @@ window.__ModuleLoader__.load({
|
|
|
1007
1110
|
else next[candId] = v;
|
|
1008
1111
|
setSel(next);
|
|
1009
1112
|
};
|
|
1113
|
+
const pickedRules = (p) => (p.candidates || []).filter((c) => sel[c.id]).map((c) => ({ id: c.id, value: c.value, kind: c.kind, decision: sel[c.id] }));
|
|
1114
|
+
// 拒绝语义下只接受 deny 方向的规则:候选行残留的「允许此项」勾选绝不能变成持久 allow
|
|
1115
|
+
const pickedDenyRules = (p) => pickedRules(p).filter((r) => r.decision === 'deny');
|
|
1010
1116
|
const submit = (p, action) => {
|
|
1011
|
-
|
|
1012
|
-
decide(p.id, action, rules);
|
|
1117
|
+
decide(p.id, action, pickedRules(p));
|
|
1013
1118
|
};
|
|
1014
1119
|
// 两段式拒绝:确认阶段提交(携带意见)或取消返回
|
|
1015
1120
|
const confirmDeny = (p) => {
|
|
1016
1121
|
const reason = (denyText[p.id] || '').trim();
|
|
1017
1122
|
setDenyMode(Object.assign({}, denyMode, { [p.id]: false }));
|
|
1018
|
-
decide(p.id, 'deny',
|
|
1123
|
+
decide(p.id, 'deny', pickedDenyRules(p), reason || undefined);
|
|
1019
1124
|
};
|
|
1020
1125
|
const cancelDeny = (p) => setDenyMode(Object.assign({}, denyMode, { [p.id]: false }));
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1126
|
+
// 进入拒绝态:只切状态,不动用户已勾选的内容。拒绝语义下「允许此项」由 radio 置灰,
|
|
1127
|
+
// 确认拒绝时也只提交 deny 方向(pickedDenyRules),因此点「取消」能原样回到进入前的勾选。
|
|
1128
|
+
const enterDeny = (p) => setDenyMode(Object.assign({}, denyMode, { [p.id]: true }));
|
|
1129
|
+
const radio = (p, c, v, label, cls) => {
|
|
1130
|
+
// 拒绝态下「允许此项」不生效(confirmDeny 只提交 deny 方向):置灰禁用而不是清除勾选,
|
|
1131
|
+
// 这样点「取消」保留进入前选择,点「确认拒绝」也不会把 allow 方向写进配置。
|
|
1132
|
+
const muted = v === 'allow' && !!denyMode[p.id];
|
|
1133
|
+
return React.createElement('button', {
|
|
1134
|
+
className: 'pg-radio' + (sel[c.id] === v ? ' ' + cls : ''),
|
|
1135
|
+
disabled: busyId === p.id || muted,
|
|
1136
|
+
style: muted ? { opacity: 0.45, cursor: 'not-allowed' } : undefined,
|
|
1137
|
+
onClick: () => pick(c.id, v),
|
|
1138
|
+
}, label);
|
|
1139
|
+
};
|
|
1026
1140
|
// 编辑/写入审批(有 diff):详情默认展开、参数默认收起;无 diff 时参数照常显示
|
|
1027
|
-
|
|
1141
|
+
// 图片详情是整图 data URL(体积大):默认收起,用户点开该条时才拉取
|
|
1142
|
+
const detailOpen = (p) => (openDetail[p.id] === undefined ? (!!p.hasDiff && !p.imagePreview) : openDetail[p.id]);
|
|
1028
1143
|
const argsOpen = (p) => (openArgs[p.id] === undefined ? !p.hasDiff : openArgs[p.id]);
|
|
1029
1144
|
const toggle = (map, setMap, p, v) => setMap(Object.assign({}, map, { [p.id]: v }));
|
|
1030
1145
|
// 点击文件名 → 打开右侧对比抽屉(父组件 OverlayRoot 持有 pin 状态)
|
|
@@ -1066,9 +1181,7 @@ window.__ModuleLoader__.load({
|
|
|
1066
1181
|
const c = diffCache.get(p.id)
|
|
1067
1182
|
if (!c) return React.createElement('div', { className: 'pg2-load' }, T('app.diffLoading'))
|
|
1068
1183
|
if (!c.ok) return React.createElement('div', { className: 'pg2-err' }, (c.error || T('app.diffErr')))
|
|
1069
|
-
return c
|
|
1070
|
-
? React.createElement(PGErrorBoundary, { resetKey: payloadKey(c), fallback: T('app.diffErr') }, React.createElement(ReadBlockMemo, { data: c, onOpenFile: (f) => openFile(p, f) }))
|
|
1071
|
-
: React.createElement(PGErrorBoundary, { resetKey: payloadKey(c), fallback: T('app.diffErr') }, React.createElement(DiffBlockMemo, { data: c, onOpenFile: (f) => openFile(p, f), changesOnly: true }))
|
|
1184
|
+
return detailBody(c, { onOpenFile: (f) => openFile(p, f), changesOnly: true, resetKey: payloadKey(c) })
|
|
1072
1185
|
})() : null,
|
|
1073
1186
|
) : null,
|
|
1074
1187
|
(p.candidates || []).length ? React.createElement('div', null,
|
|
@@ -1093,7 +1206,7 @@ window.__ModuleLoader__.load({
|
|
|
1093
1206
|
React.createElement('button', { className: 'pg-action', disabled: busyId === p.id, onClick: () => cancelDeny(p) }, T('panel.cancel')),
|
|
1094
1207
|
),
|
|
1095
1208
|
) : React.createElement('div', { className: 'pg-footer' },
|
|
1096
|
-
React.createElement('button', { className: 'pg-action pg-action-deny', disabled: busyId === p.id, onClick: () =>
|
|
1209
|
+
React.createElement('button', { className: 'pg-action pg-action-deny', disabled: busyId === p.id, onClick: () => enterDeny(p) }, T('app.deny')),
|
|
1097
1210
|
React.createElement('button', { className: 'pg-action pg-action-allow', disabled: busyId === p.id, onClick: () => submit(p, 'allow') }, T('app.allow')),
|
|
1098
1211
|
),
|
|
1099
1212
|
)),
|
|
@@ -1323,6 +1436,49 @@ window.__ModuleLoader__.load({
|
|
|
1323
1436
|
const diffPropsEqual = (a, b) => a.data === b.data && a.changesOnly === b.changesOnly;
|
|
1324
1437
|
const DiffBlockMemo = React.memo(DiffBlock, diffPropsEqual);
|
|
1325
1438
|
const ReadBlockMemo = React.memo(ReadBlock, diffPropsEqual);
|
|
1439
|
+
|
|
1440
|
+
// kind → 详情组件 分派单点:待审批内联详情与右侧对比抽屉共用,新增 kind(如 image)只改这一处,
|
|
1441
|
+
// 避免两处漏改导致某处把数据落到 DiffBlock,被错误边界兜成「无法生成对比」。
|
|
1442
|
+
function detailBody(data, opts) {
|
|
1443
|
+
const o = opts || {};
|
|
1444
|
+
const fb = T('app.diffErr');
|
|
1445
|
+
const boundaryProps = o.resetKey !== undefined ? { resetKey: o.resetKey, fallback: fb } : { fallback: fb };
|
|
1446
|
+
if (data.kind === 'image') return React.createElement(PGErrorBoundary, boundaryProps, React.createElement(ImageBlock, { data }));
|
|
1447
|
+
if (data.kind === 'read') return React.createElement(PGErrorBoundary, boundaryProps, React.createElement(ReadBlockMemo, { data, onOpenFile: o.onOpenFile || null, onCollapse: o.onCollapse }));
|
|
1448
|
+
return React.createElement(PGErrorBoundary, boundaryProps, React.createElement(DiffBlockMemo, { data, onOpenFile: o.onOpenFile || null, onCollapse: o.onCollapse, changesOnly: o.changesOnly }));
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// 图片详情块:头部显示 格式 · 像素尺寸 · 体积,下面给缩略图;
|
|
1452
|
+
// 超过上限或读取失败时不给图片本体,改显示说明文本(宿主下发的 error 由外层按 .pg2-err 渲染)。
|
|
1453
|
+
function ImageBlock({ data }) {
|
|
1454
|
+
const fmtBytes = (n) => {
|
|
1455
|
+
const v = Number(n) || 0;
|
|
1456
|
+
if (v < 1024) return v + ' B';
|
|
1457
|
+
if (v < 1024 * 1024) return (v / 1024).toFixed(1) + ' KB';
|
|
1458
|
+
return (v / (1024 * 1024)).toFixed(2) + ' MB';
|
|
1459
|
+
};
|
|
1460
|
+
const meta = [
|
|
1461
|
+
String(data.format || '').toUpperCase(),
|
|
1462
|
+
data.width && data.height ? data.width + '×' + data.height : '',
|
|
1463
|
+
data.size ? fmtBytes(data.size) : '',
|
|
1464
|
+
].filter(Boolean).join(' · ');
|
|
1465
|
+
const note = data.sizeUnknown
|
|
1466
|
+
? T('app.imageSizeUnknown')
|
|
1467
|
+
: data.tooLarge
|
|
1468
|
+
? T('app.imageTooLarge') + (data.limit ? '(' + fmtBytes(data.limit) + ')' : '')
|
|
1469
|
+
: T('app.imageNoPreview');
|
|
1470
|
+
return React.createElement('div', { className: 'pg2-block' },
|
|
1471
|
+
React.createElement('div', { className: 'pg2-header' },
|
|
1472
|
+
React.createElement('span', { className: 'pg2-status pg2-status-read' }, T('app.imageTag')),
|
|
1473
|
+
React.createElement('span', { className: 'pg2-path', title: data.file }, data.file),
|
|
1474
|
+
meta ? React.createElement('span', { style: { marginLeft: 'auto', color: 'rgba(128,128,128,0.9)' } }, meta) : null,
|
|
1475
|
+
),
|
|
1476
|
+
data.dataUrl
|
|
1477
|
+
? React.createElement('div', { style: { padding: 10, display: 'flex', justifyContent: 'center', alignItems: 'center', overflow: 'auto', background: 'rgba(128,128,128,0.06)' } },
|
|
1478
|
+
React.createElement('img', { src: data.dataUrl, alt: data.file, style: { maxWidth: '100%', maxHeight: 320, objectFit: 'contain', borderRadius: 4, display: 'block' } }))
|
|
1479
|
+
: React.createElement('div', { className: 'pg2-foot' }, note),
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1326
1482
|
function CompareDrawer({ pin, onClose }) {
|
|
1327
1483
|
const [data, setData] = React.useState(null);
|
|
1328
1484
|
const [err, setErr] = React.useState('');
|
|
@@ -1388,9 +1544,7 @@ window.__ModuleLoader__.load({
|
|
|
1388
1544
|
};
|
|
1389
1545
|
let body;
|
|
1390
1546
|
if (data) {
|
|
1391
|
-
body = data
|
|
1392
|
-
? React.createElement(PGErrorBoundary, { fallback: T('app.diffErr') }, React.createElement(ReadBlockMemo, { data, onOpenFile: null, onCollapse: onClose }))
|
|
1393
|
-
: React.createElement(PGErrorBoundary, { fallback: T('app.diffErr') }, React.createElement(DiffBlockMemo, { data, onOpenFile: null, onCollapse: onClose }));
|
|
1547
|
+
body = detailBody(data, { onCollapse: onClose })
|
|
1394
1548
|
} else if (err) {
|
|
1395
1549
|
body = React.createElement('div', { className: 'pg2-err' }, err);
|
|
1396
1550
|
} else {
|
|
@@ -1477,7 +1631,7 @@ window.__ModuleLoader__.load({
|
|
|
1477
1631
|
return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: 'rgba(128,128,128,0.95)', padding: '2px 0' } },
|
|
1478
1632
|
React.createElement('span', { style: { fontWeight: 600 } }, T('dock.title')),
|
|
1479
1633
|
React.createElement('span', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1480
|
-
chip(catShort('directory'), eff.directory), chip(catShort('command'), eff.command), chip(catShort('read'), eff.read), chip(catShort('edit'), eff.edit), chip(catShort('subagent'), eff.subagent), chip(catShort('doomloop'), eff.doomloop),
|
|
1634
|
+
chip(catShort('directory'), eff.directory), chip(catShort('command'), eff.command), chip(catShort('read'), eff.read), chip(catShort('image'), eff.image), chip(catShort('edit'), eff.edit), chip(catShort('undo'), eff.undo), chip(catShort('subagent'), eff.subagent), chip(catShort('doomloop'), eff.doomloop),
|
|
1481
1635
|
),
|
|
1482
1636
|
React.createElement('span', { style: { cursor: 'pointer', padding: '0 4px' }, onClick: refresh, title: T('dock.refresh') }, '↻'),
|
|
1483
1637
|
);
|
|
@@ -1498,6 +1652,7 @@ window.__ModuleLoader__.load({
|
|
|
1498
1652
|
const [exAction, setExAction] = React.useState('allow');
|
|
1499
1653
|
const [exReasonVal, setExReasonVal] = React.useState('');
|
|
1500
1654
|
const [newTool, setNewTool] = React.useState('');
|
|
1655
|
+
const [newToolAction, setNewToolAction] = React.useState('allow');
|
|
1501
1656
|
const [form, setForm] = React.useState({ action: 'deny', tool: '', path: '', args: '', reason: '' });
|
|
1502
1657
|
const [confirm, setConfirm] = React.useState(null);
|
|
1503
1658
|
const [excCollapsed, setExcCollapsed] = React.useState({});
|
|
@@ -1508,8 +1663,13 @@ window.__ModuleLoader__.load({
|
|
|
1508
1663
|
return () => clearTimeout(id);
|
|
1509
1664
|
}, [confirm]);
|
|
1510
1665
|
|
|
1666
|
+
// 切换全局/项目 tab 时清掉未完成的二次确认:确认的对象是「某一层的某个键」,换层后不能沿用
|
|
1667
|
+
// 同一确认态,否则再点会删掉另一层。
|
|
1668
|
+
React.useEffect(() => { setConfirm(null); }, [tab]);
|
|
1669
|
+
|
|
1511
1670
|
const applyStatus = (s) => {
|
|
1512
1671
|
if (!s) return;
|
|
1672
|
+
applyStatusLists(s);
|
|
1513
1673
|
setStatus(s);
|
|
1514
1674
|
const cs = { global: {}, project: {} };
|
|
1515
1675
|
for (const t of ['global', 'project']) {
|
|
@@ -1522,12 +1682,15 @@ window.__ModuleLoader__.load({
|
|
|
1522
1682
|
const qs = {};
|
|
1523
1683
|
const gq = s.quickTools ? s.quickTools.global : {};
|
|
1524
1684
|
const pq = s.quickTools ? s.quickTools.project : {};
|
|
1685
|
+
// 会话兜底只作最后一级(预设之外、且未配置的工具);宿主 statusView 恒下发合法的
|
|
1686
|
+
// fallback.effective,下面的 `|| 'ask'` 只是协议异常兜底,不是决策链的一环。
|
|
1687
|
+
QUICK_FALLBACK = (s.fallback && s.fallback.effective) || 'ask';
|
|
1525
1688
|
const names = {};
|
|
1526
1689
|
for (const t of QUICK_PRESET) names[t] = 1;
|
|
1527
1690
|
for (const k of Object.keys(gq)) names[k] = 1;
|
|
1528
1691
|
for (const k of Object.keys(pq)) names[k] = 1;
|
|
1529
1692
|
for (const t of Object.keys(names)) {
|
|
1530
|
-
qs[t] = { g:
|
|
1693
|
+
qs[t] = { g: quickGlobalValue(t, gq), p: hasOwnKey(pq, t) && pq[t] !== 'inherit' ? pq[t] : 'inherit' };
|
|
1531
1694
|
}
|
|
1532
1695
|
setQuickSel(qs);
|
|
1533
1696
|
};
|
|
@@ -1577,7 +1740,14 @@ window.__ModuleLoader__.load({
|
|
|
1577
1740
|
|
|
1578
1741
|
const addQuick = () => {
|
|
1579
1742
|
if (!newTool || !String(newTool).trim()) { setMsg(T('panel.needTool')); return; }
|
|
1580
|
-
invoke('permgate:set-quick', { target: tab, tool: String(newTool).trim(), action:
|
|
1743
|
+
invoke('permgate:set-quick', { target: tab, tool: String(newTool).trim(), action: newToolAction }, () => setNewTool(''));
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1746
|
+
// 删除该行在当前 tab 的快捷工具设置(服务端收到 action=inherit 即删除该键);
|
|
1747
|
+
// 只发请求、不做本地乐观删除:quickSel 完全由服务端返回的 status 收敛,请求失败时
|
|
1748
|
+
// 不会留下「面板已删、服务端仍生效」的漂移(与例外/规则的删除入口一致)。
|
|
1749
|
+
const removeQuick = (tool) => {
|
|
1750
|
+
invoke('permgate:set-quick', { target: tab, tool, action: 'inherit' });
|
|
1581
1751
|
};
|
|
1582
1752
|
|
|
1583
1753
|
const addException = (c) => {
|
|
@@ -1588,7 +1758,18 @@ window.__ModuleLoader__.load({
|
|
|
1588
1758
|
invoke('permgate:add-exception', { target: tab, category: c, match: v, action: exAction, reason: exAction === 'deny' ? (String(exReasonVal || '').trim() || undefined) : undefined });
|
|
1589
1759
|
};
|
|
1590
1760
|
|
|
1591
|
-
const removeException = (c, id) =>
|
|
1761
|
+
const removeException = (c, id) => {
|
|
1762
|
+
setBusy(true);
|
|
1763
|
+
setMsg('');
|
|
1764
|
+
// 注意:invoke 是无返回值的回调式封装,这里必须用返回 promise 的 call
|
|
1765
|
+
call('permgate:remove-exception', { target: tab, category: c, id }).then((r) => {
|
|
1766
|
+
if (r && r.error) { setMsg(String(r.error)); return; }
|
|
1767
|
+
applyStatus(r && r.status ? r.status : r);
|
|
1768
|
+
// 删除只作用于选中行:明确反馈结果;同路径若仍有例外则提示剩余条数,否则清掉旧提示
|
|
1769
|
+
if (r && r.removed === false) setMsg(r.reason ? String(r.reason) : T('panel.delFailed'));
|
|
1770
|
+
else setMsg(r && r.remaining > 0 ? T('panel.excRemaining').replace('{n}', String(r.remaining)) : '');
|
|
1771
|
+
}).catch((e) => setMsg(String((e && e.message) || e))).then(() => setBusy(false));
|
|
1772
|
+
};
|
|
1592
1773
|
|
|
1593
1774
|
const setFormKey = (key) => (e) => setForm(Object.assign({}, form, { [key]: e.target.value }));
|
|
1594
1775
|
const addRule = () => invoke('permgate:add-rule', { target: tab, action: form.action, tool: form.tool || undefined, path: form.path || undefined, args: form.args || undefined, reason: form.reason || undefined });
|
|
@@ -1651,13 +1832,31 @@ window.__ModuleLoader__.load({
|
|
|
1651
1832
|
);
|
|
1652
1833
|
};
|
|
1653
1834
|
|
|
1654
|
-
|
|
1835
|
+
// 行集合按 tab 区分:项目 tab 显示全集(预设 + 全局键 + 项目键),便于在项目里覆盖全局;
|
|
1836
|
+
// 全局 tab 只显示「预设 + 全局配置里确实存在的键」,避免项目层专有的键在这里冒充全局设置。
|
|
1837
|
+
const quickGq = status && status.quickTools ? status.quickTools.global : null;
|
|
1838
|
+
const quickTools = Object.keys(quickSel).filter((t) => {
|
|
1839
|
+
if (tab === 'project') return true;
|
|
1840
|
+
if (QUICK_PRESET.indexOf(t) !== -1) return true;
|
|
1841
|
+
return hasOwnKey(quickGq, t);
|
|
1842
|
+
});
|
|
1655
1843
|
const quickRow = (t) => {
|
|
1656
1844
|
const key = tab === 'global' ? 'g' : 'p';
|
|
1657
|
-
const
|
|
1845
|
+
const gqMap = status && status.quickTools ? status.quickTools.global : null;
|
|
1846
|
+
const pqMap = status && status.quickTools ? status.quickTools.project : null;
|
|
1847
|
+
const mode = quickSel[t] && quickSel[t][key] ? quickSel[t][key] : (tab === 'project' ? 'inherit' : quickGlobalValue(t, gqMap));
|
|
1848
|
+
// 删除按钮只给「用户自己添加的名字」(不在 22 个预设清单里的行),且本行在当前 tab 的配置里
|
|
1849
|
+
// 确实存在该键时才显示:预设行始终会列出,删掉配置键也只是回退默认值,容易被误读成「收紧」。
|
|
1850
|
+
const isPreset = QUICK_PRESET.indexOf(t) !== -1;
|
|
1851
|
+
const hasKey = key === 'g' ? hasOwnKey(gqMap, t) : hasOwnKey(pqMap, t);
|
|
1852
|
+
const desc = quickDesc(t);
|
|
1658
1853
|
return React.createElement('div', { key: t, style: rowStyle },
|
|
1659
|
-
React.createElement('span', { style: {
|
|
1854
|
+
React.createElement('span', { style: { display: 'flex', alignItems: 'baseline', gap: 6, minWidth: 260 } },
|
|
1855
|
+
React.createElement('span', { style: { fontFamily: 'monospace', fontSize: 13 } }, t),
|
|
1856
|
+
desc ? React.createElement('span', { style: { fontSize: 12, color: 'rgba(128,128,128,0.9)' } }, desc) : null,
|
|
1857
|
+
),
|
|
1660
1858
|
sel(mode, changeQuick(t), tab === 'project' ? ALL_MODES : MODES, busy),
|
|
1859
|
+
(!isPreset && hasKey) ? React.createElement('button', { className: 'pg-btn pg-btn-danger' + (confirm === 'quick:' + tab + ':' + t ? ' pg-btn-confirm' : ''), disabled: busy, onClick: () => confirmDelete('quick:' + tab + ':' + t, () => removeQuick(t)) }, confirm === 'quick:' + tab + ':' + t ? T('panel.confirmDel') : T('panel.del')) : null,
|
|
1661
1860
|
);
|
|
1662
1861
|
};
|
|
1663
1862
|
|
|
@@ -1687,6 +1886,11 @@ window.__ModuleLoader__.load({
|
|
|
1687
1886
|
|
|
1688
1887
|
const sandboxLabel = (m) => (m === 'workspace-write' ? T('sandbox.ww') : m === 'danger-full-access' ? T('sandbox.fa') : T('sandbox.inherit'));
|
|
1689
1888
|
const sandboxVal = tab === 'global' ? sandbox.global : sandbox.project;
|
|
1889
|
+
const fbVal = status && status.fallback ? (tab === 'global' ? status.fallback.global : status.fallback.project) : (tab === 'global' ? 'ask' : 'inherit');
|
|
1890
|
+
const changeFallback = (e) => {
|
|
1891
|
+
const mode = e.target.value;
|
|
1892
|
+
invoke('permgate:set-fallback', { target: tab, mode }, () => setMsg(T('panel.fallbackSaved').replace('{t}', T(tab === 'global' ? 'panel.tabGlobal' : 'panel.tabProject')).replace('{v}', modeLabel(mode))));
|
|
1893
|
+
};
|
|
1690
1894
|
const sandboxOptions = tab === 'global' ? ['workspace-write', 'danger-full-access'] : ['workspace-write', 'danger-full-access', 'inherit'];
|
|
1691
1895
|
const changeSandbox = (e) => {
|
|
1692
1896
|
const mode = e.target.value;
|
|
@@ -1745,12 +1949,12 @@ window.__ModuleLoader__.load({
|
|
|
1745
1949
|
quickTools.map(quickRow),
|
|
1746
1950
|
React.createElement('div', { style: { display: 'flex', gap: 6, marginTop: 6, flexWrap: 'wrap' } },
|
|
1747
1951
|
React.createElement('input', { className: 'pg-field', placeholder: T('panel.quickAdd'), value: newTool, onChange: (e) => setNewTool(e.target.value), disabled: busy }),
|
|
1952
|
+
sel(newToolAction, (e) => setNewToolAction(e.target.value), MODES, busy),
|
|
1748
1953
|
React.createElement('button', { className: 'pg-btn', disabled: busy, onClick: addQuick }, T('panel.quickAddBtn')),
|
|
1749
1954
|
),
|
|
1750
1955
|
),
|
|
1751
1956
|
React.createElement('div', { style: card },
|
|
1752
1957
|
React.createElement('div', { style: h() }, T('panel.rules')),
|
|
1753
|
-
React.createElement('div', { style: { fontSize: 12, color: 'rgba(128,128,128,0.8)', marginBottom: 8 } }, T('panel.rulesHint')),
|
|
1754
1958
|
React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center', marginBottom: 8 } },
|
|
1755
1959
|
React.createElement('select', { value: form.action, onChange: setFormKey('action'), className: 'pg-field', disabled: busy },
|
|
1756
1960
|
React.createElement('option', { value: 'deny' }, T('panel.actionDeny')), React.createElement('option', { value: 'ask' }, T('panel.actionAsk')), React.createElement('option', { value: 'allow' }, T('panel.actionAllow')),
|
|
@@ -1766,6 +1970,13 @@ window.__ModuleLoader__.load({
|
|
|
1766
1970
|
customList && customList.length ? customList.map(ruleRow) : React.createElement('div', { style: { fontSize: 12, color: 'rgba(128,128,128,0.8)' } }, T('panel.noRules')),
|
|
1767
1971
|
),
|
|
1768
1972
|
),
|
|
1973
|
+
React.createElement('div', { style: card },
|
|
1974
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 } },
|
|
1975
|
+
React.createElement('span', { style: { fontSize: 13, fontWeight: 600 } }, T('panel.fallback')),
|
|
1976
|
+
sel(fbVal, changeFallback, tab === 'global' ? MODES : ALL_MODES, busy),
|
|
1977
|
+
),
|
|
1978
|
+
React.createElement('div', { style: { fontSize: 12, color: 'rgba(128,128,128,0.85)' } }, T('panel.fallbackHint')),
|
|
1979
|
+
),
|
|
1769
1980
|
React.createElement('div', { style: card },
|
|
1770
1981
|
React.createElement('div', { style: h() }, T('panel.decisions')),
|
|
1771
1982
|
decisions.length ? decisions.map((d, i) => React.createElement('div', { key: i, style: rowStyle },
|