@mzzsfy/dsh-context-manager 0.3.0 → 0.4.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/README.md +2 -2
- package/package.json +1 -1
- package/src/client.js +52 -11
- package/src/index.js +61 -36
- package/test/client-history-scope.test.mjs +34 -0
- package/test/client-runtime.test.mjs +14 -0
- package/test/index.test.mjs +116 -4
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
输入框按 `Alt+↑`,或点击输入框下方工具排的历史按钮(指令/附件右侧,与 Alt+↑ 等效,再点关闭)唤起历史输入浮层:
|
|
10
10
|
|
|
11
|
-
-
|
|
11
|
+
- 范围导航:顶栏范围标签点击展开 pill 组选择;`←/→` 按 常用(prompts)→ 当前会话(session)→ 本工作区(workspace)→ 全部工作区(global) 依次切换(边界停住),列表时间倒序
|
|
12
12
|
- 收录范围:普通输入与斜杠命令(如 `/goal …`,按会话日志的 command/run 重组为完整命令行),工具结果回填与插件注入不收录
|
|
13
13
|
- 搜索:输入即时过滤(IME 组合输入守卫,命中计数),`↑/↓` 选中、`Enter` 回填
|
|
14
14
|
- 回填经宿主公共契约 `inputActions.setDraft`;`Esc` 两级退出(搜索词非空先清词,再按关闭浮层)
|
|
@@ -66,4 +66,4 @@
|
|
|
66
66
|
|
|
67
67
|
## dsh 版本兼容
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
三版本(0.1.2-rc.1 / 0.1.5-rc.3 / 0.1.7-rc.1)兼容通过:样式注入 data-plugin 契约、历史输入浮层(Alt+↑ 或输入框按钮)、启停路由与方法白名单、激活 live。0.1.6 设置分区卡片位于「插件」页,非独立导航项;0.1.7-rc.1 内置插件页改版为分组列表(无卡片开关锚),样式与 API 面实测正常。
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -18,6 +18,7 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
factory(require) {
|
|
19
19
|
const React = require('react')
|
|
20
20
|
const { useState, useEffect, useRef } = React
|
|
21
|
+
const PKG_ID = '@mzzsfy/dsh-context-manager'
|
|
21
22
|
|
|
22
23
|
// 导航图标声明:交给 dsh-settings-nav-icons 统一渲染;键 = 市场短名(发现页
|
|
23
24
|
// 收录显示形态);该插件未就绪时入队,由其启动时排空
|
|
@@ -80,11 +81,19 @@ const CSS = [
|
|
|
80
81
|
' color-scheme:light dark; color:light-dark(#0f1115, #e8eaed);',
|
|
81
82
|
' background:light-dark(#fff, #1e1f22); border-radius:14px;',
|
|
82
83
|
' box-shadow:0 0 0 0.5px light-dark(rgba(15,17,21,.18), rgba(255,255,255,.14)), 0 4px 16px rgba(0,0,0,.08), 0 16px 48px rgba(0,0,0,.16); }',
|
|
83
|
-
'.cx-hist__hint { display:flex; align-items:center; gap:10px; padding:9px 14px; flex:none;',
|
|
84
|
+
'.cx-hist__hint { display:flex; flex-wrap:wrap; align-items:center; gap:10px; padding:9px 14px; flex:none;',
|
|
84
85
|
' border-bottom:1px solid light-dark(rgba(15,17,21,.08), rgba(255,255,255,.1));',
|
|
85
86
|
' font:var(--dsw-font-xxs-12, 12px/18px sans-serif); color:light-dark(rgba(15,17,21,.55), rgba(232,234,237,.55)); }',
|
|
86
|
-
|
|
87
|
-
'
|
|
87
|
+
// 范围 pill 组:点击直达切换,选中实底蓝,未选淡底
|
|
88
|
+
'.cx-hist__scopebar { display:flex; align-items:center; gap:4px; flex:none; }',
|
|
89
|
+
'.cx-hist__pill { appearance:none; border:0; cursor:pointer; flex:none; padding:2px 9px;',
|
|
90
|
+
' border-radius:999px; font:var(--dsw-font-xxs-12, 12px/18px sans-serif);',
|
|
91
|
+
' color:light-dark(rgba(15,17,21,.6), rgba(232,234,237,.6));',
|
|
92
|
+
' background:light-dark(rgba(15,17,21,.06), rgba(255,255,255,.08)); }',
|
|
93
|
+
'.cx-hist__pill:hover { color:light-dark(rgba(15,17,21,.85), rgba(232,234,237,.85));',
|
|
94
|
+
' background:light-dark(rgba(15,17,21,.1), rgba(255,255,255,.14)); }',
|
|
95
|
+
'.cx-hist__pill--on, .cx-hist__pill--on:hover { background:#1677ff; color:#fff; font-weight:600; }',
|
|
96
|
+
'.cx-hist__pill:focus-visible { outline:2px solid #1677ff; outline-offset:1px; }',
|
|
88
97
|
'.cx-hist__search { flex:1; min-width:0; padding:3px 10px; border-radius:8px; color-scheme:light dark;',
|
|
89
98
|
' border:1px solid light-dark(rgba(15,17,21,.14), rgba(255,255,255,.18));',
|
|
90
99
|
' background:transparent; color:inherit; font:var(--dsw-font-xxs-12, 12px/18px sans-serif); }',
|
|
@@ -301,6 +310,8 @@ function HistoryDock({ session, inputActions }) {
|
|
|
301
310
|
const [items, setItems] = useState(null)
|
|
302
311
|
const [cursor, setCursor] = useState(-1)
|
|
303
312
|
const [scopeIndex, setScopeIndex] = useState(HISTORY_SCOPE_DEFAULT)
|
|
313
|
+
// 范围标签展开态:默认单胶囊只显当前范围,点击展开 pill 组,选择后收起
|
|
314
|
+
const [scopeOpen, setScopeOpen] = useState(false)
|
|
304
315
|
const [aligning, setAligning] = useState(false)
|
|
305
316
|
const [loadError, setLoadError] = useState(false)
|
|
306
317
|
// 收藏集合:浮层打开时并行拉一次,行悬停星标据此显示实/空心;
|
|
@@ -456,6 +467,7 @@ function HistoryDock({ session, inputActions }) {
|
|
|
456
467
|
rawItemsRef.current = null
|
|
457
468
|
queryRef.current = ''
|
|
458
469
|
setQuery('')
|
|
470
|
+
setScopeOpen(false)
|
|
459
471
|
syncView({ open: true, items: null, cursor: -1, scopeIndex: HISTORY_SCOPE_DEFAULT, aligning: false })
|
|
460
472
|
setOpen(true)
|
|
461
473
|
setScopeIndex(HISTORY_SCOPE_DEFAULT)
|
|
@@ -468,10 +480,10 @@ function HistoryDock({ session, inputActions }) {
|
|
|
468
480
|
requestScope(HISTORY_SCOPE_DEFAULT)
|
|
469
481
|
}
|
|
470
482
|
|
|
471
|
-
//
|
|
472
|
-
function
|
|
473
|
-
|
|
474
|
-
|
|
483
|
+
// 范围切换共用落点:清过滤与选择态后拉取目标范围;同范围不动,切换即收起展开态
|
|
484
|
+
function selectScope(next) {
|
|
485
|
+
if (next === viewRef.current.scopeIndex) return
|
|
486
|
+
setScopeOpen(false)
|
|
475
487
|
rawItemsRef.current = null
|
|
476
488
|
queryRef.current = ''
|
|
477
489
|
setQuery('')
|
|
@@ -485,6 +497,13 @@ function HistoryDock({ session, inputActions }) {
|
|
|
485
497
|
requestScope(next)
|
|
486
498
|
}
|
|
487
499
|
|
|
500
|
+
// ←/→ 相对切换:→ 向大(工作区/全局),← 返回收藏;边界停住
|
|
501
|
+
function switchScope(delta) {
|
|
502
|
+
const next = viewRef.current.scopeIndex + delta
|
|
503
|
+
if (next < 0 || next >= HISTORY_SCOPES.length) return
|
|
504
|
+
selectScope(next)
|
|
505
|
+
}
|
|
506
|
+
|
|
488
507
|
// Alt+↑ 唤起浮层;浮层开 = 菜单模态,捕获阶段拦截导航键,先于 Lexical 光标移动。
|
|
489
508
|
// 设置开关停用时监听器直接放行(读 ref,开关值挂载拉取后即时反映)
|
|
490
509
|
useEffect(() => {
|
|
@@ -675,7 +694,20 @@ function HistoryDock({ session, inputActions }) {
|
|
|
675
694
|
return h('div', { className: 'cx-hist', ref: rootRef },
|
|
676
695
|
open && h('div', { className: 'cx-hist__pop' },
|
|
677
696
|
h('div', { className: 'cx-hist__hint' },
|
|
678
|
-
h('span', { className: 'cx-
|
|
697
|
+
h('span', { className: 'cx-hist__scopebar' },
|
|
698
|
+
scopeOpen
|
|
699
|
+
? HISTORY_SCOPE_LABELS.map((label, idx) => h('button', {
|
|
700
|
+
key: label,
|
|
701
|
+
className: 'cx-hist__pill' + (idx === scopeIndex ? ' cx-hist__pill--on' : ''),
|
|
702
|
+
title: idx === scopeIndex ? '收起' : '切换到' + label,
|
|
703
|
+
onClick: () => (idx === scopeIndex ? setScopeOpen(false) : selectScope(idx)),
|
|
704
|
+
}, label))
|
|
705
|
+
: h('button', {
|
|
706
|
+
className: 'cx-hist__pill cx-hist__pill--on',
|
|
707
|
+
title: '点击选择范围',
|
|
708
|
+
onClick: () => setScopeOpen(true),
|
|
709
|
+
}, HISTORY_SCOPE_LABELS[scopeIndex] + ' ▾'),
|
|
710
|
+
),
|
|
679
711
|
inPrompts
|
|
680
712
|
? h('button', {
|
|
681
713
|
className: 'cx-hist__editbtn',
|
|
@@ -1184,7 +1216,7 @@ function ForkDock({ session, forkSession, cancelSession, turnEnds, refreshRef })
|
|
|
1184
1216
|
|
|
1185
1217
|
// 面板设置项悬停说明:原生 title(设置侧栏为滚动容器,CSS 气泡会被 overflow
|
|
1186
1218
|
// 裁剪,JS 定位复杂度不成比例);文案与功能行为同源维护,由源码契约测试锁定
|
|
1187
|
-
const HISTORY_SWITCH_TITLE = '在输入框按 Alt+↑ 唤起历史输入浮层,浏览并回填历史输入;浮层内 ←/→
|
|
1219
|
+
const HISTORY_SWITCH_TITLE = '在输入框按 Alt+↑ 唤起历史输入浮层,浏览并回填历史输入;浮层内 ←/→ 切换范围,点击顶栏范围标签展开选择(常用 / 当前会话 / 本工作区 / 全部工作区),顶部搜索框过滤条目,行悬停星标可收藏常用提示词。停用后快捷键与浮层整体关闭,刷新页面生效。'
|
|
1188
1220
|
const HISTORY_BUTTON_SWITCH_TITLE = '输入框下方工具排显示历史输入按钮,点击打开浮层,再点关闭,与 Alt+↑ 等效;停用仅隐藏按钮,快捷键与浮层不受影响;刷新页面生效。'
|
|
1189
1221
|
const STEER_SWITCH_TITLE = '插话发送后、尚未被智能体应用期间,在该插话气泡的操作图标排显示撤回按钮,点击撤回并把原文填回输入框(覆盖输入框现有草稿);含附件的插话不可撤回;消息被应用后按钮随气泡消失,恰在应用瞬间点击会提示已应用且不动草稿。停用即不再注入,刷新页面生效。'
|
|
1190
1222
|
const FORK_SWITCH_TITLE = '消息气泡操作排显示分叉按钮,点击分叉出新会话到该轮之前(该轮不带入子会话),该轮的用户输入自动回填子会话输入框供编辑重发,子会话自动打开且标题尾号递增;进行中的轮(回复尚未完成)同样可分叉,分叉后自动停止本会话该轮未完成的回复;首轮(无更早上下文可继承,新建会话即为同义操作)与无文本输入的轮(纯图等,无从重发)不注入;停用即不再注入,刷新页面生效。'
|
|
@@ -1364,15 +1396,24 @@ function ContextPanel() {
|
|
|
1364
1396
|
}, 'context-manager styles')
|
|
1365
1397
|
|
|
1366
1398
|
// 设置卡片:注册进官方「插件」设置页的可配置插件区(keyed slot,
|
|
1367
|
-
// key = 本插件 settings namespace)
|
|
1399
|
+
// key = 本插件 settings namespace),不占独立设置导航项。
|
|
1400
|
+
// 0.1.7-rc.1+ 该槽被宿主移除,卡片换形注册进插件管理页包详情配置槽
|
|
1401
|
+
// (plugins.bundle.config,key = 包名,管理页 ledger.bundles 判定在场);
|
|
1402
|
+
// 两代槽位并存注册:inject 是声明生命周期效应,错误世代宿主永不声明
|
|
1403
|
+
// 该槽,回调挂起不执行,零成本
|
|
1368
1404
|
ctx.slots.inject('settings.plugin.item', () =>
|
|
1369
1405
|
ctx.slots.register(
|
|
1370
1406
|
{ name: 'settings.plugin.item', id: 'context', key: 'context' },
|
|
1371
1407
|
() => React.createElement(ContextPanel),
|
|
1372
1408
|
))
|
|
1409
|
+
ctx.slots.inject('plugins.bundle.config', () =>
|
|
1410
|
+
ctx.slots.register(
|
|
1411
|
+
{ name: 'plugins.bundle.config', key: PKG_ID },
|
|
1412
|
+
() => React.createElement(ContextPanel),
|
|
1413
|
+
))
|
|
1373
1414
|
|
|
1374
1415
|
// 历史输入回溯入口:官方 conversation.input.dock 插槽(dsh-client-ui-goal 同构)。
|
|
1375
|
-
//
|
|
1416
|
+
// 宿主缺该插槽时 inject 挂起不注册(声明生命周期效应),本功能静默缺席
|
|
1376
1417
|
try {
|
|
1377
1418
|
ctx.slots.inject('conversation.input.dock', () =>
|
|
1378
1419
|
ctx.slots.register(
|
package/src/index.js
CHANGED
|
@@ -61,6 +61,21 @@ const SETTINGS_SCHEMA = schemastery.object({
|
|
|
61
61
|
.description('会话标题栏的复制 sessionId 按钮,平时隐藏,鼠标悬停标题栏时显示,点击复制当前会话 id;关闭后按钮整体不渲染;变更刷新页面生效'),
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
+
// 0.1.7 宿主以静态 Config 导出生成设置节表单(maintain 同构);legacy 宿主经
|
|
65
|
+
// settings.register 注册命名空间。根级 volatile 包装:0.1.7 下整节为 live 表单,
|
|
66
|
+
// 节写经 loader 原地热更;旧宿主 schemastery 无 volatile 方法,特性检测原样返回
|
|
67
|
+
export const Config = volatileWrap(SETTINGS_SCHEMA)
|
|
68
|
+
|
|
69
|
+
function volatileWrap(schema) {
|
|
70
|
+
return typeof schema.volatile === 'function' ? schema.volatile() : schema
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// volatile ref 动态解包(get 协议):0.1.7 下 apply 入参 config 为整节单 ref;
|
|
74
|
+
// legacy 宿主 config 为普通对象原样透传
|
|
75
|
+
function unwrapVolatile(value) {
|
|
76
|
+
return typeof value?.get === 'function' ? value.get() : value
|
|
77
|
+
}
|
|
78
|
+
|
|
64
79
|
function sendJson(res, status, payload) {
|
|
65
80
|
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
66
81
|
res.end(JSON.stringify(payload))
|
|
@@ -77,9 +92,11 @@ function respondError(ctx, res, error) {
|
|
|
77
92
|
sendJson(res, 400, { error: message })
|
|
78
93
|
}
|
|
79
94
|
|
|
80
|
-
function readSettings(ctx) {
|
|
95
|
+
function readSettings(ctx, config) {
|
|
81
96
|
const settings = ctx.get('settings')
|
|
82
|
-
|
|
97
|
+
// 双形读(maintain 同构):legacy settings 命名空间 get;0.1.7+ apply 入参
|
|
98
|
+
// config 整节解包。方法面守卫兼防:服务在但缺 get 时回落默认值,防路由 handler 抛错
|
|
99
|
+
const value = settings && typeof settings.get === 'function' ? settings.get(NAMESPACE) : unwrapVolatile(config)
|
|
83
100
|
return {
|
|
84
101
|
historyEnabled: !value || value.historyEnabled !== false,
|
|
85
102
|
historyButtonEnabled: !value || value.historyButtonEnabled !== false,
|
|
@@ -90,6 +107,22 @@ function readSettings(ctx) {
|
|
|
90
107
|
}
|
|
91
108
|
}
|
|
92
109
|
|
|
110
|
+
// 双形写(maintain 同构,判定延迟到使用点):legacy 方法面以 register 为准
|
|
111
|
+
// (0.1.7 的 settings.update 名义在场但按命名空间找条目必败,不可作判据);
|
|
112
|
+
// 0.1.7+ 走 configEditor.edit 落 profile 条目(fiber.entry,与命名空间解耦),
|
|
113
|
+
// 仅 volatile 字段变化时 loader 原地热更不重启。await 保证持久化完成后再读回
|
|
114
|
+
async function persistToggle(ctx, patch) {
|
|
115
|
+
const settings = ctx.get('settings')
|
|
116
|
+
if (typeof settings?.register === 'function' && typeof settings?.update === 'function') {
|
|
117
|
+
await settings.update(NAMESPACE, patch)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
const editor = ctx.get('configEditor')
|
|
121
|
+
const entry = ctx.fiber?.entry
|
|
122
|
+
if (!editor || typeof editor.edit !== 'function' || !entry) throw new Error('宿主设置服务不可用')
|
|
123
|
+
await editor.edit(entry, (current) => ({ ...current, ...patch }))
|
|
124
|
+
}
|
|
125
|
+
|
|
93
126
|
function readBody(req) {
|
|
94
127
|
const BODY_MAX_BYTES = 64 * 1024
|
|
95
128
|
return new Promise((resolve, reject) => {
|
|
@@ -453,7 +486,7 @@ export function apply(ctx, config) {
|
|
|
453
486
|
handler: async (req, res) => {
|
|
454
487
|
try {
|
|
455
488
|
if (req.method === 'GET') {
|
|
456
|
-
sendJson(res, 200, { enabled: readSettings(ctx).historyEnabled })
|
|
489
|
+
sendJson(res, 200, { enabled: readSettings(ctx, config).historyEnabled })
|
|
457
490
|
return
|
|
458
491
|
}
|
|
459
492
|
if (!rejectMethod(req, res, 'POST')) return
|
|
@@ -463,11 +496,8 @@ export function apply(ctx, config) {
|
|
|
463
496
|
} catch {
|
|
464
497
|
throw new Error(MESSAGES.badJsonBody)
|
|
465
498
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
// update 异步落盘后才提交新值:await 保证持久化完成后再读回
|
|
469
|
-
await settings.update(NAMESPACE, { historyEnabled: Boolean(body && body.enabled) })
|
|
470
|
-
sendJson(res, 200, { enabled: readSettings(ctx).historyEnabled })
|
|
499
|
+
await persistToggle(ctx, { historyEnabled: Boolean(body && body.enabled) })
|
|
500
|
+
sendJson(res, 200, { enabled: readSettings(ctx, config).historyEnabled })
|
|
471
501
|
} catch (error) {
|
|
472
502
|
respondError(ctx, res, error)
|
|
473
503
|
}
|
|
@@ -479,7 +509,7 @@ export function apply(ctx, config) {
|
|
|
479
509
|
handler: async (req, res) => {
|
|
480
510
|
try {
|
|
481
511
|
if (req.method === 'GET') {
|
|
482
|
-
sendJson(res, 200, { enabled: readSettings(ctx).historyButtonEnabled })
|
|
512
|
+
sendJson(res, 200, { enabled: readSettings(ctx, config).historyButtonEnabled })
|
|
483
513
|
return
|
|
484
514
|
}
|
|
485
515
|
if (!rejectMethod(req, res, 'POST')) return
|
|
@@ -489,11 +519,8 @@ export function apply(ctx, config) {
|
|
|
489
519
|
} catch {
|
|
490
520
|
throw new Error(MESSAGES.badJsonBody)
|
|
491
521
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
// update 异步落盘后才提交新值:await 保证持久化完成后再读回
|
|
495
|
-
await settings.update(NAMESPACE, { historyButtonEnabled: Boolean(body && body.enabled) })
|
|
496
|
-
sendJson(res, 200, { enabled: readSettings(ctx).historyButtonEnabled })
|
|
522
|
+
await persistToggle(ctx, { historyButtonEnabled: Boolean(body && body.enabled) })
|
|
523
|
+
sendJson(res, 200, { enabled: readSettings(ctx, config).historyButtonEnabled })
|
|
497
524
|
} catch (error) {
|
|
498
525
|
respondError(ctx, res, error)
|
|
499
526
|
}
|
|
@@ -505,7 +532,7 @@ export function apply(ctx, config) {
|
|
|
505
532
|
handler: async (req, res) => {
|
|
506
533
|
try {
|
|
507
534
|
if (req.method === 'GET') {
|
|
508
|
-
sendJson(res, 200, { enabled: readSettings(ctx).steerRecallEnabled })
|
|
535
|
+
sendJson(res, 200, { enabled: readSettings(ctx, config).steerRecallEnabled })
|
|
509
536
|
return
|
|
510
537
|
}
|
|
511
538
|
if (!rejectMethod(req, res, 'POST')) return
|
|
@@ -515,11 +542,8 @@ export function apply(ctx, config) {
|
|
|
515
542
|
} catch {
|
|
516
543
|
throw new Error(MESSAGES.badJsonBody)
|
|
517
544
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
// update 异步落盘后才提交新值:await 保证持久化完成后再读回
|
|
521
|
-
await settings.update(NAMESPACE, { steerRecallEnabled: Boolean(body && body.enabled) })
|
|
522
|
-
sendJson(res, 200, { enabled: readSettings(ctx).steerRecallEnabled })
|
|
545
|
+
await persistToggle(ctx, { steerRecallEnabled: Boolean(body && body.enabled) })
|
|
546
|
+
sendJson(res, 200, { enabled: readSettings(ctx, config).steerRecallEnabled })
|
|
523
547
|
} catch (error) {
|
|
524
548
|
respondError(ctx, res, error)
|
|
525
549
|
}
|
|
@@ -531,7 +555,7 @@ export function apply(ctx, config) {
|
|
|
531
555
|
handler: async (req, res) => {
|
|
532
556
|
try {
|
|
533
557
|
if (req.method === 'GET') {
|
|
534
|
-
sendJson(res, 200, { enabled: readSettings(ctx).forkEnabled })
|
|
558
|
+
sendJson(res, 200, { enabled: readSettings(ctx, config).forkEnabled })
|
|
535
559
|
return
|
|
536
560
|
}
|
|
537
561
|
if (!rejectMethod(req, res, 'POST')) return
|
|
@@ -541,11 +565,8 @@ export function apply(ctx, config) {
|
|
|
541
565
|
} catch {
|
|
542
566
|
throw new Error(MESSAGES.badJsonBody)
|
|
543
567
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
// update 异步落盘后才提交新值:await 保证持久化完成后再读回
|
|
547
|
-
await settings.update(NAMESPACE, { forkEnabled: Boolean(body && body.enabled) })
|
|
548
|
-
sendJson(res, 200, { enabled: readSettings(ctx).forkEnabled })
|
|
568
|
+
await persistToggle(ctx, { forkEnabled: Boolean(body && body.enabled) })
|
|
569
|
+
sendJson(res, 200, { enabled: readSettings(ctx, config).forkEnabled })
|
|
549
570
|
} catch (error) {
|
|
550
571
|
respondError(ctx, res, error)
|
|
551
572
|
}
|
|
@@ -557,7 +578,7 @@ export function apply(ctx, config) {
|
|
|
557
578
|
handler: async (req, res) => {
|
|
558
579
|
try {
|
|
559
580
|
if (req.method === 'GET') {
|
|
560
|
-
sendJson(res, 200, { enabled: readSettings(ctx).forkAutoResendEnabled })
|
|
581
|
+
sendJson(res, 200, { enabled: readSettings(ctx, config).forkAutoResendEnabled })
|
|
561
582
|
return
|
|
562
583
|
}
|
|
563
584
|
if (!rejectMethod(req, res, 'POST')) return
|
|
@@ -567,10 +588,8 @@ export function apply(ctx, config) {
|
|
|
567
588
|
} catch {
|
|
568
589
|
throw new Error(MESSAGES.badJsonBody)
|
|
569
590
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
await settings.update(NAMESPACE, { forkAutoResendEnabled: Boolean(body && body.enabled) })
|
|
573
|
-
sendJson(res, 200, { enabled: readSettings(ctx).forkAutoResendEnabled })
|
|
591
|
+
await persistToggle(ctx, { forkAutoResendEnabled: Boolean(body && body.enabled) })
|
|
592
|
+
sendJson(res, 200, { enabled: readSettings(ctx, config).forkAutoResendEnabled })
|
|
574
593
|
} catch (error) {
|
|
575
594
|
respondError(ctx, res, error)
|
|
576
595
|
}
|
|
@@ -582,7 +601,7 @@ export function apply(ctx, config) {
|
|
|
582
601
|
handler: async (req, res) => {
|
|
583
602
|
try {
|
|
584
603
|
if (req.method === 'GET') {
|
|
585
|
-
sendJson(res, 200, { enabled: readSettings(ctx).copySidEnabled })
|
|
604
|
+
sendJson(res, 200, { enabled: readSettings(ctx, config).copySidEnabled })
|
|
586
605
|
return
|
|
587
606
|
}
|
|
588
607
|
if (!rejectMethod(req, res, 'POST')) return
|
|
@@ -592,10 +611,8 @@ export function apply(ctx, config) {
|
|
|
592
611
|
} catch {
|
|
593
612
|
throw new Error(MESSAGES.badJsonBody)
|
|
594
613
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
await settings.update(NAMESPACE, { copySidEnabled: Boolean(body && body.enabled) })
|
|
598
|
-
sendJson(res, 200, { enabled: readSettings(ctx).copySidEnabled })
|
|
614
|
+
await persistToggle(ctx, { copySidEnabled: Boolean(body && body.enabled) })
|
|
615
|
+
sendJson(res, 200, { enabled: readSettings(ctx, config).copySidEnabled })
|
|
599
616
|
} catch (error) {
|
|
600
617
|
respondError(ctx, res, error)
|
|
601
618
|
}
|
|
@@ -609,6 +626,14 @@ export function apply(ctx, config) {
|
|
|
609
626
|
}
|
|
610
627
|
|
|
611
628
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
629
|
+
// 方法面守卫:settings 服务缺 register 面(0.1.7 已移除)即走新形态分支;
|
|
630
|
+
// 0.1.7 关闭原生自动设置页,设置面由本包自带分区承担(maintain 同构)
|
|
631
|
+
if (typeof settingsCtx.settings.register !== 'function') {
|
|
632
|
+
if (typeof settingsCtx.settings?.configure === 'function') {
|
|
633
|
+
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber))
|
|
634
|
+
}
|
|
635
|
+
return
|
|
636
|
+
}
|
|
612
637
|
settingsCtx.settings.register(NAMESPACE, SETTINGS_SCHEMA, { base: config })
|
|
613
638
|
})
|
|
614
639
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const CLIENT_SRC = readFileSync(fileURLToPath(new URL('../src/client.js', import.meta.url)), 'utf8')
|
|
7
|
+
|
|
8
|
+
// 范围标签契约:默认单胶囊只显当前范围,点击展开 pill 组,选择后收起;←/→ 键盘切换走同一落点
|
|
9
|
+
test('源码契约:范围标签默认单胶囊,点击展开 pill 组选择', () => {
|
|
10
|
+
assert.ok(CLIENT_SRC.includes('const [scopeOpen, setScopeOpen] = useState(false)'), '展开态应默认收起')
|
|
11
|
+
assert.ok(CLIENT_SRC.includes('HISTORY_SCOPE_LABELS[scopeIndex]'), '默认应单胶囊显示当前范围')
|
|
12
|
+
assert.ok(CLIENT_SRC.includes('HISTORY_SCOPE_LABELS.map('), '展开后应循环渲染 pill 组')
|
|
13
|
+
assert.ok(CLIENT_SRC.includes('onClick: () => setScopeOpen(true)'), '单胶囊点击应展开')
|
|
14
|
+
assert.ok(CLIENT_SRC.includes('onClick: () => (idx === scopeIndex ? setScopeOpen(false) : selectScope(idx))'), 'pill 点击应直达对应范围,当前项点击收起')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('源码契约:selectScope 为键盘与点击共用的唯一切换落点,切换即收起', () => {
|
|
18
|
+
assert.ok(CLIENT_SRC.includes('function selectScope('), '应有 selectScope 直达切换函数')
|
|
19
|
+
// 键盘相对切换委托 selectScope,消除重复切换链
|
|
20
|
+
assert.ok(/function switchScope\(\s*delta\s*\)\s*\{[\s\S]{0,200}selectScope\(next\)/.test(CLIENT_SRC),
|
|
21
|
+
'switchScope 应委托 selectScope')
|
|
22
|
+
// 同范围重复点击不重拉
|
|
23
|
+
const body = CLIENT_SRC.match(/function selectScope\([^)]*\)\s*\{[\s\S]*?\n \}/)
|
|
24
|
+
assert.ok(body && body[0].includes('viewRef.current.scopeIndex'), 'selectScope 应以当前范围守卫幂等')
|
|
25
|
+
assert.ok(body && body[0].includes('setScopeOpen(false)'), '切换后应收起展开态')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('源码契约:重开浮层展开态复位,设置悬停说明同步展开语义', () => {
|
|
29
|
+
// openPopup 内 setScopeOpen(false):浮层重开不残留展开态
|
|
30
|
+
assert.ok(/function openPopup\(\)\s*\{[\s\S]{0,300}setScopeOpen\(false\)/.test(CLIENT_SRC),
|
|
31
|
+
'浮层重开应复位展开态')
|
|
32
|
+
assert.ok(CLIENT_SRC.includes('点击顶栏范围标签展开选择'), '历史说明缺范围标签展开选择语义')
|
|
33
|
+
assert.ok(CLIENT_SRC.includes('←/→ 切换范围'), '历史说明应保留键盘切换语义')
|
|
34
|
+
})
|
|
@@ -131,4 +131,18 @@ test('运行时:缺 remote.session 时仍完成全部插槽注册', () => {
|
|
|
131
131
|
plugin.apply(ctx)
|
|
132
132
|
const names = slots.map(({ definition }) => definition.name)
|
|
133
133
|
assert.ok(names.includes('conversation.input.dock'), '输入框 dock 是 fork/历史/撤回三入口的载体')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// 双代插件卡:settings.plugin.item(0.1.2-0.1.5)与 plugins.bundle.config(0.1.7-rc.1+)
|
|
137
|
+
// 并存注册,各自宿主只消费其一;两代定义都必须在 apply 后注册在案
|
|
138
|
+
test('运行时:插件卡双代槽位并存注册且组件可渲染', () => {
|
|
139
|
+
const { plugin, slots, ctx } = loadPlugin()
|
|
140
|
+
plugin.apply(ctx)
|
|
141
|
+
const legacy = slots.find(({ definition }) => definition.name === 'settings.plugin.item')
|
|
142
|
+
const modern = slots.find(({ definition }) => definition.name === 'plugins.bundle.config')
|
|
143
|
+
assert.ok(legacy, '缺 settings.plugin.item 注册(0.1.2-0.1.5 设置卡)')
|
|
144
|
+
assert.equal(legacy.definition.key, 'context', '旧代 key 须配对 settings namespace')
|
|
145
|
+
assert.ok(modern, '缺 plugins.bundle.config 注册(0.1.7-rc.1+ 插件管理页卡)')
|
|
146
|
+
assert.equal(modern.definition.key, '@mzzsfy/dsh-context-manager', '新代 key 须为完整包名')
|
|
147
|
+
assert.equal(typeof modern.component, 'function')
|
|
134
148
|
})
|
package/test/index.test.mjs
CHANGED
|
@@ -54,11 +54,16 @@ function makeCtx({
|
|
|
54
54
|
settingsValue,
|
|
55
55
|
settingsGetError,
|
|
56
56
|
readSessions,
|
|
57
|
+
configRef,
|
|
58
|
+
configEditor,
|
|
59
|
+
settingsService: settingsServiceOverride,
|
|
57
60
|
}) {
|
|
58
61
|
const routes = []
|
|
59
62
|
const pendingInjects = []
|
|
60
63
|
const settingsState = { value: settingsValue }
|
|
61
|
-
|
|
64
|
+
// settingsServiceOverride(0.1.7 形态桩,如仅 update 的 forms 面)优先;
|
|
65
|
+
// 缺省时构造 legacy 方法面桩(get/register/update 命名空间语义)
|
|
66
|
+
const settingsService = settingsServiceOverride ?? {
|
|
62
67
|
// settingsGetError 注入读取期故障:验证 respondError 的系统级错误收敛分支
|
|
63
68
|
get: () => {
|
|
64
69
|
if (settingsGetError) throw settingsGetError
|
|
@@ -95,7 +100,12 @@ function makeCtx({
|
|
|
95
100
|
if (typeof disposer === 'function') effectDisposers.push(disposer)
|
|
96
101
|
},
|
|
97
102
|
inject: (_deps, fn) => { pendingInjects.push(fn) },
|
|
98
|
-
|
|
103
|
+
// fiber 桩:0.1.7 configure 装配以本条目 fiber 为 owner
|
|
104
|
+
fiber: { entry: { options: { id: 'context-manager', name: '@mzzsfy/dsh-context-manager' } } },
|
|
105
|
+
get: (name) => {
|
|
106
|
+
if (name === 'configEditor') return configEditor
|
|
107
|
+
return { agents, sessionPersistence, settings: settingsService }[name]
|
|
108
|
+
},
|
|
99
109
|
logger: { warns: [], warn(message) { this.warns.push(message) }, infos: [], info(message) { this.infos.push(message) } },
|
|
100
110
|
}
|
|
101
111
|
const ctx = new Proxy(base, {
|
|
@@ -108,14 +118,23 @@ function makeCtx({
|
|
|
108
118
|
return services[prop]
|
|
109
119
|
},
|
|
110
120
|
})
|
|
111
|
-
apply(ctx,
|
|
121
|
+
apply(ctx, configRef)
|
|
112
122
|
return {
|
|
113
123
|
handlers: new Map(routes.map((route) => [route.path, route.handler])),
|
|
114
124
|
logger: base.logger,
|
|
115
125
|
readCounts,
|
|
116
126
|
// 模拟宿主 settings 服务激活:触发 inject 回调(注册)
|
|
117
127
|
activateSettings: () => {
|
|
118
|
-
while (pendingInjects.length > 0)
|
|
128
|
+
while (pendingInjects.length > 0) {
|
|
129
|
+
pendingInjects.shift()({
|
|
130
|
+
settings: settingsService,
|
|
131
|
+
// 注入上下文的 effect 桩:0.1.7 分支经它注册 configure 装配
|
|
132
|
+
effect(fn) {
|
|
133
|
+
const disposer = fn()
|
|
134
|
+
if (typeof disposer === 'function') effectDisposers.push(disposer)
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
}
|
|
119
138
|
},
|
|
120
139
|
}
|
|
121
140
|
}
|
|
@@ -899,3 +918,96 @@ test('输入框历史按钮启停:GET 默认启用,POST 切换经 settings 持
|
|
|
899
918
|
await handlers.get('/api/context/history-button-enabled')(getRequest2('/api/context/history-button-enabled', 'GET'), restored)
|
|
900
919
|
assert.equal(restored.body.enabled, true)
|
|
901
920
|
})
|
|
921
|
+
|
|
922
|
+
// --- 0.1.7 settings 面双形适配(maintain a913c2c 同构) ---
|
|
923
|
+
|
|
924
|
+
test('Config 导出契约:根级 volatile 包装,validate 产整节单 ref,默认值对拍', () => {
|
|
925
|
+
// Given 静态 Config 导出(0.1.7 节表单事实源)
|
|
926
|
+
assert.ok(indexModule.Config, 'Config 导出必须在场')
|
|
927
|
+
// When 空配置校验
|
|
928
|
+
const resolved = indexModule.Config['~standard'].validate({})
|
|
929
|
+
// Then 产整节单 ref(get 协议),布尔开关默认全开(forkAutoResend 除外)
|
|
930
|
+
assert.equal(resolved.issues, undefined)
|
|
931
|
+
const section = resolved.value.get()
|
|
932
|
+
assert.equal(section.historyEnabled, true)
|
|
933
|
+
assert.equal(section.forkAutoResendEnabled, false)
|
|
934
|
+
assert.equal(section.copySidEnabled, true)
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
test('0.1.7 形态:settings 无 get/register 时读回落 config ref 节值', skipMissingDeps, async () => {
|
|
938
|
+
// Given settingsForms 只有 update(0.1.7 真实方法面)+ config ref 携带用户值
|
|
939
|
+
const settingsForms = { update: async () => {} }
|
|
940
|
+
const configRef = { get: () => ({ historyEnabled: false, copySidEnabled: false }) }
|
|
941
|
+
const { handlers } = makeCtx({
|
|
942
|
+
headers: [{ id: 's1', cwd: 'C:/x', createdAt: 0 }],
|
|
943
|
+
agents: new Map(),
|
|
944
|
+
settingsService: settingsForms,
|
|
945
|
+
configRef,
|
|
946
|
+
})
|
|
947
|
+
// When GET 开关路由
|
|
948
|
+
const res = response()
|
|
949
|
+
await handlers.get('/api/context/history-enabled')(getRequest2('/api/context/history-enabled', 'GET'), res)
|
|
950
|
+
// Then 返回 config 节值而非默认
|
|
951
|
+
assert.equal(res.body.enabled, false)
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
test('0.1.7 形态:开关 POST 经 configEditor.edit 落 fiber.entry 并回显新值', skipMissingDeps, async () => {
|
|
955
|
+
// Given settingsForms 只有 update + configEditor 桩(合并写回 configStore)
|
|
956
|
+
const settingsForms = { update: async () => {} }
|
|
957
|
+
const configStore = {}
|
|
958
|
+
const editCalls = []
|
|
959
|
+
const configRef = { get: () => ({ ...configStore }) }
|
|
960
|
+
const { handlers } = makeCtx({
|
|
961
|
+
headers: [{ id: 's1', cwd: 'C:/x', createdAt: 0 }],
|
|
962
|
+
agents: new Map(),
|
|
963
|
+
settingsService: settingsForms,
|
|
964
|
+
configRef,
|
|
965
|
+
configEditor: {
|
|
966
|
+
async edit(entry, change) {
|
|
967
|
+
editCalls.push(entry)
|
|
968
|
+
const next = change({ ...configStore }, {})
|
|
969
|
+
for (const [key, value] of Object.entries(next)) configStore[key] = value
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
})
|
|
973
|
+
// When POST 关闭历史浮层
|
|
974
|
+
const off = await postJson(handlers, '/api/context/history-enabled', { enabled: false })
|
|
975
|
+
// Then edit 定位本条目,合并写回,回显新值
|
|
976
|
+
assert.equal(off.body.enabled, false)
|
|
977
|
+
assert.equal(editCalls.length, 1)
|
|
978
|
+
assert.equal(editCalls[0].options.id, 'context-manager')
|
|
979
|
+
assert.equal(configStore.historyEnabled, false)
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
test('0.1.7 形态:configEditor 缺失即 400 拒写,配置不变', skipMissingDeps, async () => {
|
|
983
|
+
// Given settingsForms 只有 update 且无 configEditor
|
|
984
|
+
const settingsForms = { update: async () => {} }
|
|
985
|
+
const configRef = { get: () => ({}) }
|
|
986
|
+
const { handlers } = makeCtx({
|
|
987
|
+
headers: [{ id: 's1', cwd: 'C:/x', createdAt: 0 }],
|
|
988
|
+
agents: new Map(),
|
|
989
|
+
settingsService: settingsForms,
|
|
990
|
+
configRef,
|
|
991
|
+
})
|
|
992
|
+
// When POST 写开关
|
|
993
|
+
const denied = await postJson(handlers, '/api/context/history-enabled', { enabled: false })
|
|
994
|
+
// Then 400 错误响应
|
|
995
|
+
assert.equal(denied.status, 400)
|
|
996
|
+
assert.ok(denied.body.error)
|
|
997
|
+
})
|
|
998
|
+
|
|
999
|
+
test('0.1.7 形态:settings.configure 在场即关闭原生自动页,register 缺失不抛错', skipMissingDeps, () => {
|
|
1000
|
+
// Given settingsForms 带 configure 无 register
|
|
1001
|
+
const configureCalls = []
|
|
1002
|
+
const settingsForms = { configure: (presentation, owner) => { configureCalls.push({ presentation, owner }); return () => {} } }
|
|
1003
|
+
const { activateSettings } = makeCtx({
|
|
1004
|
+
headers: [{ id: 's1', cwd: 'C:/x', createdAt: 0 }],
|
|
1005
|
+
agents: new Map(),
|
|
1006
|
+
settingsService: settingsForms,
|
|
1007
|
+
})
|
|
1008
|
+
// When 激活 settings 注入
|
|
1009
|
+
activateSettings()
|
|
1010
|
+
// Then 关闭自动页且 owner 为本插件 fiber
|
|
1011
|
+
assert.equal(configureCalls.length, 1)
|
|
1012
|
+
assert.deepEqual(configureCalls[0].presentation, { auto: false })
|
|
1013
|
+
})
|