@langify-org/dsh-side-chat 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 langify-org
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ja.md ADDED
@@ -0,0 +1,42 @@
1
+ # @langify-org/dsh-side-chat
2
+
3
+ [English](README.md) | 日本語 | [中文](README.zh.md)
4
+
5
+ DeepSeek Harness の会話を止めずに、その会話について横で質問できます。サイドチャットは会話を fork したもので、右サイドバーに DSH 自身のチャットと入力欄で開き、サイドチャットであることがモデルに伝えられます。
6
+
7
+ ## 必要なもの
8
+
9
+ - Web UI のある DSH `>=0.2.1-rc.5 <0.3.0-0`。
10
+
11
+ ## 導入
12
+
13
+ ```sh
14
+ dsh plugin --profile <profile> add @langify-org/dsh-side-chat
15
+ ```
16
+
17
+ DSH の Web 版や Desktop 版では **Plugins** ページから追加できます。
18
+
19
+ ## 使い方
20
+
21
+ assistant のメッセージの下にあるボタンの列でサイドチャットのボタンを押すか、右サイドバーのガイドで **サイドチャット** を押します。会話が fork されて `サイドチャット: <会話のタイトル>` という名前が付き、右サイドバーに開くので、普通の会話と同じように話せます。セッション一覧でサイドチャットを選ぶと、元の会話が開き、その横にサイドチャットが開きます。
22
+
23
+ - サイドチャットは、どのメッセージで押しても、会話の最後に終わった turn から始まります。メインの会話のその後の turn は届かず、サイドチャットの内容もメインの会話には届きません。
24
+ - モデルに、サイドチャットであること、メインの作業を再開せず質問に答えること、サイドチャットで頼まれない限り変更を加えないことを伝えます。この指示はチャットには表示されません。
25
+
26
+ ## 仕組み
27
+
28
+ ホスト側のプラグインは、DSH の session controller でセッションを fork します(**Fork** の操作と同じ fork です)。そのすぐあとに、サイドチャットの指示を `agent.inject` で fork の inbox に入れます。指示は最初のメッセージと一緒にモデルに届きます。inbox は変わった時点でログに書かれるので、fork は作った瞬間からサイドチャットとして印が付き、再起動しても残ります。セッションの projection `langifySideChat` が、fork 自身のログからこの印を読み取り、セッション一覧を通してどのブラウザーにも届けます。ブラウザー側は、fork を自前の右サイドバーのタブで開きます。タブの中身は DSH 共通の `conversation.content` を Chat 表示で描いたもので、DSH の subagent のタブと同じ埋め込み方です。fork は普通のセッションなので、メッセージは普通の送信の経路で届きます。
29
+
30
+ ## 既知の制約
31
+
32
+ - **サイドバーに並ぶ**:サイドチャットは普通のセッションなので、セッション一覧に出ます。使い終わったらアーカイブしてください。
33
+ - **最新の turn からだけ**:サイドチャットは最後に終わった turn から始まります。前のメッセージから始めることは、まだできません。
34
+
35
+ ## ブラウザーでの確認
36
+
37
+ リポジトリのルートで `pnpm dev` を実行し、終わった turn が 1 つ以上ある会話で次を確かめます。
38
+
39
+ 1. assistant のメッセージの下にあるサイドチャットのボタンを押す。右サイドバーに、空の入力欄のある **サイドチャット** のタブが開く。
40
+ 2. 会話についての質問を送る。会話の内容を踏まえて答える。
41
+ 3. メインの会話に何も増えていない。
42
+ 4. サイドチャットの指示がサイドチャットに表示されず、fork の Trajectory 表示には出ている。
package/README.md CHANGED
@@ -1,3 +1,42 @@
1
- # Temporary Holding Version
1
+ # @langify-org/dsh-side-chat
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ English | [日本語](README.ja.md) | [中文](README.zh.md)
4
+
5
+ Ask a side question about a DeepSeek Harness conversation without interrupting it. The side chat is a fork of the conversation, opened in the right sidebar with DSH's own chat and composer, and told that it is a side chat.
6
+
7
+ ## Requirements
8
+
9
+ - DSH `>=0.2.1-rc.5 <0.3.0-0` with the Web UI.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ dsh plugin --profile <profile> add @langify-org/dsh-side-chat
15
+ ```
16
+
17
+ In DSH Web and Desktop you can also add it from the **Plugins** page.
18
+
19
+ ## Usage
20
+
21
+ Click the side-chat button in an assistant message's action row, or **Side chat** in the right sidebar's guide. The conversation is forked, titled `Side chat: <conversation title>`, and opens in the right sidebar, where you talk to it as in any conversation. Selecting a side chat in the session list opens its source conversation with the side chat beside it.
22
+
23
+ - The side chat starts from the conversation's latest completed turn, whichever message you clicked. Later turns of the main conversation do not reach it, and nothing in the side chat reaches the main conversation.
24
+ - The model is told it is in a side chat: to answer questions rather than resume the main task, and not to make changes unless you ask in the side chat. You do not see this instruction in the chat.
25
+
26
+ ## How it works
27
+
28
+ The host plugin forks the session through DSH's session controller, the same fork as the **Fork** action, and puts the side-chat instruction into the fork's inbox at once with `agent.inject`; it reaches the model with the first message. The inbox is logged as soon as it changes, so the fork is marked as a side chat from its creation, across restarts. A session projection, `langifySideChat`, folds that mark from the fork's own log and reaches every browser with the session list. The browser half opens the fork in its own right-sidebar tab, which renders DSH's shared `conversation.content` with the Chat view, embedded as DSH's subagent tab embeds it. The fork is an ordinary session, so messages go through the ordinary prompt path.
29
+
30
+ ## Known limitations
31
+
32
+ - **Listed in the sidebar**: a side chat is an ordinary session, so it appears in the session list. Archive it when you are done.
33
+ - **Latest turn only**: the side chat starts from the latest completed turn; it cannot yet start from an earlier message.
34
+
35
+ ## Verifying in a browser
36
+
37
+ `pnpm dev` from the repository root, then in a conversation with at least one completed turn:
38
+
39
+ 1. Click the side-chat button under an assistant message. A **Side chat** tab opens in the right sidebar with an empty composer.
40
+ 2. Send a question about the conversation. It answers with the conversation's context.
41
+ 3. Check that the main conversation shows nothing new.
42
+ 4. Check that the side-chat instruction is not shown in the side chat, and that it appears in the fork's Trajectory view.
package/README.zh.md ADDED
@@ -0,0 +1,42 @@
1
+ # @langify-org/dsh-side-chat
2
+
3
+ [English](README.md) | [日本語](README.ja.md) | 中文
4
+
5
+ 在不打断 DeepSeek Harness 对话的情况下,就该对话在旁边提问。旁路对话是对话的一个 fork,以 DSH 自带的对话界面和输入框在右侧边栏中打开,并告知模型它处于旁路对话中。
6
+
7
+ ## 环境要求
8
+
9
+ - 带 Web UI 的 DSH `>=0.2.1-rc.5 <0.3.0-0`。
10
+
11
+ ## 安装
12
+
13
+ ```sh
14
+ dsh plugin --profile <profile> add @langify-org/dsh-side-chat
15
+ ```
16
+
17
+ 在 DSH Web 版和 Desktop 版中也可以通过 **Plugins** 页面添加。
18
+
19
+ ## 使用
20
+
21
+ 点击 assistant 消息操作栏中的旁路对话按钮,或右侧边栏引导页中的 **旁路对话**。对话会被 fork,命名为 `旁路对话: <对话标题>`,并在右侧边栏中打开,可以像普通对话一样交谈。在会话列表中选择旁路对话时,会打开其来源对话,并在旁边打开该旁路对话。
22
+
23
+ - 无论点击哪条消息,旁路对话都从对话最近一个已完成的轮次开始。主对话之后的轮次不会传入,旁路对话的内容也不会传到主对话。
24
+ - 模型会被告知它处于旁路对话中:回答问题而不是继续主任务,除非在旁路对话中被要求,否则不做任何修改。这条指示不会显示在对话中。
25
+
26
+ ## 工作原理
27
+
28
+ Host 端插件通过 DSH 的 session controller fork 会话(与 **Fork** 操作相同),随即用 `agent.inject` 把旁路对话的指示放入 fork 的 inbox,指示会随第一条消息到达模型。inbox 一变化就写入日志,因此 fork 从创建起就带有旁路对话的标记,重启后依然保留。会话 projection `langifySideChat` 从 fork 自身的日志中读取该标记,并随会话列表送达每个浏览器。浏览器端在自己的右侧边栏标签页中打开 fork,标签页以 Chat 视图渲染 DSH 共用的 `conversation.content`,嵌入方式与 DSH 的 subagent 标签页相同。fork 是普通会话,消息走普通的发送路径。
29
+
30
+ ## 已知限制
31
+
32
+ - **出现在侧边栏中**:旁路对话是普通会话,会出现在会话列表中。用完后请归档。
33
+ - **只能从最新轮次开始**:旁路对话从最近一个已完成的轮次开始,暂时不能从更早的消息开始。
34
+
35
+ ## 在浏览器中验证
36
+
37
+ 在仓库根目录运行 `pnpm dev`,然后在至少有一个已完成轮次的对话中确认:
38
+
39
+ 1. 点击 assistant 消息下方的旁路对话按钮,右侧边栏打开带有空输入框的 **旁路对话** 标签页。
40
+ 2. 发送一个关于该对话的问题,它会结合对话内容作答。
41
+ 3. 主对话中没有新增内容。
42
+ 4. 旁路对话中不显示旁路对话的指示,而 fork 的 Trajectory 视图中可以看到它。
@@ -0,0 +1,4 @@
1
+ # Bundle layer: inserts the side-chat plugin when a profile enables this package.
2
+ - insert:
3
+ - id: langify-side-chat
4
+ name: '@langify-org/dsh-side-chat'
@@ -0,0 +1,363 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@langify-org/dsh-side-chat",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let zod = require("zod");
10
+ let react_jsx_runtime = require("react/jsx-runtime");
11
+ // ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.2.1-rc.5_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-brand/lib/index.js
12
+ /**
13
+ * Duplicate-install-safe nominal primitive helpers.
14
+ *
15
+ * A brand makes structurally identical strings or numbers non-interchangeable
16
+ * at the type level: a `SessionId` cannot be passed where a `ToolCallId` is
17
+ * expected, and an event sequence cannot be passed as a log offset. Comparison,
18
+ * logging, and serialization retain the underlying primitive behavior.
19
+ *
20
+ * This package owns no concrete domain value and keeps no runtime identity or mutable
21
+ * state, so independently installed copies produce interchangeable values.
22
+ *
23
+ * @module @deepseek-ai/dsh-brand
24
+ */
25
+ /**
26
+ * Apply a compile-time string brand without changing the value.
27
+ * @param value - string admitted by the domain that owns the target brand.
28
+ * @returns the same string with the requested compile-time brand.
29
+ */
30
+ function brandString(value) {
31
+ return value;
32
+ }
33
+ // ../../node_modules/.pnpm/@deepseek-ai+dsh-session@0.2.1-rc.5_@deepseek-ai+cordis@4.0.5_@deepseek-ai+dsh-scope@0._1afa55f8edfbf99388e776171daf732a/node_modules/@deepseek-ai/dsh-session/lib/types/types.js
34
+ /**
35
+ * Brand a string as a {@link SessionId}.
36
+ * @param id - the raw session id string.
37
+ * @returns the same string with the session-id brand.
38
+ */
39
+ function SessionId(id) {
40
+ return brandString(id);
41
+ }
42
+ // src/rpc.ts
43
+ /** DSH's shared RPC channel, and the endpoint this plugin owns on it. */
44
+ const RPC_CHANNEL = "/api";
45
+ const RPC_METHOD = "langify-side-chat/create";
46
+ /** Failures the host reports; each one has a translated message. */
47
+ const ERROR_CODES = [
48
+ "invalid_request",
49
+ "fork_failed",
50
+ "internal_error"
51
+ ];
52
+ const id = zod.z.string().min(1).max(256).refine((value) => !value.includes("\0"));
53
+ zod.z.strictObject({
54
+ sessionId: id,
55
+ title: zod.z.string().trim().min(1).max(200)
56
+ });
57
+ const createResponseSchema = zod.z.strictObject({ sessionId: id });
58
+ // src/client/chat-tab.tsx
59
+ /** Resource-address prefix of a side chat in the right sidebar. */
60
+ const ADDRESS_PREFIX = "dsh-resource://langify-side-chat/session/";
61
+ function chatAddress(sessionId) {
62
+ return `${ADDRESS_PREFIX}${encodeURIComponent(sessionId)}`;
63
+ }
64
+ /** The session id a side-chat address names, or undefined for any other address. */
65
+ function parseChatAddress(address) {
66
+ if (!address.startsWith("dsh-resource://langify-side-chat/session/")) return void 0;
67
+ const rest = address.slice(41);
68
+ if (rest === "" || rest.includes("/")) return void 0;
69
+ try {
70
+ return SessionId(decodeURIComponent(rest));
71
+ } catch {
72
+ return;
73
+ }
74
+ }
75
+ /** The Session-scoped child slot inside the tab; its one entry is ConversationPanel. */
76
+ const CONVERSATION_SLOT = "langify-side-chat.conversation";
77
+ /**
78
+ * The side chat's tab: binds the fork's Session around the conversation slot.
79
+ */
80
+ function ChatTab({ retain, useTabInfo, SessionProvider, renderSlot }) {
81
+ const { tab } = useTabInfo();
82
+ const sessionId = parseChatAddress(tab.contentId);
83
+ const [reference, setReference] = (0, react.useState)();
84
+ (0, react.useEffect)(() => {
85
+ if (sessionId === void 0) return void 0;
86
+ const held = retain(sessionId);
87
+ setReference(held);
88
+ return () => {
89
+ setReference(void 0);
90
+ held.release();
91
+ };
92
+ }, [retain, sessionId]);
93
+ if (reference === void 0) return null;
94
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
95
+ style: {
96
+ height: "100%",
97
+ display: "flex",
98
+ flexDirection: "column",
99
+ minHeight: 0
100
+ },
101
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionProvider, {
102
+ session: reference,
103
+ children: renderSlot(CONVERSATION_SLOT, {})
104
+ })
105
+ });
106
+ }
107
+ /**
108
+ * DSH's shared conversation and composer for the surrounding Session, embedded
109
+ * as DSH's subagent tab embeds it: Chat view only, without the main header and
110
+ * width controls.
111
+ */
112
+ function ConversationPanel({ renderFactorySlot }) {
113
+ return renderFactorySlot("conversation.content", {
114
+ variant: "embedded",
115
+ phase: "active",
116
+ hero: false
117
+ }, { slots: { views: ChatView } });
118
+ }
119
+ function ChatView({ renderSlot }) {
120
+ return renderSlot("conversation.session", { view: "chat" });
121
+ }
122
+ // src/client/messages.ts
123
+ /** DSH locale namespace of this plugin's messages. */
124
+ const NAMESPACE = "langify.sideChat";
125
+ const en = {
126
+ title: "Side chat",
127
+ action: "Ask in a side chat",
128
+ guide: "Ask about this conversation without interrupting it",
129
+ starting: "Starting a side chat…",
130
+ retry: "Retry",
131
+ "error.invalid_request": "The request was invalid: {message}",
132
+ "error.fork_failed": "The side chat could not start: {message}",
133
+ "error.internal_error": "Something went wrong: {message}",
134
+ "error.unexpected": "Something went wrong: {message}"
135
+ };
136
+ const zh = {
137
+ title: "旁路对话",
138
+ action: "在旁路对话中提问",
139
+ guide: "在不打断当前对话的情况下提问",
140
+ starting: "正在启动旁路对话…",
141
+ retry: "重试",
142
+ "error.invalid_request": "请求无效:{message}",
143
+ "error.fork_failed": "旁路对话启动失败:{message}",
144
+ "error.internal_error": "出现错误:{message}",
145
+ "error.unexpected": "出现错误:{message}"
146
+ };
147
+ const ja = {
148
+ title: "サイドチャット",
149
+ action: "サイドチャットで質問する",
150
+ guide: "今の会話を止めずに質問する",
151
+ starting: "サイドチャットを始めています…",
152
+ retry: "もう一度",
153
+ "error.invalid_request": "リクエストが正しくありません:{message}",
154
+ "error.fork_failed": "サイドチャットを始められませんでした:{message}",
155
+ "error.internal_error": "エラーが起きました:{message}",
156
+ "error.unexpected": "エラーが起きました:{message}"
157
+ };
158
+ // src/client/start-page.tsx
159
+ /** The guide card's page: starts a side chat at once and is replaced by it. */
160
+ function StartPage({ start, describe, t, useTabInfo }) {
161
+ const { tab } = useTabInfo();
162
+ const [attempt, setAttempt] = (0, react.useState)(0);
163
+ const [error, setError] = (0, react.useState)();
164
+ const started = (0, react.useRef)(-1);
165
+ (0, react.useEffect)(() => {
166
+ if (started.current === attempt) return;
167
+ started.current = attempt;
168
+ setError(void 0);
169
+ start(tab.id).catch((failure) => {
170
+ if (!tab.signal.aborted) setError(describe(failure));
171
+ });
172
+ }, [
173
+ attempt,
174
+ start,
175
+ describe,
176
+ tab.id,
177
+ tab.signal
178
+ ]);
179
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
180
+ role: error === void 0 ? "status" : "alert",
181
+ style: styles.page,
182
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: error ?? t("starting") }), error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
183
+ variant: "outline",
184
+ size: "sm",
185
+ onClick: () => setAttempt((n) => n + 1),
186
+ children: t("retry")
187
+ })]
188
+ });
189
+ }
190
+ const styles = { page: {
191
+ height: "100%",
192
+ display: "flex",
193
+ flexDirection: "column",
194
+ alignItems: "center",
195
+ justifyContent: "center",
196
+ gap: 12,
197
+ padding: 16,
198
+ textAlign: "center",
199
+ overflowWrap: "anywhere",
200
+ color: "var(--dsw-alias-label-secondary)"
201
+ } };
202
+ // src/client/index.tsx
203
+ const inject = [
204
+ "connection",
205
+ "locale",
206
+ "sessions",
207
+ "slots",
208
+ "sidebarRight",
209
+ "sidebarRightTabs",
210
+ "uiWorkspace"
211
+ ];
212
+ const KIND = "langify-side-chat";
213
+ const TAB_ID = "langify-side-chat/chat";
214
+ const START_KIND = "langify-side-chat-new";
215
+ const START_ID = "langify-side-chat/new";
216
+ const RPC_TIMEOUT_MS = 6e4;
217
+ const ACTION_CLASS = "langify-side-chat-action";
218
+ const ACTION_CSS = `.${ACTION_CLASS}{width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;padding:6px;display:inline-flex}.${ACTION_CLASS} svg{width:calc(15px + var(--dsh-content-font-delta,0px));height:calc(15px + var(--dsh-content-font-delta,0px))}.${ACTION_CLASS}:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.${ACTION_CLASS}:disabled{cursor:default;opacity:.4}`;
219
+ function apply(ctx) {
220
+ ctx.effect(() => ctx.locale.register(NAMESPACE, {
221
+ en,
222
+ zh
223
+ }), "side-chat: dictionaries");
224
+ ctx.effect(() => ctx.locale.register(NAMESPACE, "ja", ja), "side-chat: Japanese dictionary");
225
+ const t = ctx.locale.bind(NAMESPACE);
226
+ ctx.effect(() => {
227
+ const style = document.createElement("style");
228
+ style.textContent = ACTION_CSS;
229
+ document.head.append(style);
230
+ return () => style.remove();
231
+ }, "side-chat: styles");
232
+ const chatFace = { retain: (sessionId) => ctx.sessions.retain(sessionId, { source: "langifySideChat" }) };
233
+ ctx.effect(() => ctx.sidebarRightTabs.register({
234
+ id: TAB_ID,
235
+ kind: KIND,
236
+ patterns: [`${ADDRESS_PREFIX}**`],
237
+ canOpen: (address) => parseChatAddress(address) !== void 0,
238
+ title: (address) => {
239
+ const id = parseChatAddress(address);
240
+ return id && ctx.sessions.list.getSnapshot().byId[id]?.title || t("title");
241
+ },
242
+ keepMounted: true
243
+ }), "side-chat: chat tab type");
244
+ ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
245
+ name: "sidebar.right.pane.tab",
246
+ key: TAB_ID,
247
+ inject: () => chatFace,
248
+ children: { [CONVERSATION_SLOT]: {
249
+ kind: "single",
250
+ scope: "session"
251
+ } }
252
+ }, ChatTab)), "side-chat: chat tab");
253
+ ctx.effect(() => ctx.slots.inject(CONVERSATION_SLOT, () => ctx.slots.register({ name: CONVERSATION_SLOT }, ConversationPanel)), "side-chat: conversation");
254
+ const showChat = (fork, replaceTab) => ctx.sidebarRight.openResource(chatAddress(fork), {
255
+ kind: KIND,
256
+ ...replaceTab === void 0 ? {} : { replaceTab }
257
+ });
258
+ /** Fork `sessionId` into a side chat and show it, in place of `replaceTab` when given. */
259
+ const open = async (sessionId, replaceTab) => {
260
+ const source = ctx.sessions.list.getSnapshot().byId[SessionId(sessionId)];
261
+ const title = `${t("title")}: ${source?.title ?? source?.displayTitle ?? sessionId}`;
262
+ const result = await ctx.connection.rpc.call(RPC_CHANNEL, RPC_METHOD, {
263
+ sessionId,
264
+ title
265
+ }, AbortSignal.timeout(RPC_TIMEOUT_MS));
266
+ if (!result.ok) throw new RpcFailure(result.error.code, result.error.message);
267
+ const fork = createResponseSchema.parse(result.value).sessionId;
268
+ showChat(fork, replaceTab);
269
+ };
270
+ ctx.effect(() => ctx.sidebarRightTabs.register({
271
+ id: START_ID,
272
+ kind: START_KIND,
273
+ title: () => t("title"),
274
+ guide: [{
275
+ id: "new-side-chat",
276
+ order: 30,
277
+ title: () => t("title"),
278
+ description: () => t("guide")
279
+ }]
280
+ }), "side-chat: start page type");
281
+ ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
282
+ name: "sidebar.right.pane.tab",
283
+ key: START_ID,
284
+ locale: NAMESPACE,
285
+ inject: (sessionId) => ({
286
+ start: (tab) => open(sessionId, tab),
287
+ describe: (error) => errorText(error, t)
288
+ })
289
+ }, StartPage)), "side-chat: start page");
290
+ let reopen;
291
+ ctx.effect(() => ctx.sidebarRight.mounted.subscribe(() => {
292
+ const shown = ctx.sidebarRight.mounted.getSnapshot();
293
+ if (shown === void 0) return;
294
+ if (reopen?.parent === shown) {
295
+ const { chat } = reopen;
296
+ reopen = void 0;
297
+ setTimeout(() => showChat(chat));
298
+ return;
299
+ }
300
+ const row = ctx.sessions.list.getSnapshot().byId[shown];
301
+ const parent = row?.parentId;
302
+ if (row?.projectionValues?.["langifySideChat"] !== true || parent === void 0) return;
303
+ reopen = {
304
+ parent,
305
+ chat: shown
306
+ };
307
+ setTimeout(() => ctx.uiWorkspace.openSession(parent));
308
+ }), "side-chat: open beside its conversation");
309
+ ctx.effect(() => ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
310
+ name: "conversation.chat.assistant-actions",
311
+ id: "langify-side-chat",
312
+ order: 20,
313
+ locale: NAMESPACE,
314
+ inject: (sessionId) => ({ open: () => open(sessionId) })
315
+ }, SideChatAction)), "side-chat: message action");
316
+ }
317
+ /** A failed call; `code` is one of the host's error codes unless DSH's transport failed first. */
318
+ var RpcFailure = class extends Error {
319
+ code;
320
+ constructor(code, message) {
321
+ super(message);
322
+ this.code = code;
323
+ }
324
+ };
325
+ function SideChatAction({ open, t }) {
326
+ const [pending, setPending] = (0, react.useState)(false);
327
+ const [failure, setFailure] = (0, react.useState)();
328
+ const onClick = () => {
329
+ setPending(true);
330
+ setFailure(void 0);
331
+ open().catch((error) => setFailure(errorText(error, t))).finally(() => setPending(false));
332
+ };
333
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
334
+ label: t("action"),
335
+ side: "bottom",
336
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
337
+ type: "button",
338
+ className: ACTION_CLASS,
339
+ "aria-label": t("action"),
340
+ disabled: pending,
341
+ onClick,
342
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutlineRegular, {})
343
+ })
344
+ }), failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
345
+ role: "status",
346
+ style: {
347
+ fontSize: 13,
348
+ color: "var(--dsw-alias-label-tertiary)"
349
+ },
350
+ children: failure
351
+ })] });
352
+ }
353
+ function errorText(error, t) {
354
+ const message = error instanceof Error ? error.message : String(error);
355
+ const code = error instanceof RpcFailure ? error.code : void 0;
356
+ const known = ERROR_CODES.find((value) => value === code);
357
+ return known === void 0 ? t("error.unexpected", { message }) : t(`error.${known}`, { message });
358
+ }
359
+ exports.apply = apply;
360
+ exports.inject = inject;
361
+ return module.exports;
362
+ }
363
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,144 @@
1
+ import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
2
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
3
+ import { SessionId } from "@deepseek-ai/dsh-session/types";
4
+ import { z } from "zod";
5
+ // src/rpc.ts
6
+ /** DSH's shared RPC channel, and the endpoint this plugin owns on it. */
7
+ const RPC_CHANNEL = "/api";
8
+ const RPC_METHOD = "langify-side-chat/create";
9
+ /** Session projection: `true` for a side chat, from its creation. */
10
+ const PROJECTION_KEY = "langifySideChat";
11
+ const id = z.string().min(1).max(256).refine((value) => !value.includes("\0"));
12
+ const createRequestSchema = z.strictObject({
13
+ sessionId: id,
14
+ title: z.string().trim().min(1).max(200)
15
+ });
16
+ z.strictObject({ sessionId: id });
17
+ // src/host/index.ts
18
+ const name = "langify-side-chat";
19
+ const SOURCE_KIND = "langify-side-chat";
20
+ const inject = [
21
+ "connection",
22
+ "sessionController",
23
+ "sessionProjections"
24
+ ];
25
+ /** Model-facing; the Chat view hides context messages, so the user never sees it. */
26
+ const SIDE_CHAT_CONTEXT = `This is a side chat. The user opened it beside the conversation above to ask questions about it without interrupting it. The conversation above is a copy you share as background; your replies here never reach it.
27
+
28
+ - Answer the user's questions in this side chat; do not resume or continue the main task.
29
+ - Investigate as needed, but do not change files, run state-changing commands, or take other actions with side effects unless the user explicitly asks for them in this side chat.
30
+ - Keep answers short and to the point.`;
31
+ var SideChatError = class extends Error {
32
+ code;
33
+ constructor(code, message, options) {
34
+ super(message, options);
35
+ this.code = code;
36
+ }
37
+ };
38
+ function apply(ctx) {
39
+ ctx.effect(() => ctx.sessionProjections.register(sideChatProjection), "side-chat: projection");
40
+ async function create(request) {
41
+ let sessionId;
42
+ try {
43
+ ({sessionId} = await ctx.sessionController.fork({ sessionId: SessionId(request.sessionId) }));
44
+ } catch (error) {
45
+ throw new SideChatError("fork_failed", messageOf(error), { cause: error });
46
+ }
47
+ const resolved = await ctx.sessionController.resolveAgent(sessionId);
48
+ if ("error" in resolved) throw new SideChatError("fork_failed", resolved.error.message);
49
+ resolved.agent.inject(sideChatContext());
50
+ await ctx.sessionController.rename({
51
+ sessionId: SessionId(sessionId),
52
+ title: request.title
53
+ }).catch((error) => ctx.logger.warn(`side-chat: rename failed: ${messageOf(error)}`));
54
+ return { sessionId };
55
+ }
56
+ const route = {
57
+ path: `${RPC_CHANNEL}/${RPC_METHOD}`,
58
+ methods: ["POST"],
59
+ requestBody: "buffered",
60
+ async fetch(request) {
61
+ if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return new Response("JSON required", { status: 415 });
62
+ const body = await request.json().catch(() => void 0);
63
+ const envelope = clientRequestSchema.safeParse(body);
64
+ if (!envelope.success || envelope.data.method !== "langify-side-chat/create") return new Response("Invalid RPC envelope", { status: 400 });
65
+ const response = {
66
+ type: "server-response",
67
+ rpcId: envelope.data.rpcId,
68
+ result: await answer(envelope.data.payload)
69
+ };
70
+ return Response.json(response);
71
+ }
72
+ };
73
+ async function answer(payload) {
74
+ try {
75
+ const request = createRequestSchema.safeParse(payload);
76
+ if (!request.success) throw new SideChatError("invalid_request", z.prettifyError(request.error));
77
+ return {
78
+ ok: true,
79
+ value: await create(request.data)
80
+ };
81
+ } catch (error) {
82
+ const { code, message } = error instanceof SideChatError ? error : {
83
+ code: "internal_error",
84
+ message: messageOf(error)
85
+ };
86
+ return {
87
+ ok: false,
88
+ error: {
89
+ code,
90
+ message,
91
+ details: {}
92
+ }
93
+ };
94
+ }
95
+ }
96
+ ctx.effect(() => ctx.connection.fetch.register(route), `side-chat: ${route.path}`);
97
+ }
98
+ function sideChatContext() {
99
+ return createUserMessage({
100
+ content: [{
101
+ type: "text",
102
+ text: SIDE_CHAT_CONTEXT
103
+ }],
104
+ source: {
105
+ kind: SOURCE_KIND,
106
+ form: "instructions"
107
+ }
108
+ });
109
+ }
110
+ /**
111
+ * `langifySideChat`: whether the session's own log holds the side-chat context,
112
+ * from the moment it enters the inbox. A Fork of a side chat inherits the
113
+ * context but is not one.
114
+ */
115
+ const sideChatProjection = {
116
+ key: PROJECTION_KEY,
117
+ stateVersion: 1,
118
+ stateSchema: z.strictObject({
119
+ inherited: z.int().nonnegative(),
120
+ sideChat: z.boolean()
121
+ }),
122
+ init: (_header, inheritedEventCount) => ({
123
+ inherited: inheritedEventCount,
124
+ sideChat: false
125
+ }),
126
+ apply: (state, event) => state.sideChat || event.seq < state.inherited || !carriesContext(event) ? state : {
127
+ ...state,
128
+ sideChat: true
129
+ },
130
+ wire: {
131
+ viewSchema: z.boolean(),
132
+ view: (state) => state.sideChat
133
+ }
134
+ };
135
+ const isContext = (message) => message.source.kind === SOURCE_KIND;
136
+ function carriesContext(event) {
137
+ if (event.type === "user/message") return isContext(event.data);
138
+ if (event.type === "agent/inbox/spliced") return event.data.inserted.some(isContext);
139
+ return false;
140
+ }
141
+ function messageOf(error) {
142
+ return error instanceof Error ? error.message : String(error);
143
+ }
144
+ export { SIDE_CHAT_CONTEXT, apply, inject, name, sideChatProjection };
package/package.json CHANGED
@@ -1,6 +1,78 @@
1
1
  {
2
2
  "name": "@langify-org/dsh-side-chat",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Ask a side question beside a DeepSeek Harness conversation, in a fork shown in the right sidebar",
5
+ "keywords": [
6
+ "deepseek-harness",
7
+ "dsh",
8
+ "dsh-plugin",
9
+ "side-chat"
10
+ ],
11
+ "homepage": "https://github.com/langify-org/deepseek-harness-plugins/tree/main/packages/side-chat#readme",
12
+ "bugs": "https://github.com/langify-org/deepseek-harness-plugins/issues",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/langify-org/deepseek-harness-plugins.git",
17
+ "directory": "packages/side-chat"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "cordis.patch.yml",
22
+ "README*.md"
23
+ ],
24
+ "type": "module",
25
+ "exports": {
26
+ ".": "./dist/index.mjs",
27
+ "./client": "./dist/client.cjs",
28
+ "./package.json": "./package.json"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "zod": "4.4.3"
35
+ },
36
+ "devDependencies": {
37
+ "@deepseek-ai/cordis": "4.0.5",
38
+ "@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.5",
39
+ "@deepseek-ai/dsh-client-connection": "0.2.1-rc.5",
40
+ "@deepseek-ai/dsh-client-locale": "0.2.1-rc.5",
41
+ "@deepseek-ai/dsh-client-ui-chat": "0.2.1-rc.5",
42
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.5",
43
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.5",
44
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.5",
45
+ "@deepseek-ai/dsh-client-ui-sidebar-right": "0.2.1-rc.5",
46
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.5",
47
+ "@deepseek-ai/dsh-llm": "0.2.1-rc.5",
48
+ "@deepseek-ai/dsh-session": "0.2.1-rc.5",
49
+ "@deepseek-ai/dsh-session-projection": "0.2.1-rc.5",
50
+ "@types/react": "18.3.31"
51
+ },
52
+ "peerDependencies": {
53
+ "@deepseek-ai/cordis": "^4.0.5",
54
+ "@deepseek-ai/dsh-client-connection": ">=0.2.1-rc.5 <0.3.0-0",
55
+ "@deepseek-ai/dsh-llm": ">=0.2.1-rc.5 <0.3.0-0",
56
+ "@deepseek-ai/dsh-session": ">=0.2.1-rc.5 <0.3.0-0"
57
+ },
58
+ "engines": {
59
+ "dsh": ">=0.2.1-rc.5 <0.3.0-0"
60
+ },
61
+ "dsh": {
62
+ "bundle": {
63
+ "patch": "./cordis.patch.yml"
64
+ },
65
+ "client": {
66
+ "platform": "web",
67
+ "inject": [
68
+ "@deepseek-ai/dsh-client-connection",
69
+ "@deepseek-ai/dsh-client-locale",
70
+ "@deepseek-ai/dsh-client-ui-sidebar-right"
71
+ ]
72
+ }
73
+ },
74
+ "scripts": {
75
+ "build": "tsdown",
76
+ "typecheck": "tsc -p src/client"
77
+ }
6
78
  }