dsh-brief-sidebar 0.9.2 → 0.9.3

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.
@@ -61,6 +61,9 @@ export declare const zh: {
61
61
  'recall.supersededTag': string;
62
62
  'recall.noBody': string;
63
63
  'section.skillUsage': string;
64
+ 'section.references': string;
65
+ 'references.tabSkills': string;
66
+ 'references.tabMemory': string;
64
67
  'skillUsage.empty': string;
65
68
  'skillUsage.emptyHint': string;
66
69
  'skillUsage.uses': string;
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ import type { FeedbackFetch } from './recall-feedback';
4
+ export interface ReferenceSectionProps {
5
+ /** The DSH locale lookup, passed down from `apply`. */
6
+ t: (key: string) => string;
7
+ /** The client root context (the tab body's only way to reach services). */
8
+ ctx?: Context;
9
+ /** The session this tab is scoped to. */
10
+ scope?: {
11
+ readonly sessionId?: string;
12
+ };
13
+ /** The rating transport, handed straight to the recall pane. */
14
+ fetchImpl?: FeedbackFetch;
15
+ }
16
+ /** Which pane is showing. Two values, no third tab by design. */
17
+ export type ReferenceTab = 'skills' | 'memory';
18
+ /**
19
+ * The references section. All hooks run before any early return, so the tab
20
+ * strip can disappear (no capability at all) without ever changing hook order.
21
+ *
22
+ * @param props - translation, client context, session scope, rating transport.
23
+ */
24
+ export declare function ReferenceSection(props: ReferenceSectionProps): ReactNode;
@@ -9,6 +9,12 @@ export interface SkillUsageSectionProps {
9
9
  scope?: {
10
10
  readonly sessionId?: string;
11
11
  };
12
+ /**
13
+ * Drop the section header. The references tab already says “技能”, so
14
+ * repeating it as a heading inside the pane is noise; standalone renders
15
+ * (and every existing test) keep the default `false`.
16
+ */
17
+ hideHeader?: boolean;
12
18
  }
13
19
  /**
14
20
  * The skill-usage section. The subscription is unconditional (a plain listener,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-brief-sidebar",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "DSH web plugin (dsh-better-sidebar consumer): render the session brief — the todo list (the `todos` projection board) plus the artifact list (a live produced-files feed, codeplan artifacts marked) — as a summary tab in the right sidebar, and shadow the official composer-band todo panel so the board is the single visible carrier. Read-only.",
5
5
  "type": "module",
6
6
  "main": "lib/index.mjs",
@@ -2,11 +2,13 @@
2
2
  * The summary tab body: the session's at-a-glance state, composed of sections.
3
3
  *
4
4
  * Section order: 进展 (the todos board) → 产物 (produced files, codeplan
5
- * artifacts marked inline) → 技能引用 (which skills this session loaded) →
6
- * 本会话召回 (what prime-memory injected into this session and compaction has
7
- * not overwritten) → 记忆槽位 (hand-pinned resident conventions, off by default).
8
- * Each section owns its own data chain and its own gating contract; this
9
- * component owns only the shell.
5
+ * artifacts marked inline) → 引用 (two switchable tabs: 技能 and 记忆).
6
+ * The reference tabs replaced three stacked sections: which skills entered the
7
+ * session, what prime-memory injected and compaction has not overwritten, and
8
+ * the hand-pinned resident slots — all three answer "what is in this session's
9
+ * context", and stacking them pushed the board off a narrow sidebar. Each pane
10
+ * keeps its own data chain and its own gating contract; this component owns
11
+ * only the shell.
10
12
  *
11
13
  * Two better-sidebar contracts are honoured:
12
14
  *
@@ -24,9 +26,7 @@ import type { Context } from '@deepseek-ai/cordis'
24
26
  import { TodoSection } from './TodoBoardTab'
25
27
  import type { TodoScope } from './TodoBoardTab'
26
28
  import { DeliverablesSection } from './summary/DeliverablesSection'
27
- import { SkillUsageSection } from './summary/SkillUsageSection'
28
- import { MemorySlotsSection } from './summary/MemorySlotsSection'
29
- import { MemoryRecallSection } from './summary/MemoryRecallSection'
29
+ import { ReferenceSection } from './summary/ReferenceSection'
30
30
 
31
31
  export interface SummaryTabProps {
32
32
  /** The DSH locale lookup, passed down from `apply`. */
@@ -69,16 +69,10 @@ export function SummaryTab(props: SummaryTabProps): ReactNode {
69
69
  createElement('div', { style: SCROLL_STYLE },
70
70
  createElement(TodoSection, { key: 'progress', t, ctx, scope }),
71
71
  createElement(DeliverablesSection, { key: 'deliverables', t, ctx, scope }),
72
- // 技能引用:本会话通过宿主 `skill` 工具载入过哪些技能(`dshSummarySkillUsage`
73
- // 投影,本插件自己的 host 半注册)。与记忆两栏正交:这里讲“拉过哪些说明书”,
74
- // 下面两栏讲“模型看到了哪些记忆”。只读,无开关,投影缺席即整段隐藏。
75
- createElement(SkillUsageSection, { key: 'skill-usage', t, ctx, scope }),
76
- // 本会话召回(固定最底):入口开关(configForms `showMemoryRecall`,默认开)
77
- // 与能力存在(dsh-prime-memory 的 `memoryRecall` 投影)缺一即整段隐藏。
78
- // 这是"模型此刻看到哪些记忆"的诚实视图:非常驻,压缩后即清空。
79
- createElement(MemoryRecallSection, { key: 'memory-recall', t, ctx, scope }),
80
- // 记忆槽位分区:手动 pin 的常驻约定(`memorySlots` 投影),默认关。
81
- createElement(MemorySlotsSection, { key: 'memory-slots', t, ctx, scope }),
72
+ // 引用:技能页(`dshSummarySkillUsage`,本插件自己的 host 半注册)与记忆页
73
+ // (`memoryRecall` / `memorySlots`,由 dsh-prime-memory 提供,各有开关)并列,
74
+ // 可点击或用左右方向键切换。两个页签都空时整节隐藏(无能力 ≠ 空态)。
75
+ createElement(ReferenceSection, { key: 'references', t, ctx, scope }),
82
76
  ),
83
77
  )
84
78
  }
