dsh-titlecraft 0.1.0-rc.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/lib/client.js ADDED
@@ -0,0 +1,1247 @@
1
+ ;
2
+ window.__ModuleLoader__.load({
3
+ id: 'dsh-titlecraft',
4
+ factory: (require) => {
5
+ const React = require('react');
6
+ const { jsx, jsxs } = require('react/jsx-runtime');
7
+ const { Button, Input, Menu, IconChevronDownOutline14, } = require('@deepseek-ai/dsh-client-ui-primitives');
8
+ const inject = ['slots', 'settingsScope', 'connection'];
9
+ const NS = 'titlecraft';
10
+ const RENAME_SLOT = 'sidebar.workspaces.session.rename.extra';
11
+ const TYPES = ['功能', '设计', '修复', '优化', '发布', '探索', '文档', '研究'];
12
+ const ICON_DATA = {"legacyGlyphs":["◇","◈","▲","◉"],"groups":[{"id":"core-signals","zh":"通用信号","en":"Core signals","icons":[["spark","✦","通用亮点","general highlight"],["star","★","重点","priority"],["diamond","◆","模块","module"],["target","◎","目标","goal"],["plus","+","新增","add"],["arrow-up","↗","优化","optimization"],["check","✓","完成","completion"],["alert","△","警告","warning"],["info","ⓘ","信息","information"],["flag","⚑","发布","release"],["bolt","⚡","性能","performance"],["sparkles","✨","闪耀亮点","sparkle"],["question","❔","疑问","question"],["cross","✕","否定","negative"],["minus","−","减少","decrease"],["prohibited","⛔","禁止","prohibited"],["warning","⚠","高风险","high-risk warning"]]},{"id":"dev-operations","zh":"开发与运维","en":"Development and operations","icons":[["gear","⚙","配置","configuration"],["tools","🛠","开发","development"],["wrench","🔧","修复","fix"],["bug","🐛","缺陷","bug"],["patch","🩹","补丁","patch"],["broom","🧹","清理","cleanup"],["recycle","♻","重构","refactor"],["puzzle","🧩","插件","plugin"],["thread","🧵","流程","flow"],["refresh","↻","刷新","refresh"],["terminal","❯_","终端","terminal"],["play","▶","运行","run"],["pause","Ⅱ","暂停","pause"],["stop","■","停止","stop"],["record","●","记录","record"],["hammer","🔨","构建","build"],["screwdriver","🪛","配置","configure"],["nut-bolt","🔩","装配","assembly"],["toolbox","🧰","工具集","toolbox"],["magnet","🧲","吸附","magnet"],["construction","🚧","施工","under construction"]]},{"id":"security-compliance","zh":"安全与合规","en":"Security and compliance","icons":[["lock","🔒","安全","security"],["key","🔑","认证","authentication"],["shield","🛡","保护","protection"],["legal","⚖","法律","legal"],["unlock","🔓","解锁","unlock"],["keyhole","🗝","密钥","keyhole"],["biohazard","☣","危险","biohazard"],["radioactive","☢","辐射","radioactive"],["cross-mark","❌","错误","error"],["check-mark-button","✅","确认","confirmed"]]},{"id":"analysis-automation","zh":"分析与自动化","en":"Analysis and automation","icons":[["search","🔍","搜索","search"],["experiment","🧪","测试","testing"],["chart","📊","数据","data"],["bulb","💡","想法","idea"],["robot","🤖","智能体","agent"],["brain","🧠","推理","reasoning"],["science","⚗","科学","science"],["math","∑","数学","mathematics"],["microscope","🔬","深度检查","deep inspection"],["telescope","🔭","远期观察","foresight"],["dna","🧬","谱系","lineage"],["test-tube","🧫","样本","sample"],["atom","⚛","原理","principle"],["abacus","🧮","计算","calculation"]]},{"id":"planning-navigation","zh":"规划与导航","en":"Planning and navigation","icons":[["compass","🧭","规划","planning"],["map","🗺","架构","architecture"],["clock","⏱","时间","time"],["location","📍","位置","location"],["signpost","🪧","指引","guidance"],["hourglass","⌛","等待","waiting"],["hourglass-flowing","⏳","处理中","processing"],["timer","⏲","倒计时","countdown"],["alarm","⏰","提醒","reminder"],["anchor","⚓","稳定","stability"],["route","🛣","路线","route"]]},{"id":"knowledge-docs","zh":"知识与文档","en":"Knowledge and documentation","icons":[["document","▤","文档","documentation"],["note","📝","写作","writing"],["book","📚","知识","knowledge"],["graduate","🎓","教育","education"],["newspaper","📰","新闻","news"],["scroll","📜","长文","long-form"],["open-book","📖","阅读","reading"],["receipt","🧾","凭证","receipt"],["bookmark-tabs","📑","索引","index"],["card-index","🗃","档案","archive"]]},{"id":"communication-media","zh":"沟通与媒体","en":"Communication and media","icons":[["chat","💬","对话","conversation"],["bell","🔔","通知","notification"],["palette","🎨","设计","design"],["image","🖼","图片","image"],["film","🎬","视频","video"],["eye","👁","查看","view"],["ear","👂","音频","audio"],["mic","🎙","语音","voice"],["speaker","🔊","声音","sound"],["camera","📷","摄影","photography"],["music","🎵","音乐","music"],["game","🎮","游戏","game"],["dice","🎲","随机","randomness"],["thought","💭","思考","thought"],["speech","🗨","引用","quote"],["headphones","🎧","耳机","headphones"],["video-camera","📹","录像","video capture"],["camera-flash","📸","闪光","flash capture"],["radio","📻","广播","broadcast"]]},{"id":"systems-infrastructure","zh":"系统与基础设施","en":"Systems and infrastructure","icons":[["rocket","🚀","发布","release"],["package","📦","包","package"],["bricks","🧱","基础设施","infrastructure"],["link","🔗","集成","integration"],["globe","🌐","网络","network"],["desktop","🖥","桌面端","desktop"],["mobile","📱","移动端","mobile"],["database","🗄","数据库","database"],["disk","💾","持久化","persistence"],["cloud","☁","云端","cloud"],["plug","🔌","电源","power"],["battery","🔋","电量","power"],["satellite","🛰","卫星","satellite"],["antenna","📡","信号","signal"],["keyboard","⌨","输入","input"],["printer","🖨","打印","print"],["mouse","🖱","指针","pointer"],["api","⇄","接口","API"],["laptop","💻","笔记本","laptop"],["hard-drive","💽","硬盘","hard drive"],["cd","💿","光盘","optical disc"],["wifi","📶","无线网络","Wi-Fi"],["power","⏻","电源","power"],["folder-open","📂","打开目录","open folder"]]},{"id":"files-workflow","zh":"文件与流程","en":"Files and workflow","icons":[["calendar","📅","日历","calendar"],["event","🗓","事件","event"],["pin","📌","置顶","pin"],["inbox","📥","收件","inbox"],["outbox","📤","发件","outbox"],["mail","✉","邮件","mail"],["folder","📁","目录","directory"],["file","📄","文件","file"],["clipboard","📋","清单","checklist"],["bookmark","🔖","书签","bookmark"],["label","🏷","标签","label"],["paperclip","📎","附件","attachment"],["ruler","📏","尺寸","dimension"],["pencil","✏","编辑","edit"],["pen","🖊","撰写","authoring"],["upload","⬆","上传","upload"],["download","⬇","下载","download"],["layers","◫","图层","layers"],["grid","▦","网格","grid"],["list","☷","列表","list"],["filter","▽","筛选","filter"],["sort","⇅","排序","sort"],["expand","⤢","展开","expand"],["back","↩","返回","back"],["forward","↪","前进","forward"],["undo","↶","撤销","undo"],["redo","↷","重做","redo"],["left-arrow","←","向左","left"],["right-arrow","→","向右","right"]]},{"id":"organization-commerce","zh":"组织与商业","en":"Organization and commerce","icons":[["home","🏠","首页","home"],["office","🏢","组织","organization"],["factory","🏭","生产","production"],["cart","🛒","商店","store"],["money","💰","成本","cost"],["card","💳","支付","payment"],["gift","🎁","奖励","reward"],["trophy","🏆","成就","achievement"],["medal","🏅","质量","quality"],["crown","👑","高级","premium"],["person","🧑","个人","individual"],["family","👪","家庭","family"],["ticket","🎫","票据","ticket"],["shopping-bag","🛍","商品","product"],["house-with-garden","🏡","住所","residence"],["office-building","🏬","部门","department"]]},{"id":"people-wellbeing","zh":"人物与关怀","en":"People and wellbeing","icons":[["user","👤","用户","user"],["users","👥","团队","team"],["handshake","🤝","合作","collaboration"],["heart","♥","喜爱","favorite"],["pulse","💓","状态","status"],["smile","☺","体验","experience"],["health","⚕","医疗","medical"],["accessibility","♿","无障碍","accessibility"],["pill","💊","药物","medication"],["syringe","💉","注入","injection"],["stethoscope","🩺","诊断","diagnosis"],["hospital","🏥","医疗","medical care"],["broken-heart","💔","故障","breakage"],["party","🎉","庆祝","celebration"]]},{"id":"nature-transport","zh":"自然与交通","en":"Nature and transport","icons":[["sun","☀","亮色","light theme"],["moon","☾","暗色","dark theme"],["rain","🌧","天气","weather"],["snow","❄","冻结","freeze"],["leaf","🍃","轻量","lightweight"],["tree","🌳","层级","hierarchy"],["flower","✿","美化","beauty"],["seedling","🌱","初始化","initialization"],["water","💧","流动","flow"],["mountain","⛰","挑战","challenge"],["plane","✈","旅行","travel"],["car","🚗","车辆","vehicle"],["train","🚆","队列","queue"],["ship","🚢","容器","container"],["bike","🚲","轻量出行","light mobility"],["earth","🌍","地球","earth"],["storm","⛈","风暴","storm"],["ocean","🌊","广域流动","ocean"],["volcano","🌋","爆发","eruption"],["bus","🚌","公共交通","public transit"],["truck","🚚","运输","transport"]]}]};
13
+ const ICON_GROUPS = ICON_DATA.groups;
14
+ const LEGACY_ICON_GLYPHS = ICON_DATA.legacyGlyphs;
15
+ const ICONS = ICON_GROUPS.flatMap((group) => group.icons);
16
+ const FIELDS = [
17
+ 'schemaVersion',
18
+ 'enabled',
19
+ 'dateSource',
20
+ 'types',
21
+ 'enabledTypes',
22
+ 'formatTemplate',
23
+ 'timezone',
24
+ 'datePattern',
25
+ 'instantEnabled',
26
+ 'firstMessageLlmEnabled',
27
+ 'refineEnabled',
28
+ 'idleDelayMs',
29
+ 'maxInputBytes',
30
+ 'maxOutputTokens',
31
+ 'timeoutMs',
32
+ 'topicMaxChars',
33
+ 'minConfidence',
34
+ 'preferCheap',
35
+ 'fallbackToSessionModel',
36
+ 'modelProvider',
37
+ 'modelName',
38
+ 'respectManualTitles',
39
+ 'topicGuidance',
40
+ 'templatePreset',
41
+ 'iconMode',
42
+ 'iconId',
43
+ 'fontFamily',
44
+ 'fontSize',
45
+ 'sessionTitleLeftOffset',
46
+ ];
47
+ const DEFAULTS = {
48
+ schemaVersion: 2,
49
+ enabled: true,
50
+ dateSource: 'createdAt',
51
+ types: TYPES,
52
+ enabledTypes: TYPES,
53
+ templatePreset: 'standard',
54
+ formatTemplate: '{icon} {date}|{type}|{topic}',
55
+ iconMode: 'none',
56
+ iconId: 'spark',
57
+ fontFamily: 'dsh',
58
+ fontSize: 14,
59
+ sessionTitleLeftOffset: 0,
60
+ timezone: 'Asia/Shanghai',
61
+ datePattern: 'MMDD',
62
+ instantEnabled: true,
63
+ firstMessageLlmEnabled: false,
64
+ refineEnabled: true,
65
+ idleDelayMs: 3000,
66
+ maxInputBytes: 6000,
67
+ maxOutputTokens: 96,
68
+ timeoutMs: 60000,
69
+ topicMaxChars: 16,
70
+ minConfidence: 0.72,
71
+ preferCheap: true,
72
+ fallbackToSessionModel: false,
73
+ modelProvider: '',
74
+ modelName: '',
75
+ respectManualTitles: true,
76
+ topicGuidance: '主题应简洁、具体,基于对话实际内容;不要重复项目名称。无法判断时返回空主题。',
77
+ };
78
+ const STYLE_ID = 'dsh-titlecraft-style';
79
+ const CSS = `
80
+ .dtc-plugin{list-style:none;min-width:0;border:.5px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);transition:border-color .16s,background .16s}
81
+ .dtc-plugin:hover,.dtc-plugin[data-open=true]{border-color:var(--dsw-alias-label-dimmed)}.dtc-plugin[data-open=true]{background:var(--dsw-alias-bg-layer-2)}
82
+ .dtc-plugin__header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:transparent;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px;height:auto}
83
+ .dtc-plugin__header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
84
+ .dtc-plugin__heading{display:flex;flex:1;flex-direction:column;gap:4px;min-width:0}.dtc-plugin__heading strong{font-size:15px;font-weight:600;line-height:1.4}.dtc-plugin__heading small{font-size:13px;color:var(--dsw-alias-label-tertiary);line-height:1.5}
85
+ .dtc-plugin__chevron{flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s}.dtc-plugin[data-open=true] .dtc-plugin__chevron{transform:rotate(180deg)}
86
+ .dtc-plugin__pending{white-space:nowrap;border-radius:999px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);padding:1px 8px;font-size:11px;line-height:17px}
87
+ .dtc{container-type:inline-size;box-sizing:border-box;min-width:0;margin:0 16px;padding:12px 0 8px;border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}.dtc[hidden]{display:none}
88
+ .dtc__fields{display:flex;flex-direction:column;gap:16px;border:0;padding:0;margin:0;min-width:0}.dtc h3{margin:0 0 12px;font-size:13px;font-weight:600}
89
+ .dtc__lede,.dtc__hint,.dtc__status{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;margin:0;font-weight:400}.dtc__status[data-error=true]{color:var(--dsw-alias-label-error)}.dtc__compat{margin:0;padding:10px 12px;border:.5px solid var(--dsw-alias-border-l3);border-radius:10px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}
90
+ .dtc__card{min-width:0}.dtc__card+.dtc__card{border-top:.5px solid var(--dsw-alias-border-l2);padding-top:12px}
91
+ .dtc__grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.dtc__wide{grid-column:1/-1}
92
+ .dtc label,.dtc__field{display:flex;flex-direction:column;min-width:0;gap:6px;font-size:13px;line-height:1.5;font-weight:500}.dtc__input{box-sizing:border-box;width:100%;min-width:0;height:36px;min-height:36px}.dtc input.dtc__input,.dtc .dtc__input input{box-sizing:border-box;width:100%;min-width:0;height:100%;min-height:0}
93
+ .dtc .dtc__check{flex-direction:row;align-items:flex-start;gap:8px;cursor:pointer;font-weight:400;padding:6px 0}.dtc__check span{min-width:0;overflow-wrap:break-word}.dtc .dtc__check input[type=checkbox]{box-sizing:border-box;width:16px;height:16px;min-width:16px;flex:0 0 16px;margin:2px 0 0;padding:0;accent-color:var(--dsw-alias-brand-primary)}
94
+ .dtc textarea{box-sizing:border-box;width:100%;min-width:0;min-height:88px;resize:vertical;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;background:var(--dsw-alias-bg-layer-3);color:inherit;padding:8px 12px;font:inherit}.dtc textarea:focus-visible{outline:2px solid var(--dsw-alias-brand-primary)}
95
+ .dtc__checks{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 12px;margin-bottom:12px}.dtc__select{box-sizing:border-box;width:100%;min-width:0;height:36px;min-height:36px;justify-content:space-between;border-radius:10px}.dtc__select>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dtc__select svg{flex:none}.dtc__select[aria-expanded=true]{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2)}
96
+ .dtc-icon-option{display:inline-grid;grid-template-columns:24px minmax(0,1fr);align-items:center;gap:8px;min-width:0}.dtc-icon-glyph,.dtc-session-title-icon{box-sizing:border-box;width:24px;height:20px;display:inline-flex;align-items:center;justify-content:center;flex:0 0 24px;font-family:"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif;font-weight:400;line-height:1;text-align:center}.dtc-icon-glyph{font-size:16px}.dtc-session-title-icon{font-size:inherit}.dtc-icon-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dtc-session-left-offset{translate:var(--dtc-session-title-left-offset,0px) 0}.dtc-session-title-display{font-size:var(--dtc-session-title-font-size,14px)!important}.dtc-session-title-with-icon{display:flex!important;align-items:center;min-width:0}.dtc-session-title-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dtc-rename-workbench{display:flex;flex-direction:column;gap:12px;margin-top:12px;padding-top:12px;border-top:.5px solid var(--dsw-alias-border-l2);min-width:0}.dtc-rename-workbench label{display:flex;flex-direction:column;gap:6px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}.dtc-rename-workbench textarea{box-sizing:border-box;width:100%;min-height:108px;max-height:240px;resize:vertical;border:.5px solid var(--dsw-alias-border-l4);border-radius:10px;background:var(--dsw-alias-bg-layer-3);color:inherit;padding:9px 12px;font:inherit;line-height:1.5}.dtc-rename-workbench textarea:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-1px}.dtc-summary-count{align-self:flex-end;color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:400}.dtc-rename-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.dtc-rename-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.dtc-rename-actions button{height:36px;min-height:36px}.dtc-rename-workbench .dtc__status{margin:0}@media(max-width:520px){.dtc-rename-grid{grid-template-columns:1fr}}body:has(.dtc-icon-menu .dtc__select[aria-expanded=true])>[role=menu]>[role=presentation]{box-sizing:border-box;max-height:min(360px,calc(100vh - 96px));overflow-y:auto}
97
+ .dtc__preview{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;margin-top:6px;padding:10px;border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3);overflow-wrap:anywhere}
98
+ .dtc__actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;align-items:center;border-top:.5px solid var(--dsw-alias-border-l2);padding:12px 0 4px;margin-top:12px}.dtc__actions .dtc__status{flex:1;min-width:100px}
99
+ .dtc__type-editor{border:0;padding:0;margin:0;min-width:0}.dtc__type-editor legend{font-size:13px;font-weight:500;margin-bottom:6px}.dtc__type-list{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0}.dtc__type-option{display:flex;align-items:center;border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;padding:0 4px 0 10px}.dtc .dtc__type-remove{padding:4px 8px;margin-left:4px}
100
+ .dtc__type-tools{display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px;margin-top:12px}.dtc__type-tools label{flex:1 1 160px}.dtc__type-tools button{flex-shrink:0;white-space:nowrap}.dtc__type-tools+.dtc__hint{margin-top:10px}.dtc__type-option:has(input:checked){border-color:var(--dsw-alias-brand-primary)}
101
+ @container(max-width:520px){.dtc__grid,.dtc__checks{grid-template-columns:1fr}.dtc__wide{grid-column:auto}}
102
+ @media(prefers-reduced-motion:reduce){.dtc-plugin,.dtc-plugin__chevron{transition:none}}
103
+ `;
104
+ function apply(ctx) {
105
+ let style = document.getElementById(STYLE_ID);
106
+ if (!style) {
107
+ style = document.createElement('style');
108
+ style.id = STYLE_ID;
109
+ style.textContent = CSS;
110
+ document.head.appendChild(style);
111
+ }
112
+ ctx.effect(() => () => style?.remove(), 'titlecraft: remove styles');
113
+ const scope = ctx.settingsScope.bind({ namespace: NS });
114
+ const workspaceCompatibility = {
115
+ getSnapshot: () => {
116
+ if (typeof ctx.slots.entriesOfSlot !== 'function')
117
+ return 'unavailable';
118
+ const owner = ctx.slots.entriesOfSlot('sidebar.workspaces')[0];
119
+ if (!owner)
120
+ return 'unavailable';
121
+ const spec = owner.children?.[RENAME_SLOT];
122
+ return spec?.kind === 'list' && spec.scope === 'root' ? 'compatible' : 'incompatible';
123
+ },
124
+ subscribe: (listener) => typeof ctx.slots.subscribe === 'function'
125
+ ? ctx.slots.subscribe('sidebar.workspaces', listener)
126
+ : () => { },
127
+ };
128
+ ctx.effect(() => {
129
+ if (typeof scope.subscribe !== 'function' ||
130
+ typeof scope.getSnapshot !== 'function' ||
131
+ !document.documentElement?.style)
132
+ return () => { };
133
+ const syncAppearance = () => {
134
+ const snapshot = scope.getSnapshot();
135
+ if (snapshot.status !== 'ready')
136
+ return;
137
+ const raw = Number(snapshot.value?.sessionTitleLeftOffset);
138
+ const offset = Number.isFinite(raw)
139
+ ? Math.min(8, Math.max(0, Math.round(raw)))
140
+ : DEFAULTS.sessionTitleLeftOffset;
141
+ const fontRaw = Number(snapshot.value?.fontSize);
142
+ const fontSize = Number.isFinite(fontRaw)
143
+ ? Math.min(20, Math.max(12, Math.round(fontRaw)))
144
+ : DEFAULTS.fontSize;
145
+ document.documentElement.style.setProperty('--dtc-session-title-left-offset', offset === 0 ? '0px' : `-${offset}px`);
146
+ document.documentElement.style.setProperty('--dtc-session-title-font-size', `${fontSize}px`);
147
+ };
148
+ const unsubscribe = scope.subscribe(syncAppearance);
149
+ syncAppearance();
150
+ return () => {
151
+ unsubscribe();
152
+ document.documentElement.style.removeProperty('--dtc-session-title-left-offset');
153
+ document.documentElement.style.removeProperty('--dtc-session-title-font-size');
154
+ };
155
+ }, 'titlecraft: sync session title appearance');
156
+ ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
157
+ name: 'settings.plugin.item',
158
+ key: NS,
159
+ order: 95,
160
+ }, () => jsx(SettingsSection, { scope, workspaceCompatibility, connection: ctx.connection })));
161
+ ctx.slots.inject(RENAME_SLOT, () => ctx.slots.register({
162
+ name: RENAME_SLOT,
163
+ id: NS,
164
+ order: 95,
165
+ }, (owner) => jsx(SessionRenameWorkbench, { ...owner, scope, connection: ctx.connection })));
166
+ }
167
+ const iconOptionLabel = (glyph, label) => jsxs('span', {
168
+ className: 'dtc-icon-option',
169
+ children: [
170
+ jsx('span', { className: 'dtc-icon-glyph', 'aria-hidden': true, children: glyph }),
171
+ jsx('span', { className: 'dtc-icon-label', children: label }),
172
+ ],
173
+ });
174
+ const groupedIconMenuItems = (zh) => ICON_GROUPS.flatMap((group, index) => [
175
+ ...(index ? [{ type: 'separator', id: `icon-separator-${group.id}` }] : []),
176
+ { type: 'label', id: `icon-group-${group.id}`, text: zh ? group.zh : group.en },
177
+ ...group.icons.map(([id, glyph, cn, en]) => ({
178
+ id,
179
+ label: iconOptionLabel(glyph, zh ? cn : en),
180
+ })),
181
+ ]);
182
+ const iconIdFromLeadingToken = (title) => {
183
+ const token = title.match(/^(\S+)(?:\s|$)/u)?.[1];
184
+ return ICONS.find(([, glyph]) => glyph === token)?.[0];
185
+ };
186
+ function replaceTitleIcon(title, glyph, formatTemplate) {
187
+ const leading = title.match(/^(\S+)(\s+)(.*)$/u);
188
+ const knownGlyphs = new Set([
189
+ ...ICONS.map(([, candidate]) => candidate),
190
+ ...LEGACY_ICON_GLYPHS,
191
+ ]);
192
+ if (leading && knownGlyphs.has(leading[1]))
193
+ return `${glyph}${leading[2]}${leading[3]}`;
194
+ const iconAt = formatTemplate.indexOf('{icon}');
195
+ const topicAt = formatTemplate.indexOf('{topic}');
196
+ return iconAt >= 0 && (topicAt < 0 || iconAt < topicAt)
197
+ ? `${glyph} ${title}`
198
+ : `${title} ${glyph}`;
199
+ }
200
+ function SessionRenameWorkbench({ scope, connection, sessionId, title, onTitleChange, disabled, }) {
201
+ const snapshot = React.useSyncExternalStore((listener) => scope.subscribe(listener), () => scope.getSnapshot(), () => scope.getSnapshot());
202
+ const config = { ...DEFAULTS, ...(snapshot.value || {}) };
203
+ const language = document.documentElement?.lang || navigator.language || 'en';
204
+ const zh = /^zh(?:-|$)/i.test(language);
205
+ const text = (cn, en) => (zh ? cn : en);
206
+ const types = Array.isArray(config.enabledTypes) && config.enabledTypes.length
207
+ ? config.enabledTypes
208
+ : TYPES;
209
+ const mode = config.iconMode === 'type' ? 'smart' : config.iconMode;
210
+ const initialIcon = iconIdFromLeadingToken(title) || config.iconId;
211
+ const [summary, setSummary] = React.useState('');
212
+ const [type, setType] = React.useState(types[0]);
213
+ const [iconId, setIconId] = React.useState(initialIcon);
214
+ const [busy, setBusy] = React.useState('');
215
+ const [error, setError] = React.useState('');
216
+ const [message, setMessage] = React.useState('');
217
+ const active = React.useRef(null);
218
+ React.useEffect(() => () => active.current?.abort(), []);
219
+ React.useEffect(() => {
220
+ if (!types.includes(type))
221
+ setType(types[0]);
222
+ }, [snapshot.revision]);
223
+ if (snapshot.status !== 'ready')
224
+ return null;
225
+ const failText = (code) => ({
226
+ 'no-messages': text('当前会话没有可归纳的真人消息。', 'No human messages are available to summarize.'),
227
+ 'no-route': text('没有可用的 AI 模型。', 'No AI model is available.'),
228
+ 'route-unavailable': text('AI 模型暂时不可用。', 'The AI model is temporarily unavailable.'),
229
+ timeout: text('AI 归纳超时,请稍后重试。', 'AI summarization timed out. Try again later.'),
230
+ 'uncertain-summary': text('AI 对会话摘要把握不足,未覆盖你的草稿。', 'AI was not confident enough to replace your summary draft.'),
231
+ 'generic-topic': text('AI 结果过于笼统,未覆盖你的草稿。', 'The AI result was too generic, so your draft was left unchanged.'),
232
+ 'invalid-json': text('AI 返回格式无效,请重试。', 'AI returned an invalid format. Try again.'),
233
+ 'invalid-schema': text('AI 返回内容不完整,请重试。', 'AI returned incomplete data. Try again.'),
234
+ 'invalid-type': text('AI 返回了未启用的标题类型。', 'AI returned a disabled title type.'),
235
+ 'invalid-icon': text('AI 返回了白名单之外的图标。', 'AI returned an icon outside the allowlist.'),
236
+ 'invalid-summary-length': text('会话摘要应为 2–240 个字符;AI 摘要通常为几十字。', 'The session summary must contain 2–240 characters; AI normally writes several dozen.'),
237
+ 'invalid-topic-format': text('AI 生成的主题含有不允许的格式字符。', 'The AI topic contained forbidden formatting characters.'),
238
+ aborted: text('操作已取消。', 'The operation was cancelled.'),
239
+ 'title-too-long': text('压缩后的标题仍超过 80 字节,请精简摘要后重试。', 'The compressed title still exceeds 80 bytes. Shorten the summary and try again.'),
240
+ 'type-disabled': text('所选标题类型已停用。', 'The selected title type is disabled.'),
241
+ 'connection-unavailable': text('标题工艺 Host 接口不可用;更新插件后请重启 DSH Web。', 'The TitleCraft Host endpoint is unavailable. Restart DSH Web after updating the plugin.'),
242
+ })[code] ||
243
+ text('未能完成标题处理,请稍后重试。', 'Title processing could not be completed.');
244
+ const call = async (endpoint, payload) => {
245
+ active.current?.abort();
246
+ const controller = new AbortController();
247
+ active.current = controller;
248
+ let result;
249
+ try {
250
+ result = await connection.rpc.call('/titlecraft', endpoint, { sessionId, ...payload }, controller.signal);
251
+ }
252
+ catch (cause) {
253
+ throw Object.assign(cause instanceof Error ? cause : new Error('connection-unavailable'), { code: controller.signal.aborted ? 'aborted' : 'connection-unavailable' });
254
+ }
255
+ if (!result?.ok)
256
+ throw Object.assign(new Error(result?.error?.message || 'request-failed'), {
257
+ code: result?.error?.code || 'request-failed',
258
+ });
259
+ return result.value;
260
+ };
261
+ const summarize = async () => {
262
+ if (disabled || busy)
263
+ return;
264
+ setBusy('summarize');
265
+ setError('');
266
+ setMessage('');
267
+ try {
268
+ const value = await call('summarize', {});
269
+ setSummary(value.summary);
270
+ setType(value.type);
271
+ if (value.iconId && ICONS.some(([id]) => id === value.iconId))
272
+ setIconId(value.iconId);
273
+ setMessage(text('AI 已生成一段可编辑摘要;确认内容后再压缩为标题。', 'AI generated an editable summary. Review it before compressing it into a title.'));
274
+ }
275
+ catch (cause) {
276
+ if (cause?.code !== 'aborted')
277
+ setError(failText(cause?.code));
278
+ }
279
+ finally {
280
+ setBusy('');
281
+ active.current = null;
282
+ }
283
+ };
284
+ const applyTemplate = async () => {
285
+ const clean = summary.trim();
286
+ if (disabled || busy || !clean) {
287
+ if (!clean)
288
+ setError(text('请先输入或生成一段会话摘要。', 'Enter or generate a session summary first.'));
289
+ return;
290
+ }
291
+ setBusy('generate');
292
+ setError('');
293
+ setMessage('');
294
+ try {
295
+ const value = await call('generate', { summary: clean, type, iconId });
296
+ onTitleChange(value.title);
297
+ setMessage(text(`已将摘要压缩为主题“${value.topic}”并更新上方标题草稿;点击原生“重命名”才会保存。`, `Compressed the summary to “${value.topic}” and updated the native title draft. Use Rename to save it.`));
298
+ }
299
+ catch (cause) {
300
+ if (cause?.code !== 'aborted')
301
+ setError(failText(cause?.code));
302
+ }
303
+ finally {
304
+ setBusy('');
305
+ active.current = null;
306
+ }
307
+ };
308
+ const chooseIcon = (id) => {
309
+ setIconId(id);
310
+ const glyph = ICONS.find(([candidate]) => candidate === id)?.[1];
311
+ if (!glyph || mode === 'none' || !config.formatTemplate.includes('{icon}'))
312
+ return;
313
+ const next = replaceTitleIcon(title, glyph, config.formatTemplate);
314
+ if (new TextEncoder().encode(next).length > 80) {
315
+ setError(text('更换后标题超过 80 字节,未修改。', 'The resulting title exceeds 80 bytes and was not changed.'));
316
+ return;
317
+ }
318
+ setError('');
319
+ onTitleChange(next);
320
+ };
321
+ const showIcon = mode !== 'none' && config.formatTemplate.includes('{icon}');
322
+ return jsxs('div', {
323
+ className: 'dtc-rename-workbench',
324
+ children: [
325
+ jsx('label', {
326
+ children: [
327
+ text('会话摘要草稿', 'Session summary draft'),
328
+ jsx('textarea', {
329
+ value: summary,
330
+ maxLength: 240,
331
+ disabled: disabled || Boolean(busy),
332
+ placeholder: text('输入一段几十字的会话摘要,或让 AI 根据当前会话生成。', 'Enter a several-dozen-character session summary, or let AI generate one from this session.'),
333
+ onChange: (event) => setSummary(event.currentTarget.value),
334
+ }),
335
+ jsx('span', {
336
+ className: 'dtc-summary-count',
337
+ children: text(`AI 通常生成 40–100 字 · ${[...summary].length}/240`, `AI normally writes 40–100 characters · ${[...summary].length}/240`),
338
+ }),
339
+ ],
340
+ }),
341
+ jsxs('div', {
342
+ className: 'dtc-rename-grid',
343
+ children: [
344
+ jsx(SelectField, {
345
+ active: !disabled && !busy,
346
+ label: text('标题类型', 'Title type'),
347
+ value: type,
348
+ options: types.map((value) => [value, value]),
349
+ onChange: setType,
350
+ }),
351
+ showIcon &&
352
+ jsx(SelectField, {
353
+ active: !disabled && !busy,
354
+ iconMenu: true,
355
+ label: text('会话图标', 'Session icon'),
356
+ value: iconId,
357
+ options: ICONS.map(([id, glyph, cn, en]) => [
358
+ id,
359
+ iconOptionLabel(glyph, zh ? cn : en),
360
+ ]),
361
+ menuItems: groupedIconMenuItems(zh),
362
+ onChange: chooseIcon,
363
+ }),
364
+ ],
365
+ }),
366
+ jsxs('div', {
367
+ className: 'dtc-rename-actions',
368
+ children: [
369
+ jsx(Button, {
370
+ type: 'button',
371
+ disabled: disabled || Boolean(busy),
372
+ onClick: summarize,
373
+ children: busy === 'summarize'
374
+ ? text('AI 生成摘要中…', 'Generating summary…')
375
+ : text('AI 生成摘要', 'AI generate summary'),
376
+ }),
377
+ jsx(Button, {
378
+ type: 'button',
379
+ disabled: disabled || Boolean(busy) || !summary.trim(),
380
+ onClick: applyTemplate,
381
+ children: busy === 'generate'
382
+ ? text('压缩标题中…', 'Compressing title…')
383
+ : text('从摘要生成标题', 'Generate title from summary'),
384
+ }),
385
+ ],
386
+ }),
387
+ message && jsx('p', { className: 'dtc__status', role: 'status', children: message }),
388
+ error &&
389
+ jsx('p', {
390
+ className: 'dtc__status',
391
+ 'data-error': true,
392
+ role: 'alert',
393
+ children: error,
394
+ }),
395
+ ],
396
+ });
397
+ }
398
+ function SettingsSection({ scope, workspaceCompatibility, connection }) {
399
+ const language = document.documentElement?.lang || navigator.language || 'en';
400
+ const zh = /^zh(?:-|$)/i.test(language);
401
+ const snapshot = React.useSyncExternalStore((listener) => scope.subscribe(listener), () => scope.getSnapshot(), () => scope.getSnapshot());
402
+ const workspaceStatus = React.useSyncExternalStore((listener) => workspaceCompatibility.subscribe(listener), () => workspaceCompatibility.getSnapshot(), () => 'unavailable');
403
+ const resolved = { ...DEFAULTS, ...(snapshot.value || {}) };
404
+ const [draft, setDraft] = React.useState(resolved);
405
+ const [message, setMessage] = React.useState('');
406
+ const [saving, setSaving] = React.useState(false);
407
+ const [open, setOpen] = React.useState(false);
408
+ const [failed, setFailed] = React.useState(false);
409
+ const bodyId = React.useId();
410
+ const header = React.useRef(null);
411
+ const [routes, setRoutes] = React.useState({
412
+ status: 'idle',
413
+ providers: [],
414
+ models: [],
415
+ provider: '',
416
+ });
417
+ const [routeRevision, setRouteRevision] = React.useState(0);
418
+ React.useEffect(() => {
419
+ if (!open)
420
+ return;
421
+ const controller = new AbortController();
422
+ const provider = draft.modelProvider;
423
+ setRoutes({ status: 'loading', providers: [], models: [], provider });
424
+ Promise.resolve()
425
+ .then(() => connection.rpc.call('/titlecraft', 'models', { provider }, controller.signal))
426
+ .then((result) => {
427
+ if (controller.signal.aborted)
428
+ return;
429
+ if (!result?.ok)
430
+ throw new Error('unavailable');
431
+ setRoutes({
432
+ status: 'ready',
433
+ providers: result.value.providers,
434
+ models: result.value.models,
435
+ provider,
436
+ });
437
+ })
438
+ .catch(() => {
439
+ if (!controller.signal.aborted)
440
+ setRoutes({ status: 'error', providers: [], models: [], provider });
441
+ });
442
+ return () => controller.abort();
443
+ }, [open, draft.modelProvider, routeRevision]);
444
+ React.useEffect(() => setDraft(resolved), [snapshot.revision]);
445
+ const set = (field, value) => setDraft((old) => ({ ...old, [field]: value }));
446
+ const typesSupported = Array.isArray(snapshot.value?.types) && Array.isArray(snapshot.value?.enabledTypes);
447
+ const text = (cn, en) => (zh ? cn : en);
448
+ const previewIcon = draft.iconMode === 'none' ? '' : ICONS.find(([id]) => id === draft.iconId)?.[1] || '';
449
+ const preview = draft.formatTemplate
450
+ .replaceAll('{date}', draft.datePattern
451
+ .replaceAll('YYYY', '2026')
452
+ .replaceAll('MM', '09')
453
+ .replaceAll('DD', '03'))
454
+ .replaceAll('{type}', draft.enabledTypes.includes('优化') ? '优化' : draft.enabledTypes[0] || '未选择')
455
+ .replaceAll('{topic}', '标题模板设计')
456
+ .replaceAll('{icon}', previewIcon ? previewIcon + ' ' : '');
457
+ const save = async (event) => {
458
+ event.preventDefault();
459
+ if (unavailable || saving || !typesSupported || !dirty || invalid)
460
+ return;
461
+ setFailed(false);
462
+ setSaving(true);
463
+ setMessage('');
464
+ try {
465
+ const ops = FIELDS.filter((field) => field !== 'fallbackToSessionModel' ||
466
+ typeof snapshot.value?.fallbackToSessionModel === 'boolean').map((field) => ({ op: 'set', path: [field], value: draft[field] }));
467
+ await scope.mutate(ops, snapshot.revision);
468
+ setMessage(text('已保存,规则将实时用于后续标题。', 'Saved. New rules apply to subsequent titles.'));
469
+ setOpen(false);
470
+ header.current?.focus();
471
+ }
472
+ catch (error) {
473
+ setOpen(true);
474
+ setFailed(true);
475
+ setMessage(text(`保存失败:${String(error)}`, `Save failed: ${String(error)}`));
476
+ }
477
+ finally {
478
+ setSaving(false);
479
+ }
480
+ };
481
+ const reset = async () => {
482
+ if (unavailable || saving)
483
+ return;
484
+ setFailed(false);
485
+ setSaving(true);
486
+ setMessage('');
487
+ try {
488
+ await scope.mutate(FIELDS.filter((field) => field !== 'fallbackToSessionModel' ||
489
+ typeof snapshot.value?.fallbackToSessionModel === 'boolean').map((field) => ({ op: 'unset', path: [field] })), snapshot.revision);
490
+ setDraft(DEFAULTS);
491
+ setMessage(text('已恢复默认规则。', 'Defaults restored.'));
492
+ }
493
+ catch (error) {
494
+ setOpen(true);
495
+ setFailed(true);
496
+ setMessage(text(`重置失败:${String(error)}`, `Reset failed: ${String(error)}`));
497
+ }
498
+ finally {
499
+ setSaving(false);
500
+ }
501
+ };
502
+ const unavailable = snapshot.status !== 'ready' || !snapshot.writable;
503
+ const dirty = FIELDS.some((field) => JSON.stringify(draft[field]) !== JSON.stringify(resolved[field]));
504
+ const limits = {
505
+ fontSize: [12, 20],
506
+ sessionTitleLeftOffset: [0, 8],
507
+ idleDelayMs: [250, 600000],
508
+ maxInputBytes: [512, 65536],
509
+ maxOutputTokens: [32, 512],
510
+ timeoutMs: [1000, 2147483647],
511
+ topicMaxChars: [4, 40],
512
+ minConfidence: [0, 1],
513
+ };
514
+ const invalid = (Boolean(draft.modelProvider !== '' || draft.modelName !== '') &&
515
+ (routes.status !== 'ready' ||
516
+ routes.provider !== draft.modelProvider ||
517
+ !routes.providers.includes(draft.modelProvider) ||
518
+ !routes.models.includes(draft.modelName))) ||
519
+ draft.enabledTypes.length === 0 ||
520
+ Object.entries(limits).some(([key, [min, max]]) => !Number.isFinite(draft[key]) ||
521
+ draft[key] < min ||
522
+ draft[key] > max ||
523
+ (key !== 'minConfidence' && !Number.isInteger(draft[key])));
524
+ const form = jsxs('form', {
525
+ id: bodyId,
526
+ hidden: !open,
527
+ className: 'dtc',
528
+ onSubmit: save,
529
+ children: [
530
+ unavailable &&
531
+ jsx('p', {
532
+ className: 'dtc__status',
533
+ role: 'status',
534
+ children: text('当前配置只读或尚未就绪。', 'Settings are read-only or not ready.'),
535
+ }),
536
+ jsxs('fieldset', {
537
+ className: 'dtc__fields',
538
+ disabled: unavailable || saving,
539
+ children: [
540
+ jsxs('div', {
541
+ children: [
542
+ jsx('p', {
543
+ className: 'dtc__lede',
544
+ children: text('标题工艺仅修改会话标题;无法可靠判断主题时保留原名。', 'Changes TitleCraft session titles only; ambiguous topics keep their current title.'),
545
+ }),
546
+ ],
547
+ }),
548
+ workspaceStatus !== 'compatible' &&
549
+ jsx('p', {
550
+ className: 'dtc__compat',
551
+ role: 'status',
552
+ children: text(workspaceStatus === 'incompatible'
553
+ ? '当前 Workspace 会话列表未声明标题工艺兼容契约。自动标题仍可用;原生重命名工作台和列表外观可能不可用。标题工艺不会接管或争抢 Workspace,第三方界面适配应由独立兼容插件提供。'
554
+ : '尚未检测到 Workspace 会话列表。自动标题仍可用;重命名工作台和列表外观将在兼容 Workspace 可用后启用。标题工艺不会接管该界面。', workspaceStatus === 'incompatible'
555
+ ? 'The active Workspace session list does not declare the TitleCraft compatibility contract. Automatic titles still work; the native Rename workbench and list appearance may be unavailable. TitleCraft never takes over or competes for Workspace ownership; third-party UI support belongs in a separate compatibility plugin.'
556
+ : 'No Workspace session list is currently available. Automatic titles still work; the Rename workbench and list appearance activate when a compatible Workspace is present. TitleCraft never takes over this surface.'),
557
+ }),
558
+ snapshot.status === 'unavailable' &&
559
+ jsx('p', {
560
+ className: 'dtc__status',
561
+ children: text('当前 Host 未开放此设置命名空间。', 'This settings namespace is not exposed by the Host.'),
562
+ }),
563
+ jsxs('section', {
564
+ className: 'dtc__card',
565
+ children: [
566
+ jsx('h3', { children: text('格式规则', 'Format rules') }),
567
+ jsxs('div', {
568
+ className: 'dtc__grid',
569
+ children: [
570
+ jsx(Check, {
571
+ label: text('启用自动统一', 'Enable automatic titles'),
572
+ checked: draft.enabled,
573
+ onChange: (v) => set('enabled', v),
574
+ }),
575
+ jsx(Check, {
576
+ label: text('保护人工标题', 'Preserve manual titles'),
577
+ checked: draft.respectManualTitles,
578
+ onChange: (v) => set('respectManualTitles', v),
579
+ }),
580
+ jsx(SelectField, {
581
+ active: open && !unavailable && !saving,
582
+ label: text('模板样式', 'Template style'),
583
+ value: draft.templatePreset,
584
+ options: [
585
+ [
586
+ 'standard',
587
+ text('标准(默认):图标 日期|类型|主题', 'Standard (default): icon date | type | topic'),
588
+ ],
589
+ ['concise', text('精简:图标 类型|主题', 'Concise: icon type | topic')],
590
+ [
591
+ 'topic-first',
592
+ text('主题优先:图标 主题|类型 · 日期', 'Topic first: icon topic | type · date'),
593
+ ],
594
+ [
595
+ 'bracketed',
596
+ text('标签:图标 [类型] 主题 · 日期', 'Bracketed: icon [type] topic · date'),
597
+ ],
598
+ [
599
+ 'compact',
600
+ text('日期简报:图标 日期 · 主题', 'Date brief: icon date · topic'),
601
+ ],
602
+ [
603
+ 'timeline',
604
+ text('时间线:图标 日期 — 主题 [类型]', 'Timeline: icon date — topic [type]'),
605
+ ],
606
+ ['minimal', text('极简:图标 主题', 'Minimal: icon topic')],
607
+ ['custom', text('自定义', 'Custom')],
608
+ ],
609
+ onChange: (v) => {
610
+ const templates = {
611
+ standard: '{icon} {date}|{type}|{topic}',
612
+ concise: '{icon} {type}|{topic}',
613
+ 'topic-first': '{icon} {topic}|{type} · {date}',
614
+ bracketed: '{icon} [{type}] {topic} · {date}',
615
+ compact: '{icon} {date} · {topic}',
616
+ timeline: '{icon} {date} — {topic} [{type}]',
617
+ minimal: '{icon} {topic}',
618
+ };
619
+ setDraft((old) => ({
620
+ ...old,
621
+ templatePreset: v,
622
+ formatTemplate: templates[v] || old.formatTemplate,
623
+ }));
624
+ },
625
+ }),
626
+ jsx(Field, {
627
+ label: text('标题模板', 'Title template'),
628
+ value: draft.formatTemplate,
629
+ onChange: (v) => setDraft((old) => ({
630
+ ...old,
631
+ templatePreset: 'custom',
632
+ formatTemplate: v,
633
+ })),
634
+ wide: true,
635
+ hint: '{date} / {type} / {topic} / {icon}',
636
+ }),
637
+ jsx(SelectField, {
638
+ active: open && !unavailable && !saving,
639
+ label: text('图标模式', 'Icon mode'),
640
+ value: draft.iconMode === 'type' ? 'smart' : draft.iconMode,
641
+ options: [
642
+ ['none', text('不写入标题', 'Do not persist')],
643
+ ['selected', text('固定图标', 'Fixed icon')],
644
+ ['smart', text('智能语义选择', 'Smart semantic selection')],
645
+ ],
646
+ onChange: (v) => set('iconMode', v),
647
+ hint: text('智能模式由精修模型按主题从本地图标白名单选择。', 'Smart mode lets the refinement model choose from the local icon allowlist by topic.'),
648
+ }),
649
+ jsx(SelectField, {
650
+ active: open && !unavailable && !saving,
651
+ iconMenu: true,
652
+ label: text('固定/回退图标', 'Fixed / fallback icon'),
653
+ value: draft.iconId,
654
+ options: ICONS.map(([id, glyph, cn, en]) => [
655
+ id,
656
+ iconOptionLabel(glyph, text(cn, en)),
657
+ ]),
658
+ menuItems: groupedIconMenuItems(zh),
659
+ onChange: (v) => set('iconId', v),
660
+ hint: text('智能精修前或智能结果不可用时使用。', 'Used before smart refinement or when no smart icon is available.'),
661
+ }),
662
+ jsx(SelectField, {
663
+ active: open && !unavailable && !saving,
664
+ label: text('预览字体', 'Preview font'),
665
+ value: draft.fontFamily,
666
+ options: [
667
+ ['dsh', text('跟随 DSH', 'DSH default')],
668
+ ['system-ui', text('系统界面', 'System UI')],
669
+ ['ui-monospace', text('等宽', 'Monospace')],
670
+ ['ui-serif', text('衬线', 'Serif')],
671
+ ],
672
+ onChange: (v) => set('fontFamily', v),
673
+ hint: text('仅用于设置预览;DSH 暂无会话行样式接口。', 'Preview only; DSH exposes no session-row styling API yet.'),
674
+ }),
675
+ jsx(NumberField, {
676
+ label: text('会话标题字号', 'Session title size'),
677
+ value: draft.fontSize,
678
+ min: 12,
679
+ max: 20,
680
+ onChange: (v) => set('fontSize', v),
681
+ }),
682
+ jsx(NumberField, {
683
+ label: text('会话左侧内容与标题向左偏移(0–8 px)', 'Session leading content and title left offset (0–8 px)'),
684
+ value: draft.sessionTitleLeftOffset,
685
+ min: 0,
686
+ max: 8,
687
+ onChange: (v) => set('sessionTitleLeftOffset', v),
688
+ }),
689
+ jsx(SelectField, {
690
+ active: open && !unavailable && !saving,
691
+ label: text('日期来源', 'Date source'),
692
+ value: draft.dateSource,
693
+ options: [
694
+ ['createdAt', text('创建时间(稳定)', 'Created time (stable)')],
695
+ ['lastUserMessage', text('最后真人消息', 'Last human message')],
696
+ ['lastCompletedTurn', text('最后完成回合', 'Last completed turn')],
697
+ ['lastActivity', text('最后持久活动', 'Last durable activity')],
698
+ ],
699
+ onChange: (v) => set('dateSource', v),
700
+ hint: text('后三项会让标题日期随对应活动更新;无匹配事件时回退到创建时间。', 'Activity-based modes update the title date and fall back to creation time when no matching event exists.'),
701
+ }),
702
+ jsx(Field, {
703
+ label: text('时区', 'Timezone'),
704
+ value: draft.timezone,
705
+ onChange: (v) => set('timezone', v),
706
+ placeholder: 'Asia/Shanghai',
707
+ }),
708
+ jsx(DateMode, {
709
+ active: open && !unavailable && !saving,
710
+ label: text('日期模式', 'Date pattern'),
711
+ value: draft.datePattern,
712
+ onChange: (v) => set('datePattern', v),
713
+ }),
714
+ jsx(TypeSelector, {
715
+ types: draft.types,
716
+ enabledTypes: draft.enabledTypes,
717
+ zh,
718
+ disabled: unavailable || saving,
719
+ saved: { types: resolved.types, enabledTypes: resolved.enabledTypes },
720
+ onChange: (value) => setDraft((old) => ({ ...old, ...value })),
721
+ }),
722
+ !typesSupported &&
723
+ jsx('p', {
724
+ className: 'dtc__hint dtc__wide',
725
+ role: 'status',
726
+ children: text('需要重启 DSH 以加载类型配置支持,重启前无法保存。', 'Restart DSH to load type configuration support before saving.'),
727
+ }),
728
+ jsxs('div', {
729
+ className: 'dtc__wide',
730
+ children: [
731
+ jsx('span', {
732
+ className: 'dtc__hint',
733
+ children: text('预览', 'Preview'),
734
+ }),
735
+ jsx('div', {
736
+ className: 'dtc__preview',
737
+ style: {
738
+ fontFamily: draft.fontFamily === 'dsh' ? 'inherit' : draft.fontFamily,
739
+ fontSize: `${draft.fontSize}px`,
740
+ },
741
+ children: preview,
742
+ }),
743
+ ],
744
+ }),
745
+ ],
746
+ }),
747
+ ],
748
+ }),
749
+ jsxs('section', {
750
+ className: 'dtc__card',
751
+ children: [
752
+ jsx('h3', { children: text('生成时机', 'Generation timing') }),
753
+ jsx(SelectField, {
754
+ label: text('首次标题', 'Initial title'),
755
+ active: open && !unavailable && !saving,
756
+ value: draft.firstMessageLlmEnabled
757
+ ? 'ai'
758
+ : draft.instantEnabled
759
+ ? 'rules'
760
+ : 'keep',
761
+ options: [
762
+ ['rules', text('本地规则 · 不调用 AI', 'Local rules · no AI call')],
763
+ ['ai', text('AI 生成 · 更早获得语义标题', 'AI · earlier semantic title')],
764
+ ['keep', text('保持原名', 'Keep existing title')],
765
+ ],
766
+ onChange: (value) => setDraft((old) => ({
767
+ ...old,
768
+ firstMessageLlmEnabled: value === 'ai',
769
+ instantEnabled: value === 'ai' ? old.instantEnabled : value === 'rules',
770
+ })),
771
+ hint: text('AI 生成需要等待模型响应并产生费用;无法可靠判断时保留原名。', 'AI generation takes time and incurs costs; uncertain results keep the existing title.'),
772
+ }),
773
+ jsx(Check, {
774
+ label: text('对话结束后用 AI 更新标题', 'Update title with AI after each turn'),
775
+ checked: draft.refineEnabled,
776
+ onChange: (value) => set('refineEnabled', value),
777
+ }),
778
+ jsx('p', {
779
+ className: 'dtc__hint',
780
+ children: text('随对话内容变化更新标题;已尝试 AI 起名的首回合不会重复调用。', 'Keep the title up to date; a first turn already attempted by AI is not generated twice.'),
781
+ }),
782
+ jsx('h3', {
783
+ style: { marginTop: 24 },
784
+ children: text('模型与调用', 'Models and calls'),
785
+ }),
786
+ jsxs('div', {
787
+ className: 'dtc__checks',
788
+ children: [
789
+ jsx(Check, {
790
+ label: text('自动路由时优先低成本模型', 'Prefer low-cost models for automatic routing'),
791
+ checked: draft.preferCheap,
792
+ onChange: (value) => set('preferCheap', value),
793
+ }),
794
+ typeof snapshot.value?.fallbackToSessionModel === 'boolean'
795
+ ? jsx(Check, {
796
+ label: text('失败后用会话模型再试一次', 'Retry once with the session model on failure'),
797
+ checked: draft.fallbackToSessionModel,
798
+ onChange: (value) => set('fallbackToSessionModel', value),
799
+ })
800
+ : jsx('p', {
801
+ className: 'dtc__hint',
802
+ children: text('重启 DSH 后可配置失败重试。', 'Restart DSH to configure retries.'),
803
+ }),
804
+ ],
805
+ }),
806
+ jsx('p', {
807
+ className: 'dtc__hint',
808
+ children: text('低成本按模型名称估计,不保证价格;失败重试可能增加一次调用费用。', 'Low cost is estimated from model names, not guaranteed; retrying may incur another call.'),
809
+ }),
810
+ jsxs('div', {
811
+ className: 'dtc__grid',
812
+ children: [
813
+ jsx(NumberField, {
814
+ label: text('空闲延迟(毫秒)', 'Idle delay (ms)'),
815
+ value: draft.idleDelayMs,
816
+ min: 250,
817
+ max: 600000,
818
+ onChange: (v) => set('idleDelayMs', v),
819
+ }),
820
+ jsx(NumberField, {
821
+ label: text('主题最长字符', 'Topic max characters'),
822
+ value: draft.topicMaxChars,
823
+ min: 4,
824
+ max: 40,
825
+ onChange: (v) => set('topicMaxChars', v),
826
+ }),
827
+ jsx(NumberField, {
828
+ label: text('最低置信度', 'Minimum confidence'),
829
+ value: draft.minConfidence,
830
+ min: 0,
831
+ max: 1,
832
+ step: 0.01,
833
+ onChange: (v) => set('minConfidence', v),
834
+ }),
835
+ jsx(NumberField, {
836
+ label: text('输入字节上限', 'Input byte limit'),
837
+ value: draft.maxInputBytes,
838
+ min: 512,
839
+ max: 65536,
840
+ onChange: (v) => set('maxInputBytes', v),
841
+ }),
842
+ jsx(NumberField, {
843
+ label: text('输出 Token 上限', 'Output token limit'),
844
+ value: draft.maxOutputTokens,
845
+ min: 32,
846
+ max: 512,
847
+ onChange: (v) => set('maxOutputTokens', v),
848
+ }),
849
+ jsx(NumberField, {
850
+ label: text('超时(毫秒)', 'Timeout (ms)'),
851
+ value: draft.timeoutMs,
852
+ min: 1000,
853
+ max: 2147483647,
854
+ onChange: (v) => set('timeoutMs', v),
855
+ }),
856
+ jsx(SelectField, {
857
+ label: text('固定 Provider(可选)', 'Fixed provider (optional)'),
858
+ value: draft.modelProvider,
859
+ active: open && !unavailable && !saving,
860
+ options: [
861
+ ['', text('自动路由', 'Automatic routing')],
862
+ ...routes.providers.map((id) => [id, id]),
863
+ ...(draft.modelProvider && !routes.providers.includes(draft.modelProvider)
864
+ ? [
865
+ [
866
+ draft.modelProvider,
867
+ `${draft.modelProvider} (${text('待验证/不可用', 'unverified / unavailable')})`,
868
+ ],
869
+ ]
870
+ : []),
871
+ ],
872
+ onChange: (value) => setDraft((old) => ({ ...old, modelProvider: value, modelName: '' })),
873
+ }),
874
+ jsx(SelectField, {
875
+ label: text('固定 Model(可选)', 'Fixed model (optional)'),
876
+ value: draft.modelName,
877
+ active: open &&
878
+ !unavailable &&
879
+ !saving &&
880
+ Boolean(draft.modelProvider) &&
881
+ routes.status === 'ready' &&
882
+ routes.provider === draft.modelProvider,
883
+ options: [
884
+ ['', text('请选择模型', 'Select a model')],
885
+ ...routes.models.map((id) => [id, id]),
886
+ ...(draft.modelName && !routes.models.includes(draft.modelName)
887
+ ? [
888
+ [
889
+ draft.modelName,
890
+ `${draft.modelName} (${text('待验证/不可用', 'unverified / unavailable')})`,
891
+ ],
892
+ ]
893
+ : []),
894
+ ],
895
+ onChange: (value) => set('modelName', value),
896
+ }),
897
+ jsx('p', {
898
+ className: 'dtc__hint',
899
+ role: 'status',
900
+ children: routes.status === 'loading'
901
+ ? text('正在读取可用模型…', 'Loading available models…')
902
+ : routes.status === 'error'
903
+ ? text('无法读取模型。保留原配置;可重试或选择自动路由。', 'Model discovery failed. Existing settings are preserved; retry or choose automatic routing.')
904
+ : !routes.providers.length
905
+ ? text('没有可用 Provider。自动路由仍按会话配置尝试。', 'No providers available. Automatic routing still uses the session configuration.')
906
+ : draft.modelProvider && !routes.models.includes(draft.modelName)
907
+ ? text('请选择该 Provider 下的可用模型;失效配置不能保存。', 'Select an available model for this provider; invalid routes cannot be saved.')
908
+ : text('模型发现不发起生成请求,不改变主会话模型。', 'Discovery does not generate text or change the session model.'),
909
+ }),
910
+ jsx(Button, {
911
+ type: 'button',
912
+ disabled: routes.status === 'loading',
913
+ onClick: () => setRouteRevision((value) => value + 1),
914
+ children: text('刷新模型列表', 'Refresh models'),
915
+ }),
916
+ jsxs('label', {
917
+ className: 'dtc__wide',
918
+ children: [
919
+ text('主题提炼补充规则', 'Additional topic guidance'),
920
+ jsx('textarea', {
921
+ value: draft.topicGuidance,
922
+ onChange: (e) => set('topicGuidance', e.target.value),
923
+ }),
924
+ ],
925
+ }),
926
+ ],
927
+ }),
928
+ ],
929
+ }),
930
+ ],
931
+ }),
932
+ jsxs('div', {
933
+ className: 'dtc__actions',
934
+ children: [
935
+ (message || invalid) &&
936
+ jsx('span', {
937
+ className: 'dtc__status',
938
+ 'aria-live': 'polite',
939
+ 'data-error': failed || invalid,
940
+ role: 'status',
941
+ children: invalid
942
+ ? text('请检查数值范围、启用类型及固定模型的可用性。', 'Check numeric ranges, enabled types, and fixed-model availability.')
943
+ : message,
944
+ }),
945
+ jsx(Button, {
946
+ type: 'button',
947
+ variant: 'outline',
948
+ size: 'sm',
949
+ disabled: !dirty || saving,
950
+ onClick: () => {
951
+ setDraft(resolved);
952
+ setMessage('');
953
+ setFailed(false);
954
+ },
955
+ children: text('放弃修改', 'Discard changes'),
956
+ }),
957
+ jsx(Button, {
958
+ type: 'button',
959
+ variant: 'outline',
960
+ size: 'sm',
961
+ disabled: unavailable || saving,
962
+ onClick: reset,
963
+ children: text('恢复默认', 'Restore defaults'),
964
+ }),
965
+ jsx(Button, {
966
+ type: 'submit',
967
+ variant: 'primary',
968
+ size: 'sm',
969
+ disabled: unavailable || saving || !typesSupported || !dirty || invalid,
970
+ children: saving ? text('保存中…', 'Saving…') : text('保存', 'Save'),
971
+ }),
972
+ ],
973
+ }),
974
+ ],
975
+ });
976
+ if (snapshot.status === 'unavailable')
977
+ return null;
978
+ return jsxs('li', {
979
+ className: 'dtc-plugin',
980
+ 'data-open': open,
981
+ children: [
982
+ jsxs('button', {
983
+ ref: header,
984
+ type: 'button',
985
+ className: 'dtc-plugin__header',
986
+ 'aria-expanded': open,
987
+ 'aria-controls': bodyId,
988
+ 'aria-label': text(`${open ? '收起' : '展开'}:标题工艺`, `${open ? 'Collapse' : 'Expand'}: TitleCraft`),
989
+ onClick: () => setOpen((v) => !v),
990
+ children: [
991
+ jsxs('span', {
992
+ className: 'dtc-plugin__heading',
993
+ children: [
994
+ jsx('strong', { children: text('标题工艺', 'TitleCraft') }),
995
+ jsx('small', {
996
+ children: text('模板、图标、主题提炼与生成时机。', 'Templates, icons, topic extraction and generation timing.'),
997
+ }),
998
+ ],
999
+ }),
1000
+ dirty &&
1001
+ jsx('span', {
1002
+ className: 'dtc-plugin__pending',
1003
+ children: text('未保存', 'Unsaved'),
1004
+ }),
1005
+ jsx(IconChevronDownOutline14, { className: 'dtc-plugin__chevron' }),
1006
+ ],
1007
+ }),
1008
+ form,
1009
+ ],
1010
+ });
1011
+ }
1012
+ function TypeSelector({ types, enabledTypes, saved, onChange, disabled, zh }) {
1013
+ const [name, setName] = React.useState('');
1014
+ const [error, setError] = React.useState('');
1015
+ const text = (cn, en) => (zh ? cn : en);
1016
+ const add = () => {
1017
+ const next = name.trim();
1018
+ if (!/^[\p{L}\p{N}][\p{L}\p{N} _-]*$/u.test(next) || [...next].length > 12) {
1019
+ setError(text('名称需为 1–12 个字符,仅允许文字、数字、空格、下划线和短横线。', 'Use 1–12 letters, digits, spaces, underscores or hyphens.'));
1020
+ return;
1021
+ }
1022
+ if (types.some((type) => type.toLowerCase() === next.toLowerCase())) {
1023
+ setError(text('该类型已存在。', 'This type already exists.'));
1024
+ return;
1025
+ }
1026
+ if (types.length >= 32) {
1027
+ setError(text('最多 32 个类型。', 'Maximum 32 types.'));
1028
+ return;
1029
+ }
1030
+ onChange({ types: [...types, next], enabledTypes: [...enabledTypes, next] });
1031
+ setName('');
1032
+ setError('');
1033
+ };
1034
+ const change = (value) => {
1035
+ onChange(value);
1036
+ setError('');
1037
+ };
1038
+ return jsxs('fieldset', {
1039
+ className: 'dtc__type-editor dtc__wide',
1040
+ disabled,
1041
+ children: [
1042
+ jsx('legend', { children: text('标题类型', 'Title types') }),
1043
+ jsx('p', {
1044
+ className: 'dtc__hint',
1045
+ children: text('勾选允许生成的类型;新增类型默认启用。更改后需保存,不会批量修改已有标题。', 'Select types allowed for new titles. New types are enabled. Save to apply; existing titles are not batch-renamed.'),
1046
+ }),
1047
+ jsx('div', {
1048
+ className: 'dtc__type-list',
1049
+ children: types.map((type) => jsxs('div', {
1050
+ className: 'dtc__type-option',
1051
+ children: [
1052
+ jsx(Check, {
1053
+ label: type,
1054
+ checked: enabledTypes.includes(type),
1055
+ onChange: (checked) => change({
1056
+ types,
1057
+ enabledTypes: checked
1058
+ ? [...enabledTypes, type]
1059
+ : enabledTypes.filter((item) => item !== type),
1060
+ }),
1061
+ }),
1062
+ jsx(Button, {
1063
+ size: 'sm',
1064
+ type: 'button',
1065
+ className: 'dtc__type-remove',
1066
+ 'aria-label': text(`删除类型 ${type}`, `Delete type ${type}`),
1067
+ onClick: () => change({
1068
+ types: types.filter((item) => item !== type),
1069
+ enabledTypes: enabledTypes.filter((item) => item !== type),
1070
+ }),
1071
+ children: '×',
1072
+ }),
1073
+ ],
1074
+ }, type)),
1075
+ }),
1076
+ jsx('p', {
1077
+ className: 'dtc__hint',
1078
+ role: 'status',
1079
+ children: enabledTypes.length === 0
1080
+ ? text('至少启用一个类型才能保存。', 'Enable at least one type to save.')
1081
+ : text(`已启用 ${enabledTypes.length} / ${types.length} 个类型`, `${enabledTypes.length} / ${types.length} types enabled`),
1082
+ }),
1083
+ jsxs('div', {
1084
+ className: 'dtc__type-tools',
1085
+ children: [
1086
+ jsxs('label', {
1087
+ children: [
1088
+ text('新增类型', 'New type'),
1089
+ jsx(Input, {
1090
+ className: 'dtc__input',
1091
+ value: name,
1092
+ placeholder: text('例如:测试', 'e.g. Testing'),
1093
+ onChange: (e) => setName(e.target.value),
1094
+ onKeyDown: (e) => {
1095
+ if (e.key === 'Enter' && !e.nativeEvent?.isComposing) {
1096
+ e.preventDefault();
1097
+ add();
1098
+ }
1099
+ },
1100
+ }),
1101
+ ],
1102
+ }),
1103
+ jsx(Button, {
1104
+ variant: 'outline',
1105
+ size: 'sm',
1106
+ type: 'button',
1107
+ onClick: add,
1108
+ disabled: !name.trim() || types.length >= 32,
1109
+ children: text('添加', 'Add'),
1110
+ }),
1111
+ ],
1112
+ }),
1113
+ error && jsx('p', { role: 'alert', className: 'dtc__hint', children: error }),
1114
+ jsxs('div', {
1115
+ className: 'dtc__type-tools',
1116
+ children: [
1117
+ jsx(Button, {
1118
+ variant: 'outline',
1119
+ size: 'sm',
1120
+ type: 'button',
1121
+ onClick: () => {
1122
+ change({ types: [...TYPES], enabledTypes: [...TYPES] });
1123
+ setName('');
1124
+ },
1125
+ children: text('重置类型', 'Reset types'),
1126
+ title: text('恢复默认八种类型,保存后生效', 'Restore eight defaults; save to apply'),
1127
+ }),
1128
+ jsx(Button, {
1129
+ variant: 'outline',
1130
+ size: 'sm',
1131
+ type: 'button',
1132
+ onClick: () => {
1133
+ change({ types: [...saved.types], enabledTypes: [...saved.enabledTypes] });
1134
+ setName('');
1135
+ },
1136
+ children: text('还原类型', 'Revert types'),
1137
+ title: text('撤销类型的未保存修改', 'Discard unsaved type changes'),
1138
+ }),
1139
+ ],
1140
+ }),
1141
+ jsx('p', {
1142
+ className: 'dtc__hint',
1143
+ children: text('重置:恢复默认八种;还原:回到上次保存。自定义类型由模型精修识别;未启用精修时不会自动推断自定义类型。', 'Reset restores eight defaults; Revert restores saved types. Custom types require model refinement.'),
1144
+ }),
1145
+ ],
1146
+ });
1147
+ }
1148
+ function Check({ label, checked, onChange }) {
1149
+ return jsxs('label', {
1150
+ className: 'dtc__check',
1151
+ children: [
1152
+ jsx('input', {
1153
+ type: 'checkbox',
1154
+ checked: Boolean(checked),
1155
+ onChange: (event) => onChange(event.target.checked),
1156
+ }),
1157
+ jsx('span', { children: label }),
1158
+ ],
1159
+ });
1160
+ }
1161
+ function Field({ label, value, onChange, placeholder, wide, hint }) {
1162
+ return jsxs('label', {
1163
+ className: wide ? 'dtc__wide' : undefined,
1164
+ children: [
1165
+ label,
1166
+ jsx(Input, {
1167
+ className: 'dtc__input',
1168
+ value: value ?? '',
1169
+ placeholder,
1170
+ onChange: (event) => onChange(event.target.value),
1171
+ }),
1172
+ hint && jsx('span', { className: 'dtc__hint', children: hint }),
1173
+ ],
1174
+ });
1175
+ }
1176
+ function SelectField({ label, value, options, menuItems, onChange, hint, active = true, iconMenu = false, }) {
1177
+ const [open, setOpen] = React.useState(false);
1178
+ React.useEffect(() => {
1179
+ if (!active)
1180
+ setOpen(false);
1181
+ }, [active]);
1182
+ const selected = options.find(([id]) => id === value)?.[1] ?? value;
1183
+ return jsxs('div', {
1184
+ className: 'dtc__field',
1185
+ children: [
1186
+ jsx('span', { children: label }),
1187
+ jsx(Menu, {
1188
+ open: open && active,
1189
+ onClose: () => setOpen(false),
1190
+ portal: true,
1191
+ align: 'end',
1192
+ className: iconMenu ? 'dtc-icon-menu' : undefined,
1193
+ items: menuItems ?? options.map(([id, name]) => ({ id, label: name })),
1194
+ selectedId: value,
1195
+ onSelect: (id) => {
1196
+ onChange(id);
1197
+ setOpen(false);
1198
+ },
1199
+ anchor: jsxs(Button, {
1200
+ type: 'button',
1201
+ variant: 'outline',
1202
+ className: 'dtc__select',
1203
+ disabled: !active,
1204
+ 'aria-label': label,
1205
+ 'aria-haspopup': 'menu',
1206
+ 'aria-expanded': open && active,
1207
+ onClick: () => {
1208
+ if (active)
1209
+ setOpen((v) => !v);
1210
+ },
1211
+ children: [jsx('span', { children: selected }), jsx(IconChevronDownOutline14, {})],
1212
+ }),
1213
+ }),
1214
+ hint && jsx('span', { className: 'dtc__hint', children: hint }),
1215
+ ],
1216
+ });
1217
+ }
1218
+ function NumberField({ label, value, onChange, min, max, step = 1 }) {
1219
+ return jsxs('label', {
1220
+ children: [
1221
+ label,
1222
+ jsx(Input, {
1223
+ className: 'dtc__input',
1224
+ type: 'number',
1225
+ required: true,
1226
+ value,
1227
+ min,
1228
+ max,
1229
+ step,
1230
+ onChange: (event) => onChange(event.target.value === '' ? '' : Number(event.target.value)),
1231
+ }),
1232
+ ],
1233
+ });
1234
+ }
1235
+ function DateMode({ value, onChange, label, active }) {
1236
+ return jsx(SelectField, {
1237
+ label,
1238
+ value,
1239
+ onChange,
1240
+ active,
1241
+ options: ['MMDD', 'YYYYMMDD', 'YYYY-MM-DD'].map((id) => [id, id]),
1242
+ });
1243
+ }
1244
+ return { apply, inject };
1245
+ },
1246
+ });
1247
+ //# sourceMappingURL=client.js.map