@jingyi0605/codingns4dsh 0.1.2 → 0.1.7-rc.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.en.md +4 -4
- package/README.md +14 -5
- package/data/build/dist/client/account-bar.js +57 -3
- package/data/build/dist/client/bundle.js +36246 -268
- package/data/build/dist/client/cli-slots.js +60 -18
- package/data/build/dist/client/dsh-h5-bootstrap.d.ts +2 -0
- package/data/build/dist/client/dsh-h5-bootstrap.js +79 -13
- package/data/build/dist/client/features/file-management-panel.d.ts +5 -0
- package/data/build/dist/client/features/file-management-panel.js +48 -0
- package/data/build/dist/client/features/file-management.d.ts +4 -0
- package/data/build/dist/client/features/file-management.js +50 -0
- package/data/build/dist/client/features/index.d.ts +10 -1
- package/data/build/dist/client/features/index.js +12 -1
- package/data/build/dist/client/features/login-protection-panel.js +1 -1
- package/data/build/dist/client/features/login-protection-session.d.ts +2 -0
- package/data/build/dist/client/features/login-protection-session.js +18 -0
- package/data/build/dist/client/features/peer-host.d.ts +4 -0
- package/data/build/dist/client/features/peer-host.js +89 -0
- package/data/build/dist/client/features/reverse-proxy-panel.js +23 -7
- package/data/build/dist/client/features/reverse-proxy.d.ts +1 -2
- package/data/build/dist/client/features/reverse-proxy.js +75 -5
- package/data/build/dist/client/features/types.d.ts +9 -0
- package/data/build/dist/client/features/workspace-session-enhancement-panel.js +10 -0
- package/data/build/dist/client/features/workspace-session-enhancement.js +18 -1
- package/data/build/dist/client/file-management-dom.d.ts +12 -0
- package/data/build/dist/client/file-management-dom.js +527 -0
- package/data/build/dist/client/git-management.js +24 -5
- package/data/build/dist/client/host-navigation.d.ts +29 -0
- package/data/build/dist/client/host-navigation.js +29 -0
- package/data/build/dist/client/host-router.d.ts +3 -0
- package/data/build/dist/client/host-router.js +2 -0
- package/data/build/dist/client/index.d.ts +6 -1
- package/data/build/dist/client/index.js +30 -10
- package/data/build/dist/client/locale.js +36 -7
- package/data/build/dist/client/peer-host-connection-button.d.ts +13 -0
- package/data/build/dist/client/peer-host-connection-button.js +69 -0
- package/data/build/dist/client/peer-host-management-api.d.ts +40 -0
- package/data/build/dist/client/peer-host-management-api.js +33 -0
- package/data/build/dist/client/peer-host-management-panel.d.ts +13 -0
- package/data/build/dist/client/peer-host-management-panel.js +181 -0
- package/data/build/dist/client/peer-host-native-session-ui.d.ts +60 -0
- package/data/build/dist/client/peer-host-native-session-ui.js +372 -0
- package/data/build/dist/client/peer-host-scoped-client.d.ts +67 -0
- package/data/build/dist/client/peer-host-scoped-client.js +316 -0
- package/data/build/dist/client/peer-host-session-controller.d.ts +30 -0
- package/data/build/dist/client/peer-host-session-controller.js +95 -0
- package/data/build/dist/client/quick-phrase-slot.js +14 -0
- package/data/build/dist/client/remote-web-context.js +26 -4
- package/data/build/dist/client/session-changed-files-view.d.ts +15 -0
- package/data/build/dist/client/session-changed-files-view.js +337 -0
- package/data/build/dist/client/settings-bridge.d.ts +14 -4
- package/data/build/dist/client/settings-bridge.js +59 -12
- package/data/build/dist/client/settings-section.js +17 -4
- package/data/build/dist/client/subscription-slot.js +21 -8
- package/data/build/dist/client/workspace-session-rightbar-dom.d.ts +28 -0
- package/data/build/dist/client/workspace-session-rightbar-dom.js +291 -0
- package/data/build/dist/dsh-capabilities/client/config-forms-adapter.js +25 -33
- package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.d.ts +7 -1
- package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.js +34 -11
- package/data/build/dist/dsh-capabilities/matrix.js +9 -0
- package/data/build/dist/dsh-capabilities/routes.js +35 -0
- package/data/build/dist/dsh-capabilities/settings-store.d.ts +12 -0
- package/data/build/dist/dsh-capabilities/settings-store.js +34 -0
- package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
- package/data/build/dist/features/host-router.d.ts +26 -0
- package/data/build/dist/features/host-router.js +110 -0
- package/data/build/dist/features/registry.js +3 -0
- package/data/build/dist/host/cli-adapters/codex-driver.d.ts +13 -1
- package/data/build/dist/host/cli-adapters/codex-driver.js +528 -72
- package/data/build/dist/host/cli-adapters/command-code-driver.d.ts +21 -2
- package/data/build/dist/host/cli-adapters/command-code-driver.js +408 -105
- package/data/build/dist/host/cli-adapters/driver.d.ts +2 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts +2 -0
- package/data/build/dist/host/cli-adapters/dsh-message-projector.js +47 -28
- package/data/build/dist/host/cli-adapters/dsh-tool-history.js +37 -0
- package/data/build/dist/host/cli-adapters/feature.js +94 -10
- package/data/build/dist/host/cli-adapters/json-rpc-process.d.ts +6 -0
- package/data/build/dist/host/cli-adapters/json-rpc-process.js +14 -3
- package/data/build/dist/host/cli-adapters/kimi-driver.js +3 -3
- package/data/build/dist/host/cli-adapters/model-catalog.d.ts +6 -0
- package/data/build/dist/host/cli-adapters/model-catalog.js +8 -0
- package/data/build/dist/host/cli-adapters/opencode-driver.js +14 -2
- package/data/build/dist/host/cli-adapters/pi-driver.js +2 -2
- package/data/build/dist/host/cli-adapters/process-utils.d.ts +8 -0
- package/data/build/dist/host/cli-adapters/process-utils.js +48 -0
- package/data/build/dist/host/cli-adapters/registry.d.ts +6 -1
- package/data/build/dist/host/cli-adapters/registry.js +41 -7
- package/data/build/dist/host/cli-adapters/rpc-driver-utils.js +23 -4
- package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +3 -0
- package/data/build/dist/host/cli-adapters/standard-stream-driver.js +36 -15
- package/data/build/dist/host/dsh-device-runtime.d.ts +2 -0
- package/data/build/dist/host/dsh-device-runtime.js +55 -29
- package/data/build/dist/host/dsh-runtime-version.d.ts +3 -2
- package/data/build/dist/host/dsh-runtime-version.js +71 -2
- package/data/build/dist/host/features/auth.js +10 -2
- package/data/build/dist/host/features/file-management.d.ts +5 -0
- package/data/build/dist/host/features/file-management.js +267 -0
- package/data/build/dist/host/features/git-management.js +24 -2
- package/data/build/dist/host/features/index.d.ts +2 -0
- package/data/build/dist/host/features/index.js +6 -0
- package/data/build/dist/host/features/peer-host.d.ts +22 -0
- package/data/build/dist/host/features/peer-host.js +322 -0
- package/data/build/dist/host/features/types.d.ts +4 -0
- package/data/build/dist/host/host-router.d.ts +3 -0
- package/data/build/dist/host/host-router.js +2 -0
- package/data/build/dist/host/index-injection.d.ts +17 -0
- package/data/build/dist/host/index-injection.js +24 -0
- package/data/build/dist/host/index.d.ts +4 -1
- package/data/build/dist/host/index.js +41 -7
- package/data/build/dist/host/lan-access-dsh.d.ts +34 -1
- package/data/build/dist/host/lan-access-dsh.js +145 -26
- package/data/build/dist/host/modules/peer-host/host-api-proxy-service.d.ts +52 -0
- package/data/build/dist/host/modules/peer-host/host-api-proxy-service.js +166 -0
- package/data/build/dist/host/modules/peer-host/host-ws-connector.d.ts +31 -0
- package/data/build/dist/host/modules/peer-host/host-ws-connector.js +108 -0
- package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.d.ts +26 -0
- package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.js +159 -0
- package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.d.ts +50 -0
- package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.js +97 -0
- package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.d.ts +19 -0
- package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.js +54 -0
- package/data/build/dist/host/modules/peer-host/peer-host-handshake.d.ts +44 -0
- package/data/build/dist/host/modules/peer-host/peer-host-handshake.js +133 -0
- package/data/build/dist/host/modules/peer-host/peer-host-relay.d.ts +65 -0
- package/data/build/dist/host/modules/peer-host/peer-host-relay.js +196 -0
- package/data/build/dist/host/modules/peer-host/peer-host-session.d.ts +42 -0
- package/data/build/dist/host/modules/peer-host/peer-host-session.js +152 -0
- package/data/build/dist/host/modules/peer-host/peer-host-store.d.ts +92 -0
- package/data/build/dist/host/modules/peer-host/peer-host-store.js +323 -0
- package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.d.ts +34 -0
- package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.js +156 -0
- package/data/build/dist/host/native-session-bridge.d.ts +18 -2
- package/data/build/dist/host/native-session-bridge.js +236 -8
- package/data/build/dist/host/remote-web-runtime.js +26 -1
- package/data/build/dist/host/rpc.js +8 -3
- package/data/build/dist/host/session-changes.d.ts +5 -0
- package/data/build/dist/host/session-changes.js +163 -0
- package/data/build/dist/host/settings.js +6 -0
- package/data/build/dist/index.d.ts +1 -1
- package/data/build/dist/index.js +1 -1
- package/data/build/dist/shared/contracts/cli-adapter.d.ts +23 -2
- package/data/build/dist/shared/contracts/config.d.ts +15 -0
- package/data/build/dist/shared/contracts/config.js +10 -0
- package/data/build/dist/shared/contracts/dsh-device.d.ts +2 -0
- package/data/build/dist/shared/contracts/errors.d.ts +14 -0
- package/data/build/dist/shared/contracts/errors.js +15 -0
- package/data/build/dist/shared/contracts/feature.d.ts +2 -0
- package/data/build/dist/shared/contracts/file-management.d.ts +6 -0
- package/data/build/dist/shared/contracts/file-management.js +3 -0
- package/data/build/dist/shared/contracts/peer-host.d.ts +116 -15
- package/data/build/dist/shared/contracts/peer-host.js +19 -0
- package/data/build/dist/shared/contracts/version.d.ts +1 -1
- package/data/build/dist/shared/contracts/version.js +1 -1
- package/data/build/dist/shared/index.d.ts +4 -3
- package/data/build/dist/shared/index.js +2 -2
- package/data/build/dist/transport/debug.d.ts +5 -0
- package/data/build/dist/transport/debug.js +23 -1
- package/data/build/dist/transport/dsh-transport.d.ts +2 -0
- package/data/build/dist/transport/dsh-transport.js +11 -0
- package/data/build/dist/transport/webrtc-client.d.ts +2 -0
- package/data/build/dist/transport/webrtc-client.js +2 -1
- package/data/build/dist/transport/webrtc-host.d.ts +2 -0
- package/data/build/dist/transport/webrtc-host.js +6 -3
- package/data/build/dist/typert.host.d.ts +1 -1
- package/data/build/dist/typert.host.js +2 -1
- package/dsh.bundle.patch +10 -0
- package/package.json +19 -11
- package/profile/README.md +2 -2
- package/profile/package.json +2 -2
- package/profile/version.json +1 -1
- package/version.json +1 -1
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { createElement, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { callCodingNsRpc } from './settings-bridge.js';
|
|
3
|
+
import { resolveGitWorkspaceId } from './git-management.js';
|
|
4
|
+
export const SESSION_CHANGED_FILES_VIEW_ID = 'codingns4dsh/session-changed-files';
|
|
5
|
+
/** 会话“修改文件”视图;数据只通过插件 RPC 和现有 Git RPC 读取。 */
|
|
6
|
+
export function SessionChangedFilesView(props) {
|
|
7
|
+
const compactLayout = useCompactLayout();
|
|
8
|
+
const [workspaceId, setWorkspaceId] = useState();
|
|
9
|
+
const [changes, setChanges] = useState([]);
|
|
10
|
+
const [selectedPath, setSelectedPath] = useState();
|
|
11
|
+
const [diff, setDiff] = useState();
|
|
12
|
+
const [collapsed, setCollapsed] = useState(new Set());
|
|
13
|
+
const [hoveredPath, setHoveredPath] = useState();
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const [busy, setBusy] = useState(false);
|
|
16
|
+
const [error, setError] = useState();
|
|
17
|
+
const load = async () => {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
setError(undefined);
|
|
20
|
+
try {
|
|
21
|
+
const resolved = await resolveGitWorkspaceId(props.remote, props.sessionId);
|
|
22
|
+
if (resolved === undefined)
|
|
23
|
+
throw new Error('当前会话没有可用的工作区');
|
|
24
|
+
const [sessionFiles, status] = await Promise.all([
|
|
25
|
+
call(props.rpc, 'fileManagement/session-changes', { sessionId: props.sessionId, workspaceId: resolved }),
|
|
26
|
+
call(props.rpc, 'git/status', { workspaceId: resolved }),
|
|
27
|
+
]);
|
|
28
|
+
const touched = new Set(sessionFiles.paths.map(normalizePath));
|
|
29
|
+
const next = status.changes.filter((item) => touched.has(normalizePath(item.path)) || item.oldPath !== null && touched.has(normalizePath(item.oldPath)));
|
|
30
|
+
setWorkspaceId(resolved);
|
|
31
|
+
setChanges(next);
|
|
32
|
+
props.reportCount?.(props.sessionId, next.length);
|
|
33
|
+
setSelectedPath((current) => current !== undefined && next.some((item) => item.path === current) ? current : next[0]?.path);
|
|
34
|
+
}
|
|
35
|
+
catch (cause) {
|
|
36
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
37
|
+
setChanges([]);
|
|
38
|
+
setWorkspaceId(undefined);
|
|
39
|
+
props.reportCount?.(props.sessionId, 0);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
setDiff(undefined);
|
|
47
|
+
setCollapsed(new Set());
|
|
48
|
+
void load();
|
|
49
|
+
const timer = globalThis.setInterval(() => { void load(); }, 5_000);
|
|
50
|
+
return () => globalThis.clearInterval(timer);
|
|
51
|
+
}, [props.sessionId, props.remote]);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (workspaceId === undefined || selectedPath === undefined) {
|
|
54
|
+
setDiff(undefined);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const selected = changes.find((item) => item.path === selectedPath);
|
|
58
|
+
if (selected === undefined)
|
|
59
|
+
return;
|
|
60
|
+
let cancelled = false;
|
|
61
|
+
void call(props.rpc, 'git/diff', { workspaceId, path: selected.path, staged: selected.staged })
|
|
62
|
+
.then((value) => { if (!cancelled)
|
|
63
|
+
setDiff(value); })
|
|
64
|
+
.catch((cause) => { if (!cancelled)
|
|
65
|
+
setError(cause instanceof Error ? cause.message : String(cause)); });
|
|
66
|
+
return () => { cancelled = true; };
|
|
67
|
+
}, [changes, props.rpc, selectedPath, workspaceId]);
|
|
68
|
+
const tree = useMemo(() => buildTree(changes), [changes]);
|
|
69
|
+
const unstaged = changes.filter((item) => !item.staged);
|
|
70
|
+
const stageTargets = async (targets, action) => {
|
|
71
|
+
if (workspaceId === undefined || targets.length === 0)
|
|
72
|
+
return;
|
|
73
|
+
setBusy(true);
|
|
74
|
+
try {
|
|
75
|
+
await call(props.rpc, `git/${action}`, { workspaceId, targets });
|
|
76
|
+
await load();
|
|
77
|
+
}
|
|
78
|
+
catch (cause) {
|
|
79
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
setBusy(false);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const toggle = (path) => {
|
|
86
|
+
setCollapsed((current) => {
|
|
87
|
+
const next = new Set(current);
|
|
88
|
+
if (next.has(path))
|
|
89
|
+
next.delete(path);
|
|
90
|
+
else
|
|
91
|
+
next.add(path);
|
|
92
|
+
return next;
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
const hasChanges = changes.length > 0;
|
|
96
|
+
const viewRootStyle = { ...rootStyle, padding: compactLayout ? '0 16px' : rootStyle.padding };
|
|
97
|
+
const viewToolbarStyle = {
|
|
98
|
+
...toolbarStyle,
|
|
99
|
+
padding: compactLayout ? '10px 0' : toolbarStyle.padding,
|
|
100
|
+
borderBottom: hasChanges ? toolbarStyle.borderBottom : 'none',
|
|
101
|
+
};
|
|
102
|
+
const viewContentStyle = { ...contentStyle, gridTemplateColumns: compactLayout ? '1fr' : contentStyle.gridTemplateColumns };
|
|
103
|
+
const viewTreePaneStyle = {
|
|
104
|
+
...treePaneStyle,
|
|
105
|
+
borderRight: compactLayout || !hasChanges ? 'none' : treePaneStyle.borderRight,
|
|
106
|
+
borderBottom: compactLayout && hasChanges ? treePaneStyle.borderRight : 'none',
|
|
107
|
+
};
|
|
108
|
+
return createElement('div', { style: viewRootStyle }, createElement('div', { style: viewToolbarStyle }, createElement('strong', { style: { fontSize: 15 } }, '修改文件'), createElement('span', { style: countStyle }, `${changes.length} 个文件`), createElement('span', { style: { flex: 1 } }), createElement('button', { type: 'button', disabled: loading || busy, onClick: () => void load(), style: toolbarRefreshButtonStyle, title: '刷新', 'aria-label': '刷新' }, createElement(RefreshIcon)), createElement('button', { type: 'button', disabled: busy || unstaged.length === 0, onClick: () => void stageTargets(unstaged.map((item) => item.path), 'stage'), style: toolbarStageButtonStyle, title: '全部暂存', 'aria-label': '全部暂存' }, createElement(StageIcon))), error === undefined ? null : createElement('div', { role: 'alert', style: errorStyle }, error), createElement('div', { style: viewContentStyle }, createElement('div', { style: viewTreePaneStyle }, loading ? createElement('div', { style: emptyStyle }, '正在读取会话修改…')
|
|
109
|
+
: changes.length === 0 ? createElement('div', { style: emptyStyle }, '本次会话没有已识别的修改文件')
|
|
110
|
+
: tree.map((node) => renderNode(node, 0, collapsed, hoveredPath, selectedPath, toggle, setSelectedPath, setHoveredPath, stageTargets))), hasChanges ? createElement('div', { style: diffPaneStyle }, selectedPath === undefined ? createElement('div', { style: emptyStyle }, '选择文件查看 Diff')
|
|
111
|
+
: diff?.content ? createElement('pre', { style: diffStyle }, renderDiff(diff.content))
|
|
112
|
+
: createElement('div', { style: emptyStyle }, '当前文件没有可显示的 Diff')) : null));
|
|
113
|
+
}
|
|
114
|
+
/** 注册 DSH 原生 conversation.view Slot;标签由 DSH 根据 Slot 的 id/label 自动投影。 */
|
|
115
|
+
export function registerSessionChangedFilesView(ctx, rpc, remote) {
|
|
116
|
+
const runtime = globalThis;
|
|
117
|
+
// DSH 热重启可能先保留旧 Context 的 Slot;复用旧注册避免 id 冲突。
|
|
118
|
+
if (runtime.__CODINGNS4DSH_SESSION_CHANGED_FILES_VIEW__ !== undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
const value = asRecord(ctx);
|
|
121
|
+
const slots = value?.slots;
|
|
122
|
+
if (typeof slots?.inject !== 'function' || typeof slots.register !== 'function')
|
|
123
|
+
return undefined;
|
|
124
|
+
let disposeSlot;
|
|
125
|
+
let disposeCounterSlot;
|
|
126
|
+
let disposed = false;
|
|
127
|
+
let labelSessionId;
|
|
128
|
+
let labelCount = 0;
|
|
129
|
+
const reportCount = (sessionId, count) => {
|
|
130
|
+
if (disposed || (labelSessionId === sessionId && labelCount === count))
|
|
131
|
+
return;
|
|
132
|
+
labelSessionId = sessionId;
|
|
133
|
+
labelCount = count;
|
|
134
|
+
// DSH 标签的 label 是字符串快照;重新登记同一个 Slot 触发标签列表刷新。
|
|
135
|
+
const previous = disposeSlot;
|
|
136
|
+
disposeSlot = undefined;
|
|
137
|
+
previous?.();
|
|
138
|
+
try {
|
|
139
|
+
disposeSlot = registerSlot();
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.warn('codingns4dsh: 修改文件标签数量刷新失败', error);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
function registerSlot() {
|
|
146
|
+
return slots.inject('conversation.view', () => {
|
|
147
|
+
// 必须通过 SlotRegistry 实例调用 register,保留其 Cordis 调用上下文。
|
|
148
|
+
return slots.register({
|
|
149
|
+
name: 'conversation.view',
|
|
150
|
+
id: SESSION_CHANGED_FILES_VIEW_ID,
|
|
151
|
+
order: 100,
|
|
152
|
+
label: () => `修改文件 ${labelCount}`,
|
|
153
|
+
inject: () => ({ rpc, remote, reportCount }),
|
|
154
|
+
}, SessionChangedFilesView);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function registerCounterSlot() {
|
|
158
|
+
return slots.inject('conversation.session.header.actions', () => slots.register({
|
|
159
|
+
name: 'conversation.session.header.actions',
|
|
160
|
+
id: `${SESSION_CHANGED_FILES_VIEW_ID}/counter`,
|
|
161
|
+
order: 1000,
|
|
162
|
+
inject: () => ({ rpc, remote, reportCount }),
|
|
163
|
+
}, SessionChangedFilesCounter));
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
disposeSlot = registerSlot();
|
|
167
|
+
disposeCounterSlot = registerCounterSlot();
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (isDuplicateRegistrationError(error)) {
|
|
171
|
+
disposeSlot?.();
|
|
172
|
+
disposeCounterSlot?.();
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
disposeSlot?.();
|
|
176
|
+
disposeCounterSlot?.();
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
const dispose = () => {
|
|
180
|
+
disposed = true;
|
|
181
|
+
disposeSlot?.();
|
|
182
|
+
disposeCounterSlot?.();
|
|
183
|
+
if (runtime.__CODINGNS4DSH_SESSION_CHANGED_FILES_VIEW__?.dispose === dispose) {
|
|
184
|
+
delete runtime.__CODINGNS4DSH_SESSION_CHANGED_FILES_VIEW__;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
runtime.__CODINGNS4DSH_SESSION_CHANGED_FILES_VIEW__ = { dispose };
|
|
188
|
+
return dispose;
|
|
189
|
+
}
|
|
190
|
+
function isDuplicateRegistrationError(error) {
|
|
191
|
+
return error instanceof Error && /already registered|已注册/u.test(error.message);
|
|
192
|
+
}
|
|
193
|
+
function SessionChangedFilesCounter(props) {
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
let disposed = false;
|
|
196
|
+
const load = async () => {
|
|
197
|
+
try {
|
|
198
|
+
const workspaceId = await resolveGitWorkspaceId(props.remote, props.sessionId);
|
|
199
|
+
if (workspaceId === undefined)
|
|
200
|
+
throw new Error('当前会话没有可用的工作区');
|
|
201
|
+
const [sessionFiles, status] = await Promise.all([
|
|
202
|
+
call(props.rpc, 'fileManagement/session-changes', { sessionId: props.sessionId, workspaceId }),
|
|
203
|
+
call(props.rpc, 'git/status', { workspaceId }),
|
|
204
|
+
]);
|
|
205
|
+
if (disposed)
|
|
206
|
+
return;
|
|
207
|
+
const touched = new Set(sessionFiles.paths.map(normalizePath));
|
|
208
|
+
const count = status.changes.filter((item) => touched.has(normalizePath(item.path)) || item.oldPath !== null && touched.has(normalizePath(item.oldPath))).length;
|
|
209
|
+
props.reportCount(props.sessionId, count);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
if (!disposed)
|
|
213
|
+
props.reportCount(props.sessionId, 0);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
void load();
|
|
217
|
+
const timer = globalThis.setInterval(() => { void load(); }, 5_000);
|
|
218
|
+
return () => { disposed = true; globalThis.clearInterval(timer); };
|
|
219
|
+
}, [props.remote, props.rpc, props.reportCount, props.sessionId]);
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
function renderDiff(content) {
|
|
223
|
+
const lines = content.split('\n');
|
|
224
|
+
return lines.map((line, index) => createElement('span', {
|
|
225
|
+
key: index,
|
|
226
|
+
style: diffLineStyle(line),
|
|
227
|
+
}, `${line}${index < lines.length - 1 ? '\n' : ''}`));
|
|
228
|
+
}
|
|
229
|
+
function RefreshIcon() {
|
|
230
|
+
return createElement('svg', { width: 18, height: 18, viewBox: '0 0 20 20', fill: 'none', 'aria-hidden': true }, createElement('path', { d: 'M16 8.5A6.2 6.2 0 1 0 16.1 12', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round' }), createElement('path', { d: 'M16 4.5v4h-4', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' }));
|
|
231
|
+
}
|
|
232
|
+
function StageIcon() {
|
|
233
|
+
return createElement('svg', { width: 18, height: 18, viewBox: '0 0 20 20', fill: 'none', 'aria-hidden': true }, createElement('path', { d: 'M4 13.5v2h12v-2', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' }), createElement('path', { d: 'M10 14V4.5m0 0L6.8 7.7M10 4.5l3.2 3.2', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' }));
|
|
234
|
+
}
|
|
235
|
+
function diffLineStyle(line) {
|
|
236
|
+
if (line.startsWith('+++') || line.startsWith('---'))
|
|
237
|
+
return diffHeaderLineStyle;
|
|
238
|
+
if (line.startsWith('+'))
|
|
239
|
+
return diffAddedLineStyle;
|
|
240
|
+
if (line.startsWith('-'))
|
|
241
|
+
return diffRemovedLineStyle;
|
|
242
|
+
if (line.startsWith('@@'))
|
|
243
|
+
return diffHunkLineStyle;
|
|
244
|
+
return diffContextLineStyle;
|
|
245
|
+
}
|
|
246
|
+
function renderNode(node, depth, collapsed, hoveredPath, selectedPath, toggle, select, hover, stageTargets) {
|
|
247
|
+
if (node.kind === 'directory') {
|
|
248
|
+
const expanded = !collapsed.has(node.path);
|
|
249
|
+
const files = flattenFiles(node);
|
|
250
|
+
return createElement('div', { key: `directory:${node.path}` }, createElement('div', { style: rowStyle(depth), onMouseEnter: () => hover(node.path), onMouseLeave: () => hover(undefined) }, createElement('button', { type: 'button', onClick: () => toggle(node.path), style: treeButtonStyle }, createElement('span', { style: treeChevronStyle }, expanded ? '⌄' : '›'), createElement('span', { style: folderIconStyle }, '▰'), createElement('span', { style: fileNameStyle, title: node.path }, node.name), createElement('span', { style: mutedCountStyle }, String(files.length))), hoveredPath === node.path ? createElement('button', { type: 'button', title: '暂存目录', disabled: files.every((item) => item.staged), onClick: () => void stageTargets(files.filter((item) => !item.staged).map((item) => item.path), 'stage'), style: iconButtonStyle }, '+') : null), expanded ? createElement('div', null, node.children.map((child) => renderNode(child, depth + 1, collapsed, hoveredPath, selectedPath, toggle, select, hover, stageTargets))) : null);
|
|
251
|
+
}
|
|
252
|
+
const item = node.change;
|
|
253
|
+
const isHovered = hoveredPath === item.path;
|
|
254
|
+
return createElement('div', { key: `file:${item.path}`, style: { ...rowStyle(depth), ...(selectedPath === item.path ? selectedRowStyle : {}) }, onMouseEnter: () => hover(item.path), onMouseLeave: () => hover(undefined) }, createElement('button', { type: 'button', onClick: () => select(item.path), style: fileButtonStyle }, createElement('span', { style: fileIconStyle }, fileIcon(node.name)), createElement('span', { style: fileNameStyle, title: item.path }, node.name), createElement('span', { style: statusStyle }, item.status)), isHovered ? createElement('span', { style: actionsStyle }, createElement('button', { type: 'button', title: item.staged ? '撤销暂存' : '添加到暂存区', onClick: () => void stageTargets([item.path], item.staged ? 'unstage' : 'stage'), style: iconButtonStyle }, item.staged ? '↶' : '+'), createElement('button', { type: 'button', title: '撤销变更', disabled: item.staged, onClick: () => void stageTargets([item.path], 'discard'), style: dangerButtonStyle }, '×')) : null);
|
|
255
|
+
}
|
|
256
|
+
function buildTree(changes) {
|
|
257
|
+
const root = new Map();
|
|
258
|
+
for (const change of changes) {
|
|
259
|
+
const parts = normalizePath(change.path).split('/').filter(Boolean);
|
|
260
|
+
let entries = root;
|
|
261
|
+
let currentPath = '';
|
|
262
|
+
parts.forEach((part, index) => {
|
|
263
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
264
|
+
const key = index === parts.length - 1 ? `file:${currentPath}` : `directory:${currentPath}`;
|
|
265
|
+
if (index === parts.length - 1)
|
|
266
|
+
entries.set(key, { kind: 'file', name: part, change });
|
|
267
|
+
else {
|
|
268
|
+
const existing = entries.get(key);
|
|
269
|
+
const directory = existing?.kind === 'directory'
|
|
270
|
+
? existing
|
|
271
|
+
: { kind: 'directory', name: part, path: currentPath, children: new Map() };
|
|
272
|
+
entries.set(key, directory);
|
|
273
|
+
entries = directory.children;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return sortNodes(finalizeTree([...root.values()]));
|
|
278
|
+
}
|
|
279
|
+
function finalizeTree(nodes) {
|
|
280
|
+
return nodes.map((node) => node.kind === 'directory'
|
|
281
|
+
? { kind: 'directory', name: node.name, path: node.path, children: finalizeTree([...node.children.values()]) }
|
|
282
|
+
: node);
|
|
283
|
+
}
|
|
284
|
+
function sortNodes(nodes) {
|
|
285
|
+
return [...nodes].sort((left, right) => left.kind === right.kind ? left.name.localeCompare(right.name, 'zh-CN') : left.kind === 'directory' ? -1 : 1).map((node) => node.kind === 'directory' ? { ...node, children: sortNodes(node.children) } : node);
|
|
286
|
+
}
|
|
287
|
+
function flattenFiles(node) {
|
|
288
|
+
return node.children.flatMap((child) => child.kind === 'directory' ? flattenFiles(child) : [child.change]);
|
|
289
|
+
}
|
|
290
|
+
function normalizePath(value) { return value.replaceAll('\\', '/').replace(/^\.\//u, '').replace(/^\/+|\/+$/gu, ''); }
|
|
291
|
+
function fileIcon(name) { return name.includes('.') ? '·' : '□'; }
|
|
292
|
+
function asRecord(value) { return typeof value === 'object' && value !== null ? value : undefined; }
|
|
293
|
+
async function call(rpc, endpoint, payload) { return await callCodingNsRpc(rpc, endpoint, payload); }
|
|
294
|
+
function useCompactLayout() {
|
|
295
|
+
const [compact, setCompact] = useState(() => typeof window !== 'undefined' && window.matchMedia?.('(max-width: 700px)').matches === true);
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function')
|
|
298
|
+
return;
|
|
299
|
+
const media = window.matchMedia('(max-width: 700px)');
|
|
300
|
+
const update = () => setCompact(media.matches);
|
|
301
|
+
update();
|
|
302
|
+
media.addEventListener?.('change', update);
|
|
303
|
+
return () => media.removeEventListener?.('change', update);
|
|
304
|
+
}, []);
|
|
305
|
+
return compact;
|
|
306
|
+
}
|
|
307
|
+
const rootStyle = { display: 'flex', flexDirection: 'column', alignItems: 'center', height: 'auto', minHeight: '100%', overflow: 'visible', padding: '0 64px', boxSizing: 'border-box', color: 'var(--dsw-alias-label-primary,inherit)', background: 'var(--dsw-alias-bg-base,transparent)', fontSize: 13 };
|
|
308
|
+
const toolbarStyle = { display: 'flex', alignItems: 'center', gap: 8, width: '100%', maxWidth: 1280, boxSizing: 'border-box', padding: '12px 16px', borderBottom: '1px solid var(--dsw-alias-border-l3,#ddd)', flex: '0 0 auto' };
|
|
309
|
+
const countStyle = { color: 'var(--dsw-alias-label-tertiary,#777)' };
|
|
310
|
+
const contentStyle = { display: 'grid', gridTemplateColumns: '3.5fr 6.5fr', width: '100%', maxWidth: 1280, flex: '0 0 auto', minHeight: 0 };
|
|
311
|
+
const treePaneStyle = { padding: '8px 0', borderRight: '1px solid var(--dsw-alias-border-l3,#ddd)' };
|
|
312
|
+
const diffPaneStyle = { minWidth: 0, background: 'var(--dsw-alias-bg-layer-1,transparent)' };
|
|
313
|
+
const rowStyle = (depth) => ({ display: 'flex', alignItems: 'center', gap: 7, minHeight: 28, padding: `0 8px 0 ${8 + depth * 14}px`, fontSize: 12 });
|
|
314
|
+
const treeButtonStyle = { display: 'flex', alignItems: 'center', gap: 7, width: '100%', minWidth: 0, minHeight: 28, border: 0, background: 'transparent', color: 'inherit', font: 'inherit', cursor: 'pointer', textAlign: 'left', padding: 0, fontWeight: 600 };
|
|
315
|
+
const fileButtonStyle = { display: 'flex', alignItems: 'center', gap: 7, border: 0, background: 'transparent', color: 'inherit', font: 'inherit', cursor: 'pointer', textAlign: 'left', flex: 1, minWidth: 0, padding: 0 };
|
|
316
|
+
const selectedRowStyle = { background: 'var(--dsw-alias-interactive-bg-selected,rgba(80,120,200,.16))' };
|
|
317
|
+
const fileIconStyle = { width: 12, color: 'var(--dsw-alias-label-tertiary,#777)', fontSize: 12, textAlign: 'center', flex: '0 0 12px' };
|
|
318
|
+
const treeChevronStyle = { width: 12, color: 'var(--dsw-alias-label-tertiary,#777)', fontSize: 12, flex: '0 0 12px' };
|
|
319
|
+
const folderIconStyle = { color: 'var(--dsw-alias-state-business-primary,#356ae6)', fontSize: 11, flex: '0 0 auto' };
|
|
320
|
+
const fileNameStyle = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 };
|
|
321
|
+
const statusStyle = { color: 'var(--dsw-alias-label-tertiary,#777)', flex: '0 0 auto', fontWeight: 700 };
|
|
322
|
+
const mutedCountStyle = { color: 'var(--dsw-alias-label-tertiary,#777)', fontVariantNumeric: 'tabular-nums', flex: '0 0 auto' };
|
|
323
|
+
const actionsStyle = { display: 'inline-flex', gap: 2, flex: '0 0 auto' };
|
|
324
|
+
const iconButtonStyle = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 24, height: 24, border: 0, borderRadius: 4, padding: 0, background: 'transparent', color: 'var(--dsw-alias-label-secondary,#777)', cursor: 'pointer', fontSize: 16 };
|
|
325
|
+
const dangerButtonStyle = { ...iconButtonStyle, color: 'var(--dsw-alias-state-danger,#c43d3d)' };
|
|
326
|
+
const toolbarIconButtonStyle = { ...iconButtonStyle, width: 36, height: 36, border: '1px solid var(--dsw-alias-border-l2,#ccc)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08))', boxShadow: '0 1px 2px rgba(0,0,0,.12)', fontSize: 18 };
|
|
327
|
+
const toolbarRefreshButtonStyle = { ...toolbarIconButtonStyle, color: 'var(--dsw-alias-state-business-primary,#356ae6)' };
|
|
328
|
+
const toolbarStageButtonStyle = { ...toolbarIconButtonStyle, color: 'var(--dsw-alias-state-success,#18864b)' };
|
|
329
|
+
const emptyStyle = { padding: 24, color: 'var(--dsw-alias-label-tertiary,#777)', textAlign: 'center' };
|
|
330
|
+
const errorStyle = { padding: '8px 16px', color: 'var(--dsw-alias-state-danger,#c43d3d)', borderBottom: '1px solid var(--dsw-alias-border-l3,#ddd)' };
|
|
331
|
+
const diffStyle = { margin: 0, padding: 16, minHeight: '100%', overflow: 'visible', whiteSpace: 'pre-wrap', wordBreak: 'break-word', font: '12px/1.55 var(--dsw-font-mono,ui-monospace,monospace)' };
|
|
332
|
+
const diffHeaderLineStyle = { display: 'block', color: 'var(--dsw-alias-label-tertiary,#777)' };
|
|
333
|
+
const diffHunkLineStyle = { display: 'block', color: 'var(--dsw-alias-state-business-primary,#356ae6)', background: 'rgba(53,106,230,.08)' };
|
|
334
|
+
const diffAddedLineStyle = { display: 'block', color: '#137333', background: 'rgba(34,197,94,.12)' };
|
|
335
|
+
const diffRemovedLineStyle = { display: 'block', color: '#b42318', background: 'rgba(220,38,38,.12)' };
|
|
336
|
+
const diffContextLineStyle = { display: 'block' };
|
|
337
|
+
//# sourceMappingURL=session-changed-files-view.js.map
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
2
2
|
import type { CodingNsSettings } from '../shared/contracts/config.js';
|
|
3
3
|
import type { CodingNsRpcClient } from './features/types.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type CodingNsSettingsSnapshot, type CodingNsSettingsStore } from '../dsh-capabilities/settings-store.js';
|
|
5
5
|
type SettingsMutation = Parameters<SettingsScope<CodingNsSettings>['mutate']>[0];
|
|
6
6
|
type SnapshotListener = () => void;
|
|
7
7
|
/**
|
|
8
8
|
* 把 DSH 本地设置和远程 Host 设置 RPC 统一成一个设置作用域。
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
|
+
* 没有本地镜像时(`local` 为 undefined)完全使用 Host RPC:DSH 只向回环页面或
|
|
11
|
+
* 声明了 Host 所有权的页面下发持久设置,非回环页面会降级为 memory 模式,此时
|
|
12
|
+
* 插件设置必须走自己的 Host 写入边界才能保持可读写。
|
|
10
13
|
*/
|
|
11
14
|
export declare class CodingNsSettingsBridge implements CodingNsSettingsStore<CodingNsSettings> {
|
|
12
15
|
private readonly local;
|
|
@@ -16,7 +19,7 @@ export declare class CodingNsSettingsBridge implements CodingNsSettingsStore<Cod
|
|
|
16
19
|
private readonly localUnsubscribe;
|
|
17
20
|
private remoteLoad;
|
|
18
21
|
private remoteLoaded;
|
|
19
|
-
constructor(local: SettingsScope<CodingNsSettings
|
|
22
|
+
constructor(local: SettingsScope<CodingNsSettings> | undefined, rpc: CodingNsRpcClient);
|
|
20
23
|
getSnapshot(): CodingNsSettingsSnapshot<CodingNsSettings>;
|
|
21
24
|
subscribe(listener: SnapshotListener): () => void;
|
|
22
25
|
load(): Promise<void>;
|
|
@@ -25,11 +28,18 @@ export declare class CodingNsSettingsBridge implements CodingNsSettingsStore<Cod
|
|
|
25
28
|
mutate(ops: SettingsMutation, expectedRevision?: number): Promise<boolean>;
|
|
26
29
|
dispose(): void;
|
|
27
30
|
private call;
|
|
31
|
+
private localSnapshot;
|
|
32
|
+
/**
|
|
33
|
+
* 内容未变化时不替换快照引用、不唤醒订阅者。
|
|
34
|
+
*
|
|
35
|
+
* DSH 的本地设置镜像会在后台索引(例如 cliSessions 心跳)更新时反复通知;
|
|
36
|
+
* 每次都发布新对象会让设置页持续重渲染,并使按快照身份触发的 effect 反复执行。
|
|
37
|
+
*/
|
|
28
38
|
private publish;
|
|
29
39
|
private isRemote;
|
|
30
40
|
}
|
|
31
41
|
/** 调用 Codingns4DSH Host RPC,并兼容 DSH 原生连接与普通 Web API 回退。 */
|
|
32
42
|
export declare function callCodingNsRpc<T>(rpc: CodingNsRpcClient, endpoint: string, payload: unknown): Promise<T>;
|
|
33
|
-
export declare function createCodingNsSettingsBridge(local: SettingsScope<CodingNsSettings
|
|
43
|
+
export declare function createCodingNsSettingsBridge(local: SettingsScope<CodingNsSettings> | undefined, rpc: CodingNsRpcClient): CodingNsSettingsBridge;
|
|
34
44
|
export {};
|
|
35
45
|
//# sourceMappingURL=settings-bridge.d.ts.map
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { CODINGNS_RPC_CHANNEL } from '../shared/contracts/transport.js';
|
|
2
2
|
import { debugInfo, debugWarn } from '../shared/debug.js';
|
|
3
|
+
import { sameSettingsSnapshot, } from '../dsh-capabilities/settings-store.js';
|
|
4
|
+
/**
|
|
5
|
+
* DSH 未提供设置镜像(memory 模式、命名空间未下发或版本缺少 ConfigForm)时的占位快照。
|
|
6
|
+
*
|
|
7
|
+
* 它必须是稳定引用:设置页通过 `useSyncExternalStore` 读取快照,每次返回新对象
|
|
8
|
+
* 会让 React 在每次渲染后都判定快照失效并强制再次渲染,最终以 React #185 崩溃整
|
|
9
|
+
* 个设置分区。
|
|
10
|
+
*/
|
|
11
|
+
const UNAVAILABLE_LOCAL_SNAPSHOT = Object.freeze({
|
|
12
|
+
status: 'unavailable',
|
|
13
|
+
value: undefined,
|
|
14
|
+
revision: undefined,
|
|
15
|
+
writable: false,
|
|
16
|
+
});
|
|
3
17
|
/**
|
|
4
18
|
* 把 DSH 本地设置和远程 Host 设置 RPC 统一成一个设置作用域。
|
|
5
|
-
*
|
|
19
|
+
*
|
|
20
|
+
* 没有本地镜像时(`local` 为 undefined)完全使用 Host RPC:DSH 只向回环页面或
|
|
21
|
+
* 声明了 Host 所有权的页面下发持久设置,非回环页面会降级为 memory 模式,此时
|
|
22
|
+
* 插件设置必须走自己的 Host 写入边界才能保持可读写。
|
|
6
23
|
*/
|
|
7
24
|
export class CodingNsSettingsBridge {
|
|
8
25
|
local;
|
|
@@ -15,15 +32,15 @@ export class CodingNsSettingsBridge {
|
|
|
15
32
|
constructor(local, rpc) {
|
|
16
33
|
this.local = local;
|
|
17
34
|
this.rpc = rpc;
|
|
18
|
-
this.snapshot = toStoreSnapshot(local
|
|
19
|
-
this.localUnsubscribe = local
|
|
35
|
+
this.snapshot = toStoreSnapshot(local?.getSnapshot() ?? UNAVAILABLE_LOCAL_SNAPSHOT);
|
|
36
|
+
this.localUnsubscribe = local?.subscribe(() => {
|
|
20
37
|
if (this.isRemote()) {
|
|
21
38
|
void this.load().catch(() => undefined);
|
|
22
39
|
return;
|
|
23
40
|
}
|
|
24
41
|
this.remoteLoaded = false;
|
|
25
|
-
this.publish(toStoreSnapshot(
|
|
26
|
-
});
|
|
42
|
+
this.publish(toStoreSnapshot(this.localSnapshot()));
|
|
43
|
+
}) ?? (() => undefined);
|
|
27
44
|
}
|
|
28
45
|
getSnapshot() { return this.snapshot; }
|
|
29
46
|
subscribe(listener) {
|
|
@@ -53,22 +70,25 @@ export class CodingNsSettingsBridge {
|
|
|
53
70
|
return this.remoteLoad;
|
|
54
71
|
}
|
|
55
72
|
async set(field, value) {
|
|
56
|
-
|
|
57
|
-
|
|
73
|
+
const local = this.local;
|
|
74
|
+
if (local !== undefined && !this.isRemote()) {
|
|
75
|
+
await local.set(field, value);
|
|
58
76
|
return true;
|
|
59
77
|
}
|
|
60
78
|
return this.mutate([{ op: 'set', path: [field], value: toJsonValue(value) }]);
|
|
61
79
|
}
|
|
62
80
|
async unset(field) {
|
|
63
|
-
|
|
64
|
-
|
|
81
|
+
const local = this.local;
|
|
82
|
+
if (local !== undefined && !this.isRemote()) {
|
|
83
|
+
await local.unset(field);
|
|
65
84
|
return true;
|
|
66
85
|
}
|
|
67
86
|
return this.mutate([{ op: 'unset', path: [field] }]);
|
|
68
87
|
}
|
|
69
88
|
async mutate(ops, expectedRevision) {
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
const local = this.local;
|
|
90
|
+
if (local !== undefined && !this.isRemote()) {
|
|
91
|
+
await local.mutate(ops, expectedRevision);
|
|
72
92
|
return true;
|
|
73
93
|
}
|
|
74
94
|
const payload = expectedRevision === undefined ? { ops } : { ops, expectedRevision };
|
|
@@ -87,16 +107,43 @@ export class CodingNsSettingsBridge {
|
|
|
87
107
|
async call(endpoint, payload) {
|
|
88
108
|
return callCodingNsRpc(this.rpc, endpoint, payload);
|
|
89
109
|
}
|
|
110
|
+
localSnapshot() {
|
|
111
|
+
return this.local?.getSnapshot() ?? UNAVAILABLE_LOCAL_SNAPSHOT;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 内容未变化时不替换快照引用、不唤醒订阅者。
|
|
115
|
+
*
|
|
116
|
+
* DSH 的本地设置镜像会在后台索引(例如 cliSessions 心跳)更新时反复通知;
|
|
117
|
+
* 每次都发布新对象会让设置页持续重渲染,并使按快照身份触发的 effect 反复执行。
|
|
118
|
+
*/
|
|
90
119
|
publish(next) {
|
|
120
|
+
if (sameSettingsSnapshot(this.snapshot, next))
|
|
121
|
+
return;
|
|
91
122
|
this.snapshot = next;
|
|
92
123
|
for (const listener of [...this.listeners])
|
|
93
124
|
listener();
|
|
94
125
|
}
|
|
95
126
|
isRemote() {
|
|
127
|
+
if (this.local === undefined)
|
|
128
|
+
return true;
|
|
96
129
|
const snapshot = this.local.getSnapshot();
|
|
97
|
-
|
|
130
|
+
// 远程 DSH Web iframe 内的 settingsScope 属于被访问的 DSH Web Host;
|
|
131
|
+
// CodingNS 插件设置仍归外层 CodingNS Host 所有,不能误写入 DSH Web 的本地缓存。
|
|
132
|
+
return isRemoteWebContext() || snapshot.mode === 'memory' || snapshot.status === 'unavailable'
|
|
133
|
+
// 某些 DSH 版本只向 Client 暴露只读设置镜像,但当前 Host 仍提供
|
|
134
|
+
// Codingns4DSH 自有 settings/set RPC。此时必须切换到 Host 写入边界,
|
|
135
|
+
// 否则设置页会永久把所有模块开关显示为不可操作。
|
|
136
|
+
|| snapshot.writable === false;
|
|
98
137
|
}
|
|
99
138
|
}
|
|
139
|
+
function isRemoteWebContext() {
|
|
140
|
+
const runtime = globalThis;
|
|
141
|
+
// 中转 iframe 有显式标记;局域网代理直接打开 DSH Web 时没有 iframe,
|
|
142
|
+
// 但同样属于浏览器 Host,插件设置仍应通过 CodingNS Host RPC 读取。
|
|
143
|
+
return runtime.__CODINGNS4DSH_REMOTE_WEB_CONTEXT__ === true
|
|
144
|
+
|| runtime.location?.protocol === 'http:'
|
|
145
|
+
|| runtime.location?.protocol === 'https:';
|
|
146
|
+
}
|
|
100
147
|
/** 调用 Codingns4DSH Host RPC,并兼容 DSH 原生连接与普通 Web API 回退。 */
|
|
101
148
|
export async function callCodingNsRpc(rpc, endpoint, payload) {
|
|
102
149
|
let result;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement, useEffect, useState, useSyncExternalStore } from 'react';
|
|
1
|
+
import { createElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
|
2
2
|
import { CODINGNS_MODULES_FIELD, isFeatureEnabled, isFeatureDshVersionCompatible, } from '../shared/contracts/config.js';
|
|
3
3
|
import { settingsModules } from './features/index.js';
|
|
4
4
|
import { dshSettingsBodyStyle, dshSettingsCardStyle, dshSettingsHeaderStyle, dshSettingsPageStyle, dshSettingsSubtitleStyle, dshSettingsSummaryDescriptionStyle, dshSettingsSummaryLabelStyle, dshSettingsSummaryStyle, dshSettingsSummaryTextStyle, dshSettingsTitleStyle, dshSettingsToastStyle, dshThemeColor, } from './theme.js';
|
|
@@ -12,7 +12,10 @@ const CODINGNS_GITHUB_URL = 'https://github.com/jingyi0605/Codingns4DSH';
|
|
|
12
12
|
* 自己的 settingsPanel,所以新增模块不会在这里产生分支。
|
|
13
13
|
*/
|
|
14
14
|
export function CodingNsSettingsSection({ settings, registry, services, restartStates = {} }) {
|
|
15
|
-
|
|
15
|
+
// useSyncExternalStore 要求 subscribe/getSnapshot 保持稳定引用,否则 React
|
|
16
|
+
// 会在每次渲染后重新订阅并强制再次渲染整个设置分区。
|
|
17
|
+
const store = useMemo(() => settingsSnapshotHandle(settings), [settings]);
|
|
18
|
+
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
16
19
|
const t = useCodingNsTranslator(services.locale);
|
|
17
20
|
const [toast, setToast] = useState(null);
|
|
18
21
|
useEffect(() => {
|
|
@@ -35,16 +38,24 @@ export function CodingNsSettingsSection({ settings, registry, services, restartS
|
|
|
35
38
|
notify,
|
|
36
39
|
}))), createElement('details', { style: { alignSelf: 'center', display: 'flex', flexDirection: 'column-reverse', alignItems: 'center', marginTop: 4, color: dshThemeColor.labelTertiary, textAlign: 'center', fontSize: 12, lineHeight: 1.5 } }, createElement('summary', { style: { cursor: 'pointer', color: dshThemeColor.labelSecondary, listStylePosition: 'inside' } }, t('settings.version', { version: CODINGNS_VERSION })), createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'center', maxWidth: 'min(100%, 560px)', marginBottom: 8, padding: '8px 12px', border: `1px solid ${dshThemeColor.border}`, borderRadius: 6, background: dshThemeColor.surfaceSubtle } }, createElement('div', undefined, t('settings.compatibility', { range: DSH_COMPATIBILITY })), createElement('a', { href: CODINGNS_GITHUB_URL, target: '_blank', rel: 'noreferrer', style: { color: dshThemeColor.accent, overflowWrap: 'anywhere' } }, CODINGNS_GITHUB_URL))));
|
|
37
40
|
}
|
|
41
|
+
/** 稳定引用包装:设置页只依赖内部 Store 契约,不感知 DSH 侧实现。 */
|
|
42
|
+
function settingsSnapshotHandle(settings) {
|
|
43
|
+
return {
|
|
44
|
+
subscribe: (listener) => settings.subscribe(listener),
|
|
45
|
+
getSnapshot: () => settings.getSnapshot(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
38
48
|
/** 通用功能模块卡片:标题栏开关由 descriptor.ui 决定,内容由模块自己提供。 */
|
|
39
49
|
function FeatureCard({ entry, snapshot, services, restartStates, notify }) {
|
|
40
50
|
const { module, ui } = entry;
|
|
41
51
|
const t = useCodingNsTranslator(services.locale);
|
|
42
52
|
const versionCompatible = isFeatureDshVersionCompatible(module.descriptor, services.dshVersion);
|
|
43
53
|
const requestedEnabled = isFeatureEnabled(module.descriptor, snapshot.value);
|
|
44
|
-
const
|
|
54
|
+
const temporarilyDisabled = module.descriptor.disabled === true;
|
|
55
|
+
const enabled = !temporarilyDisabled && versionCompatible && requestedEnabled;
|
|
45
56
|
const panel = module.settingsPanel;
|
|
46
57
|
// 常驻模块不提供关闭入口;设置未就绪或只读时也不允许切换。
|
|
47
|
-
const switchDisabled = ui.alwaysEnabled === true || !versionCompatible || snapshot.status === 'loading' || !snapshot.writable;
|
|
58
|
+
const switchDisabled = temporarilyDisabled || ui.alwaysEnabled === true || !versionCompatible || snapshot.status === 'loading' || !snapshot.writable;
|
|
48
59
|
const toggle = (next) => {
|
|
49
60
|
if (!versionCompatible) {
|
|
50
61
|
notify({ kind: 'error', message: t('settings.versionBlocked', {
|
|
@@ -83,6 +94,8 @@ function FeatureCard({ entry, snapshot, services, restartStates, notify }) {
|
|
|
83
94
|
version: services.dshVersion,
|
|
84
95
|
minimum: module.descriptor.minimumDshVersion ?? '未知版本',
|
|
85
96
|
}))
|
|
97
|
+
: null, temporarilyDisabled
|
|
98
|
+
? createElement('div', { role: 'status', style: { marginBottom: 10, color: dshThemeColor.labelSecondary } }, '该模块当前由维护策略停用,设置不会启动它。')
|
|
86
99
|
: null, panel === undefined ? null : createElement(panel, { services, enabled, snapshot, notify })));
|
|
87
100
|
}
|
|
88
101
|
/** 标题栏开关:真实 checkbox 语义,点击不会连带折叠卡片。 */
|