@@ -63,6 +63,9 @@ export const zh = {
63
63
  'recall.supersededTag': '已被摘要取代',
64
64
  'recall.noBody': '(本帧没有这条记忆的全文)',
65
65
  'section.skillUsage': '技能引用',
66
+ 'section.references': '引用',
67
+ 'references.tabSkills': '技能',
68
+ 'references.tabMemory': '记忆',
66
69
  'skillUsage.empty': '本会话还没载入过技能',
67
70
  'skillUsage.emptyHint': '模型用 skill 工具载入、或你直接点用某个技能后,技能名会实时出现在这里。',
68
71
  'skillUsage.uses': '{count} 次',
@@ -129,6 +132,9 @@ export const en: Record<BriefKey, string> = {
129
132
  'recall.supersededTag': 'replaced by summary',
130
133
  'recall.noBody': '(this frame carries no full body for this record)',
131
134
  'section.skillUsage': 'Skill references',
135
+ 'section.references': 'References',
136
+ 'references.tabSkills': 'Skills',
137
+ 'references.tabMemory': 'Memory',
132
138
  'skillUsage.empty': 'No skills loaded in this session',
133
139
  'skillUsage.emptyHint': 'Skills show up here once the model loads one with the `skill` tool, or you invoke one directly.',
134
140
  'skillUsage.uses': '{count}x',
@@ -192,6 +198,9 @@ export const ja: Record<BriefKey, string> = {
192
198
  'recall.supersededTag': '要約に置換済み',
193
199
  'recall.noBody': 'このフレームには全文が含まれていない',
194
200
  'section.skillUsage': 'スキル参照',
201
+ 'section.references': '参照',
202
+ 'references.tabSkills': 'スキル',
203
+ 'references.tabMemory': 'メモリ',
195
204
  'skillUsage.empty': 'このセッションではスキルを読み込んでいません',
196
205
  'skillUsage.emptyHint': 'モデルが skill ツールで読み込むか、あなたがスキルを直接起動すると、ここにリアルタイムで表示されます。',
197
206
  'skillUsage.uses': '{count} 回',
@@ -255,6 +264,9 @@ export const ko: Record<BriefKey, string> = {
255
264
  'recall.supersededTag': '요약에 대체됨',
256
265
  'recall.noBody': '이 프레임에는 이 기록의 전문이 없습니다',
257
266
  'section.skillUsage': '스킬 참조',
267
+ 'section.references': '참조',
268
+ 'references.tabSkills': '스킬',
269
+ 'references.tabMemory': '메모리',
258
270
  'skillUsage.empty': '이 세션에서 스킬을 불러오지 않았습니다',
259
271
  'skillUsage.emptyHint': '모델이 skill 도구로 불러오거나 당신이 스킬을 직접 실행하면 여기에 실시간으로 나타납니다.',
260
272
  'skillUsage.uses': '{count}회',
@@ -318,6 +330,9 @@ export const fr: Record<BriefKey, string> = {
318
330
  'recall.supersededTag': 'remplacé par le résumé',
319
331
  'recall.noBody': '(aucun texte complet pour cette entrée dans cette trame)',
320
332
  'section.skillUsage': 'Références de compétences',
333
+ 'section.references': 'Références',
334
+ 'references.tabSkills': 'Compétences',
335
+ 'references.tabMemory': 'Mémoire',
321
336
  'skillUsage.empty': 'Aucune compétence chargée dans cette session',
322
337
  'skillUsage.emptyHint': 'Les compétences apparaissent ici dès que le modèle en charge une via l’outil `skill`, ou quand vous en lancez une directement.',
323
338
  'skillUsage.uses': '{count}×',
@@ -381,6 +396,9 @@ export const de: Record<BriefKey, string> = {
381
396
  'recall.supersededTag': 'durch Zusammenfassung ersetzt',
382
397
  'recall.noBody': '(dieser Rahmen enthält keinen vollständigen Text für diesen Eintrag)',
383
398
  'section.skillUsage': 'Skill-Referenzen',
399
+ 'section.references': 'Referenzen',
400
+ 'references.tabSkills': 'Skills',
401
+ 'references.tabMemory': 'Speicher',
384
402
  'skillUsage.empty': 'In dieser Sitzung wurden keine Skills geladen',
385
403
  'skillUsage.emptyHint': 'Skills erscheinen hier, sobald das Modell einen über das `skill`-Tool lädt oder du einen direkt aufrufst.',
386
404
  'skillUsage.uses': '{count}x',
@@ -444,6 +462,9 @@ export const it: Record<BriefKey, string> = {
444
462
  'recall.supersededTag': 'sostituito dal riassunto',
445
463
  'recall.noBody': '(questo frame non contiene il testo completo di questa voce)',
446
464
  'section.skillUsage': 'Riferimenti alle skill',
465
+ 'section.references': 'Riferimenti',
466
+ 'references.tabSkills': 'Skill',
467
+ 'references.tabMemory': 'Memoria',
447
468
  'skillUsage.empty': 'Nessuna skill caricata in questa sessione',
448
469
  'skillUsage.emptyHint': 'Le skill compaiono qui quando il modello ne carica una con il tool `skill` o quando ne avvii una direttamente.',
449
470
  'skillUsage.uses': '{count}x',
@@ -507,6 +528,9 @@ export const ru: Record<BriefKey, string> = {
507
528
  'recall.supersededTag': 'заменено сводкой',
508
529
  'recall.noBody': 'В этом кадре нет полного текста этой записи',
509
530
  'section.skillUsage': 'Отсылки к скиллам',
531
+ 'section.references': 'Отсылки',
532
+ 'references.tabSkills': 'Навыки',
533
+ 'references.tabMemory': 'Память',
510
534
  'skillUsage.empty': 'В этой сессии скиллы не загружались',
511
535
  'skillUsage.emptyHint': 'Навыки появятся здесь, когда модель загрузит их через инструмент `skill` или вы вызовете навык напрямую.',
512
536
  'skillUsage.uses': '{count} раз',
@@ -570,6 +594,9 @@ export const es: Record<BriefKey, string> = {
570
594
  'recall.supersededTag': 'reemplazado por el resumen',
571
595
  'recall.noBody': '(este fotograma no trae el texto completo de este registro)',
572
596
  'section.skillUsage': 'Referencias de skills',
597
+ 'section.references': 'Referencias',
598
+ 'references.tabSkills': 'Skills',
599
+ 'references.tabMemory': 'Memoria',
573
600
  'skillUsage.empty': 'Esta sesión no ha cargado ningún skill',
574
601
  'skillUsage.emptyHint': 'Las skills aparecen aquí cuando el modelo carga una con la herramienta `skill` o cuando invocas una directamente.',
575
602
  'skillUsage.uses': '{count}x',
@@ -0,0 +1,193 @@
1
+ /**
2
+ * The references section: 技能与记忆并列成两个可左右切换的页签。
3
+ *
4
+ * Why a tab strip instead of three stacked sections: the two lists answer the
5
+ * same question — "what entered this session's context" — from two different
6
+ * providers (this plugin's own skill fold, and prime-memory's recall/slots).
7
+ * Stacked, they pushed the board and the produced files off screen on a narrow
8
+ * sidebar; side by side, each gets the full width when the reader actually
9
+ * wants it.
10
+ *
11
+ * Two decisions worth knowing when editing this file:
12
+ *
13
+ * 1. **The inactive pane stays mounted and is hidden with `display:none`.**
14
+ * The recall pane carries real interaction state (selected row, pending
15
+ * rating, the expanded history group); unmounting it would silently reset
16
+ * what the human just did every time they glanced at the skills tab. The
17
+ * cost is that a hidden pane keeps re-rendering on projection frames —
18
+ * bounded, because the whole tab still renders nothing while `visible` is
19
+ * false (see `SummaryTab`).
20
+ * 2. **The parent repeats its children's gates.** A section that hides itself
21
+ * leaves the tab strip pointing at an empty pane, so the availability
22
+ * question is answered here with the SAME readers and resolvers the children
23
+ * use. `tests/reference-section.spec.tsx` pins both sides against the same
24
+ * fixtures — if a child gate ever drifts from this one, that test goes red
25
+ * rather than the UI growing a dead tab.
26
+ *
27
+ * Colours come from `--dsw-alias-*` tokens only (skin contract).
28
+ */
29
+ import { createElement } from 'react'
30
+ import { useCallback, useState } from 'react'
31
+ import type { CSSProperties, KeyboardEvent, ReactNode } from 'react'
32
+ import type { Context } from '@deepseek-ai/cordis'
33
+ import { SKILL_USAGE_KEY } from '../../projection/keys'
34
+ import { useProjectionValue } from '../use-projection'
35
+ import { readSkillUsage } from './skill-usage'
36
+ import { MEMORY_RECALL_KEY, readRecall } from './memory-recall'
37
+ import { MEMORY_SLOTS_KEY, readSlots } from './memory-slots'
38
+ import { resolveShowMemoryRecall, resolveShowMemorySlots } from '../scope-face'
39
+ import { useConfigFlag } from './use-config-flag'
40
+ import { SkillUsageSection } from './SkillUsageSection'
41
+ import { MemoryRecallSection } from './MemoryRecallSection'
42
+ import { MemorySlotsSection } from './MemorySlotsSection'
43
+ import type { FeedbackFetch } from './recall-feedback'
44
+
45
+ export interface ReferenceSectionProps {
46
+ /** The DSH locale lookup, passed down from `apply`. */
47
+ t: (key: string) => string
48
+ /** The client root context (the tab body's only way to reach services). */
49
+ ctx?: Context
50
+ /** The session this tab is scoped to. */
51
+ scope?: { readonly sessionId?: string }
52
+ /** The rating transport, handed straight to the recall pane. */
53
+ fetchImpl?: FeedbackFetch
54
+ }
55
+
56
+ /** Which pane is showing. Two values, no third tab by design. */
57
+ export type ReferenceTab = 'skills' | 'memory'
58
+
59
+ const SECTION_STYLE: CSSProperties = {
60
+ flexShrink: 0,
61
+ borderBottom: '1px solid var(--dsw-alias-border-secondary)',
62
+ }
63
+
64
+ const HEADER_STYLE: CSSProperties = {
65
+ padding: '10px 12px 0',
66
+ color: 'var(--dsw-alias-label-secondary)',
67
+ fontSize: 12,
68
+ fontWeight: 600,
69
+ }
70
+
71
+ /** The tab strip: flush-left, same gutter as section headers. */
72
+ const TABS_STYLE: CSSProperties = {
73
+ display: 'flex',
74
+ gap: 4,
75
+ padding: '6px 12px 0',
76
+ }
77
+
78
+ /** One tab. Selected gets a hairline of secondary label; never a colour literal. */
79
+ const TAB_STYLE: CSSProperties = {
80
+ background: 'none',
81
+ border: 'none',
82
+ borderBottom: '1px solid transparent',
83
+ font: 'inherit',
84
+ fontSize: 12,
85
+ color: 'var(--dsw-alias-label-tertiary)',
86
+ cursor: 'pointer',
87
+ padding: '4px 8px 6px',
88
+ }
89
+
90
+ const TAB_ACTIVE_STYLE: CSSProperties = {
91
+ ...TAB_STYLE,
92
+ borderBottom: '1px solid var(--dsw-alias-label-secondary)',
93
+ color: 'var(--dsw-alias-label-primary)',
94
+ fontWeight: 600,
95
+ }
96
+
97
+ const ORDER: readonly ReferenceTab[] = ['skills', 'memory']
98
+
99
+ /**
100
+ * The references section. All hooks run before any early return, so the tab
101
+ * strip can disappear (no capability at all) without ever changing hook order.
102
+ *
103
+ * @param props - translation, client context, session scope, rating transport.
104
+ */
105
+ export function ReferenceSection(props: ReferenceSectionProps): ReactNode {
106
+ const { t, ctx, scope, fetchImpl } = props
107
+ const [tab, setTab] = useState<ReferenceTab>('skills')
108
+
109
+ // The same three reads the children make, used only to decide whether the
110
+ // strip has anything to point at.
111
+ const skillsLive = readSkillUsage(useProjectionValue(ctx, scope?.sessionId, SKILL_USAGE_KEY)) !== undefined
112
+ const recallFrame = readRecall(useProjectionValue(ctx, scope?.sessionId, MEMORY_RECALL_KEY))
113
+ const slotsFrame = readSlots(useProjectionValue(ctx, scope?.sessionId, MEMORY_SLOTS_KEY))
114
+ const recallOn = useConfigFlag(ctx, resolveShowMemoryRecall) !== false
115
+ const slotsOn = useConfigFlag(ctx, resolveShowMemorySlots) === true
116
+ const memoryLive = (recallFrame !== undefined && recallOn) || (slotsFrame !== undefined && slotsOn)
117
+
118
+ const onTabsKeyDown = useCallback((event: KeyboardEvent<HTMLDivElement>): void => {
119
+ // 左右方向键换页:页签条只有两个成员,环绕即可,不需要 Home/End。
120
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
121
+ event.preventDefault()
122
+ const at = ORDER.indexOf(tab)
123
+ const step = event.key === 'ArrowRight' ? 1 : ORDER.length - 1
124
+ setTab(ORDER[(at + step) % ORDER.length])
125
+ }, [tab])
126
+
127
+ // Nothing to reference — no provider in the composition, or both switched off.
128
+ if (!skillsLive && !memoryLive) return null
129
+
130
+ const tabButton = (id: ReferenceTab, label: string): ReactNode => createElement(
131
+ 'button',
132
+ {
133
+ key: id,
134
+ type: 'button',
135
+ role: 'tab',
136
+ id: `dsh-brief-reference-tab-${id}`,
137
+ 'aria-selected': active === id,
138
+ 'aria-controls': `dsh-brief-reference-pane-${id}`,
139
+ style: active === id ? TAB_ACTIVE_STYLE : TAB_STYLE,
140
+ 'data-dsh-brief-sidebar': `reference-tab-${id}`,
141
+ onClick: () => { setTab(id) },
142
+ },
143
+ label,
144
+ )
145
+
146
+ // 只剩记忆一侧时默认页得跟着换:否则开场就是一个隐藏的空技能页。
147
+ const active: ReferenceTab = tab === 'skills' && !skillsLive ? 'memory' : tab
148
+
149
+ return createElement(
150
+ 'div',
151
+ { style: SECTION_STYLE, 'data-dsh-brief-sidebar': 'references-section' },
152
+ createElement('div', { style: HEADER_STYLE }, t('section.references')),
153
+ createElement(
154
+ 'div',
155
+ {
156
+ role: 'tablist',
157
+ 'aria-label': t('section.references'),
158
+ style: TABS_STYLE,
159
+ onKeyDown: onTabsKeyDown,
160
+ 'data-dsh-brief-sidebar': 'reference-tabs',
161
+ },
162
+ skillsLive && tabButton('skills', t('references.tabSkills')),
163
+ // 两个页签各自条件化:只有一侧能力时不该留个点了就空的页签。
164
+ memoryLive && tabButton('memory', t('references.tabMemory')),
165
+ ),
166
+ skillsLive && createElement(
167
+ 'div',
168
+ {
169
+ role: 'tabpanel',
170
+ id: 'dsh-brief-reference-pane-skills',
171
+ 'aria-labelledby': 'dsh-brief-reference-tab-skills',
172
+ hidden: active !== 'skills',
173
+ style: active === 'skills' ? undefined : { display: 'none' },
174
+ 'data-dsh-brief-sidebar': 'reference-pane-skills',
175
+ },
176
+ createElement(SkillUsageSection, { t, ctx, scope, hideHeader: true }),
177
+ ),
178
+ memoryLive && createElement(
179
+ 'div',
180
+ {
181
+ role: 'tabpanel',
182
+ id: 'dsh-brief-reference-pane-memory',
183
+ 'aria-labelledby': 'dsh-brief-reference-tab-memory',
184
+ hidden: active !== 'memory',
185
+ style: active === 'memory' ? undefined : { display: 'none' },
186
+ 'data-dsh-brief-sidebar': 'reference-pane-memory',
187
+ },
188
+ // 两条记忆链各有自己的标题:召回与常驻槽位是不同事实,合成一节会糊掉。
189
+ createElement(MemoryRecallSection, { t, ctx, scope, fetchImpl }),
190
+ createElement(MemorySlotsSection, { t, ctx, scope }),
191
+ ),
192
+ )
193
+ }
@@ -43,6 +43,12 @@ export interface SkillUsageSectionProps {
43
43
  ctx?: Context
44
44
  /** The session this tab is scoped to. */
45
45
  scope?: { readonly sessionId?: string }
46
+ /**
47
+ * Drop the section header. The references tab already says “技能”, so
48
+ * repeating it as a heading inside the pane is noise; standalone renders
49
+ * (and every existing test) keep the default `false`.
50
+ */
51
+ hideHeader?: boolean
46
52
  }
47
53
 
48
54
  const SECTION_STYLE: CSSProperties = {
@@ -121,7 +127,7 @@ const EMPTY_STYLE: CSSProperties = {
121
127
  * @param props - translation, client context, session scope.
122
128
  */
123
129
  export function SkillUsageSection(props: SkillUsageSectionProps): ReactNode {
124
- const { t, ctx, scope } = props
130
+ const { t, ctx, scope, hideHeader = false } = props
125
131
  const [availableOpen, setAvailableOpen] = useState(false)
126
132
 
127
133
  const view = readSkillUsage(useProjectionValue(ctx, scope?.sessionId, SKILL_USAGE_KEY))
@@ -137,7 +143,7 @@ export function SkillUsageSection(props: SkillUsageSectionProps): ReactNode {
137
143
  return createElement(
138
144
  'div',
139
145
  { style: SECTION_STYLE, 'data-dsh-brief-sidebar': 'skill-usage-section' },
140
- createElement('div', { style: HEADER_STYLE }, t('section.skillUsage')),
146
+ !hideHeader && createElement('div', { style: HEADER_STYLE }, t('section.skillUsage')),
141
147
  view.session.length === 0 && createElement(
142
148
  'div',
143
149
  { style: EMPTY_STYLE },