@sleep2agi/dsh-anet 0.1.0-preview.26 → 0.1.0-preview.28

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 CHANGED
@@ -13,7 +13,7 @@ A [DSH (DeepSeek Harness)](https://www.npmjs.com/package/@deepseek-ai/dsh) plugi
13
13
  ## Install
14
14
 
15
15
  ```bash
16
- dsh plugin --profile web add /path/to/agent-network/integrations/dsh-commhub
16
+ dsh plugin --profile web add @sleep2agi/dsh-anet
17
17
  # restart DSH
18
18
  ```
19
19
 
@@ -29,8 +29,8 @@ DSH `0.1.0-rc.6` / `0.2.0-rc.2` are supported. On the desktop app, the same pack
29
29
  is minted through the logged-in account and the node connects immediately.
30
30
  4. Others dispatch tasks to that alias from the hub/dashboard. Each task runs in the node's DSH
31
31
  session and the answer is sent back to the hub.
32
- 5. Multiple nodes can run together. Use the session dropdown on a node card to switch sessions,
33
- or **Delete** to stop it.
32
+ 5. Multiple nodes can run together. On a node card you can switch **project** (a new session is created
33
+ for that project) and switch **session** at any time; **Delete** stops the node.
34
34
 
35
35
  Config lives in `~/.anet/dsh/` (`nodes.json`, `account.json`, both 0600).
36
36
 
package/README.zh.md CHANGED
@@ -13,7 +13,7 @@
13
13
  ## 安装
14
14
 
15
15
  ```bash
16
- dsh plugin --profile web add /path/to/agent-network/integrations/dsh-commhub
16
+ dsh plugin --profile web add @sleep2agi/dsh-anet
17
17
  # 重启 DSH
18
18
  ```
19
19
 
@@ -28,7 +28,7 @@ dsh plugin --profile web add /path/to/agent-network/integrations/dsh-commhub
28
28
  3. 创建节点:节点名称(alias)、项目目录、Session(新建或选已有)。token 由当前登录账号
29
29
  自动签发,节点立即上线。
30
30
  4. 其他人在 hub/看板给该 alias 派任务;任务在节点的 DSH session 里执行,结果自动回 hub。
31
- 5. 可同时运行多个节点;节点卡片里的 Session 下拉可随时切换,**删除** 即下线。
31
+ 5. 可同时运行多个节点;节点卡片里可随时切换 **项目**(会为该节点新建 session)和 **Session**,**删除** 即下线。
32
32
 
33
33
  配置在 `~/.anet/dsh/`(`nodes.json`、`account.json`,均 0600)。
34
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/dsh-anet",
3
- "version": "0.1.0-preview.26",
3
+ "version": "0.1.0-preview.28",
4
4
  "description": "DSH (DeepSeek Harness) plugin: expose one or more DSH sessions as Agent Network (anet.sh / CommHub) nodes.",
5
5
  "type": "module",
6
6
  "main": "./src/plugin.mjs",
package/src/client.js CHANGED
@@ -22,10 +22,11 @@ window.__ModuleLoader__.load({
22
22
  loggedIn: "已登录", createTitle: "创建节点", createHint: "节点 token 会由当前账号自动创建",
23
23
  create: "创建节点", creating: "创建中…", created: "节点已创建并接入",
24
24
  delete: "删除", confirmDelete: "删除这个节点?", switchSession: "切换会话", switched: "已切换会话",
25
+ switchProject: "切换项目", switchProjectHint: "切换项目会为该节点新建 session", projectSwitched: "已切换项目",
25
26
  loading: "正在加载…", offline: "本地服务不可用(127.0.0.1:9460)", failed: "操作失败",
26
27
  note: "配置保存在 ~/.anet/dsh/nodes.json(0600)",
27
28
  running: "已连接", starting: "连接中", stopped: "未连接", error: "异常",
28
- network: "Network", projectLabel: "项目", noNetwork: "默认网络",
29
+ network: "Network", projectLabel: "项目", noNetwork: "默认网络", unnamedSession: "未命名会话",
29
30
  };
30
31
  const en = {
31
32
  title: "ANet", subtitle: "Turn a project/session into an Agent Network node",
@@ -38,10 +39,11 @@ window.__ModuleLoader__.load({
38
39
  loggedIn: "Logged in", createTitle: "Create node", createHint: "The node token is minted with the current account",
39
40
  create: "Create node", creating: "Creating…", created: "Node created and connected",
40
41
  delete: "Delete", confirmDelete: "Delete this node?", switchSession: "Switch session", switched: "Session switched",
42
+ switchProject: "Switch project", switchProjectHint: "Switching project creates a new session for this node", projectSwitched: "Project switched",
41
43
  loading: "Loading…", offline: "Local service unavailable (127.0.0.1:9460)", failed: "Failed",
42
44
  note: "Stored in ~/.anet/dsh/nodes.json (0600)",
43
45
  running: "Connected", starting: "Connecting", stopped: "Disconnected", error: "Error",
44
- network: "Network", projectLabel: "Project", noNetwork: "default network",
46
+ network: "Network", projectLabel: "Project", noNetwork: "default network", unnamedSession: "Untitled session",
45
47
  };
46
48
 
47
49
  const T = {
@@ -70,6 +72,22 @@ window.__ModuleLoader__.load({
70
72
  const sectionHint = { margin: "4px 0 12px", fontSize: 12, color: T.muted, lineHeight: 1.5 };
71
73
  const grid2 = { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 };
72
74
 
75
+ function baseName(path) {
76
+ if (!path) return "";
77
+ const parts = String(path).split("/").filter(Boolean);
78
+ return parts.length ? parts[parts.length - 1] : path;
79
+ }
80
+ function sessionWhen(ms) {
81
+ if (!ms) return "";
82
+ const date = new Date(ms);
83
+ const pad = (value) => String(value).padStart(2, "0");
84
+ return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
85
+ }
86
+ function sessionLabel(t, session) {
87
+ const title = session.title || t("unnamedSession");
88
+ return `${title} · ${sessionWhen(session.createdAt)} · ${baseName(session.cwd)}`;
89
+ }
90
+
73
91
  function Field(props) {
74
92
  return h("div", { style: props.style ?? null },
75
93
  h("label", { style: labelStyle, htmlFor: props.id }, props.label),
@@ -97,20 +115,30 @@ window.__ModuleLoader__.load({
97
115
  }
98
116
 
99
117
  function NodeRow(props) {
100
- const { node, t, onDelete, sessions, onSwitch } = props;
118
+ const { node, t, onDelete, sessions, onSwitch, projects, onSwitchProject } = props;
101
119
  return h("div", { style: { ...cardStyle, padding: "14px 16px", marginBottom: 10 } },
102
120
  h("div", { style: { display: "flex", alignItems: "center", gap: 10 } },
103
121
  h("div", { style: { fontSize: 14, fontWeight: 650, color: T.text, flex: 1, minWidth: 0, overflowWrap: "anywhere" } }, node.name),
104
122
  h(StatusPill, { status: node.status, t }),
105
123
  h("button", { type: "button", style: ghostButton(false), onClick: () => onDelete(node) }, t("delete")),
106
124
  ),
107
- h("div", { style: { display: "grid", gridTemplateColumns: "1.2fr 1fr 1.6fr", gap: 12, marginTop: 10 } },
125
+ h("div", { style: { display: "grid", gridTemplateColumns: "1.2fr 1fr", gap: 12, marginTop: 10 } },
108
126
  h(Meta, { label: "Hub", value: node.hub }),
109
127
  h(Meta, { label: "NETWORK", value: node.networkId || t("noNetwork") }),
110
- h(Meta, { label: "PROJECT", value: node.projectDir }),
111
128
  ),
112
129
  h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 10, paddingTop: 10, borderTop: `1px solid ${T.border}` } },
113
- h("span", { style: { fontSize: 11.5, color: T.muted, flex: "none" } }, t("sessionPick")),
130
+ h("span", { style: { fontSize: 11.5, color: T.muted, flex: "none", width: 44 } }, t("projectLabel")),
131
+ h("select", {
132
+ value: node.projectDir,
133
+ onChange: (event) => onSwitchProject(node, event.target.value),
134
+ style: { ...inputStyle, height: 30, padding: "0 8px", fontSize: 12, flex: 1, minWidth: 0 },
135
+ },
136
+ (projects ?? []).some((project) => project.path === node.projectDir) ? null : h("option", { value: node.projectDir }, node.projectDir),
137
+ (projects ?? []).map((project) => h("option", { key: project.path, value: project.path }, `${project.label} — ${project.path}`)),
138
+ ),
139
+ ),
140
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 8 } },
141
+ h("span", { style: { fontSize: 11.5, color: T.muted, flex: "none", width: 44 } }, t("sessionPick")),
114
142
  h("select", {
115
143
  value: node.sessionId,
116
144
  disabled: !sessions || sessions.length === 0,
@@ -118,8 +146,8 @@ window.__ModuleLoader__.load({
118
146
  style: { ...inputStyle, height: 30, padding: "0 8px", fontSize: 12, flex: 1, minWidth: 0 },
119
147
  },
120
148
  (sessions ?? []).some((session) => session.id === node.sessionId) ? null : h("option", { value: node.sessionId }, node.sessionId),
121
- (sessions ?? []).map((session) => h("option", { key: session.id, value: session.id },
122
- `${session.id}${session.cwd ? ` — ${session.cwd}` : ""}`)),
149
+ (sessions ?? []).map((session) => h("option", { key: session.id, value: session.id, title: session.id },
150
+ sessionLabel(t, session))),
123
151
  ),
124
152
  ),
125
153
  node.lastError ? h("div", { style: { fontSize: 11.5, color: T.err, marginTop: 6 } }, node.lastError) : null,
@@ -296,6 +324,22 @@ window.__ModuleLoader__.load({
296
324
  }
297
325
  };
298
326
 
327
+ const onSwitchProject = async (node, projectDir) => {
328
+ if (!projectDir || projectDir === node.projectDir) return;
329
+ if (!window.confirm(`${t("switchProjectHint")}?`)) return;
330
+ try {
331
+ const res = await fetch(`${ENDPOINT}/nodes/${encodeURIComponent(node.id)}`, {
332
+ method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ projectDir }),
333
+ });
334
+ const body = await res.json();
335
+ if (!res.ok || body.ok === false) throw new Error(body.error ?? `HTTP ${res.status}`);
336
+ setMessage(t("projectSwitched"));
337
+ await refresh();
338
+ } catch (error) {
339
+ setFailed(true); setMessage(`${t("failed")}: ${error?.message ?? error}`);
340
+ }
341
+ };
342
+
299
343
  const onSwitch = async (node, sessionId) => {
300
344
  if (!sessionId || sessionId === node.sessionId) return;
301
345
  if (!window.confirm(t("confirmDelete").replace(/.*/, t("switchSession") + "?"))) return;
@@ -333,7 +377,7 @@ window.__ModuleLoader__.load({
333
377
  state.nodes.length === 0
334
378
  ? h("p", { style: { fontSize: 12, color: T.muted, margin: "10px 0 2px" } }, t("noNodes"))
335
379
  : h("div", { style: { marginTop: 12 } }, state.nodes.map((node) =>
336
- h(NodeRow, { key: node.id, node, t, onDelete, sessions, onSwitch }))),
380
+ h(NodeRow, { key: node.id, node, t, onDelete, sessions, onSwitch, projects, onSwitchProject }))),
337
381
  ),
338
382
 
339
383
  session
@@ -376,7 +420,7 @@ window.__ModuleLoader__.load({
376
420
  onChange: (event) => setSessionChoice(event.target.value),
377
421
  },
378
422
  h("option", { value: "__new__" }, t("newSession")),
379
- sessions.map((item) => h("option", { key: item.id, value: item.id }, `${item.id}${item.cwd ? ` — ${item.cwd}` : ""}`)),
423
+ sessions.map((item) => h("option", { key: item.id, value: item.id, title: item.id }, sessionLabel(t, item))),
380
424
  ),
381
425
  h("p", { style: hintStyle }, t("sessionHint")),
382
426
  ),
package/src/plugin.mjs CHANGED
@@ -13,7 +13,7 @@ import { createLedger } from "./ledger.mjs";
13
13
  import { createCommhubNode } from "./node-loop.mjs";
14
14
  import { commhubToolSpecs } from "./tools.mjs";
15
15
  import { summarizeTurn, turnFailure } from "./turn.mjs";
16
- import { findNodeBySession, loadNodes, normalizeNode, registryPath, saveNodes, validateNode } from "./nodes.mjs";
16
+ import { findNodeBySession, loadNodes, newSessionId, normalizeNode, registryPath, saveNodes, validateNode } from "./nodes.mjs";
17
17
  import { startConfigServer } from "./ui-server.mjs";
18
18
 
19
19
  export { summarizeTurn } from "./turn.mjs";
@@ -21,7 +21,7 @@ export { summarizeTurn } from "./turn.mjs";
21
21
  export const name = "anet";
22
22
  export const inject = ["agents", "agentDefaultModel", "sessions", "tools"];
23
23
 
24
- const VERSION = "dsh-anet@0.1.0-preview.26";
24
+ const VERSION = "dsh-anet@0.1.0-preview.28";
25
25
 
26
26
  function agentSetup(ctx) {
27
27
  const selection = ctx.agentDefaultModel.currentSelection();
@@ -196,6 +196,22 @@ export function apply(ctx, config) {
196
196
  return { projects, defaultProject: projects[0]?.path ?? process.cwd() };
197
197
  }
198
198
 
199
+ async function sessionTitle(persistence, id) {
200
+ try {
201
+ const { events } = await persistence.inspect(id);
202
+ const titled = events.find((event) => event.type === "session/title");
203
+ const title = titled?.data?.title ?? titled?.data?.text;
204
+ if (typeof title === "string" && title.trim()) return title.trim();
205
+ const first = events.find((event) => event.type === "user/message");
206
+ const content = first?.data?.message?.content ?? first?.data?.content;
207
+ if (Array.isArray(content)) {
208
+ const text = content.filter((block) => block?.type === "text").map((block) => block.text).join(" ").trim();
209
+ if (text) return text.slice(0, 48);
210
+ }
211
+ } catch { /* unreadable session */ }
212
+ return "";
213
+ }
214
+
199
215
  async function listSessions() {
200
216
  const persistence = ctx.get("sessionPersistence");
201
217
  const sessions = [];
@@ -207,7 +223,9 @@ export function apply(ctx, config) {
207
223
  } catch { /* no persisted sessions */ }
208
224
  }
209
225
  sessions.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
210
- return { sessions: sessions.slice(0, 200) };
226
+ const recent = sessions.slice(0, 40);
227
+ for (const session of recent) session.title = await sessionTitle(persistence, session.id);
228
+ return { sessions: [...recent, ...sessions.slice(40, 200)] };
211
229
  }
212
230
 
213
231
  async function sessionById(id) {
@@ -242,6 +260,10 @@ export function apply(ctx, config) {
242
260
  const node = requireNode(id);
243
261
  let sessionId = node.sessionId;
244
262
  let projectDir = String(draft.projectDir ?? node.projectDir);
263
+ if (draft.projectDir !== undefined && String(draft.projectDir) !== node.projectDir && draft.sessionId === undefined) {
264
+ projectDir = String(draft.projectDir);
265
+ sessionId = newSessionId(node.name);
266
+ }
245
267
  if (draft.sessionId !== undefined && String(draft.sessionId) !== node.sessionId) {
246
268
  const session = await sessionById(String(draft.sessionId));
247
269
  if (!session) throw new Error(`session 不存在: ${draft.sessionId}`);