dsh-new-session 0.1.0 → 0.2.0
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 +38 -34
- package/lib/client.js +88 -26
- package/lib/index.d.ts +4 -2
- package/lib/index.js +197 -4
- package/lib/index.js.map +1 -1
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
# `dsh-new-session`
|
|
2
2
|
|
|
3
|
-
在输入框里敲 `/new
|
|
4
|
-
|
|
3
|
+
在输入框里敲 `/clear`(别名 `/new`),**开一段全新的对话,但留在当前工作目录**——和 Claude
|
|
4
|
+
Code 的 `/clear` 对齐:旧对话完整留在侧边栏历史里,上下文从零开始。
|
|
5
5
|
|
|
6
6
|
## 它做什么
|
|
7
7
|
|
|
8
|
-
- 在 `/`
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
- 在 `/` 菜单里注册两个客户端 action:**Clear conversation / 清除对话**(`/clear`)和
|
|
9
|
+
**New session / 新建会话**(`/new`,Claude Code 语义下的别名)。
|
|
10
|
+
- 执行时:
|
|
11
|
+
1. 找到当前会话所在的 Workspace,向宿主要一个空白会话(有可复用的空白就复用,和
|
|
12
|
+
New Session 按钮一致,未发送的草稿也还在);
|
|
13
|
+
2. 通过本插件的宿主路由,把新会话的**工作目录**设成当前会话的工作目录;
|
|
14
|
+
3. 导航到新会话。
|
|
15
|
+
- **工作目录(包括 worktree 副本)会继承**:新会话的创建目录仍是 Workspace 路径,但工作
|
|
16
|
+
目录与当前会话一致。因此在 worktree 会话里 `/clear` 会留在**同一个 worktree**,而不是像
|
|
17
|
+
普通新会话那样让 `dsh-worktree-switch` 再建一份副本——`dsh-worktree-switch` 看到新会话已
|
|
18
|
+
在副本里,只显示 ready,不再 prepare。
|
|
19
|
+
- 旧会话原样保留(侧边栏历史、`/resume` 语义上的可回)。
|
|
20
|
+
- 只在当前 Peer 有 `session/create` 权限时注册;只读的远程访问者看不到这两行。
|
|
21
|
+
- 不提交任何提示词:`action` 类型会消费掉 `/clear` / `/new` 这个 token。
|
|
15
22
|
|
|
16
23
|
## 安装
|
|
17
24
|
|
|
@@ -21,39 +28,36 @@ dsh plugin --profile web add /home/ubuntu/code/deepseek-plugins/packages/dsh-new
|
|
|
21
28
|
dsh --profile web --dump-config # 应能看到 "# == dsh-new-session"
|
|
22
29
|
```
|
|
23
30
|
|
|
24
|
-
装完需要重启正在运行的 Web profile
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
构建产物由 `prepare` 脚本(tsdown)产出;改完源码后重新
|
|
28
|
-
`pnpm --filter dsh-new-session build` 即可。外部安装的插件不在 `pnpm run dev:web` 的监视
|
|
29
|
-
范围内,client-hmr 每 500ms 轮询 `lib/client.js`,重新构建后页面会自动换上新的浏览器半。
|
|
31
|
+
装完需要重启正在运行的 Web profile:宿主半新增了一条 Connection 路由,浏览器半由宿主发布,
|
|
32
|
+
页面刷新不够。构建产物由 `prepare` 脚本(tsdown)产出;改完源码后重新
|
|
33
|
+
`pnpm --filter dsh-new-session build` 即可(外部安装的插件由 client-hmr 轮询 `lib/client.js`)。
|
|
30
34
|
|
|
31
35
|
## 在 GUI 里走一遍
|
|
32
36
|
|
|
33
|
-
1. 重启 Web profile
|
|
34
|
-
2.
|
|
35
|
-
3.
|
|
36
|
-
|
|
37
|
+
1. 重启 Web profile,打开任意会话(在 worktree 里的会话也可以)。
|
|
38
|
+
2. 输入 `/clear`(或 `/new`)回车:视图切到一个空白会话,工作目录还是刚才那个目录。
|
|
39
|
+
3. 如果原来在 worktree 副本里,新会话仍在同一个副本里——不会多出一份副本;旧会话在侧边栏
|
|
40
|
+
里还在。
|
|
37
41
|
|
|
38
42
|
## 实现
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
- **宿主半**(`src/index.ts` + `src/host/adopt.ts`)在 Connection 上注册一条精确路由
|
|
45
|
+
`POST /api/dsh-new-session/adopt`。它只做浏览器做不到的事:把目标会话的工作目录设成源会话
|
|
46
|
+
的当前目录(`ctx.workingDirectory.set`)。守卫:
|
|
47
|
+
- 目标会话必须还没有开始任何 turn;
|
|
48
|
+
- 源会话的工作目录必须位于它自己的创建目录(`header.cwd`)之内——`cd` 到项目外的情况
|
|
49
|
+
报告为 no-op,避免把会话挪进不属于它的项目。
|
|
50
|
+
- **浏览器半**(`src/client/index.tsx`)用 `ctx.commandUi.register()` 注册 `/clear` 与 `/new`
|
|
51
|
+
两个 `action`;`run` 里 `uiWorkspace.connectWorkspace()` 拿空白会话 → 调宿主路由 →
|
|
52
|
+
`uiWorkspace.openSession()`。注册包在 `ctx.remoteAccess.whenAccess('session/create', …)` 里。
|
|
53
|
+
- 没有 Workspace 的会话回退到普通的新会话导航(`startSession()`)。
|
|
50
54
|
|
|
51
55
|
## 已知边界
|
|
52
56
|
|
|
53
57
|
- **只对 Web GUI 生效**。SDK / 桌面纯文本入口的斜杠命令识别只认内置白名单
|
|
54
58
|
(dsh 的 `query-input.ts`),第三方插件命令不会从那条路进入;要覆盖那些入口需要改 dsh 本体。
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
- **没有 `/clear <name>` 参数**(Claude Code 用它给旧对话命名)。dsh 的客户端 action 只处理
|
|
60
|
+
不带参数的调用;要支持参数得把命令做成宿主 command + 客户端装饰,留作后续。
|
|
61
|
+
- `/reset` 别名没有注册:dsh 的客户端命令没有别名机制,每个别名都要占用菜单里的一行,所以
|
|
62
|
+
只保留 `/clear` 和 `/new` 两行。
|
|
63
|
+
- 旧会话不会被删除或改名,只是留在历史里。
|
package/lib/client.js
CHANGED
|
@@ -6,48 +6,105 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
6
6
|
// src/client/locales.ts
|
|
7
7
|
/** English copy. */
|
|
8
8
|
const en = {
|
|
9
|
-
label: "
|
|
10
|
-
description: "Start a fresh conversation in
|
|
9
|
+
"clear.label": "Clear conversation",
|
|
10
|
+
"clear.description": "Start a fresh conversation here; the previous one stays in your history",
|
|
11
|
+
"new.label": "New session",
|
|
12
|
+
"new.description": "Same as /clear: start a fresh conversation here"
|
|
11
13
|
};
|
|
12
14
|
/** Simplified Chinese copy. */
|
|
13
15
|
const zh = {
|
|
14
|
-
label: "
|
|
15
|
-
description: "
|
|
16
|
+
"clear.label": "清除对话",
|
|
17
|
+
"clear.description": "在这里开始全新对话,旧对话保留在历史里",
|
|
18
|
+
"new.label": "新建会话",
|
|
19
|
+
"new.description": "与 /clear 相同:在这里开始全新对话"
|
|
16
20
|
};
|
|
17
21
|
// src/client/index.tsx
|
|
18
22
|
/** Browser module id: package name, bundle row id, plugin id. */
|
|
19
23
|
const name = "dsh-new-session";
|
|
20
24
|
/** Locale namespace this plugin owns. */
|
|
21
25
|
const NS = "newSession";
|
|
22
|
-
/**
|
|
26
|
+
/** Host route namespace; the package name and the wire method prefix are the same. */
|
|
27
|
+
const PLUGIN_ID = "dsh-new-session";
|
|
28
|
+
/** Client services the commands cannot work without. */
|
|
23
29
|
const inject = [
|
|
24
30
|
"commandUi",
|
|
31
|
+
"connection",
|
|
25
32
|
"locale",
|
|
26
33
|
"remoteAccess",
|
|
27
|
-
"uiWorkspace"
|
|
34
|
+
"uiWorkspace",
|
|
35
|
+
"workspaces"
|
|
28
36
|
];
|
|
29
37
|
/**
|
|
30
|
-
* Build the `/new`
|
|
38
|
+
* Build the `/clear` contribution and its `/new` alias.
|
|
31
39
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
40
|
+
* The client command registry has no alias mechanism (reserved for first-party
|
|
41
|
+
* host commands), so an alias is a second contribution with the same action;
|
|
42
|
+
* its copy says so.
|
|
34
43
|
*
|
|
35
|
-
* @param deps - translator and
|
|
36
|
-
* @returns
|
|
44
|
+
* @param deps - translator and conversation starter.
|
|
45
|
+
* @returns both contributions, primary first.
|
|
37
46
|
*/
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
name
|
|
41
|
-
label: () => deps.t(
|
|
42
|
-
description: () => deps.t(
|
|
47
|
+
function createCommands(deps) {
|
|
48
|
+
const command = (name, prefix) => ({
|
|
49
|
+
name,
|
|
50
|
+
label: () => deps.t(`${prefix}.label`),
|
|
51
|
+
description: () => deps.t(`${prefix}.description`),
|
|
43
52
|
available: () => true,
|
|
44
53
|
ui: {
|
|
45
54
|
kind: "action",
|
|
46
|
-
run: () => {
|
|
47
|
-
deps.start();
|
|
55
|
+
run: (session) => {
|
|
56
|
+
deps.start(session.sessionId);
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
|
-
};
|
|
59
|
+
});
|
|
60
|
+
return [command("clear", "clear"), command("new", "new")];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Call the Host adoption verb.
|
|
64
|
+
* @param ctx - browser Cordis context providing `connection`.
|
|
65
|
+
* @param sourceSessionId - Session being left.
|
|
66
|
+
* @param targetSessionId - blank Session that should start where the source is.
|
|
67
|
+
* @throws {Error} when the Host refused or the reply is malformed.
|
|
68
|
+
*/
|
|
69
|
+
async function adopt(ctx, sourceSessionId, targetSessionId) {
|
|
70
|
+
const connection = ctx.get("connection");
|
|
71
|
+
if (connection === void 0) throw new Error("the connection service is not mounted");
|
|
72
|
+
const result = await connection.rpc.call("/api", `${PLUGIN_ID}/adopt`, {
|
|
73
|
+
sourceSessionId,
|
|
74
|
+
targetSessionId
|
|
75
|
+
});
|
|
76
|
+
if (result === null || typeof result !== "object" || !("ok" in result)) throw new Error("malformed reply from the adopt verb");
|
|
77
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Start a fresh conversation in the source Session's working directory.
|
|
81
|
+
*
|
|
82
|
+
* Sessions that do not belong to a Workspace fall back to plain navigation:
|
|
83
|
+
* there is no Workspace whose blank Session could reuse the directory, and the
|
|
84
|
+
* previous behavior is still the honest answer for that shape.
|
|
85
|
+
*
|
|
86
|
+
* @param ctx - browser Cordis context.
|
|
87
|
+
* @param sessionId - Session the user is leaving.
|
|
88
|
+
*/
|
|
89
|
+
async function start(ctx, sessionId) {
|
|
90
|
+
const workspace = ctx.workspaces.list.getSnapshot().items.find((item) => item.sessionIds.includes(sessionId));
|
|
91
|
+
if (workspace === void 0) {
|
|
92
|
+
ctx.uiWorkspace.startSession();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
let target;
|
|
96
|
+
try {
|
|
97
|
+
target = await ctx.uiWorkspace.connectWorkspace(workspace.workspaceId);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.warn("dsh-new-session: could not create the next Session:", error);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
await adopt(ctx, sessionId, target);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.warn("dsh-new-session: could not inherit the working directory:", error);
|
|
106
|
+
}
|
|
107
|
+
ctx.uiWorkspace.openSession(target);
|
|
51
108
|
}
|
|
52
109
|
/**
|
|
53
110
|
* Mount the browser half.
|
|
@@ -59,15 +116,20 @@ function apply(ctx) {
|
|
|
59
116
|
zh
|
|
60
117
|
}), "dsh-new-session: dictionaries");
|
|
61
118
|
const t = ctx.locale.bind(NS);
|
|
62
|
-
ctx.effect(() => ctx.remoteAccess.whenAccess("session/create", () =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
119
|
+
ctx.effect(() => ctx.remoteAccess.whenAccess("session/create", () => {
|
|
120
|
+
const disposers = createCommands({
|
|
121
|
+
t,
|
|
122
|
+
start: (sessionId) => {
|
|
123
|
+
start(ctx, sessionId);
|
|
124
|
+
}
|
|
125
|
+
}).map((command) => ctx.commandUi.register(command));
|
|
126
|
+
return () => {
|
|
127
|
+
for (const dispose of disposers) dispose();
|
|
128
|
+
};
|
|
129
|
+
}), "dsh-new-session: /clear commands");
|
|
68
130
|
}
|
|
69
131
|
exports.apply = apply;
|
|
70
|
-
exports.
|
|
132
|
+
exports.createCommands = createCommands;
|
|
71
133
|
exports.inject = inject;
|
|
72
134
|
exports.name = name;
|
|
73
135
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Context } from "@deepseek-ai/cordis";
|
|
2
2
|
/** Plugin id: package name, bundle row id, and browser module id. */
|
|
3
3
|
export declare const name = "dsh-new-session";
|
|
4
|
+
/** Services this plugin cannot work without. */
|
|
5
|
+
export declare const inject: string[];
|
|
4
6
|
/**
|
|
5
|
-
* Mount the
|
|
6
|
-
* @param ctx -
|
|
7
|
+
* Mount the Host half.
|
|
8
|
+
* @param ctx - Host context.
|
|
7
9
|
*/
|
|
8
10
|
export declare function apply(ctx: Context): void;
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
CHANGED
|
@@ -1,11 +1,204 @@
|
|
|
1
|
+
import { isAbsolute, relative } from "node:path";
|
|
2
|
+
// src/host/adopt.ts
|
|
3
|
+
/**
|
|
4
|
+
* Host half of the conversation reset: adopt a freshly created Session into the
|
|
5
|
+
* source Session's working directory.
|
|
6
|
+
*
|
|
7
|
+
* This is the dsh equivalent of Claude Code recording a new conversation under
|
|
8
|
+
* the same working directory as the previous one. A dsh Session owns two
|
|
9
|
+
* directories: the immutable creation directory (`header.cwd`, which is what
|
|
10
|
+
* keeps it inside its Workspace) and the committed working directory (the
|
|
11
|
+
* `working-directory` projection). `/clear` must move the *working* directory
|
|
12
|
+
* only, so the new Session stays a member of the same Workspace while any
|
|
13
|
+
* worktree/isolation state the source had is carried over.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-new-session/host/adopt
|
|
16
|
+
*/
|
|
17
|
+
/** Absolute prefix every route below lives under. */
|
|
18
|
+
const API_PREFIX = "/api/dsh-new-session";
|
|
19
|
+
/** Plugin id; the wire method prefix and the package name are the same string. */
|
|
20
|
+
const PLUGIN_ID = "dsh-new-session";
|
|
21
|
+
/** This plugin serves exactly one verb. */
|
|
22
|
+
const ENDPOINT = "adopt";
|
|
23
|
+
/**
|
|
24
|
+
* Whether `directory` is `root` or one of its descendants.
|
|
25
|
+
* @param directory - candidate absolute directory.
|
|
26
|
+
* @param root - absolute ancestor directory.
|
|
27
|
+
* @returns whether `directory` is contained in `root`.
|
|
28
|
+
*/
|
|
29
|
+
function isInside(directory, root) {
|
|
30
|
+
const path = relative(root, directory);
|
|
31
|
+
return path === "" || !path.startsWith("..") && !isAbsolute(path);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve one live Agent, resuming it through the Session Controller when
|
|
35
|
+
* available and falling back to the registry otherwise.
|
|
36
|
+
* @param ctx - Host context.
|
|
37
|
+
* @param sessionId - wire session identity.
|
|
38
|
+
* @param signal - caller cancellation.
|
|
39
|
+
* @returns the live Agent.
|
|
40
|
+
* @throws when the Session cannot be resolved.
|
|
41
|
+
*/
|
|
42
|
+
async function agentOf(ctx, sessionId, signal) {
|
|
43
|
+
signal.throwIfAborted();
|
|
44
|
+
if (sessionId === "") throw new Error("a session id is required");
|
|
45
|
+
const live = ctx.agents.get(sessionId);
|
|
46
|
+
if (live !== void 0) return live;
|
|
47
|
+
const controller = ctx.get("sessionController");
|
|
48
|
+
if (controller !== void 0) {
|
|
49
|
+
const resolved = await controller.resolveAgent(sessionId);
|
|
50
|
+
if (!("error" in resolved)) return resolved.agent;
|
|
51
|
+
throw new Error(resolved.error.message);
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`no live agent for session ${sessionId}`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the adoption verb.
|
|
57
|
+
*
|
|
58
|
+
* Discovery is deliberately conservative: the target must be a Session that
|
|
59
|
+
* has not started a turn yet, and the source's working directory must live
|
|
60
|
+
* inside the source's own creation directory. Copying a directory from outside
|
|
61
|
+
* that tree (a Session that `cd`-ed into another repository) would contradict
|
|
62
|
+
* the target's project membership and make an isolating plugin misread it, so
|
|
63
|
+
* that case is a reported no-op instead.
|
|
64
|
+
*
|
|
65
|
+
* @param ctx - Host context carrying `agents` and `workingDirectory`.
|
|
66
|
+
* @returns the verb.
|
|
67
|
+
*/
|
|
68
|
+
function createAdoption(ctx) {
|
|
69
|
+
return async (input, signal) => {
|
|
70
|
+
const source = await agentOf(ctx, input.sourceSessionId, signal);
|
|
71
|
+
const target = await agentOf(ctx, input.targetSessionId, signal);
|
|
72
|
+
signal.throwIfAborted();
|
|
73
|
+
if (source.session.id === target.session.id) throw new Error("the source and target sessions must differ");
|
|
74
|
+
const boundary = ctx.agents.turnBoundaryOf(target.session);
|
|
75
|
+
if (boundary.lastTurn !== 0 || boundary.openTurnStartSeq !== null) throw new Error(`target session "${target.session.id}" has already started`);
|
|
76
|
+
const workingDirectory = ctx.workingDirectory.get(source.session);
|
|
77
|
+
const home = source.session.header.cwd;
|
|
78
|
+
if (home === void 0 || !isInside(workingDirectory, home)) return {
|
|
79
|
+
adopted: false,
|
|
80
|
+
workingDirectory
|
|
81
|
+
};
|
|
82
|
+
await ctx.workingDirectory.set(target, workingDirectory, signal);
|
|
83
|
+
return {
|
|
84
|
+
adopted: true,
|
|
85
|
+
workingDirectory
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Decode the browser's `client-request` envelope.
|
|
91
|
+
* @param body - parsed JSON body.
|
|
92
|
+
* @returns the request, or null when the envelope is malformed.
|
|
93
|
+
*/
|
|
94
|
+
function readClientRequest(body) {
|
|
95
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) return null;
|
|
96
|
+
const record = body;
|
|
97
|
+
if (record["type"] !== "client-request") return null;
|
|
98
|
+
const { rpcId, method } = record;
|
|
99
|
+
if (typeof rpcId !== "string" || rpcId === "") return null;
|
|
100
|
+
if (typeof method !== "string" || method === "") return null;
|
|
101
|
+
const payload = record["payload"];
|
|
102
|
+
return {
|
|
103
|
+
rpcId,
|
|
104
|
+
method,
|
|
105
|
+
payload: payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : {}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Read one required non-empty string field.
|
|
110
|
+
* @param payload - decoded payload.
|
|
111
|
+
* @param field - field name.
|
|
112
|
+
* @returns the value.
|
|
113
|
+
* @throws when the field is missing or empty.
|
|
114
|
+
*/
|
|
115
|
+
function requiredString(payload, field) {
|
|
116
|
+
const value = payload[field];
|
|
117
|
+
if (typeof value !== "string" || value.trim() === "") throw new Error(`payload.${field} must be a non-empty string`);
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
/** A successful wire result. */
|
|
121
|
+
function ok(value) {
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
value
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** A failed wire result carrying the stable code. */
|
|
128
|
+
function fail(code, message) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
error: {
|
|
132
|
+
code,
|
|
133
|
+
message,
|
|
134
|
+
details: {}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Register the adoption route on the shared `/api` channel.
|
|
140
|
+
*
|
|
141
|
+
* Exact routes are the only shape an out-of-tree plugin can claim: `/api` is
|
|
142
|
+
* the one logical channel the API Gateway owns, so `rpc.handle` refuses it and
|
|
143
|
+
* a second `rpc.intercept` throws. The route answers owner peers only, which is
|
|
144
|
+
* what the local Web GUI is.
|
|
145
|
+
*
|
|
146
|
+
* @param ctx - Host context carrying `connection`.
|
|
147
|
+
* @param adopt - the verb this route serves.
|
|
148
|
+
*/
|
|
149
|
+
function registerAdoptionRoute(ctx, adopt) {
|
|
150
|
+
ctx.effect(() => ctx.connection.fetch.register({
|
|
151
|
+
path: `${API_PREFIX}/${ENDPOINT}`,
|
|
152
|
+
methods: ["POST"],
|
|
153
|
+
requestBody: "buffered",
|
|
154
|
+
async fetch(request) {
|
|
155
|
+
if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return new Response("content type must be application/json", { status: 415 });
|
|
156
|
+
let body;
|
|
157
|
+
try {
|
|
158
|
+
body = await request.json();
|
|
159
|
+
} catch {
|
|
160
|
+
return new Response("body is not JSON", { status: 400 });
|
|
161
|
+
}
|
|
162
|
+
const message = readClientRequest(body);
|
|
163
|
+
if (message === null) return new Response("invalid client-request message", { status: 400 });
|
|
164
|
+
if (message.method !== `dsh-new-session/adopt`) return Response.json({
|
|
165
|
+
type: "server-response",
|
|
166
|
+
rpcId: message.rpcId,
|
|
167
|
+
result: fail("bad-request", "RPC method does not match endpoint")
|
|
168
|
+
});
|
|
169
|
+
let result;
|
|
170
|
+
try {
|
|
171
|
+
result = ok(await adopt({
|
|
172
|
+
sourceSessionId: requiredString(message.payload, "sourceSessionId"),
|
|
173
|
+
targetSessionId: requiredString(message.payload, "targetSessionId")
|
|
174
|
+
}, request.signal));
|
|
175
|
+
} catch (error) {
|
|
176
|
+
result = fail("adopt-failed", error instanceof Error ? error.message : String(error));
|
|
177
|
+
}
|
|
178
|
+
return Response.json({
|
|
179
|
+
type: "server-response",
|
|
180
|
+
rpcId: message.rpcId,
|
|
181
|
+
result
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}), `${PLUGIN_ID} ${ENDPOINT} route`);
|
|
185
|
+
}
|
|
1
186
|
// src/index.ts
|
|
2
187
|
/** Plugin id: package name, bundle row id, and browser module id. */
|
|
3
188
|
const name = "dsh-new-session";
|
|
189
|
+
/** Services this plugin cannot work without. */
|
|
190
|
+
const inject = [
|
|
191
|
+
"agents",
|
|
192
|
+
"connection",
|
|
193
|
+
"workingDirectory"
|
|
194
|
+
];
|
|
4
195
|
/**
|
|
5
|
-
* Mount the
|
|
6
|
-
* @param ctx -
|
|
196
|
+
* Mount the Host half.
|
|
197
|
+
* @param ctx - Host context.
|
|
7
198
|
*/
|
|
8
|
-
function apply(ctx) {
|
|
9
|
-
|
|
199
|
+
function apply(ctx) {
|
|
200
|
+
registerAdoptionRoute(ctx, createAdoption(ctx));
|
|
201
|
+
}
|
|
202
|
+
export { apply, inject, name };
|
|
10
203
|
|
|
11
204
|
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Host half of the `/new` command plugin.\n *\n * The command itself is a browser contribution: the slash menu and the\n * new-Session navigation both live in the Web GUI, so this Host row only mounts\n * the package — which is enough for the client-module scanner to publish\n * `lib/client.js`, and for the plugin's lifecycle to follow the bundle.\n *\n * @module dsh-new-session\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\n\n/** Plugin id: package name, bundle row id, and browser module id. */\nexport const name = 'dsh-new-session'\n\n/**\n * Mount the inert Host half.\n * @param ctx - the Cordis context.\n */\nexport function apply(ctx: Context): void {\n // No host-side capability on purpose: a Host handler could not move the\n // browser's current Session, so `/new` is a client-owned action instead.\n void ctx\n}\n"],"mappings":";;AAcA,MAAa,OAAO;;;;;AAMpB,SAAgB,MAAM,KAAoB,CAI1C"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/host/adopt.ts","../src/index.ts"],"sourcesContent":["/**\n * Host half of the conversation reset: adopt a freshly created Session into the\n * source Session's working directory.\n *\n * This is the dsh equivalent of Claude Code recording a new conversation under\n * the same working directory as the previous one. A dsh Session owns two\n * directories: the immutable creation directory (`header.cwd`, which is what\n * keeps it inside its Workspace) and the committed working directory (the\n * `working-directory` projection). `/clear` must move the *working* directory\n * only, so the new Session stays a member of the same Workspace while any\n * worktree/isolation state the source had is carried over.\n *\n * @module dsh-new-session/host/adopt\n */\n\nimport { isAbsolute, relative } from 'node:path'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { Agent } from '@deepseek-ai/dsh-agent'\nimport type { SessionId } from '@deepseek-ai/dsh-session'\nimport type { ConnectionRpcResult } from '@deepseek-ai/dsh-client-connection'\n\n/** Absolute prefix every route below lives under. */\nexport const API_PREFIX = '/api/dsh-new-session'\n\n/** Plugin id; the wire method prefix and the package name are the same string. */\nexport const PLUGIN_ID = 'dsh-new-session'\n\n/** This plugin serves exactly one verb. */\nexport const ENDPOINT = 'adopt' as const\n\n/** One decoded adoption request. */\nexport interface AdoptionInput {\n /** Session the user is leaving; its working directory is the target. */\n readonly sourceSessionId: string\n /** Freshly created Session that should start where the source is. */\n readonly targetSessionId: string\n}\n\n/** What the Host did with one adoption request. */\nexport interface AdoptionValue {\n /** Whether the target's working directory was moved. */\n readonly adopted: boolean\n /** The source's current working directory. */\n readonly workingDirectory: string | null\n}\n\n/** The verb implementation the route dispatches to. */\nexport type Adopt = (input: AdoptionInput, signal: AbortSignal) => Promise<AdoptionValue>\n\n/**\n * Whether `directory` is `root` or one of its descendants.\n * @param directory - candidate absolute directory.\n * @param root - absolute ancestor directory.\n * @returns whether `directory` is contained in `root`.\n */\nexport function isInside(directory: string, root: string): boolean {\n const path = relative(root, directory)\n return path === '' || (!path.startsWith('..') && !isAbsolute(path))\n}\n\n/**\n * Resolve one live Agent, resuming it through the Session Controller when\n * available and falling back to the registry otherwise.\n * @param ctx - Host context.\n * @param sessionId - wire session identity.\n * @param signal - caller cancellation.\n * @returns the live Agent.\n * @throws when the Session cannot be resolved.\n */\nasync function agentOf(ctx: Context, sessionId: string, signal: AbortSignal): Promise<Agent> {\n signal.throwIfAborted()\n if (sessionId === '') throw new Error('a session id is required')\n // The common case (the Session the user is in, and the blank Session just\n // created for them) is already live; the controller handles a cold Session.\n const live = ctx.agents.get(sessionId as SessionId)\n if (live !== undefined) return live\n const controller = ctx.get('sessionController')\n if (controller !== undefined) {\n const resolved = await controller.resolveAgent(sessionId as SessionId)\n if (!('error' in resolved)) return resolved.agent\n throw new Error(resolved.error.message)\n }\n throw new Error(`no live agent for session ${sessionId}`)\n}\n\n/**\n * Build the adoption verb.\n *\n * Discovery is deliberately conservative: the target must be a Session that\n * has not started a turn yet, and the source's working directory must live\n * inside the source's own creation directory. Copying a directory from outside\n * that tree (a Session that `cd`-ed into another repository) would contradict\n * the target's project membership and make an isolating plugin misread it, so\n * that case is a reported no-op instead.\n *\n * @param ctx - Host context carrying `agents` and `workingDirectory`.\n * @returns the verb.\n */\nexport function createAdoption(ctx: Context): Adopt {\n return async (input, signal) => {\n const source = await agentOf(ctx, input.sourceSessionId, signal)\n const target = await agentOf(ctx, input.targetSessionId, signal)\n signal.throwIfAborted()\n if (source.session.id === target.session.id) {\n throw new Error('the source and target sessions must differ')\n }\n const boundary = ctx.agents.turnBoundaryOf(target.session)\n if (boundary.lastTurn !== 0 || boundary.openTurnStartSeq !== null) {\n throw new Error(`target session \"${target.session.id}\" has already started`)\n }\n const workingDirectory = ctx.workingDirectory.get(source.session)\n const home = source.session.header.cwd\n if (home === undefined || !isInside(workingDirectory, home)) {\n return { adopted: false, workingDirectory }\n }\n await ctx.workingDirectory.set(target, workingDirectory, signal)\n return { adopted: true, workingDirectory }\n }\n}\n\n/** One decoded client request. */\ninterface ClientRequest {\n readonly rpcId: string\n readonly method: string\n readonly payload: Record<string, unknown>\n}\n\n/**\n * Decode the browser's `client-request` envelope.\n * @param body - parsed JSON body.\n * @returns the request, or null when the envelope is malformed.\n */\nfunction readClientRequest(body: unknown): ClientRequest | null {\n if (body === null || typeof body !== 'object' || Array.isArray(body)) return null\n const record = body as Record<string, unknown>\n if (record['type'] !== 'client-request') return null\n const { rpcId, method } = record\n if (typeof rpcId !== 'string' || rpcId === '') return null\n if (typeof method !== 'string' || method === '') return null\n const payload = record['payload']\n return {\n rpcId,\n method,\n payload: payload !== null && typeof payload === 'object' && !Array.isArray(payload)\n ? payload as Record<string, unknown>\n : {},\n }\n}\n\n/**\n * Read one required non-empty string field.\n * @param payload - decoded payload.\n * @param field - field name.\n * @returns the value.\n * @throws when the field is missing or empty.\n */\nfunction requiredString(payload: Record<string, unknown>, field: string): string {\n const value = payload[field]\n if (typeof value !== 'string' || value.trim() === '') {\n throw new Error(`payload.${field} must be a non-empty string`)\n }\n return value\n}\n\n/** A successful wire result. */\nfunction ok<T>(value: T): ConnectionRpcResult<T> {\n return { ok: true, value }\n}\n\n/** A failed wire result carrying the stable code. */\nfunction fail(code: string, message: string): ConnectionRpcResult<never> {\n return { ok: false, error: { code, message, details: {} } }\n}\n\n/**\n * Register the adoption route on the shared `/api` channel.\n *\n * Exact routes are the only shape an out-of-tree plugin can claim: `/api` is\n * the one logical channel the API Gateway owns, so `rpc.handle` refuses it and\n * a second `rpc.intercept` throws. The route answers owner peers only, which is\n * what the local Web GUI is.\n *\n * @param ctx - Host context carrying `connection`.\n * @param adopt - the verb this route serves.\n */\nexport function registerAdoptionRoute(ctx: Context, adopt: Adopt): void {\n ctx.effect(() => ctx.connection.fetch.register({\n path: `${API_PREFIX}/${ENDPOINT}`,\n methods: ['POST'],\n requestBody: 'buffered',\n async fetch(request) {\n const contentType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase()\n if (contentType !== 'application/json') {\n return new Response('content type must be application/json', { status: 415 })\n }\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return new Response('body is not JSON', { status: 400 })\n }\n const message = readClientRequest(body)\n if (message === null) return new Response('invalid client-request message', { status: 400 })\n if (message.method !== `${PLUGIN_ID}/${ENDPOINT}`) {\n return Response.json({\n type: 'server-response',\n rpcId: message.rpcId,\n result: fail('bad-request', 'RPC method does not match endpoint'),\n })\n }\n let result: ConnectionRpcResult<AdoptionValue>\n try {\n result = ok(await adopt({\n sourceSessionId: requiredString(message.payload, 'sourceSessionId'),\n targetSessionId: requiredString(message.payload, 'targetSessionId'),\n }, request.signal))\n } catch (error) {\n result = fail('adopt-failed', error instanceof Error ? error.message : String(error))\n }\n return Response.json({ type: 'server-response', rpcId: message.rpcId, result })\n },\n }), `${PLUGIN_ID} ${ENDPOINT} route`)\n}\n","/**\n * Host half of the conversation reset.\n *\n * The browser half owns the `/clear` (alias `/new`) command: it decides when\n * the user wants a fresh conversation and drives Workspace navigation. This\n * half owns the one thing only the Host can do — start the new Session in the\n * source Session's working directory — and exposes it as one exact route on the\n * Connection fetch bridge.\n *\n * @module dsh-new-session\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-agent'\nimport type {} from '@deepseek-ai/dsh-api-session-controller'\nimport type {} from '@deepseek-ai/dsh-client-connection'\nimport type {} from '@deepseek-ai/dsh-working-directory'\nimport { createAdoption, registerAdoptionRoute } from './host/adopt.ts'\n\n/** Plugin id: package name, bundle row id, and browser module id. */\nexport const name = 'dsh-new-session'\n\n/** Services this plugin cannot work without. */\nexport const inject = ['agents', 'connection', 'workingDirectory']\n\n/**\n * Mount the Host half.\n * @param ctx - Host context.\n */\nexport function apply(ctx: Context): void {\n registerAdoptionRoute(ctx, createAdoption(ctx))\n}\n"],"mappings":"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"}
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
|
-
"title": "
|
|
4
|
-
"description": "Type /new in the composer to start a fresh conversation in the current
|
|
3
|
+
"title": "Clear conversation command",
|
|
4
|
+
"description": "Type /clear (or /new) in the composer to start a fresh conversation in the current working directory; the previous one stays in your history."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-new-session",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "`/clear` (alias `/new`) in the Web GUI composer: start a fresh conversation in the current working directory and keep the previous one in your history.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
"platform": "web",
|
|
46
46
|
"inject": [
|
|
47
47
|
"@deepseek-ai/dsh-api-remote-access",
|
|
48
|
+
"@deepseek-ai/dsh-api-workspace-controller",
|
|
49
|
+
"@deepseek-ai/dsh-client-connection",
|
|
48
50
|
"@deepseek-ai/dsh-client-locale",
|
|
49
51
|
"@deepseek-ai/dsh-client-ui-commands",
|
|
50
52
|
"@deepseek-ai/dsh-client-ui-workspace"
|
|
@@ -53,21 +55,31 @@
|
|
|
53
55
|
},
|
|
54
56
|
"peerDependencies": {
|
|
55
57
|
"@deepseek-ai/cordis": "^4.1.0",
|
|
58
|
+
"@deepseek-ai/dsh-agent": ">=0.2.2-rc.1 <0.2.3-0",
|
|
56
59
|
"@deepseek-ai/dsh-api-remote-access": ">=0.2.2-rc.1 <0.2.3-0",
|
|
57
60
|
"@deepseek-ai/dsh-api-session-controller": ">=0.2.2-rc.1 <0.2.3-0",
|
|
61
|
+
"@deepseek-ai/dsh-api-workspace-controller": ">=0.2.2-rc.1 <0.2.3-0",
|
|
62
|
+
"@deepseek-ai/dsh-client-connection": ">=0.2.2-rc.1 <0.2.3-0",
|
|
58
63
|
"@deepseek-ai/dsh-client-locale": ">=0.2.2-rc.1 <0.2.3-0",
|
|
59
64
|
"@deepseek-ai/dsh-client-ui-commands": ">=0.2.2-rc.1 <0.2.3-0",
|
|
60
65
|
"@deepseek-ai/dsh-client-ui-slots": ">=0.2.2-rc.1 <0.2.3-0",
|
|
61
|
-
"@deepseek-ai/dsh-client-ui-workspace": ">=0.2.2-rc.1 <0.2.3-0"
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-workspace": ">=0.2.2-rc.1 <0.2.3-0",
|
|
67
|
+
"@deepseek-ai/dsh-session": ">=0.2.2-rc.1 <0.2.3-0",
|
|
68
|
+
"@deepseek-ai/dsh-working-directory": ">=0.2.2-rc.1 <0.2.3-0"
|
|
62
69
|
},
|
|
63
70
|
"devDependencies": {
|
|
64
71
|
"@deepseek-ai/cordis": "4.1.0",
|
|
72
|
+
"@deepseek-ai/dsh-agent": "0.2.2-rc.1",
|
|
65
73
|
"@deepseek-ai/dsh-api-remote-access": "0.2.2-rc.1",
|
|
66
74
|
"@deepseek-ai/dsh-api-session-controller": "0.2.2-rc.1",
|
|
75
|
+
"@deepseek-ai/dsh-api-workspace-controller": "0.2.2-rc.1",
|
|
76
|
+
"@deepseek-ai/dsh-client-connection": "0.2.2-rc.1",
|
|
67
77
|
"@deepseek-ai/dsh-client-locale": "0.2.2-rc.1",
|
|
68
78
|
"@deepseek-ai/dsh-client-ui-commands": "0.2.2-rc.1",
|
|
69
79
|
"@deepseek-ai/dsh-client-ui-slots": "0.2.2-rc.1",
|
|
70
80
|
"@deepseek-ai/dsh-client-ui-workspace": "0.2.2-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-session": "0.2.2-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-working-directory": "0.2.2-rc.1",
|
|
71
83
|
"@types/node": "^26.6.6",
|
|
72
84
|
"tsdown": "^0.23.4",
|
|
73
85
|
"typescript": "^7.0.2",
|