@restregular/dsh-filekit 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/client.js +419 -47
- package/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,10 +40,17 @@ dsh plugin --profile web add @restregular/dsh-filekit
|
|
|
40
40
|
### FileKit 文件管理页
|
|
41
41
|
|
|
42
42
|
- 独立侧边栏标签页(`filekit` kind),不是浮层弹窗;
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
- 面包屑浏览、新建、复制/剪切/粘贴、移动、删除、回收站;
|
|
44
|
+
- 右键菜单与拖拽移动。
|
|
45
|
+
|
|
46
|
+
### 编辑器标签页
|
|
47
|
+
|
|
48
|
+
- 每个文件一个独立标签页(`filekit-edit` kind),和预览页可以互相切换:
|
|
49
|
+
- 预览标题栏的**编辑**按钮把当前预览标签页切换成编辑页;
|
|
50
|
+
- 编辑页的**保存**按钮保存后切回该文件的预览页;**放弃修改**确认后也切回预览。
|
|
51
|
+
- 与内置预览同一套 Shiki 高亮器、行内滚动同步、`Ctrl/Cmd+S` 保存;
|
|
52
|
+
- 未保存时标签页标题右侧显示橙色圆点;关闭未保存的标签页会被阻止并提示;
|
|
53
|
+
- 工具栏全部为图标按钮,悬停显示提示文本(复制相对路径、引用、预览、放弃修改、保存)。
|
|
47
54
|
|
|
48
55
|
### 设置页
|
|
49
56
|
|
package/client.js
CHANGED
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
const FILEKIT_ID = '@restregular/dsh-filekit';
|
|
33
33
|
const FILES_KIND = 'files';
|
|
34
34
|
const FILEKIT_KIND = 'filekit';
|
|
35
|
+
const EDITOR_KIND = 'filekit-edit';
|
|
36
|
+
/** Identity of the per-file editor tab type and its body cell. */
|
|
37
|
+
const EDITOR_ID = '@restregular/dsh-filekit/editor';
|
|
38
|
+
/** Resource scheme for editor tabs; the address carries the file, so it survives restarts. */
|
|
39
|
+
const EDITOR_PREFIX = 'dsh-resource://filekit-edit/session/';
|
|
35
40
|
|
|
36
41
|
const factories = window.__ModuleLoader__?.factories;
|
|
37
42
|
if (factories !== undefined && typeof factories.has === 'function' && factories.has(MODULE_ID)) return;
|
|
@@ -126,6 +131,9 @@
|
|
|
126
131
|
emptyHint: '回收站里的所有条目都会被永久删除,无法恢复。',
|
|
127
132
|
discardTitle: '放弃未保存的修改?',
|
|
128
133
|
discardConfirm: '放弃修改',
|
|
134
|
+
discardHint: '放弃后该标签页会切换回文件预览。',
|
|
135
|
+
editorTab: '编辑',
|
|
136
|
+
editorNoFile: '这个标签页没有要编辑的文件。',
|
|
129
137
|
dirty: '未保存',
|
|
130
138
|
discard: '放弃修改',
|
|
131
139
|
binary: '这是二进制文件,不能用文本编辑器打开。',
|
|
@@ -269,6 +277,9 @@
|
|
|
269
277
|
emptyHint: 'Every entry in the recycle bin is permanently deleted; this cannot be undone.',
|
|
270
278
|
discardTitle: 'Discard unsaved changes?',
|
|
271
279
|
discardConfirm: 'Discard',
|
|
280
|
+
discardHint: 'Discarding switches this tab back to the file preview.',
|
|
281
|
+
editorTab: 'Edit',
|
|
282
|
+
editorNoFile: 'This tab has no file to edit.',
|
|
272
283
|
dirty: 'Unsaved',
|
|
273
284
|
discard: 'Discard',
|
|
274
285
|
binary: 'This is a binary file; the text editor cannot open it.',
|
|
@@ -543,6 +554,26 @@
|
|
|
543
554
|
return 'dsh-resource://file/session/' + id + '/' + rel.split('/').map((part) => encodeURIComponent(part)).join('/');
|
|
544
555
|
}
|
|
545
556
|
|
|
557
|
+
/** Address of a FileKit editor tab; unique per session+file, and persisted as its contentId. */
|
|
558
|
+
function editorAddress(sessionId, rel) {
|
|
559
|
+
return EDITOR_PREFIX + encodeURIComponent(String(sessionId)) + '/' + rel.split('/').map((part) => encodeURIComponent(part)).join('/');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Read the file out of an editor tab address; `undefined` for any other resource. */
|
|
563
|
+
function parseEditorAddress(address) {
|
|
564
|
+
if (typeof address !== 'string' || !address.startsWith(EDITOR_PREFIX)) return undefined;
|
|
565
|
+
const rest = address.slice(EDITOR_PREFIX.length);
|
|
566
|
+
const slash = rest.indexOf('/');
|
|
567
|
+
if (slash <= 0 || slash === rest.length - 1) return undefined;
|
|
568
|
+
try {
|
|
569
|
+
const sessionId = decodeURIComponent(rest.slice(0, slash));
|
|
570
|
+
const rel = rest.slice(slash + 1).split('/').map((part) => decodeURIComponent(part)).join('/');
|
|
571
|
+
return { sessionId, rel };
|
|
572
|
+
} catch {
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
546
577
|
function formatBytes(size) {
|
|
547
578
|
if (typeof size !== 'number' || !Number.isFinite(size)) return '';
|
|
548
579
|
if (size < 1024) return `${size} B`;
|
|
@@ -766,6 +797,264 @@
|
|
|
766
797
|
);
|
|
767
798
|
}
|
|
768
799
|
|
|
800
|
+
/**
|
|
801
|
+
* One per-file editor tab. Saving (or discarding) swaps this tab for the
|
|
802
|
+
* file's preview tab, so the pair behave as one page with two modes.
|
|
803
|
+
*/
|
|
804
|
+
function FileEditorTab(props) {
|
|
805
|
+
const { useTabInfo, sessionId, inputActions } = props;
|
|
806
|
+
const { tab } = useTabInfo();
|
|
807
|
+
const settingsSnapshot = useSettings();
|
|
808
|
+
const params = tab.navigation?.params;
|
|
809
|
+
// Navigation params carry the file on first open; a restored layout only
|
|
810
|
+
// has the tab's contentId, which is the editor address.
|
|
811
|
+
const rel =
|
|
812
|
+
typeof params?.rel === 'string' && params.rel !== '' ? params.rel : parseEditorAddress(tab.contentId)?.rel ?? '';
|
|
813
|
+
const [root, setRoot] = React.useState('');
|
|
814
|
+
const [error, setError] = React.useState('');
|
|
815
|
+
const [editor, setEditor] = React.useState(null);
|
|
816
|
+
const [draft, setDraft] = React.useState('');
|
|
817
|
+
const [saving, setSaving] = React.useState(false);
|
|
818
|
+
const [discardOpen, setDiscardOpen] = React.useState(false);
|
|
819
|
+
const draftRef = React.useRef('');
|
|
820
|
+
const savingRef = React.useRef(false);
|
|
821
|
+
const saveRef = React.useRef(null);
|
|
822
|
+
draftRef.current = draft;
|
|
823
|
+
const dirty = editor !== null && draft !== editor.original;
|
|
824
|
+
|
|
825
|
+
// The tab chip shows the unsaved marker through this flag.
|
|
826
|
+
React.useEffect(() => {
|
|
827
|
+
setTabDirty(tab.id, dirty);
|
|
828
|
+
return () => setTabDirty(tab.id, false);
|
|
829
|
+
}, [tab.id, dirty]);
|
|
830
|
+
|
|
831
|
+
React.useEffect(() => {
|
|
832
|
+
if (typeof sessionId !== 'string' || sessionId === '') return undefined;
|
|
833
|
+
let dead = false;
|
|
834
|
+
api('workspace', { sessionId })
|
|
835
|
+
.then((data) => {
|
|
836
|
+
if (!dead) setRoot(data.root);
|
|
837
|
+
})
|
|
838
|
+
.catch((failure) => {
|
|
839
|
+
if (!dead) setError(errorText(failure));
|
|
840
|
+
});
|
|
841
|
+
return () => {
|
|
842
|
+
dead = true;
|
|
843
|
+
};
|
|
844
|
+
}, [sessionId]);
|
|
845
|
+
|
|
846
|
+
React.useEffect(() => {
|
|
847
|
+
if (root === '' || rel === '') return undefined;
|
|
848
|
+
let dead = false;
|
|
849
|
+
setEditor(null);
|
|
850
|
+
setDraft('');
|
|
851
|
+
setError('');
|
|
852
|
+
api('read', { root, path: rel, sessionId })
|
|
853
|
+
.then((data) => {
|
|
854
|
+
if (dead) return;
|
|
855
|
+
if (data.binary === true) {
|
|
856
|
+
setError(t('binary'));
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
setEditor({
|
|
860
|
+
rel,
|
|
861
|
+
original: data.text,
|
|
862
|
+
mtimeMs: data.mtimeMs,
|
|
863
|
+
size: data.size,
|
|
864
|
+
truncated: data.truncated === true,
|
|
865
|
+
});
|
|
866
|
+
setDraft(data.text);
|
|
867
|
+
})
|
|
868
|
+
.catch((failure) => {
|
|
869
|
+
if (!dead) setError(errorText(failure));
|
|
870
|
+
});
|
|
871
|
+
return () => {
|
|
872
|
+
dead = true;
|
|
873
|
+
};
|
|
874
|
+
}, [root, rel, sessionId]);
|
|
875
|
+
|
|
876
|
+
const switchToPreview = () => {
|
|
877
|
+
setTabDirty(tab.id, false);
|
|
878
|
+
try {
|
|
879
|
+
tab.actions.openResource(fileAddress(sessionId, rel), { replaceTab: true });
|
|
880
|
+
} catch (failure) {
|
|
881
|
+
showToast(errorText(failure), 'error');
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
const save = async () => {
|
|
886
|
+
const current = editor;
|
|
887
|
+
const text = draftRef.current;
|
|
888
|
+
if (current === null || savingRef.current || current.truncated || text === current.original) return;
|
|
889
|
+
savingRef.current = true;
|
|
890
|
+
setSaving(true);
|
|
891
|
+
try {
|
|
892
|
+
await api('write', { root, path: rel, content: text, expectedMtimeMs: current.mtimeMs, sessionId });
|
|
893
|
+
showToast(t('saved'));
|
|
894
|
+
switchToPreview();
|
|
895
|
+
} catch (failure) {
|
|
896
|
+
setError(errorText(failure));
|
|
897
|
+
showToast(errorText(failure), 'error');
|
|
898
|
+
} finally {
|
|
899
|
+
savingRef.current = false;
|
|
900
|
+
setSaving(false);
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
saveRef.current = save;
|
|
904
|
+
|
|
905
|
+
const discard = () => {
|
|
906
|
+
if (!dirty) {
|
|
907
|
+
switchToPreview();
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
setDiscardOpen(true);
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
React.useEffect(() => {
|
|
914
|
+
const onKeyDown = (event) => {
|
|
915
|
+
if (tab.visible !== true) return;
|
|
916
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's' && editor !== null) {
|
|
917
|
+
event.preventDefault();
|
|
918
|
+
saveRef.current?.();
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
window.addEventListener('keydown', onKeyDown);
|
|
922
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
923
|
+
}, [editor, tab.visible]);
|
|
924
|
+
|
|
925
|
+
const copyRelPath = async () => {
|
|
926
|
+
const ok = await copyText(rel);
|
|
927
|
+
showToast(ok ? t('copiedPath') : t('error.internal'), ok ? 'ok' : 'error');
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const referenceRel = async () => {
|
|
931
|
+
const text = `@${rel} `;
|
|
932
|
+
if (inputActions !== undefined && typeof inputActions.captureInsertion === 'function' && typeof inputActions.insertText === 'function') {
|
|
933
|
+
try {
|
|
934
|
+
const span = inputActions.captureInsertion();
|
|
935
|
+
if (inputActions.insertText(text, span) === true) {
|
|
936
|
+
inputActions.persistDraft?.();
|
|
937
|
+
showToast(t('referenced'));
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
} catch {
|
|
941
|
+
/* fall back to the clipboard */
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const ok = await copyText(text);
|
|
945
|
+
showToast(ok ? t('referencedCopied') : t('error.internal'), ok ? 'ok' : 'error');
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
if (params === undefined) {
|
|
949
|
+
return h('div', { className: 'dsh-fk-tabstatus' }, h('p', { className: 'dsh-fk-hint' }, t('loading')));
|
|
950
|
+
}
|
|
951
|
+
if (rel === '') {
|
|
952
|
+
return h('div', { className: 'dsh-fk-tabstatus' }, h('p', { className: 'dsh-fk-error' }, t('editorNoFile')));
|
|
953
|
+
}
|
|
954
|
+
if (error !== '' && editor === null) {
|
|
955
|
+
return h(
|
|
956
|
+
'div',
|
|
957
|
+
{ className: 'dsh-fk-tabstatus' },
|
|
958
|
+
h('p', { className: 'dsh-fk-error' }, error),
|
|
959
|
+
h('button', { type: 'button', className: 'dsh-fk-btn', onClick: switchToPreview }, t('openPreview')),
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
if (editor === null) {
|
|
963
|
+
return h('div', { className: 'dsh-fk-tabstatus' }, h('p', { className: 'dsh-fk-hint' }, t('loading')));
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return h(
|
|
967
|
+
'div',
|
|
968
|
+
{ className: 'dsh-fk-tab' },
|
|
969
|
+
h(
|
|
970
|
+
'div',
|
|
971
|
+
{ className: 'dsh-fk-editorhead' },
|
|
972
|
+
h(
|
|
973
|
+
'div',
|
|
974
|
+
{ className: 'dsh-fk-editorpath', title: joinAbs(root, rel) },
|
|
975
|
+
h(Icon, { name: 'file', size: 15 }),
|
|
976
|
+
h('span', { className: 'dsh-fk-editorpathname' }, baseOf(rel)),
|
|
977
|
+
h('span', { className: 'dsh-fk-editorpathdir' }, dirOf(rel)),
|
|
978
|
+
),
|
|
979
|
+
h('span', { className: 'dsh-fk-spacer' }),
|
|
980
|
+
dirty ? h('span', { className: 'dsh-fk-badge' }, t('dirty')) : null,
|
|
981
|
+
h(
|
|
982
|
+
'span',
|
|
983
|
+
{ className: 'dsh-fk-iconactions' },
|
|
984
|
+
h(IconButton, { icon: 'copy', label: t('copyRel'), onClick: copyRelPath }),
|
|
985
|
+
h(IconButton, { icon: 'link', label: t('reference'), onClick: referenceRel }),
|
|
986
|
+
h(IconButton, { icon: 'file', label: t('openPreview'), onClick: switchToPreview, disabled: saving }),
|
|
987
|
+
h(IconButton, {
|
|
988
|
+
icon: 'close',
|
|
989
|
+
label: t('discard'),
|
|
990
|
+
onClick: discard,
|
|
991
|
+
disabled: saving || !dirty,
|
|
992
|
+
}),
|
|
993
|
+
h(IconButton, {
|
|
994
|
+
icon: 'check',
|
|
995
|
+
label: saving ? t('saving') : t('save'),
|
|
996
|
+
onClick: save,
|
|
997
|
+
disabled: saving || !dirty || editor.truncated,
|
|
998
|
+
}),
|
|
999
|
+
),
|
|
1000
|
+
),
|
|
1001
|
+
h(
|
|
1002
|
+
'div',
|
|
1003
|
+
{ className: 'dsh-fk-editor' },
|
|
1004
|
+
editor.truncated ? h('p', { className: 'dsh-fk-warn' }, h(Icon, { name: 'warning', size: 14 }), t('truncated')) : null,
|
|
1005
|
+
h(CodeEditor, {
|
|
1006
|
+
value: draft,
|
|
1007
|
+
onChange: setDraft,
|
|
1008
|
+
rel,
|
|
1009
|
+
fontSize: settingsSnapshot.editorFontSize,
|
|
1010
|
+
readOnly: editor.truncated,
|
|
1011
|
+
}),
|
|
1012
|
+
h(
|
|
1013
|
+
'div',
|
|
1014
|
+
{ className: 'dsh-fk-editor-meta' },
|
|
1015
|
+
h('span', null, `${formatBytes(editor.size)}${dirty ? ` → ${formatBytes(new TextEncoder().encode(draft).length)}` : ''}`),
|
|
1016
|
+
h('span', { className: 'dsh-fk-spacer' }),
|
|
1017
|
+
h('span', null, t('modified')),
|
|
1018
|
+
h('span', null, new Date(editor.mtimeMs).toLocaleString()),
|
|
1019
|
+
),
|
|
1020
|
+
),
|
|
1021
|
+
error !== '' ? h('div', { className: 'dsh-fk-status', 'data-kind': 'error' }, error) : null,
|
|
1022
|
+
discardOpen
|
|
1023
|
+
? h(ConfirmDialog, {
|
|
1024
|
+
key: 'discard',
|
|
1025
|
+
title: t('discardTitle'),
|
|
1026
|
+
text: rel,
|
|
1027
|
+
hint: t('discardHint'),
|
|
1028
|
+
danger: true,
|
|
1029
|
+
confirmText: t('discardConfirm'),
|
|
1030
|
+
onClose: () => setDiscardOpen(false),
|
|
1031
|
+
onConfirm: () => {
|
|
1032
|
+
setTabDirty(tab.id, false);
|
|
1033
|
+
switchToPreview();
|
|
1034
|
+
return true;
|
|
1035
|
+
},
|
|
1036
|
+
})
|
|
1037
|
+
: null,
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function EditorTitle({ useTabInfo }) {
|
|
1042
|
+
const { tab } = useTabInfo();
|
|
1043
|
+
const dirty = useTabDirty(tab.id);
|
|
1044
|
+
const rel =
|
|
1045
|
+
typeof tab.navigation?.params?.rel === 'string' && tab.navigation.params.rel !== ''
|
|
1046
|
+
? tab.navigation.params.rel
|
|
1047
|
+
: parseEditorAddress(tab.contentId)?.rel ?? '';
|
|
1048
|
+
const name = rel === '' ? t('editorTab') : baseOf(rel);
|
|
1049
|
+
return h(
|
|
1050
|
+
'span',
|
|
1051
|
+
{ className: 'dsh-fk-tabtitle', title: rel === '' ? name : rel },
|
|
1052
|
+
h(Icon, { name: 'file', size: 15 }),
|
|
1053
|
+
h('span', { className: 'dsh-fk-tabtitle-name' }, name),
|
|
1054
|
+
dirty ? h('span', { className: 'dsh-fk-tabdot', title: t('dirty') }) : null,
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
769
1058
|
function createOwnedStyle(css) {
|
|
770
1059
|
const el = document.createElement('style');
|
|
771
1060
|
el.setAttribute('data-plugin', OWNER);
|
|
@@ -775,15 +1064,69 @@
|
|
|
775
1064
|
}
|
|
776
1065
|
|
|
777
1066
|
// ------------------------------------------------------------------
|
|
778
|
-
// Dirty tabs (the
|
|
1067
|
+
// Dirty tabs (close guard + the tab title's unsaved dot)
|
|
779
1068
|
// ------------------------------------------------------------------
|
|
780
1069
|
|
|
781
1070
|
const dirtyTabs = new Set();
|
|
1071
|
+
const dirtyListeners = new Set();
|
|
782
1072
|
|
|
783
1073
|
function setTabDirty(tabId, dirty) {
|
|
784
1074
|
if (typeof tabId !== 'string' || tabId === '') return;
|
|
1075
|
+
const before = dirtyTabs.has(tabId);
|
|
785
1076
|
if (dirty) dirtyTabs.add(tabId);
|
|
786
1077
|
else dirtyTabs.delete(tabId);
|
|
1078
|
+
if (before === dirtyTabs.has(tabId)) return;
|
|
1079
|
+
for (const listener of dirtyListeners) {
|
|
1080
|
+
try {
|
|
1081
|
+
listener();
|
|
1082
|
+
} catch {
|
|
1083
|
+
/* ignore */
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/** Live dirty flag for one tab, used by its title chip. */
|
|
1089
|
+
function useTabDirty(tabId) {
|
|
1090
|
+
const [dirty, setDirty] = React.useState(() => dirtyTabs.has(tabId));
|
|
1091
|
+
React.useEffect(() => {
|
|
1092
|
+
const listener = () => setDirty(dirtyTabs.has(tabId));
|
|
1093
|
+
dirtyListeners.add(listener);
|
|
1094
|
+
listener();
|
|
1095
|
+
return () => dirtyListeners.delete(listener);
|
|
1096
|
+
}, [tabId]);
|
|
1097
|
+
return dirty;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// ------------------------------------------------------------------
|
|
1101
|
+
// Per-file editor tabs
|
|
1102
|
+
// ------------------------------------------------------------------
|
|
1103
|
+
|
|
1104
|
+
/** Find the open tab whose content is one resource address (a preview tab). */
|
|
1105
|
+
function tabIdForContent(sessionId, contentId) {
|
|
1106
|
+
const open = sidebarRightService?.tabsIn?.(sessionId) ?? [];
|
|
1107
|
+
return open.find((item) => item.contentId === contentId)?.id;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Open one file's editor tab. The address carries session+file, so the
|
|
1112
|
+
* same file reuses its tab and a restored layout can still resolve it.
|
|
1113
|
+
* An optional `replaceTabId` swaps an existing tab — the preview page's
|
|
1114
|
+
* edit button passes its own tab.
|
|
1115
|
+
*/
|
|
1116
|
+
function openEditorTab(sessionId, rel, replaceTabId) {
|
|
1117
|
+
if (typeof rel !== 'string' || rel === '') return;
|
|
1118
|
+
if (sidebarRightService === undefined || typeof sidebarRightService.openResource !== 'function') {
|
|
1119
|
+
showToast(t('error.internal'), 'error');
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
try {
|
|
1123
|
+
sidebarRightService.openResource(editorAddress(sessionId, rel), {
|
|
1124
|
+
params: { rel },
|
|
1125
|
+
...(typeof replaceTabId === 'string' ? { replaceTab: replaceTabId } : {}),
|
|
1126
|
+
});
|
|
1127
|
+
} catch (failure) {
|
|
1128
|
+
showToast(errorText(failure), 'error');
|
|
1129
|
+
}
|
|
787
1130
|
}
|
|
788
1131
|
|
|
789
1132
|
// ------------------------------------------------------------------
|
|
@@ -932,6 +1275,14 @@
|
|
|
932
1275
|
h('rect', { x: 3, y: 4.5, width: 18, height: 15, rx: 2.2 }),
|
|
933
1276
|
h('path', { d: 'M14.5 4.5v15' }),
|
|
934
1277
|
);
|
|
1278
|
+
case 'filesPanel':
|
|
1279
|
+
return h(
|
|
1280
|
+
'svg',
|
|
1281
|
+
common,
|
|
1282
|
+
h('rect', { x: 3, y: 4.5, width: 18, height: 15, rx: 2.2 }),
|
|
1283
|
+
h('path', { d: 'M14.5 4.5v15' }),
|
|
1284
|
+
h('path', { d: 'M6 9h5M6 12h5M6 15h5' }),
|
|
1285
|
+
);
|
|
935
1286
|
case 'spark':
|
|
936
1287
|
return h('svg', common, h('path', { d: 'M12 3.5l1.9 5.1 5.1 1.9-5.1 1.9L12 17.5l-1.9-5.1L5 10.5l5.1-1.9z' }));
|
|
937
1288
|
default:
|
|
@@ -1369,30 +1720,12 @@
|
|
|
1369
1720
|
}, [notice]);
|
|
1370
1721
|
|
|
1371
1722
|
const loadEditor = React.useCallback(
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
if (data.binary === true) {
|
|
1377
|
-
setNotice({ kind: 'error', text: t('binary') });
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
setEditor({
|
|
1381
|
-
rel,
|
|
1382
|
-
original: data.text,
|
|
1383
|
-
mtimeMs: data.mtimeMs,
|
|
1384
|
-
size: data.size,
|
|
1385
|
-
truncated: data.truncated === true,
|
|
1386
|
-
});
|
|
1387
|
-
setDraft(data.text);
|
|
1388
|
-
setMenu(null);
|
|
1389
|
-
setDialog(null);
|
|
1390
|
-
setNotice(null);
|
|
1391
|
-
} catch (failure) {
|
|
1392
|
-
setNotice({ kind: 'error', text: errorText(failure) });
|
|
1393
|
-
}
|
|
1723
|
+
(rel) => {
|
|
1724
|
+
setMenu(null);
|
|
1725
|
+
setDialog(null);
|
|
1726
|
+
openEditorTab(sessionId, rel);
|
|
1394
1727
|
},
|
|
1395
|
-
[
|
|
1728
|
+
[sessionId],
|
|
1396
1729
|
);
|
|
1397
1730
|
|
|
1398
1731
|
const refresh = React.useCallback(() => setReload((value) => value + 1), []);
|
|
@@ -1906,7 +2239,6 @@
|
|
|
1906
2239
|
key: entry.path,
|
|
1907
2240
|
className: 'dsh-fk-row',
|
|
1908
2241
|
'data-kind': entry.kind,
|
|
1909
|
-
'data-selected': selected === entry.path,
|
|
1910
2242
|
'data-menu': menu !== null && menu.rel === entry.path,
|
|
1911
2243
|
'data-drop': dropDir === entry.path,
|
|
1912
2244
|
tabIndex: 0,
|
|
@@ -1941,7 +2273,6 @@
|
|
|
1941
2273
|
}
|
|
1942
2274
|
: undefined,
|
|
1943
2275
|
onClick: () => {
|
|
1944
|
-
setSelected(entry.path);
|
|
1945
2276
|
if (entry.kind === 'directory') {
|
|
1946
2277
|
setCwd(entry.path);
|
|
1947
2278
|
setMenu(null);
|
|
@@ -2127,7 +2458,7 @@
|
|
|
2127
2458
|
),
|
|
2128
2459
|
h('span', { className: 'dsh-fk-spacer' }),
|
|
2129
2460
|
h(IconButton, {
|
|
2130
|
-
icon: '
|
|
2461
|
+
icon: 'filesPanel',
|
|
2131
2462
|
label: t('openFiles'),
|
|
2132
2463
|
onClick: () => {
|
|
2133
2464
|
try {
|
|
@@ -2464,7 +2795,7 @@
|
|
|
2464
2795
|
const isDir = menu.kind === 'directory';
|
|
2465
2796
|
if (isDir) menuItems.push({ label: t('openDirInFileKit'), icon: 'brand', onSelect: () => openFileKit({ dir: menu.rel }) });
|
|
2466
2797
|
else menuItems.push({ label: t('openPreview'), icon: 'file', onSelect: () => openPreview(menu.rel) });
|
|
2467
|
-
if (!isDir) menuItems.push({ label: t('openEditor'), icon: 'edit', onSelect: () =>
|
|
2798
|
+
if (!isDir) menuItems.push({ label: t('openEditor'), icon: 'edit', onSelect: () => openEditorTab(sessionId, menu.rel) });
|
|
2468
2799
|
menuItems.push({ separator: true });
|
|
2469
2800
|
if (isDir) {
|
|
2470
2801
|
menuItems.push({ label: t('newFileHere'), icon: 'filePlus', onSelect: () => setDialog({ kind: 'create-file', dir: menu.rel }) });
|
|
@@ -2571,7 +2902,6 @@
|
|
|
2571
2902
|
key: entry.path,
|
|
2572
2903
|
className: 'dsh-fk-trow',
|
|
2573
2904
|
'data-kind': entry.kind,
|
|
2574
|
-
'data-selected': selected === entry.path,
|
|
2575
2905
|
'data-menu': menu !== null && menu.rel === entry.path,
|
|
2576
2906
|
'data-drop': dropDir === entry.path,
|
|
2577
2907
|
role: 'treeitem',
|
|
@@ -2582,7 +2912,6 @@
|
|
|
2582
2912
|
draggable: true,
|
|
2583
2913
|
title: entry.path,
|
|
2584
2914
|
onClick: () => {
|
|
2585
|
-
setSelected(entry.path);
|
|
2586
2915
|
if (isDir) toggleDir(entry.path);
|
|
2587
2916
|
else openPreview(entry.path);
|
|
2588
2917
|
},
|
|
@@ -2856,6 +3185,14 @@
|
|
|
2856
3185
|
const [menu, setMenu] = React.useState(null);
|
|
2857
3186
|
if (abs === '') return null;
|
|
2858
3187
|
|
|
3188
|
+
/** Swap this preview tab for the file's editor tab. */
|
|
3189
|
+
const startEditing = async (target, session) => {
|
|
3190
|
+
const workspace = await api('workspace', { sessionId: session, file: target });
|
|
3191
|
+
const rel = relFromAbs(target, workspace.root);
|
|
3192
|
+
if (rel === null) throw Object.assign(new Error('outside'), { code: 'outside-workspace' });
|
|
3193
|
+
openEditorTab(session, rel, tabIdForContent(session, fileAddress(session, rel)));
|
|
3194
|
+
};
|
|
3195
|
+
|
|
2859
3196
|
const run = async (label, fn) => {
|
|
2860
3197
|
try {
|
|
2861
3198
|
await fn();
|
|
@@ -2870,7 +3207,7 @@
|
|
|
2870
3207
|
};
|
|
2871
3208
|
|
|
2872
3209
|
const items = [
|
|
2873
|
-
{ label: t('openEditor'), icon: 'edit', onSelect: () => run(t('openEditor'), () =>
|
|
3210
|
+
{ label: t('openEditor'), icon: 'edit', onSelect: () => run(t('openEditor'), () => startEditing(abs, sessionId)) },
|
|
2874
3211
|
{ label: t('rename'), icon: 'edit', onSelect: () => run(t('rename'), () => openFileInKit(abs, sessionId, inputActions, 'rename')) },
|
|
2875
3212
|
{ label: t('move'), icon: 'folder', onSelect: () => run(t('move'), () => openFileInKit(abs, sessionId, inputActions, 'move')) },
|
|
2876
3213
|
{ label: t('copyAbs'), icon: 'copy', onSelect: () => run(t('copyAbs'), async () => {
|
|
@@ -2897,9 +3234,9 @@
|
|
|
2897
3234
|
{
|
|
2898
3235
|
type: 'button',
|
|
2899
3236
|
className: 'dsh-fk-tool',
|
|
2900
|
-
title: t('
|
|
2901
|
-
'aria-label': t('
|
|
2902
|
-
onClick: () => run(t('
|
|
3237
|
+
title: t('openEditor'),
|
|
3238
|
+
'aria-label': t('openEditor'),
|
|
3239
|
+
onClick: () => run(t('openEditor'), () => startEditing(abs, sessionId)),
|
|
2903
3240
|
},
|
|
2904
3241
|
h(Icon, { name: 'edit', size: 15 }),
|
|
2905
3242
|
),
|
|
@@ -3110,7 +3447,7 @@ body[data-ds-dark-theme]{--dsh-fk-dir:#f3bf4d;--dsh-fk-soft:rgb(247 201 72/.14)}
|
|
|
3110
3447
|
.dsh-fk-body{flex:1;min-height:0;overflow:auto;padding:4px 6px 10px}
|
|
3111
3448
|
.dsh-fk-row{display:flex;align-items:center;gap:8px;min-height:32px;padding:3px 6px;border-radius:7px;cursor:pointer;outline:none}
|
|
3112
3449
|
.dsh-fk-row:hover{background:var(--dsh-fk-hover)}
|
|
3113
|
-
.dsh-fk-row[data-menu=true]
|
|
3450
|
+
.dsh-fk-row[data-menu=true]{background:var(--dsh-fk-hover)}
|
|
3114
3451
|
.dsh-fk-row:focus-visible{outline:1.5px solid var(--dsh-fk-orange);outline-offset:-2px}
|
|
3115
3452
|
.dsh-fk-row[data-drop=true]{outline:1.5px dashed var(--dsh-fk-orange);outline-offset:-2px;background:var(--dsh-fk-soft)}
|
|
3116
3453
|
.dsh-fk-row-static{cursor:default}
|
|
@@ -3124,6 +3461,12 @@ body[data-ds-dark-theme]{--dsh-fk-dir:#f3bf4d;--dsh-fk-soft:rgb(247 201 72/.14)}
|
|
|
3124
3461
|
.dsh-fk-row:hover .dsh-fk-rowactions,.dsh-fk-row:focus-within .dsh-fk-rowactions{opacity:1}
|
|
3125
3462
|
@media (hover:none){.dsh-fk-rowactions{opacity:1}}
|
|
3126
3463
|
.dsh-fk-editor{flex:1;min-height:0;display:flex;flex-direction:column;gap:8px;padding:10px 12px 12px}
|
|
3464
|
+
.dsh-fk-editorhead{display:flex;align-items:center;gap:8px;padding:6px 8px 6px 12px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}
|
|
3465
|
+
.dsh-fk-editorpath{display:flex;align-items:center;gap:6px;min-width:0;color:var(--dsw-alias-label-primary);font-size:12.5px}
|
|
3466
|
+
.dsh-fk-editorpath svg{color:var(--dsh-fk-dir)}
|
|
3467
|
+
.dsh-fk-editorpathname{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
3468
|
+
.dsh-fk-editorpathdir{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
3469
|
+
.dsh-fk-iconactions{display:flex;align-items:center;gap:2px;flex:none}
|
|
3127
3470
|
.dsh-fk-codeeditor{position:relative;flex:1;min-height:160px;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base)}
|
|
3128
3471
|
.dsh-fk-codeeditor:focus-within{border-color:var(--dsh-fk-orange)}
|
|
3129
3472
|
.dsh-fk-codeeditor-pre{position:absolute;inset:0;margin:0;padding:10px 12px;overflow:hidden;pointer-events:none;font:var(--dsh-fk-editor-font)/1.62 var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);white-space:pre;tab-size:2;color:var(--dsw-alias-label-primary)}
|
|
@@ -3166,6 +3509,8 @@ body[data-ds-dark-theme]{--dsh-fk-dir:#f3bf4d;--dsh-fk-soft:rgb(247 201 72/.14)}
|
|
|
3166
3509
|
.dsh-fk-toast[data-kind=error]{color:var(--dsw-alias-state-error-primary)}
|
|
3167
3510
|
.dsh-fk-tabtitle{display:inline-flex;align-items:center;gap:5px;min-width:0}
|
|
3168
3511
|
.dsh-fk-tabtitle svg{color:var(--dsh-fk-dir)}
|
|
3512
|
+
.dsh-fk-tabtitle-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3513
|
+
.dsh-fk-tabdot{width:7px;height:7px;border-radius:50%;background:var(--dsh-fk-orange);flex:none;margin-left:2px;box-shadow:0 0 0 2px rgb(244 81 30/.18)}
|
|
3169
3514
|
.dsh-fk-filestab{background:transparent}
|
|
3170
3515
|
.dsh-fk-treehead{display:flex;align-items:center;gap:6px;padding:7px 8px 7px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}
|
|
3171
3516
|
.dsh-fk-rootpath{display:flex;align-items:center;gap:6px;min-width:0;max-width:100%;padding:4px 7px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer}
|
|
@@ -3176,7 +3521,7 @@ body[data-ds-dark-theme]{--dsh-fk-dir:#f3bf4d;--dsh-fk-soft:rgb(247 201 72/.14)}
|
|
|
3176
3521
|
.dsh-fk-tlevel{display:flex;flex-direction:column;gap:0}
|
|
3177
3522
|
.dsh-fk-trow{display:flex;align-items:center;gap:5px;min-height:26px;padding:2px 6px 2px 5px;border-radius:6px;cursor:pointer;outline:none;user-select:none}
|
|
3178
3523
|
.dsh-fk-trow:hover{background:var(--dsh-fk-hover)}
|
|
3179
|
-
.dsh-fk-trow[data-menu=true]
|
|
3524
|
+
.dsh-fk-trow[data-menu=true]{background:var(--dsh-fk-hover)}
|
|
3180
3525
|
.dsh-fk-trow:focus-visible{outline:1.5px solid var(--dsh-fk-orange);outline-offset:-2px}
|
|
3181
3526
|
.dsh-fk-trow[data-drop=true]{outline:1.5px dashed var(--dsh-fk-orange);outline-offset:-2px;background:var(--dsh-fk-soft)}
|
|
3182
3527
|
.dsh-fk-trow[data-kind=directory] .dsh-fk-trowname{font-weight:500}
|
|
@@ -3304,24 +3649,49 @@ body[data-ds-dark-theme]{--dsh-fk-dir:#f3bf4d;--dsh-fk-soft:rgb(247 201 72/.14)}
|
|
|
3304
3649
|
}),
|
|
3305
3650
|
'dsh-filekit: filekit type',
|
|
3306
3651
|
);
|
|
3652
|
+
ctx.effect(
|
|
3653
|
+
() =>
|
|
3654
|
+
tabs.register({
|
|
3655
|
+
id: EDITOR_ID,
|
|
3656
|
+
kind: EDITOR_KIND,
|
|
3657
|
+
priority: 'extension',
|
|
3658
|
+
keepMounted: true,
|
|
3659
|
+
patterns: ['dsh-resource://filekit-edit/[\\s\\S]+'],
|
|
3660
|
+
title: () => t('editorTab'),
|
|
3661
|
+
}),
|
|
3662
|
+
'dsh-filekit: editor type',
|
|
3663
|
+
);
|
|
3307
3664
|
} else {
|
|
3308
3665
|
console.warn('[dsh-filekit] sidebarRightTabs service missing; tabs not registered');
|
|
3309
3666
|
}
|
|
3310
3667
|
|
|
3311
3668
|
if (sidebarRightService !== undefined && typeof sidebarRightService.registerCloseHandler === 'function') {
|
|
3312
3669
|
ctx.effect(() => {
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3670
|
+
const releases = [];
|
|
3671
|
+
for (const kind of [FILEKIT_KIND, EDITOR_KIND]) {
|
|
3672
|
+
try {
|
|
3673
|
+
releases.push(
|
|
3674
|
+
sidebarRightService.registerCloseHandler(kind, (sessionId, tab) => {
|
|
3675
|
+
if (dirtyTabs.has(tab.id)) {
|
|
3676
|
+
showToast(t('saveBeforeClose'), 'error');
|
|
3677
|
+
throw new Error('dsh-filekit: unsaved changes');
|
|
3678
|
+
}
|
|
3679
|
+
}),
|
|
3680
|
+
);
|
|
3681
|
+
} catch (error) {
|
|
3682
|
+
console.warn(`[dsh-filekit] close guard for ${kind} not registered:`, error?.message ?? error);
|
|
3683
|
+
}
|
|
3323
3684
|
}
|
|
3324
|
-
|
|
3685
|
+
return () => {
|
|
3686
|
+
for (const release of releases) {
|
|
3687
|
+
try {
|
|
3688
|
+
release?.();
|
|
3689
|
+
} catch {
|
|
3690
|
+
/* ignore */
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
};
|
|
3694
|
+
}, 'dsh-filekit: close guards');
|
|
3325
3695
|
}
|
|
3326
3696
|
|
|
3327
3697
|
const registerList = (slot, options, Component) => {
|
|
@@ -3356,8 +3726,10 @@ body[data-ds-dark-theme]{--dsh-fk-dir:#f3bf4d;--dsh-fk-soft:rgb(247 201 72/.14)}
|
|
|
3356
3726
|
FilesBody,
|
|
3357
3727
|
);
|
|
3358
3728
|
registerKeyed('sidebar.right.pane.tab', { key: FILEKIT_ID, locale: NS }, FileKitTab);
|
|
3729
|
+
registerKeyed('sidebar.right.pane.tab', { key: EDITOR_ID, locale: NS }, FileEditorTab);
|
|
3359
3730
|
registerKeyed('sidebar.right.pane.tab.title', { key: FILES_ID }, FilesTitle);
|
|
3360
3731
|
registerKeyed('sidebar.right.pane.tab.title', { key: FILEKIT_ID }, FileKitTitle);
|
|
3732
|
+
registerKeyed('sidebar.right.pane.tab.title', { key: EDITOR_ID }, EditorTitle);
|
|
3361
3733
|
|
|
3362
3734
|
// The workspace-files guide card gains a FileKit button underneath.
|
|
3363
3735
|
registerKeyed('sidebar.right.tab.guide.entry', { key: FILES_ID, locale: NS }, FilesGuideCard);
|
package/index.js
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
const ROUTE = '/dsh-filekit';
|
|
39
39
|
|
|
40
40
|
/** Plugin version mirrored in package.json. */
|
|
41
|
-
const VERSION = '0.2.
|
|
41
|
+
const VERSION = '0.2.3';
|
|
42
42
|
|
|
43
43
|
/** Request body cap: larger than the editor's text cap, small enough to stay sane. */
|
|
44
44
|
const BODY_LIMIT = 16 * 1024 * 1024;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@restregular/dsh-filekit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "FileKit — 在右侧「工作区文件」面板中右键操作、拖拽移动、编辑、复制引用与可恢复删除(.dsh-trash 回收站),变更自动告知 Agent;另含独立 FileKit 文件管理页与设置页。 · Workspace file operations for the DeepSeek Harness right sidebar: context menus, drag-to-move, editor, recoverable trash, Agent change notices, a FileKit page, and settings.",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
6
|
"type": "module",
|