@gatherthread/dsh-host 0.1.0-alpha.8 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -21
- package/bundle/manifest.json +5 -5
- package/client/client.js +160 -45
- package/dist/bundle/native-plugin.js +2493 -312
- package/dist/bundle/plugin.js +1541 -109
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,43 +7,45 @@ installation, GatherThread does not load in DSH or alter the GatherThread
|
|
|
7
7
|
server, Web app, Codex bridge, or Claude adapter.
|
|
8
8
|
|
|
9
9
|
The normal path is the published `@deepseek-ai/dsh` CLI, not a DSH source
|
|
10
|
-
checkout. The verified distribution is `@deepseek-ai/dsh@0.1.
|
|
11
|
-
`web` profile and its public `sessionPersistence
|
|
12
|
-
|
|
13
|
-
`
|
|
10
|
+
checkout. The verified distribution is `@deepseek-ai/dsh@0.1.7-rc.2` with the
|
|
11
|
+
`web` profile and its public `sessionPersistence` recovery surface. The
|
|
12
|
+
source-only path remains fixed to `dsh-v0.1.7-rc.2`, commit
|
|
13
|
+
`477b4f420553e8a52c2fbccc464d7561b239c443`, and is only an advanced diagnostic
|
|
14
14
|
fallback. All version-sensitive Host calls remain inside `src/dsh-compat.ts`.
|
|
15
15
|
The package does not install a DSH runtime dependency.
|
|
16
16
|
|
|
17
17
|
## Plugin-first connection
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Use **Connect DeepSeek Harness → Download packages for this server** in GatherThread to download its source-matched DSH plugin TGZ. Replace the example path below with the downloaded file's full path:
|
|
20
20
|
|
|
21
21
|
```text
|
|
22
|
-
npx @deepseek-ai/dsh@0.1.
|
|
23
|
-
npx @deepseek-ai/dsh@0.1.
|
|
22
|
+
npx @deepseek-ai/dsh@0.1.7-rc.2 plugin --profile web add "/absolute/path/to/LOCAL_DSH_PLUGIN.tgz"
|
|
23
|
+
npx @deepseek-ai/dsh@0.1.7-rc.2 web
|
|
24
24
|
# In DSH: Settings -> GatherThread / 共序 -> Sign in and pair
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
On Windows, use a quoted path such as `C:/Users/you/Downloads/your-plugin.tgz`. The registry's `@gatherthread/dsh-host@0.1.0-beta.1` is an older build and does not include the current message-deletion safeguards. If server downloads are unavailable, [build the matching source checkout](../../docs/DSH_CONNECT.md#source-checkout-fallback); do not substitute the old npm build.
|
|
28
|
+
|
|
27
29
|
Check the active CLI with `npx @deepseek-ai/dsh --version`. The normal start
|
|
28
|
-
command already pins `0.1.
|
|
30
|
+
command already pins `0.1.7-rc.2`. The pinned DSH does not
|
|
29
31
|
expose the root CLI version as a stable Host service, so the package does not
|
|
30
32
|
inspect npx cache paths or claim runtime-version introspection. The install
|
|
31
33
|
command pins the exact verified CLI, while every required Host/Client service is
|
|
32
34
|
validated structurally and fails closed before a request can be claimed.
|
|
33
35
|
|
|
34
|
-
`dsh plugin --profile web add` is DSH
|
|
36
|
+
`dsh plugin --profile web add` is the pinned DSH release's official package installation
|
|
35
37
|
mechanism. It initializes a missing `web` profile on first use, invokes pnpm,
|
|
36
|
-
and appends only packages declaring `dsh.bundle`. DSH
|
|
38
|
+
and appends only packages declaring `dsh.bundle`. The pinned DSH has no verified
|
|
37
39
|
public plugin marketplace, marketplace index, or registration workflow. This
|
|
38
40
|
repository change prepares the package but does not publish it or modify a real
|
|
39
41
|
DSH profile.
|
|
40
42
|
|
|
41
|
-
Inside DSH, choose the
|
|
43
|
+
Inside DSH, choose the GatherThread public service or enter one custom
|
|
42
44
|
server origin. The same field covers an HTTPS LAN host, self-hosted deployment,
|
|
43
45
|
or Tailscale address; plain HTTP is accepted only on loopback. The plugin opens
|
|
44
46
|
an outbound HTTPS connection. A five-minute, single-use short code is confirmed
|
|
45
|
-
through
|
|
46
|
-
|
|
47
|
+
through a signed-in GatherThread email account. Project invitations grant
|
|
48
|
+
membership only and cannot authorize pairing. Signup is closed by default. The resulting long-lived device
|
|
47
49
|
grant is stored only by DSH's credential service and never appears in argv, a
|
|
48
50
|
URL, browser storage, logs, status RPC, or shared history. Once paired, the
|
|
49
51
|
plugin reconnects automatically whenever DSH starts.
|
|
@@ -52,7 +54,7 @@ Before removal, use **Disconnect** inside the DSH panel to clear the local
|
|
|
52
54
|
grant, then run:
|
|
53
55
|
|
|
54
56
|
```text
|
|
55
|
-
npx @deepseek-ai/dsh@0.1.
|
|
57
|
+
npx @deepseek-ai/dsh@0.1.7-rc.2 plugin --profile web remove @gatherthread/dsh-host
|
|
56
58
|
```
|
|
57
59
|
|
|
58
60
|
The official command removes only this package dependency and profile layer;
|
|
@@ -60,12 +62,23 @@ other plugins remain untouched. The repository-level `npm run dsh:connect --
|
|
|
60
62
|
--help` source-checkout connector and the examples under `bundle/` remain
|
|
61
63
|
advanced/offline diagnostics, not the normal user path.
|
|
62
64
|
|
|
65
|
+
## GitHub code synchronization
|
|
66
|
+
|
|
67
|
+
Beta 2 includes direct GitHub controls and requires matching server, Web app and native plugin releases. The project owner first connects an existing repository and base branch in Web. Install Git 2.38+ and GitHub CLI on the device, then open **Settings → GatherThread / 共序 → 项目代码 · GitHub(推荐)**. Click **连接 GitHub · 打开浏览器** to start local browser sign-in if needed, then authorize that exact project/repository configuration. `gh auth login --hostname github.com --git-protocol https --web` remains a local fallback. This consent is separate from GT Cloud and conversation sync; a new GitHub binding automatically uploads settled changes while idle and can be switched off. Configuration changes, including pause/resume, require renewed local authorization.
|
|
68
|
+
|
|
69
|
+
The Web app may request code operations only on your selected online runtime and cannot grant this local consent. Code and GitHub credentials travel directly to GitHub. GitHub permissions remain independent of GT membership; removing a GT member or project does not revoke GitHub access or delete GitHub data. See [Project code collaboration](../../docs/CODE_SYNC.md) for setup, recovery, limits and GitHub workflow-trigger risks.
|
|
70
|
+
|
|
71
|
+
GitHub 接入仍为**未发布源码预览**,需使用匹配的服务端、网页与原生插件源码构建,不能假定固定版本 npm 包已支持。在网页配置现有仓库后,每台设备安装 Git 2.38+ 与 GitHub CLI,再进入 **Settings → GatherThread / 共序 → 项目代码 · GitHub(推荐)**,点击“连接 GitHub · 打开浏览器”完成本机登录,并核对、逐项目授权。终端命令仍可作为保底。GitHub、GT Cloud 和会话同步分别授权;新 GitHub 绑定在 Agent 空闲时会自动上传稳定改动,可关闭且重连后保持关闭。配置变化及暂停后恢复需重新本地授权。网页不能替代本地文件授权,共序成员变化或项目删除不会撤销 GitHub 权限或删除 GitHub 数据。完整流程见[项目代码协作文档](../../docs/CODE_SYNC.md)。
|
|
72
|
+
|
|
63
73
|
## Security and recovery
|
|
64
74
|
|
|
65
|
-
Only allowlisted public text/tool fields leave DSH.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
Only allowlisted public text/tool fields leave DSH. An exact GatherThread-claimed
|
|
76
|
+
prompt can additionally stream native-exposed thinking and real plans. The live
|
|
77
|
+
listener verifies owned Agent, Session and execution; the npm fallback requires
|
|
78
|
+
DSH's genuine Agent-loop marker rather than a model-name match. Raw request
|
|
79
|
+
bodies, headers, replay state, tool-private metadata and unrelated native streams
|
|
80
|
+
stay outside the mapper. Tool arguments are parsed and redacted before upload;
|
|
81
|
+
incomplete JSON emits only a preparation marker. The connector holds one active request, persists a private atomic
|
|
69
82
|
cursor/outbox, reuses DSH's official Session resume/repair path, and releases its
|
|
70
83
|
listeners, timer, HTTP abort signal, Agent handle, and write ownership on unload.
|
|
71
84
|
|
|
@@ -106,9 +119,10 @@ cancels its queued permit before it can claim a request. Existing single-Session
|
|
|
106
119
|
valid when `bindingMode` is omitted; both modes resolve the real current actor
|
|
107
120
|
before applying Solo permissions.
|
|
108
121
|
|
|
109
|
-
During an active prompt,
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
During an active claimed prompt, selected native-visible answer, thinking, tool
|
|
123
|
+
and plan updates coalesce into bounded redacted snapshots. Generic activity
|
|
124
|
+
markers carry no native content and remain separate from presence. Accepted
|
|
125
|
+
progress renews the exact claim attempt. Progress, completion,
|
|
112
126
|
and request-linked tool outbox entries are rebound to the renewed attempt after a
|
|
113
127
|
crash recovery, while stale attempts remain fenced by the server.
|
|
114
128
|
|
package/bundle/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"name": "@gatherthread/dsh-host",
|
|
4
|
-
"version": "0.1.0-
|
|
4
|
+
"version": "0.1.0-beta.2",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"defaultEnabled": false,
|
|
7
7
|
"hostEntry": "@gatherthread/dsh-host",
|
|
@@ -15,15 +15,15 @@
|
|
|
15
15
|
"primary": {
|
|
16
16
|
"distribution": "npm",
|
|
17
17
|
"package": "@deepseek-ai/dsh",
|
|
18
|
-
"version": "0.1.
|
|
18
|
+
"version": "0.1.7-rc.2",
|
|
19
19
|
"profile": "web",
|
|
20
20
|
"persistenceProbe": "list"
|
|
21
21
|
},
|
|
22
22
|
"sourceFallback": {
|
|
23
23
|
"distribution": "source",
|
|
24
|
-
"tag": "dsh-v0.1.
|
|
25
|
-
"version": "0.1.
|
|
26
|
-
"commit": "
|
|
24
|
+
"tag": "dsh-v0.1.7-rc.2",
|
|
25
|
+
"version": "0.1.7-rc.2",
|
|
26
|
+
"commit": "477b4f420553e8a52c2fbccc464d7561b239c443",
|
|
27
27
|
"verifiedProfiles": ["headless", "web"],
|
|
28
28
|
"persistenceProbe": "stat"
|
|
29
29
|
}
|
package/client/client.js
CHANGED
|
@@ -35,6 +35,21 @@ function GatherThreadStatusPanel({ connection, sessions }) {
|
|
|
35
35
|
const [provider, setProvider] = React.useState("");
|
|
36
36
|
const [model, setModel] = React.useState("");
|
|
37
37
|
const visibleSessionSignature = React.useRef("");
|
|
38
|
+
/** Recovery observed from both status polls and action responses. */
|
|
39
|
+
const lastRecoverableError = React.useRef(undefined);
|
|
40
|
+
|
|
41
|
+
const observeRouteRecovery = (next, configureResponse = false) => {
|
|
42
|
+
const recovery = next?.recoverableError;
|
|
43
|
+
// A failed configure is a new revoked choice even if the error code has not
|
|
44
|
+
// changed. Steady polls must leave an operator's new selection alone.
|
|
45
|
+
if (recovery === "route_unavailable"
|
|
46
|
+
&& (configureResponse || lastRecoverableError.current !== "route_unavailable")) {
|
|
47
|
+
setCatalog(undefined);
|
|
48
|
+
setProvider("");
|
|
49
|
+
setModel("");
|
|
50
|
+
}
|
|
51
|
+
lastRecoverableError.current = recovery;
|
|
52
|
+
};
|
|
38
53
|
|
|
39
54
|
React.useEffect(() => {
|
|
40
55
|
let disposed = false;
|
|
@@ -60,6 +75,7 @@ function GatherThreadStatusPanel({ connection, sessions }) {
|
|
|
60
75
|
}
|
|
61
76
|
setSnapshot(result.snapshot);
|
|
62
77
|
setNativeState(result.nativeState);
|
|
78
|
+
observeRouteRecovery(result.nativeState);
|
|
63
79
|
setUnavailable(false);
|
|
64
80
|
timer = setTimeout(refresh, POLL_INTERVAL_MS);
|
|
65
81
|
}
|
|
@@ -95,17 +111,21 @@ function GatherThreadStatusPanel({ connection, sessions }) {
|
|
|
95
111
|
const next = endpoint === "catalog/get" ? parseCatalog(value) : parseNativeState(value);
|
|
96
112
|
if (endpoint === "catalog/get") {
|
|
97
113
|
setCatalog(next);
|
|
98
|
-
const
|
|
99
|
-
setProvider(
|
|
100
|
-
setModel((current) =>
|
|
114
|
+
const selectedProvider = next.providers.find((entry) => entry.id === provider) ?? next.providers[0];
|
|
115
|
+
setProvider(selectedProvider?.id ?? "");
|
|
116
|
+
setModel((current) => selectedProvider?.models.some((entry) => entry.id === current)
|
|
117
|
+
? current : selectedProvider?.models[0]?.id ?? "");
|
|
101
118
|
} else {
|
|
119
|
+
observeRouteRecovery(next, endpoint === "connection/configure");
|
|
102
120
|
setNativeState(next);
|
|
103
121
|
setSnapshot(next.runtime);
|
|
104
122
|
if (endpoint === "connection/configure") await sessions.refresh();
|
|
105
123
|
}
|
|
106
124
|
onSuccess?.(next);
|
|
107
125
|
} catch {
|
|
108
|
-
setNotice(endpoint.startsWith("
|
|
126
|
+
setNotice(endpoint.startsWith("github/") && githubEnglish()
|
|
127
|
+
? "GitHub sync did not finish. Check local sign-in, project access and the remote version; no files were forcibly overwritten."
|
|
128
|
+
: endpoint.startsWith("code/") || endpoint.startsWith("github/")
|
|
109
129
|
? "代码同步未完成。请检查本机授权、项目权限和云端版本;不会强制覆盖文件。"
|
|
110
130
|
: endpoint.startsWith("sync/")
|
|
111
131
|
? "上传操作未完成。请检查连接和会话权限后重试。"
|
|
@@ -182,66 +202,115 @@ function GatherThreadStatusPanel({ connection, sessions }) {
|
|
|
182
202
|
setDeviceName,
|
|
183
203
|
setProvider,
|
|
184
204
|
setModel,
|
|
205
|
+
setCatalog,
|
|
185
206
|
runAction,
|
|
186
207
|
}),
|
|
208
|
+
nativeState === undefined || unavailable ? null : renderCodeSyncControls(nativeState, busy, runAction, true),
|
|
187
209
|
nativeState === undefined || unavailable ? null : renderCodeSyncControls(nativeState, busy, runAction));
|
|
188
210
|
}
|
|
189
211
|
|
|
190
|
-
function
|
|
191
|
-
|
|
212
|
+
function githubEnglish() {
|
|
213
|
+
return typeof navigator !== "undefined" && typeof navigator.language === "string"
|
|
214
|
+
&& !/^zh(?:-|$)/iu.test(navigator.language);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function renderCodeSyncControls(state, busy, runAction, github = false) {
|
|
218
|
+
const projects = github ? state.githubSync : state.codeSync;
|
|
219
|
+
if (!projects?.length) return null;
|
|
220
|
+
const label = (zh, en) => github && githubEnglish() ? en : zh;
|
|
192
221
|
const actions = [
|
|
193
|
-
["code_sync_status", "检查状态"],
|
|
194
|
-
["code_upload", "上传代码"],
|
|
195
|
-
["code_download", "下载更新"],
|
|
196
|
-
["code_recover", "恢复到新目录"],
|
|
222
|
+
["code_sync_status", label("检查状态", "Check status")],
|
|
223
|
+
["code_upload", label("上传代码", "Upload code")],
|
|
224
|
+
["code_download", label("下载更新", "Download updates")],
|
|
225
|
+
["code_recover", label("恢复到新目录", "Recover to a new folder")],
|
|
197
226
|
];
|
|
198
|
-
|
|
199
|
-
|
|
227
|
+
if (github) actions.push(["code_update", label("从主分支更新", "Update from base branch")]);
|
|
228
|
+
const prefix = github ? "github_" : "";
|
|
229
|
+
return React.createElement("section", { style: styles.connectionCard, "aria-label": github ? label("GitHub 项目代码同步", "GitHub project code sync") : "项目代码同步" },
|
|
230
|
+
React.createElement("h3", { style: styles.sectionTitle }, github ? label("项目代码 · GitHub(推荐)", "Project code · GitHub (recommended)") : "项目代码 · GatherThread 云端"),
|
|
200
231
|
React.createElement("p", { style: styles.muted },
|
|
201
|
-
"代码与会话上传独立。每位成员使用自己的云端分支;项目成员均可读取代码。上传前请检查源文件,不上传密钥或私人资料。"),
|
|
202
|
-
|
|
203
|
-
|
|
232
|
+
github ? label("本机直接与 GitHub 同步,代码和 GitHub 凭据不经过 GatherThread。GitHub 仓库成员权限独立管理;自动上传可能触发仓库 Actions。", "This device syncs directly with GitHub; source and credentials do not pass through GatherThread. GitHub access is managed separately, and automatic pushes may trigger Actions.") : "代码与会话上传独立。每位成员使用自己的云端分支;项目成员均可读取代码。上传前请检查源文件,不上传密钥或私人资料。"),
|
|
233
|
+
github ? React.createElement("p", { style: styles.muted }, label("先在本机安装 Git 和 GitHub CLI;点击下方按钮会打开浏览器完成 GitHub 登录。", "Install Git and GitHub CLI on this device, then use the button below to open browser sign-in.")) : null,
|
|
234
|
+
...projects.map((project) => {
|
|
235
|
+
const invoke = (action) => void runAction(github ? "github/action" : "code/action", { projectId: project.projectId, action: `${prefix}${action}` });
|
|
204
236
|
const status = project.status;
|
|
237
|
+
const enabled = github ? project.connection?.enabled === true : status?.enabled;
|
|
205
238
|
return React.createElement("details", { key: project.projectId, style: { ...styles.row, display: "block" } },
|
|
206
239
|
React.createElement("summary", { style: styles.sessionTitle }, project.projectName),
|
|
207
240
|
React.createElement("div", { style: styles.form },
|
|
241
|
+
github ? React.createElement("p", { style: styles.sessionMeta }, project.connection
|
|
242
|
+
? `${project.connection.repository} · ${label("主分支", "Base")} ${project.connection.base_branch} · ${label("成员分支", "My branch")} ${project.branch ?? label("尚未分配", "not assigned")}${enabled ? "" : ` · ${label("已暂停", "paused")}`}`
|
|
243
|
+
: label("请由项目创建者在 GatherThread 的项目代码中绑定 GitHub 仓库。", "Ask the project owner to connect a GitHub repository in GatherThread project code settings.")) : null,
|
|
244
|
+
github ? React.createElement("button", { type: "button", style: styles.compactButton,
|
|
245
|
+
disabled: busy || !project.connection,
|
|
246
|
+
onClick: () => void runAction("github/connect", { projectId: project.projectId }),
|
|
247
|
+
}, label("连接 GitHub · 打开浏览器", "Connect GitHub · Open browser")) : null,
|
|
208
248
|
React.createElement("label", { style: styles.syncToggle },
|
|
209
249
|
React.createElement("input", {
|
|
210
|
-
type: "checkbox", checked: project.authorized, disabled: busy,
|
|
211
|
-
onChange: (event) => void runAction("code/authorize", {
|
|
250
|
+
type: "checkbox", checked: project.authorized, disabled: busy || (github && !project.authorized && !enabled),
|
|
251
|
+
onChange: (event) => void runAction(github ? "github/authorize" : "code/authorize", {
|
|
212
252
|
projectId: project.projectId, enabled: event.target.checked,
|
|
253
|
+
...(github && event.target.checked && project.connection ? {
|
|
254
|
+
repository: project.connection.repository, base_branch: project.connection.base_branch, revision: project.connection.revision,
|
|
255
|
+
} : {}),
|
|
213
256
|
}),
|
|
214
|
-
}), "允许此 DSH 同步该项目代码"),
|
|
215
|
-
React.createElement("p", { style: styles.muted }, "仅访问此项目已经绑定的本地目录,不改动原 Git 分支或暂存区。下载要求本地没有未上传改动;恢复始终新建目录,不更换当前 Agent 工作目录。"),
|
|
257
|
+
}), github ? label("允许此 DSH 将本项目代码同步到上述 GitHub 仓库", "Allow this DSH to sync this project's code to the GitHub repository above") : "允许此 DSH 同步该项目代码"),
|
|
258
|
+
React.createElement("p", { style: styles.muted }, label("仅访问此项目已经绑定的本地目录,不改动原 Git 分支或暂存区。下载要求本地没有未上传改动;恢复始终新建目录,不更换当前 Agent 工作目录。", "Only the local folder already bound to this project is accessed. Your original Git branch and index stay unchanged. Downloads require no unuploaded changes; recovery creates a separate folder.")),
|
|
216
259
|
status ? React.createElement("p", { style: styles.sessionMeta },
|
|
217
|
-
status.local_status_unknown ? "已恢复云端副本;原工作区状态未知,请单独检查。" : status.enabled
|
|
218
|
-
? `${status.file_count} 个源文件 · ${status.local_changes} 项待上传 · ${status.excluded_count} 项已排除 · 云端 ${status.cloud_commit?.slice(0, 8) ?? "尚无版本"}`
|
|
219
|
-
: "请先由项目创建者在 GatherThread 的「项目代码」中启用 Git。") : null,
|
|
220
|
-
status?.needs_download ? React.createElement("p", { style: styles.notice }, "云端有新版本。请先下载;本地有改动时请恢复到新目录比较,不会自动覆盖。") : null,
|
|
260
|
+
status.local_status_unknown ? label("已恢复云端副本;原工作区状态未知,请单独检查。", "A remote copy was recovered. Review the original workspace separately; its state is unknown.") : status.enabled
|
|
261
|
+
? `${status.file_count} ${label("个源文件", "source files")} · ${status.local_changes} ${label("项待上传", "pending changes")} · ${status.excluded_count} ${label("项已排除", "excluded")} · ${label("云端", "remote")} ${status.cloud_commit?.slice(0, 8) ?? label("尚无版本", "no version yet")}`
|
|
262
|
+
: github ? label("GitHub 同步已暂停,请由项目创建者恢复。", "GitHub sync is paused; ask the project owner to resume it.") : "请先由项目创建者在 GatherThread 的「项目代码」中启用 Git。") : null,
|
|
263
|
+
status?.needs_download ? React.createElement("p", { style: styles.notice }, label("云端有新版本。请先下载;本地有改动时请恢复到新目录比较,不会自动覆盖。", "A newer remote version exists. Download when your local files are clean; otherwise recover to a new folder for comparison.")) : null,
|
|
221
264
|
project.authorized ? React.createElement("label", { style: styles.syncToggle },
|
|
222
265
|
React.createElement("input", {
|
|
223
266
|
type: "checkbox", checked: status?.automatic_upload === true,
|
|
224
|
-
disabled: busy || !status?.
|
|
267
|
+
disabled: busy || (!enabled && !status?.automatic_upload),
|
|
225
268
|
onChange: (event) => invoke(event.target.checked ? "code_auto_upload_enable" : "code_auto_upload_disable"),
|
|
226
|
-
}), "空闲时自动上传本地代码至云端") : null,
|
|
227
|
-
React.createElement("div", { style: styles.syncActions }, ...actions.map(([action,
|
|
269
|
+
}), github ? label("空闲时自动上传本地代码至 GitHub", "Automatically upload local code to GitHub while idle") : "空闲时自动上传本地代码至云端") : null,
|
|
270
|
+
React.createElement("div", { style: styles.syncActions }, ...actions.map(([action, actionLabel]) => (
|
|
228
271
|
React.createElement("button", {
|
|
229
272
|
key: action, type: "button", style: styles.compactButton,
|
|
230
|
-
disabled: busy || !project.authorized || (action !== "code_sync_status" && !
|
|
273
|
+
disabled: busy || (!project.authorized && !(github && action === "code_sync_status")) || (action !== "code_sync_status" && !enabled),
|
|
231
274
|
onClick: () => {
|
|
232
|
-
if (action === "code_recover" && !window.confirm("将云端代码恢复到当前项目旁的新目录,原目录和会话保持不变。继续?")) return;
|
|
275
|
+
if (action === "code_recover" && !window.confirm(label("将云端代码恢复到当前项目旁的新目录,原目录和会话保持不变。继续?", "Recover remote code to a new sibling folder? The original folder and conversations stay unchanged."))) return;
|
|
233
276
|
invoke(action);
|
|
234
277
|
},
|
|
235
|
-
},
|
|
278
|
+
}, actionLabel)
|
|
236
279
|
))),
|
|
237
280
|
status?.recovery_directory ? React.createElement("p", { role: "status", style: styles.notice },
|
|
238
|
-
|
|
239
|
-
project.error ? React.createElement("p", { role: "status", style: styles.notice }, codeSyncErrorLabel(project.error)) : null,
|
|
281
|
+
`${label("已恢复至项目同级目录:", "Recovered to a sibling folder: ")}${status.recovery_directory}${label("。在本地 Agent 中打开该目录继续工作;原会话保持不变。", ". Open that folder in your local Agent to continue; original conversations remain unchanged.")}`) : null,
|
|
282
|
+
project.error ? React.createElement("p", { role: "status", style: styles.notice }, codeSyncErrorLabel(project.error, github)) : null,
|
|
240
283
|
));
|
|
241
284
|
}));
|
|
242
285
|
}
|
|
243
286
|
|
|
244
|
-
function codeSyncErrorLabel(code) {
|
|
287
|
+
function codeSyncErrorLabel(code, github = false) {
|
|
288
|
+
if (github && githubEnglish()) {
|
|
289
|
+
const labels = {
|
|
290
|
+
code_sync_disabled: "Allow this DSH to sync project code first.",
|
|
291
|
+
code_sync_busy: "The Agent is working. Try again after this turn finishes.",
|
|
292
|
+
code_workspace_busy: "The Agent is working. Try again after this turn finishes.",
|
|
293
|
+
code_sync_dirty: "Local changes are not uploaded. Upload them first, or recover remote code to a new folder for comparison.",
|
|
294
|
+
code_sync_recovery_required: "The local folder is missing or a download was interrupted. Recover remote code to a new folder first.",
|
|
295
|
+
code_sync_forbidden: "Code access was denied. Check this device's sign-in and project permissions.",
|
|
296
|
+
code_sync_conflict: "The remote version changed. Download safely or recover to a new folder; nothing was overwritten.",
|
|
297
|
+
code_conflict: "The remote version conflicted. Check the latest branch while preserving local changes.",
|
|
298
|
+
code_stale_head: "A newer remote version exists. Recover to a new folder before comparing changes.",
|
|
299
|
+
code_merge_conflict: "Branches conflict. Preserve local changes and resolve the conflict after comparing versions.",
|
|
300
|
+
code_git_unavailable: "Git is unavailable. Check this device's Git installation.",
|
|
301
|
+
code_secret_detected: "A possible secret was detected, so the upload was blocked. Check project files.",
|
|
302
|
+
code_sync_secret: "A possible secret was detected, so the upload was blocked. Check project files.",
|
|
303
|
+
code_github_binding_changed: "The GitHub repository changed. Review the repository and base branch, then authorize again.",
|
|
304
|
+
code_github_not_configured: "Ask the project owner to connect a GitHub repository first.",
|
|
305
|
+
code_github_unavailable: "GitHub settings are temporarily unavailable. Check the connection and retry.",
|
|
306
|
+
code_sync_binding: "The code sync target changed. Review the repository and local authorization.",
|
|
307
|
+
code_sync_unavailable: "Cannot connect to GitHub. Check Git, GitHub CLI sign-in, network and repository access on this device.",
|
|
308
|
+
code_sync_limit: "Project files exceed this sync method's safety limits. Check large files and exclusions.",
|
|
309
|
+
code_sync_unsupported: "This repository contains unsupported content; no code was changed.",
|
|
310
|
+
code_sync_unsafe_path: "The project contains an unsafe or unsupported path. Sync was stopped.",
|
|
311
|
+
};
|
|
312
|
+
return labels[code] ?? "Code sync did not finish. Check authorization, files and remote status; conversation sync is unaffected.";
|
|
313
|
+
}
|
|
245
314
|
const labels = {
|
|
246
315
|
code_sync_disabled: "请先允许此 DSH 同步项目代码。",
|
|
247
316
|
code_sync_busy: "Agent 正在工作,请等待本轮结束后再同步。",
|
|
@@ -259,6 +328,14 @@ function codeSyncErrorLabel(code) {
|
|
|
259
328
|
code_secret_detected: "检测到可能的密钥,已阻止上传。请检查项目文件。",
|
|
260
329
|
code_not_enabled: "请先由项目创建者在 GatherThread 启用项目 Git。",
|
|
261
330
|
code_sync_secret: "检测到可能的密钥,已阻止上传。请检查项目文件。",
|
|
331
|
+
code_github_binding_changed: "GitHub 仓库配置已变化,请核对最新仓库和主分支后重新授权。",
|
|
332
|
+
code_github_not_configured: "请先由项目创建者绑定 GitHub 仓库。",
|
|
333
|
+
code_github_unavailable: "GitHub 配置信息暂不可用,请检查连接后重试。",
|
|
334
|
+
code_sync_binding: "代码同步绑定已变化。请核对当前仓库与本地授权后重试。",
|
|
335
|
+
code_sync_unavailable: github ? "无法连接 GitHub。请检查本机 Git、GitHub CLI 登录状态、网络和仓库访问权限。" : "代码同步暂不可用,请检查本机 Git 与服务连接。",
|
|
336
|
+
code_sync_limit: "项目文件超过同步限制,未上传。请检查大文件和排除范围。",
|
|
337
|
+
code_sync_unsupported: "仓库包含当前同步方式不支持的内容,未修改代码。",
|
|
338
|
+
code_sync_unsafe_path: "项目含不安全或不支持的文件路径,已停止同步。",
|
|
262
339
|
};
|
|
263
340
|
return labels[code] ?? "代码同步未完成。请检查授权、文件和云端状态后重试;会话同步不受影响。";
|
|
264
341
|
}
|
|
@@ -377,8 +454,9 @@ function renderNativeControls(input) {
|
|
|
377
454
|
);
|
|
378
455
|
} else if (state.route === undefined) {
|
|
379
456
|
const providers = input.catalog?.providers ?? [];
|
|
380
|
-
const selectedProvider = providers.find((entry) => entry.id === input.provider)
|
|
457
|
+
const selectedProvider = providers.find((entry) => entry.id === input.provider);
|
|
381
458
|
const models = selectedProvider?.models ?? [];
|
|
459
|
+
const validSelection = models.some((entry) => entry.id === input.model);
|
|
382
460
|
controls.push(
|
|
383
461
|
connectionSummary(state),
|
|
384
462
|
// Reaching this branch means authorization is already "paired": the
|
|
@@ -387,7 +465,12 @@ function renderNativeControls(input) {
|
|
|
387
465
|
// is registered, so a bare stopped connection is indistinguishable from a
|
|
388
466
|
// fresh install. State the remaining step explicitly.
|
|
389
467
|
React.createElement("p", { key: "select-model-notice", style: styles.notice },
|
|
390
|
-
|
|
468
|
+
// A dropped route is not the same situation as a fresh pairing, and saying
|
|
469
|
+
// so is the whole point of not repointing the model silently: the reader has
|
|
470
|
+
// to know their previous choice no longer exists before picking another.
|
|
471
|
+
state.recoverableError === "route_unavailable"
|
|
472
|
+
? "DSH 当前已不再提供此前为该连接选择的模型,因此该连接已停止并取消注册。请重新选择 Provider 与 Model;在此之前的旧模型不会被自动替换。"
|
|
473
|
+
: "配对完成后,尚未选择 DSH Provider 与 Model。完成选择并点击下方按钮后,本机 DSH 运行时才会注册到 GatherThread;在此之前,GatherThread 网页无法发现这台 DSH。"),
|
|
391
474
|
input.catalog === undefined
|
|
392
475
|
? React.createElement("button", {
|
|
393
476
|
key: "load-catalog",
|
|
@@ -401,6 +484,7 @@ function renderNativeControls(input) {
|
|
|
401
484
|
style: styles.form,
|
|
402
485
|
onSubmit: (event) => {
|
|
403
486
|
event.preventDefault();
|
|
487
|
+
if (input.busy || !validSelection) return;
|
|
404
488
|
void input.runAction("connection/configure", {
|
|
405
489
|
provider: input.provider,
|
|
406
490
|
model: input.model,
|
|
@@ -429,7 +513,7 @@ function renderNativeControls(input) {
|
|
|
429
513
|
)),
|
|
430
514
|
React.createElement("button", {
|
|
431
515
|
type: "submit",
|
|
432
|
-
disabled: input.busy || !
|
|
516
|
+
disabled: input.busy || !validSelection,
|
|
433
517
|
style: styles.primaryButton,
|
|
434
518
|
}, input.busy ? "正在连接…" : "连接全部可访问项目")),
|
|
435
519
|
disconnectButton(input),
|
|
@@ -482,11 +566,12 @@ function selectControl(entries, value, setValue, keyOf, labelOf) {
|
|
|
482
566
|
return React.createElement("select", {
|
|
483
567
|
required: true,
|
|
484
568
|
value,
|
|
485
|
-
style: styles.
|
|
569
|
+
style: styles.select,
|
|
486
570
|
onChange: (event) => setValue(event.target.value),
|
|
487
571
|
}, ...entries.map((entry) => React.createElement("option", {
|
|
488
572
|
key: keyOf(entry),
|
|
489
573
|
value: keyOf(entry),
|
|
574
|
+
style: styles.option,
|
|
490
575
|
}, labelOf(entry))));
|
|
491
576
|
}
|
|
492
577
|
|
|
@@ -499,13 +584,13 @@ function detail(label, value) {
|
|
|
499
584
|
function parseNativeState(value) {
|
|
500
585
|
exactObject(value, [
|
|
501
586
|
"schemaVersion", "integration", "authorization", "compatibility", "runtime",
|
|
502
|
-
"officialServerUrl", "serverUrl", "deviceName", "route", "projectCount", "bindings", "localSync", "codeSync", "pairing", "recoverableError",
|
|
587
|
+
"officialServerUrl", "serverUrl", "deviceName", "route", "projectCount", "bindings", "localSync", "codeSync", "githubSync", "pairing", "recoverableError",
|
|
503
588
|
]);
|
|
504
589
|
if (value.schemaVersion !== 2 || value.integration !== "gatherthread") throw new Error("invalid native status identity");
|
|
505
590
|
if (!AUTHORIZATION_STATES.has(value.authorization)) throw new Error("invalid native authorization state");
|
|
506
591
|
exactObject(value.compatibility, ["package", "version", "profile"]);
|
|
507
592
|
if (value.compatibility.package !== "@deepseek-ai/dsh"
|
|
508
|
-
|| value.compatibility.version !== "0.1.
|
|
593
|
+
|| value.compatibility.version !== "0.1.7-rc.2"
|
|
509
594
|
|| value.compatibility.profile !== "web") throw new Error("incompatible native DSH Host");
|
|
510
595
|
const runtime = parseSnapshot(value.runtime);
|
|
511
596
|
const optionalUrl = (url) => {
|
|
@@ -524,6 +609,7 @@ function parseNativeState(value) {
|
|
|
524
609
|
let bindings;
|
|
525
610
|
let localSync;
|
|
526
611
|
let codeSync;
|
|
612
|
+
let githubSync;
|
|
527
613
|
if (value.route !== undefined || value.bindings !== undefined || value.projectCount !== undefined) {
|
|
528
614
|
if (value.route === undefined || !Number.isSafeInteger(value.projectCount) || value.projectCount < 0
|
|
529
615
|
|| !Array.isArray(value.bindings) || value.bindings.length > 100
|
|
@@ -564,17 +650,34 @@ function parseNativeState(value) {
|
|
|
564
650
|
}
|
|
565
651
|
return { ...sync };
|
|
566
652
|
});
|
|
567
|
-
const codeSyncValue =
|
|
653
|
+
const parseCodeProjects = (codeSyncValue, github = false) => {
|
|
568
654
|
if (!Array.isArray(codeSyncValue) || codeSyncValue.length > 100) throw new Error("invalid code sync status");
|
|
569
|
-
|
|
570
|
-
exactObject(project, ["projectId", "projectName", "authorized", "status", "error"]);
|
|
655
|
+
return codeSyncValue.map((project) => {
|
|
656
|
+
exactObject(project, ["projectId", "projectName", "authorized", "status", "error", ...(github ? ["connection", "branch"] : [])]);
|
|
571
657
|
boundedText(project.projectId, 128);
|
|
572
658
|
boundedText(project.projectName, 160);
|
|
573
659
|
if (typeof project.authorized !== "boolean") throw new Error("invalid code sync authorization");
|
|
574
|
-
if (project.error !== undefined && !/^code_[a-z_]{1,60}$/u.test(project.error)) throw new Error("invalid code sync error");
|
|
660
|
+
if (project.error !== undefined && !/^(?:github_)?code_[a-z_]{1,60}$/u.test(project.error)) throw new Error("invalid code sync error");
|
|
661
|
+
if (github) {
|
|
662
|
+
if (project.connection !== null) {
|
|
663
|
+
exactObject(project.connection, ["repository", "base_branch", "revision", "enabled"]);
|
|
664
|
+
boundedText(project.connection.repository, 160);
|
|
665
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(project.connection.repository)) throw new Error("invalid GitHub repository");
|
|
666
|
+
boundedText(project.connection.base_branch, 255);
|
|
667
|
+
boundedText(project.connection.revision, 128);
|
|
668
|
+
if (typeof project.connection.enabled !== "boolean") throw new Error("invalid GitHub state");
|
|
669
|
+
}
|
|
670
|
+
if (project.branch !== null) boundedText(project.branch, 255);
|
|
671
|
+
}
|
|
575
672
|
if (project.status !== undefined) {
|
|
576
673
|
const status = project.status;
|
|
577
|
-
exactObject(status, ["enabled", "automatic_upload", "local_changes", "file_count", "excluded_count", "base_commit", "cloud_commit", "branch_id", "needs_download", "recovery_directory", "local_status_unknown"]);
|
|
674
|
+
exactObject(status, ["enabled", "automatic_upload", "local_changes", "file_count", "excluded_count", "base_commit", "cloud_commit", "branch_id", "needs_download", "recovery_directory", "local_status_unknown", ...(github ? ["provider", "repository", "branch", "base_branch"] : [])]);
|
|
675
|
+
if (github) {
|
|
676
|
+
if (status.provider !== undefined && status.provider !== "github") throw new Error("invalid code provider");
|
|
677
|
+
if (status.repository !== undefined && status.repository !== project.connection?.repository) throw new Error("GitHub target mismatch");
|
|
678
|
+
if (status.base_branch !== undefined && status.base_branch !== project.connection?.base_branch) throw new Error("GitHub base mismatch");
|
|
679
|
+
if (status.branch !== undefined) boundedText(status.branch, 255);
|
|
680
|
+
}
|
|
578
681
|
if (status.local_status_unknown !== undefined && typeof status.local_status_unknown !== "boolean") throw new Error("invalid local status flag");
|
|
579
682
|
for (const key of ["enabled", "automatic_upload", "needs_download"]) {
|
|
580
683
|
if (typeof status[key] !== "boolean") throw new Error("invalid code sync flag");
|
|
@@ -593,6 +696,9 @@ function parseNativeState(value) {
|
|
|
593
696
|
}
|
|
594
697
|
return { ...project };
|
|
595
698
|
});
|
|
699
|
+
};
|
|
700
|
+
codeSync = parseCodeProjects(value.codeSync ?? []);
|
|
701
|
+
githubSync = parseCodeProjects(value.githubSync ?? [], true);
|
|
596
702
|
}
|
|
597
703
|
let pairing;
|
|
598
704
|
if (value.pairing !== undefined) {
|
|
@@ -618,7 +724,8 @@ function parseNativeState(value) {
|
|
|
618
724
|
}
|
|
619
725
|
if (value.recoverableError !== undefined
|
|
620
726
|
&& value.recoverableError !== "pairing_failed"
|
|
621
|
-
&& value.recoverableError !== "connection_failed"
|
|
727
|
+
&& value.recoverableError !== "connection_failed"
|
|
728
|
+
&& value.recoverableError !== "route_unavailable") throw new Error("invalid native recovery state");
|
|
622
729
|
return {
|
|
623
730
|
schemaVersion: 2,
|
|
624
731
|
integration: "gatherthread",
|
|
@@ -628,7 +735,7 @@ function parseNativeState(value) {
|
|
|
628
735
|
...(officialServerUrl === undefined ? {} : { officialServerUrl }),
|
|
629
736
|
...(serverUrl === undefined ? {} : { serverUrl }),
|
|
630
737
|
...(value.deviceName === undefined ? {} : { deviceName: value.deviceName }),
|
|
631
|
-
...(route === undefined ? {} : { route, projectCount: value.projectCount, bindings, localSync, codeSync }),
|
|
738
|
+
...(route === undefined ? {} : { route, projectCount: value.projectCount, bindings, localSync, codeSync, githubSync }),
|
|
632
739
|
...(pairing === undefined ? {} : { pairing }),
|
|
633
740
|
...(value.recoverableError === undefined ? {} : { recoverableError: value.recoverableError }),
|
|
634
741
|
};
|
|
@@ -766,6 +873,14 @@ const styles = {
|
|
|
766
873
|
field: { display: "grid", gap: 6 },
|
|
767
874
|
fieldLabel: { fontSize: 12, fontWeight: 600, opacity: 0.76 },
|
|
768
875
|
input: { width: "100%", minHeight: 36, boxSizing: "border-box", padding: "7px 10px", borderRadius: 9, border: "1px solid rgba(127,127,127,.28)", background: "transparent", color: "inherit", font: "inherit" },
|
|
876
|
+
// A dropdown owns a popup it cannot inherit: the list surface is painted from the
|
|
877
|
+
// control's own background and each row from its option, while the text keeps
|
|
878
|
+
// following the panel. Sharing `input` therefore drew the model names in the
|
|
879
|
+
// panel's foreground on the UA's own surface — white on white under a dark harness
|
|
880
|
+
// theme. Both the control and the rows name the system field pair instead, which
|
|
881
|
+
// the UA resolves against the used color scheme, so neither can match the other.
|
|
882
|
+
select: { width: "100%", minHeight: 36, boxSizing: "border-box", padding: "7px 10px", borderRadius: 9, border: "1px solid rgba(127,127,127,.28)", background: "Field", color: "FieldText", font: "inherit" },
|
|
883
|
+
option: { background: "Field", color: "FieldText" },
|
|
769
884
|
primaryButton: { minHeight: 36, padding: "8px 12px", borderRadius: 9, border: "1px solid Highlight", background: "Highlight", color: "HighlightText", WebkitTextFillColor: "HighlightText", font: "inherit", fontWeight: 600, cursor: "pointer" },
|
|
770
885
|
secondaryButton: { minHeight: 36, padding: "8px 12px", borderRadius: 9, border: "1px solid rgba(127,127,127,.3)", background: "transparent", color: "inherit", font: "inherit", cursor: "pointer" },
|
|
771
886
|
linkButton: { display: "inline-flex", alignItems: "center", justifyContent: "center", minHeight: 36, padding: "0 12px", borderRadius: 9, border: "1px solid rgba(127,127,127,.3)", color: "inherit", textDecoration: "none", fontWeight: 600 },
|