@ash-qw/dsh-theme-prts 2.1.1 → 2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ash-qw/dsh-theme-prts",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Unofficial Arknights P.R.T.S.-inspired UI theme for DeepSeek Harness Web and the official desktop client",
@@ -42,7 +42,9 @@ export const DEFAULT_PREFERENCES = Object.freeze({
42
42
  particlePattern: 'orthogonal',
43
43
  railDefaultHidden: false,
44
44
  sessionPreview: true,
45
+ aiToolsEnabled: true,
45
46
  previewOngoing: true,
47
+ previewTools: true,
46
48
  previewWarning: true,
47
49
  previewDone: true,
48
50
  previewFollowOngoing: true,
@@ -101,7 +103,8 @@ export const PREFERENCE_GROUPS = Object.freeze({
101
103
  particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity', 'particleTraversalSpeed']),
102
104
  material: Object.freeze(['glass', 'composerGlass']),
103
105
  accessibility: Object.freeze(['motion', 'bootAnimation', 'cursor']),
104
- navigation: Object.freeze(['railDefaultHidden', 'sessionPreview', 'previewOngoing', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']),
106
+ aiTools: Object.freeze(['aiToolsEnabled']),
107
+ navigation: Object.freeze(['railDefaultHidden', 'sessionPreview', 'previewOngoing', 'previewTools', 'previewWarning', 'previewDone', 'previewDoubleClick', 'previewFollowOngoing', 'previewFollowWarning', 'previewReplyLimit']),
105
108
  conversation: Object.freeze(['sessionFlow', 'sessionGlow', 'sessionFlowPalette', 'sessionFlowColorBack', 'sessionFlowColorCore', 'sessionFlowColorFront', 'sessionFlowSpeed', 'conversationStyle', 'bakerGroupAvatars', 'bakerLoading', 'conversationEmblem']),
106
109
  })
107
110
 
@@ -237,7 +240,9 @@ export function normalizePreferences(value) {
237
240
  sessionFlowColorFront: sessionFlowColors.front,
238
241
  sessionFlowSpeed: sessionFlowSpeedOr(input.sessionFlowSpeed, DEFAULT_PREFERENCES.sessionFlowSpeed),
239
242
  sessionPreview: booleanOr(input.sessionPreview, DEFAULT_PREFERENCES.sessionPreview),
243
+ aiToolsEnabled: booleanOr(input.aiToolsEnabled, DEFAULT_PREFERENCES.aiToolsEnabled),
240
244
  previewOngoing: booleanOr(input.previewOngoing, DEFAULT_PREFERENCES.previewOngoing),
245
+ previewTools: booleanOr(input.previewTools, DEFAULT_PREFERENCES.previewTools),
241
246
  previewWarning: booleanOr(input.previewWarning, DEFAULT_PREFERENCES.previewWarning),
242
247
  previewDone: booleanOr(input.previewDone, DEFAULT_PREFERENCES.previewDone),
243
248
  previewReplyLimit: [0, 5, 10, '0', '5', '10'].includes(input.previewReplyLimit) ? Number(input.previewReplyLimit) : 5,
@@ -39,6 +39,60 @@ export function latestAssistantReply(snapshot) {
39
39
  return ''
40
40
  }
41
41
 
42
+ // Call objects may be mutable on older hosts; compare raw arguments before reuse.
43
+ const toolSummaryCache = new WeakMap()
44
+ function toolSummary(call) {
45
+ const raw = call.argsRaw ?? call.call?.argsRaw ?? ''
46
+ const cached = toolSummaryCache.get(call)
47
+ if (cached?.raw === raw) return cached.text
48
+ let args
49
+ try { args = JSON.parse(raw) } catch {}
50
+ const summary = ['description', 'command', 'cmd', 'file_path', 'filePath', 'path', 'query', 'pattern', 'url']
51
+ .map(field => args?.[field]).find(value => typeof value === 'string' && value.trim())
52
+ const normalized = summary?.replace(/\s+/g, ' ').trim()
53
+ const text = normalized ? normalized.slice(0, 160) + (normalized.length > 160 ? '…' : '') : ''
54
+ toolSummaryCache.set(call, {raw, text})
55
+ return text
56
+ }
57
+
58
+ // Only calls after the latest settled prose belong to the current progress preview.
59
+ export function previewToolCalls(snapshot) {
60
+ const turn = snapshot?.timeline?.turnOrder?.at(-1)
61
+ const order = snapshot?.order ?? []
62
+ const calls = [], seen = new Set()
63
+ const visit = (call, key) => {
64
+ if (!call) return
65
+ const id = call.callId ?? key
66
+ if (!seen.has(id)) { seen.add(id); calls.push({call, key: id}) }
67
+ for (const [index, child] of (call.subCalls ?? []).entries()) visit(child, key + ':' + index)
68
+ }
69
+ for (let i = order.length - 1; i >= 0; i--) {
70
+ const node = snapshot.nodes?.get?.(order[i])
71
+ if (['user', 'steering', 'turn-trigger'].includes(node?.kind)) break
72
+ if (turn !== undefined && node?.location?.turn?.turn !== turn) continue
73
+ if (node?.visibility === 'hidden') continue
74
+ if (node?.kind === 'assistant-step' && node.data?.status === 'settled'
75
+ && node.data.blocks?.some(block => block.kind === 'text' && block.text?.trim())) break
76
+ if (node?.kind === 'tool-call') visit(node.data?.root, order[i])
77
+ }
78
+ return calls.sort((a, b) => (b.call.callTime ?? b.call.time ?? 0) - (a.call.callTime ?? a.call.time ?? 0)).map(({call, key}) => {
79
+ const text = toolSummary(call)
80
+ return {
81
+ key,
82
+ name: call.name || call.call?.name || '工具调用',
83
+ status: call.kind === 'tool-result' ? call.isError ? '失败' : '已完成'
84
+ : call.phase === 'preparing' ? '准备中' : '执行中',
85
+ summary: text ? text
86
+ : call.phase === 'preparing' ? '正在准备调用…' : '暂无操作摘要',
87
+ }
88
+ })
89
+ }
90
+
91
+ export function latestToolCall(snapshot) {
92
+ const records = previewToolCalls(snapshot)
93
+ return records.length ? {...records[0], records} : undefined
94
+ }
95
+
42
96
  export function completedPreviewReplies(snapshot) {
43
97
  const replies = []
44
98
  const latestTurn = snapshot?.timeline?.turnOrder?.at(-1)
@@ -142,8 +196,8 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
142
196
  active = undefined
143
197
  if (!old) return
144
198
  window.clearTimeout(old.leaveTimer)
145
- window.clearTimeout(old.delay)
146
- window.clearTimeout(old.refresh)
199
+ window.cancelAnimationFrame(old.delay)
200
+ window.cancelAnimationFrame(old.refresh)
147
201
  old.unsubscribe?.()
148
202
  old.unsubscribeStatus?.()
149
203
  if (keepWarm && old.reference && old.source) {
@@ -311,6 +365,39 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
311
365
  body.setAttribute('data-prts-preview-body', '')
312
366
  panel.querySelector('p').replaceWith(body)
313
367
  }
368
+ panel.querySelector('[data-prts-preview-tool]')?.remove()
369
+ const toolSection = panel.ownerDocument.createElement('div')
370
+ toolSection.setAttribute('data-prts-preview-tool', '')
371
+ toolSection.hidden = true
372
+ body.before(toolSection)
373
+ let previousTool, currentTool
374
+ const toolEntry = panel.ownerDocument.createElement('button')
375
+ toolEntry.type = 'button'
376
+ toolEntry.setAttribute('data-prts-tool-entry', '')
377
+ const toolHeading = panel.ownerDocument.createElement('small')
378
+ const toolName = panel.ownerDocument.createElement('span')
379
+ toolEntry.append(toolHeading, toolName)
380
+ toolSection.append(toolEntry)
381
+ const openToolDetail = (refresh = false) => {
382
+ if (currentTool) showDetail({key: 'latest-tool', tool: currentTool}, 0, toolEntry, refresh)
383
+ }
384
+ toolEntry.addEventListener('pointermove', () => openToolDetail())
385
+ toolEntry.addEventListener('focus', () => openToolDetail())
386
+ toolEntry.addEventListener('click', () => openToolDetail())
387
+ const renderTool = tool => {
388
+ const signature = JSON.stringify(tool)
389
+ if (signature === previousTool) return
390
+ previousTool = signature
391
+ currentTool = tool
392
+ toolSection.hidden = !tool
393
+ if (!tool) {
394
+ if (detailKey === 'latest-tool') hideDetail()
395
+ return
396
+ }
397
+ toolHeading.textContent = '工具 · ' + tool.status + ' · ' + tool.records.length + ' 次'
398
+ toolName.textContent = tool.name
399
+ if (detailKey === 'latest-tool') openToolDetail(true)
400
+ }
314
401
  const choices = panel.ownerDocument.createElement('div')
315
402
  choices.setAttribute('data-prts-preview-choices', '')
316
403
  panel.querySelector('[data-prts-preview-choices]')?.remove()
@@ -450,9 +537,9 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
450
537
  detailEntry?.removeAttribute('data-prts-reply-active')
451
538
  detailEntry = undefined
452
539
  }
453
- const showDetail = (reply, index, item) => {
540
+ const showDetail = (reply, index, item, refresh = false) => {
454
541
  keep()
455
- if (detailKey === reply.key && current.detail && !current.detail.hidden) return
542
+ if (!refresh && detailKey === reply.key && current.detail && !current.detail.hidden) return
456
543
  if (!current.detail) {
457
544
  const detail = panel.ownerDocument.createElement('section')
458
545
  detail.setAttribute('data-prts-summary-reply', '')
@@ -472,10 +559,36 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
472
559
  detailEntry?.removeAttribute('data-prts-reply-active')
473
560
  detailEntry = item
474
561
  item?.setAttribute('data-prts-reply-active', '')
475
- detail.querySelector('strong').textContent = '回复 ' + (reply.ordinal ?? index + 1)
562
+ detail.toggleAttribute('data-prts-tool-history', Boolean(reply.tool))
563
+ detail.querySelector('strong').textContent = reply.tool ? '本段工具记录 · ' + reply.tool.records.length + ' 次' : '回复 ' + (reply.ordinal ?? index + 1)
476
564
  const content = detail.querySelector('[data-prts-preview-body]')
477
- content.innerHTML = current.contentCache(reply.text).html
478
- content.scrollTop = 0
565
+ const scrollTop = refresh ? content.scrollTop : 0
566
+ let anchorKey, anchorOffset
567
+ if (refresh && scrollTop > 0) {
568
+ const anchor = [...content.children].find(row => row.getBoundingClientRect().bottom > content.getBoundingClientRect().top)
569
+ anchorKey = anchor?.dataset.toolKey
570
+ anchorOffset = anchor?.getBoundingClientRect().top
571
+ }
572
+ if (reply.tool) {
573
+ content.replaceChildren(...reply.tool.records.map(record => {
574
+ const row = panel.ownerDocument.createElement('article')
575
+ row.setAttribute('data-prts-tool-record', '')
576
+ row.dataset.toolKey = record.key
577
+ const heading = panel.ownerDocument.createElement('div')
578
+ const name = panel.ownerDocument.createElement('b')
579
+ name.textContent = record.name
580
+ const status = panel.ownerDocument.createElement('small')
581
+ status.textContent = record.status
582
+ heading.append(name, status)
583
+ const summary = panel.ownerDocument.createElement('p')
584
+ summary.textContent = record.summary
585
+ row.append(heading, summary)
586
+ return row
587
+ }))
588
+ } else content.innerHTML = current.contentCache(reply.text).html
589
+ content.scrollTop = scrollTop
590
+ const retainedAnchor = anchorKey && [...content.children].find(row => row.dataset.toolKey === anchorKey)
591
+ if (retainedAnchor) content.scrollTop += retainedAnchor.getBoundingClientRect().top - anchorOffset
479
592
  detail.hidden = false
480
593
  const rect = panel.getBoundingClientRect()
481
594
  const width = Math.min(340, window.innerWidth - 16)
@@ -548,11 +661,18 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
548
661
  }))
549
662
  }
550
663
  choices.hidden = !labels.length
664
+ const tool = state === 'ongoing' && !question && getPreviewPreferences?.().previewTools !== false ? latestToolCall(source?.getSnapshot()) : undefined
665
+ renderTool(tool)
666
+ if (tool) label.textContent = '运行中 · 最新活动'
551
667
  panel.setAttribute('aria-label', title + ':' + label.textContent)
552
668
  if (state === 'ongoing' && source) {
553
669
  const replies = selectReplies(source.getSnapshot(), replyLimit())
554
- if (replies === historyKeys) return
555
- panel.toggleAttribute('data-prts-preview-empty', !replies.length)
670
+ panel.toggleAttribute('data-prts-preview-empty', !replies.length && !tool)
671
+ if (replies === historyKeys) {
672
+ const emptyText = tool ? '' : '正在处理…'
673
+ if (!replies.length && body.textContent !== emptyText) body.textContent = emptyText
674
+ return
675
+ }
556
676
  const previousTop = body.scrollTop
557
677
  const scrollAnchor = previousTop > 0 ? [...body.children].find(entry => entry.getBoundingClientRect().bottom > body.getBoundingClientRect().top) : undefined
558
678
  const anchorTop = scrollAnchor?.getBoundingClientRect().top
@@ -568,7 +688,7 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
568
688
  }
569
689
  }
570
690
  if (!historyKeys.length) body.replaceChildren()
571
- if (!replies.length) body.textContent = '正在处理,等待本轮正文输出完成…'
691
+ if (!replies.length) body.textContent = tool ? '' : '正在处理…'
572
692
  for (let index = 0; index < replies.length; index++) {
573
693
  const reply = replies[index]
574
694
  if (retained.has(reply.key)) continue
@@ -580,6 +700,13 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
580
700
  item.setAttribute('data-prts-reply-entry', '')
581
701
  const heading = panel.ownerDocument.createElement('small')
582
702
  heading.textContent = '回复 ' + reply.ordinal
703
+ const icon = panel.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'svg')
704
+ icon.setAttribute('viewBox', '0 0 16 16')
705
+ icon.setAttribute('aria-hidden', 'true')
706
+ const outline = panel.ownerDocument.createElementNS(icon.namespaceURI, 'path')
707
+ outline.setAttribute('d', 'M3 2.5h10a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7l-4 2v-2a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1Z')
708
+ icon.append(outline)
709
+ heading.prepend(icon)
583
710
  const summary = panel.ownerDocument.createElement('span')
584
711
  summary.textContent = current.contentCache(reply.text).summary
585
712
  item.append(heading, summary)
@@ -610,7 +737,7 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
610
737
  ? approval
611
738
  ? [pending.toolName, pending.displayReason?.['zh-CN'] || pending.displayReason?.zh || pending.reason || pending.displayReason?.en || '请进入会话查看操作详情'].filter(Boolean).join(':')
612
739
  : first?.question || pendingQuestionPreview(pending)
613
- : source ? excerpt || (state === 'ongoing' ? '正在处理,尚未输出正文…' : '暂无正文回复,可进入会话查看执行记录。') : '正在读取…'
740
+ : source ? excerpt || (state === 'ongoing' ? '正在处理…' : '暂无正文回复,可进入会话查看执行记录。') : '正在读取…'
614
741
  if (previousText !== text || previousQuestion !== question) {
615
742
  const extendsReply = previousQuestion === question && previousText && text.startsWith(previousText)
616
743
  if (question) body.textContent = text
@@ -631,7 +758,8 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
631
758
  })
632
759
  render()
633
760
  body.scrollTop = 0
634
- current.delay = window.setTimeout(async () => {
761
+ current.delay = window.requestAnimationFrame(async () => {
762
+ current.delay = undefined
635
763
  try {
636
764
  const service = getUiConversation?.()
637
765
  const controller = getSessions?.() ?? sessions
@@ -643,11 +771,13 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
643
771
  current.source = source
644
772
  current.unsubscribe = source.subscribe(() => {
645
773
  if (active !== current || current.refresh !== undefined) return
646
- current.refresh = window.setTimeout(() => {
774
+ current.refresh = window.requestAnimationFrame(() => {
647
775
  current.refresh = undefined
648
- if (current.state === 'ongoing' && !current.editor && selectReplies(source.getSnapshot(), replyLimit()) === historyKeys) return
776
+ if (current.state === 'ongoing' && !current.editor
777
+ && selectReplies(source.getSnapshot(), replyLimit()) === historyKeys
778
+ && JSON.stringify(getPreviewPreferences?.().previewTools === false ? undefined : latestToolCall(source.getSnapshot())) === previousTool) return
649
779
  render()
650
- }, 120)
780
+ })
651
781
  })
652
782
  render()
653
783
  } catch {
@@ -659,7 +789,7 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
659
789
  render()
660
790
  if (body.textContent.trim() === '正在读取…') body.textContent = '暂时无法读取回复,可点击会话查看'
661
791
  }
662
- }, cached ? 0 : 80)
792
+ })
663
793
  }
664
794
  function leave() {
665
795
  if (!active || active.sticky) return
@@ -673,6 +803,8 @@ export function createSessionSummaryPreview({ document, window, sessions, getSes
673
803
  if (!active) return
674
804
  const anchor = [...root.querySelectorAll('[data-prts-session-summary-item]')]
675
805
  .find(item => item.getAttribute('data-prts-session-summary-item') === active.id)
806
+ // A read completion (or removed session) has no status row to own a preview.
807
+ if (!anchor) { stop(); return }
676
808
  active.group?.removeAttribute('data-prts-preview-open')
677
809
  active.anchor?.removeAttribute('data-prts-preview-active')
678
810
  active.anchor = anchor
@@ -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
@@ -59,7 +59,7 @@ function settingButtons(key, label, options) {
59
59
  : key === 'sessionFlowPalette'
60
60
  ? '<small data-prts-setting-note>同时适配日间与夜间模式</small>'
61
61
  : ''
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}">
62
+ if (['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
63
  <span><strong>${label}</strong>${note}</span>
64
64
  <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
65
  </div>`
@@ -127,6 +127,7 @@ export function themeSettingsMarkup() {
127
127
  <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
128
  <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
129
  <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>
130
+ <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
131
  </nav>
131
132
  <footer data-prts-settings-actions aria-label="全局主题设置操作">
132
133
  <div data-prts-persistence-error hidden role="alert">
@@ -167,6 +168,33 @@ export function themeSettingsMarkup() {
167
168
  ${COMMON_CONTROLS.filter(([key]) => ['cursor', 'railDefaultHidden', 'motion'].includes(key)).map(([key, label, options]) => settingButtons(key, label, options)).join('')}
168
169
  </div>
169
170
  </section>
171
+ <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>
172
+ <div data-prts-preview-settings><div data-prts-preview-master>
173
+ ${previewSetting('aiToolsEnabled', '允许 AI 定制主题', '默认开启 · 共 5 个工具 · 影响此 DSH 实例的所有会话')}
174
+ </div></div>
175
+ <p>开启后,AI 可以通过工具调整粒子徽记和 Baker 头像。关闭后,后续模型请求不再提供这些工具,手动设置不受影响;已有对话中的工具记录不会删除。</p>
176
+ <section>
177
+ <h3>粒子徽记 <small>3 个工具</small></h3>
178
+ <p>将会话中的图片制作成粒子徽记,用于新会话页面或正文轮换。</p>
179
+ <ol><li>上传图片,或让 AI 读取已生成的图片。</li><li>让 AI 制作草稿,调整轮廓、位置和缩放,先查看预览。</li><li>满意后明确要求应用到新会话徽记,或加入正文轮换。</li></ol>
180
+ <blockquote>“把这张图片做成粒子徽记,先给我看看效果。”</blockquote>
181
+ <details><summary>工具说明与限制</summary><dl>
182
+ <dt><code>prts_emblem_inspect</code></dt><dd>查看徽记配置、版本与当前会话的可用图片,不修改设置。</dd>
183
+ <dt><code>prts_emblem_preview</code></dt><dd>制作或调整粒子徽记草稿,只生成预览,不直接应用。</dd>
184
+ <dt><code>prts_emblem_apply</code></dt><dd>经你确认后,替换新会话徽记,或添加、替换正文轮换徽记。</dd>
185
+ </dl><p>单张来源图片最大 8 MB。徽记设置跨工作区和会话共享。粒子预览不等同于原图;配置发生变化时,AI 需要重新核对后再应用。</p></details>
186
+ </section>
187
+ <section>
188
+ <h3>Baker 头像 <small>2 个工具</small></h3>
189
+ <p>为用户和 AI 设置头像,调整圆形裁剪,或恢复默认头像。仅在 Baker 会话样式下显示。</p>
190
+ <ol><li>上传图片,说明要修改用户头像还是 AI 头像。</li><li>让 AI 查看当前配置,再按你的要求替换、裁剪或恢复默认。</li></ol>
191
+ <blockquote>“把这张图片设为 AI 头像,居中裁剪。”</blockquote>
192
+ <details><summary>工具说明与限制</summary><dl>
193
+ <dt><code>prts_avatar_inspect</code></dt><dd>查看头像配置、原图尺寸、裁剪参数及会话图片,不修改设置。</dd>
194
+ <dt><code>prts_avatar_apply</code></dt><dd>替换用户或 AI 头像、基于现有原图重新裁剪,或恢复默认头像。</dd>
195
+ </dl><p>单图最大 5 MB、2500 万像素;裁剪支持 1~4 倍缩放。修改立即影响此实例所有会话,不会自动切换会话样式。替换后不保留旧原图;恢复默认会删除自定义原图。</p></details>
196
+ </section>
197
+ </section>
170
198
  <section data-prts-settings-section="assistance" role="tabpanel" id="prts-page-assistance" aria-labelledby="prts-tab-assistance" tabindex="0" hidden>
171
199
  <section data-prts-preview-settings>
172
200
  <div data-prts-preview-master>
@@ -177,6 +205,7 @@ export function themeSettingsMarkup() {
177
205
  <header><strong>悬停预览</strong><small>选择哪些状态显示详细浮窗</small></header>
178
206
  ${previewSetting('previewOngoing', '运行中预览', '逐轮查看已完成的回复,最新内容置顶')}
179
207
  <div data-prts-preview-details="previewOngoing">
208
+ ${previewSetting('previewTools', '显示工具调用', '显示工具进度,悬停查看最新正文之后的调用记录')}
180
209
  <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
210
  ${previewSetting('previewFollowOngoing', '运行结束后跟随', '转为待处理或已完成时,自动切换预览')}
182
211
  </div>
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 disposers=[...emblemTools(service,ctx.attachments),...avatarTools(avatars,ctx.attachments)].map(tool=>ctx.tools.register(tool))
31
- disposers.push(registerEmblemApi(ctx,service),registerAvatarApi(ctx,avatars),registerBackgroundApi(ctx,backgrounds))
32
- ctx.effect(()=>async()=>{for(const dispose of disposers.reverse())dispose();await service.dispose();await backgrounds.dispose()})
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
  }
@@ -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; }
@@ -919,3 +919,13 @@ html[data-dsh-prts] [data-prts-preview-settings] {
919
919
  & [data-prts-setting-row] { gap:12px; }
920
920
  }
921
921
  }
922
+
923
+ html[data-dsh-prts] [data-prts-theme-settings] [data-prts-ai-tools] { padding:0 20px 20px;font:12px/1.75 var(--prts-body);overflow-wrap:anywhere; }
924
+ html[data-dsh-prts] [data-prts-ai-tools] > section { padding:16px 0;border-top:1px solid var(--prts-line); }
925
+ html[data-dsh-prts] [data-prts-ai-tools] h3 { margin:0 0 8px;font-size:14px; }
926
+ html[data-dsh-prts] [data-prts-ai-tools] :is(p,dd,small) { color:var(--prts-muted); }
927
+ html[data-dsh-prts] [data-prts-ai-tools] h3 small { margin-left:8px;font-size:11px;font-weight:400; }
928
+ html[data-dsh-prts] [data-prts-ai-tools] blockquote { margin:12px 0;padding:8px 12px;border-left:2px solid var(--prts-cyan);background:color-mix(in srgb,var(--prts-cyan) 5%,transparent); }
929
+ html[data-dsh-prts] [data-prts-ai-tools] summary { cursor:pointer; }
930
+ html[data-dsh-prts] [data-prts-ai-tools] dt { margin-top:12px; }
931
+ html[data-dsh-prts] [data-prts-ai-tools] dd { margin:3px 0 0; }