dsh-session-title-stamp 0.0.0-stage → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +267 -2
- package/README.zh.md +200 -0
- package/client.js +278 -0
- package/cordis.patch.yml +29 -0
- package/icon.svg +45 -0
- package/lib/format.js +636 -0
- package/lib/index.js +544 -0
- package/market/index.json +74 -0
- package/package.json +68 -4
package/client.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of dsh-session-title-stamp.
|
|
3
|
+
*
|
|
4
|
+
* Registers two seats:
|
|
5
|
+
* - a refresh button in the conversation header
|
|
6
|
+
* (`conversation.session.header.actions`)
|
|
7
|
+
* - a compact cost/duration line in the Session hover card
|
|
8
|
+
* (`sidebar.session.row.hover`)
|
|
9
|
+
*
|
|
10
|
+
* Hand-written module-loader bundle: React comes from the browser module
|
|
11
|
+
* table, so this package has no client build step.
|
|
12
|
+
*/
|
|
13
|
+
window.__ModuleLoader__.load({
|
|
14
|
+
id: 'dsh-session-title-stamp',
|
|
15
|
+
factory(require) {
|
|
16
|
+
const React = require('react')
|
|
17
|
+
const { useEffect, useState } = React
|
|
18
|
+
const h = React.createElement
|
|
19
|
+
|
|
20
|
+
const NS = 'dsh-session-title-stamp'
|
|
21
|
+
const STATS_PATH = '/api/session-title-stamp/stats'
|
|
22
|
+
const inject = ['slots', 'remote', 'remote.commands', 'locale']
|
|
23
|
+
const DICT = {
|
|
24
|
+
zh: {
|
|
25
|
+
menu: '自动生成标题',
|
|
26
|
+
tip: '自动识别分类,生成「日期 | 分类 | 名称」标题',
|
|
27
|
+
running: '会话运行中,稍后再试',
|
|
28
|
+
duration: '耗时',
|
|
29
|
+
estimate: '估算',
|
|
30
|
+
},
|
|
31
|
+
en: {
|
|
32
|
+
menu: 'Generate title',
|
|
33
|
+
tip: 'Detect the category and generate a DATE | CATEGORY | NAME title',
|
|
34
|
+
running: 'Session is running; try again later',
|
|
35
|
+
duration: 'Duration',
|
|
36
|
+
estimate: 'est.',
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatDuration(ms) {
|
|
41
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) return undefined
|
|
42
|
+
const seconds = Math.round(ms / 1000)
|
|
43
|
+
if (seconds < 60) return `${seconds}s`
|
|
44
|
+
const minutes = Math.round(seconds / 60)
|
|
45
|
+
if (minutes < 60) return `${minutes}m`
|
|
46
|
+
const hours = Math.floor(minutes / 60)
|
|
47
|
+
const rest = minutes % 60
|
|
48
|
+
return rest === 0 ? `${hours}h` : `${hours}h ${rest}m`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatCost(cny) {
|
|
52
|
+
if (typeof cny !== 'number' || !Number.isFinite(cny) || cny < 0) return undefined
|
|
53
|
+
if (cny < 0.01) return `≈¥${cny.toFixed(3)}`
|
|
54
|
+
return `≈¥${cny.toFixed(2)}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatTokens(tokens) {
|
|
58
|
+
if (typeof tokens !== 'number' || !Number.isFinite(tokens) || tokens <= 0) return undefined
|
|
59
|
+
if (tokens < 1000) return `${tokens}`
|
|
60
|
+
if (tokens < 1_000_000) return `${(tokens / 1000).toFixed(1)}k`
|
|
61
|
+
return `${(tokens / 1_000_000).toFixed(2)}M`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function RefreshIcon() {
|
|
65
|
+
return h(
|
|
66
|
+
'svg',
|
|
67
|
+
{
|
|
68
|
+
width: 14,
|
|
69
|
+
height: 14,
|
|
70
|
+
viewBox: '0 0 16 16',
|
|
71
|
+
fill: 'none',
|
|
72
|
+
stroke: 'currentColor',
|
|
73
|
+
strokeWidth: 1.5,
|
|
74
|
+
strokeLinecap: 'round',
|
|
75
|
+
strokeLinejoin: 'round',
|
|
76
|
+
'aria-hidden': 'true',
|
|
77
|
+
},
|
|
78
|
+
h('path', { d: 'M13.5 8a5.5 5.5 0 1 1-1.6-3.9' }),
|
|
79
|
+
h('path', { d: 'M13.5 2v4h-4' }),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function RefreshAction({ sessionId, useSession, run, t }) {
|
|
84
|
+
const [busy, setBusy] = useState(false)
|
|
85
|
+
const [failed, setFailed] = useState(false)
|
|
86
|
+
const running =
|
|
87
|
+
typeof useSession === 'function' ? useSession((snapshot) => snapshot?.running === true) : false
|
|
88
|
+
const disabled = busy || running || typeof sessionId !== 'string' || sessionId === ''
|
|
89
|
+
|
|
90
|
+
const click = async () => {
|
|
91
|
+
if (busy || disabled) return
|
|
92
|
+
setBusy(true)
|
|
93
|
+
setFailed(false)
|
|
94
|
+
try {
|
|
95
|
+
await run(sessionId)
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn('session-title-stamp: /retitle failed', error)
|
|
98
|
+
setFailed(true)
|
|
99
|
+
setTimeout(() => setFailed(false), 2500)
|
|
100
|
+
} finally {
|
|
101
|
+
setBusy(false)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return h(
|
|
106
|
+
'button',
|
|
107
|
+
{
|
|
108
|
+
type: 'button',
|
|
109
|
+
className: 'dsh-title-refresh',
|
|
110
|
+
disabled,
|
|
111
|
+
title: running ? t('running') : t('tip'),
|
|
112
|
+
'aria-label': t('menu'),
|
|
113
|
+
onClick: click,
|
|
114
|
+
style: {
|
|
115
|
+
display: 'inline-flex',
|
|
116
|
+
alignItems: 'center',
|
|
117
|
+
justifyContent: 'center',
|
|
118
|
+
width: 26,
|
|
119
|
+
height: 26,
|
|
120
|
+
padding: 0,
|
|
121
|
+
border: 0,
|
|
122
|
+
borderRadius: 7,
|
|
123
|
+
background: 'transparent',
|
|
124
|
+
color: failed
|
|
125
|
+
? 'var(--dsw-alias-state-error-primary, #f87171)'
|
|
126
|
+
: 'var(--dsw-alias-label-secondary, #94a3b8)',
|
|
127
|
+
cursor: disabled ? 'default' : 'pointer',
|
|
128
|
+
opacity: disabled && !busy ? 0.45 : 1,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
busy ? h('span', { style: { fontSize: 12 } }, '…') : h(RefreshIcon),
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function HoverStats({ sessionId, t }) {
|
|
136
|
+
const [data, setData] = useState(null)
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
let cancelled = false
|
|
139
|
+
if (typeof sessionId !== 'string' || sessionId === '') return undefined
|
|
140
|
+
const url = `${STATS_PATH}?sessionId=${encodeURIComponent(sessionId)}`
|
|
141
|
+
fetch(url, { headers: { accept: 'application/json' } })
|
|
142
|
+
.then((response) => (response.ok ? response.json() : undefined))
|
|
143
|
+
.then((payload) => {
|
|
144
|
+
if (!cancelled && payload?.ok === true) setData(payload)
|
|
145
|
+
})
|
|
146
|
+
.catch(() => undefined)
|
|
147
|
+
return () => {
|
|
148
|
+
cancelled = true
|
|
149
|
+
}
|
|
150
|
+
}, [sessionId])
|
|
151
|
+
|
|
152
|
+
if (data === null) return null
|
|
153
|
+
const duration = formatDuration(data.durationMs)
|
|
154
|
+
const cost = formatCost(data.costCny)
|
|
155
|
+
const tokens = formatTokens(data.tokens)
|
|
156
|
+
const parts = [
|
|
157
|
+
duration === undefined ? undefined : `${t('duration')} ${duration}`,
|
|
158
|
+
cost === undefined ? undefined : `${t('estimate')} ${cost}`,
|
|
159
|
+
tokens === undefined ? undefined : `${tokens} tokens`,
|
|
160
|
+
].filter(Boolean)
|
|
161
|
+
if (parts.length === 0) return null
|
|
162
|
+
return h(
|
|
163
|
+
'div',
|
|
164
|
+
{
|
|
165
|
+
style: {
|
|
166
|
+
marginTop: 6,
|
|
167
|
+
color: 'var(--dsw-alias-label-tertiary, #94a3b8)',
|
|
168
|
+
fontSize: 12,
|
|
169
|
+
lineHeight: 1.4,
|
|
170
|
+
whiteSpace: 'nowrap',
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
parts.join(' · '),
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function RegenerateMenuItem({ sessionId, useMenuOpenState, run, t }) {
|
|
178
|
+
const menuState =
|
|
179
|
+
typeof useMenuOpenState === 'function' ? useMenuOpenState() : null
|
|
180
|
+
return h(
|
|
181
|
+
'button',
|
|
182
|
+
{
|
|
183
|
+
type: 'button',
|
|
184
|
+
role: 'menuitem',
|
|
185
|
+
className: 'dsh-title-menu-item',
|
|
186
|
+
title: t('tip'),
|
|
187
|
+
onClick: async () => {
|
|
188
|
+
if (Array.isArray(menuState) && typeof menuState[1] === 'function') {
|
|
189
|
+
menuState[1](false)
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
await run(sessionId)
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.warn('session-title-stamp: /retitle failed', error)
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
h(
|
|
199
|
+
'span',
|
|
200
|
+
{ className: 'dsh-title-menu-icon', 'aria-hidden': 'true' },
|
|
201
|
+
h(RefreshIcon),
|
|
202
|
+
),
|
|
203
|
+
h('span', null, t('menu')),
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const STYLE_ID = 'dsh-session-title-stamp-style'
|
|
208
|
+
|
|
209
|
+
function installStyle(ctx) {
|
|
210
|
+
const style = document.createElement('style')
|
|
211
|
+
style.id = STYLE_ID
|
|
212
|
+
style.textContent = `
|
|
213
|
+
.dsh-title-menu-item{display:flex;align-items:center;gap:6px;width:100%;min-height:34px;padding:6px 8px;border:0;border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;line-height:20px;font-weight:400;text-align:left;cursor:pointer;box-sizing:border-box}
|
|
214
|
+
.dsh-title-menu-item:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06))}
|
|
215
|
+
.dsh-title-menu-icon{display:inline-flex;flex:none;width:14px;height:14px;align-items:center;justify-content:center;color:var(--dsw-alias-menu-icon,var(--dsw-alias-label-tertiary,#94a3b8))}
|
|
216
|
+
.dsh-title-menu-icon svg{width:14px;height:14px}
|
|
217
|
+
.dsh-title-menu-item > span:last-child{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
218
|
+
`
|
|
219
|
+
document.head.appendChild(style)
|
|
220
|
+
ctx.effect?.(() => () => style.remove())
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function apply(ctx) {
|
|
224
|
+
installStyle(ctx)
|
|
225
|
+
ctx.effect?.(() => ctx.locale.register(NS, DICT))
|
|
226
|
+
const t = ctx.locale.bind(NS)
|
|
227
|
+
const run = async (sessionId) => {
|
|
228
|
+
const response = await ctx.remote.commands.execute(sessionId, '/retitle', [])
|
|
229
|
+
if (!response?.ok) {
|
|
230
|
+
throw new Error(response?.error?.message ?? 'title command failed')
|
|
231
|
+
}
|
|
232
|
+
const outcome = response.value?.result
|
|
233
|
+
if (outcome?.kind !== 'success') {
|
|
234
|
+
throw new Error(outcome?.text ?? 'title command returned no result')
|
|
235
|
+
}
|
|
236
|
+
return outcome.text
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
ctx.slots.inject('conversation.session.header.actions', () =>
|
|
240
|
+
ctx.slots.register(
|
|
241
|
+
{
|
|
242
|
+
name: 'conversation.session.header.actions',
|
|
243
|
+
id: 'session-title-stamp-refresh',
|
|
244
|
+
order: 40,
|
|
245
|
+
inject: () => ({ run, t }),
|
|
246
|
+
},
|
|
247
|
+
RefreshAction,
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
ctx.slots.inject('sidebar.workspaces.session.menu.item', () =>
|
|
252
|
+
ctx.slots.register(
|
|
253
|
+
{
|
|
254
|
+
name: 'sidebar.workspaces.session.menu.item',
|
|
255
|
+
id: 'session-title-stamp-retitle',
|
|
256
|
+
order: 0,
|
|
257
|
+
inject: () => ({ run, t }),
|
|
258
|
+
},
|
|
259
|
+
RegenerateMenuItem,
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
ctx.slots.inject('sidebar.session.row.hover', () =>
|
|
264
|
+
ctx.slots.register(
|
|
265
|
+
{
|
|
266
|
+
name: 'sidebar.session.row.hover',
|
|
267
|
+
id: 'session-title-stamp-stats',
|
|
268
|
+
order: 0,
|
|
269
|
+
inject: () => ({ t }),
|
|
270
|
+
},
|
|
271
|
+
HoverStats,
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { inject, apply }
|
|
277
|
+
},
|
|
278
|
+
})
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# DSH profile bundle for dated, categorised session titles.
|
|
2
|
+
#
|
|
3
|
+
# Selecting this bundle replaces the shipped first-prompt title provider. The
|
|
4
|
+
# inserted row is the provider package itself; disabling `session-title-llm`
|
|
5
|
+
# keeps only one provider registered, as the title service requires.
|
|
6
|
+
- id: session-title-llm
|
|
7
|
+
disabled: true
|
|
8
|
+
|
|
9
|
+
- insert:
|
|
10
|
+
- id: session-title-stamp
|
|
11
|
+
name: dsh-session-title-stamp
|
|
12
|
+
config:
|
|
13
|
+
# Refresh after a completed turn, at most once every 2 minutes, and
|
|
14
|
+
# stop automatic refreshes after 100 user messages.
|
|
15
|
+
refreshOnTurnEnd: true
|
|
16
|
+
refreshMinIntervalMs: 120000
|
|
17
|
+
refreshMaxTurns: 100
|
|
18
|
+
categoryPreset: zh
|
|
19
|
+
# MMDD matches the reference list (1027). The template can also use
|
|
20
|
+
# {YYYYMMDD}, {YYMM}, {HHmm}, {type}, {topic}, etc.
|
|
21
|
+
dateFormat: MMDD
|
|
22
|
+
# Omit timeZone to follow the host's local zone.
|
|
23
|
+
maxInputBytes: 4096
|
|
24
|
+
maxOutputTokens: 4096
|
|
25
|
+
timeoutMs: 90000
|
|
26
|
+
# Optional: repair fallback titles of live sessions on startup.
|
|
27
|
+
backfillOnStart: false
|
|
28
|
+
backfillMaxSessions: 30
|
|
29
|
+
backfillIntervalMs: 2000
|
package/icon.svg
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-label="Session title stamp">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#0b1220"/>
|
|
5
|
+
<stop offset="1" stop-color="#111c33"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
|
|
8
|
+
<stop offset="0" stop-color="#38bdf8"/>
|
|
9
|
+
<stop offset="1" stop-color="#22d3ee"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<clipPath id="calendar">
|
|
12
|
+
<rect x="120" y="210" width="330" height="510" rx="48"/>
|
|
13
|
+
</clipPath>
|
|
14
|
+
</defs>
|
|
15
|
+
|
|
16
|
+
<rect width="1024" height="1024" rx="220" fill="url(#bg)"/>
|
|
17
|
+
<rect x="14" y="14" width="996" height="996" rx="206" fill="none" stroke="#1e293b" stroke-width="4"/>
|
|
18
|
+
|
|
19
|
+
<g clip-path="url(#calendar)">
|
|
20
|
+
<rect x="120" y="210" width="330" height="510" rx="48" fill="#0f1b33"/>
|
|
21
|
+
<rect x="120" y="210" width="330" height="104" fill="url(#accent)"/>
|
|
22
|
+
<rect x="120" y="210" width="330" height="510" rx="48" fill="none" stroke="#38bdf8" stroke-width="12"/>
|
|
23
|
+
</g>
|
|
24
|
+
<rect x="180" y="178" width="34" height="84" rx="17" fill="#e2e8f0"/>
|
|
25
|
+
<rect x="356" y="178" width="34" height="84" rx="17" fill="#e2e8f0"/>
|
|
26
|
+
|
|
27
|
+
<g fill="#1e3a5f">
|
|
28
|
+
<rect x="176" y="378" width="66" height="58" rx="14"/>
|
|
29
|
+
<rect x="252" y="378" width="66" height="58" rx="14"/>
|
|
30
|
+
<rect x="328" y="378" width="66" height="58" rx="14"/>
|
|
31
|
+
<rect x="176" y="460" width="66" height="58" rx="14"/>
|
|
32
|
+
<rect x="252" y="460" width="66" height="58" rx="14"/>
|
|
33
|
+
<rect x="328" y="460" width="66" height="58" rx="14"/>
|
|
34
|
+
<rect x="176" y="542" width="66" height="58" rx="14"/>
|
|
35
|
+
<rect x="252" y="542" width="66" height="58" rx="14"/>
|
|
36
|
+
</g>
|
|
37
|
+
<rect x="176" y="624" width="218" height="44" rx="22" fill="#38bdf8"/>
|
|
38
|
+
|
|
39
|
+
<rect x="500" y="300" width="10" height="360" rx="5" fill="#1e293b"/>
|
|
40
|
+
|
|
41
|
+
<rect x="556" y="300" width="360" height="68" rx="34" fill="#f8fafc"/>
|
|
42
|
+
<rect x="556" y="420" width="292" height="68" rx="34" fill="url(#accent)"/>
|
|
43
|
+
<rect x="556" y="540" width="330" height="68" rx="34" fill="#64748b"/>
|
|
44
|
+
<rect x="556" y="660" width="228" height="68" rx="34" fill="#334155"/>
|
|
45
|
+
</svg>
|