@ash-qw/dsh-theme-prts 2.1.1 → 2.3.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/CHANGELOG.md +23 -0
- package/README.en.md +8 -0
- package/README.md +8 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/THIRD_PARTY_NOTICES.zh-CN.md +1 -1
- package/lib/client.js +556 -46
- package/package.json +2 -2
- package/src/client/conversation-control-adapter.js +2 -0
- package/src/client/custom-background.js +2 -1
- package/src/client/index.js +8 -0
- package/src/client/native-preferences.js +31 -0
- package/src/client/operations-shell-v2.js +2 -2
- package/src/client/particle-field-adapter.js +67 -14
- package/src/client/preferences.js +9 -2
- package/src/client/session-summary-preview.js +148 -16
- package/src/client/sidebar-control-adapter.js +1 -1
- package/src/client/theme-controller-v2.js +2 -1
- package/src/client/theme-settings-workbench.js +42 -5
- package/src/client/theme-transfer-panel.js +98 -0
- package/src/client/theme-transfer.js +167 -0
- package/src/index.js +21 -4
- package/src/styles/custom-background.css +14 -0
- package/src/styles/facility-vector.css +26 -4
- package/src/styles/refinement.css +22 -0
- package/src/styles/theme-settings-workbench.css +97 -2
|
@@ -976,7 +976,7 @@ export function createSidebarControlAdapter({ document, window, sessions, worksp
|
|
|
976
976
|
|
|
977
977
|
return {
|
|
978
978
|
start(preferences = {}) {
|
|
979
|
-
const keys = ['sessionPreview','previewOngoing','previewWarning','previewDone','previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']
|
|
979
|
+
const keys = ['sessionPreview','previewOngoing','previewTools','previewWarning','previewDone','previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']
|
|
980
980
|
if (keys.some(key => previewPreferences[key] !== preferences[key])) preview.stop()
|
|
981
981
|
previewPreferences = Object.fromEntries(keys.map(key => [key, preferences[key]]))
|
|
982
982
|
if (!document) return
|
|
@@ -72,7 +72,7 @@ function resolveScheme(value) {
|
|
|
72
72
|
return identity.includes('dark') || identity.includes('night') ? 'dark' : 'light'
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
export function createThemeController({ document, window, cssText, service, cursorAssets, onTransitionStateChange = () => {} }) {
|
|
75
|
+
export function createThemeController({ document, window, cssText, service, cursorAssets, onTransitionStateChange = () => {}, onSchemeChange = () => {} }) {
|
|
76
76
|
if (!document || !window) {
|
|
77
77
|
const gesture = { update() {}, finish() {}, cancel() {} }
|
|
78
78
|
return { apply() {}, sync() {}, refresh() {}, setTheme() {}, beginThemeTransition() { return gesture }, toggle() {}, dispose() {} }
|
|
@@ -153,6 +153,7 @@ export function createThemeController({ document, window, cssText, service, curs
|
|
|
153
153
|
if (disposed) return
|
|
154
154
|
const next = resolveScheme(value)
|
|
155
155
|
root.dataset.prtsScheme = next
|
|
156
|
+
onSchemeChange(next)
|
|
156
157
|
return next
|
|
157
158
|
}
|
|
158
159
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { themeTransferMarkup, mountThemeTransfer } from './theme-transfer-panel.js'
|
|
1
2
|
import { backgroundMarkup, mountBackgroundSettings } from './custom-background.js'
|
|
2
3
|
import { avatarMarkup, mountAvatarSettings } from './custom-avatar.js'
|
|
3
4
|
import { customEmblemMarkup, mountCustomEmblemEditor } from './custom-emblem.js'
|
|
@@ -38,7 +39,9 @@ function previewSetting(key, label, description) {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
function settingButtons(key, label, options) {
|
|
41
|
-
const note = key === '
|
|
42
|
+
const note = key === 'heroContrast'
|
|
43
|
+
? '<small data-prts-setting-note>为新会话徽记添加柔光衬底,使它在背景图片上更清晰。</small>'
|
|
44
|
+
: key === 'composerGlass'
|
|
42
45
|
? '<small data-prts-setting-note>增强输入框的背景模糊与轻微折射,关闭时使用原有材质</small>'
|
|
43
46
|
: key === 'bakerLoading'
|
|
44
47
|
? '<small data-prts-setting-note>每轮正文生成时显示三点动画,完成后显示全文;中断时保留已收到的内容</small>'
|
|
@@ -59,7 +62,7 @@ function settingButtons(key, label, options) {
|
|
|
59
62
|
: key === 'sessionFlowPalette'
|
|
60
63
|
? '<small data-prts-setting-note>同时适配日间与夜间模式</small>'
|
|
61
64
|
: ''
|
|
62
|
-
if (['composerGlass', 'bakerLoading', 'motion', 'cursor', 'railDefaultHidden', 'sessionFlow', 'sessionGlow', 'bakerGroupAvatars', 'conversationEmblem', 'sessionPreview', 'previewOngoing', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning'].includes(key)) return `<div data-prts-setting-row="${key}">
|
|
65
|
+
if (['heroContrast', 'composerGlass', 'bakerLoading', 'motion', 'cursor', 'railDefaultHidden', 'sessionFlow', 'sessionGlow', 'bakerGroupAvatars', 'conversationEmblem', 'sessionPreview', 'aiToolsEnabled', 'previewOngoing', 'previewTools', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning'].includes(key)) return `<div data-prts-setting-row="${key}">
|
|
63
66
|
<span><strong>${label}</strong>${note}</span>
|
|
64
67
|
<button type="button" role="switch" aria-label="${label}" aria-checked="false" data-prts-setting-switch="${key}" ${key === 'cursor' ? 'data-prts-cursor-switch' : ''}><span aria-hidden="true"></span></button>
|
|
65
68
|
</div>`
|
|
@@ -127,15 +130,17 @@ export function themeSettingsMarkup() {
|
|
|
127
130
|
<button type="button" data-prts-settings-jump="flow" role="tab" id="prts-tab-flow" aria-controls="prts-page-flow" aria-selected="false" tabindex="-1">会话流线</button>
|
|
128
131
|
<button type="button" data-prts-settings-jump="particles" role="tab" id="prts-tab-particles" aria-controls="prts-page-particles" aria-selected="false" tabindex="-1">粒子徽记</button>
|
|
129
132
|
<button type="button" data-prts-settings-jump="assistance" role="tab" id="prts-tab-assistance" aria-controls="prts-page-assistance" aria-selected="false" tabindex="-1">辅助功能</button>
|
|
133
|
+
<button type="button" data-prts-settings-jump="ai-tools" role="tab" id="prts-tab-ai-tools" aria-controls="prts-page-ai-tools" aria-selected="false" tabindex="-1">AI 工具</button>
|
|
130
134
|
</nav>
|
|
131
135
|
<footer data-prts-settings-actions aria-label="全局主题设置操作">
|
|
132
136
|
<div data-prts-persistence-error hidden role="alert">
|
|
133
137
|
<span><strong>设置未保存</strong><small>未能保存到 DSH,可能存在连接中断、配置冲突或当前访问不支持持久保存。重试将重新提交本次修改。</small></span>
|
|
134
138
|
<button type="button" data-prts-retry-save>重试保存</button>
|
|
135
139
|
</div>
|
|
140
|
+
<button type="button" data-prts-transfer-open data-prts-footer-action aria-haspopup="dialog" aria-expanded="false"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M6 14V3m-3 3 3-3 3 3M14 6v11m-3-3 3 3 3-3"/></svg><span>导入与导出</span></button>
|
|
136
141
|
<div data-prts-reset-zone>
|
|
137
142
|
|
|
138
|
-
<button type="button" data-prts-reset-visual title="恢复全部主题设置" aria-label="恢复全部主题设置">恢复默认</button>
|
|
143
|
+
<button type="button" data-prts-reset-visual data-prts-footer-action title="恢复全部主题设置" aria-label="恢复全部主题设置"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4 7a6.5 6.5 0 1 1-.4 5M4 3v4h4"/></svg><span>恢复默认</span></button>
|
|
139
144
|
<div data-prts-reset-confirm hidden role="group" aria-label="确认恢复全部主题设置">
|
|
140
145
|
<span>重置全部分类的主题设置,保留主题开关、启动动画和徽记配置。</span>
|
|
141
146
|
<button type="button" data-prts-reset-confirm-action>确认恢复</button>
|
|
@@ -143,6 +148,7 @@ export function themeSettingsMarkup() {
|
|
|
143
148
|
</div>
|
|
144
149
|
</div>
|
|
145
150
|
</footer>
|
|
151
|
+
${themeTransferMarkup()}
|
|
146
152
|
</div>
|
|
147
153
|
<div data-prts-theme-settings-scroll>
|
|
148
154
|
<section data-prts-settings-section="appearance" role="tabpanel" id="prts-page-appearance" aria-labelledby="prts-tab-appearance" tabindex="0">
|
|
@@ -158,7 +164,7 @@ export function themeSettingsMarkup() {
|
|
|
158
164
|
</section>
|
|
159
165
|
</section>
|
|
160
166
|
<section data-prts-settings-section="background" role="tabpanel" id="prts-page-background" aria-labelledby="prts-tab-background" tabindex="0" hidden>
|
|
161
|
-
${backgroundMarkup()}
|
|
167
|
+
${backgroundMarkup(settingButtons('heroContrast', '新会话徽记增强', []))}
|
|
162
168
|
<div data-prts-composer-glass-setting>${settingButtons('composerGlass', '输入框增强毛玻璃', [])}</div>
|
|
163
169
|
<small data-prts-composer-glass-required hidden>自定义背景下,输入框增强毛玻璃始终开启。</small>
|
|
164
170
|
</section>
|
|
@@ -167,6 +173,33 @@ export function themeSettingsMarkup() {
|
|
|
167
173
|
${COMMON_CONTROLS.filter(([key]) => ['cursor', 'railDefaultHidden', 'motion'].includes(key)).map(([key, label, options]) => settingButtons(key, label, options)).join('')}
|
|
168
174
|
</div>
|
|
169
175
|
</section>
|
|
176
|
+
<section data-prts-settings-section="ai-tools" data-prts-ai-tools role="tabpanel" id="prts-page-ai-tools" aria-labelledby="prts-tab-ai-tools" tabindex="0" hidden>
|
|
177
|
+
<div data-prts-preview-settings><div data-prts-preview-master>
|
|
178
|
+
${previewSetting('aiToolsEnabled', '允许 AI 定制主题', '默认开启 · 共 5 个工具 · 影响此 DSH 实例的所有会话')}
|
|
179
|
+
</div></div>
|
|
180
|
+
<p>开启后,AI 可以通过工具调整粒子徽记和 Baker 头像。关闭后,后续模型请求不再提供这些工具,手动设置不受影响;已有对话中的工具记录不会删除。</p>
|
|
181
|
+
<section>
|
|
182
|
+
<h3>粒子徽记 <small>3 个工具</small></h3>
|
|
183
|
+
<p>将会话中的图片制作成粒子徽记,用于新会话页面或正文轮换。</p>
|
|
184
|
+
<ol><li>上传图片,或让 AI 读取已生成的图片。</li><li>让 AI 制作草稿,调整轮廓、位置和缩放,先查看预览。</li><li>满意后明确要求应用到新会话徽记,或加入正文轮换。</li></ol>
|
|
185
|
+
<blockquote>“把这张图片做成粒子徽记,先给我看看效果。”</blockquote>
|
|
186
|
+
<details><summary>工具说明与限制</summary><dl>
|
|
187
|
+
<dt><code>prts_emblem_inspect</code></dt><dd>查看徽记配置、版本与当前会话的可用图片,不修改设置。</dd>
|
|
188
|
+
<dt><code>prts_emblem_preview</code></dt><dd>制作或调整粒子徽记草稿,只生成预览,不直接应用。</dd>
|
|
189
|
+
<dt><code>prts_emblem_apply</code></dt><dd>经你确认后,替换新会话徽记,或添加、替换正文轮换徽记。</dd>
|
|
190
|
+
</dl><p>单张来源图片最大 8 MB。徽记设置跨工作区和会话共享。粒子预览不等同于原图;配置发生变化时,AI 需要重新核对后再应用。</p></details>
|
|
191
|
+
</section>
|
|
192
|
+
<section>
|
|
193
|
+
<h3>Baker 头像 <small>2 个工具</small></h3>
|
|
194
|
+
<p>为用户和 AI 设置头像,调整圆形裁剪,或恢复默认头像。仅在 Baker 会话样式下显示。</p>
|
|
195
|
+
<ol><li>上传图片,说明要修改用户头像还是 AI 头像。</li><li>让 AI 查看当前配置,再按你的要求替换、裁剪或恢复默认。</li></ol>
|
|
196
|
+
<blockquote>“把这张图片设为 AI 头像,居中裁剪。”</blockquote>
|
|
197
|
+
<details><summary>工具说明与限制</summary><dl>
|
|
198
|
+
<dt><code>prts_avatar_inspect</code></dt><dd>查看头像配置、原图尺寸、裁剪参数及会话图片,不修改设置。</dd>
|
|
199
|
+
<dt><code>prts_avatar_apply</code></dt><dd>替换用户或 AI 头像、基于现有原图重新裁剪,或恢复默认头像。</dd>
|
|
200
|
+
</dl><p>单图最大 5 MB、2500 万像素;裁剪支持 1~4 倍缩放。修改立即影响此实例所有会话,不会自动切换会话样式。替换后不保留旧原图;恢复默认会删除自定义原图。</p></details>
|
|
201
|
+
</section>
|
|
202
|
+
</section>
|
|
170
203
|
<section data-prts-settings-section="assistance" role="tabpanel" id="prts-page-assistance" aria-labelledby="prts-tab-assistance" tabindex="0" hidden>
|
|
171
204
|
<section data-prts-preview-settings>
|
|
172
205
|
<div data-prts-preview-master>
|
|
@@ -177,6 +210,7 @@ export function themeSettingsMarkup() {
|
|
|
177
210
|
<header><strong>悬停预览</strong><small>选择哪些状态显示详细浮窗</small></header>
|
|
178
211
|
${previewSetting('previewOngoing', '运行中预览', '逐轮查看已完成的回复,最新内容置顶')}
|
|
179
212
|
<div data-prts-preview-details="previewOngoing">
|
|
213
|
+
${previewSetting('previewTools', '显示工具调用', '显示工具进度,悬停查看最新正文之后的调用记录')}
|
|
180
214
|
<label data-prts-setting-row="previewReplyLimit"><span><strong>回复条数</strong><small data-prts-preview-setting-description>仅保留最新回复</small></span><select data-prts-preview-reply-limit aria-label="最多显示回复"><option value="5">最新 5 条</option><option value="10">最新 10 条</option><option value="0">不限制</option></select></label>
|
|
181
215
|
${previewSetting('previewFollowOngoing', '运行结束后跟随', '转为待处理或已完成时,自动切换预览')}
|
|
182
216
|
</div>
|
|
@@ -233,7 +267,7 @@ export function createThemeSettingsOverlay({
|
|
|
233
267
|
trigger,
|
|
234
268
|
panel,
|
|
235
269
|
backdrop,
|
|
236
|
-
customEmblemStore, avatarStore, backgroundStore,
|
|
270
|
+
customEmblemStore, avatarStore, backgroundStore, themeTransfer,
|
|
237
271
|
onOpen = () => {},
|
|
238
272
|
onPreferenceChange = () => {},
|
|
239
273
|
onPreferencePreview = () => {},
|
|
@@ -241,6 +275,7 @@ export function createThemeSettingsOverlay({
|
|
|
241
275
|
onRetrySave = () => {},
|
|
242
276
|
}) {
|
|
243
277
|
const window = document?.defaultView
|
|
278
|
+
const transferEditor = mountThemeTransfer({ panel, window, transfer: themeTransfer })
|
|
244
279
|
const backgroundEditor = mountBackgroundSettings({panel,window,store:backgroundStore,onChange:onPreferenceChange})
|
|
245
280
|
const avatarEditor = mountAvatarSettings({panel,window,store:avatarStore})
|
|
246
281
|
const customEditor = mountCustomEmblemEditor({ panel, window, store: customEmblemStore })
|
|
@@ -720,6 +755,7 @@ export function createThemeSettingsOverlay({
|
|
|
720
755
|
panel.querySelector('[data-prts-baker-options]').hidden = preferences?.conversationStyle !== 'deck-chat'
|
|
721
756
|
panel.querySelector('[data-prts-avatars]').hidden = preferences?.conversationStyle !== 'deck-chat'
|
|
722
757
|
const customBackground = preferences.backgroundMode !== 'off'
|
|
758
|
+
panel.querySelector('[data-prts-background-hero-setting]').hidden = !customBackground
|
|
723
759
|
panel.querySelector('[data-prts-composer-glass-setting]').hidden = customBackground
|
|
724
760
|
panel.querySelector('[data-prts-composer-glass-required]').hidden = !customBackground
|
|
725
761
|
backgroundEditor.update(preferences)
|
|
@@ -737,6 +773,7 @@ export function createThemeSettingsOverlay({
|
|
|
737
773
|
close: () => setOpen(false),
|
|
738
774
|
toggle,
|
|
739
775
|
dispose() {
|
|
776
|
+
transferEditor.dispose()
|
|
740
777
|
customEditor.dispose()
|
|
741
778
|
avatarEditor.dispose()
|
|
742
779
|
backgroundEditor.dispose()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { THEME_PACKAGE_LIMIT, validateThemePackage, describeThemePackage } from './theme-transfer.js'
|
|
2
|
+
|
|
3
|
+
export function themeTransferMarkup() {
|
|
4
|
+
return `<dialog data-prts-transfer-dialog aria-labelledby="prts-transfer-title">
|
|
5
|
+
<section data-prts-transfer>
|
|
6
|
+
<header data-prts-transfer-heading><div><small>P.R.T.S. / 主题文件</small><h2 id="prts-transfer-title">导入与导出</h2></div><button type="button" data-prts-transfer-close aria-label="关闭导入与导出">×</button></header>
|
|
7
|
+
<div data-prts-transfer-columns>
|
|
8
|
+
<section data-prts-transfer-card>
|
|
9
|
+
<h3>导出主题</h3><p>保存一份主题,备份或带到另一台设备。</p>
|
|
10
|
+
<label data-prts-transfer-option><input type="checkbox" data-prts-transfer-assets checked><span>包含自定义素材<small>背景、头像和徽记,保留编辑参数与排列。</small></span></label>
|
|
11
|
+
<small>取消勾选,仅导出主题设置。</small>
|
|
12
|
+
<button type="button" data-prts-transfer-export>导出主题文件 <span aria-hidden="true">↓</span></button>
|
|
13
|
+
</section>
|
|
14
|
+
<section data-prts-transfer-card>
|
|
15
|
+
<h3>导入主题</h3><p>选择已导出的文件,确认内容后应用。</p>
|
|
16
|
+
<div data-prts-transfer-file-hint><span aria-hidden="true">↑</span><small>.prts-theme.json · 最大 160 MB</small></div>
|
|
17
|
+
<button type="button" data-prts-transfer-choose>选择主题文件</button>
|
|
18
|
+
<input type="file" accept=".prts-theme.json,application/json" aria-label="导入主题文件" data-prts-transfer-file hidden>
|
|
19
|
+
</section>
|
|
20
|
+
</div>
|
|
21
|
+
<div data-prts-transfer-preview hidden>
|
|
22
|
+
<strong>确认导入</strong><p data-prts-transfer-summary></p>
|
|
23
|
+
<small>应用前会下载当前配置备份,请保留备份文件。</small>
|
|
24
|
+
<div data-prts-transfer-buttons><button type="button" data-prts-transfer-apply>确认导入</button><button type="button" data-prts-transfer-cancel>取消</button></div>
|
|
25
|
+
</div>
|
|
26
|
+
<div data-prts-transfer-recovery hidden><small>恢复将替换最近一次导入后所做的修改。</small><button type="button" data-prts-transfer-restore>恢复导入前配置</button></div>
|
|
27
|
+
<p data-prts-transfer-status role="status" aria-live="polite"></p>
|
|
28
|
+
</section>
|
|
29
|
+
</dialog>`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function mountThemeTransfer({ panel, window, transfer }) {
|
|
33
|
+
const section = panel.querySelector('[data-prts-transfer]')
|
|
34
|
+
if (!section || !transfer) return { dispose() {} }
|
|
35
|
+
const dialog = panel.querySelector('[data-prts-transfer-dialog]')
|
|
36
|
+
const trigger = panel.querySelector('[data-prts-transfer-open]')
|
|
37
|
+
const open = () => { dialog.showModal(); trigger?.setAttribute('aria-expanded', 'true'); get('close').focus() }
|
|
38
|
+
const close = () => { if (busy) return; dialog.close(); trigger?.setAttribute('aria-expanded', 'false'); trigger?.focus() }
|
|
39
|
+
const keydown = event => { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); close() } }
|
|
40
|
+
const cancel = event => { if (event.target === dialog) { event.preventDefault(); event.stopPropagation(); close() } }
|
|
41
|
+
const get = name => section.querySelector(`[data-prts-transfer-${name}]`)
|
|
42
|
+
let pending, busy = false, disposed = false, generation = 0
|
|
43
|
+
const render = () => {
|
|
44
|
+
if (disposed) return
|
|
45
|
+
get('preview').hidden = !pending
|
|
46
|
+
get('recovery').hidden = !transfer.hasBackup
|
|
47
|
+
for (const control of section.querySelectorAll('button,input')) control.disabled = busy
|
|
48
|
+
// Prevent local editors from racing a multi-service import; other clients use revision checks.
|
|
49
|
+
for (const page of panel.querySelectorAll('[data-prts-settings-section]')) if (!page.contains(section)) page.inert = busy
|
|
50
|
+
}
|
|
51
|
+
async function run(task) {
|
|
52
|
+
if (busy) return
|
|
53
|
+
busy = true; get('status').textContent = '正在处理,请稍候…'; render()
|
|
54
|
+
try { get('status').textContent = await task() }
|
|
55
|
+
catch (error) { if (!disposed) get('status').textContent = error.message }
|
|
56
|
+
finally { busy = false; render() }
|
|
57
|
+
}
|
|
58
|
+
const change = event => {
|
|
59
|
+
if (event.target !== get('file')) return
|
|
60
|
+
const file = event.target.files?.[0], selected = ++generation
|
|
61
|
+
pending = undefined; render()
|
|
62
|
+
if (!file) return
|
|
63
|
+
void run(async () => {
|
|
64
|
+
if (file.size > THEME_PACKAGE_LIMIT) throw Error('主题文件不能超过 160 MB')
|
|
65
|
+
const text = await new Promise((resolve, reject) => {
|
|
66
|
+
const reader = new window.FileReader()
|
|
67
|
+
reader.onload = () => resolve(reader.result); reader.onerror = () => reject(Error('无法读取主题文件')); reader.readAsText(file)
|
|
68
|
+
})
|
|
69
|
+
let value
|
|
70
|
+
try { value = JSON.parse(text) } catch { throw Error('主题文件不是有效的 JSON') }
|
|
71
|
+
validateThemePackage(value)
|
|
72
|
+
if (disposed || selected !== generation) return ''
|
|
73
|
+
pending = value
|
|
74
|
+
get('summary').textContent = describeThemePackage(value)
|
|
75
|
+
return '已读取文件,请确认导入内容。'
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
const click = event => {
|
|
79
|
+
const button = event.target.closest?.('button')
|
|
80
|
+
if (button === get('close')) close()
|
|
81
|
+
if (button === get('choose')) get('file').click()
|
|
82
|
+
if (button === get('export')) void run(async () => {
|
|
83
|
+
const value = await transfer.export(get('assets').checked)
|
|
84
|
+
transfer.download(value)
|
|
85
|
+
return '已发起下载,请确认主题文件已保存。'
|
|
86
|
+
})
|
|
87
|
+
if (button === get('apply') && pending) void run(async () => {
|
|
88
|
+
await transfer.import(pending); pending = undefined; get('file').value = ''
|
|
89
|
+
return '主题已导入并保存。'
|
|
90
|
+
})
|
|
91
|
+
if (button === get('cancel')) { pending = undefined; get('file').value = ''; get('status').textContent = ''; render() }
|
|
92
|
+
if (button === get('restore')) void run(async () => { await transfer.restore(); return '已恢复导入前配置。' })
|
|
93
|
+
}
|
|
94
|
+
trigger?.addEventListener('click', open)
|
|
95
|
+
dialog.addEventListener('keydown', keydown); dialog.addEventListener('cancel', cancel)
|
|
96
|
+
section.addEventListener('change', change); section.addEventListener('click', click); render()
|
|
97
|
+
return { dispose() { disposed = true; generation++; if (dialog.open) dialog.close(); trigger?.removeEventListener('click', open); dialog.removeEventListener('keydown', keydown); dialog.removeEventListener('cancel', cancel); section.removeEventListener('change', change); section.removeEventListener('click', click) } }
|
|
98
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { DEFAULT_PREFERENCES, normalizePreferences } from './preferences.js'
|
|
2
|
+
|
|
3
|
+
export const THEME_PACKAGE_LIMIT = 160 * 1024 * 1024
|
|
4
|
+
const transferSlots = ['full', 'sidebar', 'conversation']
|
|
5
|
+
const transferRoles = ['user', 'assistant']
|
|
6
|
+
const transferBuiltins = ['rhodes-island', 'lungmen', 'penguin-logistics', 'rhine-lab', 'reunion']
|
|
7
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
8
|
+
const encodedImage = (value, limit) => typeof value === 'string' && value.length > 0 && value.length <= Math.ceil(limit / 3) * 4 && value.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value)
|
|
9
|
+
|
|
10
|
+
export function validateThemePackage(value) {
|
|
11
|
+
if (!record(value) || value.format !== 'prts-theme' || value.version !== 1) throw Error('不支持的主题文件格式或版本')
|
|
12
|
+
if (!record(value.preferences) || !Object.keys(value.preferences).some(key => key !== 'version' && Object.hasOwn(DEFAULT_PREFERENCES, key))) throw Error('主题文件缺少设置')
|
|
13
|
+
if (!Number.isInteger(value.preferences.version) || value.preferences.version < 1 || value.preferences.version > DEFAULT_PREFERENCES.version) throw Error('不支持此设置版本,请更新主题后重试')
|
|
14
|
+
const normalized = normalizePreferences(value.preferences)
|
|
15
|
+
for (const [key, input] of Object.entries(value.preferences)) {
|
|
16
|
+
if (!Object.hasOwn(DEFAULT_PREFERENCES, key) || key === 'version') continue
|
|
17
|
+
if (typeof input !== typeof DEFAULT_PREFERENCES[key] || (typeof input === 'number' && !Number.isFinite(input))) throw Error(`设置 ${key} 的类型无效`)
|
|
18
|
+
// Current-format files must not silently turn malformed settings into defaults.
|
|
19
|
+
if (value.preferences.version === DEFAULT_PREFERENCES.version && normalized[key] !== input) throw Error(`设置 ${key} 的值无效`)
|
|
20
|
+
}
|
|
21
|
+
if (value.assets === undefined) return value
|
|
22
|
+
const a = value.assets
|
|
23
|
+
if (!record(a) || !record(a.backgrounds) || !record(a.avatars) || !record(a.emblems)) throw Error('主题素材数据不完整')
|
|
24
|
+
for (const slot of transferSlots) if (a.backgrounds[slot] !== null && !encodedImage(a.backgrounds[slot], 10 * 1024 * 1024)) throw Error('背景图片数据无效')
|
|
25
|
+
for (const role of transferRoles) {
|
|
26
|
+
const avatar = a.avatars[role]
|
|
27
|
+
if (avatar === null) continue
|
|
28
|
+
const crop = avatar?.crop
|
|
29
|
+
if (!record(avatar) || !encodedImage(avatar.image, 5 * 1024 * 1024) || !record(crop)
|
|
30
|
+
|| !Number.isFinite(crop.zoom) || crop.zoom < 1 || crop.zoom > 4
|
|
31
|
+
|| !Number.isFinite(crop.x) || crop.x < 0 || crop.x > 1 || !Number.isFinite(crop.y) || crop.y < 0 || crop.y > 1) throw Error('头像数据或裁剪参数无效')
|
|
32
|
+
}
|
|
33
|
+
const { items, hero, conversation } = a.emblems
|
|
34
|
+
if (!Array.isArray(items) || items.length > 13 || !Array.isArray(conversation) || conversation.length < 1 || conversation.length > 12) throw Error('徽记数量无效')
|
|
35
|
+
for (const item of items) {
|
|
36
|
+
if (!record(item) || item.format !== 'prts-emblem' || item.version !== 1 || typeof item.label !== 'string' || !item.label.trim() || item.label.length > 100 || !record(item.params) || !encodedImage(item.image, 8 * 1024 * 1024)) throw Error('徽记素材无效')
|
|
37
|
+
}
|
|
38
|
+
const validEntry = entry => record(entry) && (entry.kind === 'builtin' ? transferBuiltins.includes(entry.key) : entry.kind === 'custom' && Number.isInteger(entry.asset) && entry.asset >= 0 && entry.asset < items.length && typeof entry.label === 'string' && entry.label.length <= 100)
|
|
39
|
+
if ((hero !== null && (!validEntry(hero) || hero.kind !== 'custom')) || !conversation.every(validEntry)) throw Error('徽记引用无效')
|
|
40
|
+
return value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function mergeThemePreferences(current, imported) {
|
|
44
|
+
const normalized = normalizePreferences(imported)
|
|
45
|
+
const patch = Object.fromEntries(Object.keys(DEFAULT_PREFERENCES).filter(key => key !== 'version' && Object.hasOwn(imported, key)).map(key => [key, normalized[key]]))
|
|
46
|
+
return normalizePreferences({ ...current, ...patch, version: DEFAULT_PREFERENCES.version })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function describeThemePackage(value) {
|
|
50
|
+
const count = Object.keys(value.preferences).filter(key => key !== 'version' && Object.hasOwn(DEFAULT_PREFERENCES, key)).length
|
|
51
|
+
if (!value.assets) return `${count} 项主题设置;不包含素材,将保留当前背景、头像和徽记。`
|
|
52
|
+
const a = value.assets
|
|
53
|
+
return `${count} 项主题设置、${transferSlots.filter(k => a.backgrounds[k]).length} 张背景、${transferRoles.filter(k => a.avatars[k]).length} 个头像、${a.emblems.items.length} 个自定义徽记。将替换当前素材及徽记排列,空项恢复为默认。`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createThemeTransfer({ window, getPreferences, savePreferences, checkWritable, checkExport = checkWritable, themeVersion }) {
|
|
57
|
+
let busy = false, backup
|
|
58
|
+
const request = async (path, data) => {
|
|
59
|
+
const response = await window.fetch('/prts-theme-api/' + path, { credentials: 'same-origin',
|
|
60
|
+
...(data === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json', 'x-prts-request': '1' }, body: JSON.stringify(data) }) })
|
|
61
|
+
const result = await response.json()
|
|
62
|
+
if (!response.ok) throw Error(result.error || '无法连接主题素材服务')
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
65
|
+
const readImage = blob => new Promise((resolve, reject) => {
|
|
66
|
+
const reader = new window.FileReader()
|
|
67
|
+
reader.onload = () => resolve(String(reader.result).split(',')[1])
|
|
68
|
+
reader.onerror = () => reject(Error('无法读取背景图片'))
|
|
69
|
+
reader.readAsDataURL(blob)
|
|
70
|
+
})
|
|
71
|
+
async function capture(includeAssets) {
|
|
72
|
+
const value = { format: 'prts-theme', version: 1, themeVersion, exportedAt: new Date().toISOString(), preferences: { ...getPreferences() } }
|
|
73
|
+
const states = {}
|
|
74
|
+
if (includeAssets) {
|
|
75
|
+
const backgrounds = {}, avatars = {}, items = [], ids = new Map()
|
|
76
|
+
states.backgrounds = await request('backgrounds/configuration')
|
|
77
|
+
for (const slot of transferSlots) {
|
|
78
|
+
const entry = states.backgrounds[slot]
|
|
79
|
+
backgrounds[slot] = null
|
|
80
|
+
if (entry) {
|
|
81
|
+
if (!/^[a-f0-9-]{36}$/.test(entry.id)) throw Error('背景引用无效')
|
|
82
|
+
const response = await window.fetch('/prts-theme-api/backgrounds/image/' + entry.id, { credentials: 'same-origin' })
|
|
83
|
+
if (!response.ok) throw Error('无法导出背景图片')
|
|
84
|
+
backgrounds[slot] = await readImage(await response.blob())
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
states.avatars = await request('avatars/configuration')
|
|
88
|
+
for (const role of transferRoles) {
|
|
89
|
+
const source = await request('avatars/edit/' + role)
|
|
90
|
+
if (source.revision !== states.avatars.revision) throw Error('头像已变化,请重新导出')
|
|
91
|
+
avatars[role] = source.image ? { image: source.image, crop: source.crop } : null
|
|
92
|
+
}
|
|
93
|
+
states.emblems = await request('emblems/configuration')
|
|
94
|
+
async function reference(entry) {
|
|
95
|
+
if (!entry) return null
|
|
96
|
+
if (entry.kind === 'builtin') return { kind: 'builtin', key: entry.key, label: entry.label }
|
|
97
|
+
if (!ids.has(entry.draftId)) {
|
|
98
|
+
ids.set(entry.draftId, items.length)
|
|
99
|
+
items.push(await request('emblems/draft/' + encodeURIComponent(entry.draftId) + '/export'))
|
|
100
|
+
}
|
|
101
|
+
return { kind: 'custom', asset: ids.get(entry.draftId), label: entry.label }
|
|
102
|
+
}
|
|
103
|
+
const hero = await reference(states.emblems.configuration.hero), conversation = []
|
|
104
|
+
for (const entry of states.emblems.configuration.conversation) conversation.push(await reference(entry))
|
|
105
|
+
value.assets = { backgrounds, avatars, emblems: { items, hero, conversation } }
|
|
106
|
+
for (const kind of ['backgrounds', 'avatars', 'emblems']) {
|
|
107
|
+
if ((await request(kind + '/configuration')).revision !== states[kind].revision) throw Error('素材在导出期间发生变化,请重试')
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (JSON.stringify(value.preferences) !== JSON.stringify(getPreferences())) throw Error('设置在导出期间发生变化,请重试')
|
|
111
|
+
validateThemePackage(value)
|
|
112
|
+
return { value, states }
|
|
113
|
+
}
|
|
114
|
+
async function exclusive(fn) {
|
|
115
|
+
if (busy) throw Error('正在处理主题文件,请稍候')
|
|
116
|
+
busy = true
|
|
117
|
+
try { return await fn() } finally { busy = false }
|
|
118
|
+
}
|
|
119
|
+
function download(value, name = 'theme') {
|
|
120
|
+
const blob = new window.Blob([JSON.stringify(value)], { type: 'application/json' })
|
|
121
|
+
if (blob.size > THEME_PACKAGE_LIMIT) throw Error('主题文件超过 160 MB,请减少素材后重试')
|
|
122
|
+
const url = window.URL.createObjectURL(blob), link = window.document.createElement('a')
|
|
123
|
+
link.href = url; link.download = `prts-${name}-${new Date().toISOString().replace(/[:.]/g, '-')}.prts-theme.json`
|
|
124
|
+
window.document.body.append(link); link.click(); link.remove()
|
|
125
|
+
window.setTimeout(() => window.URL.revokeObjectURL(url), 1000)
|
|
126
|
+
}
|
|
127
|
+
async function apply(value) {
|
|
128
|
+
validateThemePackage(value)
|
|
129
|
+
const preferenceRevision = checkWritable()
|
|
130
|
+
const before = await capture(Boolean(value.assets))
|
|
131
|
+
backup = before.value
|
|
132
|
+
download(backup, 'before-import')
|
|
133
|
+
const undo = [], revisions = Object.fromEntries(Object.entries(before.states).map(([key, state]) => [key, state.revision]))
|
|
134
|
+
const write = async (kind, data, previous) => {
|
|
135
|
+
const result = await request(kind + '/configuration', { ...data, revision: revisions[kind] })
|
|
136
|
+
revisions[kind] = result.revision
|
|
137
|
+
undo.push(async () => {
|
|
138
|
+
const restored = await request(kind + '/configuration', { ...previous, revision: revisions[kind] })
|
|
139
|
+
revisions[kind] = restored.revision
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
if (value.assets) {
|
|
144
|
+
const a = value.assets, old = before.value.assets, drafts = []
|
|
145
|
+
// Generate every custom draft before changing the active configuration.
|
|
146
|
+
for (const item of a.emblems.items) drafts.push(await request('emblems/import', item))
|
|
147
|
+
const entry = (ref, index) => ref === null ? null : ref.kind === 'builtin' ? ref : { kind: 'custom', key: 'import-' + index + '-' + drafts[ref.asset].draftId, label: ref.label, draftId: drafts[ref.asset].draftId }
|
|
148
|
+
const configuration = { version: 2, hero: entry(a.emblems.hero, 'hero'), conversation: a.emblems.conversation.map(entry) }
|
|
149
|
+
for (const slot of transferSlots) await write('backgrounds', { slot, image: a.backgrounds[slot] }, { slot, image: old.backgrounds[slot] })
|
|
150
|
+
for (const role of transferRoles) await write('avatars', { role, image: a.avatars[role]?.image ?? null, crop: a.avatars[role]?.crop }, { role, image: old.avatars[role]?.image ?? null, crop: old.avatars[role]?.crop })
|
|
151
|
+
await write('emblems', { configuration }, { configuration: before.states.emblems.configuration })
|
|
152
|
+
}
|
|
153
|
+
await savePreferences(mergeThemePreferences(before.value.preferences, value.preferences), preferenceRevision)
|
|
154
|
+
} catch (error) {
|
|
155
|
+
let failures = 0
|
|
156
|
+
for (const restore of undo.reverse()) { try { await restore() } catch { failures++ } }
|
|
157
|
+
throw Error(`${error.message}。${failures ? '部分素材未能自动恢复。' : '已回退已确认写入的素材。'}若连接中断,请检查实际状态;导入前备份已发起下载,可重新导入恢复。`)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
export: includeAssets => exclusive(async () => { checkExport(); return (await capture(includeAssets)).value }),
|
|
162
|
+
import: value => exclusive(() => apply(value)),
|
|
163
|
+
restore: () => exclusive(() => { if (!backup) throw Error('没有可恢复的备份'); return apply(backup) }),
|
|
164
|
+
get hasBackup() { return Boolean(backup) },
|
|
165
|
+
download,
|
|
166
|
+
}
|
|
167
|
+
}
|
package/src/index.js
CHANGED
|
@@ -19,7 +19,7 @@ export const Config = Schema.object(Object.fromEntries([
|
|
|
19
19
|
['preferencesInitialized', Schema.boolean().default(false).volatile()],
|
|
20
20
|
]))
|
|
21
21
|
|
|
22
|
-
export async function apply(ctx) {
|
|
22
|
+
export async function apply(ctx, config = {}) {
|
|
23
23
|
ctx.inject(['settings'], settingsCtx => {
|
|
24
24
|
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber))
|
|
25
25
|
})
|
|
@@ -27,7 +27,24 @@ export async function apply(ctx) {
|
|
|
27
27
|
const service=await createEmblemService(directory)
|
|
28
28
|
const backgrounds=await createBackgroundService(join(directory,'..','backgrounds'))
|
|
29
29
|
const avatars=await createAvatarService(join(directory,'..','avatars'))
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const definitions=[...emblemTools(service,ctx.attachments),...avatarTools(avatars,ctx.attachments)]
|
|
31
|
+
let toolDisposers=[]
|
|
32
|
+
const syncTools=()=>{
|
|
33
|
+
const enabled=(config.aiToolsEnabled?.get?.() ?? config.aiToolsEnabled) !== false
|
|
34
|
+
if(enabled && !toolDisposers.length) {
|
|
35
|
+
for(const tool of definitions)toolDisposers.push(ctx.tools.register(tool))
|
|
36
|
+
} else if(!enabled) {
|
|
37
|
+
for(const dispose of toolDisposers.splice(0).reverse())dispose()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
syncTools()
|
|
41
|
+
// Host volatile fields are updated before this notification; they expose get(), not subscribe().
|
|
42
|
+
const stopConfig=ctx.on('app-boot/config-reload',syncTools)
|
|
43
|
+
const disposers=[registerEmblemApi(ctx,service),registerAvatarApi(ctx,avatars),registerBackgroundApi(ctx,backgrounds)]
|
|
44
|
+
ctx.effect(()=>async()=>{
|
|
45
|
+
stopConfig()
|
|
46
|
+
for(const dispose of toolDisposers.splice(0).reverse())dispose()
|
|
47
|
+
for(const dispose of disposers.reverse())dispose()
|
|
48
|
+
await service.dispose();await backgrounds.dispose()
|
|
49
|
+
})
|
|
33
50
|
}
|
|
@@ -329,3 +329,17 @@ animation: prts-baker-typing 1.2s linear infinite;
|
|
|
329
329
|
33% { background: currentColor; box-shadow: -10px 0 0 color-mix(in srgb,currentColor 30%,transparent), 10px 0 0 color-mix(in srgb,currentColor 30%,transparent); }
|
|
330
330
|
66% { background: color-mix(in srgb,currentColor 30%,transparent); box-shadow: -10px 0 0 color-mix(in srgb,currentColor 30%,transparent), 10px 0 0 currentColor; }
|
|
331
331
|
}
|
|
332
|
+
|
|
333
|
+
/* Keep the hero treatment beside the background readability controls. */
|
|
334
|
+
html[data-dsh-prts] [data-prts-background-hero-setting] {
|
|
335
|
+
padding:16px 0 2px; border-top:1px solid var(--prts-line);
|
|
336
|
+
& [data-prts-setting-row] { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:20px; margin:0; padding:0; border:0; background:none; }
|
|
337
|
+
& [data-prts-setting-row] > span { display:grid; gap:5px; min-width:0; }
|
|
338
|
+
& strong { font:500 13px/1.5 var(--prts-body); color:var(--prts-ink); }
|
|
339
|
+
& small { max-width:36em; font:400 12px/1.6 var(--prts-body); color:var(--prts-muted); }
|
|
340
|
+
& [data-prts-setting-switch] { justify-self:end; align-self:center; margin:0; }
|
|
341
|
+
}
|
|
342
|
+
html[data-dsh-prts] [data-prts-background-hero-setting][hidden] { display:none; }
|
|
343
|
+
|
|
344
|
+
/* Windows chrome must not clip a different canvas color into the conversation. */
|
|
345
|
+
html[data-dsh-prts][data-windows-titlebar] [data-prts-region="operation"] { border-radius: 0; }
|
|
@@ -776,10 +776,10 @@ html[data-dsh-prts] [data-prts-preview-body] a { color:var(--prts-cyan);text-dec
|
|
|
776
776
|
html[data-dsh-prts] [data-prts-preview-message] + [data-prts-preview-message] { margin-top:5px; }
|
|
777
777
|
html[data-dsh-prts] [data-prts-preview-message] > small { display:block;margin-bottom:6px;color:var(--prts-muted);font:10px/1.5 var(--prts-mono); }
|
|
778
778
|
|
|
779
|
-
html[data-dsh-prts] [data-prts-reply-entry] { display:flex;align-items:flex-start;gap:8px;width:100%;padding:6px 8px;text-align:left;border:1px solid var(--prts-line);border-radius:5px;background:transparent;color:var(--prts-ink);cursor:pointer; }
|
|
780
|
-
html[data-dsh-prts] [data-prts-reply-entry]:is(:hover,:focus-visible,[data-prts-reply-active]) { background:color-mix(in srgb,var(--prts-ink) 7%,transparent);border-color:var(--prts-muted); }
|
|
781
|
-
html[data-dsh-prts] [data-prts-reply-entry] small { flex:0 0 auto;white-space:nowrap;padding-top:2px;color:var(--prts-muted);font:10px/1.5 var(--prts-mono); }
|
|
782
|
-
html[data-dsh-prts] [data-prts-reply-entry] span { min-width:0;flex:1;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;font:12px/1.6 var(--prts-body);overflow-wrap:anywhere; }
|
|
779
|
+
html[data-dsh-prts] :is([data-prts-reply-entry],[data-prts-tool-entry]) { display:flex;align-items:flex-start;gap:8px;width:100%;padding:6px 8px;text-align:left;border:1px solid var(--prts-line);border-radius:5px;background:transparent;color:var(--prts-ink);cursor:pointer; }
|
|
780
|
+
html[data-dsh-prts] :is([data-prts-reply-entry],[data-prts-tool-entry]):is(:hover,:focus-visible,[data-prts-reply-active]) { background:color-mix(in srgb,var(--prts-ink) 7%,transparent);border-color:var(--prts-muted); }
|
|
781
|
+
html[data-dsh-prts] :is([data-prts-reply-entry],[data-prts-tool-entry]) small { flex:0 0 auto;white-space:nowrap;padding-top:2px;color:var(--prts-muted);font:10px/1.5 var(--prts-mono); }
|
|
782
|
+
html[data-dsh-prts] :is([data-prts-reply-entry],[data-prts-tool-entry]) span { min-width:0;flex:1;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;font:12px/1.6 var(--prts-body);overflow-wrap:anywhere; }
|
|
783
783
|
html[data-dsh-prts] [data-prts-reply-detail]:not([data-prts-preview-question]) > [data-prts-preview-body] { max-height:190px; }
|
|
784
784
|
|
|
785
785
|
html[data-dsh-prts] [data-prts-preview-marker] { display:none;flex:none;margin-left:auto;color:var(--prts-muted);font:600 9px/1.4 var(--prts-body); }
|
|
@@ -864,3 +864,25 @@ html[data-dsh-prts] [data-prts-summary-dismissed] [data-prts-session-summary-pop
|
|
|
864
864
|
pointer-events: none !important;
|
|
865
865
|
transition: none;
|
|
866
866
|
}
|
|
867
|
+
|
|
868
|
+
html[data-dsh-prts] [data-prts-preview-tool] { margin-top:8px; }
|
|
869
|
+
html[data-dsh-prts] [data-prts-preview-tool][hidden] { display:none; }
|
|
870
|
+
html[data-dsh-prts] [data-prts-preview-tool] > button { box-sizing:border-box;height:32px;align-items:center;gap:7px;padding:4px 8px;overflow:hidden;border-left:2px solid var(--prts-cyan);background:color-mix(in srgb,var(--prts-cyan) 7%,transparent); }
|
|
871
|
+
html[data-dsh-prts] [data-prts-tool-entry]::before { content:">_";flex:none;color:var(--prts-cyan);font:600 11px var(--prts-mono); }
|
|
872
|
+
html[data-dsh-prts] [data-prts-preview-tool] small { padding:0;color:var(--prts-cyan); }
|
|
873
|
+
html[data-dsh-prts] [data-prts-preview-tool] span { display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:11px/1.6 var(--prts-mono); }
|
|
874
|
+
|
|
875
|
+
html[data-dsh-prts] [data-prts-summary-reply] > [data-prts-preview-body]:empty { display:none;margin:0; }
|
|
876
|
+
|
|
877
|
+
html[data-dsh-prts] [data-prts-preview-message] > [data-prts-reply-entry] { border-color:transparent;border-radius:3px; }
|
|
878
|
+
html[data-dsh-prts] [data-prts-reply-entry] small { display:flex;align-items:center;gap:4px;color:#8aaee0; }
|
|
879
|
+
html[data-dsh-prts] [data-prts-reply-entry] svg { width:14px;height:14px;flex:none;fill:none;stroke:currentColor;stroke-width:1.2; }
|
|
880
|
+
|
|
881
|
+
html[data-dsh-prts][data-prts-scheme="light"] [data-prts-reply-entry] small { color:#476b9c; }
|
|
882
|
+
|
|
883
|
+
html[data-dsh-prts] [data-prts-summary-reply][data-prts-tool-history] > [data-prts-preview-body] { height:190px;max-height:190px; }
|
|
884
|
+
html[data-dsh-prts] [data-prts-tool-record] { padding:8px 0;border-bottom:1px solid var(--prts-line); }
|
|
885
|
+
html[data-dsh-prts] [data-prts-tool-record] > div { display:flex;align-items:baseline;gap:8px; }
|
|
886
|
+
html[data-dsh-prts] [data-prts-tool-record] b { flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--prts-cyan);font:11px/1.6 var(--prts-mono); }
|
|
887
|
+
html[data-dsh-prts] [data-prts-tool-record] small { flex:none;color:var(--prts-muted);font-size:10px; }
|
|
888
|
+
html[data-dsh-prts] [data-prts-tool-record] p { display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;margin:4px 0 0; }
|
|
@@ -482,3 +482,25 @@ html[data-dsh-prts][data-prts-conversation-style="deck-chat"] :is([class$="_bubb
|
|
|
482
482
|
padding:2px 7px!important;border-radius:6px!important;
|
|
483
483
|
&:is(:hover,:focus-visible){border-color:var(--prts-cyan)!important}
|
|
484
484
|
}
|
|
485
|
+
|
|
486
|
+
/* Notification cards own one surface across the disclosure and its content. */
|
|
487
|
+
html[data-dsh-prts] [data-turn-trigger] {
|
|
488
|
+
background:var(--prts-glass-soft-surface);
|
|
489
|
+
border:1px solid var(--prts-line);
|
|
490
|
+
border-radius:var(--dsw-radius-xl,16px);
|
|
491
|
+
-webkit-backdrop-filter:blur(12px);
|
|
492
|
+
backdrop-filter:blur(12px);
|
|
493
|
+
color:var(--prts-ink);
|
|
494
|
+
& > button {
|
|
495
|
+
background:transparent;
|
|
496
|
+
border:0;
|
|
497
|
+
border-radius:inherit;
|
|
498
|
+
color:inherit;
|
|
499
|
+
&:hover { background:color-mix(in srgb,var(--prts-ink) 5%,transparent); }
|
|
500
|
+
&:focus-visible { outline:2px solid var(--prts-cyan); outline-offset:-3px; }
|
|
501
|
+
& time { color:var(--prts-muted); }
|
|
502
|
+
}
|
|
503
|
+
& > div { border-top:1px solid var(--prts-line); }
|
|
504
|
+
& > div > p { color:var(--prts-muted); }
|
|
505
|
+
& [data-context-text] { color:var(--prts-ink); }
|
|
506
|
+
}
|