dsh-just-chat 0.1.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,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # 快速对话 · Just Chat
2
+
3
+ [![npm version](https://img.shields.io/npm/v/dsh-just-chat?logo=npm)](https://www.npmjs.com/package/dsh-just-chat)
4
+ [![CI](https://github.com/exoticknight/dsh-just-chat/actions/workflows/ci.yml/badge.svg)](https://github.com/exoticknight/dsh-just-chat/actions/workflows/ci.yml)
5
+ [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/exoticknight/dsh-just-chat/blob/main/LICENSE)
6
+ ![DSH compatibility: 0.1.5-rc.2](https://img.shields.io/badge/DSH-0.1.5--rc.2-blue)
7
+ <a href="https://github.com/exoticknight/red"><img src="https://img.shields.io/badge/maintained_with-RED-C1121F" alt="Maintained with RED"></a>
8
+
9
+ 为 DeepSeek Harness 添加一键对话入口:点击即可进入独立工作目录中的原生会话,省去手动选择工作区。
10
+
11
+ ![首页标准模式旁和侧栏的快速对话入口](https://raw.githubusercontent.com/exoticknight/dsh-just-chat/main/docs/images/quick-chat.png)
12
+
13
+ ## 功能特点
14
+
15
+ - **一键开始**:首页“标准模式”旁、侧栏各有一个入口,可分别开启或关闭。
16
+ - **独立目录**:每次完成一次入口点击,都会新建独立工作区及对话。启动插件时不预建工作区。
17
+ - **原生对话体验**:复用 DSH 的会话、模型、权限与附件流程;Workspace 和 Session ID 由 DSH 生成。
18
+ - **自定义命名**:用占位符设置新工作区名称,编辑时预览,保存后生效。
19
+ - **融入现有界面**:使用 DSH 原生图标和按钮,窄窗口自动换行;中文显示“快速对话”,英文显示“Just Chat”。
20
+
21
+ ## 快速开始
22
+
23
+ ### 安装插件
24
+
25
+ 适用于 DSH `0.1.5-rc.2` 的 Web 界面。先确认你的 DSH 已能正常对话,并且终端中可以使用 `dsh`。
26
+
27
+ 通过 DSH 插件管理命令从 npm 安装:
28
+
29
+ ```sh
30
+ dsh plugin --profile web add dsh-just-chat
31
+ ```
32
+
33
+ 如果你使用自定义 `DSH_HOME` 或其他 profile,请选择与平时启动 DSH 相同的环境。安装后重启该 DSH 并刷新页面。
34
+
35
+ ### 开始对话
36
+
37
+ 1. 打开你平时使用的 DSH 页面。
38
+ 2. 点击首页“标准模式”旁或侧栏的 **快速对话**。
39
+ 3. 在打开的对话中输入消息并发送。
40
+
41
+ 插件会自动准备独立工作目录并打开原生对话,无需选择目录。模型与权限沿用 DSH 的配置。
42
+
43
+ ## 配置入口与命名
44
+
45
+ 打开 **设置 → 插件 → 插件配置 → 快速对话**。
46
+
47
+ ![快速对话配置卡片:入口开关、名称模板、预览和保存操作](https://raw.githubusercontent.com/exoticknight/dsh-just-chat/main/docs/images/settings.png)
48
+
49
+ “标准模式旁入口”和“侧栏入口”控制两个按钮的显示位置。修改开关或模板后,卡片会标记“未保存”;点击“保存”统一应用,或点击“放弃更改”恢复已保存的值。配置集中在这张卡片中。
50
+
51
+ ### 工作区名称模板
52
+
53
+ 默认模板:
54
+
55
+ ```text
56
+ {label} · {MM}-{DD} {HH}:{mm}
57
+ ```
58
+
59
+ 例如,中文界面创建的工作区显示为 `快速对话 · 09-14 18:01`;英文界面显示为 `Just Chat · 09-14 18:01`。
60
+
61
+ | 占位符 | 含义 | 示例 |
62
+ | --- | --- | --- |
63
+ | `{label}` | 创建时的本地化名称 | 快速对话 / Just Chat |
64
+ | `{YYYY}` | 四位年份 | 2026 |
65
+ | `{MM}` / `{DD}` | 两位月份 / 日期 | 09 / 14 |
66
+ | `{HH}` / `{mm}` / `{ss}` | 24 小时制的时 / 分 / 秒 | 18 / 01 / 05 |
67
+
68
+ 也可以写成 `灵感 {YYYY}-{MM}-{DD}` 或 `{label} {HH}:{mm}:{ss}`,普通文字会原样保留。
69
+
70
+ - 时间取自浏览器本地时间,占位符区分大小写。
71
+ - 模板最多 200 个字符;展开并去除首尾空白后的名称必须为 1–100 个字符。
72
+ - 未知或不完整的占位符会显示错误并阻止保存。
73
+ - “恢复默认模板”只修改草稿,还需点击“保存”。
74
+ - 模板与语言变更只影响之后新建的工作区,不会重命名已有工作区,也不会改变磁盘目录。
75
+
76
+ ## 工作区与对话的关系
77
+
78
+ 每次通过快速对话入口创建的工作目录位于:
79
+
80
+ ```text
81
+ <DSH_HOME>/just-chat/sessions/chat-<随机后缀>
82
+ ```
83
+
84
+ `DSH_HOME` 是当前 DSH 的用户数据目录。显示名称与目录名分开管理;即使两个工作区显示名称相同,目录和原生 ID 仍然不同。
85
+
86
+ | 操作 | 结果 |
87
+ | --- | --- |
88
+ | 点击快速对话入口 | 新建独立目录、工作区和原生对话 |
89
+ | 在该工作区使用 DSH 原生“新建会话” | 仍使用该工作区的目录 |
90
+ | 归档会话 | 隐藏会话,保留记录和目录,可恢复 |
91
+ | 原生删除工作区 | 移除工作区登记,保留目录与会话记录,会话转入“未分组” |
92
+
93
+ 快速对话创建的会话和工作区使用 DSH 原生管理操作。删除工作区后,磁盘上的工作目录仍会保留;快速对话插件不会自动清理这些目录。
94
+
95
+ ## 开发与维护
96
+
97
+ 本地环境初始化、调试、测试和实现说明见[开发指南](https://github.com/exoticknight/dsh-just-chat/blob/main/docs/development.md)。
package/RED.md ADDED
@@ -0,0 +1,18 @@
1
+ # RED 工作约定
2
+
3
+ 本项目使用 RED Protocol 1,把未解决的研究证据、进行中的工程变更和已接受的项目知识分开管理。
4
+
5
+ ## 当前映射
6
+
7
+ - Document:`README.md` 与 `docs/**`。README 面向使用者说明安装、功能、配置与生命周期;`docs/development.md` 面向开发者说明初始化、调试、测试与实现结构;`docs/images/` 保存配图。
8
+ - Research:`.research/**`。这里保留既有纯净 DSH 调研、运行时证据和交接材料,不迁移旧文件,也不把其中的历史工作记录当作规范要求。
9
+ - Evolve:`.evolve/**`。这里记录当前 Just Chat 插件的实施范围、验收证据、未决替代方案和后续工作。
10
+
11
+ 项目使用 Git。Research 和 Evolve 由 `.gitignore` 排除,仅作为本地工作记录保存;Document 和配置纳入版本管理。
12
+
13
+ ## 维护规则
14
+
15
+ - 新 Research / Evolve 文件使用 UTF-8 Markdown 和 RED TOML front matter。
16
+ - Document 只写已接受、可供使用者和后续 agent 直接依赖的知识;验证日志、历史过程和开放问题留在 Research / Evolve。
17
+ - Evolve 的验收证据提交评审后,获得单独接受前不更新 Document。
18
+ - DSH 宿主和宿主 UI 的边界以实现证据为准;如规范文档与代码、配置或运行时结果冲突,先记录为 Research 冲突。
package/client.js ADDED
@@ -0,0 +1,425 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-just-chat',
3
+ factory: (require) => {
4
+ const React = require('react')
5
+ const { createPortal } = require('react-dom')
6
+ const { Button, Tag, IconChevronDownOutline14, IconNewChatOutline16 } = require('@deepseek-ai/dsh-client-ui-primitives')
7
+ const module = { exports: {} }
8
+ const exports = module.exports
9
+
10
+ let runtimeCtx
11
+ let entrySettings
12
+ let sidebarRegistration
13
+ let sidebarInjectionLive = false
14
+ let openingChat
15
+ let pendingPath
16
+
17
+ const localeNamespace = 'dsh-just-chat'
18
+ const settingsNamespace = 'dsh-just-chat'
19
+ const defaultNameTemplate = '{label} · {MM}-{DD} {HH}:{mm}'
20
+ const defaultSettings = Object.freeze({ hero: true, sidebar: true, workspaceNameTemplate: defaultNameTemplate })
21
+ const fallbackSettingsSnapshot = Object.freeze({
22
+ status: 'loading',
23
+ value: defaultSettings,
24
+ writable: false,
25
+ })
26
+
27
+ const localeDictionaries = {
28
+ zh: {
29
+ entry: '快速对话',
30
+ opening: '打开中…',
31
+ 'panel.loaded': '正在打开快速对话…',
32
+ 'panel.failed': '打开失败:{error}',
33
+ 'panel.retry': '重试',
34
+ 'settings.description': '配置快速对话入口和新工作区的命名规则',
35
+ 'settings.hero': '标准模式旁入口',
36
+ 'settings.sidebar': '侧栏入口',
37
+ 'settings.template': '工作区名称模板',
38
+ 'settings.help': '可用占位符:{label} 本地化名称、{YYYY} 年、{MM} 月、{DD} 日、{HH} 时、{mm} 分、{ss} 秒。使用本地时间,仅影响新建工作区。',
39
+ 'settings.preview': '预览',
40
+ 'settings.save': '保存',
41
+ 'settings.reset': '恢复默认模板',
42
+ 'settings.invalid': '请使用已列出的占位符,且展开后的名称须为 1–100 个字符。',
43
+ 'settings.discard': '放弃更改',
44
+ 'settings.unsaved': '未保存',
45
+ 'settings.saving': '保存中…',
46
+ 'settings.expand': '展开设置',
47
+ 'settings.collapse': '收起设置',
48
+ 'settings.readOnly': '当前设置只读。',
49
+ },
50
+ en: {
51
+ entry: 'Just Chat',
52
+ opening: 'Opening…',
53
+ 'panel.loaded': 'Opening Just Chat…',
54
+ 'panel.failed': 'Could not open Just Chat: {error}',
55
+ 'panel.retry': 'Retry',
56
+ 'settings.description': 'Configure entry points and names for new workspaces',
57
+ 'settings.hero': 'Beside Standard mode',
58
+ 'settings.sidebar': 'In the sidebar',
59
+ 'settings.template': 'Workspace name template',
60
+ 'settings.help': 'Placeholders: {label} localized name, {YYYY} year, {MM} month, {DD} day, {HH} hour, {mm} minute, {ss} second. Uses local time; affects new workspaces only.',
61
+ 'settings.preview': 'Preview',
62
+ 'settings.save': 'Save',
63
+ 'settings.reset': 'Restore default template',
64
+ 'settings.invalid': 'Use the listed placeholders and a resulting name of 1–100 characters.',
65
+ 'settings.discard': 'Discard changes',
66
+ 'settings.unsaved': 'Unsaved',
67
+ 'settings.saving': 'Saving…',
68
+ 'settings.expand': 'Show settings',
69
+ 'settings.collapse': 'Hide settings',
70
+ 'settings.readOnly': 'These settings are read-only.',
71
+ },
72
+ }
73
+
74
+ function PanelIcon({ size = 16 }) {
75
+ return React.createElement(IconNewChatOutline16, { size })
76
+ }
77
+
78
+ // Deliberately a small literal-token formatter, not an executable template language.
79
+ function formatWorkspaceName(template, label, now = new Date()) {
80
+ const pad = (value) => String(value).padStart(2, '0')
81
+ const values = { label, YYYY: String(now.getFullYear()), MM: pad(now.getMonth() + 1),
82
+ DD: pad(now.getDate()), HH: pad(now.getHours()), mm: pad(now.getMinutes()), ss: pad(now.getSeconds()) }
83
+ if (typeof template !== 'string' || template.length > 200) throw new Error('Invalid template')
84
+ const title = template.replace(/\{([^{}]*)\}/g, (token, key) => {
85
+ if (!Object.hasOwn(values, key)) throw new Error('Unknown placeholder')
86
+ return values[key]
87
+ }).trim()
88
+ if (/[{}]/.test(title) || !title || title.length > 100) throw new Error('Invalid workspace name')
89
+ return title
90
+ }
91
+
92
+ function useJustChatT() {
93
+ const locale = runtimeCtx.get('locale')
94
+ const subscribe = React.useCallback((listener) => locale.subscribe(listener), [locale])
95
+ const getRevision = React.useCallback(() => locale.getSnapshot().revision, [locale])
96
+ React.useSyncExternalStore(subscribe, getRevision, getRevision)
97
+ return locale.bind(localeNamespace)
98
+ }
99
+
100
+ function useEntrySettings() {
101
+ const scope = entrySettings
102
+ const subscribe = React.useCallback(
103
+ (listener) => scope?.subscribe(listener) ?? (() => {}),
104
+ [scope],
105
+ )
106
+ const getSnapshot = React.useCallback(
107
+ () => scope?.getSnapshot?.() ?? fallbackSettingsSnapshot,
108
+ [scope],
109
+ )
110
+ return React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
111
+ }
112
+
113
+ function settingEnabled(snapshot, key) {
114
+ return snapshot?.value?.[key] !== false
115
+ }
116
+
117
+ function openJustChat() {
118
+ if (openingChat) return openingChat
119
+ openingChat = (async () => {
120
+ if (!pendingPath) {
121
+ let title
122
+ const t = runtimeCtx.get('locale').bind(localeNamespace)
123
+ try {
124
+ title = formatWorkspaceName(entrySettings.getSnapshot()?.value?.workspaceNameTemplate ?? defaultNameTemplate, t('entry'))
125
+ } catch {
126
+ throw new Error(t('settings.invalid'))
127
+ }
128
+ const response = await fetch('/api/just-chat/prepare', {
129
+ method: 'POST',
130
+ headers: { 'Content-Type': 'application/json' },
131
+ body: JSON.stringify({ title }),
132
+ })
133
+ if (!response.ok) throw new Error(await response.text())
134
+ const result = await response.json()
135
+ pendingPath = result.path
136
+ }
137
+ // Native create adopts the registered path and synchronizes Client state.
138
+ // Keep it for retry if opening fails; both native operations are idempotent.
139
+ const workspace = await runtimeCtx.get('workspaces').create({ path: pendingPath })
140
+ await runtimeCtx.get('uiWorkspace').openWorkspace(workspace.workspaceId)
141
+ pendingPath = undefined
142
+ })().finally(() => { openingChat = undefined })
143
+ return openingChat
144
+ }
145
+
146
+ function useHeroMount() {
147
+ const [mount, setMount] = React.useState(null)
148
+ React.useEffect(() => {
149
+ const node = document.createElement('span')
150
+ node.dataset.justChatMount = 'true'
151
+ const style = document.createElement('style')
152
+ style.textContent = 'div:has(> [data-just-chat-mount]) { flex-wrap: wrap; row-gap: 4px; }'
153
+ document.head.appendChild(style)
154
+ Object.assign(node.style, { display: 'inline-flex', minWidth: '0', flexShrink: '0', marginLeft: '6px' })
155
+ const sync = () => {
156
+ const slot = document.querySelector('[data-slot="conversation.hero.agentPreset"]')
157
+ if (!slot) {
158
+ node.remove()
159
+ setMount(null)
160
+ } else {
161
+ // Join the existing flex row, so long workspace labels reserve space for the action.
162
+ if (node.previousSibling !== slot) slot.after(node)
163
+ setMount(node)
164
+ }
165
+ }
166
+ const observer = new MutationObserver(sync)
167
+ observer.observe(document.getElementById('root') ?? document.body, { childList: true, subtree: true })
168
+ sync()
169
+ return () => { observer.disconnect(); node.remove(); style.remove() }
170
+ }, [])
171
+ return mount
172
+ }
173
+
174
+ function JustChatOverlay() {
175
+ const t = useJustChatT()
176
+ const settings = useEntrySettings()
177
+ const mount = useHeroMount()
178
+ const [busy, setBusy] = React.useState(false)
179
+ const [error, setError] = React.useState(null)
180
+ if (!mount || !settingEnabled(settings, 'hero')) return null
181
+ const openChat = async () => {
182
+ if (busy) return
183
+ setBusy(true)
184
+ setError(null)
185
+ try { await openJustChat() } catch (error) { setError(error.message) }
186
+ finally { setBusy(false) }
187
+ }
188
+ return createPortal(React.createElement('span', { style: { position: 'relative' } },
189
+ React.createElement(Button, {
190
+ variant: 'ghost', size: 'sm', onClick: openChat, disabled: busy,
191
+ 'aria-label': busy ? t('opening') : t('entry'), title: t('entry'),
192
+ style: { borderRadius: 16, height: 28, padding: '0 8px', gap: 4, whiteSpace: 'nowrap' },
193
+ }, React.createElement(IconNewChatOutline16, { size: 14 }), busy ? t('opening') : t('entry')),
194
+ error && React.createElement('span', { role: 'alert', style: {
195
+ position: 'absolute', top: '100%', right: 0, width: 280, zIndex: 10,
196
+ padding: 8, background: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-error)',
197
+ } }, error),
198
+ ), mount)
199
+ }
200
+
201
+ function ChatPanel() {
202
+ const t = useJustChatT()
203
+ const [attempt, setAttempt] = React.useState(0)
204
+ const [message, setMessage] = React.useState({ key: 'panel.loaded' })
205
+
206
+ React.useEffect(() => {
207
+ let active = true
208
+ setMessage({ key: 'panel.loaded' })
209
+ void openJustChat().catch((error) => {
210
+ if (active) {
211
+ setMessage({
212
+ key: 'panel.failed',
213
+ params: { error: error?.message ?? JSON.stringify(error) },
214
+ })
215
+ }
216
+ })
217
+ return () => {
218
+ active = false
219
+ }
220
+ }, [attempt])
221
+
222
+ return React.createElement(
223
+ 'section',
224
+ { style: { fontFamily: 'sans-serif', padding: '24px' } },
225
+ React.createElement('h1', null, t('entry')),
226
+ React.createElement('p', null, t(message.key, message.params)),
227
+ message.key === 'panel.failed'
228
+ ? React.createElement(
229
+ Button,
230
+ { variant: 'outline', size: 'sm', onClick: () => setAttempt((value) => value + 1), type: 'button' },
231
+ t('panel.retry'),
232
+ )
233
+ : null,
234
+ )
235
+ }
236
+
237
+ // The host's PluginCard is private. Match its layout with owned, scoped styles.
238
+ const settingsCss = `
239
+ .jc-card{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);border-radius:16px;list-style:none;transition:border-color .16s,background .16s}
240
+ .jc-card:hover{border-color:var(--dsw-alias-label-dimmed)}
241
+ .jc-card[data-open=true]{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
242
+ .jc-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:transparent;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}
243
+ .jc-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
244
+ .jc-card-heading{display:flex;flex:1;flex-direction:column;gap:4px;min-width:0}
245
+ .jc-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
246
+ .jc-card-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
247
+ .jc-card-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}
248
+ .jc-card[data-open=true] .jc-card-chevron{transform:rotate(180deg)}
249
+ .jc-card-body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
250
+ .jc-field{display:flex;flex-direction:column;gap:6px;padding:12px 0}
251
+ .jc-field+.jc-field{border-top:.5px solid var(--dsw-alias-border-l2)}
252
+ .jc-field-head{display:flex;align-items:center;gap:8px}
253
+ .jc-field-label{flex:1;min-width:0;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:1.5}
254
+ .jc-field-toggle{accent-color:var(--dsw-alias-brand-primary);width:16px;height:16px;cursor:pointer}
255
+ .jc-field-input{box-sizing:border-box;width:100%;height:34px;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}
256
+ .jc-field-input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
257
+ .jc-field-input[aria-invalid=true]{border-color:var(--dsw-alias-label-error)}
258
+ .jc-field-hint{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;overflow-wrap:anywhere}
259
+ .jc-field-reset{background:none;border:0;padding:0;font:inherit;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer}
260
+ .jc-card-footer{display:flex;justify-content:flex-end;align-items:center;flex-wrap:wrap;gap:8px;border-top:.5px solid var(--dsw-alias-border-l2);padding:12px 0 4px}
261
+ .jc-card-error{color:var(--dsw-alias-label-error);font-size:12px;line-height:1.5;overflow-wrap:anywhere}
262
+ `
263
+
264
+ function PluginSettingsCard() {
265
+ const t = useJustChatT()
266
+ const snapshot = useEntrySettings()
267
+ const current = { ...defaultSettings, ...(snapshot.value ?? {}) }
268
+ const [open, setOpen] = React.useState(false)
269
+ const [editor, setEditor] = React.useState(null)
270
+ const [saving, setSaving] = React.useState(false)
271
+ const [error, setError] = React.useState(null)
272
+ const draft = editor?.value ?? current
273
+ const dirty = Object.keys(defaultSettings).some((key) => draft[key] !== current[key])
274
+ const disabled = saving || snapshot.writable === false
275
+ let preview
276
+ try { preview = formatWorkspaceName(draft.workspaceNameTemplate, t('entry')) } catch {}
277
+ const edit = (key, value) => {
278
+ setEditor((previous) => ({ revision: previous?.revision ?? snapshot.revision,
279
+ value: { ...(previous?.value ?? current), [key]: value } }))
280
+ setError(null)
281
+ }
282
+ const save = async () => {
283
+ if (!dirty || !preview || disabled) return
284
+ setSaving(true); setError(null)
285
+ try {
286
+ const ops = Object.keys(defaultSettings).filter((key) => draft[key] !== current[key])
287
+ .map((key) => ({ op: 'set', path: [key], value: draft[key] }))
288
+ await entrySettings.mutate(ops, editor?.revision)
289
+ setEditor(null); setOpen(false)
290
+ } catch (error) { setError(error.message) }
291
+ finally { setSaving(false) }
292
+ }
293
+ if (snapshot.status === 'unavailable') return null
294
+ return React.createElement('li', { className: 'jc-card', 'data-open': open },
295
+ React.createElement('button', {
296
+ className: 'jc-card-header', type: 'button', 'aria-expanded': open,
297
+ 'aria-controls': 'just-chat-settings-body',
298
+ 'aria-label': t(open ? 'settings.collapse' : 'settings.expand') + ': ' + t('entry'),
299
+ onClick: () => setOpen(!open),
300
+ },
301
+ React.createElement('span', { className: 'jc-card-heading' },
302
+ React.createElement('span', { className: 'jc-card-name' }, t('entry')),
303
+ React.createElement('span', { className: 'jc-card-description' }, t('settings.description'))),
304
+ dirty && React.createElement(Tag, { tone: 'neutral' }, t('settings.unsaved')),
305
+ React.createElement(IconChevronDownOutline14, { className: 'jc-card-chevron' })),
306
+ open && React.createElement('div', { className: 'jc-card-body', id: 'just-chat-settings-body' },
307
+ snapshot.writable === false && React.createElement('p', { className: 'jc-field-hint', role: 'status' }, t('settings.readOnly')),
308
+ ['hero', 'sidebar'].map((key) => React.createElement('div', { key, className: 'jc-field' },
309
+ React.createElement('label', { className: 'jc-field-head' },
310
+ React.createElement('span', { className: 'jc-field-label' }, t('settings.' + key)),
311
+ React.createElement('input', { className: 'jc-field-toggle', type: 'checkbox',
312
+ checked: draft[key], disabled, 'aria-label': t('settings.' + key),
313
+ onChange: (event) => edit(key, event.target.checked) })))),
314
+ React.createElement('div', { className: 'jc-field' },
315
+ React.createElement('div', { className: 'jc-field-head' },
316
+ React.createElement('label', { className: 'jc-field-label', htmlFor: 'just-chat-name-template' }, t('settings.template')),
317
+ React.createElement('button', { className: 'jc-field-reset', type: 'button', disabled,
318
+ onClick: () => edit('workspaceNameTemplate', defaultNameTemplate) }, t('settings.reset'))),
319
+ React.createElement('input', { className: 'jc-field-input', id: 'just-chat-name-template',
320
+ value: draft.workspaceNameTemplate, maxLength: 200, disabled, 'aria-invalid': !preview,
321
+ 'aria-describedby': 'just-chat-template-help just-chat-template-preview',
322
+ onChange: (event) => edit('workspaceNameTemplate', event.target.value) }),
323
+ React.createElement('p', { id: 'just-chat-template-help', className: 'jc-field-hint' }, t('settings.help')),
324
+ React.createElement('p', { id: 'just-chat-template-preview', role: preview ? 'status' : 'alert',
325
+ className: preview ? 'jc-field-hint' : 'jc-card-error' },
326
+ preview ? t('settings.preview') + ': ' + preview : t('settings.invalid'))),
327
+ error && React.createElement('p', { role: 'alert', className: 'jc-card-error' }, error),
328
+ React.createElement('div', { className: 'jc-card-footer' },
329
+ React.createElement(Button, { variant: 'outline', size: 'sm', disabled: !dirty || saving,
330
+ onClick: () => { setEditor(null); setError(null) } }, t('settings.discard')),
331
+ React.createElement(Button, { size: 'sm', disabled: disabled || !dirty || !preview,
332
+ onClick: save }, t(saving ? 'settings.saving' : 'settings.save'))),
333
+ ))
334
+ }
335
+
336
+ const inject = [
337
+ 'slots',
338
+ 'sessions',
339
+ 'workspaces',
340
+ 'uiWorkspace',
341
+ 'layout',
342
+ 'locale',
343
+ 'settingsScope',
344
+ ]
345
+
346
+ function apply(ctx) {
347
+ runtimeCtx = ctx
348
+ ctx.effect(() => {
349
+ const style = document.createElement('style')
350
+ style.dataset.pluginCss = 'dsh-just-chat/settings'
351
+ style.textContent = settingsCss
352
+ document.head.appendChild(style)
353
+ return () => style.remove()
354
+ }, 'dsh-just-chat: settings styles')
355
+ const locale = ctx.get('locale')
356
+ entrySettings = ctx.settingsScope.bind({ namespace: settingsNamespace })
357
+
358
+ ctx.effect(
359
+ () => locale.register(localeNamespace, localeDictionaries),
360
+ 'dsh-just-chat: dictionaries',
361
+ )
362
+
363
+ let sidebarEnabled = settingEnabled(entrySettings.getSnapshot(), 'sidebar')
364
+ const registerSidebarEntry = () => ctx.slots.register(
365
+ {
366
+ name: 'sidebar.panellist',
367
+ id: 'dsh-just-chat',
368
+ order: 10,
369
+ label: () => locale.bind(localeNamespace)('entry'),
370
+ },
371
+ PanelIcon,
372
+ )
373
+
374
+ ctx.slots.inject('sidebar.panellist', () => {
375
+ sidebarInjectionLive = true
376
+ sidebarRegistration = sidebarEnabled ? registerSidebarEntry() : undefined
377
+ return () => {
378
+ sidebarRegistration?.()
379
+ sidebarRegistration = undefined
380
+ sidebarInjectionLive = false
381
+ }
382
+ })
383
+
384
+ ctx.effect(() => {
385
+ const syncSidebar = () => {
386
+ const next = settingEnabled(entrySettings.getSnapshot(), 'sidebar')
387
+ if (next === sidebarEnabled) return
388
+ sidebarEnabled = next
389
+ if (!sidebarInjectionLive) return
390
+ sidebarRegistration?.()
391
+ sidebarRegistration = sidebarEnabled ? registerSidebarEntry() : undefined
392
+ }
393
+ const stop = entrySettings.subscribe(syncSidebar)
394
+ syncSidebar()
395
+ return stop
396
+ }, 'dsh-just-chat: sidebar entry setting')
397
+
398
+ ctx.slots.inject('shell.overlay', () => ctx.slots.register(
399
+ {
400
+ name: 'shell.overlay',
401
+ id: 'dsh-just-chat-hero',
402
+ order: 100,
403
+ },
404
+ JustChatOverlay,
405
+ ))
406
+
407
+ ctx.slots.inject('main', () => ctx.slots.register(
408
+ { name: 'main', key: 'dsh-just-chat' },
409
+ ChatPanel,
410
+ ))
411
+
412
+ ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
413
+ { name: 'settings.plugin.item', key: settingsNamespace, locale: localeNamespace },
414
+ PluginSettingsCard,
415
+ ))
416
+
417
+
418
+ }
419
+
420
+ exports.formatWorkspaceName = formatWorkspaceName
421
+ exports.apply = apply
422
+ exports.inject = inject
423
+ return module.exports
424
+ },
425
+ })
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: dsh-just-chat
3
+ name: dsh-just-chat
@@ -0,0 +1,124 @@
1
+ # 开发指南
2
+
3
+ 本页面向修改和调试插件的开发者。插件安装、功能与配置见 [README](../README.md)。
4
+
5
+ ## 准备环境
6
+
7
+ 开发脚本面向 Windows,要求系统已安装 Node.js(`^22.19.0 || >=24.0.0`)、npm、pnpm,并可使用 PowerShell。
8
+
9
+ 从 GitHub 克隆或下载源码后,在仓库根目录运行:
10
+
11
+ ```powershell
12
+ npm run dev
13
+ ```
14
+
15
+ 脚本会依次:
16
+
17
+ 1. 使用系统 pnpm 按锁文件安装依赖,包括项目内的 DSH `0.1.5-rc.2`。
18
+ 2. 创建或复用仓库中的 `.dsh-dev`,作为调试用 `DSH_HOME`。
19
+ 3. 将当前插件安装到该 Home 的 `web` profile,同时安装调试伴随插件 `@michengai/dsh-archive-manager@0.1.41`。
20
+ 4. 使用项目内的 DSH 启动服务,在终端输出带登录 token 的本地访问链接。
21
+
22
+ 打开终端链接,在 DSH 模型设置中配置凭据,即可点击“快速对话”调试。端口由系统分配,每次启动可能不同;不要公开带 token 的链接。结束调试时在启动终端按 `Ctrl+C`。
23
+
24
+ 仅初始化、不启动服务:
25
+
26
+ ```powershell
27
+ npm run setup
28
+ ```
29
+
30
+ 重复运行会保留 `.dsh-dev` 中的对话、设置与凭据。脚本不依赖全局 DSH,不复制其他 Home 的凭据;Node/pnpm 使用系统原生环境,不重定位缓存。修改插件后先停止当前调试服务,再运行 `npm run dev`,打开新输出的链接。
31
+
32
+ ### 安装本地源码到已有 DSH
33
+
34
+ 若需要在指定的已有 DSH 环境中验证源码,在仓库根目录运行:
35
+
36
+ ```powershell
37
+ pnpm install --frozen-lockfile
38
+ dsh plugin --profile web add .
39
+ ```
40
+
41
+ 确认 `DSH_HOME` 和 profile 与目标环境一致,安装后重启目标 DSH 并刷新页面。本地安装引用当前目录,请保留该目录。日常开发优先使用上面的隔离调试环境。
42
+
43
+ ## 业务验证
44
+
45
+ ```powershell
46
+ npm test
47
+ ```
48
+
49
+ 自动测试覆盖入口调用和命名规则。运行环境中还应检查:
50
+
51
+ - 从没有工作区的状态点击入口,能够进入对话并收到真实模型回复。
52
+ - 两个入口连续新建时使用不同目录;原生“新建会话”仍使用所在工作区的目录。
53
+ - 归档会话、删除工作区后,再次点击入口仍可对话;刷新后记录正常保留。
54
+ - 两个入口开关、模板预览与校验、保存/放弃、语言切换,以及刷新和重启后的配置保留。
55
+ - 窄窗口布局、附件、取消响应和断线恢复。
56
+
57
+ 模型凭据与原生目录选择器由人工操作。清理测试数据前确认目标,保留非测试会话和文件。
58
+
59
+ ### 归档管理伴随插件
60
+
61
+ 开发初始化额外安装的归档管理插件在 **设置 → 归档会话** 中提供恢复和永久删除功能,用于验证快速对话与原生会话管理的兼容性。它不是快速对话插件的依赖。
62
+
63
+ **永久删除会话不可恢复**,但会保留工作目录。原生删除工作区也不删除磁盘目录;详细生命周期见 [README](../README.md#工作区与对话的关系)。
64
+
65
+ ## 实现结构
66
+
67
+ 仓库根目录就是可安装的插件 package root,无需单独构建。
68
+
69
+ | 文件 | 职责 |
70
+ | --- | --- |
71
+ | `index.js` | 注册设置、准备目录、调用原生 Workspace 服务 |
72
+ | `client.js` | 入口、原生 UI 组件、配置卡片和命名模板 |
73
+ | `cordis.patch.yml` | DSH bundle 的 Host 激活配置 |
74
+ | `scripts/install-dsh-dev.ps1` | 安装项目内 DSH 与调试插件 |
75
+ | `scripts/start-dsh-dev.ps1` | 初始化并启动隔离的调试环境 |
76
+ | `tests/` | 入口调用与命名规则测试 |
77
+
78
+ 所有功能在插件中实现,不修改 DSH 宿主源码。首页入口使用 `conversation.hero.agentPreset` 的 DOM 锚点,侧栏和配置卡片使用官方插槽;升级 DSH 时需重新验证入口挂载和布局。
79
+
80
+ 调试时生成的工作目录位于 `.dsh-dev/just-chat/sessions/`。Workspace 和 Session ID、归档和工作区删除均由 DSH 管理。
81
+
82
+ ## 打包与版本控制
83
+
84
+ ```powershell
85
+ npm pack --dry-run
86
+ ```
87
+
88
+ 检查发布清单包含插件运行文件、README、开发指南和配图,不含调试 Home。提交源码、锁文件和文档;`.gitignore` 排除调试 Home、依赖、临时输出及本地研究记录。不要提交 `.dsh-dev` 中的凭据与对话数据。
89
+
90
+ ### GitHub 与 npm 发布准备
91
+
92
+ 仓库提供以下自动化:
93
+
94
+ - `ci.yml`:main 推送和 PR 触发,在 Windows/Linux、Node 22.19/24 上按锁文件安装依赖,检查 JavaScript 语法、运行现有测试并预检发布包。
95
+ - `release.yml`:推送 `v*` tag 后先运行同一套 CI;tag 必须等于 `v` 加 package.json 的版本号。通过后发布 npm,随后创建带自动发布说明的 GitHub Release。含预发布后缀的版本使用 npm `next` 和 GitHub prerelease,其余使用 `latest`。
96
+ - Dependabot:每周检查 GitHub Actions 更新并创建更新 PR。
97
+
98
+ CI 不启动模型服务,真实对话和界面流程仍按上面的业务验证执行。
99
+
100
+ 首次启用发布:
101
+
102
+ 1. 将源码推送到实际 GitHub 仓库,完成下面的发布准备项。发布工作流会检查 repository.url 与当前仓库一致,并要求设置许可证。
103
+ 2. 在 GitHub 创建 `npm` environment,限制仅发布 tag 可部署;按团队需要配置审批,并保护 main 和 `v*` tag。
104
+ 3. 在 npm 包设置中添加 GitHub Actions Trusted Publisher,填写实际 owner/repository、工作流文件名 `release.yml`、environment `npm`,允许直接 `npm publish`。见 [npm Trusted Publishing 配置说明](https://docs.npmjs.com/trusted-publishers/)。如果新包尚不能配置 Trusted Publisher,先由维护者登录 npm 手动完成首次发布,再绑定后续自动发布。
105
+ 4. 工作流使用 Node 24 和 OIDC,不需保存 NPM_TOKEN。npm CLI 需至少 11.5.1。
106
+
107
+ 发布一个新版本(以下以 0.1.1 为例,工作区应先保持干净):
108
+
109
+ ```sh
110
+ npm version 0.1.1
111
+ git push origin main
112
+ git push origin v0.1.1
113
+ ```
114
+
115
+ `npm version` 会更新版本、提交并创建 tag;首次手动发布过的版本不要再次触发 npm 发布。npm 版本不可覆盖。若 npm 成功但 GitHub Release 失败,使用 Actions 的 **Re-run failed jobs**,只重试后续 Release job;不要重跑已经成功的 npm 发布 job。
116
+
117
+ GitHub 提供源码和开发文档,npm 提供可安装的 `dsh-just-chat` 插件包。发布前:
118
+
119
+ - 确认 npm 包名可用、版本号和发布权限,以及项目许可证。
120
+ - 在 `package.json` 中补齐实际 GitHub 地址对应的 `repository`、`homepage` 和 `bugs`。
121
+ - 将 README 配图和开发指南链接改为该 GitHub 仓库的绝对链接,并检查 GitHub 与 npm 页面都能正常展示;仅把图片打入 npm 包不能保证 npm 页面解析相对图片地址。
122
+ - 检查发布清单和业务流程,发布后在独立 DSH Home 中验证 README 的 npm 安装命令。
123
+ - 首次 npm 发布可用后,移除 README 中“发布后可用”的提示。
124
+ - npm 版本 badge 从注册表读取已发布版本,首次发布前可能显示无法获取;RED badge 使用 RED 项目提供的样式并链接到其 GitHub 仓库。
Binary file
Binary file
package/index.js ADDED
@@ -0,0 +1,50 @@
1
+ import z from '@deepseek-ai/schemastery'
2
+ import { mkdir, mkdtemp, rmdir } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
5
+
6
+ export const name = 'dsh-just-chat'
7
+
8
+ const SETTINGS_NAMESPACE = 'dsh-just-chat'
9
+ const SettingsSchema = z.object({
10
+ hero: z.boolean().default(true),
11
+ sidebar: z.boolean().default(true),
12
+ workspaceNameTemplate: z.string().min(1).max(200).default('{label} · {MM}-{DD} {HH}:{mm}'),
13
+ })
14
+
15
+ export function apply(ctx) {
16
+ ctx.inject(['settings'], (pluginCtx) => {
17
+ pluginCtx.settings.register(SETTINGS_NAMESPACE, SettingsSchema)
18
+ })
19
+ ctx.inject(['connection', 'workspaceRegistry'], (pluginCtx) => {
20
+ // The shared API channel owns authentication, origin checks, and route lifetime.
21
+ pluginCtx.connection.fetch.register({
22
+ path: '/api/just-chat/prepare',
23
+ methods: ['POST'],
24
+ requestBody: 'buffered',
25
+ async fetch(request) {
26
+ let path
27
+ try {
28
+ request.signal.throwIfAborted()
29
+ const { title } = await request.json()
30
+ if (typeof title !== 'string' || !title.trim() || title.length > 100) {
31
+ return Response.json({ error: 'Invalid workspace title' }, { status: 400 })
32
+ }
33
+ const root = dshHomePath('just-chat', 'sessions')
34
+ await mkdir(root, { recursive: true })
35
+ path = await mkdtemp(join(root, 'chat-'))
36
+ request.signal.throwIfAborted()
37
+ // Only the folder name is ours; DSH generates Workspace and Session IDs.
38
+ await pluginCtx.workspaceRegistry.create(path, title)
39
+ return Response.json({ path })
40
+ } catch (error) {
41
+ // Undo only an empty directory that never acquired a Workspace.
42
+ if (path && !pluginCtx.workspaceRegistry.list().some((workspace) => workspace.path === path)) {
43
+ await rmdir(path).catch(() => {})
44
+ }
45
+ return Response.json({ error: error.message }, { status: 500 })
46
+ }
47
+ },
48
+ })
49
+ })
50
+ }
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "dsh-just-chat",
3
+ "description": "One-click native conversations with independent workspaces for DeepSeek Harness",
4
+ "homepage": "https://github.com/exoticknight/dsh-just-chat#readme",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/exoticknight/dsh-just-chat.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/exoticknight/dsh-just-chat/issues"
11
+ },
12
+ "keywords": ["dsh", "deepseek-harness", "dsh-plugin", "chat", "workspace"],
13
+ "engines": {
14
+ "node": "^22.19.0 || >=24.0.0"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org/"
19
+ },
20
+ "version": "0.1.0",
21
+ "license": "Apache-2.0",
22
+ "author": "exoticknight",
23
+ "type": "module",
24
+ "main": "./index.js",
25
+ "exports": {
26
+ ".": "./index.js",
27
+ "./client": "./client.js",
28
+ "./cordis.patch.yml": "./cordis.patch.yml",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "files": [
32
+ "index.js",
33
+ "client.js",
34
+ "cordis.patch.yml",
35
+ "README.md",
36
+ "RED.md",
37
+ "docs/images",
38
+ "docs/development.md"
39
+ ],
40
+ "scripts": {
41
+ "setup": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\scripts\\install-dsh-dev.ps1",
42
+ "test": "node --test \"tests/*.test.js\"",
43
+ "dev": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\scripts\\start-dsh-dev.ps1",
44
+ "dev:install": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\scripts\\install-dsh-dev.ps1"
45
+ },
46
+ "dsh": {
47
+ "bundle": {
48
+ "patch": "./cordis.patch.yml"
49
+ },
50
+ "client": {
51
+ "platform": "web",
52
+ "inject": [
53
+ "@deepseek-ai/dsh-api-session-controller",
54
+ "@deepseek-ai/dsh-api-workspace-controller",
55
+ "@deepseek-ai/dsh-client-locale",
56
+ "@deepseek-ai/dsh-client-ui-layout",
57
+ "@deepseek-ai/dsh-client-ui-renderer",
58
+ "@deepseek-ai/dsh-client-ui-sidebar",
59
+ "@deepseek-ai/dsh-client-ui-settings",
60
+ "@deepseek-ai/dsh-client-ui-settings-plugins",
61
+ "@deepseek-ai/dsh-client-ui-workspace"
62
+ ]
63
+ }
64
+ },
65
+ "dependencies": {
66
+ "@deepseek-ai/dsh-home-paths": "0.1.5-rc.2",
67
+ "@deepseek-ai/schemastery": "^3.18.2"
68
+ },
69
+ "devDependencies": {
70
+ "@deepseek-ai/dsh": "0.1.5-rc.2"
71
+ }
72
+ }