@jingyi0605/codingns4dsh 0.1.7-rc.3 → 0.2.0-beta.1
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/LICENSE +695 -0
- package/README.en.md +23 -9
- package/README.md +23 -9
- package/data/build/dist/bootstrap/dsh-connection-adapter.d.ts +1 -1
- package/data/build/dist/bootstrap/index.d.ts +1 -1
- package/data/build/dist/client/bundle.js +1200 -170
- package/data/build/dist/client/context-breakdown-dom.d.ts +53 -0
- package/data/build/dist/client/context-breakdown-dom.js +171 -0
- package/data/build/dist/client/dsh-h5-bootstrap.js +7 -1
- package/data/build/dist/client/features/cli-adapters.js +11 -0
- package/data/build/dist/client/features/index.d.ts +1 -0
- package/data/build/dist/client/features/index.js +1 -0
- package/data/build/dist/client/features/subscription-usage-panel.d.ts +15 -0
- package/data/build/dist/client/features/subscription-usage-panel.js +117 -0
- package/data/build/dist/client/features/workspace-session-enhancement-panel.js +22 -5
- package/data/build/dist/client/features/workspace-session-enhancement.js +17 -2
- package/data/build/dist/client/git-management.js +80 -10
- package/data/build/dist/client/index.d.ts +1 -0
- package/data/build/dist/client/index.js +16 -49
- package/data/build/dist/client/locale.js +20 -2
- package/data/build/dist/client/remote-web-context.js +26 -2
- package/data/build/dist/client/settings-bridge.d.ts +20 -5
- package/data/build/dist/client/settings-bridge.js +30 -7
- package/data/build/dist/client/subscription-slot.d.ts +3 -1
- package/data/build/dist/client/subscription-slot.js +31 -6
- package/data/build/dist/client/terminal/ui.js +1 -1
- package/data/build/dist/client/terminal/xterm-view.js +1 -1
- package/data/build/dist/dsh-capabilities/client/config-forms-adapter.d.ts +10 -0
- package/data/build/dist/dsh-capabilities/client/config-forms-adapter.js +22 -0
- package/data/build/dist/dsh-capabilities/client/primitives-adapter.d.ts +5 -2
- package/data/build/dist/dsh-capabilities/client/primitives-adapter.js +3 -2
- package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.d.ts +2 -2
- package/data/build/dist/dsh-capabilities/host/config-forms-adapter.d.ts +22 -1
- package/data/build/dist/dsh-capabilities/host/config-forms-adapter.js +29 -3
- package/data/build/dist/dsh-capabilities/host/settings-scope-adapter.d.ts +2 -2
- package/data/build/dist/dsh-capabilities/matrix.js +31 -1
- package/data/build/dist/dsh-capabilities/routes.d.ts +8 -1
- package/data/build/dist/dsh-capabilities/routes.js +173 -5
- package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
- package/data/build/dist/features/registry.d.ts +7 -0
- package/data/build/dist/features/registry.js +20 -2
- package/data/build/dist/host/cli-adapters/feature.js +24 -2
- package/data/build/dist/host/cli-adapters/native-team-proxy.d.ts +36 -0
- package/data/build/dist/host/cli-adapters/native-team-proxy.js +108 -0
- package/data/build/dist/host/cli-adapters/provider-subscription.d.ts +35 -0
- package/data/build/dist/host/cli-adapters/provider-subscription.js +405 -5
- package/data/build/dist/host/cli-adapters/registry.d.ts +9 -3
- package/data/build/dist/host/cli-adapters/registry.js +30 -0
- package/data/build/dist/host/cli-adapters/session-store.d.ts +8 -2
- package/data/build/dist/host/cli-adapters/session-store.js +19 -0
- package/data/build/dist/host/features/git-management.js +41 -18
- package/data/build/dist/host/features/types.d.ts +6 -3
- package/data/build/dist/host/index-injection.d.ts +12 -5
- package/data/build/dist/host/index-injection.js +32 -11
- package/data/build/dist/host/index.js +15 -4
- package/data/build/dist/host/lan-access-dsh.d.ts +2 -2
- package/data/build/dist/host/native-session-bridge.d.ts +4 -0
- package/data/build/dist/host/native-session-bridge.js +51 -9
- package/data/build/dist/host/relay-tunnel-runtime.js +10 -1
- package/data/build/dist/host/remote-web-runtime.js +40 -17
- package/data/build/dist/host/rpc.d.ts +24 -5
- package/data/build/dist/host/rpc.js +38 -9
- package/data/build/dist/host/session-migration-repair.js +14 -0
- package/data/build/dist/host/settings.d.ts +2 -2
- package/data/build/dist/host/settings.js +11 -1
- package/data/build/dist/host/terminal/startup.d.ts +2 -2
- package/data/build/dist/shared/contracts/cli-adapter.d.ts +22 -1
- package/data/build/dist/shared/contracts/config.d.ts +21 -0
- package/data/build/dist/shared/contracts/config.js +17 -0
- package/data/build/dist/shared/contracts/errors.d.ts +2 -2
- package/data/build/dist/shared/contracts/subscription.d.ts +7 -0
- package/data/build/dist/shared/contracts/subscription.js +14 -1
- package/data/build/dist/shared/contracts/version.d.ts +4 -4
- package/data/build/dist/shared/contracts/version.js +3 -3
- package/data/build/dist/shared/index.d.ts +3 -2
- package/data/build/dist/shared/index.js +2 -1
- package/data/build/dist/transport/carrier.d.ts +11 -1
- package/data/build/dist/transport/carrier.js +77 -26
- package/data/build/dist/transport/debug.d.ts +5 -0
- package/data/build/dist/transport/debug.js +17 -2
- package/data/build/dist/transport/dsh-session.d.ts +18 -1
- package/data/build/dist/transport/dsh-session.js +48 -13
- package/data/build/dist/transport/index.d.ts +1 -1
- package/data/build/dist/transport/index.js +1 -1
- package/package.json +23 -19
- package/profile/README.md +2 -2
- package/profile/package.json +4 -3
- package/profile/version.json +3 -3
- package/version.json +3 -3
|
@@ -71,15 +71,39 @@ function GitPanel(props) {
|
|
|
71
71
|
const [busy, setBusy] = useState(false);
|
|
72
72
|
const [toast, setToast] = useState(null);
|
|
73
73
|
const [diffView, setDiffView] = useState(null);
|
|
74
|
+
const [fileDiff, setFileDiff] = useState(null);
|
|
75
|
+
const [singleColumn, setSingleColumn] = useState(false);
|
|
76
|
+
const [activeColumn, setActiveColumn] = useState('files');
|
|
74
77
|
const [historyTotalCount, setHistoryTotalCount] = useState(0);
|
|
75
78
|
const [historyLoadingMore, setHistoryLoadingMore] = useState(false);
|
|
79
|
+
const contentGridRef = useRef(null);
|
|
76
80
|
const historyExpanded = useRef(false);
|
|
81
|
+
const gridVisible = status !== null && status.snapshot.enabled !== false;
|
|
77
82
|
useEffect(() => {
|
|
78
83
|
if (toast === null)
|
|
79
84
|
return;
|
|
80
85
|
const timer = globalThis.setTimeout(() => setToast(null), 3200);
|
|
81
86
|
return () => globalThis.clearTimeout(timer);
|
|
82
87
|
}, [toast]);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const element = contentGridRef.current;
|
|
90
|
+
if (element === null) {
|
|
91
|
+
setSingleColumn(false);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const update = () => {
|
|
95
|
+
const tracks = getComputedStyle(element).gridTemplateColumns.split(' ').filter((track) => track !== '');
|
|
96
|
+
setSingleColumn(tracks.length <= 1);
|
|
97
|
+
};
|
|
98
|
+
update();
|
|
99
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
100
|
+
window.addEventListener('resize', update);
|
|
101
|
+
return () => window.removeEventListener('resize', update);
|
|
102
|
+
}
|
|
103
|
+
const observer = new ResizeObserver(update);
|
|
104
|
+
observer.observe(element);
|
|
105
|
+
return () => observer.disconnect();
|
|
106
|
+
}, [gridVisible]);
|
|
83
107
|
const notify = (kind, message) => setToast({ kind, message });
|
|
84
108
|
useEffect(() => {
|
|
85
109
|
let disposed = false;
|
|
@@ -92,6 +116,7 @@ function GitPanel(props) {
|
|
|
92
116
|
historyExpanded.current = false;
|
|
93
117
|
setBranches(null);
|
|
94
118
|
setDiffView(null);
|
|
119
|
+
setFileDiff(null);
|
|
95
120
|
const load = async (resolvedWorkspaceId) => {
|
|
96
121
|
const cached = readCache(resolvedWorkspaceId);
|
|
97
122
|
// 已经手动展开历史后,定时刷新只能更新状态和分支,不能用首屏缓存覆盖已加载的分页。
|
|
@@ -235,6 +260,16 @@ function GitPanel(props) {
|
|
|
235
260
|
setToast(null);
|
|
236
261
|
}).catch((error) => notify('error', error instanceof Error ? error.message : String(error))).finally(() => setBusy(false));
|
|
237
262
|
};
|
|
263
|
+
const openFileDiff = (path, staged) => {
|
|
264
|
+
if (workspaceId === undefined)
|
|
265
|
+
return;
|
|
266
|
+
setBusy(true);
|
|
267
|
+
notify('info', '正在读取文件 Diff…');
|
|
268
|
+
void call(props.rpc, 'git/diff', { workspaceId, path, staged }).then((value) => {
|
|
269
|
+
setFileDiff({ path, staged, diff: value });
|
|
270
|
+
setToast(null);
|
|
271
|
+
}).catch((error) => notify('error', error instanceof Error ? error.message : String(error))).finally(() => setBusy(false));
|
|
272
|
+
};
|
|
238
273
|
const loadMoreHistory = () => {
|
|
239
274
|
if (workspaceId === undefined || historyLoadingMore || history.length >= historyTotalCount)
|
|
240
275
|
return;
|
|
@@ -257,10 +292,10 @@ function GitPanel(props) {
|
|
|
257
292
|
};
|
|
258
293
|
const stageAll = () => { void run('git/stage', { targets: unstaged.map((item) => item.path) }); };
|
|
259
294
|
const discardAll = () => { void run('git/discard', { targets: changes.map((item) => item.path) }); };
|
|
260
|
-
return createElement('section', { style: panelStyle, 'data-git-management-panel': 'true' }, createElement('header', { style: headerStyle }, createElement('div', { style: { minWidth: 0 } }, createElement('h1', { style: titleStyle }, 'Git'), createElement('div', { style:
|
|
261
|
-
event.preventDefault(); }, placeholder: '在这里输入提交信息', rows: 1, style: commitSubjectStyle }), createElement('button', { type: 'button', disabled: busy || workspaceId === undefined, onClick: () => notify('info', '生成提交信息功能暂未开放'), style: draftButtonStyle, title: '生成提交信息', 'aria-label': '生成提交信息' }, '✦')), createElement('div', { style: commitActionsStyle }, createElement('button', { type: 'button', disabled: busy || workspaceId === undefined, onClick: () => void run('git/status', {}, (value) => setStatus(value)), style: refreshActionStyle }, '刷新'), createElement('button', { type: 'button', disabled: busy || workspaceId === undefined || staged.length === 0 || subject.trim().length === 0, onClick: commit, style: submitActionStyle }, '提交'))), staged.length > 0 ? createElement(ChangeSection, { title: '暂存文件', items: staged, busy, onAction: (action, path) => void run(`git/${action}`, { targets: [path] }), onBatchAction: (action, paths) => void run(`git/${action}`, { targets: paths }), onBulkAction: () => void run('git/unstage', { targets: staged.map((item) => item.path) }) }) : null, unstaged.length > 0 ? createElement(ChangeSection, { title: '未提交文件', items: unstaged, busy, onAction: (action, path) => void run(`git/${action}`, { targets: [path] }), onBatchAction: (action, paths) => void run(`git/${action}`, { targets: paths }), onBulkAction: () => void run('git/stage', { targets: unstaged.map((item) => item.path) }) }) : null), createElement('div', { style: columnStyle }, createElement(HistorySection, { history, totalCount: historyTotalCount, hasMore: history.length < historyTotalCount, loadingMore: historyLoadingMore, onLoadMore: loadMoreHistory, branches, busy, onSwitch: (branchName) => void run('git/switch', { branchName, create: false }, (value) => setBranches(normalizeBranchSnapshot(value))), onCopy: copyCommitHash, onCopyMessage: copyCommitMessage, onViewDiff: openCommitDiff, onUndo: () => runGitOperation('undo') }))) : null);
|
|
295
|
+
return createElement('section', { style: panelStyle, 'data-git-management-panel': 'true' }, createElement('header', { style: headerStyle }, createElement('div', { style: { minWidth: 0 } }, createElement('h1', { style: titleStyle }, 'Git')), createElement('div', { style: headerActionsStyle }, singleColumn ? createElement('div', { style: columnSwitchStyle, role: 'group', 'aria-label': '切换文件与版本视图' }, createElement('button', { type: 'button', onClick: () => setActiveColumn('files'), style: activeColumn === 'files' ? columnSwitchActiveButtonStyle : columnSwitchButtonStyle, title: '切换文件', 'aria-label': '切换文件', 'aria-pressed': activeColumn === 'files' }, '切换文件'), createElement('button', { type: 'button', onClick: () => setActiveColumn('history'), style: activeColumn === 'history' ? columnSwitchActiveButtonStyle : columnSwitchButtonStyle, title: '切换版本', 'aria-label': '切换版本', 'aria-pressed': activeColumn === 'history' }, '切换版本')) : null, createElement(GitOperationsMenu, { busy, hasRemote: Boolean(status?.snapshot.hasRemote || branches?.remote.length), canUndo: history.length > 0, hasMoreVersions: history.length < historyTotalCount, stagedCount: staged.length, unstagedCount: unstaged.length, onStageAll: stageAll, onDiscardAll: discardAll, onLoadMore: loadMoreHistory, onOperation: runGitOperation }))), toast === null ? null : createElement('div', { role: toast.kind === 'error' ? 'alert' : 'status', 'aria-live': 'polite', style: { ...dshSettingsToastStyle, position: 'absolute', top: 8, right: 'auto', left: '50%', transform: 'translateX(-50%)', width: 'min(300px, calc(100% - 24px))', pointerEvents: 'none', borderColor: toast.kind === 'error' ? dshThemeColor.error : toast.kind === 'success' ? dshThemeColor.success : dshThemeColor.border } }, toast.message), status === null ? createElement('div', { style: emptyStyle }, '正在读取 Git 状态…') : null, status !== null && status.snapshot.enabled === false ? createElement('section', { style: sectionStyle }, createElement('strong', undefined, '当前目录还没有 Git 仓库'), createElement('div', { style: mutedStyle }, '初始化后即可查看改动、提交和版本历史。'), createElement('button', { type: 'button', disabled: busy, onClick: () => void run('git/init', {}), style: primaryButtonStyle }, '初始化 Git')) : null, status !== null ? createElement('div', { style: summaryStyle }, `${staged.length} 个已暂存 · ${unstaged.length} 个未暂存 · ${historyTotalCount} 条提交`) : null, diffView !== null ? createElement(DiffViewer, { diff: diffView, onClose: () => setDiffView(null) }) : null, fileDiff !== null ? createElement(FileDiffViewer, { path: fileDiff.path, staged: fileDiff.staged, diff: fileDiff.diff, onClose: () => setFileDiff(null) }) : null, status !== null && status.snapshot.enabled !== false ? createElement('div', { ref: contentGridRef, style: contentGridStyle }, singleColumn && activeColumn !== 'files' ? null : createElement('div', { style: columnStyle }, createElement('section', { style: commitSectionStyle }, createElement('div', { style: commitEditorRowStyle }, createElement('textarea', { value: subject, disabled: busy || workspaceId === undefined, onChange: (event) => setSubject(event.currentTarget.value), onKeyDown: (event) => { if (event.key === 'Enter')
|
|
296
|
+
event.preventDefault(); }, placeholder: '在这里输入提交信息', rows: 1, style: commitSubjectStyle }), createElement('button', { type: 'button', disabled: busy || workspaceId === undefined, onClick: () => notify('info', '生成提交信息功能暂未开放'), style: draftButtonStyle, title: '生成提交信息', 'aria-label': '生成提交信息' }, '✦')), createElement('div', { style: commitActionsStyle }, createElement('button', { type: 'button', disabled: busy || workspaceId === undefined, onClick: () => void run('git/status', {}, (value) => setStatus(value)), style: refreshActionStyle }, '刷新'), createElement('button', { type: 'button', disabled: busy || workspaceId === undefined || staged.length === 0 || subject.trim().length === 0, onClick: commit, style: submitActionStyle }, '提交'))), staged.length > 0 ? createElement(ChangeSection, { title: '暂存文件', items: staged, busy, onOpenDiff: openFileDiff, onAction: (action, path) => void run(`git/${action}`, { targets: [path] }), onBatchAction: (action, paths) => void run(`git/${action}`, { targets: paths }), onBulkAction: () => void run('git/unstage', { targets: staged.map((item) => item.path) }) }) : null, unstaged.length > 0 ? createElement(ChangeSection, { title: '未提交文件', items: unstaged, busy, onOpenDiff: openFileDiff, onAction: (action, path) => void run(`git/${action}`, { targets: [path] }), onBatchAction: (action, paths) => void run(`git/${action}`, { targets: paths }), onBulkAction: () => void run('git/stage', { targets: unstaged.map((item) => item.path) }) }) : null), singleColumn && activeColumn !== 'history' ? null : createElement('div', { style: columnStyle }, createElement(HistorySection, { history, totalCount: historyTotalCount, hasMore: history.length < historyTotalCount, loadingMore: historyLoadingMore, onLoadMore: loadMoreHistory, branches, busy, onSwitch: (branchName) => void run('git/switch', { branchName, create: false }, (value) => setBranches(normalizeBranchSnapshot(value))), onCopy: copyCommitHash, onCopyMessage: copyCommitMessage, onViewDiff: openCommitDiff, onUndo: () => runGitOperation('undo') }))) : null);
|
|
262
297
|
}
|
|
263
|
-
function ChangeSection({ title, items, busy, onAction, onBatchAction, onBulkAction }) {
|
|
298
|
+
function ChangeSection({ title, items, busy, onOpenDiff, onAction, onBatchAction, onBulkAction }) {
|
|
264
299
|
const [hoveredPath, setHoveredPath] = useState(null);
|
|
265
300
|
const nodes = buildChangeTree(items);
|
|
266
301
|
const staged = title === '暂存文件';
|
|
@@ -272,7 +307,7 @@ function ChangeSection({ title, items, busy, onAction, onBatchAction, onBulkActi
|
|
|
272
307
|
return createElement('details', { key: `dir:${node.path}`, open: true, style: treeDirectoryStyle }, createElement('summary', { style: { ...treeRowStyle, paddingLeft: 8 + depth * 14 }, onMouseEnter: () => setHoveredPath(directoryKey), onMouseLeave: () => setHoveredPath(null) }, createElement('span', { style: treeChevronStyle }, '⌄'), createElement('span', { style: folderIconStyle }, '▰'), createElement('span', { style: fileNameStyle, title: node.path }, node.name), createElement('span', { style: mutedStyle }, countTreeFiles(node)), isHovered ? createElement('div', { style: rowActionsStyle }, createElement('button', { type: 'button', disabled: busy, onClick: (event) => { event.preventDefault(); event.stopPropagation(); onBatchAction(staged ? 'unstage' : 'stage', directoryTargets); }, style: iconButtonStyle, title: staged ? '撤销目录暂存' : '将目录添加到暂存区', 'aria-label': staged ? '撤销目录暂存' : '将目录添加到暂存区' }, staged ? '↶' : '+'), !staged ? createElement('button', { type: 'button', disabled: busy, onClick: (event) => { event.preventDefault(); event.stopPropagation(); onBatchAction('discard', directoryTargets); }, style: dangerIconButtonStyle, title: '撤销目录变更', 'aria-label': '撤销目录变更' }, '×') : null) : null), createElement('div', undefined, ...node.children.map((child) => renderNode(child, depth + 1))));
|
|
273
308
|
}
|
|
274
309
|
const isHovered = hoveredPath === node.path;
|
|
275
|
-
return createElement('div', { key: `file:${node.path}`, style: { ...treeRowStyle, paddingLeft: 28 + depth * 14 }, onMouseEnter: () => setHoveredPath(node.path), onMouseLeave: () => setHoveredPath(null) }, createElement('span', { style: fileIconStyle }, fileIcon(node.name)), createElement('span', { title: node.path, style: fileNameStyle }, node.name), createElement('span', { style: fileStatusStyle }, changeStatus(node.item, staged)), isHovered ? createElement('div', { style: rowActionsStyle }, createElement('button', { type: 'button', disabled: busy, onClick: () => onAction(staged ? 'unstage' : 'stage', node.path), style: iconButtonStyle, title: staged ? '撤销暂存' : '添加到暂存区', 'aria-label': staged ? '撤销暂存' : '添加到暂存区' }, staged ? '↶' : '+'), !staged ? createElement('button', { type: 'button', disabled: busy, onClick: () => onAction('discard', node.path), style: dangerIconButtonStyle, title: '撤销变更', 'aria-label': '撤销变更' }, '×') : null) : null);
|
|
310
|
+
return createElement('div', { key: `file:${node.path}`, style: { ...treeRowStyle, paddingLeft: 28 + depth * 14 }, onMouseEnter: () => setHoveredPath(node.path), onMouseLeave: () => setHoveredPath(null), onDoubleClick: () => onOpenDiff(node.path, staged) }, createElement('span', { style: fileIconStyle }, fileIcon(node.name)), createElement('span', { title: node.path, style: fileNameStyle }, node.name), createElement('span', { style: fileStatusStyle }, changeStatus(node.item, staged)), isHovered ? createElement('div', { style: rowActionsStyle }, createElement('button', { type: 'button', disabled: busy, onClick: () => onAction(staged ? 'unstage' : 'stage', node.path), style: iconButtonStyle, title: staged ? '撤销暂存' : '添加到暂存区', 'aria-label': staged ? '撤销暂存' : '添加到暂存区' }, staged ? '↶' : '+'), !staged ? createElement('button', { type: 'button', disabled: busy, onClick: () => onAction('discard', node.path), style: dangerIconButtonStyle, title: '撤销变更', 'aria-label': '撤销变更' }, '×') : null) : null);
|
|
276
311
|
};
|
|
277
312
|
return createElement('section', { style: sectionStyle }, createElement('div', { style: sectionHeaderStyle }, createElement('strong', undefined, `${title} (${items.length})`), createElement('button', { type: 'button', disabled: busy, onClick: onBulkAction, style: iconButtonStyle, title: staged ? '取消全部暂存' : '全部添加到暂存区', 'aria-label': staged ? '取消全部暂存' : '全部添加到暂存区' }, staged ? '↶' : '+')), createElement('div', { style: treeStyle }, ...nodes.map((node) => renderNode(node, 0))));
|
|
278
313
|
}
|
|
@@ -282,8 +317,16 @@ function GitOperationsMenu({ busy, hasRemote, canUndo, hasMoreVersions, stagedCo
|
|
|
282
317
|
function DiffViewer({ diff, onClose }) {
|
|
283
318
|
const providedFiles = diff.files ?? [];
|
|
284
319
|
const files = providedFiles.length > 0 ? providedFiles : parseDiffFiles(diff.content);
|
|
285
|
-
|
|
286
|
-
|
|
320
|
+
return createElement('div', { style: diffOverlayStyle }, createElement('section', { role: 'dialog', 'aria-modal': true, 'aria-label': '提交 Diff', style: diffStyle }, createElement('div', { style: sectionHeaderStyle }, createElement('strong', undefined, `提交 Diff · ${diff.commitHash.slice(0, 8)}`), createElement('button', { type: 'button', onClick: onClose, style: iconButtonStyle, title: '关闭 Diff', 'aria-label': '关闭 Diff' }, '×')), createElement('div', { style: diffBodyStyle }, createElement('section', { style: diffFilesSectionStyle }, createElement('div', { style: diffSectionHeaderStyle }, createElement('strong', undefined, '变更文件'), createElement('span', { style: diffCountStyle }, String(files.length))), files.length === 0 ? createElement('div', { style: diffEmptyStyle }, '没有检测到文件变更。') : createElement('div', { style: diffFileListStyle }, ...files.map((file) => createElement('div', { key: `${file.status}:${file.oldPath ?? ''}:${file.path}`, style: diffFileRowStyle }, createElement('span', { style: diffFileStatusStyle, 'data-status': file.status }, diffFileStatusLabel(file.status)), createElement('div', { style: diffFileNameStyle }, createElement('strong', undefined, file.path), file.oldPath ? createElement('span', { style: diffFileOldPathStyle }, `原路径:${file.oldPath}`) : null), file.binary ? createElement('span', { style: diffBinaryStyle }, '二进制') : null)))), createElement('section', { style: diffDiffSectionStyle }, createElement('div', { style: diffSectionHeaderStyle }, createElement('strong', undefined, 'Diff'), diff.truncated ? createElement('span', { style: diffTruncatedStyle }, '内容已截断') : null), createElement(DiffLines, { content: diff.content })))));
|
|
321
|
+
}
|
|
322
|
+
function DiffLines({ content }) {
|
|
323
|
+
const lines = parseDiffLines(content);
|
|
324
|
+
if (lines.length === 0)
|
|
325
|
+
return createElement('div', { style: diffEmptyStyle }, '当前没有可显示的文本差异。');
|
|
326
|
+
return createElement('div', { style: diffLinesStyle }, ...lines.map((line, index) => createElement('div', { key: `${index}:${line.kind}:${line.text}`, style: diffLineStyle(line.kind) }, createElement('span', { style: diffLineNumberStyle }, line.oldLineNo === null ? '' : String(line.oldLineNo)), createElement('span', { style: diffLineNumberStyle }, line.newLineNo === null ? '' : String(line.newLineNo)), createElement('code', { style: diffCodeStyle }, `${diffLinePrefix(line.kind)}${line.text}`))));
|
|
327
|
+
}
|
|
328
|
+
function FileDiffViewer({ path, staged, diff, onClose }) {
|
|
329
|
+
return createElement('div', { style: diffOverlayStyle }, createElement('section', { role: 'dialog', 'aria-modal': true, 'aria-label': '文件 Diff', style: diffStyle }, createElement('div', { style: sectionHeaderStyle }, createElement('strong', { style: fileDiffTitleStyle, title: path }, `文件 Diff · ${path}`), createElement('div', { style: rowActionsStyle }, createElement('span', { style: diffCountStyle }, staged ? '已暂存' : '未暂存'), createElement('button', { type: 'button', onClick: onClose, style: iconButtonStyle, title: '关闭 Diff', 'aria-label': '关闭 Diff' }, '×'))), createElement('div', { style: diffBodyStyle }, createElement('section', { style: diffDiffSectionStyle }, createElement('div', { style: diffSectionHeaderStyle }, createElement('strong', undefined, staged ? '暂存区 Diff(相对 HEAD)' : '工作区 Diff'), diff.binary ? createElement('span', { style: diffBinaryStyle }, '二进制文件') : null, diff.truncated ? createElement('span', { style: diffTruncatedStyle }, '内容已截断') : null), createElement(DiffLines, { content: diff.content })))));
|
|
287
330
|
}
|
|
288
331
|
function parseDiffLines(content) {
|
|
289
332
|
const lines = [];
|
|
@@ -350,7 +393,10 @@ function diffLineStyle(kind) {
|
|
|
350
393
|
function HistorySection({ history, totalCount, hasMore, loadingMore, onLoadMore, branches, busy, onSwitch, onCopy, onCopyMessage, onViewDiff, onUndo }) {
|
|
351
394
|
const rows = groupHistoryByDate(history).flatMap((group) => [
|
|
352
395
|
createElement('div', { key: `date:${group.key}`, style: historyDateHeaderStyle }, createElement('time', { dateTime: group.key, style: historyDateHeaderTextStyle }, group.label)),
|
|
353
|
-
...group.items.map(({ item, index, timeLabel }) =>
|
|
396
|
+
...group.items.map(({ item, index, timeLabel }) => {
|
|
397
|
+
const refs = item.refs ?? [];
|
|
398
|
+
return createElement('div', { key: item.commitHash, style: historyRowStyle }, createElement('div', { style: historyRowMainStyle }, createElement('code', { style: hashStyle }, item.commitHash.slice(0, 8)), createElement('span', { style: fileNameStyle, title: item.subject }, item.subject), createElement('time', { style: historyTimeStyle, dateTime: item.authoredAt }, timeLabel), createElement('details', { style: menuStyle }, createElement('summary', { style: menuSummaryStyle, title: '版本操作菜单', 'aria-label': '版本操作菜单' }, '⋯'), createElement('div', { style: menuPopupStyle }, createElement('button', { type: 'button', onClick: () => onViewDiff(item.commitHash), style: menuButtonStyle }, '查看更改文件与 Diff'), createElement('button', { type: 'button', onClick: () => onCopy(item.commitHash), style: menuButtonStyle }, '复制 Commit Hash'), createElement('button', { type: 'button', onClick: () => onCopyMessage(buildCommitMessageText(item.subject, item.body)), style: menuButtonStyle }, '复制提交信息'), createElement('button', { type: 'button', onClick: () => onCopy(item.commitHash), style: menuButtonStyle }, '复制 Git 版本号'), index === 0 ? createElement('button', { type: 'button', disabled: busy, onClick: onUndo, style: dangerMenuButtonStyle }, '撤销上次提交') : null))), refs.length === 0 ? null : createElement('div', { style: historyRefListStyle }, ...refs.map((ref) => createElement('span', { key: `${ref.kind}:${ref.name}`, title: ref.name, style: historyRefPillStyle(ref.kind, ref.remoteName) }, ref.name))));
|
|
399
|
+
}),
|
|
354
400
|
]);
|
|
355
401
|
return createElement('section', { style: sectionStyle }, createElement('div', { style: sectionHeaderStyle }, createElement('strong', undefined, `Git 版本 (${totalCount})`), branches === null ? null : createElement('select', { value: branches.currentBranch, disabled: busy, onChange: (event) => onSwitch(event.currentTarget.value), style: branchSelectStyle }, ...branches.local.map((branch) => createElement('option', { key: branch.name, value: branch.name }, branch.name)))), history.length === 0 ? createElement('div', { style: mutedStyle }, '暂无提交') : rows, hasMore ? createElement('button', { type: 'button', disabled: busy || loadingMore, onClick: onLoadMore, style: loadMoreButtonStyle }, loadingMore ? '正在加载…' : '查看更多版本(每次 100 条)') : null);
|
|
356
402
|
}
|
|
@@ -698,12 +744,32 @@ function formatHistoryTimestamp(value) {
|
|
|
698
744
|
return { key: `${get('year')}-${get('month')}-${get('day')}`, dateLabel: `${get('month')}月${get('day')}日`, timeLabel: `${get('hour')}:${get('minute')}` };
|
|
699
745
|
}
|
|
700
746
|
function buildCommitMessageText(subject, body) { const normalizedSubject = subject.trim(); const normalizedBody = body.trim(); return normalizedBody ? `${normalizedSubject}\n\n${normalizedBody}` : normalizedSubject; }
|
|
701
|
-
const
|
|
747
|
+
const REMOTE_REF_PALETTE = ['#8b5cf6', '#ef4444', '#10b981', '#f59e0b', '#06b6d4', '#ec4899'];
|
|
748
|
+
function resolveRemotePaletteIndex(remoteName) {
|
|
749
|
+
if (remoteName === null)
|
|
750
|
+
return 0;
|
|
751
|
+
let hash = 0;
|
|
752
|
+
for (const character of remoteName)
|
|
753
|
+
hash = (hash * 33 + character.charCodeAt(0)) >>> 0;
|
|
754
|
+
return hash % REMOTE_REF_PALETTE.length;
|
|
755
|
+
}
|
|
756
|
+
function historyRefPillStyle(kind, remoteName) {
|
|
757
|
+
if (kind === 'remote') {
|
|
758
|
+
const color = REMOTE_REF_PALETTE[resolveRemotePaletteIndex(remoteName)];
|
|
759
|
+
return { ...historyRefPillBaseStyle, border: `1px solid color-mix(in srgb, ${color} 34%, transparent)`, background: `color-mix(in srgb, ${color} 14%, transparent)`, color };
|
|
760
|
+
}
|
|
761
|
+
const color = kind === 'head' ? '#4f9cff' : '#67b0ff';
|
|
762
|
+
const mix = kind === 'head' ? { border: 36, background: 18 } : { border: 28, background: 12 };
|
|
763
|
+
return { ...historyRefPillBaseStyle, border: `1px solid color-mix(in srgb, ${color} ${mix.border}%, transparent)`, background: `color-mix(in srgb, ${color} ${mix.background}%, transparent)`, color: '#2b74d8' };
|
|
764
|
+
}
|
|
765
|
+
const panelStyle = { position: 'relative', userSelect: 'none', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 12, minHeight: '100%', padding: '16px 18px 24px', overflow: 'auto', background: dshThemeColor.pageBackground, color: dshThemeColor.labelPrimary };
|
|
702
766
|
const headerStyle = { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, paddingBottom: 10, borderBottom: `1px solid ${dshThemeColor.border}` };
|
|
703
767
|
const titleStyle = { margin: 0, fontSize: 20, lineHeight: 1.2 };
|
|
704
768
|
const tabTitleStyle = { display: 'inline-flex', alignItems: 'center', minWidth: 0, color: dshThemeColor.labelPrimary, fontSize: 12 };
|
|
705
769
|
const headerActionsStyle = { display: 'inline-flex', alignItems: 'center', gap: 4 };
|
|
706
|
-
const
|
|
770
|
+
const columnSwitchStyle = { display: 'inline-flex', alignItems: 'center', gap: 2, padding: 2, border: `1px solid ${dshThemeColor.border}`, borderRadius: 5, background: dshThemeColor.pageBackground };
|
|
771
|
+
const columnSwitchButtonStyle = { border: 0, borderRadius: 3, padding: '3px 6px', color: dshThemeColor.labelSecondary, background: 'transparent', cursor: 'pointer', fontSize: 11, whiteSpace: 'nowrap' };
|
|
772
|
+
const columnSwitchActiveButtonStyle = { ...columnSwitchButtonStyle, color: dshThemeColor.accent, background: 'color-mix(in srgb, currentColor 12%, transparent)', fontWeight: 600 };
|
|
707
773
|
const summaryStyle = { color: dshThemeColor.labelSecondary, fontSize: 12 };
|
|
708
774
|
const contentGridStyle = { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', alignItems: 'start', gap: 12 };
|
|
709
775
|
const columnStyle = { display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 };
|
|
@@ -717,7 +783,10 @@ const folderIconStyle = { color: dshThemeColor.accent, fontSize: 11 };
|
|
|
717
783
|
const fileIconStyle = { width: 12, color: dshThemeColor.labelTertiary, fontSize: 12, textAlign: 'center' };
|
|
718
784
|
const fileNameStyle = { minWidth: 0, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
|
|
719
785
|
const fileStatusStyle = { color: dshThemeColor.labelTertiary, fontFamily: 'monospace', fontSize: 11 };
|
|
720
|
-
const historyRowStyle = { display: 'flex',
|
|
786
|
+
const historyRowStyle = { display: 'flex', flexDirection: 'column', gap: 3, fontSize: 12 };
|
|
787
|
+
const historyRowMainStyle = { display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, minHeight: 28 };
|
|
788
|
+
const historyRefListStyle = { display: 'flex', flexWrap: 'wrap', gap: '4px 6px', paddingLeft: 2 };
|
|
789
|
+
const historyRefPillBaseStyle = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', minHeight: 18, padding: '0 8px', borderRadius: 999, fontSize: 10, lineHeight: 1, whiteSpace: 'nowrap' };
|
|
721
790
|
const historyDateHeaderStyle = { paddingTop: 6, color: dshThemeColor.labelSecondary, fontSize: 11, fontWeight: 600 };
|
|
722
791
|
const historyDateHeaderTextStyle = { display: 'block' };
|
|
723
792
|
const hashStyle = { color: dshThemeColor.labelTertiary, fontSize: 11 };
|
|
@@ -740,6 +809,7 @@ const diffFileStatusStyle = { display: 'inline-flex', alignItems: 'center', just
|
|
|
740
809
|
const diffFileNameStyle = { display: 'flex', flexDirection: 'column', minWidth: 0, flex: 1, overflow: 'hidden' };
|
|
741
810
|
const diffFileOldPathStyle = { overflow: 'hidden', color: dshThemeColor.labelTertiary, textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 11 };
|
|
742
811
|
const diffBinaryStyle = { color: dshThemeColor.labelTertiary, fontSize: 11 };
|
|
812
|
+
const fileDiffTitleStyle = { ...fileNameStyle, fontSize: 12 };
|
|
743
813
|
const diffTruncatedStyle = { color: dshThemeColor.labelTertiary, fontSize: 11 };
|
|
744
814
|
const diffEmptyStyle = { padding: 10, color: dshThemeColor.labelTertiary, fontSize: 12 };
|
|
745
815
|
const diffLinesStyle = { overflow: 'auto', border: `1px solid ${dshThemeColor.border}`, borderRadius: 4, background: dshThemeColor.pageBackground, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 12 };
|
|
@@ -23,6 +23,7 @@ export type { CodingNsClientFeatureModule, CodingNsClientServices, CodingNsRpcCl
|
|
|
23
23
|
export { CodingNsSettingsSection } from './settings-section.js';
|
|
24
24
|
export { startWorkspaceSessionArchiveDom, WORKSPACE_SESSION_ARCHIVE_ATTRIBUTE, WORKSPACE_SESSION_ARCHIVE_MODAL_ATTRIBUTE, } from './workspace-session-archive-dom.js';
|
|
25
25
|
export { startWorkspaceSessionVisibilityDom, WORKSPACE_SESSION_HIDDEN_ATTRIBUTE, WORKSPACE_SESSION_HIDDEN_MENU_ATTRIBUTE, WORKSPACE_SESSION_HIDDEN_LIST_ATTRIBUTE, } from './workspace-session-visibility-dom.js';
|
|
26
|
+
export { startContextBreakdownDom, CONTEXT_BREAKDOWN_HIDDEN_ATTRIBUTE, CONTEXT_BREAKDOWN_STYLE_ID, CONTEXT_BREAKDOWN_CONVERSATION_ATTRIBUTE, DEFAULT_CONTEXT_BREAKDOWN_HIDDEN_ADAPTERS, } from './context-breakdown-dom.js';
|
|
26
27
|
export { CodingNsTerminalView, CodingNsWebTerminals, registerCodingNsTerminalUi } from './terminal/index.js';
|
|
27
28
|
export { registerSubscriptionSlot, registerCommandCodeSubscriptionSlot, CommandCodeSubscriptionSlot } from './subscription-slot.js';
|
|
28
29
|
/** Client Runner 用于等待服务就绪的 Cordis 依赖声明。 */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as dshUiPrimitives from '@deepseek-ai/dsh-client-ui-primitives';
|
|
1
2
|
import './provider-icon-assets.js';
|
|
2
3
|
import { FeatureRegistry } from '../features/registry.js';
|
|
3
4
|
import { registerCodingNsLocale } from './locale.js';
|
|
@@ -7,7 +8,7 @@ import { ensureCryptoRandomUUID } from './lan-access.js';
|
|
|
7
8
|
import { CodingNsSettingsSection } from './settings-section.js';
|
|
8
9
|
import { callCodingNsRpc, createCodingNsSettingsBridge } from './settings-bridge.js';
|
|
9
10
|
import { debugInfo, debugWarn } from '../shared/debug.js';
|
|
10
|
-
import { createConfigFormSettingsStore } from '../dsh-capabilities/client/config-forms-adapter.js';
|
|
11
|
+
import { createConfigFormSettingsStore, resolveServedConfigFormNamespace } from '../dsh-capabilities/client/config-forms-adapter.js';
|
|
11
12
|
import { CodingNsWebTerminals, registerCodingNsTerminalUi } from './terminal/index.js';
|
|
12
13
|
import { startCodingNsAccountBar } from './account-bar.js';
|
|
13
14
|
import { assertInjectedDshVersion } from './dsh-runtime-version.js';
|
|
@@ -26,6 +27,7 @@ export { startPeerHostNativeNavigation, startPeerHostNativeSession, probePeerHos
|
|
|
26
27
|
export { CodingNsSettingsSection } from './settings-section.js';
|
|
27
28
|
export { startWorkspaceSessionArchiveDom, WORKSPACE_SESSION_ARCHIVE_ATTRIBUTE, WORKSPACE_SESSION_ARCHIVE_MODAL_ATTRIBUTE, } from './workspace-session-archive-dom.js';
|
|
28
29
|
export { startWorkspaceSessionVisibilityDom, WORKSPACE_SESSION_HIDDEN_ATTRIBUTE, WORKSPACE_SESSION_HIDDEN_MENU_ATTRIBUTE, WORKSPACE_SESSION_HIDDEN_LIST_ATTRIBUTE, } from './workspace-session-visibility-dom.js';
|
|
30
|
+
export { startContextBreakdownDom, CONTEXT_BREAKDOWN_HIDDEN_ATTRIBUTE, CONTEXT_BREAKDOWN_STYLE_ID, CONTEXT_BREAKDOWN_CONVERSATION_ATTRIBUTE, DEFAULT_CONTEXT_BREAKDOWN_HIDDEN_ADAPTERS, } from './context-breakdown-dom.js';
|
|
29
31
|
export { CodingNsTerminalView, CodingNsWebTerminals, registerCodingNsTerminalUi } from './terminal/index.js';
|
|
30
32
|
export { registerSubscriptionSlot, registerCommandCodeSubscriptionSlot, CommandCodeSubscriptionSlot } from './subscription-slot.js';
|
|
31
33
|
/** Client Runner 用于等待服务就绪的 Cordis 依赖声明。 */
|
|
@@ -93,7 +95,8 @@ export function apply(ctx) {
|
|
|
93
95
|
debugInfo('codingns4dsh: client account bar registration begin');
|
|
94
96
|
const disposeAccountBar = startCodingNsAccountBar(connection.rpc, undefined, settings);
|
|
95
97
|
debugInfo('codingns4dsh: client account bar registration ready');
|
|
96
|
-
|
|
98
|
+
// 图标导出由客户端静态导入决定,无法从 Context 探测;随装配一起交给注册表。
|
|
99
|
+
const capabilityProfile = createDshCapabilityRegistry(dshVersion, 'client', settingsCtx, { primitives: dshUiPrimitives }).getProfile(settingsCtx);
|
|
97
100
|
debugInfo('codingns4dsh: client capabilities resolved', {
|
|
98
101
|
dshVersion,
|
|
99
102
|
capabilities: [...capabilityProfile.capabilities.entries()].map(([capability, resolution]) => ({ capability, status: resolution.status, route: resolution.routeId, reason: resolution.reason ?? null })),
|
|
@@ -183,61 +186,25 @@ function createClientSettingsStore(ctx, rpc) {
|
|
|
183
186
|
return createCodingNsSettingsBridge(local, rpc);
|
|
184
187
|
}
|
|
185
188
|
const forms = ctx.get('configForms');
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
// DSH
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
189
|
+
const namespace = resolveServedConfigFormNamespace(forms, CODINGNS_SETTINGS_ENTRY_IDS);
|
|
190
|
+
if (namespace === undefined || forms === undefined) {
|
|
191
|
+
// DSH 只把 Host 真正下发的命名空间接进 ConfigForm。Host 持久模式(中继页面的
|
|
192
|
+
// 插件 Transport 声明 ownsHost)会为任意 entry 造出一份停在 loading 的空表单,
|
|
193
|
+
// memory 模式(局域网页面)则完全没有命名空间;两种情况下绑定原生表单都会让
|
|
194
|
+
// 设置页停在未就绪状态、把模块开关显示为不可操作。插件设置本来就由自己的 Host
|
|
195
|
+
// RPC 承载,退回 RPC 边界后这些页面依然可读写;原生设置页的 Host 所有权由宿主
|
|
196
|
+
// 启动页的合并写脚本行单独声明(见 src/host/index-injection.ts)。
|
|
197
|
+
debugWarn('codingns4dsh: client config form not served; falling back to Host settings RPC', {
|
|
193
198
|
hasConfigForms: forms !== undefined,
|
|
194
199
|
});
|
|
195
200
|
return createCodingNsSettingsBridge(undefined, rpc);
|
|
196
201
|
}
|
|
197
|
-
debugInfo('codingns4dsh: client settings source=configForms', {
|
|
198
|
-
return createConfigFormSettingsStore(
|
|
202
|
+
debugInfo('codingns4dsh: client settings source=configForms', { namespace });
|
|
203
|
+
return createConfigFormSettingsStore(forms, namespace, {
|
|
199
204
|
// DSH 0.1.7 的 ConfigForm 会把 Host 后台索引更新也纳入 revision。
|
|
200
205
|
// Codingns4DSH 的路径操作是原子的,交给 Host RPC 无条件合并,避免
|
|
201
206
|
// 模块开关因为 cliSessions 的后台心跳而永久冲突。
|
|
202
207
|
writeUnfenced: async (ops) => callCodingNsRpc(rpc, 'settings/set', { ops }),
|
|
203
208
|
});
|
|
204
209
|
}
|
|
205
|
-
function findConfigForm(forms) {
|
|
206
|
-
if (forms === undefined)
|
|
207
|
-
return undefined;
|
|
208
|
-
// ConfigForms.get 对未知 entry 也会返回一个 Form;必须先看共享 mirror 中
|
|
209
|
-
// Host 实际提供的 namespace,否则会永远拿到旧的 `codingns` 空表单。
|
|
210
|
-
try {
|
|
211
|
-
const namespaces = forms.describe?.().getSnapshot().view?.namespaces;
|
|
212
|
-
debugInfo('codingns4dsh: client config form namespaces', {
|
|
213
|
-
namespaces: namespaces?.map((item) => item.ns),
|
|
214
|
-
});
|
|
215
|
-
const servedId = CODINGNS_SETTINGS_ENTRY_IDS.find((id) => namespaces?.some((item) => item.ns === id));
|
|
216
|
-
if (servedId !== undefined) {
|
|
217
|
-
const form = forms.get(servedId);
|
|
218
|
-
debugInfo('codingns4dsh: client config form namespace selected', { namespace: servedId, hasForm: form !== undefined });
|
|
219
|
-
return form;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
// mirror 尚未就绪时继续按 scoped entry 优先的兼容顺序探测。
|
|
224
|
-
}
|
|
225
|
-
for (const id of CODINGNS_SETTINGS_ENTRY_IDS) {
|
|
226
|
-
try {
|
|
227
|
-
const form = forms.get(id);
|
|
228
|
-
if (form !== undefined && form.getSnapshot().status !== 'unavailable') {
|
|
229
|
-
debugInfo('codingns4dsh: client config form namespace fallback selected', {
|
|
230
|
-
namespace: id,
|
|
231
|
-
status: form.getSnapshot().status,
|
|
232
|
-
revision: form.getSnapshot().revision,
|
|
233
|
-
});
|
|
234
|
-
return form;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
// 不同 0.1.7 构建可能只接受其中一个 entry id,继续尝试别名。
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return undefined;
|
|
242
|
-
}
|
|
243
210
|
//# sourceMappingURL=index.js.map
|
|
@@ -21,7 +21,7 @@ const en = {
|
|
|
21
21
|
'feature.reverseProxy.label': 'Relay access service',
|
|
22
22
|
'feature.reverseProxy.description': 'Access a DSH Host through the isolated Codingns4DSH relay.',
|
|
23
23
|
'feature.cliAdapters.label': 'External Agent integration',
|
|
24
|
-
'feature.cliAdapters.description': 'View installed external Agents, versions, command paths, available models, and enable them independently.',
|
|
24
|
+
'feature.cliAdapters.description': 'View installed external Agents, versions, command paths, available models, and enable them independently. External Agent context panels keep only the occupancy percentage and totals.',
|
|
25
25
|
'feature.workspaceSession.label': 'Workspace session enhancement',
|
|
26
26
|
'feature.workspaceSession.description': 'Show Agent logos, archived-session entries, workspace hiding controls, subscription or usage information, local quick sessions, and an optional remembered conversation/right-sidebar width ratio.',
|
|
27
27
|
'feature.terminal.label': 'Terminal enhancements',
|
|
@@ -209,6 +209,15 @@ const en = {
|
|
|
209
209
|
'workspace.workspaceHidingDescription': 'Add a Hide workspace action to each workspace menu and show hidden workspaces through the workspace filter.',
|
|
210
210
|
'workspace.showSubscriptionUsage': 'Show subscription and usage',
|
|
211
211
|
'workspace.subscriptionUsageDescription': 'Detect and display provider subscription balances and upstream usage in the conversation footer.',
|
|
212
|
+
'workspace.subscriptionUsageSettings': 'Settings',
|
|
213
|
+
'subscriptionUsage.dialogTitle': 'Usage query settings',
|
|
214
|
+
'subscriptionUsage.dialogHint': 'The timeout applies to every adapter. The refresh interval drives the footer usage query; 0 disables automatic refresh, and a result is reused while it is still fresh.',
|
|
215
|
+
'subscriptionUsage.timeoutSecs': 'Query timeout (seconds)',
|
|
216
|
+
'subscriptionUsage.refreshInterval': 'Auto refresh interval (minutes)',
|
|
217
|
+
'subscriptionUsage.refreshIntervalHint': '0 disables automatic refresh.',
|
|
218
|
+
'subscriptionUsage.cancel': 'Cancel',
|
|
219
|
+
'subscriptionUsage.save': 'Save',
|
|
220
|
+
'subscriptionUsage.saved': 'Usage query settings saved',
|
|
212
221
|
'workspace.showQuickPhrases': 'Show quick sessions',
|
|
213
222
|
'workspace.quickPhrasesDescription': 'Keep reusable session instructions locally in Codingns4DSH and show the quick sessions button inside the composer.',
|
|
214
223
|
'workspace.rememberConversationRightbarRatio': 'Remember conversation and right sidebar width ratio',
|
|
@@ -250,7 +259,7 @@ const zh = {
|
|
|
250
259
|
'feature.reverseProxy.label': '中转访问服务',
|
|
251
260
|
'feature.reverseProxy.description': '通过隔离的 Codingns4DSH 中继访问 DSH Host。',
|
|
252
261
|
'feature.cliAdapters.label': '外部Agent集成',
|
|
253
|
-
'feature.cliAdapters.description': '查看外部 Agent
|
|
262
|
+
'feature.cliAdapters.description': '查看外部 Agent 的安装状态、版本、命令路径和可用模型,并单独启用或停用。外部 Agent 的上下文面板只保留占用百分比与总量。',
|
|
254
263
|
'feature.workspaceSession.label': '工作区会话增强',
|
|
255
264
|
'feature.workspaceSession.description': '在原生工作区会话行显示 Agent Logo、归档会话入口、工作区隐藏功能和订阅/用量信息,提供本地快捷会话,并可记忆对话窗口与右侧栏宽度比例。',
|
|
256
265
|
'feature.terminal.label': '终端增强',
|
|
@@ -438,6 +447,15 @@ const zh = {
|
|
|
438
447
|
'workspace.workspaceHidingDescription': '在工作区菜单中加入“隐藏工作区”,通过工作区筛选菜单显示隐藏的工作区并支持恢复。',
|
|
439
448
|
'workspace.showSubscriptionUsage': '显示订阅/用量检测',
|
|
440
449
|
'workspace.subscriptionUsageDescription': '在对话底部检测并显示 Agent 订阅余额和上游用量。',
|
|
450
|
+
'workspace.subscriptionUsageSettings': '设置',
|
|
451
|
+
'subscriptionUsage.dialogTitle': '用量查询设置',
|
|
452
|
+
'subscriptionUsage.dialogHint': '超时时间作用于所有适配器;自动查询间隔作用于对话底部的用量查询,0 表示不自动查询,间隔内的查询结果会直接复用。',
|
|
453
|
+
'subscriptionUsage.timeoutSecs': '超时时间(秒)',
|
|
454
|
+
'subscriptionUsage.refreshInterval': '自动查询间隔(分钟)',
|
|
455
|
+
'subscriptionUsage.refreshIntervalHint': '0 表示不自动查询。',
|
|
456
|
+
'subscriptionUsage.cancel': '取消',
|
|
457
|
+
'subscriptionUsage.save': '保存',
|
|
458
|
+
'subscriptionUsage.saved': '用量查询设置已保存',
|
|
441
459
|
'workspace.showQuickPhrases': '显示快捷会话',
|
|
442
460
|
'workspace.quickPhrasesDescription': '在 Codingns4DSH 内本地保存可复用的会话指令,并将快捷会话按钮显示在输入工具区。',
|
|
443
461
|
'workspace.rememberConversationRightbarRatio': '记忆对话窗口与右侧栏的宽度比例',
|
|
@@ -368,12 +368,34 @@ function createBridgeScript() {
|
|
|
368
368
|
const pending = new Map();
|
|
369
369
|
// RPC、WebSocket 和远程 stream 必须使用各自的 ID 空间;资源 fetch
|
|
370
370
|
// 不能改变 WebSocket 的 ID,否则 ws.open 与 ws.send 会指向不同 key。
|
|
371
|
+
//
|
|
372
|
+
// 每个 ID 还必须带 kind 前缀:早期实现把 fetch/script 计数器和 ws.open 计数器
|
|
373
|
+
// 当成裸数字放进同一个 pending Map,两个计数器各自从 1 开始,页面启动时
|
|
374
|
+
// 「插件包 fetch」与「/api/remote.mux ws.open」几乎同时发生,后到的
|
|
375
|
+
// pending.set 会覆盖先到的在途请求。被覆盖的那条 Promise 永远不会落地:
|
|
376
|
+
// 如果它正好是原生设置页的 settings/describe,DSH 的设置镜像会一直停在
|
|
377
|
+
// loading,中继下的「模型」页就永久空白且没有任何报错。
|
|
371
378
|
let nextCallId = 0;
|
|
372
379
|
let nextSocketId = 0;
|
|
373
380
|
let nextStreamId = 0;
|
|
374
381
|
let remoteLoadChain = Promise.resolve();
|
|
382
|
+
const allocateId = (prefix, counter) => {
|
|
383
|
+
let value = counter + 1;
|
|
384
|
+
let id = prefix + String(value);
|
|
385
|
+
// 绝不覆盖未完成的请求:万一同前缀 ID 仍在使用就继续往后取。
|
|
386
|
+
while (pending.has(id)) {
|
|
387
|
+
value += 1;
|
|
388
|
+
id = prefix + String(value);
|
|
389
|
+
}
|
|
390
|
+
return { id, counter: value };
|
|
391
|
+
};
|
|
375
392
|
const call = (kind, input, body, explicitId) => new Promise((resolve, reject) => {
|
|
376
|
-
|
|
393
|
+
let id = explicitId;
|
|
394
|
+
if (id === undefined) {
|
|
395
|
+
const allocated = allocateId('req:', nextCallId);
|
|
396
|
+
nextCallId = allocated.counter;
|
|
397
|
+
id = allocated.id;
|
|
398
|
+
}
|
|
377
399
|
pending.set(id, { resolve, reject });
|
|
378
400
|
bridgeLog('bridge.call', { kind, id, path: input && typeof input.path === 'string' ? input.path : undefined });
|
|
379
401
|
parent.postMessage({ kind, id, input, body }, '*');
|
|
@@ -705,7 +727,9 @@ function createBridgeScript() {
|
|
|
705
727
|
this.url = String(url);
|
|
706
728
|
this.readyState = RemoteWebSocket.CONNECTING;
|
|
707
729
|
this._listeners = new Map();
|
|
708
|
-
|
|
730
|
+
const allocated = allocateId('ws:', nextSocketId);
|
|
731
|
+
nextSocketId = allocated.counter;
|
|
732
|
+
window.__dshRemoteSockets.set(this._id = allocated.id, this);
|
|
709
733
|
bridgeLog('bridge.ws.open', { id: this._id, path: new URL(this.url, resourceBase()).pathname });
|
|
710
734
|
call('ws.open', { path: new URL(this.url, resourceBase()).pathname }, undefined, this._id).then(() => {
|
|
711
735
|
if (this.readyState !== RemoteWebSocket.CONNECTING) return;
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
2
1
|
import type { CodingNsSettings } from '../shared/contracts/config.js';
|
|
3
2
|
import type { CodingNsRpcClient } from './features/types.js';
|
|
4
|
-
import { type CodingNsSettingsSnapshot, type CodingNsSettingsStore } from '../dsh-capabilities/settings-store.js';
|
|
5
|
-
|
|
3
|
+
import { type CodingNsSettingsSnapshot, type CodingNsSettingsStore, type CodingNsSettingsOperation } from '../dsh-capabilities/settings-store.js';
|
|
4
|
+
/** DSH 0.1 SettingsScope 与 0.2 Client 设置镜像的共同最小结构。 */
|
|
5
|
+
export interface DshClientSettingsScope<T> {
|
|
6
|
+
getSnapshot(): LocalScopeSnapshot<T>;
|
|
7
|
+
subscribe(listener: () => void): () => void;
|
|
8
|
+
mutate(operations: readonly CodingNsSettingsOperation[], expectedRevision?: number): Promise<void | boolean>;
|
|
9
|
+
set(field: string, value: unknown): Promise<void | boolean>;
|
|
10
|
+
unset(field: string): Promise<void | boolean>;
|
|
11
|
+
}
|
|
12
|
+
type SettingsMutation = readonly CodingNsSettingsOperation[];
|
|
6
13
|
type SnapshotListener = () => void;
|
|
7
14
|
/**
|
|
8
15
|
* 把 DSH 本地设置和远程 Host 设置 RPC 统一成一个设置作用域。
|
|
@@ -19,7 +26,7 @@ export declare class CodingNsSettingsBridge implements CodingNsSettingsStore<Cod
|
|
|
19
26
|
private readonly localUnsubscribe;
|
|
20
27
|
private remoteLoad;
|
|
21
28
|
private remoteLoaded;
|
|
22
|
-
constructor(local:
|
|
29
|
+
constructor(local: DshClientSettingsScope<CodingNsSettings> | undefined, rpc: CodingNsRpcClient);
|
|
23
30
|
getSnapshot(): CodingNsSettingsSnapshot<CodingNsSettings>;
|
|
24
31
|
subscribe(listener: SnapshotListener): () => void;
|
|
25
32
|
load(): Promise<void>;
|
|
@@ -40,6 +47,14 @@ export declare class CodingNsSettingsBridge implements CodingNsSettingsStore<Cod
|
|
|
40
47
|
}
|
|
41
48
|
/** 调用 Codingns4DSH Host RPC,并兼容 DSH 原生连接与普通 Web API 回退。 */
|
|
42
49
|
export declare function callCodingNsRpc<T>(rpc: CodingNsRpcClient, endpoint: string, payload: unknown): Promise<T>;
|
|
43
|
-
export declare function createCodingNsSettingsBridge(local:
|
|
50
|
+
export declare function createCodingNsSettingsBridge(local: DshClientSettingsScope<CodingNsSettings> | undefined, rpc: CodingNsRpcClient): CodingNsSettingsBridge;
|
|
51
|
+
/** 本地镜像或占位快照中的通用字段;DSH 各版本另有 base/user/mode 等附加字段。 */
|
|
52
|
+
export type LocalScopeSnapshot<T = CodingNsSettings> = {
|
|
53
|
+
readonly value: T | undefined;
|
|
54
|
+
readonly revision: number | undefined;
|
|
55
|
+
readonly writable: boolean;
|
|
56
|
+
readonly status: 'loading' | 'ready' | 'unavailable';
|
|
57
|
+
readonly mode?: string;
|
|
58
|
+
};
|
|
44
59
|
export {};
|
|
45
60
|
//# sourceMappingURL=settings-bridge.d.ts.map
|
|
@@ -174,19 +174,42 @@ export async function callCodingNsRpc(rpc, endpoint, payload) {
|
|
|
174
174
|
return result.value;
|
|
175
175
|
}
|
|
176
176
|
/** 设置 RPC 只能传 JSON;在浏览器边界尽早拒绝函数、循环引用等无效值。 */
|
|
177
|
-
function toJsonValue(value) {
|
|
177
|
+
function toJsonValue(value, visiting = new WeakSet()) {
|
|
178
178
|
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
179
179
|
return value;
|
|
180
180
|
if (typeof value === 'number' && Number.isFinite(value))
|
|
181
181
|
return value;
|
|
182
|
-
if (Array.isArray(value))
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
if (visiting.has(value))
|
|
184
|
+
throw new TypeError('设置值不能包含循环引用');
|
|
185
|
+
visiting.add(value);
|
|
186
|
+
try {
|
|
187
|
+
return value.map((entry) => toJsonValue(entry, visiting));
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
visiting.delete(value);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (typeof value !== 'object' || value === undefined)
|
|
185
194
|
throw new TypeError('设置值必须是可序列化的 JSON');
|
|
195
|
+
const prototype = Object.getPrototypeOf(value);
|
|
196
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
197
|
+
throw new TypeError('设置值只能包含 JSON 对象、数组和标量');
|
|
198
|
+
if (visiting.has(value))
|
|
199
|
+
throw new TypeError('设置值不能包含循环引用');
|
|
200
|
+
visiting.add(value);
|
|
186
201
|
const result = {};
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
202
|
+
try {
|
|
203
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
204
|
+
if (entry === undefined)
|
|
205
|
+
continue;
|
|
206
|
+
result[key] = toJsonValue(entry, visiting);
|
|
207
|
+
}
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
visiting.delete(value);
|
|
212
|
+
}
|
|
190
213
|
}
|
|
191
214
|
export function createCodingNsSettingsBridge(local, rpc) {
|
|
192
215
|
return new CodingNsSettingsBridge(local, rpc);
|
|
@@ -12,9 +12,11 @@ interface SubscriptionSlotProps {
|
|
|
12
12
|
readonly rpc: CodingNsRpcClient;
|
|
13
13
|
readonly sessionId?: string;
|
|
14
14
|
readonly useSession?: SessionSelector;
|
|
15
|
+
/** 读取当前自动查询间隔(分钟);缺省或 0 表示不自动查询。 */
|
|
16
|
+
readonly getRefreshIntervalMins?: () => number;
|
|
15
17
|
}
|
|
16
18
|
/** 在 DSH 原生步骤统计左侧显示当前 Agent 的订阅余量。 */
|
|
17
|
-
export declare function registerSubscriptionSlot(slots: SlotRegistry, rpc: CodingNsRpcClient): () => void;
|
|
19
|
+
export declare function registerSubscriptionSlot(slots: SlotRegistry, rpc: CodingNsRpcClient, getRefreshIntervalMins?: () => number): () => void;
|
|
18
20
|
declare function CommandCodeSubscriptionSlot(props: SubscriptionSlotProps): ReactElement | null;
|
|
19
21
|
export { CommandCodeSubscriptionSlot };
|
|
20
22
|
export declare const registerCommandCodeSubscriptionSlot: typeof registerSubscriptionSlot;
|